Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8a6063838 | ||
|
|
bcba2be524 | ||
|
|
f7187d2017 | ||
|
|
d91b7c4406 | ||
|
|
248a7e6d69 | ||
|
|
4f2c3e772a | ||
|
|
95f7146eef | ||
|
|
9d5d19cc6b | ||
|
|
cc38129571 | ||
|
|
3dfd9f237d | ||
|
|
f3fe478dbb | ||
|
|
e10f32ae3d | ||
|
|
4e0924aa0e | ||
|
|
d0d3fd3117 | ||
|
|
4de950d8f4 | ||
|
|
03f2a1f9ba | ||
|
|
2653db90e3 | ||
|
|
ddc8c53eb9 |
@@ -20,7 +20,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from typing import Union
|
from typing import Tuple, Union
|
||||||
|
|
||||||
from pyasic.errors import APIError, APIWarning
|
from pyasic.errors import APIError, APIWarning
|
||||||
|
|
||||||
@@ -128,6 +128,18 @@ class BaseMinerAPI:
|
|||||||
data["multicommand"] = True
|
data["multicommand"] = True
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
async def _handle_multicommand(self, command: str, allow_warning: bool = True):
|
||||||
|
try:
|
||||||
|
data = await self.send_command(command, allow_warning=allow_warning)
|
||||||
|
if not "+" in command:
|
||||||
|
return {command: [data]}
|
||||||
|
return data
|
||||||
|
|
||||||
|
except APIError:
|
||||||
|
if "+" in command:
|
||||||
|
return {command: [{}] for command in command.split("+")}
|
||||||
|
return {command: [{}]}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def commands(self) -> list:
|
def commands(self) -> list:
|
||||||
return self.get_commands()
|
return self.get_commands()
|
||||||
@@ -171,7 +183,11 @@ If you are sure you want to use this command please use API.send_command("{comma
|
|||||||
)
|
)
|
||||||
return return_commands
|
return return_commands
|
||||||
|
|
||||||
async def _send_bytes(self, data: bytes, timeout: int = 100) -> bytes:
|
async def _send_bytes(
|
||||||
|
self,
|
||||||
|
data: bytes,
|
||||||
|
timeout: int = 100,
|
||||||
|
) -> bytes:
|
||||||
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
|
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
|
||||||
try:
|
try:
|
||||||
# get reader and writer streams
|
# get reader and writer streams
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and -
|
# See the License for the specific language governing permissions and -
|
||||||
# limitations under the License. -
|
# limitations under the License. -
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyasic.API import APIError, BaseMinerAPI
|
from pyasic.API import APIError, BaseMinerAPI
|
||||||
@@ -56,19 +56,19 @@ class BFGMinerAPI(BaseMinerAPI):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
async def _x19_multicommand(self, *commands) -> dict:
|
async def _x19_multicommand(self, *commands) -> dict:
|
||||||
data = None
|
tasks = []
|
||||||
try:
|
# send all commands individually
|
||||||
data = {}
|
for cmd in commands:
|
||||||
# send all commands individually
|
tasks.append(
|
||||||
for cmd in commands:
|
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
|
||||||
data[cmd] = []
|
|
||||||
data[cmd].append(await self.send_command(cmd, allow_warning=True))
|
|
||||||
except APIError:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(
|
|
||||||
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
all_data = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
for item in all_data:
|
||||||
|
data.update(item)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def version(self) -> dict:
|
async def version(self) -> dict:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and -
|
# See the License for the specific language governing permissions and -
|
||||||
# limitations under the License. -
|
# limitations under the License. -
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyasic.API import APIError, BaseMinerAPI
|
from pyasic.API import APIError, BaseMinerAPI
|
||||||
@@ -57,21 +58,19 @@ class BMMinerAPI(BaseMinerAPI):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict:
|
async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict:
|
||||||
data = None
|
tasks = []
|
||||||
try:
|
# send all commands individually
|
||||||
data = {}
|
for cmd in commands:
|
||||||
# send all commands individually
|
tasks.append(
|
||||||
for cmd in commands:
|
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
|
||||||
data[cmd] = []
|
|
||||||
data[cmd].append(
|
|
||||||
await self.send_command(cmd, allow_warning=allow_warning)
|
|
||||||
)
|
|
||||||
except APIError:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(
|
|
||||||
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
all_data = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
for item in all_data:
|
||||||
|
data.update(item)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def version(self) -> dict:
|
async def version(self) -> dict:
|
||||||
|
|||||||
@@ -203,27 +203,35 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
# make sure we can actually run each command, otherwise they will fail
|
# make sure we can actually run each command, otherwise they will fail
|
||||||
commands = self._check_commands(*commands)
|
commands = self._check_commands(*commands)
|
||||||
# standard multicommand format is "command1+command2"
|
# standard multicommand format is "command1+command2"
|
||||||
# commands starting with "get_" aren't supported, but we can fake that
|
# commands starting with "get_" and the "status" command aren't supported, but we can fake that
|
||||||
get_commands_data = {}
|
|
||||||
|
tasks = []
|
||||||
|
|
||||||
for command in list(commands):
|
for command in list(commands):
|
||||||
if command.startswith("get_"):
|
if command.startswith("get_") or command == "status":
|
||||||
commands.remove(command)
|
commands.remove(command)
|
||||||
# send seperately and append later
|
# send seperately and append later
|
||||||
try:
|
tasks.append(
|
||||||
get_commands_data[command] = [
|
asyncio.create_task(
|
||||||
await self.send_command(command, allow_warning=allow_warning)
|
self._handle_multicommand(command, allow_warning=allow_warning)
|
||||||
]
|
)
|
||||||
except APIError:
|
)
|
||||||
get_commands_data[command] = [{}]
|
|
||||||
|
|
||||||
command = "+".join(commands)
|
command = "+".join(commands)
|
||||||
try:
|
tasks.append(
|
||||||
main_data = await self.send_command(command, allow_warning=allow_warning)
|
asyncio.create_task(
|
||||||
except APIError:
|
self._handle_multicommand(command, allow_warning=allow_warning)
|
||||||
main_data = {command: [{}] for command in commands}
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
all_data = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
logging.debug(f"{self} - (Multicommand) - Received data")
|
logging.debug(f"{self} - (Multicommand) - Received data")
|
||||||
|
|
||||||
data = dict(**main_data, **get_commands_data)
|
data = {}
|
||||||
|
for item in all_data:
|
||||||
|
data.update(item)
|
||||||
|
|
||||||
data["multicommand"] = True
|
data["multicommand"] = True
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and -
|
# See the License for the specific language governing permissions and -
|
||||||
# limitations under the License. -
|
# limitations under the License. -
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyasic.API import APIError, BaseMinerAPI
|
from pyasic.API import APIError, BaseMinerAPI
|
||||||
@@ -56,19 +56,19 @@ class CGMinerAPI(BaseMinerAPI):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
async def _x19_multicommand(self, *commands) -> dict:
|
async def _x19_multicommand(self, *commands) -> dict:
|
||||||
data = None
|
tasks = []
|
||||||
try:
|
# send all commands individually
|
||||||
data = {}
|
for cmd in commands:
|
||||||
# send all commands individually
|
tasks.append(
|
||||||
for cmd in commands:
|
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
|
||||||
data[cmd] = []
|
|
||||||
data[cmd].append(await self.send_command(cmd, allow_warning=True))
|
|
||||||
except APIError:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(
|
|
||||||
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
all_data = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
for item in all_data:
|
||||||
|
data.update(item)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def version(self) -> dict:
|
async def version(self) -> dict:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import logging
|
|||||||
import time
|
import time
|
||||||
from dataclasses import asdict, dataclass, field, fields
|
from dataclasses import asdict, dataclass, field, fields
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List, Union, Any
|
from typing import Any, List, Union
|
||||||
|
|
||||||
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
|
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
|
||||||
|
|
||||||
@@ -411,8 +411,12 @@ class MinerData:
|
|||||||
field_data.append(f'error_{idx+1}="{item.error_message}"')
|
field_data.append(f'error_{idx+1}="{item.error_message}"')
|
||||||
elif attribute == "hashboards":
|
elif attribute == "hashboards":
|
||||||
for idx, item in enumerate(self[attribute]):
|
for idx, item in enumerate(self[attribute]):
|
||||||
field_data.append(f"hashboard_{idx+1}_hashrate={item.get('hashrate', 0.0)}")
|
field_data.append(
|
||||||
field_data.append(f"hashboard_{idx+1}_temperature={item.get('temp', 0)}")
|
f"hashboard_{idx+1}_hashrate={item.get('hashrate', 0.0)}"
|
||||||
|
)
|
||||||
|
field_data.append(
|
||||||
|
f"hashboard_{idx+1}_temperature={item.get('temp', 0)}"
|
||||||
|
)
|
||||||
field_data.append(
|
field_data.append(
|
||||||
f"hashboard_{idx+1}_chip_temperature={item.get('chip_temp', 0)}"
|
f"hashboard_{idx+1}_chip_temperature={item.get('chip_temp', 0)}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,11 +26,17 @@ from pyasic.miners.backends.cgminer import CGMiner
|
|||||||
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
|
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
|
||||||
|
|
||||||
ANTMINER_MODERN_DATA_LOC = {
|
ANTMINER_MODERN_DATA_LOC = {
|
||||||
"mac": {"cmd": "get_mac", "kwargs": {}},
|
"mac": {
|
||||||
|
"cmd": "get_mac",
|
||||||
|
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
|
||||||
|
},
|
||||||
"model": {"cmd": "get_model", "kwargs": {}},
|
"model": {"cmd": "get_model", "kwargs": {}},
|
||||||
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
|
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
|
||||||
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
|
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
|
||||||
"hostname": {"cmd": "get_hostname", "kwargs": {}},
|
"hostname": {
|
||||||
|
"cmd": "get_hostname",
|
||||||
|
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
|
||||||
|
},
|
||||||
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
|
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
|
||||||
"nominal_hashrate": {
|
"nominal_hashrate": {
|
||||||
"cmd": "get_nominal_hashrate",
|
"cmd": "get_nominal_hashrate",
|
||||||
@@ -42,8 +48,11 @@ ANTMINER_MODERN_DATA_LOC = {
|
|||||||
"wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}},
|
"wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}},
|
||||||
"fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}},
|
"fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}},
|
||||||
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
|
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
|
||||||
"errors": {"cmd": "get_errors", "kwargs": {}},
|
"errors": {"cmd": "get_errors", "kwargs": {"web_summary": {"web": "summary"}}},
|
||||||
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
|
"fault_light": {
|
||||||
|
"cmd": "get_fault_light",
|
||||||
|
"kwargs": {"web_get_blink_status": {"web": "get_blink_status"}},
|
||||||
|
},
|
||||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||||
"is_mining": {
|
"is_mining": {
|
||||||
"cmd": "is_mining",
|
"cmd": "is_mining",
|
||||||
@@ -121,21 +130,31 @@ class AntminerModern(BMMiner):
|
|||||||
await self.send_config(cfg)
|
await self.send_config(cfg)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_hostname(self) -> Union[str, None]:
|
async def get_hostname(self, web_get_system_info: dict = None) -> Union[str, None]:
|
||||||
try:
|
if not web_get_system_info:
|
||||||
data = await self.web.get_system_info()
|
try:
|
||||||
if data:
|
web_get_system_info = await self.web.get_system_info()
|
||||||
return data["hostname"]
|
except APIError:
|
||||||
except KeyError:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_mac(self) -> Union[str, None]:
|
if web_get_system_info:
|
||||||
try:
|
try:
|
||||||
data = await self.web.get_system_info()
|
return web_get_system_info["hostname"]
|
||||||
if data:
|
except KeyError:
|
||||||
return data["macaddr"]
|
pass
|
||||||
except KeyError:
|
|
||||||
pass
|
async def get_mac(self, web_get_system_info: dict = None) -> Union[str, None]:
|
||||||
|
if not web_get_system_info:
|
||||||
|
try:
|
||||||
|
web_get_system_info = await self.web.get_system_info()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if web_get_system_info:
|
||||||
|
try:
|
||||||
|
return web_get_system_info["macaddr"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = await self.web.get_network_info()
|
data = await self.web.get_network_info()
|
||||||
@@ -144,12 +163,17 @@ class AntminerModern(BMMiner):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def get_errors(self) -> List[MinerErrorData]:
|
async def get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
|
||||||
errors = []
|
if not web_summary:
|
||||||
data = await self.web.summary()
|
|
||||||
if data:
|
|
||||||
try:
|
try:
|
||||||
for item in data["SUMMARY"][0]["status"]:
|
web_summary = await self.web.summary()
|
||||||
|
except APIError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
if web_summary:
|
||||||
|
try:
|
||||||
|
for item in web_summary["SUMMARY"][0]["status"]:
|
||||||
try:
|
try:
|
||||||
if not item["status"] == "s":
|
if not item["status"] == "s":
|
||||||
errors.append(X19Error(item["msg"]))
|
errors.append(X19Error(item["msg"]))
|
||||||
@@ -159,15 +183,21 @@ class AntminerModern(BMMiner):
|
|||||||
pass
|
pass
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
async def get_fault_light(self) -> bool:
|
async def get_fault_light(self, web_get_blink_status: dict = None) -> bool:
|
||||||
if self.light:
|
if self.light:
|
||||||
return self.light
|
return self.light
|
||||||
try:
|
|
||||||
data = await self.web.get_blink_status()
|
if not web_get_blink_status:
|
||||||
if data:
|
try:
|
||||||
self.light = data["blink"]
|
web_get_blink_status = await self.web.get_blink_status()
|
||||||
except KeyError:
|
except APIError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if web_get_blink_status:
|
||||||
|
try:
|
||||||
|
self.light = web_get_blink_status["blink"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
return self.light
|
return self.light
|
||||||
|
|
||||||
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
|
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
|
||||||
|
|||||||
@@ -1078,7 +1078,9 @@ class BOSMiner(BaseMiner):
|
|||||||
async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]:
|
async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]:
|
||||||
if not api_devdetails:
|
if not api_devdetails:
|
||||||
try:
|
try:
|
||||||
api_devdetails = await self.api.send_command("devdetails", ignore_errors=True, allow_warning=False)
|
api_devdetails = await self.api.send_command(
|
||||||
|
"devdetails", ignore_errors=True, allow_warning=False
|
||||||
|
)
|
||||||
except APIError:
|
except APIError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ class VNish(BMMiner):
|
|||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def stop_mining(self) -> bool:
|
||||||
|
data = await self.web.stop_mining()
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
return data["success"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def resume_mining(self) -> bool:
|
||||||
|
data = await self.web.resume_mining()
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
return data["success"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
async def reboot(self) -> bool:
|
async def reboot(self) -> bool:
|
||||||
data = await self.web.reboot()
|
data = await self.web.reboot()
|
||||||
if data:
|
if data:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and -
|
# See the License for the specific language governing permissions and -
|
||||||
# limitations under the License. -
|
# limitations under the License. -
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
@@ -33,6 +33,8 @@ class BaseMiner(ABC):
|
|||||||
self.api = None
|
self.api = None
|
||||||
self.web = None
|
self.web = None
|
||||||
|
|
||||||
|
self.ssh_pwd = "root"
|
||||||
|
|
||||||
# static data
|
# static data
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.api_type = None
|
self.api_type = None
|
||||||
@@ -89,6 +91,7 @@ class BaseMiner(ABC):
|
|||||||
|
|
||||||
@pwd.setter
|
@pwd.setter
|
||||||
def pwd(self, val):
|
def pwd(self, val):
|
||||||
|
self.ssh_pwd = val
|
||||||
try:
|
try:
|
||||||
if self.web is not None:
|
if self.web is not None:
|
||||||
self.web.pwd = val
|
self.web.pwd = val
|
||||||
@@ -125,7 +128,7 @@ class BaseMiner(ABC):
|
|||||||
str(self.ip),
|
str(self.ip),
|
||||||
known_hosts=None,
|
known_hosts=None,
|
||||||
username="root",
|
username="root",
|
||||||
password="root",
|
password=self.ssh_pwd,
|
||||||
server_host_key_algs=["ssh-rsa"],
|
server_host_key_algs=["ssh-rsa"],
|
||||||
)
|
)
|
||||||
return conn
|
return conn
|
||||||
@@ -413,59 +416,45 @@ class BaseMiner(ABC):
|
|||||||
async def _get_data(self, allow_warning: bool, data_to_get: list = None) -> dict:
|
async def _get_data(self, allow_warning: bool, data_to_get: list = None) -> dict:
|
||||||
if not data_to_get:
|
if not data_to_get:
|
||||||
# everything
|
# everything
|
||||||
data_to_get = [
|
data_to_get = list(self.data_locations.keys())
|
||||||
"mac",
|
|
||||||
"model",
|
api_multicommand = set()
|
||||||
"api_ver",
|
web_multicommand = set()
|
||||||
"fw_ver",
|
|
||||||
"hostname",
|
|
||||||
"hashrate",
|
|
||||||
"nominal_hashrate",
|
|
||||||
"hashboards",
|
|
||||||
"env_temp",
|
|
||||||
"wattage",
|
|
||||||
"wattage_limit",
|
|
||||||
"fans",
|
|
||||||
"fan_psu",
|
|
||||||
"errors",
|
|
||||||
"fault_light",
|
|
||||||
"pools",
|
|
||||||
"is_mining",
|
|
||||||
"uptime",
|
|
||||||
]
|
|
||||||
api_multicommand = []
|
|
||||||
web_multicommand = []
|
|
||||||
for data_name in data_to_get:
|
for data_name in data_to_get:
|
||||||
try:
|
try:
|
||||||
fn_args = self.data_locations[data_name]["kwargs"]
|
fn_args = self.data_locations[data_name]["kwargs"]
|
||||||
for arg_name in fn_args:
|
for arg_name in fn_args:
|
||||||
if fn_args[arg_name].get("api"):
|
if fn_args[arg_name].get("api"):
|
||||||
api_multicommand.append(fn_args[arg_name]["api"])
|
api_multicommand.add(fn_args[arg_name]["api"])
|
||||||
if fn_args[arg_name].get("web"):
|
if fn_args[arg_name].get("web"):
|
||||||
web_multicommand.append(fn_args[arg_name]["web"])
|
web_multicommand.add(fn_args[arg_name]["web"])
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
logger.error(e, data_name)
|
logger.error(e, data_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
api_multicommand = list(set(api_multicommand))
|
|
||||||
_web_multicommand = web_multicommand
|
|
||||||
for item in web_multicommand:
|
|
||||||
if item not in _web_multicommand:
|
|
||||||
_web_multicommand.append(item)
|
|
||||||
web_multicommand = _web_multicommand
|
|
||||||
if len(api_multicommand) > 0:
|
if len(api_multicommand) > 0:
|
||||||
api_command_data = await self.api.multicommand(
|
api_command_task = asyncio.create_task(
|
||||||
*api_multicommand, allow_warning=allow_warning
|
self.api.multicommand(*api_multicommand, allow_warning=allow_warning)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
api_command_data = {}
|
api_command_task = asyncio.sleep(0)
|
||||||
if len(web_multicommand) > 0:
|
if len(web_multicommand) > 0:
|
||||||
web_command_data = await self.web.multicommand(
|
web_command_task = asyncio.create_task(
|
||||||
*web_multicommand, allow_warning=allow_warning
|
self.web.multicommand(*web_multicommand, allow_warning=allow_warning)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
web_command_task = asyncio.sleep(0)
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
web_command_data = await web_command_task
|
||||||
|
if web_command_data is None:
|
||||||
web_command_data = {}
|
web_command_data = {}
|
||||||
|
|
||||||
|
api_command_data = await api_command_task
|
||||||
|
if api_command_data is None:
|
||||||
|
api_command_data = {}
|
||||||
|
|
||||||
miner_data = {}
|
miner_data = {}
|
||||||
|
|
||||||
for data_name in data_to_get:
|
for data_name in data_to_get:
|
||||||
@@ -492,7 +481,7 @@ class BaseMiner(ABC):
|
|||||||
args_to_send[arg_name] = web_command_data
|
args_to_send[arg_name] = web_command_data
|
||||||
except LookupError:
|
except LookupError:
|
||||||
args_to_send[arg_name] = None
|
args_to_send[arg_name] = None
|
||||||
except LookupError as e:
|
except LookupError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
function = getattr(self, self.data_locations[data_name]["cmd"])
|
function = getattr(self, self.data_locations[data_name]["cmd"])
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
from typing import Callable, List, Optional, Tuple, Union
|
from typing import Callable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import aiohttp
|
import httpx
|
||||||
|
|
||||||
from pyasic.logger import logger
|
from pyasic.logger import logger
|
||||||
from pyasic.miners.antminer import *
|
from pyasic.miners.antminer import *
|
||||||
@@ -319,6 +319,7 @@ MINER_CLASSES = {
|
|||||||
"ANTMINER S19J": BOSMinerS19j,
|
"ANTMINER S19J": BOSMinerS19j,
|
||||||
"ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC,
|
"ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC,
|
||||||
"ANTMINER S19J PRO": BOSMinerS19jPro,
|
"ANTMINER S19J PRO": BOSMinerS19jPro,
|
||||||
|
"ANTMINER S19J PRO NOPIC": BOSMinerS19jPro,
|
||||||
"ANTMINER T19": BOSMinerT19,
|
"ANTMINER T19": BOSMinerT19,
|
||||||
},
|
},
|
||||||
MinerTypes.VNISH: {
|
MinerTypes.VNISH: {
|
||||||
@@ -455,7 +456,7 @@ class MinerFactory:
|
|||||||
|
|
||||||
async def _get_miner_web(self, ip: str):
|
async def _get_miner_web(self, ip: str):
|
||||||
urls = [f"http://{ip}/", f"https://{ip}/"]
|
urls = [f"http://{ip}/", f"https://{ip}/"]
|
||||||
async with aiohttp.ClientSession() as session:
|
async with httpx.AsyncClient(verify=False) as session:
|
||||||
tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls]
|
tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls]
|
||||||
|
|
||||||
text, resp = await concurrent_get_first_result(
|
text, resp = await concurrent_get_first_result(
|
||||||
@@ -466,26 +467,26 @@ class MinerFactory:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _web_ping(
|
async def _web_ping(
|
||||||
session: aiohttp.ClientSession, url: str
|
session: httpx.AsyncClient, url: str
|
||||||
) -> Tuple[Optional[str], Optional[aiohttp.ClientResponse]]:
|
) -> Tuple[Optional[str], Optional[httpx.Response]]:
|
||||||
try:
|
try:
|
||||||
resp = await session.get(url, allow_redirects=False)
|
resp = await session.get(url, follow_redirects=False)
|
||||||
return await resp.text(), resp
|
return resp.text, resp
|
||||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
except (httpx.HTTPError, asyncio.TimeoutError):
|
||||||
pass
|
pass
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_web_type(web_text: str, web_resp: aiohttp.ClientResponse) -> MinerTypes:
|
def _parse_web_type(web_text: str, web_resp: httpx.Response) -> MinerTypes:
|
||||||
if web_resp.status == 401 and 'realm="antMiner' in web_resp.headers.get(
|
if web_resp.status_code == 401 and 'realm="antMiner' in web_resp.headers.get(
|
||||||
"www-authenticate", ""
|
"www-authenticate", ""
|
||||||
):
|
):
|
||||||
return MinerTypes.ANTMINER
|
return MinerTypes.ANTMINER
|
||||||
if web_resp.status == 307 and "https://" in web_resp.headers.get(
|
if web_resp.status_code == 307 and "https://" in web_resp.headers.get(
|
||||||
"location", ""
|
"location", ""
|
||||||
):
|
):
|
||||||
return MinerTypes.WHATSMINER
|
return MinerTypes.WHATSMINER
|
||||||
if "Braiins OS" in web_text or 'href="/cgi-bin/luci"' in web_text:
|
if "Braiins OS" in web_text:
|
||||||
return MinerTypes.BRAIINS_OS
|
return MinerTypes.BRAIINS_OS
|
||||||
if "cloud-box" in web_text:
|
if "cloud-box" in web_text:
|
||||||
return MinerTypes.GOLDSHELL
|
return MinerTypes.GOLDSHELL
|
||||||
@@ -576,26 +577,26 @@ class MinerFactory:
|
|||||||
self,
|
self,
|
||||||
ip: Union[ipaddress.ip_address, str],
|
ip: Union[ipaddress.ip_address, str],
|
||||||
location: str,
|
location: str,
|
||||||
auth: Optional[aiohttp.BasicAuth] = None,
|
auth: Optional[httpx.DigestAuth] = None,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with httpx.AsyncClient(verify=False) as session:
|
||||||
try:
|
try:
|
||||||
data = await session.get(
|
data = await session.get(
|
||||||
f"http://{str(ip)}{location}",
|
f"http://{str(ip)}{location}",
|
||||||
auth=auth,
|
auth=auth,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
except (httpx.HTTPError, asyncio.TimeoutError):
|
||||||
logger.info(f"{ip}: Web command timeout.")
|
logger.info(f"{ip}: Web command timeout.")
|
||||||
return
|
return
|
||||||
if data is None:
|
if data is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
json_data = await data.json()
|
json_data = data.json()
|
||||||
except (aiohttp.ContentTypeError, asyncio.TimeoutError):
|
except (json.JSONDecodeError, asyncio.TimeoutError):
|
||||||
try:
|
try:
|
||||||
return json.loads(await data.text())
|
return json.loads(data.text)
|
||||||
except (json.JSONDecodeError, aiohttp.ClientError):
|
except (json.JSONDecodeError, httpx.HTTPError):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return json_data
|
return json_data
|
||||||
@@ -691,6 +692,28 @@ class MinerFactory:
|
|||||||
return UnknownMiner(str(ip))
|
return UnknownMiner(str(ip))
|
||||||
|
|
||||||
async def get_miner_model_antminer(self, ip: str):
|
async def get_miner_model_antminer(self, ip: str):
|
||||||
|
tasks = [
|
||||||
|
asyncio.create_task(self._get_model_antminer_web(ip)),
|
||||||
|
asyncio.create_task(self._get_model_antminer_sock(ip)),
|
||||||
|
]
|
||||||
|
|
||||||
|
return await concurrent_get_first_result(tasks, lambda x: x is not None)
|
||||||
|
|
||||||
|
async def _get_model_antminer_web(self, ip: str):
|
||||||
|
# last resort, this is slow
|
||||||
|
auth = httpx.DigestAuth("root", "root")
|
||||||
|
web_json_data = await self.send_web_command(
|
||||||
|
ip, "/cgi-bin/get_system_info.cgi", auth=auth
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
miner_model = web_json_data["minertype"]
|
||||||
|
|
||||||
|
return miner_model
|
||||||
|
except (TypeError, LookupError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _get_model_antminer_sock(self, ip: str):
|
||||||
sock_json_data = await self.send_api_command(ip, "version")
|
sock_json_data = await self.send_api_command(ip, "version")
|
||||||
try:
|
try:
|
||||||
miner_model = sock_json_data["VERSION"][0]["Type"]
|
miner_model = sock_json_data["VERSION"][0]["Type"]
|
||||||
@@ -715,19 +738,6 @@ class MinerFactory:
|
|||||||
except (TypeError, LookupError):
|
except (TypeError, LookupError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# last resort, this is slow
|
|
||||||
auth = aiohttp.BasicAuth("root", "root")
|
|
||||||
web_json_data = await self.send_web_command(
|
|
||||||
ip, "/cgi-bin/get_system_info.cgi", auth=auth
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
miner_model = web_json_data["minertype"]
|
|
||||||
|
|
||||||
return miner_model
|
|
||||||
except (TypeError, LookupError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_miner_model_goldshell(self, ip: str):
|
async def get_miner_model_goldshell(self, ip: str):
|
||||||
json_data = await self.send_web_command(ip, "/mcb/status")
|
json_data = await self.send_web_command(ip, "/mcb/status")
|
||||||
|
|
||||||
@@ -760,22 +770,20 @@ class MinerFactory:
|
|||||||
|
|
||||||
async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]:
|
async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with httpx.AsyncClient(verify=False) as session:
|
||||||
auth_req = await session.post(
|
auth_req = await session.post(
|
||||||
f"http://{ip}/api/auth",
|
f"http://{ip}/api/auth",
|
||||||
data={"username": "admin", "password": "admin"},
|
data={"username": "admin", "password": "admin"},
|
||||||
)
|
)
|
||||||
auth = (await auth_req.json())["jwt"]
|
auth = auth_req.json()["jwt"]
|
||||||
|
|
||||||
web_data = await (
|
web_data = (await session.post(
|
||||||
await session.post(
|
|
||||||
f"http://{ip}/api/type",
|
f"http://{ip}/api/type",
|
||||||
headers={"Authorization": "Bearer " + auth},
|
headers={"Authorization": "Bearer " + auth},
|
||||||
data={},
|
data={},
|
||||||
)
|
)).json()
|
||||||
).json()
|
|
||||||
return web_data["type"]
|
return web_data["type"]
|
||||||
except (aiohttp.ClientError, LookupError):
|
except (httpx.HTTPError, LookupError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def get_miner_model_braiins_os(self, ip: str) -> Optional[str]:
|
async def get_miner_model_braiins_os(self, ip: str) -> Optional[str]:
|
||||||
@@ -790,16 +798,16 @@ class MinerFactory:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with httpx.AsyncClient(verify=False) as session:
|
||||||
d = await session.post(
|
d = await session.post(
|
||||||
f"http://{ip}/graphql",
|
f"http://{ip}/graphql",
|
||||||
json={"query": "{bosminer {info{modelName}}}"},
|
json={"query": "{bosminer {info{modelName}}}"},
|
||||||
)
|
)
|
||||||
if d.status == 200:
|
if d.status_code == 200:
|
||||||
json_data = await d.json()
|
json_data = d.json()
|
||||||
miner_model = json_data["data"]["bosminer"]["info"]["modelName"]
|
miner_model = json_data["data"]["bosminer"]["info"]["modelName"]
|
||||||
return miner_model
|
return miner_model
|
||||||
except (aiohttp.ClientError, LookupError):
|
except (httpx.HTTPError, LookupError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def get_miner_model_vnish(self, ip: str) -> Optional[str]:
|
async def get_miner_model_vnish(self, ip: str) -> Optional[str]:
|
||||||
@@ -813,6 +821,9 @@ class MinerFactory:
|
|||||||
if "(88)" in miner_model:
|
if "(88)" in miner_model:
|
||||||
miner_model = miner_model.replace("(88)", "NOPIC")
|
miner_model = miner_model.replace("(88)", "NOPIC")
|
||||||
|
|
||||||
|
if " AML" in miner_model:
|
||||||
|
miner_model = miner_model.replace(" AML", "")
|
||||||
|
|
||||||
return miner_model
|
return miner_model
|
||||||
except (TypeError, LookupError):
|
except (TypeError, LookupError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -24,8 +24,5 @@ class M29V10(WhatsMiner): # noqa - ignore ABC method implementation
|
|||||||
super().__init__(ip, api_ver)
|
super().__init__(ip, api_ver)
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "M29 V10"
|
self.model = "M29 V10"
|
||||||
self.nominal_chips = 0
|
self.nominal_chips = 50
|
||||||
warnings.warn(
|
|
||||||
"Unknown chip count for miner type M29V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
|
|
||||||
)
|
|
||||||
self.fan_count = 2
|
self.fan_count = 2
|
||||||
|
|||||||
@@ -87,10 +87,7 @@ class M50VH60(WhatsMiner): # noqa - ignore ABC method implementation
|
|||||||
super().__init__(ip, api_ver)
|
super().__init__(ip, api_ver)
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.model = "M50 VH60"
|
self.model = "M50 VH60"
|
||||||
self.nominal_chips = 0
|
self.nominal_chips = 84
|
||||||
warnings.warn(
|
|
||||||
"Unknown chip count for miner type M50 VH60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
|
|
||||||
)
|
|
||||||
self.fan_count = 2
|
self.fan_count = 2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from pyasic.errors import APIWarning
|
|||||||
class BaseWebAPI(ABC):
|
class BaseWebAPI(ABC):
|
||||||
def __init__(self, ip: str) -> None:
|
def __init__(self, ip: str) -> None:
|
||||||
# ip address of the miner
|
# ip address of the miner
|
||||||
self.ip = ipaddress.ip_address(ip)
|
self.ip = ip # ipaddress.ip_address(ip)
|
||||||
self.username = "root"
|
self.username = "root"
|
||||||
self.pwd = "root"
|
self.pwd = "root"
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and -
|
# See the License for the specific language governing permissions and -
|
||||||
# limitations under the License. -
|
# limitations under the License. -
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
@@ -56,25 +57,37 @@ class AntminerModernWebAPI(BaseWebAPI):
|
|||||||
async def multicommand(
|
async def multicommand(
|
||||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||||
) -> dict:
|
) -> dict:
|
||||||
data = {k: None for k in commands}
|
|
||||||
data["multicommand"] = True
|
|
||||||
auth = httpx.DigestAuth(self.username, self.pwd)
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
for command in commands:
|
tasks = [
|
||||||
try:
|
asyncio.create_task(self._handle_multicommand(client, command))
|
||||||
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
|
for command in commands
|
||||||
ret = await client.get(url, auth=auth)
|
]
|
||||||
except httpx.HTTPError:
|
all_data = await asyncio.gather(*tasks)
|
||||||
pass
|
|
||||||
else:
|
data = {}
|
||||||
if ret.status_code == 200:
|
for item in all_data:
|
||||||
try:
|
data.update(item)
|
||||||
json_data = ret.json()
|
|
||||||
data[command] = json_data
|
data["multicommand"] = True
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
async def _handle_multicommand(self, client: httpx.AsyncClient, command: str):
|
||||||
|
auth = httpx.DigestAuth(self.username, self.pwd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
|
||||||
|
ret = await client.get(url, auth=auth)
|
||||||
|
except httpx.HTTPError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if ret.status_code == 200:
|
||||||
|
try:
|
||||||
|
json_data = ret.json()
|
||||||
|
return {command: json_data}
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
return {command: {}}
|
||||||
|
|
||||||
async def get_miner_conf(self) -> dict:
|
async def get_miner_conf(self) -> dict:
|
||||||
return await self.send_command("get_miner_conf")
|
return await self.send_command("get_miner_conf")
|
||||||
|
|
||||||
|
|||||||
@@ -116,8 +116,32 @@ class VNishWebAPI(BaseWebAPI):
|
|||||||
async def reboot(self) -> dict:
|
async def reboot(self) -> dict:
|
||||||
return await self.send_command("system/reboot", post=True)
|
return await self.send_command("system/reboot", post=True)
|
||||||
|
|
||||||
|
async def pause_mining(self) -> dict:
|
||||||
|
return await self.send_command("mining/pause", post=True)
|
||||||
|
|
||||||
|
async def resume_mining(self) -> dict:
|
||||||
|
return await self.send_command("mining/resume", post=True)
|
||||||
|
|
||||||
|
async def stop_mining(self) -> dict:
|
||||||
|
return await self.send_command("mining/stop", post=True)
|
||||||
|
|
||||||
|
async def start_mining(self) -> dict:
|
||||||
|
return await self.send_command("mining/start", post=True)
|
||||||
|
|
||||||
async def info(self):
|
async def info(self):
|
||||||
return await self.send_command("info")
|
return await self.send_command("info")
|
||||||
|
|
||||||
async def summary(self):
|
async def summary(self):
|
||||||
return await self.send_command("summary")
|
return await self.send_command("summary")
|
||||||
|
|
||||||
|
async def chips(self):
|
||||||
|
return await self.send_command("chips")
|
||||||
|
|
||||||
|
async def layout(self):
|
||||||
|
return await self.send_command("layout")
|
||||||
|
|
||||||
|
async def status(self):
|
||||||
|
return await self.send_command("status")
|
||||||
|
|
||||||
|
async def settings(self):
|
||||||
|
return await self.send_command("settings")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pyasic"
|
name = "pyasic"
|
||||||
version = "0.37.2"
|
version = "0.37.7"
|
||||||
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
||||||
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
||||||
repository = "https://github.com/UpstreamData/pyasic"
|
repository = "https://github.com/UpstreamData/pyasic"
|
||||||
@@ -14,7 +14,6 @@ httpx = "^0.24.0"
|
|||||||
passlib = "^1.7.4"
|
passlib = "^1.7.4"
|
||||||
pyaml = "^23.5.9"
|
pyaml = "^23.5.9"
|
||||||
toml = "^0.10.2"
|
toml = "^0.10.2"
|
||||||
aiohttp = "^3.8.4"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev]
|
[tool.poetry.group.dev]
|
||||||
optional = true
|
optional = true
|
||||||
|
|||||||
Reference in New Issue
Block a user