Merge branch 'dev_fluxos'

This commit is contained in:
b-rowan
2024-01-24 18:39:12 -07:00
43 changed files with 1075 additions and 23 deletions

View File

@@ -113,10 +113,17 @@ class MinerConfig:
**self.fan_mode.as_epic(),
**self.temperature.as_epic(),
**self.mining_mode.as_epic(),
**self.pools.as_epic(user_suffix=user_suffix),
**self.pools.as_epic(),
**self.power_scaling.as_epic(),
}
def as_auradine(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_auradine(),
**self.mining_mode.as_auradine(),
**self.pools.as_auradine(user_suffix=user_suffix),
}
@classmethod
def from_dict(cls, dict_conf: dict) -> "MinerConfig":
return cls(
@@ -189,6 +196,14 @@ class MinerConfig:
mining_mode=MiningModeConfig.from_vnish(web_settings),
)
@classmethod
def from_auradine(cls, web_conf: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_api(web_conf["pools"]),
fan_mode=FanModeConfig.from_auradine(web_conf["fan"]),
mining_mode=MiningModeConfig.from_auradine(web_conf["mode"]),
)
def merge(a: dict, b: dict) -> dict:
result = deepcopy(a)

View File

@@ -53,6 +53,9 @@ class MinerConfigOption(Enum):
def as_vnish(self) -> dict:
return self.value.as_vnish()
def as_auradine(self) -> dict:
return self.value.as_auradine()
def __call__(self, *args, **kwargs):
return self.value(*args, **kwargs)
@@ -99,3 +102,6 @@ class MinerConfigValue:
def as_vnish(self) -> dict:
return {}
def as_auradine(self) -> dict:
return {}

View File

@@ -92,6 +92,9 @@ class FanModeManual(MinerConfigValue):
"fan_control": {"min_fans": self.minimum_fans, "speed": self.speed},
}
def as_auradine(self) -> dict:
return {"fan": {"percentage": self.speed}}
@dataclass
class FanModeImmersion(MinerConfigValue):
@@ -107,6 +110,9 @@ class FanModeImmersion(MinerConfigValue):
def as_bosminer(self) -> dict:
return {"temp_control": {"mode": "disabled"}}
def as_auradine(self) -> dict:
return {"fan": {"percentage": 0}}
class FanModeConfig(MinerConfigOption):
normal = FanModeNormal
@@ -202,3 +208,13 @@ class FanModeConfig(MinerConfigOption):
if "minimumRequiredFans" in keys:
conf["minimum_fans"] = int(temperature_conf["minimumRequiredFans"])
return cls.manual(**conf)
@classmethod
def from_auradine(cls, web_fan: dict):
try:
fan_data = web_fan["Fan"][0]
fan_1_max = fan_data["Max"]
fan_1_target = fan_data["Target"]
return cls.manual(speed=round((fan_1_target / fan_1_max) * 100))
except LookupError:
return cls.default()

View File

@@ -43,6 +43,9 @@ class MiningModeNormal(MinerConfigValue):
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": self.mode}}
@dataclass
class MiningModeSleep(MinerConfigValue):
@@ -58,6 +61,9 @@ class MiningModeSleep(MinerConfigValue):
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"sleep": "on"}}
@dataclass
class MiningModeLPM(MinerConfigValue):
@@ -73,6 +79,9 @@ class MiningModeLPM(MinerConfigValue):
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": "eco"}}
@dataclass
class MiningModeHPM(MinerConfigValue):
@@ -88,6 +97,9 @@ class MiningModeHPM(MinerConfigValue):
def as_wm(self) -> dict:
return {"mode": self.mode}
def as_auradine(self) -> dict:
return {"mode": {"mode": "turbo"}}
@dataclass
class MiningModePowerTune(MinerConfigValue):
@@ -123,6 +135,9 @@ class MiningModePowerTune(MinerConfigValue):
),
}
def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
@dataclass
class MiningModeHashrateTune(MinerConfigValue):
@@ -152,6 +167,9 @@ class MiningModeHashrateTune(MinerConfigValue):
)
}
def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}}
@dataclass
class ManualBoardSettings(MinerConfigValue):
@@ -330,3 +348,22 @@ class MiningModeConfig(MinerConfigOption):
return cls.hashrate_tuning(
int(tuner_conf["hashrateTarget"]["terahashPerSecond"])
)
@classmethod
def from_auradine(cls, web_mode: dict):
try:
mode_data = web_mode["Mode"][0]
if mode_data.get("Sleep") == "on":
return cls.sleep()
if mode_data.get("Mode") == "normal":
return cls.normal()
if mode_data.get("Mode") == "eco":
return cls.low()
if mode_data.get("Mode") == "turbo":
return cls.high()
if mode_data.get("Ths") is not None:
return cls.hashrate_tuning(mode_data["Ths"])
if mode_data.get("Power") is not None:
return cls.power_tuning(mode_data["Power"])
except LookupError:
return cls.default()

