diff --git a/pyasic/device/makes.py b/pyasic/device/makes.py index 17400f42..8e2acdb7 100644 --- a/pyasic/device/makes.py +++ b/pyasic/device/makes.py @@ -27,6 +27,7 @@ class MinerMake(str, Enum): EPIC = "ePIC" BITAXE = "BitAxe" ICERIVER = "IceRiver" + HAMMER = "Hammer" def __str__(self): return self.value diff --git a/pyasic/device/models.py b/pyasic/device/models.py index c3522097..82f6e272 100644 --- a/pyasic/device/models.py +++ b/pyasic/device/models.py @@ -364,6 +364,13 @@ class IceRiverModels(str, Enum): return self.value +class HammerModels(str, Enum): + D10 = "D10" + + def __str__(self): + return self.value + + class MinerModel: ANTMINER = AntminerModels WHATSMINER = WhatsminerModels @@ -374,3 +381,4 @@ class MinerModel: EPIC = ePICModels BITAXE = BitAxeModels ICERIVER = IceRiverModels + HAMMER = HammerModels diff --git a/pyasic/miners/backends/__init__.py b/pyasic/miners/backends/__init__.py index a510d001..65fe06eb 100644 --- a/pyasic/miners/backends/__init__.py +++ b/pyasic/miners/backends/__init__.py @@ -17,16 +17,19 @@ from .antminer import AntminerModern, AntminerOld from .auradine import Auradine from .avalonminer import AvalonMiner from .bfgminer import BFGMiner +from .bitaxe import BitAxe from .bmminer import BMMiner from .braiins_os import BOSer, BOSMiner from .btminer import BTMiner from .cgminer import CGMiner from .epic import ePIC from .goldshell import GoldshellMiner +from .hammer import BlackMiner from .hiveon import Hiveon from .iceriver import IceRiver from .innosilicon import Innosilicon from .luxminer import LUXMiner from .marathon import MaraMiner +from .unknown import UnknownMiner from .vnish import VNish from .whatsminer import M2X, M3X, M5X, M6X diff --git a/pyasic/miners/backends/hammer.py b/pyasic/miners/backends/hammer.py index a30df463..e3d73bdc 100644 --- a/pyasic/miners/backends/hammer.py +++ b/pyasic/miners/backends/hammer.py @@ -17,11 +17,10 @@ from typing import List, Optional from pyasic import MinerConfig -from pyasic.data import AlgoHashRate, HashBoard, HashUnit +from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit from pyasic.data.error_codes import MinerErrorData, X19Error from pyasic.data.pools import PoolMetrics, PoolUrl from pyasic.errors import APIError -from pyasic.miners.base import BaseMiner from pyasic.miners.data import ( DataFunction, DataLocations, @@ -29,6 +28,7 @@ from pyasic.miners.data import ( RPCAPICommand, WebAPICommand, ) +from pyasic.miners.device.firmware import StockFirmware from pyasic.rpc.ccminer import CCMinerRPCAPI from pyasic.web.hammer import HammerWebAPI @@ -50,6 +50,10 @@ HAMMER_DATA_LOC = DataLocations( "_get_hostname", [WebAPICommand("web_get_system_info", "get_system_info")], ), + str(DataOptions.HASHBOARDS): DataFunction( + "_get_hashboards", + [RPCAPICommand("rpc_stats", "stats")], + ), str(DataOptions.HASHRATE): DataFunction( "_get_hashrate", [RPCAPICommand("rpc_summary", "summary")], @@ -86,7 +90,7 @@ HAMMER_DATA_LOC = DataLocations( ) -class BlackMiner(BaseMiner): +class BlackMiner(StockFirmware): """Handler for Hammer miners.""" _rpc_cls = CCMinerRPCAPI @@ -127,6 +131,186 @@ class BlackMiner(BaseMiner): return True return False + async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]: + if rpc_version is None: + try: + rpc_version = await self.rpc.version() + except APIError: + pass + + if rpc_version is not None: + try: + self.api_ver = rpc_version["VERSION"][0]["API"] + except LookupError: + pass + + return self.api_ver + + async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: + if rpc_version is None: + try: + rpc_version = await self.rpc.version() + except APIError: + pass + + if rpc_version is not None: + try: + self.fw_ver = rpc_version["VERSION"][0]["CompileTime"] + except LookupError: + pass + + return self.fw_ver + + async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + # get hr from API + if rpc_summary is None: + try: + rpc_summary = await self.rpc.summary() + except APIError: + pass + + if rpc_summary is not None: + try: + return AlgoHashRate.SHA256( + rpc_summary["SUMMARY"][0]["GHS 5s"], HashUnit.SHA256.GH + ).into(self.algo.unit.default) + except (LookupError, ValueError, TypeError): + pass + + async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: + hashboards = [] + + if rpc_stats is None: + try: + rpc_stats = await self.rpc.stats() + except APIError: + pass + + if rpc_stats is not None: + try: + board_offset = -1 + boards = rpc_stats["STATS"] + if len(boards) > 1: + for board_num in range(1, 16, 5): + for _b_num in range(5): + b = boards[1].get(f"chain_acn{board_num + _b_num}") + + if b and not b == 0 and board_offset == -1: + board_offset = board_num + if board_offset == -1: + board_offset = 1 + + real_slots = [] + + for i in range(board_offset, board_offset + 4): + try: + key = f"chain_acs{i}" + if boards[1].get(key, "") != "": + real_slots.append(i) + except LookupError: + pass + + if len(real_slots) < 3: + real_slots = list( + range(board_offset, board_offset + self.expected_hashboards) + ) + + for i in real_slots: + hashboard = HashBoard( + slot=i - board_offset, expected_chips=self.expected_chips + ) + + chip_temp = boards[1].get(f"temp{i}") + if chip_temp: + hashboard.chip_temp = round(chip_temp) + + temp = boards[1].get(f"temp2_{i}") + if temp: + hashboard.temp = round(temp) + + hashrate = boards[1].get(f"chain_rate{i}") + if hashrate: + hashboard.hashrate = AlgoHashRate.SHA256( + hashrate, HashUnit.SHA256.GH + ).into(self.algo.unit.default) + + chips = boards[1].get(f"chain_acn{i}") + if chips: + hashboard.chips = chips + hashboard.missing = False + if (not chips) or (not chips > 0): + hashboard.missing = True + hashboards.append(hashboard) + except (LookupError, ValueError, TypeError): + pass + + return hashboards + + async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]: + if rpc_stats is None: + try: + rpc_stats = await self.rpc.stats() + except APIError: + pass + + fans = [Fan() for _ in range(self.expected_fans)] + if rpc_stats is not None: + try: + fan_offset = -1 + + for fan_num in range(1, 8, 4): + for _f_num in range(4): + f = rpc_stats["STATS"][1].get(f"fan{fan_num + _f_num}", 0) + if f and not f == 0 and fan_offset == -1: + fan_offset = fan_num + if fan_offset == -1: + fan_offset = 1 + + for fan in range(self.expected_fans): + fans[fan].speed = rpc_stats["STATS"][1].get( + f"fan{fan_offset+fan}", 0 + ) + except LookupError: + pass + + return fans + + async def _get_expected_hashrate( + self, rpc_stats: dict = None + ) -> Optional[AlgoHashRate]: + # X19 method, not sure compatibility + if rpc_stats is None: + try: + rpc_stats = await self.rpc.stats() + except APIError: + pass + + if rpc_stats is not None: + try: + expected_rate = rpc_stats["STATS"][1]["total_rateideal"] + try: + rate_unit = rpc_stats["STATS"][1]["rate_unit"] + except KeyError: + rate_unit = "GH" + return AlgoHashRate.SHA256( + expected_rate, HashUnit.SHA256.from_str(rate_unit) + ).into(self.algo.unit.default) + except LookupError: + pass + + async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: + if rpc_stats is None: + try: + rpc_stats = await self.rpc.stats() + except APIError: + pass + + if rpc_stats is not None: + try: + return int(rpc_stats["STATS"][1]["Elapsed"]) + except LookupError: + pass + async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: if web_get_system_info is None: try: @@ -180,42 +364,6 @@ class BlackMiner(BaseMiner): pass return errors - async def _get_hashboards(self) -> List[HashBoard]: - hashboards = [ - HashBoard(idx, expected_chips=self.expected_chips) - for idx in range(self.expected_hashboards) - ] - - try: - rpc_stats = await self.web.stats(new_api=True) - except APIError: - return hashboards - - if rpc_stats is not None: - try: - for board in rpc_stats["STATS"][0]["chain"]: - hashboards[board["index"]].hashrate = AlgoHashRate.SHA256( - board["rate_real"], HashUnit.SHA256.GH - ).into(self.algo.unit.default) - hashboards[board["index"]].chips = board["asic_num"] - board_temp_data = list( - filter(lambda x: not x == 0, board["temp_pcb"]) - ) - hashboards[board["index"]].temp = sum(board_temp_data) / len( - board_temp_data - ) - chip_temp_data = list( - filter(lambda x: not x == 0, board["temp_chip"]) - ) - hashboards[board["index"]].chip_temp = sum(chip_temp_data) / len( - chip_temp_data - ) - hashboards[board["index"]].serial_number = board["sn"] - hashboards[board["index"]].missing = False - except LookupError: - pass - return hashboards - async def _get_fault_light( self, web_get_blink_status: dict = None ) -> Optional[bool]: diff --git a/pyasic/miners/device/makes.py b/pyasic/miners/device/makes.py index 4e4f433e..3da1a5b6 100644 --- a/pyasic/miners/device/makes.py +++ b/pyasic/miners/device/makes.py @@ -52,3 +52,7 @@ class BitAxeMake(BaseMiner): class IceRiverMake(BaseMiner): make = MinerMake.ICERIVER + + +class HammerMake(BaseMiner): + make = MinerMake.HAMMER diff --git a/pyasic/miners/device/models/__init__.py b/pyasic/miners/device/models/__init__.py index 666a9c8f..930503cc 100644 --- a/pyasic/miners/device/models/__init__.py +++ b/pyasic/miners/device/models/__init__.py @@ -19,6 +19,7 @@ from .auradine import * from .avalonminer import * from .epic import * from .goldshell import * +from .hammer import * from .iceriver import * from .innosilicon import * from .whatsminer import * diff --git a/pyasic/miners/device/models/hammer/DX/D10.py b/pyasic/miners/device/models/hammer/DX/D10.py new file mode 100644 index 00000000..3fffff99 --- /dev/null +++ b/pyasic/miners/device/models/hammer/DX/D10.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------------ +# Copyright 2024 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.models import MinerModel +from pyasic.miners.device.makes import HammerMake + + +class D10(HammerMake): + raw_model = MinerModel.HAMMER.D10 diff --git a/pyasic/miners/device/models/hammer/DX/__init__.py b/pyasic/miners/device/models/hammer/DX/__init__.py new file mode 100644 index 00000000..858e19b3 --- /dev/null +++ b/pyasic/miners/device/models/hammer/DX/__init__.py @@ -0,0 +1 @@ +from .D10 import D10 diff --git a/pyasic/miners/device/models/hammer/__init__.py b/pyasic/miners/device/models/hammer/__init__.py new file mode 100644 index 00000000..d9f82972 --- /dev/null +++ b/pyasic/miners/device/models/hammer/__init__.py @@ -0,0 +1 @@ +from .DX import * diff --git a/pyasic/miners/factory.py b/pyasic/miners/factory.py index 3c4af9a1..30cff470 100644 --- a/pyasic/miners/factory.py +++ b/pyasic/miners/factory.py @@ -32,13 +32,12 @@ from pyasic.miners.antminer import * from pyasic.miners.auradine import * from pyasic.miners.avalonminer import * from pyasic.miners.backends import * -from pyasic.miners.backends.bitaxe import BitAxe -from pyasic.miners.backends.unknown import UnknownMiner from pyasic.miners.base import AnyMiner from pyasic.miners.bitaxe import * from pyasic.miners.blockminer import * from pyasic.miners.device.makes import * from pyasic.miners.goldshell import * +from pyasic.miners.hammer import * from pyasic.miners.iceriver import * from pyasic.miners.innosilicon import * from pyasic.miners.whatsminer import * @@ -59,6 +58,7 @@ class MinerTypes(enum.Enum): MARATHON = 11 BITAXE = 12 ICERIVER = 13 + HAMMER = 14 MINER_CLASSES = { @@ -478,6 +478,10 @@ MINER_CLASSES = { "KS5L": IceRiverKS5L, "KS5M": IceRiverKS5M, }, + MinerTypes.HAMMER: { + None: type("HammerUnknown", (BlackMiner, HammerMake), {}), + "HAMMER D10": HammerD10, + }, } @@ -627,6 +631,10 @@ class MinerFactory: "www-authenticate", "" ): return MinerTypes.ANTMINER + if web_resp.status_code == 401 and 'realm="blackMiner' in web_resp.headers.get( + "www-authenticate", "" + ): + return MinerTypes.HAMMER if len(web_resp.history) > 0: history_resp = web_resp.history[0] if ( @@ -1130,6 +1138,21 @@ class MinerFactory: except httpx.HTTPError: pass + async def get_miner_model_hammer(self, ip: str) -> str | None: + auth = httpx.DigestAuth( + "root", settings.get("default_hammer_web_password", "root") + ) + web_json_data = await self.send_web_command( + ip, "/cgi-bin/get_system_info.cgi", auth=auth + ) + + try: + miner_model = web_json_data["minertype"] + + return miner_model + except (TypeError, LookupError): + pass + miner_factory = MinerFactory() diff --git a/pyasic/miners/hammer/__init__.py b/pyasic/miners/hammer/__init__.py new file mode 100644 index 00000000..d78a4d0e --- /dev/null +++ b/pyasic/miners/hammer/__init__.py @@ -0,0 +1 @@ +from .blackminer import * diff --git a/pyasic/miners/hammer/blackminer/DX/D10.py b/pyasic/miners/hammer/blackminer/DX/D10.py new file mode 100644 index 00000000..39b6aaaf --- /dev/null +++ b/pyasic/miners/hammer/blackminer/DX/D10.py @@ -0,0 +1,6 @@ +from pyasic.miners.backends import BlackMiner +from pyasic.miners.device.models import D10 + + +class HammerD10(BlackMiner, D10): + pass diff --git a/pyasic/miners/hammer/blackminer/DX/__init__.py b/pyasic/miners/hammer/blackminer/DX/__init__.py new file mode 100644 index 00000000..ad8f43f8 --- /dev/null +++ b/pyasic/miners/hammer/blackminer/DX/__init__.py @@ -0,0 +1 @@ +from .D10 import HammerD10 diff --git a/pyasic/miners/hammer/blackminer/__init__.py b/pyasic/miners/hammer/blackminer/__init__.py new file mode 100644 index 00000000..d9f82972 --- /dev/null +++ b/pyasic/miners/hammer/blackminer/__init__.py @@ -0,0 +1 @@ +from .DX import *