Compare commits

...

11 Commits

Author SHA1 Message Date
b-rowan
96bb56ebd1 version: bump version number. 2024-01-14 09:59:06 -07:00
b-rowan
cdd7beccbe Merge pull request #92 from fdeh75/fix-vnish-data-gathering
Fix VNish get_hashrate and get_fans errors
2024-01-14 09:58:16 -07:00
fdeh
1a544851df Fix VNish get_hashrate and get_fans errors
Update vnish.py. Fix data locations according to the method arguments
2024-01-14 19:53:47 +03:00
snyk-bot
84ac991685 fix: docs/requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-JINJA2-6150717
2024-01-11 16:00:03 +00:00
b-rowan
9da7b44177 feature: add vnish config parsing. 2024-01-06 11:31:12 -07:00
UpstreamData
e7f05f7a28 version: bump version number. 2024-01-05 16:22:03 -07:00
UpstreamData
2d229be9fd feature: add board serial numbers to whatsminers. 2024-01-05 16:18:03 -07:00
UpstreamData
de5038e57a feature: add AntminerModern serial numbers to Hashboard data. 2024-01-05 15:57:26 -07:00
UpstreamData
8ad1b3f72a refactor: fix formatting issue. 2024-01-05 08:49:44 -07:00
b-rowan
070fb26dbc version: bump version number. 2024-01-04 20:58:44 -07:00
b-rowan
80d9d7df1d bug: fix possible empty command when getting small data points. 2024-01-04 20:58:15 -07:00
14 changed files with 172 additions and 11 deletions

View File

@@ -1,3 +1,3 @@
jinja2<3.1.0 jinja2<3.1.3
mkdocs mkdocs
mkdocstrings[python] mkdocstrings[python]

View File

@@ -263,6 +263,12 @@ If you are sure you want to use this command please use API.send_command("{comma
else: else:
return False, data["STATUS"][0]["Msg"] return False, data["STATUS"][0]["Msg"]
elif isinstance(data["STATUS"], dict):
# new style X19 command
if data["STATUS"]["STATUS"] not in ["S", "I"]:
return False, data["STATUS"]["Msg"]
return True, None
if data["STATUS"] not in ["S", "I"]: if data["STATUS"] not in ["S", "I"]:
return False, data["Msg"] return False, data["Msg"]
else: else:

View File

@@ -170,6 +170,15 @@ class MinerConfig:
mining_mode=MiningModeConfig.from_epic(web_conf), mining_mode=MiningModeConfig.from_epic(web_conf),
) )
@classmethod
def from_vnish(cls, web_settings: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_vnish(web_settings),
fan_mode=FanModeConfig.from_vnish(web_settings),
temperature=TemperatureConfig.from_vnish(web_settings),
mining_mode=MiningModeConfig.from_vnish(web_settings),
)
def merge(a: dict, b: dict) -> dict: def merge(a: dict, b: dict) -> dict:
result = deepcopy(a) result = deepcopy(a)

View File

@@ -50,6 +50,9 @@ class MinerConfigOption(Enum):
def as_epic(self) -> dict: def as_epic(self) -> dict:
return self.value.as_epic() return self.value.as_epic()
def as_vnish(self) -> dict:
return self.value.as_vnish()
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self.value(*args, **kwargs) return self.value(*args, **kwargs)
@@ -93,3 +96,6 @@ class MinerConfigValue:
def as_epic(self) -> dict: def as_epic(self) -> dict:
return {} return {}
def as_vnish(self) -> dict:
return {}

View File

