From f63d8f4b91546145268891a801122788339da673 Mon Sep 17 00:00:00 2001 From: Upstream Data Date: Fri, 12 Apr 2024 09:55:45 -0600 Subject: [PATCH] feature: add really basic MaraFW support. --- pyasic/miners/backends/__init__.py | 1 + pyasic/miners/backends/marathon.py | 87 +++++++++++++++++++ pyasic/miners/factory.py | 38 ++++++--- pyasic/web/marathon.py | 131 +++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 pyasic/miners/backends/marathon.py create mode 100644 pyasic/web/marathon.py diff --git a/pyasic/miners/backends/__init__.py b/pyasic/miners/backends/__init__.py index 97121635..283a598c 100644 --- a/pyasic/miners/backends/__init__.py +++ b/pyasic/miners/backends/__init__.py @@ -26,5 +26,6 @@ from .goldshell import GoldshellMiner from .hiveon import Hiveon from .innosilicon import Innosilicon from .luxminer import LUXMiner +from .marathon import MaraMiner from .vnish import VNish from .whatsminer import M2X, M3X, M5X, M6X diff --git a/pyasic/miners/backends/marathon.py b/pyasic/miners/backends/marathon.py new file mode 100644 index 00000000..84c3c8f9 --- /dev/null +++ b/pyasic/miners/backends/marathon.py @@ -0,0 +1,87 @@ +from typing import Optional + +from pyasic.errors import APIError +from pyasic.miners.backends import AntminerModern +from pyasic.miners.data import ( + DataFunction, + DataLocations, + DataOptions, + RPCAPICommand, + WebAPICommand, +) +from pyasic.web.marathon import MaraWebAPI + +MARA_DATA_LOC = DataLocations( + **{ + str(DataOptions.MAC): DataFunction( + "_get_mac", + [WebAPICommand("web_get_system_info", "get_system_info")], + ), + str(DataOptions.API_VERSION): DataFunction( + "_get_api_ver", + [RPCAPICommand("rpc_version", "version")], + ), + str(DataOptions.FW_VERSION): DataFunction( + "_get_fw_ver", + [RPCAPICommand("rpc_version", "version")], + ), + str(DataOptions.HOSTNAME): DataFunction( + "_get_hostname", + [WebAPICommand("web_get_system_info", "get_system_info")], + ), + str(DataOptions.HASHRATE): DataFunction( + "_get_hashrate", + [RPCAPICommand("rpc_summary", "summary")], + ), + str(DataOptions.EXPECTED_HASHRATE): DataFunction( + "_get_expected_hashrate", + [RPCAPICommand("rpc_stats", "stats")], + ), + str(DataOptions.FANS): DataFunction( + "_get_fans", + [RPCAPICommand("rpc_stats", "stats")], + ), + str(DataOptions.ERRORS): DataFunction( + "_get_errors", + [WebAPICommand("web_summary", "summary")], + ), + str(DataOptions.FAULT_LIGHT): DataFunction( + "_get_fault_light", + [WebAPICommand("web_get_blink_status", "get_blink_status")], + ), + str(DataOptions.IS_MINING): DataFunction( + "_is_mining", + [WebAPICommand("web_get_conf", "get_miner_conf")], + ), + str(DataOptions.UPTIME): DataFunction( + "_get_uptime", + [RPCAPICommand("rpc_stats", "stats")], + ), + str(DataOptions.WATTAGE): DataFunction( + "_get_wattage", + [WebAPICommand("web_brief", "brief")], + ), + } +) + + +class MaraMiner(AntminerModern): + _web_cls = MaraWebAPI + web: MaraWebAPI + + data_locations = MARA_DATA_LOC + + firmware = "MaraFW" + + async def _get_wattage(self, web_brief: dict = None) -> Optional[int]: + if web_brief is None: + try: + web_brief = await self.web.brief() + except APIError: + pass + + if web_brief is not None: + try: + return web_brief["power_consumption_estimated"] + except LookupError: + pass diff --git a/pyasic/miners/factory.py b/pyasic/miners/factory.py index 3de449f1..c285dd2f 100644 --- a/pyasic/miners/factory.py +++ b/pyasic/miners/factory.py @@ -30,19 +30,7 @@ from pyasic.logger import logger from pyasic.miners.antminer import * from pyasic.miners.auradine import * from pyasic.miners.avalonminer import * -from pyasic.miners.backends import ( - Auradine, - AvalonMiner, - BMMiner, - BOSMiner, - BTMiner, - GoldshellMiner, - Hiveon, - Innosilicon, - LUXMiner, - VNish, - ePIC, -) +from pyasic.miners.backends import * from pyasic.miners.backends.unknown import UnknownMiner from pyasic.miners.base import AnyMiner from pyasic.miners.blockminer import * @@ -64,6 +52,7 @@ class MinerTypes(enum.Enum): LUX_OS = 8 EPIC = 9 AURADINE = 10 + MARATHON = 11 MINER_CLASSES = { @@ -424,7 +413,7 @@ MINER_CLASSES = { "ANTMINER S21": LUXMinerS21, }, MinerTypes.AURADINE: { - None: type("GoldshellUnknown", (Auradine, AuradineMake), {}), + None: type("AuradineUnknown", (Auradine, AuradineMake), {}), "AT1500": AuradineFluxAT1500, "AT2860": AuradineFluxAT2860, "AT2880": AuradineFluxAT2880, @@ -433,6 +422,9 @@ MINER_CLASSES = { "AD2500": AuradineFluxAD2500, "AD3500": AuradineFluxAD3500, }, + MinerTypes.MARATHON: { + None: MaraMiner, + }, } @@ -508,6 +500,7 @@ class MinerFactory: MinerTypes.HIVEON: self.get_miner_model_hiveon, MinerTypes.LUX_OS: self.get_miner_model_luxos, MinerTypes.AURADINE: self.get_miner_model_auradine, + MinerTypes.MARATHON: self.get_miner_model_marathon, } fn = miner_model_fns.get(miner_type) @@ -689,6 +682,8 @@ class MinerFactory: return MinerTypes.HIVEON if "LUXMINER" in upper_data: return MinerTypes.LUX_OS + if "KAONSU" in upper_data: + return MinerTypes.MARATHON if "ANTMINER" in upper_data and "DEVDETAILS" not in upper_data: return MinerTypes.ANTMINER if ( @@ -1001,6 +996,21 @@ class MinerFactory: except LookupError: pass + async def get_miner_model_marathon(self, ip: str) -> str | None: + auth = httpx.DigestAuth("root", "root") + web_json_data = await self.send_web_command( + ip, "/kaonsu/v1/overview", auth=auth + ) + + try: + miner_model = web_json_data["model"] + if miner_model == "": + return None + + return miner_model + except (TypeError, LookupError): + pass + miner_factory = MinerFactory() diff --git a/pyasic/web/marathon.py b/pyasic/web/marathon.py new file mode 100644 index 00000000..bccd7176 --- /dev/null +++ b/pyasic/web/marathon.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import json +from typing import Any + +import httpx + +from pyasic import settings +from pyasic.web.antminer import AntminerModernWebAPI + + +class MaraWebAPI(AntminerModernWebAPI): + def __init__(self, ip: str) -> None: + self.am_commands = [ + "get_miner_conf", + "set_miner_conf", + "blink", + "reboot", + "get_system_info", + "get_network_info", + "summary", + "get_blink_status", + "set_network_conf", + ] + super().__init__(ip) + + async def _send_mara_command( + self, + command: str | bytes, + ignore_errors: bool = False, + allow_warning: bool = True, + privileged: bool = False, + **parameters: Any, + ) -> dict: + url = f"http://{self.ip}:{self.port}/kaonsu/v1/{command}" + auth = httpx.DigestAuth(self.username, self.pwd) + try: + async with httpx.AsyncClient( + transport=settings.transport(), + ) as client: + if parameters: + data = await client.post( + url, + auth=auth, + timeout=settings.get("api_function_timeout", 3), + json=parameters, + ) + else: + data = await client.get(url, auth=auth) + except httpx.HTTPError: + pass + else: + if data.status_code == 200: + try: + return data.json() + except json.decoder.JSONDecodeError: + pass + + async def _send_am_command( + self, + command: str | bytes, + ignore_errors: bool = False, + allow_warning: bool = True, + privileged: bool = False, + **parameters: Any, + ): + 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: + if parameters: + data = await client.post( + url, + auth=auth, + timeout=settings.get("api_function_timeout", 3), + json=parameters, + ) + else: + data = await client.get(url, auth=auth) + except httpx.HTTPError: + pass + else: + if data.status_code == 200: + try: + return data.json() + except json.decoder.JSONDecodeError: + pass + + async def send_command( + self, + command: str | bytes, + ignore_errors: bool = False, + allow_warning: bool = True, + privileged: bool = False, + **parameters: Any, + ) -> dict: + if command in self.am_commands: + return await self._send_am_command( + command, + ignore_errors=ignore_errors, + allow_warning=allow_warning, + privileged=privileged, + **parameters, + ) + return await self._send_mara_command( + command, + ignore_errors=ignore_errors, + allow_warning=allow_warning, + privileged=privileged, + **parameters, + ) + + async def brief(self): + return await self.send_command("brief") + + async def overview(self): + return await self.send_command("overview") + + async def connections(self): + return await self.send_command("connections") + + async def event_chart(self): + return await self.send_command("event_chart") + + async def hashboards(self): + return await self.send_command("hashboards") + + async def mara_pools(self): + return await self._send_mara_command("pools")