From 64774d2017cad61dfe798711836caaac867f02e2 Mon Sep 17 00:00:00 2001 From: b-rowan Date: Tue, 23 Jan 2024 14:06:54 -0700 Subject: [PATCH] feature: add basic auradine miner framework. --- pyasic/miners/backends/auradine.py | 32 ++++ pyasic/miners/miner_factory.py | 21 +++ pyasic/rpc/btminer.py | 2 +- pyasic/rpc/gcminer.py | 182 ++++++++++++++++++++ pyasic/settings/__init__.py | 2 + pyasic/web/antminer.py | 4 +- pyasic/web/auradine.py | 259 +++++++++++++++++++++++++++++ pyasic/web/braiins_os/__init__.py | 2 +- pyasic/web/epic.py | 2 +- pyasic/web/goldshell.py | 4 +- pyasic/web/innosilicon.py | 4 +- pyasic/web/vnish.py | 4 +- 12 files changed, 507 insertions(+), 11 deletions(-) create mode 100644 pyasic/miners/backends/auradine.py create mode 100644 pyasic/rpc/gcminer.py create mode 100644 pyasic/web/auradine.py diff --git a/pyasic/miners/backends/auradine.py b/pyasic/miners/backends/auradine.py new file mode 100644 index 00000000..54d8ced9 --- /dev/null +++ b/pyasic/miners/backends/auradine.py @@ -0,0 +1,32 @@ +# ------------------------------------------------------------------------------ +# 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.miners.base import BaseMiner, DataLocations +from pyasic.rpc.gcminer import GCMinerRPCAPI +from pyasic.web.auradine import FluxWebAPI + +AURADINE_DATA_LOC = DataLocations(**{}) + + +class Auradine(BaseMiner): + """Base handler for Auradine miners""" + + _api_cls = GCMinerRPCAPI + api: GCMinerRPCAPI + _web_cls = FluxWebAPI + web: FluxWebAPI + + data_locations = AURADINE_DATA_LOC diff --git a/pyasic/miners/miner_factory.py b/pyasic/miners/miner_factory.py index bddb9bd0..32acdc5f 100644 --- a/pyasic/miners/miner_factory.py +++ b/pyasic/miners/miner_factory.py @@ -38,6 +38,7 @@ from pyasic.miners.backends import ( VNish, ePIC, ) +from pyasic.miners.backends.auradine import Auradine from pyasic.miners.backends.innosilicon import Innosilicon from pyasic.miners.base import AnyMiner from pyasic.miners.goldshell import * @@ -57,6 +58,7 @@ class MinerTypes(enum.Enum): HIVEON = 7 LUX_OS = 8 EPIC = 9 + AURADINE = 10 MINER_CLASSES = { @@ -392,6 +394,16 @@ MINER_CLASSES = { None: LUXMiner, "ANTMINER S9": LUXMinerS9, }, + MinerTypes.AURADINE: { + None: Auradine, + # "AT1500": None, + # "AT2860": None, + # "AT2880": None, + # "AI2500": None, + # "AI3680": None, + # "AD2500": None, + # "AD3500": None, + }, } @@ -660,6 +672,8 @@ class MinerFactory: return MinerTypes.GOLDSHELL if "AVALON" in upper_data: return MinerTypes.AVALONMINER + if "GCMINER" in upper_data or "FLUXOS" in upper_data: + return MinerTypes.AURADINE async def send_web_command( self, @@ -948,5 +962,12 @@ class MinerFactory: except (TypeError, LookupError): pass + async def get_miner_model_auradine(self, ip: str): + web_json_data = await self.send_web_command(ip, ":8080/ipreport") + try: + return web_json_data["IPReport"][0]["model"] + except (TypeError, LookupError): + pass + miner_factory = MinerFactory() diff --git a/pyasic/rpc/btminer.py b/pyasic/rpc/btminer.py index 89281335..8d8c8270 100644 --- a/pyasic/rpc/btminer.py +++ b/pyasic/rpc/btminer.py @@ -187,7 +187,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): def __init__(self, ip: str, port: int = 4028, api_ver: str = "0.0.0"): super().__init__(ip, port, api_ver) - self.pwd = settings.get("default_whatsminer_password", "admin") + self.pwd = settings.get("default_whatsminer_rpc_password", "admin") self.current_token = None async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict: diff --git a/pyasic/rpc/gcminer.py b/pyasic/rpc/gcminer.py new file mode 100644 index 00000000..a4a103a7 --- /dev/null +++ b/pyasic/rpc/gcminer.py @@ -0,0 +1,182 @@ +# ------------------------------------------------------------------------------ +# 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 typing import Literal + +from pyasic.rpc import BaseMinerRPCAPI + + +class GCMinerRPCAPI(BaseMinerRPCAPI): + """An abstraction of the GCMiner API. + + Each method corresponds to an API command in GCMiner. + + No documentation for this API is currently publicly available. + + This class abstracts use of the GCMiner API, as well as the + methods for sending commands to it. The `self.send_command()` + function handles sending a command to the miner asynchronously, and + as such is the base for many of the functions in this class, which + rely on it to send the command for them. + + Parameters: + ip: The IP of the miner to reference the API on. + """ + + async def asc(self, n: int) -> dict: + """Get data for ASC device n. +
+ Expand + + Parameters: + n: The device to get data for. + + Returns: + The data for ASC device n. +
+ """ + return await self.send_command("asc", parameters=n) + + async def asccount(self) -> dict: + """Get data on the number of ASC devices and their info. +
+ Expand + + Returns: + Data on all ASC devices. +
+ """ + return await self.send_command("asccount") + + async def check(self, command: str) -> dict: + """Check if the command `command` exists in LUXMiner. +
+ Expand + + Parameters: + command: The command to check. + + Returns: + ## Information about a command: + * Exists (Y/N) <- the command exists in this version + * Access (Y/N) <- you have access to use the command +
+ """ + return await self.send_command("check", parameters=command) + + async def coin(self) -> dict: + """Get information on the current coin. +
+ Expand + + Returns: + ## Information about the current coin being mined: + * Hash Method <- the hashing algorithm + * Current Block Time <- blocktime as a float, 0 means none + * Current Block Hash <- the hash of the current block, blank means none + * LP <- whether LP is in use on at least 1 pool + * Network Difficulty: the current network difficulty +
+ """ + return await self.send_command("coin") + + async def config(self) -> dict: + """Get some basic configuration info. +
+ Expand + + Returns: + Miner configuration information. +
+ """ + return await self.send_command("config") + + async def devdetails(self) -> dict: + """Get data on all devices with their static details. +
+ Expand + + Returns: + Data on all devices with their static details. +
+ """ + return await self.send_command("devdetails") + + async def devs(self) -> dict: + """Get data on each PGA/ASC with their details. +
+ Expand + + Returns: + Data on each PGA/ASC with their details. +
+ """ + return await self.send_command("devs") + + async def edevs(self) -> dict: + """Alias for devs""" + return await self.send_command("edevs") + + async def pools(self) -> dict: + """Get pool information. + +
+ Expand + + Returns: + Miner pool information. +
+ """ + return await self.send_command("pools") + + async def stats(self) -> dict: + """Get stats of each device/pool with more than 1 getwork. + +
+ Expand + + Returns: + Stats of each device/pool with more than 1 getwork. +
+ """ + return await self.send_command("stats") + + async def estats(self) -> dict: + """Alias for stats""" + return await self.send_command("estats") + + async def summary(self) -> dict: + """Get the status summary of the miner. + +
+ Expand + + Returns: + The status summary of the miner. +
+ """ + return await self.send_command("summary") + + async def version(self) -> dict: + """Get miner version info. + +
+ Expand + + Returns: + Miner version information. +
+ """ + return await self.send_command("version") diff --git a/pyasic/settings/__init__.py b/pyasic/settings/__init__.py index be2cbc2b..5b1a4280 100644 --- a/pyasic/settings/__init__.py +++ b/pyasic/settings/__init__.py @@ -34,6 +34,8 @@ _settings = { # defaults "default_bosminer_web_password": "root", "default_vnish_web_password": "admin", "default_goldshell_web_password": "123456789", + "default_auradine_web_password": "admin", + "default_epic_web_password": "letmein", "default_hive_web_password": "admin", "default_antminer_ssh_password": "miner", "default_bosminer_ssh_password": "root", diff --git a/pyasic/web/antminer.py b/pyasic/web/antminer.py index dc9051bb..944ad55a 100644 --- a/pyasic/web/antminer.py +++ b/pyasic/web/antminer.py @@ -26,7 +26,7 @@ from pyasic.web import BaseWebAPI class AntminerModernWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) - self.pwd = settings.get("default_antminer_password", "root") + self.pwd = settings.get("default_antminer_web_password", "root") async def send_command( self, @@ -142,7 +142,7 @@ class AntminerModernWebAPI(BaseWebAPI): class AntminerOldWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) - self.pwd = settings.get("default_antminer_password", "root") + self.pwd = settings.get("default_antminer_web_password", "root") async def send_command( self, diff --git a/pyasic/web/auradine.py b/pyasic/web/auradine.py new file mode 100644 index 00000000..31da7883 --- /dev/null +++ b/pyasic/web/auradine.py @@ -0,0 +1,259 @@ +# ------------------------------------------------------------------------------ +# 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 +import json +import warnings +from typing import Any, List, Union + +import httpx + +from pyasic import settings +from pyasic.errors import APIError +from pyasic.web import BaseWebAPI + + +class FluxWebAPI(BaseWebAPI): + def __init__(self, ip: str) -> None: + super().__init__(ip) + self.username = "admin" + self.pwd = settings.get("default_auradine_web_password", "admin") + self.port = 8080 + self.jwt = None + + async def auth(self): + async with httpx.AsyncClient(transport=settings.transport()) as client: + try: + auth = await client.post( + f"http://{self.ip}:{self.port}/token", + data={ + "command": "token", + "username": self.username, + "password": self.pwd, + }, + ) + except httpx.HTTPError: + warnings.warn(f"Could not authenticate web token with miner: {self}") + else: + json_auth = auth.json() + try: + self.jwt = json_auth["Token"][0]["Token"] + except LookupError: + return None + return self.jwt + + async def send_command( + self, + command: Union[str, bytes], + post: bool = False, + ignore_errors: bool = False, + allow_warning: bool = True, + **parameters: Any, + ) -> dict: + if self.jwt is None: + await self.auth() + async with httpx.AsyncClient(transport=settings.transport()) as client: + for i in range(settings.get("get_data_retries", 1)): + try: + if post: + response = await client.post( + f"http://{self.ip}:{self.port}/{command}", + headers={"Token": self.jwt}, + timeout=settings.get("api_function_timeout", 5), + ) + elif parameters: + response = await client.post( + f"http://{self.ip}:{self.port}/{command}", + headers={"Token": self.jwt}, + timeout=settings.get("api_function_timeout", 5), + json={"command": command, **parameters}, + ) + else: + response = await client.get( + f"http://{self.ip}:{self.port}/{command}", + headers={"Token": self.jwt}, + timeout=settings.get("api_function_timeout", 5), + ) + json_data = response.json() + validation = self._validate_command_output(json_data) + if not validation[0]: + if i == settings.get("get_data_retries", 1): + raise APIError(validation[1]) + # refresh the token, retry + await self.auth() + continue + return json_data + except httpx.HTTPError: + pass + except json.JSONDecodeError: + pass + + async def multicommand( + self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True + ) -> dict: + tasks = {} + # send all commands individually + for cmd in commands: + tasks[cmd] = asyncio.create_task( + self.send_command(cmd, allow_warning=allow_warning) + ) + + await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True) + + data = {} + for cmd in tasks: + try: + result = tasks[cmd].result() + if result is None or result == {}: + result = {} + data[cmd] = [result] + except APIError: + pass + + return data + + @staticmethod + def _validate_command_output(data: dict) -> tuple: + # check if the data returned is correct or an error + # if status isn't a key, it is a multicommand + if "STATUS" not in data.keys(): + for key in data.keys(): + # make sure not to try to turn id into a dict + if not key == "id": + # make sure they succeeded + if "STATUS" in data[key][0].keys(): + if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]: + # this is an error + return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"] + elif "id" not in data.keys(): + if isinstance(data["STATUS"], list): + if data["STATUS"][0].get("STATUS", None) in ["S", "I"]: + return True, None + else: + return False, data["STATUS"][0]["Msg"] + + elif isinstance(data["STATUS"], dict): + # new style X19 command + if data["STATUS"]["STATUS"] not in ["S", "I"]: + return False, data["STATUS"]["Msg"] + return True, None + + if data["STATUS"] not in ["S", "I"]: + return False, data["Msg"] + else: + # make sure the command succeeded + if isinstance(data["STATUS"], str): + if data["STATUS"] in ["RESTART"]: + return True, None + elif isinstance(data["STATUS"], dict): + if data["STATUS"].get("STATUS") in ["S", "I"]: + return True, None + elif data["STATUS"][0]["STATUS"] not in ("S", "I"): + # this is an error + if data["STATUS"][0]["STATUS"] not in ("S", "I"): + return False, data["STATUS"][0]["Msg"] + return True, None + + async def factory_reset(self): + return await self.send_command("factory-reset", post=True) + + async def get_fan(self): + return await self.send_command("fan") + + async def set_fan(self, fan: int, speed_pct: int): + return await self.send_command("fan", index=fan, percentage=speed_pct) + + async def firmware_upgrade(self, url: str = None, version: str = "latest"): + if url is not None: + return await self.send_command("firmware-upgrade", url=url) + return await self.send_command("firmware-upgrade", version=version) + + async def get_frequency(self): + return await self.send_command("frequency") + + async def set_frequency(self, board: int, frequency: float): + return await self.send_command("frequency", board=board, frequency=frequency) + + async def ipreport(self): + return await self.send_command("ipreport") + + async def get_led(self): + return await self.send_command("led") + + async def set_led(self, code: int, led_1: int, led_2: int, msg: str = ""): + return await self.send_command( + "led", code=code, led1=led_1, led2=led_2, msg=msg + ) + + async def get_mode(self): + return await self.send_command("mode") + + async def set_mode(self, **kwargs): + return await self.send_command("mode", **kwargs) + + async def get_network(self): + return await self.send_command("network") + + async def set_network(self, **kwargs): + return await self.send_command("network", **kwargs) + + async def password(self, password: str): + res = await self.send_command( + "password", user=self.username, old=self.pwd, new=password + ) + self.pwd = password + return res + + async def get_psu(self): + return await self.send_command("psu") + + async def set_psu(self, voltage: float): + return await self.send_command("psu", voltage=voltage) + + async def get_register(self): + return await self.send_command("register") + + async def set_register(self, company: str): + return await self.send_command("register", parameter=company) + + async def reboot(self): + return await self.send_command("restart", post=True) + + async def restart_gcminer(self): + return await self.send_command("restart", parameter="gcminer") + + async def restart_api_server(self): + return await self.send_command("restart", parameter="api-server") + + async def temperature(self): + return await self.send_command("temperature") + + async def timedate(self, ntp: str, timezone: str): + return await self.send_command("timedate", ntp=ntp, timezone=timezone) + + async def token(self): + return await self.send_command("token", user=self.username, password=self.pwd) + + async def update_pools(self, pools: List[dict]): + return await self.send_command("updatepools", pools=pools) + + async def voltage(self): + return await self.send_command("voltage") + + async def get_ztp(self): + return await self.send_command("ztp") + + async def set_ztp(self, enable: bool): + return await self.send_command("ztp", parameter="on" if enable else "off") diff --git a/pyasic/web/braiins_os/__init__.py b/pyasic/web/braiins_os/__init__.py index 848e4e62..30737bfa 100644 --- a/pyasic/web/braiins_os/__init__.py +++ b/pyasic/web/braiins_os/__init__.py @@ -109,7 +109,7 @@ class BOSerWebAPI(BOSMinerWebAPI): return await self.gql.send_command(command) elif command_type == "grpc": try: - return await (getattr(self.grpc, command.replace("grpc_", "")))() + return await getattr(self.grpc, command.replace("grpc_", ""))() except AttributeError: raise APIError(f"No gRPC command found for command: {command}") elif command_type == "luci": diff --git a/pyasic/web/epic.py b/pyasic/web/epic.py index 684648b9..8e4811e4 100644 --- a/pyasic/web/epic.py +++ b/pyasic/web/epic.py @@ -27,7 +27,7 @@ class ePICWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) self.username = "root" - self.pwd = settings.get("default_epic_password", "letmein") + self.pwd = settings.get("default_epic_web_password", "letmein") self.token = None self.port = 4028 diff --git a/pyasic/web/goldshell.py b/pyasic/web/goldshell.py index ea4d0ec1..10072bed 100644 --- a/pyasic/web/goldshell.py +++ b/pyasic/web/goldshell.py @@ -27,7 +27,7 @@ class GoldshellWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) self.username = "admin" - self.pwd = settings.get("default_goldshell_password", "123456789") + self.pwd = settings.get("default_goldshell_web_password", "123456789") self.jwt = None async def auth(self): @@ -69,7 +69,7 @@ class GoldshellWebAPI(BaseWebAPI): if parameters.get("pool_pwd"): parameters["pass"] = parameters["pool_pwd"] parameters.pop("pool_pwd") - if not self.jwt: + if self.jwt is None: await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: for _ in range(settings.get("get_data_retries", 1)): diff --git a/pyasic/web/innosilicon.py b/pyasic/web/innosilicon.py index 63df22c2..4a842a89 100644 --- a/pyasic/web/innosilicon.py +++ b/pyasic/web/innosilicon.py @@ -28,7 +28,7 @@ class InnosiliconWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) self.username = "admin" - self.pwd = settings.get("default_innosilicon_password", "admin") + self.pwd = settings.get("default_innosilicon_web_password", "admin") self.jwt = None async def auth(self): @@ -52,7 +52,7 @@ class InnosiliconWebAPI(BaseWebAPI): allow_warning: bool = True, **parameters: Union[str, int, bool], ) -> dict: - if not self.jwt: + if self.jwt is None: await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: for _ in range(settings.get("get_data_retries", 1)): diff --git a/pyasic/web/vnish.py b/pyasic/web/vnish.py index 91588d8d..cc92bbd4 100644 --- a/pyasic/web/vnish.py +++ b/pyasic/web/vnish.py @@ -27,7 +27,7 @@ class VNishWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) self.username = "admin" - self.pwd = settings.get("default_vnish_password", "admin") + self.pwd = settings.get("default_vnish_web_password", "admin") self.token = None async def auth(self): @@ -56,7 +56,7 @@ class VNishWebAPI(BaseWebAPI): allow_warning: bool = True, **parameters: Union[str, int, bool], ) -> dict: - if not self.token: + if self.token is None: await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: for _ in range(settings.get("get_data_retries", 1)):