From 5457ae6cd5618ef8f2ffa0ed4aa5b27715e24a6c Mon Sep 17 00:00:00 2001 From: Brett Rowan <121075405+b-rowan@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:55:57 -0600 Subject: [PATCH] feature: add support for avalon Q --- docs/miners/supported_types.md | 6 + pyasic/device/models.py | 1 + pyasic/miners/avalonminer/cgminer/Q/Q.py | 22 ++ .../miners/avalonminer/cgminer/Q/__init__.py | 1 + pyasic/miners/avalonminer/cgminer/__init__.py | 1 + .../miners/avalonminer/cgminer/nano/nano3.py | 74 ++--- pyasic/miners/backends/avalonminer.py | 312 ++++++++++++------ .../miners/device/models/avalonminer/Q/Q.py | 27 ++ .../device/models/avalonminer/Q/__init__.py | 17 + .../device/models/avalonminer/__init__.py | 1 + pyasic/miners/factory.py | 1 + pyasic/rpc/avalonminer.py | 27 ++ .../version_24102401_25462b2_9ddf522.py | 118 +++++++ 13 files changed, 461 insertions(+), 147 deletions(-) create mode 100644 pyasic/miners/avalonminer/cgminer/Q/Q.py create mode 100644 pyasic/miners/avalonminer/cgminer/Q/__init__.py create mode 100644 pyasic/miners/device/models/avalonminer/Q/Q.py create mode 100644 pyasic/miners/device/models/avalonminer/Q/__init__.py create mode 100644 pyasic/rpc/avalonminer.py diff --git a/docs/miners/supported_types.md b/docs/miners/supported_types.md index 666d6489..bc6cdea7 100644 --- a/docs/miners/supported_types.md +++ b/docs/miners/supported_types.md @@ -565,6 +565,12 @@ details {
  • Avalon 1566 (Stock)
  • +
    + Q Series: + +
    diff --git a/pyasic/device/models.py b/pyasic/device/models.py index 1ecb59a6..3a811c38 100644 --- a/pyasic/device/models.py +++ b/pyasic/device/models.py @@ -456,6 +456,7 @@ class AvalonminerModels(MinerModelType): Avalon1566 = "Avalon 1566" AvalonNano3 = "Avalon Nano 3" AvalonNano3s = "Avalon Nano 3s" + AvalonQHome = "Avalon Q Home" def __str__(self): return self.value diff --git a/pyasic/miners/avalonminer/cgminer/Q/Q.py b/pyasic/miners/avalonminer/cgminer/Q/Q.py new file mode 100644 index 00000000..42cd7476 --- /dev/null +++ b/pyasic/miners/avalonminer/cgminer/Q/Q.py @@ -0,0 +1,22 @@ +# ------------------------------------------------------------------------------ +# Copyright 2025 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. - +# ------------------------------------------------------------------------------ + +from pyasic.miners.backends import AvalonMiner +from pyasic.miners.device.models import AvalonQHome + + +class CGMinerAvalonQHome(AvalonMiner, AvalonQHome): + pass diff --git a/pyasic/miners/avalonminer/cgminer/Q/__init__.py b/pyasic/miners/avalonminer/cgminer/Q/__init__.py new file mode 100644 index 00000000..59176da9 --- /dev/null +++ b/pyasic/miners/avalonminer/cgminer/Q/__init__.py @@ -0,0 +1 @@ +from .Q import CGMinerAvalonQHome diff --git a/pyasic/miners/avalonminer/cgminer/__init__.py b/pyasic/miners/avalonminer/cgminer/__init__.py index 82118f81..9b6f020f 100644 --- a/pyasic/miners/avalonminer/cgminer/__init__.py +++ b/pyasic/miners/avalonminer/cgminer/__init__.py @@ -22,3 +22,4 @@ from .A11X import * from .A12X import * from .A15X import * from .nano import * +from .Q import * diff --git a/pyasic/miners/avalonminer/cgminer/nano/nano3.py b/pyasic/miners/avalonminer/cgminer/nano/nano3.py index 0e25a566..da6e5a57 100644 --- a/pyasic/miners/avalonminer/cgminer/nano/nano3.py +++ b/pyasic/miners/avalonminer/cgminer/nano/nano3.py @@ -49,31 +49,31 @@ AVALON_NANO_DATA_LOC = DataLocations( ), str(DataOptions.EXPECTED_HASHRATE): DataFunction( "_get_expected_hashrate", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.HASHBOARDS): DataFunction( "_get_hashboards", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.ENVIRONMENT_TEMP): DataFunction( "_get_env_temp", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.WATTAGE_LIMIT): DataFunction( "_get_wattage_limit", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.WATTAGE): DataFunction( "_get_wattage", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.FANS): DataFunction( "_get_fans", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.FAULT_LIGHT): DataFunction( "_get_fault_light", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.UPTIME): DataFunction( "_get_uptime", @@ -102,35 +102,35 @@ AVALON_NANO3S_DATA_LOC = DataLocations( ), str(DataOptions.HASHRATE): DataFunction( "_get_hashrate", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.EXPECTED_HASHRATE): DataFunction( "_get_expected_hashrate", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.HASHBOARDS): DataFunction( "_get_hashboards", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.ENVIRONMENT_TEMP): DataFunction( "_get_env_temp", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.WATTAGE_LIMIT): DataFunction( "_get_wattage_limit", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.WATTAGE): DataFunction( "_get_wattage", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.FANS): DataFunction( "_get_fans", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.FAULT_LIGHT): DataFunction( "_get_fault_light", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.UPTIME): DataFunction( "_get_uptime", @@ -170,58 +170,58 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s): data_locations = AVALON_NANO3S_DATA_LOC - async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]: - if rpc_stats is None: + async def _get_wattage(self, rpc_estats: dict = None) -> Optional[int]: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) - return int(parsed_stats["PS"][6]) + unparsed_estats = rpc_estats["STATS"][0]["MM ID0"] + parsed_estats = self.parse_estats(unparsed_estats) + return int(parsed_estats["PS"][6]) except (IndexError, KeyError, ValueError, TypeError): pass - async def _get_hashrate(self, rpc_stats: dict = None) -> Optional[AlgoHashRate]: - if rpc_stats is None: + async def _get_hashrate(self, rpc_estats: dict = None) -> Optional[AlgoHashRate]: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) + unparsed_estats = rpc_estats["STATS"][0]["MM ID0"] + parsed_estats = self.parse_estats(unparsed_estats) return self.algo.hashrate( - rate=float(parsed_stats["GHSspd"][0]), unit=self.algo.unit.GH + rate=float(parsed_estats["GHSspd"]), unit=self.algo.unit.GH ).into(self.algo.unit.default) except (IndexError, KeyError, ValueError, TypeError): pass - async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: - hashboards = await AvalonMiner._get_hashboards(self, rpc_stats) + async def _get_hashboards(self, rpc_estats: dict = None) -> List[HashBoard]: + hashboards = await AvalonMiner._get_hashboards(self, rpc_estats) - if rpc_stats is None: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) + unparsed_estats = rpc_estats["STATS"][0]["MM ID0"] + parsed_estats = self.parse_estats(unparsed_estats) except (IndexError, KeyError, ValueError, TypeError): return hashboards for board in range(len(hashboards)): try: - board_hr = parsed_stats["GHSspd"][board] + board_hr = parsed_estats["GHSspd"][board] hashboards[board].hashrate = self.algo.hashrate( rate=float(board_hr), unit=self.algo.unit.GH ).into(self.algo.unit.default) diff --git a/pyasic/miners/backends/avalonminer.py b/pyasic/miners/backends/avalonminer.py index 2390892f..0fbc50f6 100644 --- a/pyasic/miners/backends/avalonminer.py +++ b/pyasic/miners/backends/avalonminer.py @@ -13,8 +13,9 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ - +import copy import re +import time from typing import List, Optional from pyasic.data import Fan, HashBoard @@ -22,6 +23,7 @@ from pyasic.device.algorithm import AlgoHashRate from pyasic.errors import APIError from pyasic.miners.backends.cgminer import CGMiner from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand +from pyasic.rpc.avalonminer import AvalonMinerRPCAPI AVALON_DATA_LOC = DataLocations( **{ @@ -43,31 +45,31 @@ AVALON_DATA_LOC = DataLocations( ), str(DataOptions.EXPECTED_HASHRATE): DataFunction( "_get_expected_hashrate", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.HASHBOARDS): DataFunction( "_get_hashboards", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.ENVIRONMENT_TEMP): DataFunction( "_get_env_temp", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.WATTAGE_LIMIT): DataFunction( "_get_wattage_limit", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.WATTAGE): DataFunction( "_get_wattage", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.FANS): DataFunction( "_get_fans", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.FAULT_LIGHT): DataFunction( "_get_fault_light", - [RPCAPICommand("rpc_stats", "stats")], + [RPCAPICommand("rpc_estats", "estats")], ), str(DataOptions.UPTIME): DataFunction( "_get_uptime", @@ -84,6 +86,9 @@ AVALON_DATA_LOC = DataLocations( class AvalonMiner(CGMiner): """Handler for Avalon Miners""" + _rpc_cls = AvalonMinerRPCAPI + rpc: AvalonMinerRPCAPI + data_locations = AVALON_DATA_LOC async def fault_light_on(self) -> bool: @@ -134,45 +139,94 @@ class AvalonMiner(CGMiner): return False return False + async def stop_mining(self) -> bool: + try: + # Shut off 5 seconds from now + timestamp = int(time.time()) + 5 + data = await self.rpc.ascset(0, f"softoff", f"1:{timestamp}") + except APIError: + return False + if "success" in data["STATUS"][0]["Msg"]: + return True + return False + + async def resume_mining(self) -> bool: + try: + # Shut off 5 seconds from now + timestamp = int(time.time()) + 5 + data = await self.rpc.ascset(0, f"softon", f"1:{timestamp}") + except APIError: + return False + if "success" in data["STATUS"][0]["Msg"]: + return True + return False + @staticmethod - def parse_stats(stats): - _stats_items = re.findall(".+?\\[*?]", stats) - stats_items = [] - stats_dict = {} - for item in _stats_items: - if ": " in item: - data = item.replace("]", "").split("[") - data_list = [i.split(": ") for i in data[1].strip().split(", ")] - data_dict = {} - try: - for key, val in [tuple(item) for item in data_list]: - data_dict[key] = val - except ValueError: - # --avalon args - for arg_item in data_list: - item_data = arg_item[0].split(" ") - for idx, val in enumerate(item_data): - if idx % 2 == 0 or idx == 0: - data_dict[val] = item_data[idx + 1] + def parse_estats(data): + # Deep copy to preserve original structure + new_data = copy.deepcopy(data) - raw_data = [data[0].strip(), data_dict] + def convert_value(val, key): + val = val.strip() + + if key == "SYSTEMSTATU": + return val + + if " " in val: + parts = val.split() + result = [] + for part in parts: + if part.isdigit(): + result.append(int(part)) + else: + try: + result.append(float(part)) + except ValueError: + result.append(part) + return result else: - raw_data = [ - value - for value in item.replace("[", " ") - .replace("]", " ") - .split(" ")[:-1] - if value != "" - ] - if len(raw_data) == 1: - raw_data.append("") - if raw_data[0] == "": - raw_data = raw_data[1:] + if val.isdigit(): + return int(val) + try: + return float(val) + except ValueError: + return val - stats_dict[raw_data[0]] = raw_data[1:] - stats_items.append(raw_data) + def parse_info_block(info_str): + pattern = re.compile(r"(\w+)\[([^\]]*)\]") + return { + key: convert_value(val, key) for key, val in pattern.findall(info_str) + } - return stats_dict + for stat in new_data.get("STATS", []): + keys_to_replace = {} + + for key, value in stat.items(): + if "MM" in key: + # Normalize key by removing suffix after colon + norm_key = key.split(":")[0] + + mm_data = value + if not isinstance(mm_data, str): + continue + if mm_data.startswith("'STATS':"): + mm_data = mm_data[len("'STATS':") :] + keys_to_replace[norm_key] = parse_info_block(mm_data) + + elif key == "HBinfo": + match = re.search(r"'(\w+)':\{(.+)\}", value) + if match: + hb_key = match.group(1) + hb_data = match.group(2) + keys_to_replace[key] = {hb_key: parse_info_block(hb_data)} + + # Remove old keys and insert parsed versions + for k in list(stat.keys()): + if "MM" in k or k == "HBinfo": + del stat[k] + stat.update(keys_to_replace) + + return new_data ################################################## ### DATA GATHERING FUNCTIONS (get_{some_data}) ### @@ -211,7 +265,7 @@ class AvalonMiner(CGMiner): except (KeyError, IndexError, ValueError, TypeError): pass - async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_estats: dict = None) -> List[HashBoard]: if self.expected_hashboards is None: return [] @@ -220,164 +274,202 @@ class AvalonMiner(CGMiner): for i in range(self.expected_hashboards) ] - if rpc_stats is None: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) + parsed_estats = self.parse_estats(rpc_estats) except (IndexError, KeyError, ValueError, TypeError): return hashboards for board in range(self.expected_hashboards): + try: - hashboards[board].chip_temp = int(parsed_stats["MTmax"][board]) + board_hr = parsed_estats["STATS"][0]["MM ID0"]["MGHS"] + if isinstance(board_hr, list): + hashboards[board].hashrate = self.algo.hashrate( + rate=float(board_hr[board]), unit=self.algo.unit.GH + ).into(self.algo.unit.default) + else: + hashboards[board].hashrate = self.algo.hashrate( + rate=float(board_hr), unit=self.algo.unit.GH + ).into(self.algo.unit.default) + except LookupError: pass try: - board_hr = parsed_stats["MGHS"][board] - hashboards[board].hashrate = self.algo.hashrate( - rate=float(board_hr), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + hashboards[board].chip_temp = int( + parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board] + ) except LookupError: - pass + try: + hashboards[board].chip_temp = int( + parsed_estats["STATS"][0]["MM ID0"]["Tmax"] + ) + except LookupError: + pass try: - hashboards[board].temp = int(parsed_stats["MTavg"][board]) + hashboards[board].temp = int( + parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board] + ) except LookupError: - pass + try: + hashboards[board].temp = int( + parsed_estats["STATS"][0]["MM ID0"]["Tavg"] + ) + except LookupError: + pass try: - chip_data = parsed_stats[f"PVT_T{board}"] + hashboards[board].inlet_temp = int( + parsed_estats["STATS"][0]["MM ID0"]["MTavg"][board] + ) + except LookupError: + try: + hashboards[board].inlet_temp = int( + parsed_estats["STATS"][0]["MM ID0"]["HBITemp"] + ) + except LookupError: + pass + + try: + hashboards[board].outlet_temp = int( + parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board] + ) + except LookupError: + try: + hashboards[board].outlet_temp = int( + parsed_estats["STATS"][0]["MM ID0"]["HBOTemp"] + ) + except LookupError: + pass + + try: + chip_data = parsed_estats["STATS"][0]["MM ID0"][f"PVT_T{board}"] hashboards[board].missing = False if chip_data: hashboards[board].chips = len( [item for item in chip_data if not item == "0"] ) except LookupError: - pass + try: + chip_data = parsed_estats["STATS"][0]["HBinfo"][f"HB{board}"][ + f"PVT_T{board}" + ] + hashboards[board].missing = False + if chip_data: + hashboards[board].chips = len( + [item for item in chip_data if not item == "0"] + ) + except LookupError: + pass return hashboards async def _get_expected_hashrate( - self, rpc_stats: dict = None + self, rpc_estats: dict = None ) -> Optional[AlgoHashRate]: - if rpc_stats is None: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) + parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"] return self.algo.hashrate( - rate=float(parsed_stats["GHSmm"][0]), unit=self.algo.unit.GH + rate=float(parsed_estats["GHSmm"]), unit=self.algo.unit.GH ).into(self.algo.unit.default) except (IndexError, KeyError, ValueError, TypeError): pass - async def _get_env_temp(self, rpc_stats: dict = None) -> Optional[float]: - if rpc_stats is None: + async def _get_env_temp(self, rpc_estats: dict = None) -> Optional[float]: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) - return float(parsed_stats["Temp"][0]) + parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"] + return float(parsed_estats["Temp"]) except (IndexError, KeyError, ValueError, TypeError): pass - async def _get_wattage_limit(self, rpc_stats: dict = None) -> Optional[int]: - if rpc_stats is None: + async def _get_wattage_limit(self, rpc_estats: dict = None) -> Optional[int]: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) - return int(parsed_stats["MPO"][0]) + parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"] + return int(parsed_estats["MPO"]) except (IndexError, KeyError, ValueError, TypeError): pass - async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]: - if rpc_stats is None: + async def _get_wattage(self, rpc_estats: dict = None) -> Optional[int]: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) - return int(parsed_stats["WALLPOWER"][0]) + parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"] + return int(parsed_estats["WALLPOWER"]) except (IndexError, KeyError, ValueError, TypeError): pass - async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_estats: dict = None) -> List[Fan]: if self.expected_fans is None: return [] - if rpc_stats is None: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass fans_data = [Fan() for _ in range(self.expected_fans)] - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) + parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"] except LookupError: return fans_data for fan in range(self.expected_fans): try: - fans_data[fan].speed = int(parsed_stats[f"Fan{fan + 1}"][0]) + fans_data[fan].speed = int(parsed_estats[f"Fan{fan + 1}"]) except (IndexError, KeyError, ValueError, TypeError): pass return fans_data - async def _get_fault_light(self, rpc_stats: dict = None) -> Optional[bool]: + async def _get_fault_light(self, rpc_estats: dict = None) -> Optional[bool]: if self.light: return self.light - if rpc_stats is None: + if rpc_estats is None: try: - rpc_stats = await self.rpc.stats() + rpc_estats = await self.rpc.estats() except APIError: pass - if rpc_stats is not None: + if rpc_estats is not None: try: - unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] - parsed_stats = self.parse_stats(unparsed_stats) - led = int(parsed_stats["Led"][0]) + parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"] + led = int(parsed_estats["Led"]) return True if led == 1 else False except (IndexError, KeyError, ValueError, TypeError): pass - - try: - data = await self.rpc.ascset(0, "led", "1-255") - except APIError: - return False - try: - if data["STATUS"][0]["Msg"] == "ASC 0 set info: LED[1]": - return True - except LookupError: - pass return False diff --git a/pyasic/miners/device/models/avalonminer/Q/Q.py b/pyasic/miners/device/models/avalonminer/Q/Q.py new file mode 100644 index 00000000..23331c7c --- /dev/null +++ b/pyasic/miners/device/models/avalonminer/Q/Q.py @@ -0,0 +1,27 @@ +# ------------------------------------------------------------------------------ +# 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. - +# ------------------------------------------------------------------------------ +from pyasic.device.algorithm import MinerAlgo +from pyasic.device.models import MinerModel +from pyasic.miners.device.makes import AvalonMinerMake + + +class AvalonQHome(AvalonMinerMake): + raw_model = MinerModel.AVALONMINER.AvalonQHome + + expected_chips = 160 + expected_fans = 2 + expected_hashboards = 1 + algo = MinerAlgo.SHA256 diff --git a/pyasic/miners/device/models/avalonminer/Q/__init__.py b/pyasic/miners/device/models/avalonminer/Q/__init__.py new file mode 100644 index 00000000..62201b14 --- /dev/null +++ b/pyasic/miners/device/models/avalonminer/Q/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# 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. - +# ------------------------------------------------------------------------------ + +from .Q import AvalonQHome diff --git a/pyasic/miners/device/models/avalonminer/__init__.py b/pyasic/miners/device/models/avalonminer/__init__.py index 82118f81..9b6f020f 100644 --- a/pyasic/miners/device/models/avalonminer/__init__.py +++ b/pyasic/miners/device/models/avalonminer/__init__.py @@ -22,3 +22,4 @@ from .A11X import * from .A12X import * from .A15X import * from .nano import * +from .Q import * diff --git a/pyasic/miners/factory.py b/pyasic/miners/factory.py index bc6bde54..5f879ee6 100644 --- a/pyasic/miners/factory.py +++ b/pyasic/miners/factory.py @@ -512,6 +512,7 @@ MINER_CLASSES = { "AVALONMINER NANO3": CGMinerAvalonNano3, "AVALON NANO3S": CGMinerAvalonNano3s, "AVALONMINER 15-194": CGMinerAvalon1566, + "AVALON Q": CGMinerAvalonQHome, }, MinerTypes.INNOSILICON: { None: type("InnosiliconUnknown", (Innosilicon, InnosiliconMake), {}), diff --git a/pyasic/rpc/avalonminer.py b/pyasic/rpc/avalonminer.py new file mode 100644 index 00000000..0dff3c17 --- /dev/null +++ b/pyasic/rpc/avalonminer.py @@ -0,0 +1,27 @@ +# ------------------------------------------------------------------------------ +# Copyright 2025 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. - +# ------------------------------------------------------------------------------ + +from pyasic.rpc.cgminer import CGMinerRPCAPI + + +class AvalonMinerRPCAPI(CGMinerRPCAPI): + """An abstraction of the AvalonMiner API. + + Each method corresponds to an API command in AvalonMiner. + """ + + async def litestats(self): + return await self.send_command("litestats") diff --git a/tests/miners_tests/backends_tests/avalonminer_tests/version_24102401_25462b2_9ddf522.py b/tests/miners_tests/backends_tests/avalonminer_tests/version_24102401_25462b2_9ddf522.py index 57a73dd6..1a56f27f 100644 --- a/tests/miners_tests/backends_tests/avalonminer_tests/version_24102401_25462b2_9ddf522.py +++ b/tests/miners_tests/backends_tests/avalonminer_tests/version_24102401_25462b2_9ddf522.py @@ -346,6 +346,124 @@ data = { ], "id": 1, }, + "rpc_estats": { + "STATUS": [ + { + "STATUS": "S", + "When": 986, + "Code": 70, + "Msg": "CGMiner stats", + "Description": "cgminer 4.11.1", + } + ], + "STATS": [ + { + "STATS": 0, + "ID": "AVA100", + "Elapsed": 975, + "Calls": 0, + "Wait": 0.0, + "Max": 0.0, + "Min": 99999999.0, + "MM ID0": "Ver[15-194-24102401_25462b2_9ddf522] DNA[020100008c699117] MEMFREE[1392832.1381104] NETFAIL[0 0 0 0 0 0 0 0] SYSTEMSTATU[Work: In Work, Hash Board: 3 ] Elapsed[975] BOOTBY[0x04.00000000] LW[714031] MH[0 0 0] DHW[0] HW[0] DH[1.299%] Temp[31] TMax[78] TAvg[70] Fan1[4275] Fan2[4282] FanR[49%] Vo[296] PS[0 1209 1187 297 3526 1188 3731] WALLPOWER[3731] PLL0[3346 2549 4722 13703] PLL1[2620 2291 4741 14668] PLL2[1777 2019 3849 16675] GHSspd[199241.07] DHspd[1.299%] GHSmm[201920.42] GHSavg[184007.00] WU[2570548.05] Freq[345.94] Led[0] MGHS[59459.52 61407.83 63139.66] MTmax[77 78 77] MTavg[69 70 71] MVavg[285.7 286.3 285.8] TA[480] Core[A3197S] PING[131] POWS[0] EEPROM[160 160 160] HASHS[0 0 0] POOLS[0] SoftOFF[0] ECHU[0 0 0] ECMM[0] SF0[300 318 339 360] SF1[300 318 339 360] SF2[300 318 339 360] PVT_T0[ 66 65 63 62 64 64 66 65 67 68 65 64 62 64 66 68 66 64 63 63 64 65 65 66 68 66 64 64 65 67 66 68 65 65 64 62 64 67 66 66 65 65 64 65 61 66 66 65 65 65 64 61 61 65 66 66 66 68 65 64 63 63 64 66 65 64 62 64 65 64 65 67 66 68 64 62 60 63 66 64 68 68 70 71 73 73 73 69 70 74 73 73 76 75 73 71 70 73 74 74 76 76 75 71 69 74 73 74 72 72 71 68 69 71 74 73 73 75 74 69 69 74 75 75 73 74 70 70 68 70 72 70 77 75 73 71 73 72 74 74 75 75 71 68 71 71 72 75 74 73 71 69 69 71 71 71 72 74 70 69] PVT_T1[ 65 66 63 62 62 63 66 66 65 65 63 64 65 66 65 66 66 66 65 66 62 64 66 66 67 68 65 63 65 66 66 68 67 68 65 66 66 67 66 66 66 66 65 65 67 66 66 66 68 68 66 65 66 67 69 69 67 67 68 66 63 66 67 68 67 67 66 65 64 68 68 68 68 66 66 64 64 65 66 67 70 71 73 71 78 76 74 71 71 71 77 76 77 76 74 70 71 73 76 78 77 77 73 71 71 73 74 77 76 75 73 69 70 72 73 76 77 77 71 69 70 72 75 75 74 76 73 69 70 71 73 74 75 76 72 70 71 71 74 76 74 75 72 70 69 71 73 76 76 76 71 70 69 71 73 74 75 74 71 67] PVT_T2[ 67 66 66 63 69 67 67 69 66 70 67 68 66 71 68 69 69 67 67 68 69 69 68 67 69 68 68 70 70 71 68 69 71 68 70 69 71 66 68 70 67 66 67 71 69 67 68 67 69 68 68 69 66 69 69 67 70 66 67 67 67 67 66 69 70 66 71 68 67 69 68 69 70 68 69 68 70 67 67 68 72 71 74 74 74 74 72 71 70 74 76 75 74 76 76 71 73 74 76 77 77 75 74 73 72 72 77 76 73 75 74 71 71 71 73 74 74 75 72 70 69 73 74 74 74 74 74 71 71 72 73 74 75 75 74 74 73 72 77 75 75 75 72 72 71 72 74 75 75 74 74 71 69 72 74 74 74 73 70 68] PVT_V0[293 292 291 290 290 292 294 295 290 289 291 290 291 292 291 291 288 288 290 290 292 292 289 290 289 288 289 289 287 290 291 291 286 286 286 287 288 289 289 287 290 290 290 290 290 292 291 292 289 291 291 291 291 291 289 288 291 290 290 289 292 294 291 290 290 290 291 291 289 290 290 289 288 288 287 287 290 289 290 294 288 283 280 279 282 282 282 282 277 276 278 281 277 281 283 283 281 279 279 280 279 280 280 280 283 281 281 282 283 284 285 286 281 279 282 283 281 279 279 278 286 281 279 278 283 283 283 281 285 284 281 281 282 280 280 280 278 279 281 278 283 283 283 285 279 281 283 282 280 278 281 285 281 283 284 285 286 285 281 281] PVT_V1[293 292 293 290 292 293 292 292 291 290 292 291 291 291 291 291 291 293 290 290 291 292 292 293 290 290 292 292 289 289 289 287 291 291 291 290 289 289 288 287 290 289 289 289 291 291 290 291 292 291 289 288 288 289 288 289 289 290 288 288 290 289 290 290 291 291 291 291 288 289 287 287 291 291 288 288 288 289 291 294 286 283 282 281 278 282 280 283 282 282 278 279 282 280 283 281 281 278 280 278 279 281 282 282 283 282 282 283 286 285 285 284 284 281 282 282 279 280 283 281 283 282 282 282 279 284 282 284 284 282 282 282 283 282 282 284 285 283 283 282 283 283 283 283 280 282 281 281 283 284 284 286 285 285 286 287 286 286 283 285] PVT_V2[294 291 292 289 289 291 290 290 287 287 290 289 288 290 290 289 288 288 288 288 286 288 286 287 286 286 286 286 286 288 286 286 287 285 290 288 288 289 290 288 288 289 288 289 289 290 288 288 287 287 286 285 290 290 290 288 287 289 290 290 288 289 288 288 288 289 289 289 292 291 288 288 290 289 287 286 284 289 291 294 291 289 285 280 285 285 287 287 283 283 285 285 284 284 281 284 281 284 279 283 279 288 284 283 281 283 281 281 284 284 284 281 283 282 282 284 281 284 288 285 280 284 282 283 285 286 284 285 283 281 281 281 278 281 280 276 284 280 281 284 285 286 286 288 281 281 280 279 285 283 280 280 284 286 283 283 287 286 283 284] MW[238147 238178 238124] MW0[22 19 17 14 17 21 21 22 20 13 25 20 22 22 23 27 22 20 18 26 16 20 16 23 19 22 17 21 16 21 23 18 22 23 25 17 25 15 21 22 24 16 11 23 10 17 21 22 22 16 14 18 23 20 17 22 23 17 24 17 21 21 19 19 17 26 17 22 23 17 25 27 23 20 26 25 19 19 29 27 21 24 16 21 20 10 26 18 17 18 30 21 18 22 22 21 23 23 22 13 24 16 18 27 20 25 23 24 21 23 22 21 25 31 11 24 15 18 21 18 21 17 26 18 15 22 29 20 16 21 17 17 15 18 30 24 25 22 21 14 25 22 28 20 27 16 25 22 13 23 23 20 19 21 22 25 17 16 23 17] MW1[23 21 23 18 13 31 21 25 18 18 21 19 23 17 24 23 22 21 22 23 19 20 22 15 25 27 22 18 16 22 19 24 13 27 20 6 25 20 22 24 17 19 17 27 17 16 30 25 27 29 16 22 23 25 29 20 27 18 22 20 19 22 22 21 23 25 18 27 21 14 20 23 19 28 17 25 19 24 23 22 22 25 26 19 25 28 21 16 19 23 18 15 17 26 20 15 27 21 19 18 25 30 24 20 17 28 29 28 15 20 15 27 20 21 21 30 30 26 17 13 19 15 17 24 24 21 25 19 25 17 24 22 24 10 19 22 23 23 20 23 20 21 25 15 21 17 22 19 19 21 21 22 27 16 18 14 26 24 19 16] MW2[24 16 20 17 22 25 21 28 20 13 14 25 21 25 18 29 17 18 24 17 22 18 23 25 17 29 33 26 30 23 19 24 35 24 14 14 24 28 24 28 27 28 23 26 27 30 21 30 24 20 17 37 12 24 22 33 20 14 21 33 30 16 20 26 19 20 18 24 16 21 15 21 26 22 25 15 19 17 29 21 19 23 20 28 15 25 20 16 25 9 25 22 23 20 16 25 25 20 26 26 22 31 26 24 16 21 34 31 20 32 26 20 13 21 21 11 31 17 17 15 23 22 21 20 22 21 13 21 16 20 28 18 22 29 15 16 19 14 25 24 17 23 16 20 24 18 20 18 18 26 24 21 23 17 16 19 21 25 26 19] CRC[0 0 0] COMCRC[0 0 0] FACOPTS0[] FACOPTS1[] FACOPTS2[] ATAOPTS0[--avalon10-freq 240:258:279:300 --avalon10-voltage-level 1180 --hash-asic 160] ATAOPTS1[--avalon10-freq 300:318:339:360 --avalon10-voltage-level 1188 --hash-asic 160] ATAOPTS2[--avalon10-freq 300:318:339:360 --avalon10-voltage-level 1188 --hash-asic 160 --power-level 0] ADJ[1] COP[0 0 0] MPO[3515] MVL[87] ATABD0[300 318 339 360] ATABD1[300 318 339 360] ATABD2[300 318 339 360] WORKMODE[1]", + "MM Count": 1, + "Smart Speed": 1, + "Voltage Level Offset": 0, + "Nonce Mask": 25, + }, + { + "STATS": 1, + "ID": "POOL0", + "Elapsed": 975, + "Calls": 0, + "Wait": 0.0, + "Max": 0.0, + "Min": 99999999.0, + "Pool Calls": 0, + "Pool Attempts": 0, + "Pool Wait": 0.0, + "Pool Max": 0.0, + "Pool Min": 99999999.0, + "Pool Av": 0.0, + "Work Had Roll Time": False, + "Work Can Roll": False, + "Work Had Expire": False, + "Work Roll Time": 0, + "Work Diff": 262144.0, + "Min Diff": 262144.0, + "Max Diff": 262144.0, + "Min Diff Count": 2, + "Max Diff Count": 2, + "Times Sent": 152, + "Bytes Sent": 26720, + "Times Recv": 181, + "Bytes Recv": 62897, + "Net Bytes Sent": 26720, + "Net Bytes Recv": 62897, + }, + { + "STATS": 2, + "ID": "POOL1", + "Elapsed": 975, + "Calls": 0, + "Wait": 0.0, + "Max": 0.0, + "Min": 99999999.0, + "Pool Calls": 0, + "Pool Attempts": 0, + "Pool Wait": 0.0, + "Pool Max": 0.0, + "Pool Min": 99999999.0, + "Pool Av": 0.0, + "Work Had Roll Time": False, + "Work Can Roll": False, + "Work Had Expire": False, + "Work Roll Time": 0, + "Work Diff": 0.0, + "Min Diff": 0.0, + "Max Diff": 0.0, + "Min Diff Count": 0, + "Max Diff Count": 0, + "Times Sent": 3, + "Bytes Sent": 289, + "Times Recv": 7, + "Bytes Recv": 4954, + "Net Bytes Sent": 289, + "Net Bytes Recv": 4954, + }, + { + "STATS": 3, + "ID": "POOL2", + "Elapsed": 975, + "Calls": 0, + "Wait": 0.0, + "Max": 0.0, + "Min": 99999999.0, + "Pool Calls": 0, + "Pool Attempts": 0, + "Pool Wait": 0.0, + "Pool Max": 0.0, + "Pool Min": 99999999.0, + "Pool Av": 0.0, + "Work Had Roll Time": False, + "Work Can Roll": False, + "Work Had Expire": False, + "Work Roll Time": 0, + "Work Diff": 16384.0, + "Min Diff": 16384.0, + "Max Diff": 16384.0, + "Min Diff Count": 1, + "Max Diff Count": 1, + "Times Sent": 3, + "Bytes Sent": 257, + "Times Recv": 6, + "Bytes Recv": 1583, + "Net Bytes Sent": 257, + "Net Bytes Recv": 1583, + }, + ], + "id": 1, + }, } }