@@ -22,10 +22,26 @@ from pyasic.config.base import MinerConfigOption, MinerConfigValue
@dataclass @dataclass
class FanModeNormal(MinerConfigValue): class FanModeNormal(MinerConfigValue):
mode: str = field(init=False, default="normal") mode: str = field(init=False, default="normal")
minimum_fans: int = 1
minimum_speed: int = 0
@classmethod @classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "FanModeNormal": def from_dict(cls, dict_conf: Union[dict, None]) -> "FanModeNormal":
return cls() cls_conf = {}
if dict_conf.get("minimum_fans") is not None:
cls_conf["minimum_fans"] = dict_conf["minimum_fans"]
if dict_conf.get("minimum_speed") is not None:
cls_conf["minimum_speed"] = dict_conf["minimum_speed"]
return cls(**cls_conf)
@classmethod
def from_vnish(cls, web_cooling_settings: dict):
cls_conf = {}
if web_cooling_settings.get("fan_min_count") is not None:
cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"]
if web_cooling_settings.get("fan_min_duty") is not None:
cls_conf["minimum_speed"] = web_cooling_settings["fan_min_duty"]
return cls(**cls_conf)
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"} return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"}
@@ -58,6 +74,15 @@ class FanModeManual(MinerConfigValue):
cls_conf["speed"] = toml_fan_conf["speed"] cls_conf["speed"] = toml_fan_conf["speed"]
return cls(**cls_conf) return cls(**cls_conf)
@classmethod
def from_vnish(cls, web_cooling_settings: dict) -> "FanModeManual":
cls_conf = {}
if web_cooling_settings.get("fan_min_count") is not None:
cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"]
if web_cooling_settings["mode"].get("param") is not None:
cls_conf["speed"] = web_cooling_settings["mode"]["param"]
return cls(**cls_conf)
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwn": str(self.speed)} return {"bitmain-fan-ctrl": True, "bitmain-fan-pwn": str(self.speed)}
@@ -143,3 +168,17 @@ class FanModeConfig(MinerConfigOption):
return cls.manual() return cls.manual()
elif mode == "disabled": elif mode == "disabled":
return cls.immersion() return cls.immersion()
@classmethod
def from_vnish(cls, web_settings: dict):
try:
mode = web_settings["miner"]["cooling"]["mode"]["name"]
except LookupError:
return cls.default()
if mode == "auto":
return cls.normal().from_vnish(web_settings["miner"]["cooling"])
elif mode == "manual":
return cls.manual().from_vnish(web_settings["miner"]["cooling"])
elif mode == "immers":
return cls.immersion()

View File

@@ -145,6 +145,20 @@ class MiningModeManual(MinerConfigValue):
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
return {"miner-mode": "0"} return {"miner-mode": "0"}
@classmethod
def from_vnish(cls, web_overclock_settings: dict) -> "MiningModeManual":
# will raise KeyError if it cant find the settings, values cannot be empty
voltage = web_overclock_settings["globals"]["volt"]
freq = web_overclock_settings["globals"]["freq"]
boards = {
idx: ManualBoardSettings(
freq=board["freq"],
volt=voltage if not board["freq"] == 0 else 0,
)
for idx, board in enumerate(web_overclock_settings["chains"])
}
return cls(global_freq=freq, global_volt=voltage, boards=boards)
class MiningModeConfig(MinerConfigOption): class MiningModeConfig(MinerConfigOption):
normal = MiningModeNormal normal = MiningModeNormal
@@ -234,3 +248,15 @@ class MiningModeConfig(MinerConfigOption):
if autotuning_conf.get("hashrate_target") is not None: if autotuning_conf.get("hashrate_target") is not None:
return cls.hashrate_tuning(autotuning_conf["hashrate_target"]) return cls.hashrate_tuning(autotuning_conf["hashrate_target"])
return cls.hashrate_tuning() return cls.hashrate_tuning()
@classmethod
def from_vnish(cls, web_settings: dict):
try:
mode_settings = web_settings["miner"]["overclock"]
except KeyError:
return cls.default()
if mode_settings["preset"] == "disabled":
return MiningModeManual.from_vnish(mode_settings)
else:
return cls.power_tuning(int(mode_settings["preset"]))

View File

