From 03f2a1f9ba87b8978bcdc7fe066b7fe481d0dbdf Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 24 Jul 2023 11:34:16 -0600 Subject: [PATCH] feature: optimize multicommand on new X19 models. --- pyasic/API/bfgminer.py | 26 ++++----- pyasic/API/bmminer.py | 27 +++++---- pyasic/API/cgminer.py | 26 ++++----- pyasic/data/__init__.py | 10 +++- pyasic/miners/backends/antminer.py | 90 ++++++++++++++++++++---------- pyasic/miners/backends/bosminer.py | 4 +- pyasic/miners/base.py | 29 ++++++++-- pyasic/web/antminer.py | 45 +++++++++------ 8 files changed, 161 insertions(+), 96 deletions(-) diff --git a/pyasic/API/bfgminer.py b/pyasic/API/bfgminer.py index 9f4a8d3f..77dc0505 100644 --- a/pyasic/API/bfgminer.py +++ b/pyasic/API/bfgminer.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ - +import asyncio import logging from pyasic.API import APIError, BaseMinerAPI @@ -56,19 +56,19 @@ class BFGMinerAPI(BaseMinerAPI): return data async def _x19_multicommand(self, *commands) -> dict: - data = None - try: - data = {} - # send all commands individually - for cmd in commands: - data[cmd] = [] - data[cmd].append(await self.send_command(cmd, allow_warning=True)) - except APIError: - pass - except Exception as e: - logging.warning( - f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}" + tasks = [] + # send all commands individually + for cmd in commands: + tasks.append( + asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True)) ) + + all_data = await asyncio.gather(*tasks) + + data = {} + for item in all_data: + data.update(item) + return data async def version(self) -> dict: diff --git a/pyasic/API/bmminer.py b/pyasic/API/bmminer.py index cd9ff5d8..c04b210c 100644 --- a/pyasic/API/bmminer.py +++ b/pyasic/API/bmminer.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ +import asyncio import logging from pyasic.API import APIError, BaseMinerAPI @@ -57,21 +58,19 @@ class BMMinerAPI(BaseMinerAPI): return data async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict: - data = None - try: - data = {} - # send all commands individually - for cmd in commands: - data[cmd] = [] - data[cmd].append( - await self.send_command(cmd, allow_warning=allow_warning) - ) - except APIError: - pass - except Exception as e: - logging.warning( - f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}" + tasks = [] + # send all commands individually + for cmd in commands: + tasks.append( + asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True)) ) + + all_data = await asyncio.gather(*tasks) + + data = {} + for item in all_data: + data.update(item) + return data async def version(self) -> dict: diff --git a/pyasic/API/cgminer.py b/pyasic/API/cgminer.py index 23670eec..179e670f 100644 --- a/pyasic/API/cgminer.py +++ b/pyasic/API/cgminer.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ - +import asyncio import logging from pyasic.API import APIError, BaseMinerAPI @@ -56,19 +56,19 @@ class CGMinerAPI(BaseMinerAPI): return data async def _x19_multicommand(self, *commands) -> dict: - data = None - try: - data = {} - # send all commands individually - for cmd in commands: - data[cmd] = [] - data[cmd].append(await self.send_command(cmd, allow_warning=True)) - except APIError: - pass - except Exception as e: - logging.warning( - f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}" + tasks = [] + # send all commands individually + for cmd in commands: + tasks.append( + asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True)) ) + + all_data = await asyncio.gather(*tasks) + + data = {} + for item in all_data: + data.update(item) + return data async def version(self) -> dict: diff --git a/pyasic/data/__init__.py b/pyasic/data/__init__.py index 087537d3..e9f26835 100644 --- a/pyasic/data/__init__.py +++ b/pyasic/data/__init__.py @@ -20,7 +20,7 @@ import logging import time from dataclasses import asdict, dataclass, field, fields from datetime import datetime, timezone -from typing import List, Union, Any +from typing import Any, List, Union from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error @@ -411,8 +411,12 @@ class MinerData: field_data.append(f'error_{idx+1}="{item.error_message}"') elif attribute == "hashboards": for idx, item in enumerate(self[attribute]): - field_data.append(f"hashboard_{idx+1}_hashrate={item.get('hashrate', 0.0)}") - field_data.append(f"hashboard_{idx+1}_temperature={item.get('temp', 0)}") + field_data.append( + f"hashboard_{idx+1}_hashrate={item.get('hashrate', 0.0)}" + ) + field_data.append( + f"hashboard_{idx+1}_temperature={item.get('temp', 0)}" + ) field_data.append( f"hashboard_{idx+1}_chip_temperature={item.get('chip_temp', 0)}" ) diff --git a/pyasic/miners/backends/antminer.py b/pyasic/miners/backends/antminer.py index 6f57eeca..d1621203 100644 --- a/pyasic/miners/backends/antminer.py +++ b/pyasic/miners/backends/antminer.py @@ -26,11 +26,17 @@ from pyasic.miners.backends.cgminer import CGMiner from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI ANTMINER_MODERN_DATA_LOC = { - "mac": {"cmd": "get_mac", "kwargs": {}}, + "mac": { + "cmd": "get_mac", + "kwargs": {"web_get_system_info": {"web": "get_system_info"}}, + }, "model": {"cmd": "get_model", "kwargs": {}}, "api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}}, "fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}}, - "hostname": {"cmd": "get_hostname", "kwargs": {}}, + "hostname": { + "cmd": "get_hostname", + "kwargs": {"web_get_system_info": {"web": "get_system_info"}}, + }, "hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}}, "nominal_hashrate": { "cmd": "get_nominal_hashrate", @@ -42,8 +48,11 @@ ANTMINER_MODERN_DATA_LOC = { "wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}}, "fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}}, "fan_psu": {"cmd": "get_fan_psu", "kwargs": {}}, - "errors": {"cmd": "get_errors", "kwargs": {}}, - "fault_light": {"cmd": "get_fault_light", "kwargs": {}}, + "errors": {"cmd": "get_errors", "kwargs": {"web_summary": {"web": "summary"}}}, + "fault_light": { + "cmd": "get_fault_light", + "kwargs": {"web_get_blink_status": {"web": "get_blink_status"}}, + }, "pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}}, "is_mining": { "cmd": "is_mining", @@ -121,21 +130,31 @@ class AntminerModern(BMMiner): await self.send_config(cfg) return True - async def get_hostname(self) -> Union[str, None]: - try: - data = await self.web.get_system_info() - if data: - return data["hostname"] - except KeyError: - pass + async def get_hostname(self, web_get_system_info: dict = None) -> Union[str, None]: + if not web_get_system_info: + try: + web_get_system_info = await self.web.get_system_info() + except APIError: + pass - async def get_mac(self) -> Union[str, None]: - try: - data = await self.web.get_system_info() - if data: - return data["macaddr"] - except KeyError: - pass + if web_get_system_info: + try: + return web_get_system_info["hostname"] + except KeyError: + pass + + async def get_mac(self, web_get_system_info: dict = None) -> Union[str, None]: + if not web_get_system_info: + try: + web_get_system_info = await self.web.get_system_info() + except APIError: + pass + + if web_get_system_info: + try: + return web_get_system_info["macaddr"] + except KeyError: + pass try: data = await self.web.get_network_info() @@ -144,12 +163,17 @@ class AntminerModern(BMMiner): except KeyError: pass - async def get_errors(self) -> List[MinerErrorData]: - errors = [] - data = await self.web.summary() - if data: + async def get_errors(self, web_summary: dict = None) -> List[MinerErrorData]: + if not web_summary: try: - for item in data["SUMMARY"][0]["status"]: + web_summary = await self.web.summary() + except APIError: + pass + + errors = [] + if web_summary: + try: + for item in web_summary["SUMMARY"][0]["status"]: try: if not item["status"] == "s": errors.append(X19Error(item["msg"])) @@ -159,15 +183,21 @@ class AntminerModern(BMMiner): pass return errors - async def get_fault_light(self) -> bool: + async def get_fault_light(self, web_get_blink_status: dict = None) -> bool: if self.light: return self.light - try: - data = await self.web.get_blink_status() - if data: - self.light = data["blink"] - except KeyError: - pass + + if not web_get_blink_status: + try: + web_get_blink_status = await self.web.get_blink_status() + except APIError: + pass + + if web_get_blink_status: + try: + self.light = web_get_blink_status["blink"] + except KeyError: + pass return self.light async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]: diff --git a/pyasic/miners/backends/bosminer.py b/pyasic/miners/backends/bosminer.py index 7fb1aa75..95a16fa5 100644 --- a/pyasic/miners/backends/bosminer.py +++ b/pyasic/miners/backends/bosminer.py @@ -1078,7 +1078,9 @@ class BOSMiner(BaseMiner): 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) + api_devdetails = await self.api.send_command( + "devdetails", ignore_errors=True, allow_warning=False + ) except APIError: pass diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index 84144c89..882e6bb4 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ - +import asyncio import ipaddress import logging from abc import ABC, abstractmethod @@ -430,17 +430,34 @@ class BaseMiner(ABC): continue if len(api_multicommand) > 0: - api_command_data = await self.api.multicommand( - *api_multicommand, allow_warning=allow_warning + api_command_task = asyncio.create_task( + self.api.multicommand(*api_multicommand, allow_warning=allow_warning) ) else: - api_command_data = {} + api_command_task = asyncio.sleep(0) if len(web_multicommand) > 0: - web_command_data = await self.web.multicommand( - *web_multicommand, allow_warning=allow_warning + web_command_task = asyncio.create_task( + self.web.multicommand(*web_multicommand, allow_warning=allow_warning) ) else: + web_command_task = asyncio.sleep(0) + + import pprint + from datetime import datetime + + s1 = datetime.now() + web_command_data = await web_command_task + if web_command_data is None: web_command_data = {} + print("WEB", datetime.now() - s1) + # pprint.pprint(web_command_data) + + s2 = datetime.now() + api_command_data = await api_command_task + if api_command_data is None: + api_command_data = {} + print("API:", datetime.now() - s2) + # pprint.pprint(api_command_data) miner_data = {} diff --git a/pyasic/web/antminer.py b/pyasic/web/antminer.py index 47087b5a..364e3ae4 100644 --- a/pyasic/web/antminer.py +++ b/pyasic/web/antminer.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ +import asyncio import json from typing import Union @@ -56,25 +57,37 @@ class AntminerModernWebAPI(BaseWebAPI): async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True ) -> dict: - data = {k: None for k in commands} - data["multicommand"] = True - auth = httpx.DigestAuth(self.username, self.pwd) async with httpx.AsyncClient() as client: - for command in commands: - try: - url = f"http://{self.ip}/cgi-bin/{command}.cgi" - ret = await client.get(url, auth=auth) - except httpx.HTTPError: - pass - else: - if ret.status_code == 200: - try: - json_data = ret.json() - data[command] = json_data - except json.decoder.JSONDecodeError: - pass + tasks = [ + asyncio.create_task(self._handle_multicommand(client, command)) + for command in commands + ] + all_data = await asyncio.gather(*tasks) + + data = {} + for item in all_data: + data.update(item) + + data["multicommand"] = True return data + async def _handle_multicommand(self, client: httpx.AsyncClient, command: str): + auth = httpx.DigestAuth(self.username, self.pwd) + + try: + url = f"http://{self.ip}/cgi-bin/{command}.cgi" + ret = await client.get(url, auth=auth) + except httpx.HTTPError: + pass + else: + if ret.status_code == 200: + try: + json_data = ret.json() + return {command: json_data} + except json.decoder.JSONDecodeError: + pass + return {command: {}} + async def get_miner_conf(self) -> dict: return await self.send_command("get_miner_conf")