View File

@@ -27,7 +27,7 @@ class Pool(MinerConfigValue):
user: str
password: str
def as_am_modern(self, user_suffix: str = None):
def as_am_modern(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
@@ -36,7 +36,7 @@ class Pool(MinerConfigValue):
}
return {"url": self.url, "user": self.user, "pass": self.password}
def as_wm(self, idx: int = 1, user_suffix: str = None):
def as_wm(self, idx: int = 1, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
f"pool_{idx}": self.url,
@@ -49,7 +49,7 @@ class Pool(MinerConfigValue):
f"passwd_{idx}": self.password,
}
def as_am_old(self, idx: int = 1, user_suffix: str = None):
def as_am_old(self, idx: int = 1, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
f"_ant_pool{idx}url": self.url,
@@ -62,7 +62,7 @@ class Pool(MinerConfigValue):
f"_ant_pool{idx}pw": self.password,
}
def as_goldshell(self, user_suffix: str = None):
def as_goldshell(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
@@ -71,12 +71,12 @@ class Pool(MinerConfigValue):
}
return {"url": self.url, "user": self.user, "pass": self.password}
def as_avalon(self, user_suffix: str = None):
def as_avalon(self, user_suffix: str = None) -> str:
if user_suffix is not None:
return ",".join([self.url, f"{self.user}{user_suffix}", self.password])
return ",".join([self.url, self.user, self.password])
def as_inno(self, idx: int = 1, user_suffix: str = None):
def as_inno(self, idx: int = 1, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
f"Pool{idx}": self.url,
@@ -89,7 +89,7 @@ class Pool(MinerConfigValue):
f"Password{idx}": self.password,
}
def as_bosminer(self, user_suffix: str = None):
def as_bosminer(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
@@ -98,6 +98,15 @@ class Pool(MinerConfigValue):
}
return {"url": self.url, "user": self.user, "password": self.password}
def as_auradine(self, user_suffix: str = None) -> dict:
if user_suffix is not None:
return {
"url": self.url,
"user": f"{self.user}{user_suffix}",
"pass": self.password,
}
return {"url": self.url, "user": self.user, "pass": self.password}
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "Pool":
return cls(
@@ -241,6 +250,9 @@ class PoolGroup(MinerConfigValue):
return conf
return {"name": "Group", "pool": []}
def as_auradine(self, user_suffix: str = None) -> list:
return [p.as_auradine(user_suffix=user_suffix) for p in self.pools]
@classmethod
def from_dict(cls, dict_conf: Union[dict, None]) -> "PoolGroup":
cls_conf = {}
@@ -296,7 +308,7 @@ class PoolGroup(MinerConfigValue):
return cls([Pool.from_vnish(p) for p in web_settings_pools])
@classmethod
def from_boser(cls, grpc_pool_group: dict):
def from_boser(cls, grpc_pool_group: dict) -> "PoolGroup":
try:
return cls(
pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]],
@@ -373,6 +385,15 @@ class PoolConfig(MinerConfigValue):
def as_boser(self, user_suffix: str = None) -> dict:
return {}
def as_auradine(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {
"updatepools": {
"pools": self.groups[0].as_auradine(user_suffix=user_suffix)
}
}
return {"updatepools": {"pools": PoolGroup().as_auradine()}}
@classmethod
def from_api(cls, api_pools: dict) -> "PoolConfig":
try:
@@ -417,7 +438,7 @@ class PoolConfig(MinerConfigValue):
return cls()
@classmethod
def from_boser(cls, grpc_miner_conf: dict):
def from_boser(cls, grpc_miner_conf: dict) -> "PoolConfig":
try:
return cls(
groups=[

View File

@@ -0,0 +1 @@
from .flux import *

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.types import AuradineAT1500
class AuradineFluxAT1500(AuradineAT1500, Auradine):
pass

View File

@@ -0,0 +1,10 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.types import AuradineAT2860, AuradineAT2880
class AuradineFluxAT2860(AuradineAT2860, Auradine):
pass
class AuradineFluxAT2880(AuradineAT2880, Auradine):
pass

View File

@@ -0,0 +1,2 @@
from .AT1 import AuradineFluxAT1500
from .AT2 import AuradineFluxAT2860, AuradineFluxAT2880

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.types import AuradineAI2500
class AuradineFluxAI2500(AuradineAI2500, Auradine):
pass

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.types import AuradineAI3680
class AuradineFluxAI3680(AuradineAI3680, Auradine):
pass

View File

@@ -0,0 +1,2 @@
from .AI2 import AuradineFluxAI2500
from .AI3 import AuradineFluxAI3680

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.types import AuradineAD2500
class AuradineFluxAD2500(AuradineAD2500, Auradine):
pass

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends import Auradine
from pyasic.miners.types import AuradineAD3500
class AuradineFluxAD3500(AuradineAD3500, Auradine):
pass

View File

@@ -0,0 +1,2 @@
from .AD2 import AuradineFluxAD2500
from .AD3 import AuradineFluxAD3500

View File

@@ -0,0 +1,3 @@
from .AD import *
from .AI import *
from .AT import *

View File

@@ -14,6 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from .antminer import AntminerModern, AntminerOld
from .auradine import Auradine
from .avalonminer import AvalonMiner
from .bfgminer import BFGMiner
from .bmminer import BMMiner
@@ -23,6 +24,7 @@ from .cgminer import CGMiner
from .epic import ePIC
from .goldshell import GoldshellMiner
from .hiveon import Hiveon
from .innosilicon import Innosilicon
from .luxminer import LUXMiner
from .vnish import VNish
from .whatsminer import M2X, M3X, M5X, M6X

View File

@@ -0,0 +1,378 @@
# ------------------------------------------------------------------------------
# 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 logging
from enum import Enum
from typing import List, Optional
from pyasic import APIError, MinerConfig
from pyasic.data import Fan, HashBoard
from pyasic.miners.base import (
BaseMiner,
DataFunction,
DataLocations,
DataOptions,
RPCAPICommand,
WebAPICommand,
)
from pyasic.rpc.gcminer import GCMinerRPCAPI
from pyasic.web.auradine import FluxWebAPI
AURADINE_DATA_LOC = DataLocations(
**{
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_ipreport", "ipreport")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[WebAPICommand("web_ipreport", "ipreport")],
),
str(DataOptions.HOSTNAME): DataFunction(
"_get_hostname",
[WebAPICommand("web_ipreport", "ipreport")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("api_summary", "summary")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[
RPCAPICommand("api_devs", "devs"),
WebAPICommand("web_ipreport", "ipreport"),
],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[WebAPICommand("web_psu", "psu")],
),
str(DataOptions.WATTAGE_LIMIT): DataFunction(
"_get_wattage_limit",
[WebAPICommand("web_mode", "mode"), WebAPICommand("web_psu", "psu")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[WebAPICommand("web_fan", "fan")],
),
str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light",
[WebAPICommand("web_led", "led")],
),
str(DataOptions.IS_MINING): DataFunction(
"_is_mining",
[WebAPICommand("web_mode", "mode")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[RPCAPICommand("api_summary", "summary")],
),
}
)
class AuradineLEDColors(Enum):
OFF = 0
GREEN = 1
RED = 2
YELLOW = 3
GREEN_FLASHING = 4
RED_FLASHING = 5
YELLOW_FLASHING = 6
def __int__(self):
return self.value
class AuradineLEDCodes(Enum):
NO_POWER = 1
NORMAL = 2
LOCATE_MINER = 3
TEMPERATURE = 4
POOL_CONFIG = 5
NETWORK = 6
CONTROL_BOARD = 7
HASH_RATE_LOW = 8
CUSTOM1 = 101
CUSTOM2 = 102
def __int__(self):
return self.value
class Auradine(BaseMiner):
"""Base handler for Auradine miners"""
_api_cls = GCMinerRPCAPI
api: GCMinerRPCAPI
_web_cls = FluxWebAPI
web: FluxWebAPI
data_locations = AURADINE_DATA_LOC
supports_shutdown = True
supports_autotuning = True
async def fault_light_on(self) -> bool:
return await self.web.set_led(code=int(AuradineLEDCodes.LOCATE_MINER))
async def fault_light_off(self) -> bool:
return await self.web.set_led(code=int(AuradineLEDCodes.NORMAL))
async def reboot(self) -> bool:
try:
await self.web.reboot()
except APIError:
return False
return True
async def restart_backend(self) -> bool:
try:
await self.web.restart_gcminer()
except APIError:
return False
return True
async def stop_mining(self) -> bool:
try:
await self.web.set_mode(sleep="on")
except APIError:
return False
return True
async def resume_mining(self) -> bool:
try:
await self.web.set_mode(sleep="off")
except APIError:
return False
return True
async def set_power_limit(self, wattage: int) -> bool:
try:
await self.web.set_mode(mode="custom", tune="power", power=wattage)
except APIError:
return False
return True
async def get_config(self) -> MinerConfig:
try:
web_conf = await self.web.multicommand("pools", "mode", "fan")
return MinerConfig.from_auradine(web_conf=web_conf)
except APIError as e:
logging.warning(e)
except LookupError:
pass
return MinerConfig()
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
self.config = config
conf = config.as_auradine(user_suffix=user_suffix)
for key in conf.keys():
await self.web.send_command(command=key, **conf[key])
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def _get_mac(self, web_ipreport: dict = None) -> Optional[str]:
if web_ipreport is None:
try:
web_ipreport = await self.web.ipreport()
except APIError:
pass
if web_ipreport is not None:
try:
return web_ipreport["IPReport"][0]["mac"].upper()
except (LookupError, AttributeError):
pass
async def _get_fw_ver(self, web_ipreport: dict = None) -> Optional[str]:
if web_ipreport is None:
try:
web_ipreport = await self.web.ipreport()
except APIError:
pass
if web_ipreport is not None:
try:
return web_ipreport["IPReport"][0]["version"]
except LookupError:
pass
async def _get_hostname(self, web_ipreport: dict = None) -> Optional[str]:
if web_ipreport is None:
try:
web_ipreport = await self.web.ipreport()
except APIError:
pass
if web_ipreport is not None:
try:
return web_ipreport["IPReport"][0]["hostname"]
except LookupError:
pass
async def _get_hashrate(self, api_summary: dict = None) -> Optional[float]:
if api_summary is None:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary is not None:
try:
return round(
float(float(api_summary["SUMMARY"][0]["MHS 5s"]) / 1000000), 2
)
except (LookupError, ValueError, TypeError):
pass
async def _get_hashboards(
self, api_devs: dict = None, web_ipreport: dict = None
) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if api_devs is None:
try:
api_devs = await self.api.devs()
except APIError:
pass
if web_ipreport is None:
try:
web_ipreport = await self.web.ipreport()
except APIError:
pass
if api_devs is not None:
try:
for board in api_devs["DEVS"]:
b_id = board["ID"] - 1
hashboards[b_id].hashrate = round(
float(float(board["MHS 5s"]) / 1000000), 2
)
hashboards[b_id].temp = round(float(float(board["Temperature"])), 2)
hashboards[b_id].missing = False
except LookupError:
pass
if web_ipreport is not None:
try:
for board, sn in enumerate(web_ipreport["IPReport"][0]["HBSerialNo"]):
hashboards[board].serial_number = sn
hashboards[board].missing = False
except LookupError:
pass
return hashboards
async def _get_wattage(self, web_psu: dict = None) -> Optional[int]:
if web_psu is None:
try:
web_psu = await self.web.get_psu()
except APIError:
pass
if web_psu is not None:
try:
return int(float(web_psu["PSU"][0]["PowerIn"].replace("W", "")))
except (LookupError, TypeError, ValueError):
pass
async def _get_wattage_limit(
self, web_mode: dict = None, web_psu: dict = None
) -> Optional[int]:
if web_mode is None:
try:
web_mode = await self.web.get_mode()
except APIError:
pass
if web_mode is not None:
try:
return web_mode["Mode"][0]["Power"]
except (LookupError, TypeError, ValueError):
pass
if web_psu is None:
try:
web_psu = await self.web.get_psu()
except APIError:
pass
if web_psu is not None:
try:
return int(float(web_psu["PSU"][0]["PoutMax"].replace("W", "")))
except (LookupError, TypeError, ValueError):
pass
async def _get_fans(self, web_fan: dict = None) -> List[Fan]:
if web_fan is None:
try:
web_fan = await self.web.get_fan()
except APIError:
pass
fans = []
if web_fan is not None:
try:
for fan in web_fan["Fan"]:
fans.append(Fan(round(fan["Speed"])))
except LookupError:
pass
return fans
async def _get_fault_light(self, web_led: dict = None) -> Optional[bool]:
if web_led is None:
try:
web_led = await self.web.get_led()
except APIError:
pass
if web_led is not None:
try:
return web_led["LED"][0]["Code"] == int(AuradineLEDCodes.LOCATE_MINER)
except LookupError:
pass
async def _is_mining(self, web_mode: dict = None) -> Optional[bool]:
if web_mode is None:
try:
web_mode = await self.web.get_mode()
except APIError:
pass
if web_mode is not None:
try:
return web_mode["Mode"][0]["Sleep"] == "off"
except (LookupError, TypeError, ValueError):
pass
async def _get_uptime(self, api_summary: dict = None) -> Optional[int]:
if api_summary is None:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary is not None:
try:
return api_summary["SUMMARY"][0]["Elapsed"]
except LookupError:
pass

View File

@@ -35,3 +35,7 @@ class InnosiliconMake(BaseMiner):
class GoldshellMake(BaseMiner):
make = "Goldshell"
class AuradineMake(BaseMiner):
make = "Auradine"

View File

@@ -26,19 +26,21 @@ import httpx
from pyasic import settings
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.innosilicon import Innosilicon
from pyasic.miners.base import AnyMiner
from pyasic.miners.goldshell import *
from pyasic.miners.innosilicon import *
@@ -57,6 +59,7 @@ class MinerTypes(enum.Enum):
HIVEON = 7
LUX_OS = 8
EPIC = 9
AURADINE = 10
MINER_CLASSES = {
@@ -392,6 +395,16 @@ MINER_CLASSES = {
None: LUXMiner,
"ANTMINER S9": LUXMinerS9,
},
MinerTypes.AURADINE: {
None: Auradine,
"AT1500": AuradineFluxAT1500,
"AT2860": AuradineFluxAT2860,
"AT2880": AuradineFluxAT2880,
"AI2500": AuradineFluxAI2500,
"AI3680": AuradineFluxAI3680,
"AD2500": AuradineFluxAD2500,
"AD3500": AuradineFluxAD3500,
},
}
@@ -464,6 +477,7 @@ class MinerFactory:
MinerTypes.EPIC: self.get_miner_model_epic,
MinerTypes.HIVEON: self.get_miner_model_hiveon,
MinerTypes.LUX_OS: self.get_miner_model_luxos,
MinerTypes.AURADINE: self.get_miner_model_auradine,
}
fn = miner_model_fns.get(miner_type)
@@ -561,6 +575,8 @@ class MinerFactory:
return MinerTypes.AVALONMINER
if "DragonMint" in web_text:
return MinerTypes.INNOSILICON
if "Miner UI" in web_text:
return MinerTypes.AURADINE
async def _get_miner_socket(self, ip: str):
tasks = []
@@ -650,6 +666,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,
@@ -938,5 +956,12 @@ class MinerFactory:
except (TypeError, LookupError):
pass
async def get_miner_model_auradine(self, ip: str):
sock_json_data = await self.send_api_command(ip, "devdetails")
try:
return sock_json_data["DEVDETAILS"][0]["Model"]
except LookupError:
pass
miner_factory = MinerFactory()

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------
from .antminer import *
from .auradine import *
from .avalonminer import *
from .goldshell import *
from .innosilicon import *

View File

@@ -0,0 +1,6 @@
from pyasic.miners.makes import AuradineMake
class AuradineAD2500(AuradineMake):
raw_model = "AD2500"
expected_fans = 0

View File

@@ -0,0 +1,6 @@
from pyasic.miners.makes import AuradineMake
class AuradineAD3500(AuradineMake):
raw_model = "AD3500"
expected_fans = 0

View File

@@ -0,0 +1,2 @@
from .AD2 import AuradineAD2500
from .AD3 import AuradineAD3500

View File

@@ -0,0 +1,6 @@
from pyasic.miners.makes import AuradineMake
class AuradineAI2500(AuradineMake):
raw_model = "AI2500"
expected_fans = 0

View File

@@ -0,0 +1,6 @@
from pyasic.miners.makes import AuradineMake
class AuradineAI3680(AuradineMake):
raw_model = "AI3680"
expected_fans = 0

View File

@@ -0,0 +1,2 @@
from .AI2 import AuradineAI2500
from .AI3 import AuradineAI3680

View File

@@ -0,0 +1,7 @@
from pyasic.miners.makes import AuradineMake
class AuradineAT1500(AuradineMake):
raw_model = "AT1500"
expected_chips = 132
expected_fans = 4

View File

@@ -0,0 +1,11 @@
from pyasic.miners.makes import AuradineMake
class AuradineAT2860(AuradineMake):
raw_model = "AT2860"
expected_fans = 4
class AuradineAT2880(AuradineMake):
raw_model = "AT2880"
expected_fans = 4

View File

@@ -0,0 +1,2 @@
from .AT1 import AuradineAT1500
from .AT2 import AuradineAT2860, AuradineAT2880

View File

@@ -0,0 +1,3 @@
from .AD import *
from .AI import *
from .AT import *

View File

@@ -19,6 +19,7 @@ from pyasic.miners.makes import WhatsMinerMake
class M50SPlusPlusVK10(WhatsMinerMake):
raw_model = "M50S++ VK10"
expected_chips = 117
expected_fans = 2

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,

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

@@ -0,0 +1,262 @@
# ------------------------------------------------------------------------------
# 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",
json={
"command": "token",
"user": 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 = {"multicommand": True}
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):
return await self.send_command("led", code=code)
async def set_led_custom(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)):

View File

@@ -11,7 +11,6 @@ readme = "README.md"
python = "^3.8"
httpx = "^0.26.0"
asyncssh = "^2.14.2"
grpc-requests = "^0.1.13"
passlib = "^1.7.4"
pyaml = "^23.12.0"
toml = "^0.10.2"