@@ -141,6 +141,14 @@ class Pool(MinerConfigValue):
password=toml_pool_conf["password"], password=toml_pool_conf["password"],
) )
@classmethod
def from_vnish(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"],
user=web_pool["user"],
password=web_pool["pass"],
)
@dataclass @dataclass
class PoolGroup(MinerConfigValue): class PoolGroup(MinerConfigValue):
@@ -275,6 +283,10 @@ class PoolGroup(MinerConfigValue):
) )
return cls() return cls()
@classmethod
def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup":
return cls([Pool.from_vnish(p) for p in web_settings_pools])
@dataclass @dataclass
class PoolConfig(MinerConfigValue): class PoolConfig(MinerConfigValue):
@@ -375,3 +387,10 @@ class PoolConfig(MinerConfigValue):
return cls() return cls()
return cls([PoolGroup.from_bosminer(g) for g in toml_conf["group"]]) return cls([PoolGroup.from_bosminer(g) for g in toml_conf["group"]])
@classmethod
def from_vnish(cls, web_settings: dict) -> "PoolConfig":
try:
return cls([PoolGroup.from_vnish(web_settings["miner"]["pools"])])
except LookupError:
return cls()

View File

@@ -71,3 +71,12 @@ class TemperatureConfig(MinerConfigValue):
target_temp = None target_temp = None
return cls(target=target_temp, hot=hot_temp, danger=dangerous_temp) return cls(target=target_temp, hot=hot_temp, danger=dangerous_temp)
@classmethod
def from_vnish(cls, web_settings: dict):
try:
if web_settings["miner"]["cooling"]["mode"]["name"] == "auto":
return cls(target=web_settings["miner"]["cooling"]["mode"]["param"])
except KeyError:
pass
return cls()

View File

@@ -48,6 +48,7 @@ class HashBoard:
chip_temp: int = None chip_temp: int = None
chips: int = None chips: int = None
expected_chips: int = None expected_chips: int = None
serial_number: str = None
missing: bool = True missing: bool = True
def get(self, __key: str, default: Any = None): def get(self, __key: str, default: Any = None):
@@ -161,9 +162,9 @@ class MinerData:
percent_expected_wattage: float = field(init=False) percent_expected_wattage: float = field(init=False)
nominal: bool = field(init=False) nominal: bool = field(init=False)
config: MinerConfig = None config: MinerConfig = None
errors: List[Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]] = ( errors: List[
field(default_factory=list) Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
) ] = field(default_factory=list)
fault_light: Union[bool, None] = None fault_light: Union[bool, None] = None
efficiency: int = field(init=False) efficiency: int = field(init=False)
is_mining: bool = True is_mining: bool = True

View File

@@ -52,9 +52,7 @@ ANTMINER_MODERN_DATA_LOC = DataLocations(
str(DataOptions.EXPECTED_HASHRATE): DataFunction( str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"get_expected_hashrate", [RPCAPICommand("api_stats", "stats")] "get_expected_hashrate", [RPCAPICommand("api_stats", "stats")]
), ),
str(DataOptions.HASHBOARDS): DataFunction( str(DataOptions.HASHBOARDS): DataFunction("get_hashboards", []),
"get_hashboards", [RPCAPICommand("api_stats", "stats")]
),
str(DataOptions.ENVIRONMENT_TEMP): DataFunction("get_env_temp"), str(DataOptions.ENVIRONMENT_TEMP): DataFunction("get_env_temp"),
str(DataOptions.WATTAGE): DataFunction("get_wattage"), str(DataOptions.WATTAGE): DataFunction("get_wattage"),
str(DataOptions.WATTAGE_LIMIT): DataFunction("get_wattage_limit"), str(DataOptions.WATTAGE_LIMIT): DataFunction("get_wattage_limit"),
@@ -196,6 +194,42 @@ class AntminerModern(BMMiner):
pass pass
return errors return errors
async def get_hashboards(self) -> List[HashBoard]:
hashboards = [
HashBoard(idx, expected_chips=self.expected_chips)
for idx in range(self.expected_hashboards)
]
try:
api_stats = await self.api.send_command("stats", new_api=True)
except APIError:
return hashboards
if api_stats:
try:
for board in api_stats["STATS"][0]["chain"]:
hashboards[board["index"]].hashrate = round(
board["rate_real"] / 1000, 2
)
hashboards[board["index"]].chips = board["asic_num"]
board_temp_data = list(
filter(lambda x: not x == 0, board["temp_pcb"])
)
hashboards[board["index"]].temp = sum(board_temp_data) / len(
board_temp_data
)
chip_temp_data = list(
filter(lambda x: not x == 0, board["temp_chip"])
)
hashboards[board["index"]].chip_temp = sum(chip_temp_data) / len(
chip_temp_data
)
hashboards[board["index"]].serial_number = board["sn"]
hashboards[board["index"]].missing = False
except LookupError:
pass
return hashboards
async def get_fault_light(self, web_get_blink_status: dict = None) -> 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

