Merge pull request #125 from UpstreamData/dev_marafw

feature: Add maraFW support.
This commit is contained in:
Brett Rowan
2024-04-30 10:01:35 -06:00
committed by GitHub
8 changed files with 513 additions and 113 deletions

View File

@@ -138,6 +138,15 @@ class MinerConfig:
**self.power_scaling.as_auradine(),
}
def as_mara(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_mara(),
**self.temperature.as_mara(),
**self.mining_mode.as_mara(),
**self.pools.as_mara(user_suffix=user_suffix),
**self.power_scaling.as_mara(),
}
@classmethod
def from_dict(cls, dict_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from a dictionary."""
@@ -228,3 +237,11 @@ class MinerConfig:
fan_mode=FanModeConfig.from_auradine(web_conf["fan"]),
mining_mode=MiningModeConfig.from_auradine(web_conf["mode"]),
)
@classmethod
def from_mara(cls, web_miner_config: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_mara(web_miner_config),
fan_mode=FanModeConfig.from_mara(web_miner_config),
mining_mode=MiningModeConfig.from_mara(web_miner_config),
)

View File

@@ -57,6 +57,9 @@ class MinerConfigOption(Enum):
def as_auradine(self) -> dict:
return self.value.as_auradine()
def as_mara(self) -> dict:
return self.value.as_mara()
def __call__(self, *args, **kwargs):
return self.value(*args, **kwargs)
@@ -106,3 +109,6 @@ class MinerConfigValue:
def as_auradine(self) -> dict:
return {}
def as_mara(self) -> dict:
return {}

View File

@@ -71,6 +71,15 @@ class FanModeNormal(MinerConfigValue):
}
}
def as_mara(self) -> dict:
return {
"general-config": {"environment-profile": "AirCooling"},
"advance-config": {
"override-fan-control": False,
"fan-fixed-percent": 0,
},
}
@dataclass
class FanModeManual(MinerConfigValue):
@@ -120,6 +129,15 @@ class FanModeManual(MinerConfigValue):
def as_epic(self) -> dict:
return {"fans": {"Manual": {"speed": self.speed}}}
def as_mara(self) -> dict:
return {
"general-config": {"environment-profile": "AirCooling"},
"advance-config": {
"override-fan-control": True,
"fan-fixed-percent": self.speed,
},
}
@dataclass
class FanModeImmersion(MinerConfigValue):
@@ -140,6 +158,9 @@ class FanModeImmersion(MinerConfigValue):
def as_auradine(self) -> dict:
return {"fan": {"percentage": 0}}
def as_mara(self) -> dict:
return {"general-config": {"environment-profile": "OilImmersionCooling"}}
class FanModeConfig(MinerConfigOption):
normal = FanModeNormal
@@ -255,4 +276,18 @@ class FanModeConfig(MinerConfigOption):
fan_1_target = fan_data["Target"]
return cls.manual(speed=round((fan_1_target / fan_1_max) * 100))
except LookupError:
return cls.default()
pass
return cls.default()
@classmethod
def from_mara(cls, web_config: dict):
try:
mode = web_config["general-config"]["environment-profile"]
if mode == "AirCooling":
if web_config["advance-config"]["override-fan-control"]:
return cls.manual(web_config["advance-config"]["fan-fixed-percent"])
return cls.normal()
return cls.immersion()
except LookupError:
pass
return cls.default()

View File

@@ -56,6 +56,13 @@ class MiningModeNormal(MinerConfigValue):
def as_goldshell(self) -> dict:
return {"settings": {"level": 0}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Stock",
}
}
@dataclass
class MiningModeSleep(MinerConfigValue):
@@ -82,6 +89,13 @@ class MiningModeSleep(MinerConfigValue):
def as_goldshell(self) -> dict:
return {"settings": {"level": 3}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Sleep",
}
}
@dataclass
class MiningModeLPM(MinerConfigValue):
@@ -219,6 +233,17 @@ class MiningModePowerTune(MinerConfigValue):
def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Auto",
"concorde": {
"mode-select": "PowerTarget",
"power-target": self.power,
},
}
}
@dataclass
class MiningModeHashrateTune(MinerConfigValue):
@@ -269,6 +294,17 @@ class MiningModeHashrateTune(MinerConfigValue):
def as_epic(self) -> dict:
return {"ptune": {"algo": self.algo.as_epic(), "target": self.hashrate}}
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Auto",
"concorde": {
"mode-select": "Hashrate",
"hash-target": self.hashrate,
},
}
}
@dataclass
class ManualBoardSettings(MinerConfigValue):
@@ -320,6 +356,17 @@ class MiningModeManual(MinerConfigValue):
}
return cls(global_freq=freq, global_volt=voltage, boards=boards)
def as_mara(self) -> dict:
return {
"mode": {
"work-mode-selector": "Fixed",
"fixed": {
"frequency": str(self.global_freq),
"voltage": self.global_volt,
},
}
}
class MiningModeConfig(MinerConfigOption):
normal = MiningModeNormal
@@ -468,3 +515,28 @@ class MiningModeConfig(MinerConfigOption):
return cls.power_tuning(mode_data["Power"])
except LookupError:
return cls.default()
@classmethod
def from_mara(cls, web_config: dict):
try:
mode = web_config["mode"]["work-mode-selector"]
if mode == "Fixed":
fixed_conf = web_config["mode"]["fixed"]
return cls.manual(
global_freq=int(fixed_conf["frequency"]),
global_volt=fixed_conf["voltage"],
)
elif mode == "Stock":
return cls.normal()
elif mode == "Sleep":
return cls.sleep()
elif mode == "Auto":
auto_conf = web_config["mode"]["concorde"]
auto_mode = auto_conf["mode-select"]
if auto_mode == "Hashrate":
return cls.hashrate_tuning(hashrate=auto_conf["hash-target"])
elif auto_mode == "PowerTarget":
return cls.power_tuning(power=auto_conf["power-target"])
except LookupError:
pass
return cls.default()

View File

@@ -118,6 +118,15 @@ class Pool(MinerConfigValue):
}
return {"pool": self.url, "login": self.user, "password": self.password}
def as_mara(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: dict | None) -> "Pool":
return cls(
@@ -177,6 +186,14 @@ class Pool(MinerConfigValue):
password=grpc_pool["password"],
)
@classmethod
def from_mara(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"],
user=web_pool["user"],
password=web_pool["pass"],
)
@dataclass
class PoolGroup(MinerConfigValue):
@@ -264,9 +281,12 @@ class PoolGroup(MinerConfigValue):
def as_auradine(self, user_suffix: str = None) -> list:
return [p.as_auradine(user_suffix=user_suffix) for p in self.pools]
def as_epic(self, user_suffix: str = None) -> dict:
def as_epic(self, user_suffix: str = None) -> list:
return [p.as_epic(user_suffix=user_suffix) for p in self.pools]
def as_mara(self, user_suffix: str = None) -> list:
return [p.as_mara(user_suffix=user_suffix) for p in self.pools]
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolGroup":
cls_conf = {}
@@ -336,6 +356,10 @@ class PoolGroup(MinerConfigValue):
except LookupError:
return cls()
@classmethod
def from_mara(cls, web_config_pools: dict) -> "PoolGroup":
return cls(pools=[Pool.from_mara(pool_conf) for pool_conf in web_config_pools])
@dataclass
class PoolConfig(MinerConfigValue):
@@ -427,6 +451,11 @@ class PoolConfig(MinerConfigValue):
}
}
def as_mara(self, user_suffix: str = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_mara(user_suffix=user_suffix)}
return {"pools": []}
@classmethod
def from_api(cls, api_pools: dict) -> "PoolConfig":
try:
@@ -481,3 +510,7 @@ class PoolConfig(MinerConfigValue):
)
except LookupError:
return cls()
@classmethod
def from_mara(cls, web_config: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_mara(web_config["pools"])])

View File

@@ -1,71 +1,69 @@
from typing import Optional
from typing import List, Optional
from pyasic import MinerConfig
from pyasic.config import MiningModeConfig
from pyasic.data import Fan, HashBoard
from pyasic.errors import APIError
from pyasic.miners.backends import AntminerModern
from pyasic.miners.data import (
DataFunction,
DataLocations,
DataOptions,
RPCAPICommand,
WebAPICommand,
)
from pyasic.miners.base import BaseMiner
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.misc import merge_dicts
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")],
[WebAPICommand("web_overview", "overview")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[RPCAPICommand("rpc_version", "version")],
[WebAPICommand("web_overview", "overview")],
),
str(DataOptions.HOSTNAME): DataFunction(
"_get_hostname",
[WebAPICommand("web_get_system_info", "get_system_info")],
[WebAPICommand("web_network_config", "network_config")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("rpc_summary", "summary")],
[WebAPICommand("web_brief", "brief")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[RPCAPICommand("rpc_stats", "stats")],
[WebAPICommand("web_brief", "brief")],
),
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.HASHBOARDS): DataFunction(
"_get_hashboards",
[WebAPICommand("web_hashboards", "hashboards")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[WebAPICommand("web_brief", "brief")],
),
str(DataOptions.WATTAGE_LIMIT): DataFunction(
"_get_wattage_limit",
[WebAPICommand("web_miner_config", "miner_config")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[WebAPICommand("web_fans", "fans")],
),
str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light",
[WebAPICommand("web_locate_miner", "locate_miner")],
),
str(DataOptions.IS_MINING): DataFunction(
"_is_mining",
[WebAPICommand("web_brief", "brief")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_brief", "brief")],
),
}
)
class MaraMiner(AntminerModern):
class MaraMiner(BaseMiner):
_web_cls = MaraWebAPI
web: MaraWebAPI
@@ -73,6 +71,52 @@ class MaraMiner(AntminerModern):
firmware = "MaraFW"
async def fault_light_off(self) -> bool:
res = await self.web.set_locate_miner(blinking=False)
return res.get("blinking") is False
async def fault_light_on(self) -> bool:
res = await self.web.set_locate_miner(blinking=True)
return res.get("blinking") is True
async def get_config(self) -> MinerConfig:
data = await self.web.get_miner_config()
if data:
self.config = MinerConfig.from_mara(data)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
data = await self.web.get_miner_config()
cfg_data = config.as_mara(user_suffix=user_suffix)
merged_cfg = merge_dicts(data, cfg_data)
await self.web.set_miner_config(**merged_cfg)
async def set_power_limit(self, wattage: int) -> bool:
cfg = await self.get_config()
cfg.mining_mode = MiningModeConfig.power_tuning(wattage)
await self.send_config(cfg)
return True
async def stop_mining(self) -> bool:
data = await self.web.get_miner_config()
data["mode"]["work-mode-selector"] = "Sleep"
await self.web.set_miner_config(**data)
return True
async def resume_mining(self) -> bool:
data = await self.web.get_miner_config()
data["mode"]["work-mode-selector"] = "Auto"
await self.web.set_miner_config(**data)
return True
async def reboot(self) -> bool:
await self.web.reboot()
return True
async def restart_backend(self) -> bool:
await self.web.reload()
return True
async def _get_wattage(self, web_brief: dict = None) -> Optional[int]:
if web_brief is None:
try:
@@ -85,3 +129,170 @@ class MaraMiner(AntminerModern):
return web_brief["power_consumption_estimated"]
except LookupError:
pass
async def _is_mining(self, web_brief: dict = None) -> Optional[bool]:
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["status"] == "Mining"
except LookupError:
pass
async def _get_uptime(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["elapsed"]
except LookupError:
pass
async def _get_hashboards(self, web_hashboards: dict = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if web_hashboards is None:
try:
web_hashboards = await self.web.hashboards()
except APIError:
pass
if web_hashboards is not None:
try:
for hb in web_hashboards["hashboards"]:
idx = hb["index"]
hashboards[idx].hashrate = hb["hashrate_average"]
hashboards[idx].temp = round(
sum(hb["temperature_pcb"]) / len(hb["temperature_pcb"]), 2
)
hashboards[idx].chip_temp = round(
sum(hb["temperature_chip"]) / len(hb["temperature_chip"]), 2
)
hashboards[idx].chips = hb["asic_num"]
hashboards[idx].serial_number = hb["serial_number"]
hashboards[idx].missing = False
except LookupError:
pass
return hashboards
async def _get_mac(self, web_overview: dict = None) -> Optional[str]:
if web_overview is None:
try:
web_overview = await self.web.overview()
except APIError:
pass
if web_overview is not None:
try:
return web_overview["mac"].upper()
except LookupError:
pass
async def _get_fw_ver(self, web_overview: dict = None) -> Optional[str]:
if web_overview is None:
try:
web_overview = await self.web.overview()
except APIError:
pass
if web_overview is not None:
try:
return web_overview["version_firmware"]
except LookupError:
pass
async def _get_hostname(self, web_network_config: dict = None) -> Optional[str]:
if web_network_config is None:
try:
web_network_config = await self.web.get_network_config()
except APIError:
pass
if web_network_config is not None:
try:
return web_network_config["hostname"]
except LookupError:
pass
async def _get_hashrate(self, web_brief: dict = None) -> Optional[float]:
if web_brief is None:
try:
web_brief = await self.web.brief()
except APIError:
pass
if web_brief is not None:
try:
return round(web_brief["hashrate_realtime"], 2)
except LookupError:
pass
async def _get_fans(self, web_fans: dict = None) -> List[Fan]:
if web_fans is None:
try:
web_fans = await self.web.fans()
except APIError:
pass
if web_fans is not None:
fans = []
for n in range(self.expected_fans):
try:
fans.append(Fan(web_fans["fans"][n]["current_speed"]))
except (IndexError, KeyError):
pass
return fans
return [Fan() for _ in range(self.expected_fans)]
async def _get_fault_light(self, web_locate_miner: dict = None) -> bool:
if web_locate_miner is None:
try:
web_locate_miner = await self.web.get_locate_miner()
except APIError:
pass
if web_locate_miner is not None:
try:
return web_locate_miner["blinking"]
except LookupError:
pass
return False
async def _get_expected_hashrate(self, web_brief: dict = None) -> Optional[float]:
if web_brief is None:
try:
web_brief = await self.web.brief()
except APIError:
pass
if web_brief is not None:
try:
return round(web_brief["hashrate_ideal"], 2)
except LookupError:
pass
async def _get_wattage_limit(
self, web_miner_config: dict = None
) -> Optional[float]:
if web_miner_config is None:
try:
web_miner_config = await self.web.get_miner_config()
except APIError:
pass
if web_miner_config is not None:
try:
return web_miner_config["mode"]["concorde"]["power-target"]
except LookupError:
pass

View File

@@ -553,7 +553,17 @@ class MinerFactory:
and self._parse_web_type(x[0], x[1]) is not None,
)
if text is not None:
return self._parse_web_type(text, resp)
mtype = self._parse_web_type(text, resp)
if mtype == MinerTypes.ANTMINER:
# could still be mara
auth = httpx.DigestAuth("root", "root")
res = await self.send_web_command(
ip, "/kaonsu/v1/brief", auth=auth
)
if res is not None:
mtype = MinerTypes.MARATHON
return mtype
@staticmethod
async def _web_ping(

View File

@@ -1,30 +1,56 @@
from __future__ import annotations
import asyncio
import json
from typing import Any
import httpx
from pyasic import settings
from pyasic.web.antminer import AntminerModernWebAPI
from pyasic.web.base import BaseWebAPI
class MaraWebAPI(AntminerModernWebAPI):
class MaraWebAPI(BaseWebAPI):
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(
async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict:
async with httpx.AsyncClient(transport=settings.transport()) as client:
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
) -> dict:
auth = httpx.DigestAuth(self.username, self.pwd)
try:
url = f"http://{self.ip}:{self.port}/kaonsu/v1/{command}"
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 send_command(
self,
command: str | bytes,
ignore_errors: bool = False,
@@ -56,76 +82,66 @@ class MaraWebAPI(AntminerModernWebAPI):
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 ping(self):
return await self.send_command("ping")
async def get_locate_miner(self):
return await self.send_command("locate_miner")
async def set_locate_miner(self, blinking: bool):
return await self.send_command("locate_miner", blinking=blinking)
async def reboot(self):
return await self.send_command("maintenance", type="reboot")
async def reset(self):
return await self.send_command("maintenance", type="reset")
async def reload(self):
return await self.send_command("maintenance", type="reload")
async def set_password(self, new_pwd: str):
return await self.send_command(
"maintenance",
type="passwd",
params={"curPwd": self.pwd, "confirmPwd": self.pwd, "newPwd": new_pwd},
)
async def get_network_config(self):
return await self.send_command("network_config")
async def set_network_config(self, **params):
return await self.send_command("network_config", **params)
async def get_miner_config(self):
return await self.send_command("miner_config")
async def set_miner_config(self, **params):
return await self.send_command("miner_config", **params)
async def fans(self):
return await self.send_command("fans")
async def log(self):
return await self.send_command("log")
async def overview(self):
return await self.send_command("overview")
async def connections(self):
return await self.send_command("connections")
async def controlboard_info(self):
return await self.send_command("controlboard_info")
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")
async def pools(self):
return await self.send_command("pools")