Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3484d43510 | ||
|
|
dd7e352391 | ||
|
|
a32b61fe5d | ||
|
|
597a178009 | ||
|
|
409b2527f0 | ||
|
|
58234fcf7f | ||
|
|
1bf863cca8 | ||
|
|
6482d04185 | ||
|
|
3b58b11501 | ||
|
|
7485b8ef77 | ||
|
|
d2bea227db | ||
|
|
1b7afaaf7e | ||
|
|
96898d639c | ||
|
|
eb439f4dcf | ||
|
|
69f4349393 |
@@ -32,6 +32,8 @@ class BaseMinerAPI:
|
||||
# ip address of the miner
|
||||
self.ip = ipaddress.ip_address(ip)
|
||||
|
||||
self.pwd = "admin"
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is BaseMinerAPI:
|
||||
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
|
||||
|
||||
@@ -550,7 +550,7 @@ class MinerConfig:
|
||||
"bitmain-fan-ctrl": False,
|
||||
"bitmain-fan-pwn": "100",
|
||||
"freq-level": "100",
|
||||
"miner-mode": str(self.miner_mode.value),
|
||||
"miner-mode": self.miner_mode.value,
|
||||
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
||||
}
|
||||
|
||||
@@ -560,9 +560,6 @@ class MinerConfig:
|
||||
if self.fan_speed:
|
||||
cfg["bitmain-fan-pwn"] = str(self.fan_speed)
|
||||
|
||||
if self.miner_mode == X19PowerMode.Sleep:
|
||||
cfg["freq-level"] = "0"
|
||||
|
||||
return cfg
|
||||
|
||||
def as_x17(self, user_suffix: str = None) -> dict:
|
||||
|
||||
@@ -20,7 +20,7 @@ import logging
|
||||
import time
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Union
|
||||
from typing import List, Union, Any
|
||||
|
||||
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
|
||||
|
||||
@@ -40,13 +40,28 @@ class HashBoard:
|
||||
"""
|
||||
|
||||
slot: int = 0
|
||||
hashrate: float = 0.0
|
||||
temp: int = -1
|
||||
chip_temp: int = -1
|
||||
chips: int = 0
|
||||
expected_chips: int = 0
|
||||
hashrate: float = None
|
||||
temp: int = None
|
||||
chip_temp: int = None
|
||||
chips: int = None
|
||||
expected_chips: int = None
|
||||
missing: bool = True
|
||||
|
||||
def get(self, __key: str, default: Any = None):
|
||||
try:
|
||||
val = self.__getitem__(__key)
|
||||
if val is None:
|
||||
return default
|
||||
return val
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def __getitem__(self, item: str):
|
||||
try:
|
||||
return getattr(self, item)
|
||||
except AttributeError:
|
||||
raise KeyError(f"{item}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fan:
|
||||
@@ -56,7 +71,22 @@ class Fan:
|
||||
speed: The speed of the fan.
|
||||
"""
|
||||
|
||||
speed: int = -1
|
||||
speed: int = None
|
||||
|
||||
def get(self, __key: str, default: Any = None):
|
||||
try:
|
||||
val = self.__getitem__(__key)
|
||||
if val is None:
|
||||
return default
|
||||
return val
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def __getitem__(self, item: str):
|
||||
try:
|
||||
return getattr(self, item)
|
||||
except AttributeError:
|
||||
raise KeyError(f"{item}")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -102,26 +132,26 @@ class MinerData:
|
||||
|
||||
ip: str
|
||||
datetime: datetime = None
|
||||
uptime: int = 0
|
||||
mac: str = "00:00:00:00:00:00"
|
||||
model: str = "Unknown"
|
||||
make: str = "Unknown"
|
||||
api_ver: str = "Unknown"
|
||||
fw_ver: str = "Unknown"
|
||||
hostname: str = "Unknown"
|
||||
uptime: int = None
|
||||
mac: str = None
|
||||
model: str = None
|
||||
make: str = None
|
||||
api_ver: str = None
|
||||
fw_ver: str = None
|
||||
hostname: str = None
|
||||
hashrate: float = field(init=False)
|
||||
_hashrate: float = 0
|
||||
nominal_hashrate: float = 0
|
||||
_hashrate: float = None
|
||||
nominal_hashrate: float = None
|
||||
hashboards: List[HashBoard] = field(default_factory=list)
|
||||
ideal_hashboards: int = 1
|
||||
ideal_hashboards: int = None
|
||||
temperature_avg: int = field(init=False)
|
||||
env_temp: float = -1.0
|
||||
wattage: int = -1
|
||||
wattage_limit: int = -1
|
||||
env_temp: float = None
|
||||
wattage: int = None
|
||||
wattage_limit: int = None
|
||||
fans: List[Fan] = field(default_factory=list)
|
||||
fan_psu: int = -1
|
||||
fan_psu: int = None
|
||||
total_chips: int = field(init=False)
|
||||
ideal_chips: int = 1
|
||||
ideal_chips: int = None
|
||||
percent_ideal_chips: float = field(init=False)
|
||||
percent_ideal_hashrate: float = field(init=False)
|
||||
percent_ideal_wattage: float = field(init=False)
|
||||
@@ -145,7 +175,16 @@ class MinerData:
|
||||
def __post_init__(self):
|
||||
self.datetime = datetime.now(timezone.utc).astimezone()
|
||||
|
||||
def __getitem__(self, item):
|
||||
def get(self, __key: str, default: Any = None):
|
||||
try:
|
||||
val = self.__getitem__(__key)
|
||||
if val is None:
|
||||
return default
|
||||
return val
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def __getitem__(self, item: str):
|
||||
try:
|
||||
return getattr(self, item)
|
||||
except AttributeError:
|
||||
@@ -197,7 +236,12 @@ class MinerData:
|
||||
@property
|
||||
def hashrate(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) > 0:
|
||||
return round(sum(map(lambda x: x.hashrate, self.hashboards)), 2)
|
||||
hr_data = []
|
||||
for item in self.hashboards:
|
||||
if item.hashrate is not None:
|
||||
hr_data.append(item.hashrate)
|
||||
if len(hr_data) > 0:
|
||||
return sum(hr_data)
|
||||
return self._hashrate
|
||||
|
||||
@hashrate.setter
|
||||
@@ -206,7 +250,14 @@ class MinerData:
|
||||
|
||||
@property
|
||||
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||
return sum([hb.chips for hb in self.hashboards])
|
||||
if len(self.hashboards) > 0:
|
||||
chip_data = []
|
||||
for item in self.hashboards:
|
||||
if item.chips is not None:
|
||||
chip_data.append(item.chips)
|
||||
if len(chip_data) > 0:
|
||||
return sum(chip_data)
|
||||
return None
|
||||
|
||||
@total_chips.setter
|
||||
def total_chips(self, val):
|
||||
@@ -214,6 +265,8 @@ class MinerData:
|
||||
|
||||
@property
|
||||
def nominal(self): # noqa - Skip PyCharm inspection
|
||||
if self.total_chips is None or self.ideal_chips is None:
|
||||
return None
|
||||
return self.ideal_chips == self.total_chips
|
||||
|
||||
@nominal.setter
|
||||
@@ -222,6 +275,8 @@ class MinerData:
|
||||
|
||||
@property
|
||||
def percent_ideal_chips(self): # noqa - Skip PyCharm inspection
|
||||
if self.total_chips is None or self.ideal_chips is None:
|
||||
return None
|
||||
if self.total_chips == 0 or self.ideal_chips == 0:
|
||||
return 0
|
||||
return round((self.total_chips / self.ideal_chips) * 100)
|
||||
@@ -232,6 +287,8 @@ class MinerData:
|
||||
|
||||
@property
|
||||
def percent_ideal_hashrate(self): # noqa - Skip PyCharm inspection
|
||||
if self.hashrate is None or self.nominal_hashrate is None:
|
||||
return None
|
||||
if self.hashrate == 0 or self.nominal_hashrate == 0:
|
||||
return 0
|
||||
return round((self.hashrate / self.nominal_hashrate) * 100)
|
||||
@@ -242,6 +299,8 @@ class MinerData:
|
||||
|
||||
@property
|
||||
def percent_ideal_wattage(self): # noqa - Skip PyCharm inspection
|
||||
if self.wattage_limit is None or self.wattage is None:
|
||||
return None
|
||||
if self.wattage_limit == 0 or self.wattage == 0:
|
||||
return 0
|
||||
return round((self.wattage / self.wattage_limit) * 100)
|
||||
@@ -255,11 +314,11 @@ class MinerData:
|
||||
total_temp = 0
|
||||
temp_count = 0
|
||||
for hb in self.hashboards:
|
||||
if hb.temp and not hb.temp == -1:
|
||||
if hb.temp is not None:
|
||||
total_temp += hb.temp
|
||||
temp_count += 1
|
||||
if not temp_count > 0:
|
||||
return 0
|
||||
return None
|
||||
return round(total_temp / temp_count)
|
||||
|
||||
@temperature_avg.setter
|
||||
@@ -268,7 +327,9 @@ class MinerData:
|
||||
|
||||
@property
|
||||
def efficiency(self): # noqa - Skip PyCharm inspection
|
||||
if self.hashrate == 0 or self.wattage == -1:
|
||||
if self.hashrate is None or self.wattage is None:
|
||||
return None
|
||||
if self.hashrate == 0 or self.wattage == 0:
|
||||
return 0
|
||||
return round(self.wattage / self.hashrate)
|
||||
|
||||
@@ -328,7 +389,7 @@ class MinerData:
|
||||
tags = ["ip", "mac", "model", "hostname"]
|
||||
for attribute in self:
|
||||
if attribute in tags:
|
||||
escaped_data = self[attribute].replace(" ", "\\ ")
|
||||
escaped_data = self.get(attribute, "Unknown").replace(" ", "\\ ")
|
||||
tag_data.append(f"{attribute}={escaped_data}")
|
||||
continue
|
||||
elif str(attribute).startswith("_"):
|
||||
@@ -345,26 +406,24 @@ class MinerData:
|
||||
elif isinstance(self[attribute], float):
|
||||
field_data.append(f"{attribute}={self[attribute]}")
|
||||
continue
|
||||
elif attribute == "fault_light" and not self[attribute]:
|
||||
field_data.append(f"{attribute}=false")
|
||||
continue
|
||||
elif attribute == "errors":
|
||||
for idx, item in enumerate(self[attribute]):
|
||||
field_data.append(f'error_{idx+1}="{item.error_message}"')
|
||||
elif attribute == "hashboards":
|
||||
for idx, item in enumerate(self[attribute]):
|
||||
field_data.append(f"hashboard_{idx+1}_hashrate={item.hashrate}")
|
||||
field_data.append(f"hashboard_{idx+1}_temperature={item.temp}")
|
||||
field_data.append(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(
|
||||
f"hashboard_{idx+1}_chip_temperature={item.chip_temp}"
|
||||
f"hashboard_{idx+1}_chip_temperature={item.get('chip_temp', 0)}"
|
||||
)
|
||||
field_data.append(f"hashboard_{idx+1}_chips={item.chips}")
|
||||
field_data.append(f"hashboard_{idx+1}_chips={item.get('chips', 0)}")
|
||||
field_data.append(
|
||||
f"hashboard_{idx+1}_expected_chips={item.expected_chips}"
|
||||
f"hashboard_{idx+1}_expected_chips={item.get('expected_chips', 0)}"
|
||||
)
|
||||
elif attribute == "fans":
|
||||
for idx, item in enumerate(self[attribute]):
|
||||
field_data.append(f"fan_{idx+1}={item.speed}")
|
||||
if item.speed is not None:
|
||||
field_data.append(f"fan_{idx+1}={item.speed}")
|
||||
|
||||
tags_str = ",".join(tag_data)
|
||||
field_str = ",".join(field_data)
|
||||
|
||||
@@ -149,10 +149,10 @@ class _MinerPhaseBalancer:
|
||||
not self.miners[data_point.ip]["shutdown"]
|
||||
):
|
||||
# cant do anything with it so need to find a semi-accurate power limit
|
||||
if not data_point.wattage_limit == -1:
|
||||
if not data_point.wattage_limit == None:
|
||||
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
|
||||
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
|
||||
elif not data_point.wattage == -1:
|
||||
elif not data_point.wattage == None:
|
||||
self.miners[data_point.ip]["max"] = int(data_point.wattage)
|
||||
self.miners[data_point.ip]["min"] = int(data_point.wattage)
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class HiveonT9(Hiveon, T9):
|
||||
hashrate = 0
|
||||
chips = 0
|
||||
for chipset in board_map[board]:
|
||||
if hashboard.chip_temp == -1:
|
||||
if hashboard.chip_temp == None:
|
||||
try:
|
||||
hashboard.board_temp = api_stats["STATS"][1][f"temp{chipset}"]
|
||||
hashboard.chip_temp = api_stats["STATS"][1][f"temp2_{chipset}"]
|
||||
|
||||
@@ -494,7 +494,7 @@ class BOSMiner(BaseMiner):
|
||||
if graphql_version:
|
||||
try:
|
||||
fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"]
|
||||
except KeyError:
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
if not fw_ver:
|
||||
@@ -523,7 +523,7 @@ class BOSMiner(BaseMiner):
|
||||
try:
|
||||
hostname = graphql_hostname["data"]["bos"]["hostname"]
|
||||
return hostname
|
||||
except KeyError:
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
|
||||
try:
|
||||
@@ -563,7 +563,7 @@ class BOSMiner(BaseMiner):
|
||||
),
|
||||
2,
|
||||
)
|
||||
except (KeyError, IndexError, ValueError):
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# get hr from API
|
||||
@@ -617,7 +617,7 @@ class BOSMiner(BaseMiner):
|
||||
boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][
|
||||
"childSolvers"
|
||||
]
|
||||
except (KeyError, IndexError):
|
||||
except (TypeError, LookupError):
|
||||
boards = None
|
||||
|
||||
if boards:
|
||||
@@ -732,7 +732,7 @@ class BOSMiner(BaseMiner):
|
||||
return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][
|
||||
"power"
|
||||
]["approxConsumptionW"]
|
||||
except (KeyError, TypeError):
|
||||
except (LookupError, TypeError):
|
||||
pass
|
||||
|
||||
if not api_tunerstatus:
|
||||
@@ -765,7 +765,7 @@ class BOSMiner(BaseMiner):
|
||||
return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][
|
||||
"power"
|
||||
]["limitW"]
|
||||
except (KeyError, TypeError):
|
||||
except (LookupError, TypeError):
|
||||
pass
|
||||
|
||||
if not api_tunerstatus:
|
||||
@@ -801,7 +801,7 @@ class BOSMiner(BaseMiner):
|
||||
]
|
||||
)
|
||||
)
|
||||
except KeyError:
|
||||
except (LookupError, TypeError):
|
||||
pass
|
||||
return fans
|
||||
|
||||
@@ -941,7 +941,7 @@ class BOSMiner(BaseMiner):
|
||||
boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][
|
||||
"childSolvers"
|
||||
]
|
||||
except (KeyError, IndexError):
|
||||
except (LookupError, TypeError):
|
||||
boards = None
|
||||
|
||||
if boards:
|
||||
@@ -1034,17 +1034,20 @@ class BOSMiner(BaseMiner):
|
||||
try:
|
||||
self.light = graphql_fault_light["data"]["bos"]["faultLight"]
|
||||
return self.light
|
||||
except (TypeError, KeyError, ValueError, IndexError):
|
||||
except (TypeError, ValueError, LookupError):
|
||||
pass
|
||||
|
||||
# get light via ssh if that fails (10x slower)
|
||||
data = (
|
||||
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
|
||||
).strip()
|
||||
self.light = False
|
||||
if data == "50":
|
||||
self.light = True
|
||||
return self.light
|
||||
try:
|
||||
data = (
|
||||
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
|
||||
).strip()
|
||||
self.light = False
|
||||
if data == "50":
|
||||
self.light = True
|
||||
return self.light
|
||||
except TypeError:
|
||||
return self.light
|
||||
|
||||
async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]:
|
||||
if not api_devs:
|
||||
|
||||
@@ -72,6 +72,52 @@ class BaseMiner(ABC):
|
||||
def __eq__(self, other):
|
||||
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
|
||||
|
||||
@property
|
||||
def pwd(self): # noqa - Skip PyCharm inspection
|
||||
data = []
|
||||
try:
|
||||
if self.web is not None:
|
||||
data.append(f"web={self.web.pwd}")
|
||||
except TypeError:
|
||||
pass
|
||||
try:
|
||||
if self.api is not None:
|
||||
data.append(f"api={self.api.pwd}")
|
||||
except TypeError:
|
||||
pass
|
||||
return ",".join(data)
|
||||
|
||||
@pwd.setter
|
||||
def pwd(self, val):
|
||||
try:
|
||||
if self.web is not None:
|
||||
self.web.pwd = val
|
||||
except TypeError:
|
||||
pass
|
||||
try:
|
||||
if self.api is not None:
|
||||
self.api.pwd = val
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def username(self): # noqa - Skip PyCharm inspection
|
||||
data = []
|
||||
try:
|
||||
if self.web is not None:
|
||||
data.append(f"web={self.web.username}")
|
||||
except TypeError:
|
||||
pass
|
||||
return ",".join(data)
|
||||
|
||||
@username.setter
|
||||
def username(self, val):
|
||||
try:
|
||||
if self.web is not None:
|
||||
self.web.username = val
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
async def _get_ssh_connection(self) -> asyncssh.connect:
|
||||
"""Create a new asyncssh connection"""
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.36.7"
|
||||
version = "0.37.1"
|
||||
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>"]
|
||||
repository = "https://github.com/UpstreamData/pyasic"
|
||||
|
||||
Reference in New Issue
Block a user