diff --git a/pyasic/config/__init__.py b/pyasic/config/__init__.py index c3ba16d4..ae84f00b 100644 --- a/pyasic/config/__init__.py +++ b/pyasic/config/__init__.py @@ -99,13 +99,13 @@ class MinerConfig: **self.power_scaling.as_bosminer(), } - def as_bos_grpc(self, user_suffix: str = None) -> dict: + def as_boser(self, user_suffix: str = None) -> dict: return { - **self.fan_mode.as_bos_grpc(), - **self.temperature.as_bos_grpc(), - **self.mining_mode.as_bos_grpc(), - **self.pools.as_bos_grpc(user_suffix=user_suffix), - **self.power_scaling.as_bos_grpc(), + **self.fan_mode.as_boser(), + **self.temperature.as_boser(), + **self.mining_mode.as_boser(), + **self.pools.as_boser(user_suffix=user_suffix), + **self.power_scaling.as_boser(), } def as_epic(self, user_suffix: str = None) -> dict: @@ -161,6 +161,16 @@ class MinerConfig: power_scaling=PowerScalingConfig.from_bosminer(toml_conf), ) + @classmethod + def from_boser(cls, grpc_miner_conf: dict) -> "MinerConfig": + return cls( + pools=PoolConfig.from_boser(grpc_miner_conf), + mining_mode=MiningModeConfig.from_boser(grpc_miner_conf), + fan_mode=FanModeConfig.from_boser(grpc_miner_conf), + temperature=TemperatureConfig.from_boser(grpc_miner_conf), + power_scaling=PowerScalingConfig.from_boser(grpc_miner_conf), + ) + @classmethod def from_epic(cls, web_conf: dict) -> "MinerConfig": return cls( diff --git a/pyasic/config/base.py b/pyasic/config/base.py index 3f38adeb..df5946ac 100644 --- a/pyasic/config/base.py +++ b/pyasic/config/base.py @@ -44,8 +44,8 @@ class MinerConfigOption(Enum): def as_bosminer(self) -> dict: return self.value.as_bosminer() - def as_bos_grpc(self) -> dict: - return self.value.as_bos_grpc() + def as_boser(self) -> dict: + return self.value.as_boser() def as_epic(self) -> dict: return self.value.as_epic() @@ -91,7 +91,7 @@ class MinerConfigValue: def as_bosminer(self) -> dict: return {} - def as_bos_grpc(self) -> dict: + def as_boser(self) -> dict: return {} def as_epic(self) -> dict: diff --git a/pyasic/config/fans.py b/pyasic/config/fans.py index 5d93bcd0..04d95434 100644 --- a/pyasic/config/fans.py +++ b/pyasic/config/fans.py @@ -182,3 +182,23 @@ class FanModeConfig(MinerConfigOption): return cls.manual().from_vnish(web_settings["miner"]["cooling"]) elif mode == "immers": return cls.immersion() + + @classmethod + def from_boser(cls, grpc_miner_conf: dict): + try: + temperature_conf = grpc_miner_conf["temperature"] + except LookupError: + return cls.default() + + keys = temperature_conf.keys() + if "auto" in keys: + if "minimumRequiredFans" in keys: + return cls.normal(temperature_conf["minimumRequiredFans"]) + return cls.normal() + if "manual" in keys: + conf = {} + if "fanSpeedRatio" in temperature_conf["manual"].keys(): + conf["speed"] = int(temperature_conf["manual"]["fanSpeedRatio"]) + if "minimumRequiredFans" in keys: + conf["minimum_fans"] = int(temperature_conf["minimumRequiredFans"]) + return cls.manual(**conf) diff --git a/pyasic/config/mining.py b/pyasic/config/mining.py index 1b9a7eb3..b4101a51 100644 --- a/pyasic/config/mining.py +++ b/pyasic/config/mining.py @@ -17,6 +17,16 @@ from dataclasses import dataclass, field from typing import Dict, Union from pyasic.config.base import MinerConfigOption, MinerConfigValue +from pyasic.web.braiins_os.proto.braiins.bos.v1 import ( + HashrateTargetMode, + PerformanceMode, + Power, + PowerTargetMode, + SaveAction, + SetPerformanceModeRequest, + TeraHashrate, + TunerPerformanceMode, +) @dataclass @@ -99,6 +109,20 @@ class MiningModePowerTune(MinerConfigValue): def as_bosminer(self) -> dict: return {"autotuning": {"enabled": True, "psu_power_limit": self.power}} + def as_boser(self) -> dict: + return { + "set_performance_mode": SetPerformanceModeRequest( + save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY, + mode=PerformanceMode( + tuner_mode=TunerPerformanceMode( + power_target=PowerTargetMode( + power_target=Power(watt=self.power) + ) + ) + ), + ), + } + @dataclass class MiningModeHashrateTune(MinerConfigValue): @@ -112,6 +136,22 @@ class MiningModeHashrateTune(MinerConfigValue): def as_am_modern(self) -> dict: return {"miner-mode": "0"} + def as_boser(self) -> dict: + return { + "set_performance_mode": SetPerformanceModeRequest( + save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY, + mode=PerformanceMode( + tuner_mode=TunerPerformanceMode( + hashrate_target=HashrateTargetMode( + hashrate_target=TeraHashrate( + terahash_per_second=self.hashrate + ) + ) + ) + ), + ) + } + @dataclass class ManualBoardSettings(MinerConfigValue): @@ -260,3 +300,33 @@ class MiningModeConfig(MinerConfigOption): return MiningModeManual.from_vnish(mode_settings) else: return cls.power_tuning(int(mode_settings["preset"])) + + @classmethod + def from_boser(cls, grpc_miner_conf: dict): + try: + tuner_conf = grpc_miner_conf["tuner"] + if not tuner_conf.get("enabled", False): + return cls.default() + except LookupError: + return cls.default() + + if tuner_conf.get("tunerMode") is not None: + if tuner_conf["tunerMode"] == 1: + if tuner_conf.get("powerTarget") is not None: + return cls.power_tuning(tuner_conf["powerTarget"]["watt"]) + return cls.power_tuning() + + if tuner_conf["tunerMode"] == 2: + if tuner_conf.get("hashrateTarget") is not None: + return cls.hashrate_tuning( + int(tuner_conf["hashrateTarget"]["terahashPerSecond"]) + ) + return cls.hashrate_tuning() + + if tuner_conf.get("powerTarget") is not None: + return cls.power_tuning(tuner_conf["powerTarget"]["watt"]) + + if tuner_conf.get("hashrateTarget") is not None: + return cls.hashrate_tuning( + int(tuner_conf["hashrateTarget"]["terahashPerSecond"]) + ) diff --git a/pyasic/config/pools.py b/pyasic/config/pools.py index b2d10fb5..82fb7535 100644 --- a/pyasic/config/pools.py +++ b/pyasic/config/pools.py @@ -149,6 +149,14 @@ class Pool(MinerConfigValue): password=web_pool["pass"], ) + @classmethod + def from_boser(cls, grpc_pool: dict) -> "Pool": + return cls( + url=grpc_pool["url"], + user=grpc_pool["user"], + password=grpc_pool["password"], + ) + @dataclass class PoolGroup(MinerConfigValue): @@ -287,6 +295,19 @@ class PoolGroup(MinerConfigValue): def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup": return cls([Pool.from_vnish(p) for p in web_settings_pools]) + @classmethod + def from_boser(cls, grpc_pool_group: dict): + try: + return cls( + pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]], + name=grpc_pool_group["name"], + quota=grpc_pool_group["quota"]["value"] + if grpc_pool_group.get("quota") is not None + else 1, + ) + except LookupError: + return cls() + @dataclass class PoolConfig(MinerConfigValue): @@ -349,7 +370,7 @@ class PoolConfig(MinerConfigValue): } return {"group": [PoolGroup().as_bosminer()]} - def as_bos_grpc(self, user_suffix: str = None) -> dict: + def as_boser(self, user_suffix: str = None) -> dict: return {} @classmethod @@ -394,3 +415,15 @@ class PoolConfig(MinerConfigValue): return cls([PoolGroup.from_vnish(web_settings["miner"]["pools"])]) except LookupError: return cls() + + @classmethod + def from_boser(cls, grpc_miner_conf: dict): + try: + return cls( + groups=[ + PoolGroup.from_boser(group) + for group in grpc_miner_conf["poolGroups"] + ] + ) + except LookupError: + return cls() diff --git a/pyasic/config/power_scaling.py b/pyasic/config/power_scaling.py index 7df2fffe..08c48c44 100644 --- a/pyasic/config/power_scaling.py +++ b/pyasic/config/power_scaling.py @@ -17,7 +17,12 @@ from dataclasses import dataclass, field from typing import Union from pyasic.config.base import MinerConfigOption, MinerConfigValue -from pyasic.web.bosminer.proto.braiins.bos.v1 import DpsPowerTarget, DpsTarget, Hours +from pyasic.web.braiins_os.proto.braiins.bos.v1 import ( + DpsPowerTarget, + DpsTarget, + Power, + SetDpsRequest, +) @dataclass @@ -37,13 +42,8 @@ class PowerScalingShutdownEnabled(MinerConfigValue): return cfg - def as_bos_grpc(self) -> dict: - cfg = {"enable_shutdown ": True} - - if self.duration is not None: - cfg["shutdown_duration"] = Hours(self.duration) - - return cfg + def as_boser(self) -> dict: + return {"enable_shutdown": True, "shutdown_duration": self.duration} @dataclass @@ -57,7 +57,7 @@ class PowerScalingShutdownDisabled(MinerConfigValue): def as_bosminer(self) -> dict: return {"shutdown_enabled": False} - def as_bos_grpc(self) -> dict: + def as_boser(self) -> dict: return {"enable_shutdown ": False} @@ -88,6 +88,19 @@ class PowerScalingShutdown(MinerConfigOption): return cls.disabled() return None + @classmethod + def from_boser(cls, power_scaling_conf: dict): + sd_enabled = power_scaling_conf.get("shutdownEnabled") + if sd_enabled is not None: + if sd_enabled: + try: + return cls.enabled(power_scaling_conf["shutdownDuration"]["hours"]) + except KeyError: + return cls.enabled() + else: + return cls.disabled() + return None + @dataclass class PowerScalingEnabled(MinerConfigValue): @@ -133,20 +146,19 @@ class PowerScalingEnabled(MinerConfigValue): return {"power_scaling": cfg} - def as_bos_grpc(self) -> dict: - cfg = {"enable": True} - target_conf = {} - if self.power_step is not None: - target_conf["power_step"] = self.power_step - if self.minimum_power is not None: - target_conf["min_power_target"] = self.minimum_power - - cfg["target"] = DpsTarget(power_target=DpsPowerTarget(**target_conf)) - - if self.shutdown_enabled is not None: - cfg = {**cfg, **self.shutdown_enabled.as_bos_grpc()} - - return {"dps": cfg} + def as_boser(self) -> dict: + return { + "set_dps": SetDpsRequest( + enable=True, + **self.shutdown_enabled.as_boser(), + target=DpsTarget( + power_target=DpsPowerTarget( + power_step=Power(self.power_step), + min_power_target=Power(self.minimum_power), + ) + ), + ), + } @dataclass @@ -187,3 +199,20 @@ class PowerScalingConfig(MinerConfigOption): return cls.disabled() return cls.default() + + @classmethod + def from_boser(cls, grpc_miner_conf: dict): + try: + dps_conf = grpc_miner_conf["dps"] + if not dps_conf.get("enabled", False): + return cls.disabled() + except LookupError: + return cls.default() + + conf = {"shutdown_enabled": PowerScalingShutdown.from_boser(dps_conf)} + + if dps_conf.get("minPowerTarget") is not None: + conf["minimum_power"] = dps_conf["minPowerTarget"]["watt"] + if dps_conf.get("powerStep") is not None: + conf["power_step"] = dps_conf["powerStep"]["watt"] + return cls.enabled(**conf) diff --git a/pyasic/config/temperature.py b/pyasic/config/temperature.py index 07ec4f60..60b48762 100644 --- a/pyasic/config/temperature.py +++ b/pyasic/config/temperature.py @@ -80,3 +80,34 @@ class TemperatureConfig(MinerConfigValue): except KeyError: pass return cls() + + @classmethod + def from_boser(cls, grpc_miner_conf: dict): + try: + temperature_conf = grpc_miner_conf["temperature"] + except KeyError: + return cls.default() + + root_key = None + for key in ["auto", "manual", "disabled"]: + if key in temperature_conf.keys(): + root_key = key + break + if root_key is None: + return cls.default() + + conf = {} + keys = temperature_conf[root_key].keys() + if "targetTemperature" in keys: + conf["target"] = int( + temperature_conf[root_key]["targetTemperature"]["degreeC"] + ) + if "hotTemperature" in keys: + conf["hot"] = int(temperature_conf[root_key]["hotTemperature"]["degreeC"]) + if "dangerousTemperature" in keys: + conf["danger"] = int( + temperature_conf[root_key]["dangerousTemperature"]["degreeC"] + ) + + return cls(**conf) + return cls.default() diff --git a/pyasic/miners/antminer/bosminer/X17/S17.py b/pyasic/miners/antminer/bosminer/X17/S17.py index 42a515f7..ebd3b83b 100644 --- a/pyasic/miners/antminer/bosminer/X17/S17.py +++ b/pyasic/miners/antminer/bosminer/X17/S17.py @@ -14,21 +14,21 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from pyasic.miners.backends import BOSMiner +from pyasic.miners.backends import BOSer from pyasic.miners.types import S17, S17e, S17Plus, S17Pro -class BOSMinerS17(BOSMiner, S17): +class BOSMinerS17(BOSer, S17): pass -class BOSMinerS17Plus(BOSMiner, S17Plus): +class BOSMinerS17Plus(BOSer, S17Plus): pass -class BOSMinerS17Pro(BOSMiner, S17Pro): +class BOSMinerS17Pro(BOSer, S17Pro): pass -class BOSMinerS17e(BOSMiner, S17e): +class BOSMinerS17e(BOSer, S17e): pass diff --git a/pyasic/miners/antminer/bosminer/X17/T17.py b/pyasic/miners/antminer/bosminer/X17/T17.py index a939ad80..1350ce58 100644 --- a/pyasic/miners/antminer/bosminer/X17/T17.py +++ b/pyasic/miners/antminer/bosminer/X17/T17.py @@ -14,17 +14,17 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from pyasic.miners.backends import BOSMiner +from pyasic.miners.backends import BOSer from pyasic.miners.types import T17, T17e, T17Plus -class BOSMinerT17(BOSMiner, T17): +class BOSMinerT17(BOSer, T17): pass -class BOSMinerT17Plus(BOSMiner, T17Plus): +class BOSMinerT17Plus(BOSer, T17Plus): pass -class BOSMinerT17e(BOSMiner, T17e): +class BOSMinerT17e(BOSer, T17e): pass diff --git a/pyasic/miners/antminer/bosminer/X19/S19.py b/pyasic/miners/antminer/bosminer/X19/S19.py index 98d413bb..10ad1de8 100644 --- a/pyasic/miners/antminer/bosminer/X19/S19.py +++ b/pyasic/miners/antminer/bosminer/X19/S19.py @@ -14,7 +14,7 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from pyasic.miners.backends import BOSMiner +from pyasic.miners.backends import BOSer from pyasic.miners.types import ( S19, S19XP, @@ -30,45 +30,45 @@ from pyasic.miners.types import ( ) -class BOSMinerS19(BOSMiner, S19): +class BOSMinerS19(BOSer, S19): pass -class BOSMinerS19Plus(BOSMiner, S19Plus): +class BOSMinerS19Plus(BOSer, S19Plus): pass -class BOSMinerS19Pro(BOSMiner, S19Pro): +class BOSMinerS19Pro(BOSer, S19Pro): pass -class BOSMinerS19a(BOSMiner, S19a): +class BOSMinerS19a(BOSer, S19a): pass -class BOSMinerS19j(BOSMiner, S19j): +class BOSMinerS19j(BOSer, S19j): pass -class BOSMinerS19jNoPIC(BOSMiner, S19jNoPIC): +class BOSMinerS19jNoPIC(BOSer, S19jNoPIC): pass -class BOSMinerS19jPro(BOSMiner, S19jPro): +class BOSMinerS19jPro(BOSer, S19jPro): pass -class BOSMinerS19kProNoPIC(BOSMiner, S19kProNoPIC): +class BOSMinerS19kProNoPIC(BOSer, S19kProNoPIC): pass -class BOSMinerS19aPro(BOSMiner, S19aPro): +class BOSMinerS19aPro(BOSer, S19aPro): pass -class BOSMinerS19jProPlus(BOSMiner, S19jProPlus): +class BOSMinerS19jProPlus(BOSer, S19jProPlus): pass -class BOSMinerS19XP(BOSMiner, S19XP): +class BOSMinerS19XP(BOSer, S19XP): pass diff --git a/pyasic/miners/antminer/bosminer/X19/T19.py b/pyasic/miners/antminer/bosminer/X19/T19.py index e405ac8a..b57d0401 100644 --- a/pyasic/miners/antminer/bosminer/X19/T19.py +++ b/pyasic/miners/antminer/bosminer/X19/T19.py @@ -14,9 +14,9 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from pyasic.miners.backends import BOSMiner +from pyasic.miners.backends import BOSer from pyasic.miners.types import T19 -class BOSMinerT19(BOSMiner, T19): +class BOSMinerT19(BOSer, T19): pass diff --git a/pyasic/miners/antminer/hiveon/X9/T9.py b/pyasic/miners/antminer/hiveon/X9/T9.py index cd3d7b8a..58e3957d 100644 --- a/pyasic/miners/antminer/hiveon/X9/T9.py +++ b/pyasic/miners/antminer/hiveon/X9/T9.py @@ -30,7 +30,7 @@ class HiveonT9(Hiveon, T9): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_mac(self): + async def get_mac(self): try: mac = ( (await self.send_ssh_command("cat /sys/class/net/eth0/address")) @@ -41,7 +41,6 @@ class HiveonT9(Hiveon, T9): except (TypeError, ValueError, asyncssh.Error, OSError, AttributeError): pass - async def _get_hashboards(self, api_stats: dict = None) -> List[HashBoard]: hashboards = [ HashBoard(slot=board, expected_chips=self.expected_chips) @@ -70,14 +69,14 @@ class HiveonT9(Hiveon, T9): hashboards[board].chip_temp = api_stats["STATS"][1][ f"temp2_{chipset}" ] - except LookupError: + except (KeyError, IndexError): pass else: hashboards[board].missing = False try: hashrate += api_stats["STATS"][1][f"chain_rate{chipset}"] chips += api_stats["STATS"][1][f"chain_acn{chipset}"] - except LookupError: + except (KeyError, IndexError): pass hashboards[board].hashrate = round(hashrate / 1000, 2) hashboards[board].chips = chips @@ -95,7 +94,7 @@ class HiveonT9(Hiveon, T9): boards = api_stats.get("STATS") try: wattage_raw = boards[1]["chain_power"] - except LookupError: + except (KeyError, IndexError): pass else: # parse wattage position out of raw data @@ -120,7 +119,7 @@ class HiveonT9(Hiveon, T9): env_temp = api_stats["STATS"][1][f"temp3_{chipset}"] if not env_temp == 0: env_temp_list.append(int(env_temp)) - except LookupError: + except (KeyError, IndexError): pass if not env_temp_list == []: diff --git a/pyasic/miners/backends/__init__.py b/pyasic/miners/backends/__init__.py index 29caa3c6..0cba2d19 100644 --- a/pyasic/miners/backends/__init__.py +++ b/pyasic/miners/backends/__init__.py @@ -17,7 +17,7 @@ from .antminer import AntminerModern, AntminerOld from .bfgminer import BFGMiner from .bfgminer_goldshell import BFGMinerGoldshell from .bmminer import BMMiner -from .bosminer import BOSMiner +from .braiins_os import BOSer, BOSMiner from .btminer import BTMiner from .cgminer import CGMiner from .cgminer_avalon import CGMinerAvalon diff --git a/pyasic/miners/backends/bosminer_old.py b/pyasic/miners/backends/bosminer_old.py deleted file mode 100644 index 86b99309..00000000 --- a/pyasic/miners/backends/bosminer_old.py +++ /dev/null @@ -1,155 +0,0 @@ -# ------------------------------------------------------------------------------ -# Copyright 2022 Upstream Data Inc - -# - -# Licensed under the Apache License, Version 2.0 (the "License"); - -# you may not use this file except in compliance with the License. - -# You may obtain a copy of the License at - -# - -# http://www.apache.org/licenses/LICENSE-2.0 - -# - -# Unless required by applicable law or agreed to in writing, software - -# distributed under the License is distributed on an "AS IS" BASIS, - -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -# See the License for the specific language governing permissions and - -# limitations under the License. - -# ------------------------------------------------------------------------------ - -import logging -from typing import List, Optional, Tuple - -from pyasic.config import MinerConfig -from pyasic.data import Fan, HashBoard, MinerData -from pyasic.data.error_codes import MinerErrorData -from pyasic.miners.backends import BOSMiner - - -class BOSMinerOld(BOSMiner): - def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: - super().__init__(ip, api_ver) - - async def send_ssh_command(self, cmd: str) -> Optional[str]: - result = None - - try: - conn = await self._get_ssh_connection() - except ConnectionError: - return None - - # open an ssh connection - async with conn: - # 3 retries - for i in range(3): - try: - # run the command and get the result - result = await conn.run(cmd) - result = result.stdout - - except Exception as e: - # if the command fails, log it - logging.warning(f"{self} command {cmd} error: {e}") - - # on the 3rd retry, return None - if i == 3: - return - continue - # return the result, either command output or None - return result - - async def update_to_plus(self): - result = await self.send_ssh_command("opkg update && opkg install bos_plus") - return result - - async def check_light(self) -> bool: - return False - - async def fault_light_on(self) -> bool: - return False - - async def fault_light_off(self) -> bool: - return False - - async def get_config(self) -> None: - return None - - async def reboot(self) -> bool: - return False - - async def restart_backend(self) -> bool: - return False - - async def stop_mining(self) -> bool: - return False - - async def resume_mining(self) -> bool: - return False - - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: - return None - - async def set_power_limit(self, wattage: int) -> bool: - return False - - ################################################## - ### DATA GATHERING FUNCTIONS (get_{some_data}) ### - ################################################## - - async def _get_mac(self, *args, **kwargs) -> Optional[str]: - return None - - async def get_model(self, *args, **kwargs) -> str: - return "S9" - - async def get_version(self, *args, **kwargs) -> Tuple[Optional[str], Optional[str]]: - return None, None - - async def _get_hostname(self, *args, **kwargs) -> Optional[str]: - return None - - async def _get_hashrate(self, *args, **kwargs) -> Optional[float]: - return None - - async def _get_hashboards(self, *args, **kwargs) -> List[HashBoard]: - return [] - - async def _get_env_temp(self, *args, **kwargs) -> Optional[float]: - return None - - async def _get_wattage(self, *args, **kwargs) -> Optional[int]: - return None - - async def _get_wattage_limit(self, *args, **kwargs) -> Optional[int]: - return None - - async def _get_fans( - self, - *args, - **kwargs, - ) -> List[Fan]: - return [Fan(), Fan(), Fan(), Fan()] - - async def _get_fan_psu(self, *args, **kwargs) -> Optional[int]: - return None - - async def _get_api_ver(self, *args, **kwargs) -> Optional[str]: - return None - - async def _get_fw_ver(self, *args, **kwargs) -> Optional[str]: - return None - - async def _get_errors(self, *args, **kwargs) -> List[MinerErrorData]: - return [] - - async def _get_fault_light(self, *args, **kwargs) -> bool: - return False - - async def _get_expected_hashrate(self, *args, **kwargs) -> Optional[float]: - return None - - async def get_data(self, allow_warning: bool = False, **kwargs) -> MinerData: - return MinerData(ip=str(self.ip)) - - async def _is_mining(self, *args, **kwargs) -> Optional[bool]: - return None - - async def _get_uptime(self, *args, **kwargs) -> Optional[int]: - return None diff --git a/pyasic/miners/backends/bosminer.py b/pyasic/miners/backends/braiins_os.py similarity index 57% rename from pyasic/miners/backends/bosminer.py rename to pyasic/miners/backends/braiins_os.py index 84df4a05..28c0a375 100644 --- a/pyasic/miners/backends/bosminer.py +++ b/pyasic/miners/backends/braiins_os.py @@ -16,8 +16,7 @@ import asyncio import logging import time -from collections import namedtuple -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Union import toml @@ -32,50 +31,28 @@ from pyasic.miners.base import ( DataFunction, DataLocations, DataOptions, - GraphQLCommand, + GRPCCommand, RPCAPICommand, WebAPICommand, ) -from pyasic.web.bosminer import BOSMinerWebAPI +from pyasic.web.braiins_os import BOSerWebAPI, BOSMinerWebAPI BOSMINER_DATA_LOC = DataLocations( **{ str(DataOptions.MAC): DataFunction( "_get_mac", - [ - WebAPICommand( - "web_net_conf", "/cgi-bin/luci/admin/network/iface_status/lan" - ) - ], + [WebAPICommand("web_net_conf", "admin/network/iface_status/lan")], ), str(DataOptions.API_VERSION): DataFunction( "_get_api_ver", [RPCAPICommand("api_version", "version")] ), str(DataOptions.FW_VERSION): DataFunction( - "_get_fw_ver", - [ - GraphQLCommand( - "graphql_version", {"bos": {"info": {"version": {"full": None}}}} - ) - ], - ), - str(DataOptions.HOSTNAME): DataFunction( - "_get_hostname", - [GraphQLCommand("graphql_hostname", {"bos": {"hostname": None}})], + "_get_fw_ver", [WebAPICommand("web_bos_info", "bos/info")] ), + str(DataOptions.HOSTNAME): DataFunction("_get_hostname"), str(DataOptions.HASHRATE): DataFunction( "_get_hashrate", - [ - RPCAPICommand("api_summary", "summary"), - GraphQLCommand( - "graphql_hashrate", - { - "bosminer": { - "info": {"workSolver": {"realHashrate": {"mhs1M": None}}} - } - }, - ), - ], + [RPCAPICommand("api_summary", "summary")], ), str(DataOptions.EXPECTED_HASHRATE): DataFunction( "_get_expected_hashrate", [RPCAPICommand("api_devs", "devs")] @@ -86,88 +63,27 @@ BOSMINER_DATA_LOC = DataLocations( RPCAPICommand("api_temps", "temps"), RPCAPICommand("api_devdetails", "devdetails"), RPCAPICommand("api_devs", "devs"), - GraphQLCommand( - "graphql_boards", - { - "bosminer": { - "info": { - "workSolver": { - "childSolvers": { - "name": None, - "realHashrate": {"mhs1M": None}, - "hwDetails": {"chips": None}, - "temperatures": {"degreesC": None}, - } - } - } - } - }, - ), ], ), str(DataOptions.ENVIRONMENT_TEMP): DataFunction("_get_env_temp"), str(DataOptions.WATTAGE): DataFunction( "_get_wattage", - [ - RPCAPICommand("api_tunerstatus", "tunerstatus"), - GraphQLCommand( - "graphql_wattage", - { - "bosminer": { - "info": { - "workSolver": {"power": {"approxConsumptionW": None}} - } - } - }, - ), - ], + [RPCAPICommand("api_tunerstatus", "tunerstatus")], ), str(DataOptions.WATTAGE_LIMIT): DataFunction( "_get_wattage_limit", - [ - RPCAPICommand("api_tunerstatus", "tunerstatus"), - GraphQLCommand( - "graphql_wattage_limit", - {"bosminer": {"info": {"workSolver": {"power": {"limitW": None}}}}}, - ), - ], + [RPCAPICommand("api_tunerstatus", "tunerstatus")], ), str(DataOptions.FANS): DataFunction( "_get_fans", - [ - RPCAPICommand("api_fans", "fans"), - GraphQLCommand( - "graphql_fans", - {"bosminer": {"info": {"fans": {"name": None, "rpm": None}}}}, - ), - ], + [RPCAPICommand("api_fans", "fans")], ), str(DataOptions.FAN_PSU): DataFunction("_get_fan_psu"), str(DataOptions.ERRORS): DataFunction( "_get_errors", - [ - RPCAPICommand("api_tunerstatus", "tunerstatus"), - GraphQLCommand( - "graphql_errors", - { - "bosminer": { - "info": { - "workSolver": { - "childSolvers": { - "name": None, - "tuner": {"statusMessages": None}, - } - } - } - } - }, - ), - ], - ), - str(DataOptions.FAULT_LIGHT): DataFunction( - "_get_fault_light", - [GraphQLCommand("graphql_fault_light", {"bos": {"faultLight": None}})], + [RPCAPICommand("api_tunerstatus", "tunerstatus")], ), + str(DataOptions.FAULT_LIGHT): DataFunction("_get_fault_light"), str(DataOptions.IS_MINING): DataFunction( "_is_mining", [RPCAPICommand("api_devdetails", "devdetails")] ), @@ -180,15 +96,14 @@ BOSMINER_DATA_LOC = DataLocations( class BOSMiner(BaseMiner): - def __init__(self, ip: str, api_ver: str = "0.0.0", boser: bool = None) -> None: + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: super().__init__(ip) # interfaces self.api = BOSMinerAPI(ip, api_ver) - self.web = BOSMinerWebAPI(ip, boser=boser) + self.web = BOSMinerWebAPI(ip) # static data self.api_type = "BOSMiner" - self.fw_str = "BOS" # data gathering locations self.data_locations = BOSMINER_DATA_LOC # autotuning/shutdown support @@ -311,25 +226,12 @@ class BOSMiner(BaseMiner): logging.debug(f"{self}: Sending config.") self.config = config - if self.web.grpc is not None: - try: - await self._send_config_grpc(config, user_suffix) - return - except: - pass - await self._send_config_bosminer(config, user_suffix) - - async def _send_config_grpc(self, config: MinerConfig, user_suffix: str = None): - raise NotImplementedError - mining_mode = config.mining_mode - - async def _send_config_bosminer(self, config: MinerConfig, user_suffix: str = None): toml_conf = toml.dumps( { "format": { "version": "1.2+", "generator": "pyasic", - "raw_model": f"{self.make.replace('Miner', 'miner')} {self.raw_model}", + "model": f"{self.make.replace('Miner', 'miner')} {self.model.replace(' (BOS)', '').replace('j', 'J')}", "timestamp": int(time.time()), }, **config.as_bosminer(user_suffix=user_suffix), @@ -385,16 +287,17 @@ class BOSMiner(BaseMiner): gateway: str, subnet_mask: str = "255.255.255.0", ): - cfg_data_lan = ( - "config interface 'lan'\n\toption type 'bridge'\n\toption ifname 'eth0'\n\toption proto 'static'\n\toption ipaddr '" - + ip - + "'\n\toption netmask '" - + subnet_mask - + "'\n\toption gateway '" - + gateway - + "'\n\toption dns '" - + dns - + "'" + cfg_data_lan = "\n\t".join( + [ + "config interface 'lan'", + "option type 'bridge'", + "option ifname 'eth0'", + "option proto 'static'", + f"option ipaddr '{ip}'", + f"option netmask '{subnet_mask}'", + f"option gateway '{gateway}'", + f"option dns '{dns}'", + ] ) data = await self.send_ssh_command("cat /etc/config/network") @@ -410,7 +313,14 @@ class BOSMiner(BaseMiner): await conn.run("echo '" + config + "' > /etc/config/network") async def set_dhcp(self): - cfg_data_lan = "config interface 'lan'\n\toption type 'bridge'\n\toption ifname 'eth0'\n\toption proto 'dhcp'" + cfg_data_lan = "\n\t".join( + [ + "config interface 'lan'", + "option type 'bridge'", + "option ifname 'eth0'", + "option proto 'dhcp'", + ] + ) data = await self.send_ssh_command("cat /etc/config/network") split_data = data.split("\n\n") @@ -431,17 +341,13 @@ class BOSMiner(BaseMiner): async def _get_mac(self, web_net_conf: Union[dict, list] = None) -> Optional[str]: if not web_net_conf: try: - web_net_conf = await self.web.send_command( - "/cgi-bin/luci/admin/network/iface_status/lan" - ) + web_net_conf = await self.web.luci.get_net_conf() except APIError: pass if isinstance(web_net_conf, dict): - if "/cgi-bin/luci/admin/network/iface_status/lan" in web_net_conf.keys(): - web_net_conf = web_net_conf[ - "/cgi-bin/luci/admin/network/iface_status/lan" - ] + if "admin/network/iface_status/lan" in web_net_conf.keys(): + web_net_conf = web_net_conf["admin/network/iface_status/lan"] if web_net_conf: try: @@ -453,20 +359,6 @@ class BOSMiner(BaseMiner): # if result: # return result.upper().strip() - async def get_model(self) -> Optional[str]: - if self.raw_model is not None: - return self.raw_model + " (BOS)" - return "? (BOS)" - - async def get_version( - self, api_version: dict = None, graphql_version: dict = None - ) -> Tuple[Optional[str], Optional[str]]: - # check if version is cached - miner_version = namedtuple("MinerVersion", "api_ver fw_ver") - api_ver = await self.get_api_ver(api_version) - fw_ver = await self.get_fw_ver(graphql_version) - return miner_version(api_ver, fw_ver) - async def _get_api_ver(self, api_version: dict = None) -> Optional[str]: if not api_version: try: @@ -485,91 +377,38 @@ class BOSMiner(BaseMiner): return self.api_ver - async def _get_fw_ver(self, graphql_version: dict = None) -> Optional[str]: - if not graphql_version: + async def _get_fw_ver(self, web_bos_info: dict) -> Optional[str]: + if web_bos_info is None: try: - graphql_version = await self.web.send_command( - {"bos": {"info": {"version": {"full"}}}} - ) + web_bos_info = await self.web.luci.get_bos_info() except APIError: - pass + return None - fw_ver = None + if isinstance(web_bos_info, dict): + if "bos/info" in web_bos_info.keys(): + web_bos_info = web_bos_info["bos/info"] - if graphql_version: - try: - fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"] - except (KeyError, TypeError): - pass - - if not fw_ver: - # try version data file - fw_ver = await self.send_ssh_command("cat /etc/bos_version") - - # if we get the version data, parse it - if fw_ver is not None: - ver = fw_ver.split("-")[5] + try: + ver = web_bos_info["version"].split("-")[5] if "." in ver: self.fw_ver = ver logging.debug(f"Found version for {self.ip}: {self.fw_ver}") + except (LookupError, AttributeError): + return None return self.fw_ver - async def _get_hostname(self, graphql_hostname: dict = None) -> Union[str, None]: - hostname = None - - if not graphql_hostname: - try: - graphql_hostname = await self.web.send_command({"bos": {"hostname"}}) - except APIError: - pass - - if graphql_hostname: - try: - hostname = graphql_hostname["data"]["bos"]["hostname"] - return hostname - except (TypeError, KeyError): - pass - + async def _get_hostname(self) -> Union[str, None]: try: - async with await self._get_ssh_connection() as conn: - if conn is not None: - data = await conn.run("cat /proc/sys/kernel/hostname") - host = data.stdout.strip() - logging.debug(f"Found hostname for {self.ip}: {host}") - hostname = host - else: - logging.warning(f"Failed to get hostname for miner: {self}") + hostname = ( + await self.send_ssh_command("cat /proc/sys/kernel/hostname") + ).strip() except Exception as e: - logging.warning(f"Failed to get hostname for miner: {self}, {e}") + logging.error(f"BOSMiner get_hostname failed with error: {e}") + return None return hostname - async def _get_hashrate( - self, api_summary: dict = None, graphql_hashrate: dict = None - ) -> Optional[float]: - # get hr from graphql - if not graphql_hashrate: - try: - graphql_hashrate = await self.web.send_command( - {"bosminer": {"info": {"workSolver": {"realHashrate": {"mhs1M"}}}}} - ) - except APIError: - pass - - if graphql_hashrate: - try: - return round( - float( - graphql_hashrate["data"]["bosminer"]["info"]["workSolver"][ - "realHashrate" - ]["mhs1M"] - / 1000000 - ), - 2, - ) - except (LookupError, ValueError, TypeError): - pass - + async def _get_hashrate(self, api_summary: dict = None) -> Optional[float]: # get hr from API if not api_summary: try: @@ -580,7 +419,7 @@ class BOSMiner(BaseMiner): if api_summary: try: return round(float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000), 2) - except (LookupError, ValueError, TypeError): + except (KeyError, IndexError, ValueError, TypeError): pass async def _get_hashboards( @@ -588,70 +427,12 @@ class BOSMiner(BaseMiner): api_temps: dict = None, api_devdetails: dict = None, api_devs: dict = None, - graphql_boards: dict = None, ): hashboards = [ HashBoard(slot=i, expected_chips=self.expected_chips) for i in range(self.expected_hashboards) ] - if not graphql_boards and not (api_devs or api_temps or api_devdetails): - try: - graphql_boards = await self.web.send_command( - { - "bosminer": { - "info": { - "workSolver": { - "childSolvers": { - "name": None, - "realHashrate": {"mhs1M"}, - "hwDetails": {"chips"}, - "temperatures": {"degreesC"}, - } - } - } - } - }, - ) - except APIError: - pass - - if graphql_boards: - try: - boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][ - "childSolvers" - ] - except (TypeError, LookupError): - boards = None - - if boards: - b_names = [int(b["name"]) for b in boards] - offset = 0 - if 3 in b_names: - offset = 1 - elif 6 in b_names or 7 in b_names or 8 in b_names: - offset = 6 - for hb in boards: - _id = int(hb["name"]) - offset - board = hashboards[_id] - - board.hashrate = round(hb["realHashrate"]["mhs1M"] / 1000000, 2) - temps = hb["temperatures"] - try: - if len(temps) > 0: - board.temp = round(hb["temperatures"][0]["degreesC"]) - if len(temps) > 1: - board.chip_temp = round(hb["temperatures"][1]["degreesC"]) - except (LookupError, TypeError, ValueError): - pass - details = hb.get("hwDetails") - if details: - if chips := details["chips"]: - board.chips = chips - board.missing = False - - return hashboards - cmds = [] if not api_temps: cmds.append("temps") @@ -670,7 +451,7 @@ class BOSMiner(BaseMiner): api_temps = None try: api_devdetails = d["devdetails"][0] - except LookupError: + except (KeyError, IndexError): api_devdetails = None try: api_devs = d["devs"][0] @@ -686,7 +467,7 @@ class BOSMiner(BaseMiner): board_temp = round(board["Board"]) hashboards[_id].chip_temp = chip_temp hashboards[_id].temp = board_temp - except (LookupError, ValueError, TypeError): + except (IndexError, KeyError, ValueError, TypeError): pass if api_devdetails: @@ -698,7 +479,7 @@ class BOSMiner(BaseMiner): chips = board["Chips"] hashboards[_id].chips = chips hashboards[_id].missing = False - except LookupError: + except (IndexError, KeyError): pass if api_devs: @@ -709,7 +490,7 @@ class BOSMiner(BaseMiner): _id = board["ID"] - offset hashrate = round(float(board["MHS 1m"] / 1000000), 2) hashboards[_id].hashrate = hashrate - except LookupError: + except (IndexError, KeyError): pass return hashboards @@ -717,28 +498,7 @@ class BOSMiner(BaseMiner): async def _get_env_temp(self) -> Optional[float]: return None - async def _get_wattage( - self, api_tunerstatus: dict = None, graphql_wattage: dict = None - ) -> Optional[int]: - if not graphql_wattage and not api_tunerstatus: - try: - graphql_wattage = await self.web.send_command( - { - "bosminer": { - "info": {"workSolver": {"power": {"approxConsumptionW"}}} - } - } - ) - except APIError: - pass - if graphql_wattage is not None: - try: - return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][ - "power" - ]["approxConsumptionW"] - except (LookupError, TypeError): - pass - + async def _get_wattage(self, api_tunerstatus: dict = None) -> Optional[int]: if not api_tunerstatus: try: api_tunerstatus = await self.api.tunerstatus() @@ -753,25 +513,7 @@ class BOSMiner(BaseMiner): except LookupError: pass - async def _get_wattage_limit( - self, api_tunerstatus: dict = None, graphql_wattage_limit: dict = None - ) -> Optional[int]: - if not graphql_wattage_limit and not api_tunerstatus: - try: - graphql_wattage_limit = await self.web.send_command( - {"bosminer": {"info": {"workSolver": {"power": {"limitW"}}}}} - ) - except APIError: - pass - - if graphql_wattage_limit: - try: - return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][ - "power" - ]["limitW"] - except (LookupError, TypeError): - pass - + async def _get_wattage_limit(self, api_tunerstatus: dict = None) -> Optional[int]: if not api_tunerstatus: try: api_tunerstatus = await self.api.tunerstatus() @@ -784,31 +526,7 @@ class BOSMiner(BaseMiner): except LookupError: pass - async def _get_fans( - self, api_fans: dict = None, graphql_fans: dict = None - ) -> List[Fan]: - if not graphql_fans and not api_fans: - try: - graphql_fans = await self.web.send_command( - {"bosminer": {"info": {"fans": {"name", "rpm"}}}} - ) - except APIError: - pass - if graphql_fans.get("data"): - fans = [] - for n in range(self.expected_fans): - try: - fans.append( - Fan( - speed=graphql_fans["data"]["bosminer"]["info"]["fans"][n][ - "rpm" - ] - ) - ) - except (LookupError, TypeError): - pass - return fans - + async def _get_fans(self, api_fans: dict = None) -> List[Fan]: if not api_fans: try: api_fans = await self.api.fans() @@ -820,7 +538,7 @@ class BOSMiner(BaseMiner): for n in range(self.expected_fans): try: fans.append(Fan(api_fans["FANS"][n]["RPM"])) - except LookupError: + except (IndexError, KeyError): pass return fans return [Fan() for _ in range(self.expected_fans)] @@ -828,56 +546,7 @@ class BOSMiner(BaseMiner): async def _get_fan_psu(self) -> Optional[int]: return None - async def _get_errors( - self, api_tunerstatus: dict = None, graphql_errors: dict = None - ) -> List[MinerErrorData]: - if not graphql_errors and not api_tunerstatus: - try: - graphql_errors = await self.web.send_command( - { - "bosminer": { - "info": { - "workSolver": { - "childSolvers": { - "name": None, - "tuner": {"statusMessages"}, - } - } - } - } - } - ) - except APIError: - pass - - if graphql_errors: - errors = [] - try: - boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][ - "childSolvers" - ] - except (LookupError, TypeError): - boards = None - - if boards: - offset = 6 if int(boards[0]["name"]) in [6, 7, 8] else 0 - for hb in boards: - _id = int(hb["name"]) - offset - tuner = hb["tuner"] - if tuner: - if msg := tuner.get("statusMessages"): - if len(msg) > 0: - if hb["tuner"]["statusMessages"][0] not in [ - "Stable", - "Testing performance profile", - "Tuning individual chips", - ]: - errors.append( - BraiinsOSError( - f"Slot {_id} {hb['tuner']['statusMessages'][0]}" - ) - ) - + async def _get_errors(self, api_tunerstatus: dict = None) -> List[MinerErrorData]: if not api_tunerstatus: try: api_tunerstatus = await self.api.tunerstatus() @@ -904,55 +573,12 @@ class BOSMiner(BaseMiner): _error = _error[0].lower() + _error[1:] errors.append(BraiinsOSError(f"Slot {_id} {_error}")) return errors - except LookupError: + except (KeyError, IndexError): pass - async def _get_fault_light(self, graphql_fault_light: dict = None) -> bool: + async def _get_fault_light(self) -> bool: if self.light: return self.light - - if not graphql_fault_light: - if self.fw_ver: - # fw version has to be greater than 21.09 and not 21.09 - if ( - int(self.fw_ver.split(".")[0]) == 21 - and int(self.fw_ver.split(".")[1]) > 9 - ) or int(self.fw_ver.split(".")[0]) > 21: - try: - graphql_fault_light = await self.web.send_command( - {"bos": {"faultLight"}} - ) - except APIError: - pass - else: - logging.info( - f"FW version {self.fw_ver} is too low for fault light info in graphql." - ) - else: - # worth trying - try: - graphql_fault_light = await self.web.send_command( - {"bos": {"faultLight"}} - ) - except APIError: - logging.debug( - "GraphQL fault light failed, likely due to version being too low (<=21.0.9)" - ) - if not graphql_fault_light: - # also a failure - logging.debug( - "GraphQL fault light failed, likely due to version being too low (<=21.0.9)" - ) - - # get light through GraphQL - if graphql_fault_light: - try: - self.light = graphql_fault_light["data"]["bos"]["faultLight"] - return self.light - except (TypeError, ValueError, LookupError): - pass - - # get light via ssh if that fails (10x slower) try: data = ( await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off") @@ -987,6 +613,427 @@ class BOSMiner(BaseMiner): return round( (sum(hr_list) / len(hr_list)) * self.expected_hashboards, 2 ) + except (IndexError, KeyError): + pass + + async def _is_mining(self, api_devdetails: dict = None) -> Optional[bool]: + if not api_devdetails: + try: + api_devdetails = await self.api.send_command( + "devdetails", ignore_errors=True, allow_warning=False + ) + except APIError: + pass + + if api_devdetails: + try: + return not api_devdetails["STATUS"][0]["Msg"] == "Unavailable" + except LookupError: + pass + + async def _get_uptime(self, api_summary: dict = None) -> Optional[int]: + if not api_summary: + try: + api_summary = await self.api.summary() + except APIError: + pass + + if api_summary: + try: + return int(api_summary["SUMMARY"][0]["Elapsed"]) + except LookupError: + pass + + +BOSER_DATA_LOC = DataLocations( + **{ + str(DataOptions.MAC): DataFunction( + "_get_mac", + [GRPCCommand("grpc_miner_details", "get_miner_details")], + ), + str(DataOptions.API_VERSION): DataFunction( + "_get_api_ver", [GRPCCommand("api_version", "get_api_version")] + ), + str(DataOptions.FW_VERSION): DataFunction( + "_get_fw_ver", + [GRPCCommand("grpc_miner_details", "get_miner_details")], + ), + str(DataOptions.HOSTNAME): DataFunction( + "_get_hostname", + [GRPCCommand("grpc_miner_details", "get_miner_details")], + ), + str(DataOptions.HASHRATE): DataFunction( + "_get_hashrate", + [RPCAPICommand("api_summary", "summary")], + ), + str(DataOptions.EXPECTED_HASHRATE): DataFunction( + "_get_expected_hashrate", + [GRPCCommand("grpc_miner_details", "get_miner_details")], + ), + str(DataOptions.HASHBOARDS): DataFunction( + "_get_hashboards", + [GRPCCommand("grpc_hashboards", "get_hashboards")], + ), + str(DataOptions.ENVIRONMENT_TEMP): DataFunction("_get_env_temp"), + str(DataOptions.WATTAGE): DataFunction( + "_get_wattage", + [GRPCCommand("grpc_miner_stats", "get_miner_stats")], + ), + str(DataOptions.WATTAGE_LIMIT): DataFunction( + "_get_wattage_limit", + [ + GRPCCommand( + "grpc_active_performance_mode", "get_active_performance_mode" + ) + ], + ), + str(DataOptions.FANS): DataFunction( + "_get_fans", + [GRPCCommand("grpc_cooling_state", "get_cooling_state")], + ), + str(DataOptions.FAN_PSU): DataFunction("_get_fan_psu"), + str(DataOptions.ERRORS): DataFunction( + "_get_errors", + [RPCAPICommand("api_tunerstatus", "tunerstatus")], + ), + str(DataOptions.FAULT_LIGHT): DataFunction( + "_get_fault_light", + [GRPCCommand("grpc_locate_device_status", "get_locate_device_status")], + ), + str(DataOptions.IS_MINING): DataFunction( + "_is_mining", [RPCAPICommand("api_devdetails", "devdetails")] + ), + str(DataOptions.UPTIME): DataFunction( + "_get_uptime", [RPCAPICommand("api_summary", "summary")] + ), + str(DataOptions.CONFIG): DataFunction("get_config"), + } +) + + +class BOSer(BaseMiner): + def __init__(self, ip: str, api_ver: str = "0.0.0") -> None: + super().__init__(ip) + # interfaces + self.api = BOSMinerAPI(ip, api_ver) + self.web = BOSerWebAPI(ip) + + # static data + self.api_type = "BOSMiner" + # data gathering locations + self.data_locations = BOSER_DATA_LOC + # autotuning/shutdown support + self.supports_autotuning = True + self.supports_shutdown = True + + # data storage + self.api_ver = api_ver + + async def fault_light_on(self) -> bool: + resp = await self.web.grpc.set_locate_device_status(True) + if resp.get("enabled", False): + return True + return False + + async def fault_light_off(self) -> bool: + resp = await self.web.grpc.set_locate_device_status(False) + if resp == {}: + return True + return False + + async def restart_backend(self) -> bool: + return await self.restart_boser() + + async def restart_boser(self) -> bool: + ret = await self.web.grpc.restart() + return True + + async def stop_mining(self) -> bool: + try: + await self.web.grpc.pause_mining() + except APIError: + return False + return True + + async def resume_mining(self) -> bool: + try: + await self.web.grpc.resume_mining() + except APIError: + return False + return True + + async def reboot(self) -> bool: + ret = await self.web.grpc.reboot() + if ret == {}: + return True + return False + + async def get_config(self) -> MinerConfig: + grpc_conf = await self.web.grpc.get_miner_configuration() + + return MinerConfig.from_boser(grpc_conf) + + async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + raise NotImplementedError + logging.debug(f"{self}: Sending config.") + self.config = config + + async def set_power_limit(self, wattage: int) -> bool: + try: + result = await self.web.grpc.set_power_target(wattage) + except APIError: + return False + + try: + if result["powerTarget"]["watt"] == wattage: + return True + except KeyError: + pass + return False + + ################################################## + ### DATA GATHERING FUNCTIONS (get_{some_data}) ### + ################################################## + + async def _get_mac(self, grpc_miner_details: dict = None) -> Optional[str]: + if not grpc_miner_details: + try: + grpc_miner_details = await self.web.grpc.get_miner_details() + except APIError: + pass + + if grpc_miner_details: + try: + return grpc_miner_details["macAddress"].upper() + except (LookupError, TypeError): + pass + + async def _get_model(self) -> Optional[str]: + if self.model is not None: + return self.model + " (BOS)" + return "? (BOS)" + + async def _get_api_ver(self, api_version: dict = None) -> Optional[str]: + if not api_version: + try: + api_version = await self.api.version() + except APIError: + pass + + # Now get the API version + if api_version: + try: + api_ver = api_version["VERSION"][0]["API"] + except LookupError: + api_ver = None + self.api_ver = api_ver + self.api.api_ver = self.api_ver + + return self.api_ver + + async def _get_fw_ver(self, grpc_miner_details: dict = None) -> Optional[str]: + if not grpc_miner_details: + try: + grpc_miner_details = await self.web.grpc.get_miner_details() + except APIError: + pass + + fw_ver = None + + if grpc_miner_details: + try: + fw_ver = grpc_miner_details["bosVersion"]["current"] + except (KeyError, TypeError): + pass + + # if we get the version data, parse it + if fw_ver is not None: + ver = fw_ver.split("-")[5] + if "." in ver: + self.fw_ver = ver + logging.debug(f"Found version for {self.ip}: {self.fw_ver}") + + return self.fw_ver + + async def _get_hostname(self, grpc_miner_details: dict = None) -> Union[str, None]: + if not grpc_miner_details: + try: + grpc_miner_details = await self.web.grpc.get_miner_details() + except APIError: + pass + + if grpc_miner_details: + try: + return grpc_miner_details["hostname"] + except LookupError: + pass + + async def _get_hashrate(self, api_summary: dict = None) -> Optional[float]: + if not api_summary: + try: + api_summary = await self.api.summary() + except APIError: + pass + + if api_summary: + try: + return round(float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000), 2) + except (KeyError, IndexError, ValueError, TypeError): + pass + + async def _get_expected_hashrate( + self, grpc_miner_details: dict = None + ) -> Optional[float]: + if not grpc_miner_details: + try: + grpc_miner_details = await self.web.grpc.get_miner_details() + except APIError: + pass + + if grpc_miner_details: + try: + return grpc_miner_details["stickerHashrate"]["gigahashPerSecond"] / 1000 + except LookupError: + pass + + async def _get_hashboards(self, grpc_hashboards: dict = None): + hashboards = [ + HashBoard(slot=i, expected_chips=self.expected_chips) + for i in range(self.expected_hashboards) + ] + + if grpc_hashboards is None: + try: + grpc_hashboards = await self.web.grpc.get_hashboards() + except APIError: + pass + + if grpc_hashboards is not None: + for board in grpc_hashboards["hashboards"]: + idx = int(board["id"]) - 1 + if board.get("chipsCount") is not None: + hashboards[idx].chips = board["chipsCount"] + if board.get("boardTemp") is not None: + hashboards[idx].temp = board["boardTemp"]["degreeC"] + if board.get("highestChipTemp") is not None: + hashboards[idx].chip_temp = board["highestChipTemp"]["temperature"][ + "degreeC" + ] + if board.get("stats") is not None: + if not board["stats"]["realHashrate"]["last5S"] == {}: + hashboards[idx].hashrate = round( + board["stats"]["realHashrate"]["last5S"][ + "gigahashPerSecond" + ] + / 1000, + 2, + ) + hashboards[idx].missing = False + + return hashboards + + async def _get_env_temp(self) -> Optional[float]: + return None + + async def _get_wattage(self, grpc_miner_stats: dict = None) -> Optional[int]: + if grpc_miner_stats is None: + try: + grpc_miner_stats = self.web.grpc.get_miner_stats() + except APIError: + pass + + if grpc_miner_stats: + try: + return grpc_miner_stats["powerStats"]["approximatedConsumption"]["watt"] + except KeyError: + pass + + async def _get_wattage_limit( + self, grpc_active_performance_mode: dict = None + ) -> Optional[int]: + if grpc_active_performance_mode is None: + try: + grpc_active_performance_mode = ( + self.web.grpc.get_active_performance_mode() + ) + except APIError: + pass + + if grpc_active_performance_mode: + try: + return grpc_active_performance_mode["tunerMode"]["powerTarget"][ + "powerTarget" + ]["watt"] + except KeyError: + pass + + async def _get_fans(self, grpc_cooling_state: dict = None) -> List[Fan]: + if grpc_cooling_state is None: + try: + grpc_cooling_state = self.web.grpc.get_cooling_state() + except APIError: + pass + + if grpc_cooling_state: + fans = [] + for n in range(self.expected_fans): + try: + fans.append(Fan(grpc_cooling_state["fans"][n]["rpm"])) + except (IndexError, KeyError): + pass + return fans + return [Fan() for _ in range(self.expected_fans)] + + async def _get_fan_psu(self) -> Optional[int]: + return None + + async def _get_errors(self, api_tunerstatus: dict = None) -> List[MinerErrorData]: + if not api_tunerstatus: + try: + api_tunerstatus = await self.api.tunerstatus() + except APIError: + pass + + if api_tunerstatus: + errors = [] + try: + chain_status = api_tunerstatus["TUNERSTATUS"][0]["TunerChainStatus"] + if chain_status and len(chain_status) > 0: + offset = ( + 6 if int(chain_status[0]["HashchainIndex"]) in [6, 7, 8] else 0 + ) + + for board in chain_status: + _id = board["HashchainIndex"] - offset + if board["Status"] not in [ + "Stable", + "Testing performance profile", + "Tuning individual chips", + ]: + _error = board["Status"].split(" {")[0] + _error = _error[0].lower() + _error[1:] + errors.append(BraiinsOSError(f"Slot {_id} {_error}")) + return errors + except LookupError: + pass + + async def _get_fault_light(self, grpc_locate_device_status: dict = None) -> bool: + if self.light is not None: + return self.light + + if not grpc_locate_device_status: + try: + grpc_locate_device_status = ( + await self.web.grpc.get_locate_device_status() + ) + except APIError: + pass + + if grpc_locate_device_status is not None: + if grpc_locate_device_status == {}: + return False + try: + return grpc_locate_device_status["enabled"] except LookupError: pass diff --git a/pyasic/miners/backends/btminer.py b/pyasic/miners/backends/btminer.py index 543c8419..b49aecab 100644 --- a/pyasic/miners/backends/btminer.py +++ b/pyasic/miners/backends/btminer.py @@ -557,7 +557,6 @@ class BTMiner(BaseMiner): errors.append(WhatsminerError(error_code=err)) except (LookupError, ValueError, TypeError): pass - return errors async def _get_expected_hashrate(self, api_summary: dict = None): diff --git a/pyasic/miners/backends/cgminer_avalon.py b/pyasic/miners/backends/cgminer_avalon.py index 4812d0de..84d4f54b 100644 --- a/pyasic/miners/backends/cgminer_avalon.py +++ b/pyasic/miners/backends/cgminer_avalon.py @@ -208,7 +208,7 @@ class CGMinerAvalon(CGMiner): if api_devs: try: return round(float(api_devs["DEVS"][0]["MHS 1m"] / 1000000), 2) - except (LookupError, ValueError, TypeError): + except (KeyError, IndexError, ValueError, TypeError): pass async def _get_hashboards(self, api_stats: dict = None) -> List[HashBoard]: @@ -227,7 +227,7 @@ class CGMinerAvalon(CGMiner): try: unparsed_stats = api_stats["STATS"][0]["MM ID0"] parsed_stats = self.parse_stats(unparsed_stats) - except (LookupError, ValueError, TypeError): + except (IndexError, KeyError, ValueError, TypeError): return hashboards for board in range(self.expected_hashboards): @@ -271,7 +271,7 @@ class CGMinerAvalon(CGMiner): unparsed_stats = api_stats["STATS"][0]["MM ID0"] parsed_stats = self.parse_stats(unparsed_stats) return round(float(parsed_stats["GHSmm"]) / 1000, 2) - except (LookupError, ValueError, TypeError): + except (IndexError, KeyError, ValueError, TypeError): pass async def _get_env_temp(self, api_stats: dict = None) -> Optional[float]: @@ -286,7 +286,7 @@ class CGMinerAvalon(CGMiner): unparsed_stats = api_stats["STATS"][0]["MM ID0"] parsed_stats = self.parse_stats(unparsed_stats) return float(parsed_stats["Temp"]) - except (LookupError, ValueError, TypeError): + except (IndexError, KeyError, ValueError, TypeError): pass async def _get_wattage(self) -> Optional[int]: @@ -304,7 +304,7 @@ class CGMinerAvalon(CGMiner): unparsed_stats = api_stats["STATS"][0]["MM ID0"] parsed_stats = self.parse_stats(unparsed_stats) return int(parsed_stats["MPO"]) - except (LookupError, ValueError, TypeError): + except (IndexError, KeyError, ValueError, TypeError): pass async def _get_fans(self, api_stats: dict = None) -> List[Fan]: @@ -325,7 +325,7 @@ class CGMinerAvalon(CGMiner): for fan in range(self.expected_fans): try: fans_data[fan].speed = int(parsed_stats[f"Fan{fan + 1}"]) - except (LookupError, ValueError, TypeError): + except (IndexError, KeyError, ValueError, TypeError): pass return fans_data @@ -347,7 +347,7 @@ class CGMinerAvalon(CGMiner): parsed_stats = self.parse_stats(unparsed_stats) led = int(parsed_stats["Led"]) return True if led == 1 else False - except (LookupError, ValueError, TypeError): + except (IndexError, KeyError, ValueError, TypeError): pass try: @@ -366,6 +366,3 @@ class CGMinerAvalon(CGMiner): async def _get_uptime(self) -> Optional[int]: return None - - async def get_uptime(self) -> Optional[int]: - return None diff --git a/pyasic/miners/backends/epic.py b/pyasic/miners/backends/epic.py index a4519133..979a65ab 100644 --- a/pyasic/miners/backends/epic.py +++ b/pyasic/miners/backends/epic.py @@ -151,7 +151,7 @@ class ePIC(BaseMiner): try: for network in web_network: mac = web_network[network]["mac_address"] - return mac + return mac except KeyError: pass @@ -188,7 +188,7 @@ class ePIC(BaseMiner): if web_summary: try: hashrate = 0 - if web_summary["HBs"] != None: + if web_summary["HBs"] is not None: for hb in web_summary["HBs"]: hashrate += hb["Hashrate"][0] return round(float(float(hashrate / 1000000)), 2) @@ -207,7 +207,7 @@ class ePIC(BaseMiner): if web_summary: try: hashrate = 0 - if web_summary["HBs"] != None: + if web_summary["HBs"] is not None: for hb in web_summary["HBs"]: if hb["Hashrate"][1] == 0: ideal = 1.0 @@ -266,7 +266,7 @@ class ePIC(BaseMiner): HashBoard(slot=i, expected_chips=self.expected_chips) for i in range(self.expected_hashboards) ] - if web_summary["HBs"] != None: + if web_summary["HBs"] is not None: for hb in web_summary["HBs"]: for hr in web_hashrate: if hr["Index"] == hb["Index"]: @@ -312,7 +312,7 @@ class ePIC(BaseMiner): if web_summary: try: error = web_summary["Status"]["Last Error"] - if error != None: + if error is not None: errors.append(X19Error(str(error))) return errors except KeyError: @@ -328,9 +328,6 @@ class ePIC(BaseMiner): def _get_api_ver(self, *args, **kwargs) -> Optional[str]: pass - def get_config(self) -> MinerConfig: - return self.config - def _get_env_temp(self, *args, **kwargs) -> Optional[float]: pass diff --git a/pyasic/miners/backends/hiveon.py b/pyasic/miners/backends/hiveon.py index 22a6a48a..7f0cf232 100644 --- a/pyasic/miners/backends/hiveon.py +++ b/pyasic/miners/backends/hiveon.py @@ -22,7 +22,7 @@ from pyasic.miners.base import DataFunction, DataLocations, DataOptions, RPCAPIC HIVEON_DATA_LOC = DataLocations( **{ - str(DataOptions.MAC): DataFunction("_get_mac"), + str(DataOptions.MAC): DataFunction("get_mac"), str(DataOptions.API_VERSION): DataFunction( "_get_api_ver", [RPCAPICommand("api_version", "version")] ), @@ -45,7 +45,7 @@ HIVEON_DATA_LOC = DataLocations( str(DataOptions.WATTAGE): DataFunction( "_get_wattage", [RPCAPICommand("api_stats", "stats")] ), - str(DataOptions.WATTAGE_LIMIT): DataFunction("_get_wattage_limit"), + str(DataOptions.WATTAGE_LIMIT): DataFunction("get_wattage_limit"), str(DataOptions.FANS): DataFunction( "_get_fans", [RPCAPICommand("api_stats", "stats")] ), @@ -67,7 +67,6 @@ class Hiveon(BMMiner): self.pwd = "admin" # static data self.api_type = "Hiveon" - self.fw_str = "Hive" # data gathering locations self.data_locations = HIVEON_DATA_LOC diff --git a/pyasic/miners/backends/innosilicon.py b/pyasic/miners/backends/innosilicon.py index 9e85525a..67a8e77d 100644 --- a/pyasic/miners/backends/innosilicon.py +++ b/pyasic/miners/backends/innosilicon.py @@ -340,7 +340,7 @@ class Innosilicon(CGMiner): else: web_get_all = web_get_all["all"] - fans = [Fan() for _ in range(self.fan_count)] + fans = [Fan() for _ in range(self.expected_fans)] if web_get_all: try: spd = web_get_all["fansSpeed"] @@ -348,7 +348,7 @@ class Innosilicon(CGMiner): pass else: round((int(spd) * 6000) / 100) - for i in range(self.fan_count): + for i in range(self.expected_fans): fans[i].speed = spd return fans diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index aaf86998..c0f61134 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -142,7 +142,7 @@ class BaseMiner(ABC): @property def model(self): - model_data = [self.raw_model] + model_data = [self.raw_model if self.raw_model is not None else "Unknown"] if self.fw_str is not None: model_data.append(f"({self.fw_str})") return " ".join(model_data) diff --git a/pyasic/miners/miner_factory.py b/pyasic/miners/miner_factory.py index 0f4fca15..4987db02 100644 --- a/pyasic/miners/miner_factory.py +++ b/pyasic/miners/miner_factory.py @@ -476,6 +476,7 @@ class MinerFactory: fn = miner_model_fns.get(miner_type) if fn is not None: + # noinspection PyArgumentList task = asyncio.create_task(fn(ip)) try: miner_model = await asyncio.wait_for( @@ -484,15 +485,10 @@ class MinerFactory: except asyncio.TimeoutError: pass - boser_enabled = None - if miner_type == MinerTypes.BRAIINS_OS: - boser_enabled = await self.get_boser_braiins_os(ip) - miner = self._select_miner_from_classes( ip, miner_type=miner_type, miner_model=miner_model, - boser_enabled=boser_enabled, ) if miner is not None and not isinstance(miner, UnknownMiner): @@ -775,13 +771,9 @@ class MinerFactory: ip: ipaddress.ip_address, miner_model: Union[str, None], miner_type: Union[MinerTypes, None], - boser_enabled: bool = None, ) -> AnyMiner: - kwargs = {} - if boser_enabled is not None: - kwargs["boser"] = boser_enabled try: - return MINER_CLASSES[miner_type][str(miner_model).upper()](ip, **kwargs) + return MINER_CLASSES[miner_type][str(miner_model).upper()](ip) except LookupError: if miner_type in MINER_CLASSES: return MINER_CLASSES[miner_type][None](ip) @@ -909,15 +901,6 @@ class MinerFactory: except (httpx.HTTPError, LookupError): pass - async def get_boser_braiins_os(self, ip: str): - # TODO: refine this check - try: - sock_json_data = await self.send_api_command(ip, "version") - return sock_json_data["STATUS"][0]["Msg"].split(" ")[0].upper() == "BOSER" - except LookupError: - # let the bosminer class decide - return None - async def get_miner_model_vnish(self, ip: str) -> Optional[str]: sock_json_data = await self.send_api_command(ip, "stats") try: diff --git a/pyasic/miners/unknown.py b/pyasic/miners/unknown.py index ddffdc2c..6e443226 100644 --- a/pyasic/miners/unknown.py +++ b/pyasic/miners/unknown.py @@ -33,7 +33,6 @@ class UnknownMiner(BaseMiner): super().__init__(ip) self.ip = ip self.api = UnknownAPI(ip) - self.model = "Unknown" def __repr__(self) -> str: return f"Unknown: {str(self.ip)}" diff --git a/pyasic/web/__init__.py b/pyasic/web/__init__.py index 72774619..abcc9ab6 100644 --- a/pyasic/web/__init__.py +++ b/pyasic/web/__init__.py @@ -26,6 +26,7 @@ class BaseWebAPI(ABC): self.ip = ip # ipaddress.ip_address(ip) self.username = "root" self.pwd = "root" + self.port = 80 def __new__(cls, *args, **kwargs): if cls is BaseWebAPI: diff --git a/pyasic/web/antminer.py b/pyasic/web/antminer.py index 145d55ce..dc9051bb 100644 --- a/pyasic/web/antminer.py +++ b/pyasic/web/antminer.py @@ -35,10 +35,12 @@ class AntminerModernWebAPI(BaseWebAPI): allow_warning: bool = True, **parameters: Union[str, int, bool], ) -> dict: - url = f"http://{self.ip}/cgi-bin/{command}.cgi" + url = f"http://{self.ip}:{self.port}/cgi-bin/{command}.cgi" auth = httpx.DigestAuth(self.username, self.pwd) try: - async with httpx.AsyncClient(transport=settings.transport()) as client: + async with httpx.AsyncClient( + transport=settings.transport(), + ) as client: if parameters: data = await client.post( url, @@ -149,7 +151,7 @@ class AntminerOldWebAPI(BaseWebAPI): allow_warning: bool = True, **parameters: Union[str, int, bool], ) -> dict: - url = f"http://{self.ip}/cgi-bin/{command}.cgi" + url = f"http://{self.ip}:{self.port}/cgi-bin/{command}.cgi" auth = httpx.DigestAuth(self.username, self.pwd) try: async with httpx.AsyncClient(transport=settings.transport()) as client: diff --git a/pyasic/web/braiins_os/__init__.py b/pyasic/web/braiins_os/__init__.py new file mode 100644 index 00000000..848e4e62 --- /dev/null +++ b/pyasic/web/braiins_os/__init__.py @@ -0,0 +1,149 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +import asyncio +from typing import Union + +from pyasic import settings +from pyasic.errors import APIError +from pyasic.web import BaseWebAPI + +from .graphql import BOSerGraphQLAPI +from .grpc import BOSerGRPCAPI +from .luci import BOSMinerLuCIAPI + + +class BOSMinerWebAPI(BaseWebAPI): + def __init__(self, ip: str) -> None: + self.luci = BOSMinerLuCIAPI( + ip, settings.get("default_bosminer_password", "root") + ) + self._pwd = settings.get("default_bosminer_password", "root") + self._port = 80 + super().__init__(ip) + + @property + def pwd(self): + return self._pwd + + @pwd.setter + def pwd(self, other: str): + self._pwd = other + self.luci.pwd = other + + @property + def port(self): + return self._port + + @port.setter + def port(self, other: str): + self._port = other + self.luci.port = other + + async def send_command( + self, + command: Union[str, dict], + ignore_errors: bool = False, + allow_warning: bool = True, + **parameters: Union[str, int, bool], + ) -> dict: + return await self.luci.send_command(command) + + async def multicommand( + self, *commands: Union[dict, str], allow_warning: bool = True + ) -> dict: + return await self.luci.multicommand(*commands) + + +class BOSerWebAPI(BOSMinerWebAPI): + def __init__(self, ip: str) -> None: + self.gql = BOSerGraphQLAPI( + ip, settings.get("default_bosminer_password", "root") + ) + self.grpc = BOSerGRPCAPI(ip, settings.get("default_bosminer_password", "root")) + self._port = 80 + super().__init__(ip) + + @property + def pwd(self): + return self._pwd + + @pwd.setter + def pwd(self, other: str): + self._pwd = other + self.luci.pwd = other + self.gql.pwd = other + self.grpc.pwd = other + + @property + def port(self): + return self._port + + @port.setter + def port(self, other: str): + self._port = other + self.luci.port = other + self.gql.port = other + + async def send_command( + self, + command: Union[str, dict], + ignore_errors: bool = False, + allow_warning: bool = True, + **parameters: Union[str, int, bool], + ) -> dict: + command_type = self.select_command_type(command) + if command_type == "gql": + return await self.gql.send_command(command) + elif command_type == "grpc": + try: + return await (getattr(self.grpc, command.replace("grpc_", "")))() + except AttributeError: + raise APIError(f"No gRPC command found for command: {command}") + elif command_type == "luci": + return await self.luci.send_command(command) + + @staticmethod + def select_command_type(command: Union[str, dict]) -> str: + if isinstance(command, dict): + return "gql" + else: + return "grpc" + + async def multicommand( + self, *commands: Union[dict, str], allow_warning: bool = True + ) -> dict: + cmd_types = {"grpc": [], "gql": []} + for cmd in commands: + cmd_types[self.select_command_type(cmd)].append(cmd) + + async def no_op(): + return {} + + if len(cmd_types["grpc"]) > 0: + grpc_data_t = asyncio.create_task( + self.grpc.multicommand(*cmd_types["grpc"]) + ) + else: + grpc_data_t = asyncio.create_task(no_op()) + if len(cmd_types["gql"]) > 0: + gql_data_t = asyncio.create_task(self.gql.multicommand(*cmd_types["gql"])) + else: + gql_data_t = asyncio.create_task(no_op()) + + await asyncio.gather(grpc_data_t, gql_data_t) + + data = dict(**grpc_data_t.result(), **gql_data_t.result()) + return data diff --git a/pyasic/web/braiins_os/graphql.py b/pyasic/web/braiins_os/graphql.py new file mode 100644 index 00000000..093f0933 --- /dev/null +++ b/pyasic/web/braiins_os/graphql.py @@ -0,0 +1,105 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +import json +from typing import Union + +import httpx + +from pyasic import settings + + +class BOSerGraphQLAPI: + def __init__(self, ip: str, pwd: str): + self.ip = ip + self.username = "root" + self.pwd = pwd + self.port = 80 + + async def multicommand(self, *commands: dict) -> dict: + def merge(*d: dict): + ret = {} + for i in d: + if i: + for k in i: + if not k in ret: + ret[k] = i[k] + else: + ret[k] = merge(ret[k], i[k]) + return None if ret == {} else ret + + command = merge(*commands) + data = await self.send_command(command) + if data is not None: + if data.get("data") is None: + try: + commands = list(commands) + # noinspection PyTypeChecker + commands.remove({"bos": {"faultLight": None}}) + command = merge(*commands) + data = await self.send_command(command) + except (LookupError, ValueError): + pass + if not data: + data = {} + data["multicommand"] = False + return data + + async def send_command( + self, + command: dict, + ) -> dict: + url = f"http://{self.ip}:{self.port}/graphql" + query = command + if command is None: + return {} + if command.get("query") is None: + query = {"query": self.parse_command(command)} + try: + async with httpx.AsyncClient(transport=settings.transport()) as client: + await self.auth(client) + data = await client.post(url, json=query) + except httpx.HTTPError: + pass + else: + if data.status_code == 200: + try: + return data.json() + except json.decoder.JSONDecodeError: + pass + + def parse_command(self, graphql_command: Union[dict, set]) -> str: + if isinstance(graphql_command, dict): + data = [] + for key in graphql_command: + if graphql_command[key] is not None: + parsed = self.parse_command(graphql_command[key]) + data.append(key + parsed) + else: + data.append(key) + else: + data = graphql_command + return "{" + ",".join(data) + "}" + + async def auth(self, client: httpx.AsyncClient) -> None: + url = f"http://{self.ip}:{self.port}/graphql" + await client.post( + url, + json={ + "query": ( + f'mutation{{auth{{login(username:"{self.username}", password:"{self.pwd}"){{__typename}}}}}}' + ) + }, + ) diff --git a/pyasic/web/bosminer/__init__.py b/pyasic/web/braiins_os/grpc.py similarity index 57% rename from pyasic/web/bosminer/__init__.py rename to pyasic/web/braiins_os/grpc.py index ab3909d3..c5f405e9 100644 --- a/pyasic/web/bosminer/__init__.py +++ b/pyasic/web/braiins_os/grpc.py @@ -13,258 +13,20 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -import json +import asyncio +import logging from datetime import timedelta -from typing import Union -import httpx from betterproto import Message +from grpclib import GRPCError, Status from grpclib.client import Channel -from pyasic import settings from pyasic.errors import APIError -from pyasic.web import BaseWebAPI from .proto.braiins.bos import * from .proto.braiins.bos.v1 import * -class BOSMinerWebAPI(BaseWebAPI): - def __init__(self, ip: str, boser: bool = None) -> None: - if boser is None: - boser = True - - if boser: - self.gql = BOSMinerGQLAPI( - ip, settings.get("default_bosminer_password", "root") - ) - self.grpc = BOSMinerGRPCAPI( - ip, settings.get("default_bosminer_password", "root") - ) - else: - self.gql = None - self.grpc = None - self.luci = BOSMinerLuCIAPI( - ip, settings.get("default_bosminer_password", "root") - ) - - self._pwd = settings.get("default_bosminer_password", "root") - super().__init__(ip) - - @property - def pwd(self): - return self._pwd - - @pwd.setter - def pwd(self, other: str): - self._pwd = other - self.luci.pwd = other - if self.gql is not None: - self.gql.pwd = other - if self.grpc is not None: - self.grpc.pwd = other - - async def send_command( - self, - command: Union[str, dict], - ignore_errors: bool = False, - allow_warning: bool = True, - **parameters: Union[str, int, bool], - ) -> dict: - if isinstance(command, dict): - if self.gql is not None: - return await self.gql.send_command(command) - elif command.startswith("/cgi-bin/luci"): - return await self.gql.send_command(command) - else: - if self.grpc is not None: - return await self.grpc.send_command(command) - - async def multicommand( - self, *commands: Union[dict, str], allow_warning: bool = True - ) -> dict: - luci_commands = [] - gql_commands = [] - grpc_commands = [] - for cmd in commands: - if isinstance(cmd, dict): - gql_commands.append(cmd) - elif cmd.startswith("/cgi-bin/luci"): - luci_commands.append(cmd) - else: - grpc_commands.append(cmd) - - luci_data = await self.luci.multicommand(*luci_commands) - if self.gql is not None: - gql_data = await self.gql.multicommand(*gql_commands) - else: - gql_data = None - if self.grpc is not None: - grpc_data = await self.grpc.multicommand(*grpc_commands) - else: - grpc_data = None - - if gql_data is None: - gql_data = {} - if luci_data is None: - luci_data = {} - if grpc_data is None: - grpc_data = {} - - data = dict(**luci_data, **gql_data, **grpc_data) - return data - - -class BOSMinerGQLAPI: - def __init__(self, ip: str, pwd: str): - self.ip = ip - self.username = "root" - self.pwd = pwd - - async def multicommand(self, *commands: dict) -> dict: - def merge(*d: dict): - ret = {} - for i in d: - if i: - for k in i: - if not k in ret: - ret[k] = i[k] - else: - ret[k] = merge(ret[k], i[k]) - return None if ret == {} else ret - - command = merge(*commands) - data = await self.send_command(command) - if data is not None: - if data.get("data") is None: - try: - commands = list(commands) - # noinspection PyTypeChecker - commands.remove({"bos": {"faultLight": None}}) - command = merge(*commands) - data = await self.send_command(command) - except (LookupError, ValueError): - pass - if not data: - data = {} - data["multicommand"] = False - return data - - async def send_command( - self, - command: dict, - ) -> dict: - url = f"http://{self.ip}/graphql" - query = command - if command is None: - return {} - if command.get("query") is None: - query = {"query": self.parse_command(command)} - try: - async with httpx.AsyncClient(transport=settings.transport()) as client: - await self.auth(client) - data = await client.post(url, json=query) - except httpx.HTTPError: - pass - else: - if data.status_code == 200: - try: - return data.json() - except json.decoder.JSONDecodeError: - pass - - def parse_command(self, graphql_command: Union[dict, set]) -> str: - if isinstance(graphql_command, dict): - data = [] - for key in graphql_command: - if graphql_command[key] is not None: - parsed = self.parse_command(graphql_command[key]) - data.append(key + parsed) - else: - data.append(key) - else: - data = graphql_command - return "{" + ",".join(data) + "}" - - async def auth(self, client: httpx.AsyncClient) -> None: - url = f"http://{self.ip}/graphql" - await client.post( - url, - json={ - "query": ( - 'mutation{auth{login(username:"' - + "root" - + '", password:"' - + self.pwd - + '"){__typename}}}' - ) - }, - ) - - -class BOSMinerLuCIAPI: - def __init__(self, ip: str, pwd: str): - self.ip = ip - self.username = "root" - self.pwd = pwd - - async def multicommand(self, *commands: str) -> dict: - data = {} - for command in commands: - data[command] = await self.send_command(command, ignore_errors=True) - return data - - async def send_command(self, path: str, ignore_errors: bool = False) -> dict: - try: - async with httpx.AsyncClient(transport=settings.transport()) as client: - await self.auth(client) - data = await client.get( - f"http://{self.ip}{path}", headers={"User-Agent": "BTC Tools v0.1"} - ) - if data.status_code == 200: - return data.json() - if ignore_errors: - return {} - raise APIError( - f"Web command failed: path={path}, code={data.status_code}" - ) - except (httpx.HTTPError, json.JSONDecodeError): - if ignore_errors: - return {} - raise APIError(f"Web command failed: path={path}") - - async def auth(self, session: httpx.AsyncClient): - login = {"luci_username": self.username, "luci_password": self.pwd} - url = f"http://{self.ip}/cgi-bin/luci" - headers = { - "User-Agent": ( - "BTC Tools v0.1" - ), # only seems to respond if this user-agent is set - "Content-Type": "application/x-www-form-urlencoded", - } - await session.post(url, headers=headers, data=login) - - async def get_net_conf(self): - return await self.send_command("/cgi-bin/luci/admin/network/iface_status/lan") - - async def get_cfg_metadata(self): - return await self.send_command("/cgi-bin/luci/admin/miner/cfg_metadata") - - async def get_cfg_data(self): - return await self.send_command("/cgi-bin/luci/admin/miner/cfg_data") - - async def get_bos_info(self): - return await self.send_command("/cgi-bin/luci/bos/info") - - async def get_overview(self): - return await self.send_command( - "/cgi-bin/luci/admin/status/overview?status=1" - ) # needs status=1 or it fails - - async def get_api_status(self): - return await self.send_command("/cgi-bin/luci/admin/miner/api_status") - - class BOSMinerGRPCStub( ApiVersionServiceStub, AuthenticationServiceStub, @@ -279,11 +41,12 @@ class BOSMinerGRPCStub( pass -class BOSMinerGRPCAPI: +class BOSerGRPCAPI: def __init__(self, ip: str, pwd: str): self.ip = ip self.username = "root" self.pwd = pwd + self.port = 50051 self._auth = None self._auth_time = datetime.now() @@ -305,7 +68,20 @@ class BOSMinerGRPCAPI: ] async def multicommand(self, *commands: str) -> dict: - pass + result = {"multicommand": True} + tasks = {} + for command in commands: + try: + tasks[command] = asyncio.create_task(getattr(self, command)()) + except AttributeError: + result["command"] = {} + + await asyncio.gather(*list(tasks.values())) + + for cmd in tasks: + result[cmd] = tasks[cmd].result() + + return result async def send_command( self, @@ -317,13 +93,23 @@ class BOSMinerGRPCAPI: metadata = [] if auth: metadata.append(("authorization", await self.auth())) - async with Channel(self.ip, 50051) as c: - endpoint = getattr(BOSMinerGRPCStub(c), command) - if endpoint is None: - if not ignore_errors: - raise APIError(f"Command not found - {endpoint}") - return {} - return (await endpoint(message, metadata=metadata)).to_pydict() + try: + async with Channel(self.ip, self.port) as c: + endpoint = getattr(BOSMinerGRPCStub(c), command) + if endpoint is None: + if not ignore_errors: + raise APIError(f"Command not found - {endpoint}") + return {} + try: + return (await endpoint(message, metadata=metadata)).to_pydict() + except GRPCError as e: + if e.status == Status.UNAUTHENTICATED: + await self._get_auth() + metadata = [("authorization", await self.auth())] + return (await endpoint(message, metadata=metadata)).to_pydict() + raise e + except GRPCError as e: + raise APIError(f"gRPC command failed - {endpoint}") from e async def auth(self): if self._auth is not None and self._auth_time - datetime.now() < timedelta( @@ -334,7 +120,7 @@ class BOSMinerGRPCAPI: return self._auth async def _get_auth(self): - async with Channel(self.ip, 50051) as c: + async with Channel(self.ip, self.port) as c: req = LoginRequest(username=self.username, password=self.pwd) async with c.request( "/braiins.bos.v1.AuthenticationService/Login", @@ -379,7 +165,9 @@ class BOSMinerGRPCAPI: ) async def get_locate_device_status(self): - return await self.send_command("get_locate_device_status") + return await self.send_command( + "get_locate_device_status", GetLocateDeviceStatusRequest() + ) async def set_password(self, password: str = None): return await self.send_command( @@ -402,10 +190,12 @@ class BOSMinerGRPCAPI: ) async def get_tuner_state(self): - return await self.send_command("get_tuner_state") + return await self.send_command("get_tuner_state", GetTunerStateRequest()) async def list_target_profiles(self): - return await self.send_command("list_target_profiles") + return await self.send_command( + "list_target_profiles", ListTargetProfilesRequest() + ) async def set_default_power_target( self, save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY @@ -506,16 +296,72 @@ class BOSMinerGRPCAPI: async def set_dps( self, + enable: bool, + power_step: int, + min_power_target: int, + enable_shutdown: bool = None, + shutdown_duration: int = None, ): - raise NotImplementedError - return await self.send_command("braiins.bos.v1.PerformanceService/SetDPS") - - async def set_performance_mode(self): - raise NotImplementedError return await self.send_command( - "braiins.bos.v1.PerformanceService/SetPerformanceMode" + "set_dps", + message=SetDpsRequest( + enable=enable, + enable_shutdown=enable_shutdown, + shutdown_duration=shutdown_duration, + target=DpsTarget( + power_target=DpsPowerTarget( + power_step=Power(power_step), + min_power_target=Power(min_power_target), + ) + ), + ), ) + async def set_performance_mode( + self, + wattage_target: int = None, + hashrate_target: int = None, + save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY, + ): + if wattage_target is not None and hashrate_target is not None: + logging.error( + "Cannot use both wattage_target and hashrate_target, using wattage_target." + ) + elif wattage_target is None and hashrate_target is None: + raise APIError( + "No target supplied, please supply either wattage_target or hashrate_target." + ) + if wattage_target is not None: + return await self.send_command( + "set_performance_mode", + message=SetPerformanceModeRequest( + save_action=save_action, + mode=PerformanceMode( + tuner_mode=TunerPerformanceMode( + power_target=PowerTargetMode( + power_target=Power(watt=wattage_target) + ) + ) + ), + ), + ) + if hashrate_target is not None: + return await self.send_command( + "set_performance_mode", + message=SetPerformanceModeRequest( + save_action=save_action, + mode=PerformanceMode( + tuner_mode=TunerPerformanceMode( + hashrate_target=HashrateTargetMode( + hashrate_target=TeraHashrate( + terahash_per_second=hashrate_target + ) + ) + ) + ), + ), + ) + async def get_active_performance_mode(self): return await self.send_command( "get_active_performance_mode", GetPerformanceModeRequest() diff --git a/pyasic/web/braiins_os/luci.py b/pyasic/web/braiins_os/luci.py new file mode 100644 index 00000000..24aa7a5e --- /dev/null +++ b/pyasic/web/braiins_os/luci.py @@ -0,0 +1,84 @@ +# ------------------------------------------------------------------------------ +# Copyright 2022 Upstream Data Inc - +# - +# Licensed under the Apache License, Version 2.0 (the "License"); - +# you may not use this file except in compliance with the License. - +# You may obtain a copy of the License at - +# - +# http://www.apache.org/licenses/LICENSE-2.0 - +# - +# Unless required by applicable law or agreed to in writing, software - +# distributed under the License is distributed on an "AS IS" BASIS, - +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - +# See the License for the specific language governing permissions and - +# limitations under the License. - +# ------------------------------------------------------------------------------ +import json + +import httpx + +from pyasic import settings +from pyasic.errors import APIError + + +class BOSMinerLuCIAPI: + def __init__(self, ip: str, pwd: str): + self.ip = ip + self.username = "root" + self.pwd = pwd + self.port = 80 + + async def multicommand(self, *commands: str) -> dict: + data = {} + for command in commands: + data[command] = await self.send_command(command, ignore_errors=True) + return data + + async def send_command(self, path: str, ignore_errors: bool = False) -> dict: + try: + async with httpx.AsyncClient(transport=settings.transport()) as client: + await self.auth(client) + data = await client.get( + f"http://{self.ip}:{self.port}/cgi-bin/luci/{path}", + headers={"User-Agent": "BTC Tools v0.1"}, + ) + if data.status_code == 200: + return data.json() + if ignore_errors: + return {} + raise APIError( + f"LUCI web command failed: path={path}, code={data.status_code}" + ) + except (httpx.HTTPError, json.JSONDecodeError): + if ignore_errors: + return {} + raise APIError(f"LUCI web command failed: path={path}") + + async def auth(self, session: httpx.AsyncClient): + login = {"luci_username": self.username, "luci_password": self.pwd} + url = f"http://{self.ip}:{self.port}/cgi-bin/luci" + headers = { + "User-Agent": "BTC Tools v0.1", # only seems to respond if this user-agent is set + "Content-Type": "application/x-www-form-urlencoded", + } + await session.post(url, headers=headers, data=login) + + async def get_net_conf(self): + return await self.send_command("admin/network/iface_status/lan") + + async def get_cfg_metadata(self): + return await self.send_command("admin/miner/cfg_metadata") + + async def get_cfg_data(self): + return await self.send_command("admin/miner/cfg_data") + + async def get_bos_info(self): + return await self.send_command("bos/info") + + async def get_overview(self): + return await self.send_command( + "admin/status/overview?status=1" + ) # needs status=1 or it fails + + async def get_api_status(self): + return await self.send_command("admin/miner/api_status") diff --git a/pyasic/web/bosminer/proto/__init__.py b/pyasic/web/braiins_os/proto/__init__.py similarity index 100% rename from pyasic/web/bosminer/proto/__init__.py rename to pyasic/web/braiins_os/proto/__init__.py diff --git a/pyasic/web/bosminer/proto/braiins/__init__.py b/pyasic/web/braiins_os/proto/braiins/__init__.py similarity index 100% rename from pyasic/web/bosminer/proto/braiins/__init__.py rename to pyasic/web/braiins_os/proto/braiins/__init__.py diff --git a/pyasic/web/bosminer/proto/braiins/bos/__init__.py b/pyasic/web/braiins_os/proto/braiins/bos/__init__.py similarity index 100% rename from pyasic/web/bosminer/proto/braiins/bos/__init__.py rename to pyasic/web/braiins_os/proto/braiins/bos/__init__.py diff --git a/pyasic/web/bosminer/proto/braiins/bos/v1/__init__.py b/pyasic/web/braiins_os/proto/braiins/bos/v1/__init__.py similarity index 100% rename from pyasic/web/bosminer/proto/braiins/bos/v1/__init__.py rename to pyasic/web/braiins_os/proto/braiins/bos/v1/__init__.py diff --git a/pyasic/web/epic.py b/pyasic/web/epic.py index 6482bdc5..684648b9 100644 --- a/pyasic/web/epic.py +++ b/pyasic/web/epic.py @@ -29,6 +29,7 @@ class ePICWebAPI(BaseWebAPI): self.username = "root" self.pwd = settings.get("default_epic_password", "letmein") self.token = None + self.port = 4028 async def send_command( self, @@ -50,13 +51,13 @@ class ePICWebAPI(BaseWebAPI): "password": self.pwd, } response = await client.post( - f"http://{self.ip}:4028/{command}", + f"http://{self.ip}:{self.port}/{command}", timeout=5, json=epic_param, ) else: response = await client.get( - f"http://{self.ip}:4028/{command}", + f"http://{self.ip}:{self.port}/{command}", timeout=5, ) if not response.status_code == 200: @@ -88,10 +89,10 @@ class ePICWebAPI(BaseWebAPI): return data async def restart_epic(self) -> dict: - return await self.send_command("softreboot", post=True, parameters=None) + return await self.send_command("softreboot", post=True) async def reboot(self) -> dict: - return await self.send_command("reboot", post=True, parameters=None) + return await self.send_command("reboot", post=True) async def pause_mining(self) -> dict: return await self.send_command("miner", post=True, parameters="Stop") diff --git a/pyasic/web/goldshell.py b/pyasic/web/goldshell.py index f01abfad..b0626941 100644 --- a/pyasic/web/goldshell.py +++ b/pyasic/web/goldshell.py @@ -33,10 +33,10 @@ class GoldshellWebAPI(BaseWebAPI): async def auth(self): async with httpx.AsyncClient(transport=settings.transport()) as client: try: - await client.get(f"http://{self.ip}/user/logout") + await client.get(f"http://{self.ip}:{self.port}/user/logout") auth = ( await client.get( - f"http://{self.ip}/user/login?username={self.username}&password={self.pwd}&cipher=false" + f"http://{self.ip}:{self.port}/user/login?username={self.username}&password={self.pwd}&cipher=false" ) ).json() except httpx.HTTPError: @@ -46,7 +46,7 @@ class GoldshellWebAPI(BaseWebAPI): try: auth = ( await client.get( - f"http://{self.ip}/user/login?username=admin&password=bbad7537f4c8b6ea31eea0b3d760e257&cipher=true" + f"http://{self.ip}:{self.port}/user/login?username=admin&password=bbad7537f4c8b6ea31eea0b3d760e257&cipher=true" ) ).json() except (httpx.HTTPError, json.JSONDecodeError): @@ -76,14 +76,14 @@ class GoldshellWebAPI(BaseWebAPI): try: if parameters: response = await client.put( - f"http://{self.ip}/mcb/{command}", + f"http://{self.ip}:{self.port}/mcb/{command}", headers={"Authorization": "Bearer " + self.jwt}, timeout=settings.get("api_function_timeout", 5), json=parameters, ) else: response = await client.get( - f"http://{self.ip}/mcb/{command}", + f"http://{self.ip}:{self.port}/mcb/{command}", headers={"Authorization": "Bearer " + self.jwt}, timeout=settings.get("api_function_timeout", 5), ) @@ -106,7 +106,7 @@ class GoldshellWebAPI(BaseWebAPI): for command in commands: try: response = await client.get( - f"http://{self.ip}/mcb/{command}", + f"http://{self.ip}:{self.port}/mcb/{command}", headers={"Authorization": "Bearer " + self.jwt}, timeout=settings.get("api_function_timeout", 5), ) diff --git a/pyasic/web/innosilicon.py b/pyasic/web/innosilicon.py index ac174df6..ca095003 100644 --- a/pyasic/web/innosilicon.py +++ b/pyasic/web/innosilicon.py @@ -35,7 +35,7 @@ class InnosiliconWebAPI(BaseWebAPI): async with httpx.AsyncClient(transport=settings.transport()) as client: try: auth = await client.post( - f"http://{self.ip}/api/auth", + f"http://{self.ip}:{self.port}/api/auth", data={"username": self.username, "password": self.pwd}, ) except httpx.HTTPError: @@ -58,7 +58,7 @@ class InnosiliconWebAPI(BaseWebAPI): for i in range(settings.get("get_data_retries", 1)): try: response = await client.post( - f"http://{self.ip}/api/{command}", + f"http://{self.ip}:{self.port}/api/{command}", headers={"Authorization": "Bearer " + self.jwt}, timeout=settings.get("api_function_timeout", 5), json=parameters, @@ -94,7 +94,7 @@ class InnosiliconWebAPI(BaseWebAPI): for command in commands: try: response = await client.post( - f"http://{self.ip}/api/{command}", + f"http://{self.ip}:{self.port}/api/{command}", headers={"Authorization": "Bearer " + self.jwt}, timeout=settings.get("api_function_timeout", 5), ) diff --git a/pyasic/web/vnish.py b/pyasic/web/vnish.py index 881f0814..83ee092f 100644 --- a/pyasic/web/vnish.py +++ b/pyasic/web/vnish.py @@ -34,7 +34,7 @@ class VNishWebAPI(BaseWebAPI): async with httpx.AsyncClient(transport=settings.transport()) as client: try: auth = await client.post( - f"http://{self.ip}/api/v1/unlock", + f"http://{self.ip}:{self.port}/api/v1/unlock", json={"pw": self.pwd}, ) except httpx.HTTPError: @@ -68,21 +68,21 @@ class VNishWebAPI(BaseWebAPI): if parameters.get("post"): parameters.pop("post") response = await client.post( - f"http://{self.ip}/api/v1/{command}", + f"http://{self.ip}:{self.port}/api/v1/{command}", headers={"Authorization": auth}, timeout=settings.get("api_function_timeout", 5), json=parameters, ) elif not parameters == {}: response = await client.post( - f"http://{self.ip}/api/v1/{command}", + f"http://{self.ip}:{self.port}/api/v1/{command}", headers={"Authorization": auth}, timeout=settings.get("api_function_timeout", 5), json=parameters, ) else: response = await client.get( - f"http://{self.ip}/api/v1/{command}", + f"http://{self.ip}:{self.port}/api/v1/{command}", headers={"Authorization": auth}, timeout=settings.get("api_function_timeout", 5), ) @@ -145,3 +145,6 @@ class VNishWebAPI(BaseWebAPI): async def settings(self): return await self.send_command("settings") + + async def autotune_presets(self): + return await self.send_command("autotune/presets") diff --git a/tests/__init__.py b/tests/__init__.py index 0c5048c4..17b6c644 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -14,8 +14,6 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -import unittest - from tests.api_tests import * from tests.config_tests import TestConfig from tests.miners_tests import MinersTest diff --git a/tests/miners_tests/__init__.py b/tests/miners_tests/__init__.py index da761710..894661ea 100644 --- a/tests/miners_tests/__init__.py +++ b/tests/miners_tests/__init__.py @@ -18,23 +18,22 @@ import unittest import warnings from dataclasses import asdict -from pyasic.miners.backends import CGMiner # noqa from pyasic.miners.miner_factory import MINER_CLASSES class MinersTest(unittest.TestCase): - def test_miner_model_creation(self): + def test_miner_type_creation(self): warnings.filterwarnings("ignore") - for miner_model in MINER_CLASSES.keys(): - for miner_api in MINER_CLASSES[miner_model].keys(): + for miner_type in MINER_CLASSES.keys(): + for miner_model in MINER_CLASSES[miner_type].keys(): with self.subTest( msg=f"Test creation of miner", + miner_type=miner_type, miner_model=miner_model, - miner_api=miner_api, ): - miner = MINER_CLASSES[miner_model][miner_api]("127.0.0.1") + miner = MINER_CLASSES[miner_type][miner_model]("127.0.0.1") self.assertTrue( - isinstance(miner, MINER_CLASSES[miner_model][miner_api]) + isinstance(miner, MINER_CLASSES[miner_type][miner_model]) ) def test_miner_data_map_keys(self): @@ -60,14 +59,14 @@ class MinersTest(unittest.TestCase): ] ) warnings.filterwarnings("ignore") - for miner_model in MINER_CLASSES.keys(): - for miner_api in MINER_CLASSES[miner_model].keys(): + for miner_type in MINER_CLASSES.keys(): + for miner_model in MINER_CLASSES[miner_type].keys(): with self.subTest( msg=f"Data map key check", + miner_type=miner_type, miner_model=miner_model, - miner_api=miner_api, ): - miner = MINER_CLASSES[miner_model][miner_api]("127.0.0.1") + miner = MINER_CLASSES[miner_type][miner_model]("127.0.0.1") miner_keys = sorted( [str(k) for k in asdict(miner.data_locations).keys()] ) @@ -75,14 +74,14 @@ class MinersTest(unittest.TestCase): def test_data_locations_match_signatures_command(self): warnings.filterwarnings("ignore") - for miner_model in MINER_CLASSES.keys(): - for miner_api in MINER_CLASSES[miner_model].keys(): - miner = MINER_CLASSES[miner_model][miner_api]("127.0.0.1") + for miner_type in MINER_CLASSES.keys(): + for miner_model in MINER_CLASSES[miner_type].keys(): + miner = MINER_CLASSES[miner_type][miner_model]("127.0.0.1") for data_point in asdict(miner.data_locations).values(): with self.subTest( msg=f"Test {data_point['cmd']} signature matches", + miner_type=miner_type, miner_model=miner_model, - miner_api=miner_api, ): func = getattr(miner, data_point["cmd"]) signature = inspect.signature(func) @@ -98,23 +97,6 @@ class MinersTest(unittest.TestCase): set([k["name"] for k in data_point["kwargs"]]), ) - def test_data_locations_use_private_funcs(self): - warnings.filterwarnings("ignore") - for miner_model in MINER_CLASSES.keys(): - for miner_api in MINER_CLASSES[miner_model].keys(): - miner = MINER_CLASSES[miner_model][miner_api]("127.0.0.1") - for data_point in asdict(miner.data_locations).values(): - with self.subTest( - msg=f"Test {data_point['cmd']} is private", - miner_model=miner_model, - miner_api=miner_api, - ): - self.assertTrue( - data_point["cmd"].startswith("_") - or data_point["cmd"] == "get_config" - ) - - if __name__ == "__main__": unittest.main()