View File

@@ -446,6 +446,7 @@ class BTMiner(BaseMiner):
float(board["MHS 1m"] / 1000000), 2 float(board["MHS 1m"] / 1000000), 2
) )
hashboards[board["ASC"]].chips = board["Effective Chips"] hashboards[board["ASC"]].chips = board["Effective Chips"]
hashboards[board["ASC"]].serial_number = board["PCB SN"]
hashboards[board["ASC"]].missing = False hashboards[board["ASC"]].missing = False
except (KeyError, IndexError): except (KeyError, IndexError):
pass pass

View File

@@ -16,6 +16,7 @@
from typing import Optional from typing import Optional
from pyasic import MinerConfig
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.logger import logger from pyasic.logger import logger
from pyasic.miners.backends.bmminer import BMMiner from pyasic.miners.backends.bmminer import BMMiner
@@ -44,7 +45,7 @@ VNISH_DATA_LOC = DataLocations(
"get_hostname", [WebAPICommand("web_summary", "summary")] "get_hostname", [WebAPICommand("web_summary", "summary")]
), ),
str(DataOptions.HASHRATE): DataFunction( str(DataOptions.HASHRATE): DataFunction(
"get_hashrate", [WebAPICommand("web_summary", "summary")] "get_hashrate", [RPCAPICommand("api_summary", "summary")]
), ),
str(DataOptions.EXPECTED_HASHRATE): DataFunction( str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"get_expected_hashrate", [RPCAPICommand("api_stats", "stats")] "get_expected_hashrate", [RPCAPICommand("api_stats", "stats")]
@@ -60,7 +61,7 @@ VNISH_DATA_LOC = DataLocations(
"get_wattage_limit", [WebAPICommand("web_settings", "settings")] "get_wattage_limit", [WebAPICommand("web_settings", "settings")]
), ),
str(DataOptions.FANS): DataFunction( str(DataOptions.FANS): DataFunction(
"get_fans", [WebAPICommand("web_summary", "summary")] "get_fans", [RPCAPICommand("api_stats", "stats")]
), ),
str(DataOptions.FAN_PSU): DataFunction("get_fan_psu"), str(DataOptions.FAN_PSU): DataFunction("get_fan_psu"),
str(DataOptions.ERRORS): DataFunction("get_errors"), str(DataOptions.ERRORS): DataFunction("get_errors"),
@@ -219,3 +220,11 @@ class VNish(BMMiner):
async def get_uptime(self, *args, **kwargs) -> Optional[int]: async def get_uptime(self, *args, **kwargs) -> Optional[int]:
return None return None
async def get_config(self) -> MinerConfig:
try:
web_settings = await self.web.settings()
except APIError:
return self.config
self.config = MinerConfig.from_vnish(web_settings)
return self.config

View File

@@ -156,6 +156,8 @@ class BOSMinerGQLAPI:
) -> dict: ) -> dict:
url = f"http://{self.ip}/graphql" url = f"http://{self.ip}/graphql"
query = command query = command
if command is None:
return {}
if command.get("query") is None: if command.get("query") is None:
query = {"query": self.parse_command(command)} query = {"query": self.parse_command(command)}
try: try:

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyasic" name = "pyasic"
version = "0.45.0" version = "0.46.1"
description = "A simplified and standardized interface for Bitcoin ASICs." description = "A simplified and standardized interface for Bitcoin ASICs."
authors = ["UpstreamData <brett@upstreamdata.ca>"] authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic" repository = "https://github.com/UpstreamData/pyasic"