Merge branch 'master' into update_firmware_2

This commit is contained in:
Abhishek Patidar
2024-07-26 05:27:30 +05:30
committed by GitHub
78 changed files with 1317 additions and 647 deletions

View File

@@ -140,6 +140,14 @@ class MinerConfig:
**self.pools.as_mara(user_suffix=user_suffix),
}
def as_bitaxe(self, user_suffix: str = None) -> dict:
return {
**self.fan_mode.as_bitaxe(),
**self.temperature.as_bitaxe(),
**self.mining_mode.as_bitaxe(),
**self.pools.as_bitaxe(user_suffix=user_suffix),
}
@classmethod
def from_dict(cls, dict_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from a dictionary."""
@@ -235,3 +243,10 @@ class MinerConfig:
fan_mode=FanModeConfig.from_mara(web_miner_config),
mining_mode=MiningModeConfig.from_mara(web_miner_config),
)
@classmethod
def from_bitaxe(cls, web_system_info: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_bitaxe(web_system_info),
fan_mode=FanModeConfig.from_bitaxe(web_system_info),
)

View File

@@ -60,6 +60,9 @@ class MinerConfigOption(Enum):
def as_mara(self) -> dict:
return self.value.as_mara()
def as_bitaxe(self) -> dict:
return self.value.as_bitaxe()
def __call__(self, *args, **kwargs):
return self.value(*args, **kwargs)
@@ -119,6 +122,9 @@ class MinerConfigValue:
def as_mara(self) -> dict:
return {}
def as_bitaxe(self) -> dict:
return {}
def __getitem__(self, item):
try:
return getattr(self, item)

View File

@@ -80,6 +80,9 @@ class FanModeNormal(MinerConfigValue):
},
}
def as_bitaxe(self) -> dict:
return {"autoFanspeed": 1}
@dataclass
class FanModeManual(MinerConfigValue):
@@ -138,6 +141,9 @@ class FanModeManual(MinerConfigValue):
},
}
def as_bitaxe(self) -> dict:
return {"autoFanspeed": 0, "fanspeed": self.speed}
@dataclass
class FanModeImmersion(MinerConfigValue):
@@ -291,3 +297,10 @@ class FanModeConfig(MinerConfigOption):
except LookupError:
pass
return cls.default()
@classmethod
def from_bitaxe(cls, web_system_info: dict):
if web_system_info["autofanspeed"] == 1:
return cls.normal()
else:
return cls.manual(speed=web_system_info["fanspeed"])

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from dataclasses import dataclass, field
from pyasic.config.base import MinerConfigOption, MinerConfigValue

View File

@@ -127,6 +127,13 @@ class Pool(MinerConfigValue):
}
return {"url": self.url, "user": self.user, "pass": self.password}
def as_bitaxe(self, user_suffix: str = None) -> dict:
return {
"stratumURL": self.url,
"stratumUser": f"{self.user}{user_suffix}",
"stratumPassword": self.password,
}
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "Pool":
return cls(
@@ -194,6 +201,15 @@ class Pool(MinerConfigValue):
password=web_pool["pass"],
)
@classmethod
def from_bitaxe(cls, web_system_info: dict) -> "Pool":
url = f"stratum+tcp://{web_system_info['stratumURL']}:{web_system_info['stratumPort']}"
return cls(
url=url,
user=web_system_info["stratumUser"],
password=web_system_info.get("stratumPassword", ""),
)
@dataclass
class PoolGroup(MinerConfigValue):
@@ -287,6 +303,9 @@ class PoolGroup(MinerConfigValue):
def as_mara(self, user_suffix: str = None) -> list:
return [p.as_mara(user_suffix=user_suffix) for p in self.pools]
def as_bitaxe(self, user_suffix: str = None) -> dict:
return self.pools[0].as_bitaxe(user_suffix=user_suffix)
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolGroup":
cls_conf = {}
@@ -360,6 +379,10 @@ class PoolGroup(MinerConfigValue):
def from_mara(cls, web_config_pools: dict) -> "PoolGroup":
return cls(pools=[Pool.from_mara(pool_conf) for pool_conf in web_config_pools])
@classmethod
def from_bitaxe(cls, web_system_info: dict) -> "PoolGroup":
return cls(pools=[Pool.from_bitaxe(web_system_info)])
@dataclass
class PoolConfig(MinerConfigValue):
@@ -456,6 +479,9 @@ class PoolConfig(MinerConfigValue):
return {"pools": self.groups[0].as_mara(user_suffix=user_suffix)}
return {"pools": []}
def as_bitaxe(self, user_suffix: str = None) -> dict:
return self.groups[0].as_bitaxe(user_suffix=user_suffix)
@classmethod
def from_api(cls, api_pools: dict) -> "PoolConfig":
try:
@@ -514,3 +540,7 @@ class PoolConfig(MinerConfigValue):
@classmethod
def from_mara(cls, web_config: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_mara(web_config["pools"])])
@classmethod
def from_bitaxe(cls, web_system_info: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_bitaxe(web_system_info)])

View File

@@ -1,4 +1,35 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
from urllib.parse import urlparse
class Scheme(Enum):
STRATUM_V1 = "stratum+tcp"
STRATUM_V2 = "stratum2+tcp"
@dataclass
class PoolUrl:
scheme: Scheme
host: str
port: int
pubkey: Optional[str] = None
def __str__(self) -> str:
if self.scheme == Scheme.STRATUM_V2 and self.pubkey:
return f"{self.scheme.value}://{self.host}:{self.port}/{self.pubkey}"
else:
return f"{self.scheme.value}://{self.host}:{self.port}"
@classmethod
def from_str(cls, url: str) -> "PoolUrl":
parsed_url = urlparse(url)
scheme = Scheme(parsed_url.scheme)
host = parsed_url.hostname
port = parsed_url.port
pubkey = parsed_url.path.lstrip("/") if scheme == Scheme.STRATUM_V2 else None
return cls(scheme=scheme, host=host, port=port, pubkey=pubkey)
@dataclass
@@ -19,13 +50,13 @@ class PoolMetrics:
pool_stale_percent: Percentage of stale shares by the pool.
"""
url: PoolUrl
accepted: int = None
rejected: int = None
get_failures: int = None
remote_failures: int = None
active: bool = None
alive: bool = None
url: str = None
index: int = None
user: str = None
pool_rejected_percent: float = field(init=False)

View File

@@ -25,6 +25,7 @@ class MinerMake(str, Enum):
GOLDSHELL = "Goldshell"
AURADINE = "Auradine"
EPIC = "ePIC"
BITAXE = "BitAxe"
def __str__(self):
return self.value

View File

@@ -329,6 +329,15 @@ class AuradineModels(str, Enum):
return self.value
class BitAxeModels(str, Enum):
BM1366 = "Ultra"
BM1368 = "Supra"
BM1397 = "Max"
def __str__(self):
return self.value
class MinerModel:
ANTMINER = AntminerModels
WHATSMINER = WhatsminerModels
@@ -337,3 +346,4 @@ class MinerModel:
GOLDSHELL = GoldshellModels
AURADINE = AuradineModels
EPIC = ePICModels
BITAXE = BitAxeModels

View File

@@ -24,6 +24,7 @@ from pyasic.miners.device.models import (
S19jPro,
S19NoPIC,
S19Pro,
S19ProHydro,
)
@@ -57,3 +58,7 @@ class VNishS19j(VNish, S19j):
class VNishS19jPro(VNish, S19jPro):
pass
class VNishS19ProHydro(VNish, S19ProHydro):
pass

View File

@@ -22,6 +22,7 @@ from .S19 import (
VNishS19jPro,
VNishS19NoPIC,
VNishS19Pro,
VNishS19ProHydro,
VNishS19XP,
)
from .T19 import VNishT19

View File

@@ -19,6 +19,8 @@ from typing import List, Optional, Union
from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.errors import APIError
from pyasic.miners.backends.bmminer import BMMiner
from pyasic.miners.backends.cgminer import CGMiner
from pyasic.miners.data import (
@@ -31,8 +33,6 @@ from pyasic.miners.data import (
from pyasic.rpc.antminer import AntminerRPCAPI
from pyasic.ssh.antminer import AntminerModernSSH
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
from pyasic.data.pools import PoolMetrics
from pyasic.errors import APIError
ANTMINER_MODERN_DATA_LOC = DataLocations(
**{
@@ -95,7 +95,7 @@ class AntminerModern(BMMiner):
web: AntminerModernWebAPI
_rpc_cls = AntminerRPCAPI
web: AntminerRPCAPI
rpc: AntminerRPCAPI
_ssh_cls = AntminerModernSSH
ssh: AntminerModernSSH
@@ -156,7 +156,7 @@ class AntminerModern(BMMiner):
await self.send_config(cfg)
return True
async def _get_hostname(self, web_get_system_info: dict = None) -> Union[str, None]:
async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]:
if web_get_system_info is None:
try:
web_get_system_info = await self.web.get_system_info()
@@ -169,7 +169,7 @@ class AntminerModern(BMMiner):
except KeyError:
pass
async def _get_mac(self, web_get_system_info: dict = None) -> Union[str, None]:
async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]:
if web_get_system_info is None:
try:
web_get_system_info = await self.web.get_system_info()
@@ -264,7 +264,9 @@ class AntminerModern(BMMiner):
pass
return self.light
async def _get_expected_hashrate(self, rpc_stats: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, rpc_stats: dict = None
) -> Optional[AlgoHashRate]:
if rpc_stats is None:
try:
rpc_stats = await self.rpc.stats()
@@ -368,6 +370,8 @@ class AntminerModern(BMMiner):
try:
pools = rpc_pools.get("POOLS", [])
for pool_info in pools:
url = pool_info.get("URL")
pool_url = PoolUrl.from_str(url) if url else None
pool_data = PoolMetrics(
accepted=pool_info.get("Accepted"),
rejected=pool_info.get("Rejected"),
@@ -375,10 +379,9 @@ class AntminerModern(BMMiner):
remote_failures=pool_info.get("Remote Failures"),
active=pool_info.get("Stratum Active"),
alive=pool_info.get("Status") == "Alive",
url=pool_info.get("URL"),
url=pool_url,
user=pool_info.get("User"),
index=pool_info.get("POOL")
index=pool_info.get("POOL"),
)
pools_data.append(pool_data)
except LookupError:
@@ -446,7 +449,7 @@ class AntminerOld(CGMiner):
self.config = config
await self.web.set_miner_conf(config.as_am_old(user_suffix=user_suffix))
async def _get_mac(self) -> Union[str, None]:
async def _get_mac(self) -> Optional[str]:
try:
data = await self.web.get_system_info()
if data:

View File

@@ -281,7 +281,7 @@ class Auradine(StockFirmware):
except LookupError:
pass
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
if rpc_summary is None:
try:
rpc_summary = await self.rpc.summary()

View File

@@ -173,7 +173,7 @@ class AvalonMiner(CGMiner):
except (KeyError, ValueError):
pass
async def _get_hashrate(self, rpc_devs: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_devs: dict = None) -> Optional[AlgoHashRate]:
if rpc_devs is None:
try:
rpc_devs = await self.rpc.devs()
@@ -238,7 +238,9 @@ class AvalonMiner(CGMiner):
return hashboards
async def _get_expected_hashrate(self, rpc_stats: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, rpc_stats: dict = None
) -> Optional[AlgoHashRate]:
if rpc_stats is None:
try:
rpc_stats = await self.rpc.stats()

View File

@@ -105,7 +105,7 @@ class BFGMiner(StockFirmware):
return self.fw_ver
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
# get hr from API
if rpc_summary is None:
try:
@@ -207,7 +207,9 @@ class BFGMiner(StockFirmware):
return fans
async def _get_expected_hashrate(self, rpc_stats: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, rpc_stats: dict = None
) -> Optional[AlgoHashRate]:
# X19 method, not sure compatibility
if rpc_stats is None:
try:

View File

@@ -0,0 +1,189 @@
from typing import List, Optional
from pyasic import APIError, MinerConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.device import MinerFirmware
from pyasic.miners.base import BaseMiner
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.web.bitaxe import BitAxeWebAPI
BITAXE_DATA_LOC = DataLocations(
**{
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[WebAPICommand("web_system_info", "system/info")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[WebAPICommand("web_system_info", "system/info")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_system_info", "system/info")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[WebAPICommand("web_system_info", "system/info")],
),
str(DataOptions.HOSTNAME): DataFunction(
"_get_hostname",
[WebAPICommand("web_system_info", "system/info")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[WebAPICommand("web_system_info", "system/info")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[WebAPICommand("web_system_info", "system/info")],
),
str(DataOptions.API_VERSION): DataFunction(
"_get_api_ver",
[WebAPICommand("web_system_info", "system/info")],
),
}
)
class BitAxe(BaseMiner):
"""Handler for BitAxe"""
web: BitAxeWebAPI
_web_cls = BitAxeWebAPI
firmware = MinerFirmware.STOCK
data_locations = BITAXE_DATA_LOC
async def reboot(self) -> bool:
await self.web.restart()
return True
async def get_config(self) -> MinerConfig:
web_system_info = await self.web.system_info()
return MinerConfig.from_bitaxe(web_system_info)
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
await self.web.update_settings(**config.as_bitaxe())
async def _get_wattage(self, web_system_info: dict = None) -> Optional[int]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return round(web_system_info["power"])
except KeyError:
pass
async def _get_hashrate(
self, web_system_info: dict = None
) -> Optional[AlgoHashRate]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return AlgoHashRate.SHA256(
web_system_info["hashRate"], HashUnit.SHA256.GH
).into(self.algo.unit.default)
except KeyError:
pass
async def _get_uptime(self, web_system_info: dict = None) -> Optional[int]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return web_system_info["uptimeSeconds"]
except KeyError:
pass
async def _get_hashboards(self, web_system_info: dict = None) -> List[HashBoard]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return [
HashBoard(
hashrate=AlgoHashRate.SHA256(
web_system_info["hashRate"], HashUnit.SHA256.GH
).into(self.algo.unit.default),
chip_temp=web_system_info.get("temp"),
temp=web_system_info.get("vrTemp"),
chips=web_system_info.get("asicCount", 1),
expected_chips=self.expected_chips,
missing=False,
active=True,
voltage=web_system_info.get("voltage"),
)
]
except KeyError:
pass
return []
async def _get_fans(self, web_system_info: dict = None) -> List[Fan]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return [Fan(speed=web_system_info["fanrpm"])]
except KeyError:
pass
return []
async def _get_hostname(self, web_system_info: dict = None) -> Optional[str]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return web_system_info["hostname"]
except KeyError:
pass
async def _get_api_ver(self, web_system_info: dict = None) -> Optional[str]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return web_system_info["version"]
except KeyError:
pass
async def _get_fw_ver(self, web_system_info: dict = None) -> Optional[str]:
if web_system_info is None:
try:
web_system_info = await self.web.system_info()
except APIError:
pass
if web_system_info is not None:
try:
return web_system_info["version"]
except KeyError:
pass

View File

@@ -109,7 +109,7 @@ class BMMiner(StockFirmware):
return self.fw_ver
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
# get hr from API
if rpc_summary is None:
try:
@@ -223,7 +223,9 @@ class BMMiner(StockFirmware):
return fans
async def _get_expected_hashrate(self, rpc_stats: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, rpc_stats: dict = None
) -> Optional[AlgoHashRate]:
# X19 method, not sure compatibility
if rpc_stats is None:
try:

View File

@@ -26,6 +26,7 @@ from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.errors import APIError
from pyasic.miners.data import (
DataFunction,
@@ -39,7 +40,6 @@ from pyasic.rpc.bosminer import BOSMinerRPCAPI
from pyasic.ssh.braiins_os import BOSMinerSSH
from pyasic.web.braiins_os import BOSerWebAPI, BOSMinerWebAPI
from pyasic.web.braiins_os.proto.braiins.bos.v1 import SaveAction
from pyasic.data.pools import PoolMetrics
BOSMINER_DATA_LOC = DataLocations(
**{
@@ -349,7 +349,7 @@ class BOSMiner(BraiinsOSFirmware):
return None
return hostname
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
if rpc_summary is None:
try:
rpc_summary = await self.rpc.summary()
@@ -525,7 +525,9 @@ class BOSMiner(BraiinsOSFirmware):
except (TypeError, AttributeError):
return self.light
async def _get_expected_hashrate(self, rpc_devs: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, rpc_devs: dict = None
) -> Optional[AlgoHashRate]:
if rpc_devs is None:
try:
rpc_devs = await self.rpc.devs()
@@ -590,6 +592,8 @@ class BOSMiner(BraiinsOSFirmware):
try:
pools = rpc_pools.get("POOLS", [])
for pool_info in pools:
url = pool_info.get("URL")
pool_url = PoolUrl.from_str(url) if url else None
pool_data = PoolMetrics(
accepted=pool_info.get("Accepted"),
rejected=pool_info.get("Rejected"),
@@ -597,17 +601,15 @@ class BOSMiner(BraiinsOSFirmware):
remote_failures=pool_info.get("Remote Failures"),
active=pool_info.get("Stratum Active"),
alive=pool_info.get("Status") == "Alive",
url=pool_info.get("URL"),
url=pool_url,
user=pool_info.get("User"),
index=pool_info.get("POOL"),
)
pools_data.append(pool_data)
except LookupError:
pass
return pools_data
async def upgrade_firmware(self, file: Path):
"""
Upgrade the firmware of the BOSMiner device.
@@ -866,7 +868,7 @@ class BOSer(BraiinsOSFirmware):
except LookupError:
pass
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
if rpc_summary is None:
try:
rpc_summary = await self.rpc.summary()
@@ -883,7 +885,7 @@ class BOSer(BraiinsOSFirmware):
async def _get_expected_hashrate(
self, grpc_miner_details: dict = None
) -> Optional[float]:
) -> Optional[AlgoHashRate]:
if grpc_miner_details is None:
try:
grpc_miner_details = await self.web.get_miner_details()

View File

@@ -15,9 +15,10 @@
# ------------------------------------------------------------------------------
import logging
from typing import List, Optional
import aiofiles
from pathlib import Path
from typing import List, Optional
import aiofiles
from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
@@ -388,7 +389,7 @@ class BTMiner(StockFirmware):
return hostname
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
if rpc_summary is None:
try:
rpc_summary = await self.rpc.summary()
@@ -564,7 +565,9 @@ class BTMiner(StockFirmware):
pass
return errors
async def _get_expected_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, rpc_summary: dict = None
) -> Optional[AlgoHashRate]:
if rpc_summary is None:
try:
rpc_summary = await self.rpc.summary()
@@ -675,17 +678,24 @@ class BTMiner(StockFirmware):
result = await self.rpc.update_firmware(upgrade_contents)
logging.info("Firmware upgrade process completed successfully for Whatsminer.")
logging.info(
"Firmware upgrade process completed successfully for Whatsminer."
)
return result
except FileNotFoundError as e:
logging.error(f"File not found during the firmware upgrade process: {e}")
raise
except ValueError as e:
logging.error(f"Validation error occurred during the firmware upgrade process: {e}")
logging.error(
f"Validation error occurred during the firmware upgrade process: {e}"
)
raise
except OSError as e:
logging.error(f"OS error occurred during the firmware upgrade process: {e}")
raise
except Exception as e:
logging.error(f"An unexpected error occurred during the firmware upgrade process: {e}", exc_info=True)
logging.error(
f"An unexpected error occurred during the firmware upgrade process: {e}",
exc_info=True,
)
raise

View File

@@ -109,7 +109,7 @@ class CGMiner(StockFirmware):
return self.fw_ver
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
if rpc_summary is None:
try:
rpc_summary = await self.rpc.summary()

View File

@@ -14,14 +14,15 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pathlib import Path
from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.data.pools import PoolMetrics
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.data.pools import PoolMetrics
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.miners.device.firmware import ePICFirmware
from pyasic.web.epic import ePICWebAPI
@@ -220,7 +221,7 @@ class ePIC(ePICFirmware):
except KeyError:
pass
async def _get_hashrate(self, web_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, web_summary: dict = None) -> Optional[AlgoHashRate]:
if web_summary is None:
try:
web_summary = await self.web.summary()
@@ -239,7 +240,9 @@ class ePIC(ePICFirmware):
except (LookupError, ValueError, TypeError):
pass
async def _get_expected_hashrate(self, web_summary: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, web_summary: dict = None
) -> Optional[AlgoHashRate]:
if web_summary is None:
try:
web_summary = await self.web.summary()
@@ -449,4 +452,18 @@ class ePIC(ePICFirmware):
)
return pool_data
except LookupError:
pass
pass
async def upgrade_firmware(self, file: Path | str, keep_settings: bool = True) -> bool:
"""
Upgrade the firmware of the ePIC miner device.
Args:
file (Path | str): The local file path of the firmware to be uploaded.
keep_settings (bool): Whether to keep the current settings after the update.
Returns:
bool: Whether the firmware update succeeded.
"""
return await self.web.system_update(file=file, keep_settings=keep_settings)

View File

@@ -169,7 +169,7 @@ class Innosilicon(CGMiner):
async def _get_hashrate(
self, rpc_summary: dict = None, web_get_all: dict = None
) -> Optional[float]:
) -> Optional[AlgoHashRate]:
if web_get_all:
web_get_all = web_get_all["all"]

View File

@@ -17,6 +17,7 @@ from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import LuxOSFirmware
@@ -51,6 +52,9 @@ LUXMINER_DATA_LOC = DataLocations(
str(DataOptions.UPTIME): DataFunction(
"_get_uptime", [RPCAPICommand("rpc_stats", "stats")]
),
str(DataOptions.POOLS): DataFunction(
"_get_pools", [RPCAPICommand("rpc_pools", "pools")]
),
}
)
@@ -162,7 +166,7 @@ class LUXMiner(LuxOSFirmware):
return mac
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
if rpc_summary is None:
try:
rpc_summary = await self.rpc.summary()
@@ -263,7 +267,9 @@ class LUXMiner(LuxOSFirmware):
fans.append(Fan())
return fans
async def _get_expected_hashrate(self, rpc_stats: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, rpc_stats: dict = None
) -> Optional[AlgoHashRate]:
if rpc_stats is None:
try:
rpc_stats = await self.rpc.stats()
@@ -295,3 +301,33 @@ class LUXMiner(LuxOSFirmware):
return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError:
pass
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
if rpc_pools is None:
try:
rpc_pools = await self.rpc.pools()
except APIError:
pass
pools_data = []
if rpc_pools is not None:
try:
pools = rpc_pools.get("POOLS", [])
for pool_info in pools:
url = pool_info.get("URL")
pool_url = PoolUrl.from_str(url) if url else None
pool_data = PoolMetrics(
accepted=pool_info.get("Accepted"),
rejected=pool_info.get("Rejected"),
get_failures=pool_info.get("Get Failures"),
remote_failures=pool_info.get("Remote Failures"),
active=pool_info.get("Stratum Active"),
alive=pool_info.get("Status") == "Alive",
url=pool_url,
user=pool_info.get("User"),
index=pool_info.get("POOL"),
)
pools_data.append(pool_data)
except LookupError:
pass
return pools_data

View File

@@ -225,7 +225,7 @@ class MaraMiner(MaraFirmware):
except LookupError:
pass
async def _get_hashrate(self, web_brief: dict = None) -> Optional[float]:
async def _get_hashrate(self, web_brief: dict = None) -> Optional[AlgoHashRate]:
if web_brief is None:
try:
web_brief = await self.web.brief()
@@ -271,7 +271,9 @@ class MaraMiner(MaraFirmware):
pass
return False
async def _get_expected_hashrate(self, web_brief: dict = None) -> Optional[float]:
async def _get_expected_hashrate(
self, web_brief: dict = None
) -> Optional[AlgoHashRate]:
if web_brief is None:
try:
web_brief = await self.web.brief()
@@ -288,7 +290,7 @@ class MaraMiner(MaraFirmware):
async def _get_wattage_limit(
self, web_miner_config: dict = None
) -> Optional[float]:
) -> Optional[AlgoHashRate]:
if web_miner_config is None:
try:
web_miner_config = await self.web.get_miner_config()

View File

@@ -15,7 +15,7 @@
from typing import List, Optional, Tuple
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard
from pyasic.data import AlgoHashRate, Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData
from pyasic.miners.base import BaseMiner
from pyasic.rpc.unknown import UnknownRPCAPI
@@ -80,7 +80,7 @@ class UnknownMiner(BaseMiner):
async def _get_hostname(self) -> Optional[str]:
return None
async def _get_hashrate(self) -> Optional[float]:
async def _get_hashrate(self) -> Optional[AlgoHashRate]:
return None
async def _get_hashboards(self) -> List[HashBoard]:
@@ -113,7 +113,7 @@ class UnknownMiner(BaseMiner):
async def _get_fault_light(self) -> bool:
return False
async def _get_expected_hashrate(self) -> Optional[float]:
async def _get_expected_hashrate(self) -> Optional[AlgoHashRate]:
return None
async def _is_mining(self, *args, **kwargs) -> Optional[bool]:

View File

@@ -193,7 +193,7 @@ class VNish(VNishFirmware, BMMiner):
except KeyError:
pass
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[float]:
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
# get hr from API
if rpc_summary is None:
try:

View File

@@ -29,4 +29,4 @@ class M3X(BTMiner):
class M2X(BTMiner):
pass
pass

View File

@@ -19,7 +19,7 @@ import warnings
from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data import AlgoHashRate, Fan, HashBoard, MinerData
from pyasic.data.device import DeviceInfo
from pyasic.data.error_codes import MinerErrorData
from pyasic.data.pools import PoolMetrics
@@ -238,7 +238,7 @@ class MinerProtocol(Protocol):
"""
return await self._get_hostname()
async def get_hashrate(self) -> Optional[float]:
async def get_hashrate(self) -> Optional[AlgoHashRate]:
"""Get the hashrate of the miner and return it as a float in TH/s.
Returns:
@@ -318,7 +318,7 @@ class MinerProtocol(Protocol):
"""
return await self._get_fault_light()
async def get_expected_hashrate(self) -> Optional[float]:
async def get_expected_hashrate(self) -> Optional[AlgoHashRate]:
"""Get the nominal hashrate from factory if available.
Returns:
@@ -362,7 +362,7 @@ class MinerProtocol(Protocol):
async def _get_hostname(self) -> Optional[str]:
pass
async def _get_hashrate(self) -> Optional[float]:
async def _get_hashrate(self) -> Optional[AlgoHashRate]:
pass
async def _get_hashboards(self) -> List[HashBoard]:
@@ -392,7 +392,7 @@ class MinerProtocol(Protocol):
async def _get_fault_light(self) -> Optional[bool]:
pass
async def _get_expected_hashrate(self) -> Optional[float]:
async def _get_expected_hashrate(self) -> Optional[AlgoHashRate]:
pass
async def _is_mining(self) -> Optional[bool]:
@@ -574,4 +574,4 @@ class BaseMiner(MinerProtocol):
"""
return False
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)

View File

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

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends.bitaxe import BitAxe
from pyasic.miners.device.models.bitaxe import Ultra
class BitAxeUltra(BitAxe, Ultra):
pass

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends.bitaxe import BitAxe
from pyasic.miners.device.models.bitaxe import Supra
class BitAxeSupra(BitAxe, Supra):
pass

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends.bitaxe import BitAxe
from pyasic.miners.device.models.bitaxe import Max
class BitAxeMax(BitAxe, Max):
pass

View File

@@ -0,0 +1,3 @@
from .BM1366 import BitAxeUltra
from .BM1368 import BitAxeSupra
from .BM1397 import BitAxeMax

View File

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

View File

@@ -44,3 +44,7 @@ class AuradineMake(BaseMiner):
class ePICMake(BaseMiner):
make = MinerMake.EPIC
class BitAxeMake(BaseMiner):
make = MinerMake.BITAXE

View File

@@ -18,6 +18,6 @@ from pyasic.miners.device.makes import AntMinerMake
class L3Plus(AntMinerMake):
raw_model = MinerModel.ANTMINER
raw_model = MinerModel.ANTMINER.L3Plus
expected_chips = 72

View File

@@ -0,0 +1,10 @@
from pyasic.device.models import MinerModel
from pyasic.miners.device.makes import BitAxeMake
class Ultra(BitAxeMake):
raw_model = MinerModel.BITAXE.BM1366
expected_hashboards = 1
expected_chips = 1
expected_fans = 1

View File

@@ -0,0 +1,10 @@
from pyasic.device.models import MinerModel
from pyasic.miners.device.makes import BitAxeMake
class Supra(BitAxeMake):
raw_model = MinerModel.BITAXE.BM1368
expected_hashboards = 1
expected_chips = 1
expected_fans = 1

View File

@@ -0,0 +1,10 @@
from pyasic.device.models import MinerModel
from pyasic.miners.device.makes import BitAxeMake
class Max(BitAxeMake):
raw_model = MinerModel.BITAXE.BM1397
expected_hashboards = 1
expected_chips = 1
expected_fans = 1

View File

@@ -0,0 +1,3 @@
from .BM1366 import Ultra
from .BM1368 import Supra
from .BM1397 import Max

View File

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

View File

@@ -31,8 +31,10 @@ from pyasic.miners.antminer import *
from pyasic.miners.auradine import *
from pyasic.miners.avalonminer import *
from pyasic.miners.backends import *
from pyasic.miners.backends.bitaxe import BitAxe
from pyasic.miners.backends.unknown import UnknownMiner
from pyasic.miners.base import AnyMiner
from pyasic.miners.bitaxe import *
from pyasic.miners.blockminer import *
from pyasic.miners.device.makes import *
from pyasic.miners.goldshell import *
@@ -53,6 +55,7 @@ class MinerTypes(enum.Enum):
EPIC = 9
AURADINE = 10
MARATHON = 11
BITAXE = 12
MINER_CLASSES = {
@@ -383,6 +386,7 @@ MINER_CLASSES = {
"ANTMINER S19J PRO": VNishS19jPro,
"ANTMINER S19A": VNishS19a,
"ANTMINER S19A PRO": VNishS19aPro,
"ANTMINER S19 PRO HYD.": VNishS19ProHydro,
"ANTMINER T19": VNishT19,
"ANTMINER S21": VNishS21,
},
@@ -438,6 +442,12 @@ MINER_CLASSES = {
"ANTMINER S21": MaraS21,
"ANTMINER T21": MaraT21,
},
MinerTypes.BITAXE: {
None: BitAxe,
"BM1368": BitAxeSupra,
"BM1366": BitAxeUltra,
"BM1397": BitAxeMax,
},
}
@@ -514,6 +524,7 @@ class MinerFactory:
MinerTypes.LUX_OS: self.get_miner_model_luxos,
MinerTypes.AURADINE: self.get_miner_model_auradine,
MinerTypes.MARATHON: self.get_miner_model_marathon,
MinerTypes.BITAXE: self.get_miner_model_bitaxe,
}
fn = miner_model_fns.get(miner_type)
@@ -595,6 +606,8 @@ class MinerFactory:
return MinerTypes.WHATSMINER
if "Braiins OS" in web_text:
return MinerTypes.BRAIINS_OS
if "AxeOS" in web_text:
return MinerTypes.BITAXE
if "cloud-box" in web_text:
return MinerTypes.GOLDSHELL
if "AnthillOS" in web_text:
@@ -1008,6 +1021,18 @@ class MinerFactory:
except (TypeError, LookupError):
pass
async def get_miner_model_bitaxe(self, ip: str) -> str | None:
web_json_data = await self.send_web_command(ip, "/api/system/info")
try:
miner_model = web_json_data["ASICModel"]
if miner_model == "":
return None
return miner_model
except (TypeError, LookupError):
pass
miner_factory = MinerFactory()

View File

@@ -92,4 +92,4 @@ class BOSMinerSSH(BaseSSH):
Returns:
str: Status of the LED.
"""
return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off")
return await self.send_command("cat /sys/class/leds/'Red LED'/delay_off")

96
pyasic/web/bitaxe.py Normal file
View File

@@ -0,0 +1,96 @@
from __future__ import annotations
import asyncio
import json
from typing import Any
import httpx
from pyasic import APIError, settings
from pyasic.web.base import BaseWebAPI
class BitAxeWebAPI(BaseWebAPI):
async def send_command(
self,
command: str | bytes,
ignore_errors: bool = False,
allow_warning: bool = True,
privileged: bool = False,
**parameters: Any,
) -> dict:
url = f"http://{self.ip}:{self.port}/api/{command}"
try:
async with httpx.AsyncClient(
transport=settings.transport(),
) as client:
if parameters.get("post", False):
parameters.pop("post")
data = await client.post(
url,
timeout=settings.get("api_function_timeout", 3),
json=parameters,
)
elif parameters.get("patch", False):
parameters.pop("patch")
data = await client.patch(
url,
timeout=settings.get("api_function_timeout", 3),
json=parameters,
)
else:
data = await client.get(url)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict:
"""Execute multiple commands simultaneously on the BitAxe miner.
Args:
*commands (str): Commands to execute.
ignore_errors (bool): Whether to ignore errors during command execution.
allow_warning (bool): Whether to proceed despite warnings.
Returns:
dict: A dictionary containing responses for all commands executed.
"""
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
async def system_info(self):
return await self.send_command("system/info")
async def swarm_info(self):
return await self.send_command("swarm/info")
async def restart(self):
return await self.send_command("system/restart", post=True)
async def update_settings(self, **config):
return await self.send_command("system", patch=True, **config)

View File

@@ -15,9 +15,12 @@
# ------------------------------------------------------------------------------
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from typing import Any
import aiofiles
import httpx
from pyasic import settings
@@ -46,6 +49,14 @@ class ePICWebAPI(BaseWebAPI):
async with httpx.AsyncClient(transport=settings.transport()) as client:
for retry_cnt in range(settings.get("get_data_retries", 1)):
try:
if parameters.get("form") is not None:
form_data = parameters["form"]
form_data.add_field("password", self.pwd)
response = await client.post(
f"http://{self.ip}:{self.port}/{command}",
timeout=5,
data=form_data,
)
if post:
response = await client.post(
f"http://{self.ip}:{self.port}/{command}",
@@ -135,3 +146,22 @@ class ePICWebAPI(BaseWebAPI):
async def capabilities(self) -> dict:
return await self.send_command("capabilities")
async def system_update(self, file: Path | str, keep_settings: bool = True):
"""Perform a system update by uploading a firmware file and sending a
command to initiate the update."""
# calculate the SHA256 checksum of the firmware file
sha256_hash = hashlib.sha256()
async with aiofiles.open(str(file), "rb") as f:
while chunk := await f.read(8192):
sha256_hash.update(chunk)
checksum = sha256_hash.hexdigest()
# prepare the multipart/form-data request
form_data = aiohttp.FormData()
form_data.add_field("checksum", checksum)
form_data.add_field("keepsettings", str(keep_settings).lower())
form_data.add_field("update.zip", open(file, "rb"), filename="update.zip")
await self.send_command("systemupdate", form=form_data)