feature: add BTMiner V3 configuration

This commit is contained in:
Brett Rowan
2025-08-15 14:30:59 -06:00
parent 56ad6cbc6f
commit debd4d2d4d
16 changed files with 128 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,6 @@ class ApiVersionServiceStub(betterproto.ServiceStub):
class ApiVersionServiceBase(ServiceBase):
async def get_api_version(
self, api_version_request: "ApiVersionRequest"
) -> "ApiVersion":

View File

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

View File

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

View File

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