feature: add basic auradine miner framework.

This commit is contained in:
b-rowan
2024-01-23 14:06:54 -07:00
parent 0958f47cfe
commit 64774d2017
12 changed files with 507 additions and 11 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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:

182
pyasic/rpc/gcminer.py Normal file
View File

@@ -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.
<details>
<summary>Expand</summary>
Parameters:
n: The device to get data for.
Returns:
The data for ASC device n.
</details>
"""
return await self.send_command("asc", parameters=n)
async def asccount(self) -> dict:
"""Get data on the number of ASC devices and their info.
<details>
<summary>Expand</summary>
Returns:
Data on all ASC devices.
</details>
"""
return await self.send_command("asccount")
async def check(self, command: str) -> dict:
"""Check if the command `command` exists in LUXMiner.
<details>
<summary>Expand</summary>
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
</details>
"""
return await self.send_command("check", parameters=command)
async def coin(self) -> dict:
"""Get information on the current coin.
<details>
<summary>Expand</summary>
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
</details>
"""
return await self.send_command("coin")
async def config(self) -> dict:
"""Get some basic configuration info.
<details>
<summary>Expand</summary>
Returns:
Miner configuration information.
</details>
"""
return await self.send_command("config")
async def devdetails(self) -> dict:
"""Get data on all devices with their static details.
<details>
<summary>Expand</summary>
Returns:
Data on all devices with their static details.
</details>
"""
return await self.send_command("devdetails")
async def devs(self) -> dict:
"""Get data on each PGA/ASC with their details.
<details>
<summary>Expand</summary>
Returns:
Data on each PGA/ASC with their details.
</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.
<details>
<summary>Expand</summary>
Returns:
Miner pool information.
</details>
"""
return await self.send_command("pools")
async def stats(self) -> dict:
"""Get stats of each device/pool with more than 1 getwork.
<details>
<summary>Expand</summary>
Returns:
Stats of each device/pool with more than 1 getwork.
</details>
"""
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.
<details>
<summary>Expand</summary>
Returns:
The status summary of the miner.
</details>
"""
return await self.send_command("summary")
async def version(self) -> dict:
"""Get miner version info.
<details>
<summary>Expand</summary>
Returns:
Miner version information.
</details>
"""
return await self.send_command("version")

View File

@@ -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",

View File

@@ -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,

259
pyasic/web/auradine.py Normal file
View File

@@ -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")

View File

@@ -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":

View File

@@ -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

View File

@@ -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)):

View File

@@ -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)):

View File

@@ -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)):