feature: add BTMiner V3 configuration
This commit is contained in:
@@ -85,6 +85,13 @@ class MinerConfig(BaseModel):
|
||||
**self.temperature.as_wm(),
|
||||
}
|
||||
|
||||
def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
|
||||
"""Generates the configuration in the format suitable for Whatsminers running BTMiner V3."""
|
||||
return {
|
||||
"set.miner.pools": self.pools.as_btminer_v3()
|
||||
** self.mining_mode.as_btminer_v3()
|
||||
}
|
||||
|
||||
def as_am_old(self, user_suffix: str | None = None) -> dict:
|
||||
"""Generates the configuration in the format suitable for old versions of Antminers."""
|
||||
return {
|
||||
@@ -355,3 +362,14 @@ class MinerConfig(BaseModel):
|
||||
@classmethod
|
||||
def from_hammer(cls, *args, **kwargs) -> "MinerConfig":
|
||||
return cls.from_am_modern(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_btminer_v3(
|
||||
cls, rpc_pools: dict, rpc_settings: dict, rpc_device_info: dict
|
||||
) -> "MinerConfig":
|
||||
return cls(
|
||||
pools=PoolConfig.from_btminer_v3(rpc_pools=rpc_pools["msg"]),
|
||||
mining_mode=MiningModeConfig.from_btminer_v3(
|
||||
rpc_device_info=rpc_device_info, rpc_settings=rpc_settings
|
||||
),
|
||||
)
|
||||
|
||||
@@ -107,6 +107,9 @@ class MinerConfigValue(BaseModel):
|
||||
def as_wm(self) -> dict:
|
||||
return {}
|
||||
|
||||
def as_btminer_v3(self) -> dict:
|
||||
return {}
|
||||
|
||||
def as_inno(self) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ class MiningModeNormal(MinerConfigValue):
|
||||
def as_wm(self) -> dict:
|
||||
return {"mode": self.mode}
|
||||
|
||||
def as_btminer_v3(self) -> dict:
|
||||
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
|
||||
|
||||
def as_auradine(self) -> dict:
|
||||
return {"mode": {"mode": self.mode}}
|
||||
|
||||
@@ -109,6 +112,9 @@ class MiningModeSleep(MinerConfigValue):
|
||||
def as_wm(self) -> dict:
|
||||
return {"mode": self.mode}
|
||||
|
||||
def as_btminer_v3(self) -> dict:
|
||||
return {"set.miner.service": "stop"}
|
||||
|
||||
def as_auradine(self) -> dict:
|
||||
return {"mode": {"sleep": "on"}}
|
||||
|
||||
@@ -149,6 +155,9 @@ class MiningModeLPM(MinerConfigValue):
|
||||
def as_wm(self) -> dict:
|
||||
return {"mode": self.mode}
|
||||
|
||||
def as_btminer_v3(self) -> dict:
|
||||
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
|
||||
|
||||
def as_auradine(self) -> dict:
|
||||
return {"mode": {"mode": "eco"}}
|
||||
|
||||
@@ -179,6 +188,9 @@ class MiningModeHPM(MinerConfigValue):
|
||||
def as_wm(self) -> dict:
|
||||
return {"mode": self.mode}
|
||||
|
||||
def as_btminer_v3(self) -> dict:
|
||||
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
|
||||
|
||||
def as_auradine(self) -> dict:
|
||||
return {"mode": {"mode": "turbo"}}
|
||||
|
||||
@@ -222,6 +234,9 @@ class MiningModePowerTune(MinerConfigValue):
|
||||
return {"mode": self.mode, self.mode: {"wattage": self.power}}
|
||||
return {}
|
||||
|
||||
def as_btminer_v3(self) -> dict:
|
||||
return {"set.miner.service": "start", "set.miner.power_limit": self.power}
|
||||
|
||||
def as_bosminer(self) -> dict:
|
||||
tuning_cfg = {"enabled": True, "mode": "power_target"}
|
||||
if self.power is not None:
|
||||
@@ -789,6 +804,26 @@ class MiningModeConfig(MinerConfigOption):
|
||||
except LookupError:
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_btminer_v3(cls, rpc_device_info: dict, rpc_settings: dict):
|
||||
try:
|
||||
is_mining = rpc_device_info["msg"]["miner"]["working"] == "true"
|
||||
if not is_mining:
|
||||
return cls.sleep()
|
||||
power_limit = rpc_settings["msg"]["power-limit"]
|
||||
if not power_limit == 0:
|
||||
return cls.power_tuning(power=power_limit)
|
||||
power_mode = rpc_settings["msg"]["power-mode"]
|
||||
if power_mode == "normal":
|
||||
return cls.normal()
|
||||
if power_mode == "high":
|
||||
return cls.high()
|
||||
if power_mode == "low":
|
||||
return cls.low()
|
||||
|
||||
except LookupError:
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_mara(cls, web_config: dict):
|
||||
try:
|
||||
|
||||
@@ -64,6 +64,13 @@ class Pool(MinerConfigValue):
|
||||
f"passwd_{idx}": self.password,
|
||||
}
|
||||
|
||||
def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
|
||||
return {
|
||||
f"pool": self.url,
|
||||
f"worker": f"{self.user}{user_suffix or ''}",
|
||||
f"passwd": self.password,
|
||||
}
|
||||
|
||||
def as_am_old(self, idx: int = 1, user_suffix: str | None = None) -> dict:
|
||||
return {
|
||||
f"_ant_pool{idx}url": self.url,
|
||||
@@ -148,6 +155,10 @@ class Pool(MinerConfigValue):
|
||||
def from_api(cls, api_pool: dict) -> "Pool":
|
||||
return cls(url=api_pool["URL"], user=api_pool["User"], password="x")
|
||||
|
||||
@classmethod
|
||||
def from_btminer_v3(cls, api_pool: dict) -> "Pool":
|
||||
return cls(url=api_pool["url"], user=api_pool["account"], password="x")
|
||||
|
||||
@classmethod
|
||||
def from_epic(cls, api_pool: dict) -> "Pool":
|
||||
return cls(
|
||||
@@ -296,6 +307,9 @@ class PoolGroup(MinerConfigValue):
|
||||
idx += 1
|
||||
return pools
|
||||
|
||||
def as_btminer_v3(self, user_suffix: str | None = None) -> list:
|
||||
return [pool.as_btminer_v3(user_suffix) for pool in self.pools[:3]]
|
||||
|
||||
def as_am_old(self, user_suffix: str | None = None) -> dict:
|
||||
pools = {}
|
||||
idx = 0
|
||||
@@ -385,6 +399,13 @@ class PoolGroup(MinerConfigValue):
|
||||
pools.append(Pool.from_api(pool))
|
||||
return cls(pools=pools)
|
||||
|
||||
@classmethod
|
||||
def from_btminer_v3(cls, api_pool_list: list) -> "PoolGroup":
|
||||
pools = []
|
||||
for pool in api_pool_list:
|
||||
pools.append(Pool.from_btminer_v3(pool))
|
||||
return cls(pools=pools)
|
||||
|
||||
@classmethod
|
||||
def from_epic(cls, api_pool_list: list) -> "PoolGroup":
|
||||
pools = []
|
||||
@@ -513,6 +534,11 @@ class PoolConfig(MinerConfigValue):
|
||||
return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)}
|
||||
return {"pools": PoolGroup().as_wm()}
|
||||
|
||||
def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
|
||||
if len(self.groups) > 0:
|
||||
return {"pools": self.groups[0].as_btminer_v3(user_suffix=user_suffix)}
|
||||
return {"pools": PoolGroup().as_btminer_v3()}
|
||||
|
||||
def as_am_old(self, user_suffix: str | None = None) -> dict:
|
||||
if len(self.groups) > 0:
|
||||
return self.groups[0].as_am_old(user_suffix=user_suffix)
|
||||
@@ -598,6 +624,16 @@ class PoolConfig(MinerConfigValue):
|
||||
|
||||
return cls(groups=[PoolGroup.from_api(pool_data)])
|
||||
|
||||
@classmethod
|
||||
def from_btminer_v3(cls, rpc_pools: dict) -> "PoolConfig":
|
||||
try:
|
||||
pool_data = rpc_pools["pools"]
|
||||
except KeyError:
|
||||
return PoolConfig.default()
|
||||
pool_data = sorted(pool_data, key=lambda x: int(x["id"]))
|
||||
|
||||
return cls(groups=[PoolGroup.from_btminer_v3(pool_data)])
|
||||
|
||||
@classmethod
|
||||
def from_epic(cls, web_conf: dict) -> "PoolConfig":
|
||||
pool_data = web_conf["StratumConfigs"]
|
||||
|
||||
@@ -77,7 +77,6 @@ class HashBoard(BaseModel):
|
||||
raise KeyError(f"{item}")
|
||||
|
||||
def as_influxdb(self, key_root: str, level_delimiter: str = ".") -> str:
|
||||
|
||||
def serialize_int(key: str, value: int) -> str:
|
||||
return f"{key}={value}"
|
||||
|
||||
|
||||
@@ -94,7 +94,6 @@ class PoolMetrics(BaseModel):
|
||||
return (value / total) * 100
|
||||
|
||||
def as_influxdb(self, key_root: str, level_delimiter: str = ".") -> str:
|
||||
|
||||
def serialize_int(key: str, value: int) -> str:
|
||||
return f"{key}={value}"
|
||||
|
||||
|
||||
@@ -167,7 +167,6 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3):
|
||||
|
||||
|
||||
class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
|
||||
|
||||
data_locations = AVALON_NANO3S_DATA_LOC
|
||||
|
||||
async def _get_wattage(self, rpc_estats: dict = None) -> Optional[int]:
|
||||
@@ -212,7 +211,6 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
|
||||
pass
|
||||
|
||||
if rpc_estats is not None:
|
||||
|
||||
try:
|
||||
unparsed_estats = rpc_estats["STATS"][0]["MM ID0"]
|
||||
parsed_estats = self.parse_estats(unparsed_estats)
|
||||
|
||||
@@ -287,7 +287,6 @@ class AvalonMiner(CGMiner):
|
||||
return hashboards
|
||||
|
||||
for board in range(self.expected_hashboards):
|
||||
|
||||
try:
|
||||
board_hr = parsed_estats["STATS"][0]["MM ID0"]["MGHS"]
|
||||
if isinstance(board_hr, list):
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
import semver
|
||||
|
||||
from pyasic.config import MinerConfig, MiningModeConfig
|
||||
from pyasic.config import MinerConfig, MiningModeConfig, PoolConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import MinerErrorData, WhatsminerError
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
@@ -47,14 +47,14 @@ class BTMiner(StockFirmware):
|
||||
)
|
||||
except ValueError:
|
||||
return BTMinerV2
|
||||
if semantic.major > 2024 and semantic.minor > 11:
|
||||
if semantic > semver.Version(major=2024, minor=11, patch=0):
|
||||
return BTMinerV3
|
||||
return BTMinerV2
|
||||
|
||||
inject = get_new(version)
|
||||
|
||||
if inject not in bases:
|
||||
bases = (inject,) + bases
|
||||
bases = (inject,) + bases
|
||||
|
||||
cls = type(cls.__name__, bases, {})(ip=ip, version=version)
|
||||
return cls
|
||||
|
||||
@@ -859,6 +859,34 @@ class BTMinerV3(StockFirmware):
|
||||
supports_autotuning = True
|
||||
supports_power_modes = True
|
||||
|
||||
async def get_config(self) -> MinerConfig:
|
||||
pools = None
|
||||
settings = None
|
||||
device_info = None
|
||||
try:
|
||||
pools = await self.rpc.get_miner_status_pools()
|
||||
settings = await self.rpc.get_miner_setting()
|
||||
device_info = await self.rpc.get_device_info()
|
||||
except APIError as e:
|
||||
logging.warning(e)
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
self.config = MinerConfig.from_btminer_v3(
|
||||
rpc_pools=pools, rpc_settings=settings, rpc_device_info=device_info
|
||||
)
|
||||
|
||||
return self.config
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
self.config = config
|
||||
|
||||
conf = config.as_btminer_v3(user_suffix=user_suffix)
|
||||
|
||||
await asyncio.gather(
|
||||
*[self.rpc.send_command(k, parameters=v) for k, v in conf.values()]
|
||||
)
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
try:
|
||||
data = await self.rpc.set_system_led()
|
||||
|
||||
@@ -78,7 +78,6 @@ GOLDSHELL_BYTE_DATA_LOC = DataLocations(
|
||||
|
||||
|
||||
class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
data_locations = GOLDSHELL_BYTE_DATA_LOC
|
||||
|
||||
cgdev: dict | None = None
|
||||
@@ -101,11 +100,9 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
total_uptime_mins = 0
|
||||
|
||||
for minfo in self.cgdev.get("minfos", []):
|
||||
|
||||
algo_name = minfo.get("name")
|
||||
|
||||
for info in minfo.get("infos", []):
|
||||
|
||||
self.expected_hashboards += 1
|
||||
self.expected_fans += 1
|
||||
|
||||
@@ -177,7 +174,6 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
if rpc_devs is not None:
|
||||
for board in rpc_devs.get("DEVS", []):
|
||||
|
||||
algo_name = board.get("pool")
|
||||
|
||||
if algo_name == ALGORITHM_SCRYPT_NAME:
|
||||
|
||||
@@ -1223,6 +1223,9 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
|
||||
async def get_miner_status_edevs(self) -> dict | None:
|
||||
return await self.send_command("get.miner.status", parameters="edevs")
|
||||
|
||||
async def get_miner_status_pools(self) -> dict | None:
|
||||
return await self.send_command("get.miner.status", parameters="pools")
|
||||
|
||||
async def get_miner_history(self) -> dict | None:
|
||||
data = await self.send_command(
|
||||
"get.miner.history",
|
||||
|
||||
@@ -62,7 +62,6 @@ class AntminerModernWebAPI(BaseWebAPI):
|
||||
auth = httpx.DigestAuth(self.username, self.pwd)
|
||||
try:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
|
||||
if parameters:
|
||||
data = await client.post(
|
||||
url,
|
||||
|
||||
@@ -52,7 +52,6 @@ class ApiVersionServiceStub(betterproto.ServiceStub):
|
||||
|
||||
|
||||
class ApiVersionServiceBase(ServiceBase):
|
||||
|
||||
async def get_api_version(
|
||||
self, api_version_request: "ApiVersionRequest"
|
||||
) -> "ApiVersion":
|
||||
|
||||
@@ -2485,7 +2485,6 @@ class NetworkServiceStub(betterproto.ServiceStub):
|
||||
|
||||
|
||||
class ActionsServiceBase(ServiceBase):
|
||||
|
||||
async def start(self, start_request: "StartRequest") -> "StartResponse":
|
||||
raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
|
||||
|
||||
@@ -2630,7 +2629,6 @@ class ActionsServiceBase(ServiceBase):
|
||||
|
||||
|
||||
class AuthenticationServiceBase(ServiceBase):
|
||||
|
||||
async def login(self, login_request: "LoginRequest") -> "LoginResponse":
|
||||
raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
|
||||
|
||||
@@ -2671,7 +2669,6 @@ class AuthenticationServiceBase(ServiceBase):
|
||||
|
||||
|
||||
class CoolingServiceBase(ServiceBase):
|
||||
|
||||
async def get_cooling_state(
|
||||
self, get_cooling_state_request: "GetCoolingStateRequest"
|
||||
) -> "GetCoolingStateResponse":
|
||||
@@ -2716,7 +2713,6 @@ class CoolingServiceBase(ServiceBase):
|
||||
|
||||
|
||||
class PerformanceServiceBase(ServiceBase):
|
||||
|
||||
async def get_tuner_state(
|
||||
self, get_tuner_state_request: "GetTunerStateRequest"
|
||||
) -> "GetTunerStateResponse":
|
||||
@@ -2986,7 +2982,6 @@ class PerformanceServiceBase(ServiceBase):
|
||||
|
||||
|
||||
class PoolServiceBase(ServiceBase):
|
||||
|
||||
async def get_pool_groups(
|
||||
self, get_pool_groups_request: "GetPoolGroupsRequest"
|
||||
) -> "GetPoolGroupsResponse":
|
||||
@@ -3088,7 +3083,6 @@ class PoolServiceBase(ServiceBase):
|
||||
|
||||
|
||||
class ConfigurationServiceBase(ServiceBase):
|
||||
|
||||
async def get_miner_configuration(
|
||||
self, get_miner_configuration_request: "GetMinerConfigurationRequest"
|
||||
) -> "GetMinerConfigurationResponse":
|
||||
@@ -3133,7 +3127,6 @@ class ConfigurationServiceBase(ServiceBase):
|
||||
|
||||
|
||||
class LicenseServiceBase(ServiceBase):
|
||||
|
||||
async def get_license_state(
|
||||
self, get_license_state_request: "GetLicenseStateRequest"
|
||||
) -> "GetLicenseStateResponse":
|
||||
@@ -3159,7 +3152,6 @@ class LicenseServiceBase(ServiceBase):
|
||||
|
||||
|
||||
class MinerServiceBase(ServiceBase):
|
||||
|
||||
async def get_miner_status(
|
||||
self, get_miner_status_request: "GetMinerStatusRequest"
|
||||
) -> AsyncIterator[GetMinerStatusResponse]:
|
||||
@@ -3325,7 +3317,6 @@ class MinerServiceBase(ServiceBase):
|
||||
|
||||
|
||||
class NetworkServiceBase(ServiceBase):
|
||||
|
||||
async def get_network_configuration(
|
||||
self, get_network_configuration_request: "GetNetworkConfigurationRequest"
|
||||
) -> "GetNetworkConfigurationResponse":
|
||||
|
||||
@@ -60,7 +60,6 @@ class ElphapexWebAPI(BaseWebAPI):
|
||||
auth = httpx.DigestAuth(self.username, self.pwd)
|
||||
try:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
|
||||
if parameters:
|
||||
data = await client.post(
|
||||
url,
|
||||
|
||||
@@ -60,7 +60,6 @@ class HammerWebAPI(BaseWebAPI):
|
||||
auth = httpx.DigestAuth(self.username, self.pwd)
|
||||
try:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
|
||||
if parameters:
|
||||
data = await client.post(
|
||||
url,
|
||||
|
||||
Reference in New Issue
Block a user