Merge branch 'master' into update_firmware_2

This commit is contained in:
Abhishek Patidar
2024-06-22 07:57:48 +05:30
committed by GitHub
15 changed files with 511 additions and 388 deletions

View File

@@ -17,8 +17,8 @@ from dataclasses import asdict, dataclass, field
from pyasic.config.fans import FanModeConfig from pyasic.config.fans import FanModeConfig
from pyasic.config.mining import MiningModeConfig from pyasic.config.mining import MiningModeConfig
from pyasic.config.mining.scaling import ScalingConfig
from pyasic.config.pools import PoolConfig from pyasic.config.pools import PoolConfig
from pyasic.config.scaling import ScalingConfig
from pyasic.config.temperature import TemperatureConfig from pyasic.config.temperature import TemperatureConfig
from pyasic.misc import merge_dicts from pyasic.misc import merge_dicts
@@ -32,7 +32,6 @@ class MinerConfig:
fan_mode: FanModeConfig = field(default_factory=FanModeConfig.default) fan_mode: FanModeConfig = field(default_factory=FanModeConfig.default)
temperature: TemperatureConfig = field(default_factory=TemperatureConfig.default) temperature: TemperatureConfig = field(default_factory=TemperatureConfig.default)
mining_mode: MiningModeConfig = field(default_factory=MiningModeConfig.default) mining_mode: MiningModeConfig = field(default_factory=MiningModeConfig.default)
scaling: ScalingConfig = field(default_factory=ScalingConfig.default)
def __getitem__(self, item): def __getitem__(self, item):
try: try:
@@ -52,7 +51,6 @@ class MinerConfig:
**self.mining_mode.as_am_modern(), **self.mining_mode.as_am_modern(),
**self.pools.as_am_modern(user_suffix=user_suffix), **self.pools.as_am_modern(user_suffix=user_suffix),
**self.temperature.as_am_modern(), **self.temperature.as_am_modern(),
**self.scaling.as_am_modern(),
} }
def as_wm(self, user_suffix: str = None) -> dict: def as_wm(self, user_suffix: str = None) -> dict:
@@ -62,7 +60,6 @@ class MinerConfig:
**self.mining_mode.as_wm(), **self.mining_mode.as_wm(),
**self.pools.as_wm(user_suffix=user_suffix), **self.pools.as_wm(user_suffix=user_suffix),
**self.temperature.as_wm(), **self.temperature.as_wm(),
**self.scaling.as_wm(),
} }
def as_am_old(self, user_suffix: str = None) -> dict: def as_am_old(self, user_suffix: str = None) -> dict:
@@ -72,7 +69,6 @@ class MinerConfig:
**self.mining_mode.as_am_old(), **self.mining_mode.as_am_old(),
**self.pools.as_am_old(user_suffix=user_suffix), **self.pools.as_am_old(user_suffix=user_suffix),
**self.temperature.as_am_old(), **self.temperature.as_am_old(),
**self.scaling.as_am_old(),
} }
def as_goldshell(self, user_suffix: str = None) -> dict: def as_goldshell(self, user_suffix: str = None) -> dict:
@@ -82,7 +78,6 @@ class MinerConfig:
**self.mining_mode.as_goldshell(), **self.mining_mode.as_goldshell(),
**self.pools.as_goldshell(user_suffix=user_suffix), **self.pools.as_goldshell(user_suffix=user_suffix),
**self.temperature.as_goldshell(), **self.temperature.as_goldshell(),
**self.scaling.as_goldshell(),
} }
def as_avalon(self, user_suffix: str = None) -> dict: def as_avalon(self, user_suffix: str = None) -> dict:
@@ -92,7 +87,6 @@ class MinerConfig:
**self.mining_mode.as_avalon(), **self.mining_mode.as_avalon(),
**self.pools.as_avalon(user_suffix=user_suffix), **self.pools.as_avalon(user_suffix=user_suffix),
**self.temperature.as_avalon(), **self.temperature.as_avalon(),
**self.scaling.as_avalon(),
} }
def as_inno(self, user_suffix: str = None) -> dict: def as_inno(self, user_suffix: str = None) -> dict:
@@ -102,7 +96,6 @@ class MinerConfig:
**self.mining_mode.as_inno(), **self.mining_mode.as_inno(),
**self.pools.as_inno(user_suffix=user_suffix), **self.pools.as_inno(user_suffix=user_suffix),
**self.temperature.as_inno(), **self.temperature.as_inno(),
**self.scaling.as_inno(),
} }
def as_bosminer(self, user_suffix: str = None) -> dict: def as_bosminer(self, user_suffix: str = None) -> dict:
@@ -111,7 +104,6 @@ class MinerConfig:
**merge_dicts(self.fan_mode.as_bosminer(), self.temperature.as_bosminer()), **merge_dicts(self.fan_mode.as_bosminer(), self.temperature.as_bosminer()),
**self.mining_mode.as_bosminer(), **self.mining_mode.as_bosminer(),
**self.pools.as_bosminer(user_suffix=user_suffix), **self.pools.as_bosminer(user_suffix=user_suffix),
**self.scaling.as_bosminer(),
} }
def as_boser(self, user_suffix: str = None) -> dict: def as_boser(self, user_suffix: str = None) -> dict:
@@ -121,7 +113,6 @@ class MinerConfig:
**self.temperature.as_boser(), **self.temperature.as_boser(),
**self.mining_mode.as_boser(), **self.mining_mode.as_boser(),
**self.pools.as_boser(user_suffix=user_suffix), **self.pools.as_boser(user_suffix=user_suffix),
**self.scaling.as_boser(),
} }
def as_epic(self, user_suffix: str = None) -> dict: def as_epic(self, user_suffix: str = None) -> dict:
@@ -130,7 +121,6 @@ class MinerConfig:
**merge_dicts(self.fan_mode.as_epic(), self.temperature.as_epic()), **merge_dicts(self.fan_mode.as_epic(), self.temperature.as_epic()),
**self.mining_mode.as_epic(), **self.mining_mode.as_epic(),
**self.pools.as_epic(user_suffix=user_suffix), **self.pools.as_epic(user_suffix=user_suffix),
**self.scaling.as_epic(),
} }
def as_auradine(self, user_suffix: str = None) -> dict: def as_auradine(self, user_suffix: str = None) -> dict:
@@ -140,7 +130,6 @@ class MinerConfig:
**self.temperature.as_auradine(), **self.temperature.as_auradine(),
**self.mining_mode.as_auradine(), **self.mining_mode.as_auradine(),
**self.pools.as_auradine(user_suffix=user_suffix), **self.pools.as_auradine(user_suffix=user_suffix),
**self.scaling.as_auradine(),
} }
def as_mara(self, user_suffix: str = None) -> dict: def as_mara(self, user_suffix: str = None) -> dict:
@@ -149,7 +138,6 @@ class MinerConfig:
**self.temperature.as_mara(), **self.temperature.as_mara(),
**self.mining_mode.as_mara(), **self.mining_mode.as_mara(),
**self.pools.as_mara(user_suffix=user_suffix), **self.pools.as_mara(user_suffix=user_suffix),
**self.scaling.as_mara(),
} }
@classmethod @classmethod
@@ -160,7 +148,6 @@ class MinerConfig:
mining_mode=MiningModeConfig.from_dict(dict_conf.get("mining_mode")), mining_mode=MiningModeConfig.from_dict(dict_conf.get("mining_mode")),
fan_mode=FanModeConfig.from_dict(dict_conf.get("fan_mode")), fan_mode=FanModeConfig.from_dict(dict_conf.get("fan_mode")),
temperature=TemperatureConfig.from_dict(dict_conf.get("temperature")), temperature=TemperatureConfig.from_dict(dict_conf.get("temperature")),
scaling=ScalingConfig.from_dict(dict_conf.get("scaling")),
) )
@classmethod @classmethod
@@ -200,7 +187,6 @@ class MinerConfig:
mining_mode=MiningModeConfig.from_bosminer(toml_conf), mining_mode=MiningModeConfig.from_bosminer(toml_conf),
fan_mode=FanModeConfig.from_bosminer(toml_conf), fan_mode=FanModeConfig.from_bosminer(toml_conf),
temperature=TemperatureConfig.from_bosminer(toml_conf), temperature=TemperatureConfig.from_bosminer(toml_conf),
scaling=ScalingConfig.from_bosminer(toml_conf),
) )
@classmethod @classmethod
@@ -211,7 +197,6 @@ class MinerConfig:
mining_mode=MiningModeConfig.from_boser(grpc_miner_conf), mining_mode=MiningModeConfig.from_boser(grpc_miner_conf),
fan_mode=FanModeConfig.from_boser(grpc_miner_conf), fan_mode=FanModeConfig.from_boser(grpc_miner_conf),
temperature=TemperatureConfig.from_boser(grpc_miner_conf), temperature=TemperatureConfig.from_boser(grpc_miner_conf),
scaling=ScalingConfig.from_boser(grpc_miner_conf),
) )
@classmethod @classmethod

View File

@@ -46,7 +46,7 @@ class MinerConfigOption(Enum):
return self.value.as_bosminer() return self.value.as_bosminer()
def as_boser(self) -> dict: def as_boser(self) -> dict:
return self.value.as_boser() return self.value.as_boser
def as_epic(self) -> dict: def as_epic(self) -> dict:
return self.value.as_epic() return self.value.as_epic()
@@ -74,7 +74,6 @@ class MinerConfigOption(Enum):
raise KeyError raise KeyError
@dataclass @dataclass
class MinerConfigValue: class MinerConfigValue:
@classmethod @classmethod

View File

@@ -20,16 +20,23 @@ from dataclasses import dataclass, field
from pyasic import settings from pyasic import settings
from pyasic.config.base import MinerConfigOption, MinerConfigValue from pyasic.config.base import MinerConfigOption, MinerConfigValue
from pyasic.web.braiins_os.proto.braiins.bos.v1 import ( from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
DpsHashrateTarget,
DpsPowerTarget,
DpsTarget,
HashrateTargetMode, HashrateTargetMode,
PerformanceMode, PerformanceMode,
Power, Power,
PowerTargetMode, PowerTargetMode,
SaveAction, SaveAction,
SetDpsRequest,
SetPerformanceModeRequest, SetPerformanceModeRequest,
TeraHashrate, TeraHashrate,
TunerPerformanceMode, TunerPerformanceMode,
) )
from .algo import TunerAlgo
from .scaling import ScalingConfig
@dataclass @dataclass
class MiningModeNormal(MinerConfigValue): class MiningModeNormal(MinerConfigValue):
@@ -140,56 +147,12 @@ class MiningModeHPM(MinerConfigValue):
return {"mode": {"mode": "turbo"}} return {"mode": {"mode": "turbo"}}
@dataclass
class StandardTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard")
def as_epic(self) -> str:
return VOptAlgo().as_epic()
@dataclass
class VOptAlgo(MinerConfigValue):
mode: str = field(init=False, default="voltage_optimizer")
def as_epic(self) -> str:
return "VoltageOptimizer"
@dataclass
class ChipTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="chip_tune")
def as_epic(self) -> str:
return "ChipTune"
@dataclass
class TunerAlgo(MinerConfigOption):
standard = StandardTuneAlgo
voltage_optimizer = VOptAlgo
chip_tune = ChipTuneAlgo
@classmethod
def default(cls):
return cls.standard()
@classmethod
def from_dict(cls, dict_conf: dict | None):
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
cls_attr = getattr(cls, mode)
if cls_attr is not None:
return cls_attr().from_dict(dict_conf)
@dataclass @dataclass
class MiningModePowerTune(MinerConfigValue): class MiningModePowerTune(MinerConfigValue):
mode: str = field(init=False, default="power_tuning") mode: str = field(init=False, default="power_tuning")
power: int = None power: int = None
algo: TunerAlgo = field(default_factory=TunerAlgo.default) algo: TunerAlgo = field(default_factory=TunerAlgo.default)
scaling: ScalingConfig = None
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune": def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune":
@@ -198,6 +161,8 @@ class MiningModePowerTune(MinerConfigValue):
cls_conf["power"] = dict_conf["power"] cls_conf["power"] = dict_conf["power"]
if dict_conf.get("algo"): if dict_conf.get("algo"):
cls_conf["algo"] = TunerAlgo.from_dict(dict_conf["algo"]) cls_conf["algo"] = TunerAlgo.from_dict(dict_conf["algo"])
if dict_conf.get("scaling"):
cls_conf["scaling"] = ScalingConfig.from_dict(dict_conf["scaling"])
return cls(**cls_conf) return cls(**cls_conf)
@@ -212,13 +177,26 @@ class MiningModePowerTune(MinerConfigValue):
return {} return {}
def as_bosminer(self) -> dict: def as_bosminer(self) -> dict:
conf = {"enabled": True, "mode": "power_target"} tuning_cfg = {"enabled": True, "mode": "power_target"}
if self.power is not None: if self.power is not None:
conf["power_target"] = self.power tuning_cfg["power_target"] = self.power
return {"autotuning": conf}
cfg = {"autotuning": tuning_cfg}
if self.scaling is not None:
scaling_cfg = {"enabled": True}
if self.scaling.step is not None:
scaling_cfg["power_step"] = self.scaling.step
if self.scaling.minimum is not None:
scaling_cfg["min_power_target"] = self.scaling.minimum
if self.scaling.shutdown is not None:
scaling_cfg = {**scaling_cfg, **self.scaling.shutdown.as_bosminer()}
cfg["performance_scaling"] = scaling_cfg
return cfg
def as_boser(self) -> dict: def as_boser(self) -> dict:
return { cfg = {
"set_performance_mode": SetPerformanceModeRequest( "set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
mode=PerformanceMode( mode=PerformanceMode(
@@ -230,6 +208,23 @@ class MiningModePowerTune(MinerConfigValue):
), ),
), ),
} }
if self.scaling is not None:
sd_cfg = {}
if self.scaling.shutdown is not None:
sd_cfg = self.scaling.shutdown.as_boser()
cfg["set_dps"] = (
SetDpsRequest(
enable=True,
**sd_cfg,
target=DpsTarget(
power_target=DpsPowerTarget(
power_step=Power(self.scaling.step),
min_power_target=Power(self.scaling.minimum),
)
),
),
)
return cfg
def as_auradine(self) -> dict: def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "power", "power": self.power}} return {"mode": {"mode": "custom", "tune": "power", "power": self.power}}
@@ -250,21 +245,18 @@ class MiningModePowerTune(MinerConfigValue):
class MiningModeHashrateTune(MinerConfigValue): class MiningModeHashrateTune(MinerConfigValue):
mode: str = field(init=False, default="hashrate_tuning") mode: str = field(init=False, default="hashrate_tuning")
hashrate: int = None hashrate: int = None
throttle_limit: int = None
throttle_step: int = None
algo: TunerAlgo = field(default_factory=TunerAlgo.default) algo: TunerAlgo = field(default_factory=TunerAlgo.default)
scaling: ScalingConfig = None
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune": def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune":
cls_conf = {} cls_conf = {}
if dict_conf.get("hashrate"): if dict_conf.get("hashrate"):
cls_conf["hashrate"] = dict_conf["hashrate"] cls_conf["hashrate"] = dict_conf["hashrate"]
if dict_conf.get("throttle_limit"):
cls_conf["throttle_limit"] = dict_conf["throttle_limit"]
if dict_conf.get("throttle_step"):
cls_conf["throttle_step"] = dict_conf["throttle_step"]
if dict_conf.get("algo"): if dict_conf.get("algo"):
cls_conf["algo"] = TunerAlgo.from_dict(dict_conf["algo"]) cls_conf["algo"] = TunerAlgo.from_dict(dict_conf["algo"])
if dict_conf.get("scaling"):
cls_conf["scaling"] = ScalingConfig.from_dict(dict_conf["scaling"])
return cls(**cls_conf) return cls(**cls_conf)
@@ -279,8 +271,9 @@ class MiningModeHashrateTune(MinerConfigValue):
conf["hashrate_target"] = self.hashrate conf["hashrate_target"] = self.hashrate
return {"autotuning": conf} return {"autotuning": conf}
@property
def as_boser(self) -> dict: def as_boser(self) -> dict:
return { cfg = {
"set_performance_mode": SetPerformanceModeRequest( "set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY, save_action=SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
mode=PerformanceMode( mode=PerformanceMode(
@@ -294,6 +287,23 @@ class MiningModeHashrateTune(MinerConfigValue):
), ),
) )
} }
if self.scaling is not None:
sd_cfg = {}
if self.scaling.shutdown is not None:
sd_cfg = self.scaling.shutdown.as_boser()
cfg["set_dps"] = (
SetDpsRequest(
enable=True,
**sd_cfg,
target=DpsTarget(
hashrate_target=DpsHashrateTarget(
hashrate_step=TeraHashrate(self.scaling.step),
min_hashrate_target=TeraHashrate(self.scaling.minimum),
)
),
),
)
return cfg
def as_auradine(self) -> dict: def as_auradine(self) -> dict:
return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}} return {"mode": {"mode": "custom", "tune": "ths", "ths": self.hashrate}}
@@ -305,10 +315,11 @@ class MiningModeHashrateTune(MinerConfigValue):
"target": self.hashrate, "target": self.hashrate,
} }
} }
if self.throttle_limit is not None: if self.scaling is not None:
mode["ptune"]["min_throttle"] = self.throttle_limit if self.scaling.minimum is not None:
if self.throttle_step is not None: mode["ptune"]["min_throttle"] = self.scaling.minimum
mode["ptune"]["throttle_step"] = self.throttle_step if self.scaling.step is not None:
mode["ptune"]["throttle_step"] = self.scaling.step
return mode return mode
def as_mara(self) -> dict: def as_mara(self) -> dict:
@@ -373,6 +384,24 @@ class MiningModeManual(MinerConfigValue):
} }
return cls(global_freq=freq, global_volt=voltage, boards=boards) return cls(global_freq=freq, global_volt=voltage, boards=boards)
@classmethod
def from_epic(cls, epic_conf: dict) -> "MiningModeManual":
voltage = 0
freq = 0
if epic_conf.get("HwConfig") is not None:
freq = epic_conf["HwConfig"]["Boards Target Clock"][0]["Data"]
if epic_conf.get("Power Supply Stats") is not None:
voltage = epic_conf["Power Supply Stats"]["Target Voltage"]
boards = {}
if epic_conf.get("HBs") is not None:
boards = {
board["Index"]: ManualBoardSettings(
freq=board["Core Clock Avg"], volt=board["Input Voltage"]
)
for board in epic_conf["HBs"]
}
return cls(global_freq=freq, global_volt=voltage, boards=boards)
def as_mara(self) -> dict: def as_mara(self) -> dict:
return { return {
"mode": { "mode": {
@@ -432,15 +461,32 @@ class MiningModeConfig(MinerConfigOption):
if tuner_running: if tuner_running:
algo_info = web_conf["PerpetualTune"]["Algorithm"] algo_info = web_conf["PerpetualTune"]["Algorithm"]
if algo_info.get("VoltageOptimizer") is not None: if algo_info.get("VoltageOptimizer") is not None:
scaling_cfg = None
if "Throttle Step" in algo_info["VoltageOptimizer"]:
scaling_cfg = ScalingConfig(
minimum=algo_info["VoltageOptimizer"].get(
"Min Throttle Target"
),
step=algo_info["VoltageOptimizer"].get("Throttle Step"),
)
return cls.hashrate_tuning( return cls.hashrate_tuning(
hashrate=algo_info["VoltageOptimizer"].get("Target"), hashrate=algo_info["VoltageOptimizer"].get("Target"),
throttle_limit=algo_info["VoltageOptimizer"].get(
"Min Throttle Target"
),
throttle_step=algo_info["VoltageOptimizer"].get(
"Throttle Step"
),
algo=TunerAlgo.voltage_optimizer(), algo=TunerAlgo.voltage_optimizer(),
scaling=scaling_cfg,
)
elif algo_info.get("BoardTune") is not None:
scaling_cfg = None
if "Throttle Step" in algo_info["BoardTune"]:
scaling_cfg = ScalingConfig(
minimum=algo_info["BoardTune"].get("Min Throttle Target"),
step=algo_info["BoardTune"].get("Throttle Step"),
)
return cls.hashrate_tuning(
hashrate=algo_info["BoardTune"].get("Target"),
algo=TunerAlgo.board_tune(),
scaling=scaling_cfg,
) )
else: else:
return cls.hashrate_tuning( return cls.hashrate_tuning(
@@ -448,7 +494,7 @@ class MiningModeConfig(MinerConfigOption):
algo=TunerAlgo.chip_tune(), algo=TunerAlgo.chip_tune(),
) )
else: else:
return cls.normal() return MiningModeManual.from_epic(web_conf)
except KeyError: except KeyError:
return cls.default() return cls.default()
@@ -465,18 +511,31 @@ class MiningModeConfig(MinerConfigOption):
if autotuning_conf.get("psu_power_limit") is not None: if autotuning_conf.get("psu_power_limit") is not None:
# old autotuning conf # old autotuning conf
return cls.power_tuning(autotuning_conf["psu_power_limit"]) return cls.power_tuning(
autotuning_conf["psu_power_limit"],
scaling=ScalingConfig.from_bosminer(toml_conf, mode="power"),
)
if autotuning_conf.get("mode") is not None: if autotuning_conf.get("mode") is not None:
# new autotuning conf # new autotuning conf
mode = autotuning_conf["mode"] mode = autotuning_conf["mode"]
if mode == "power_target": if mode == "power_target":
if autotuning_conf.get("power_target") is not None: if autotuning_conf.get("power_target") is not None:
return cls.power_tuning(autotuning_conf["power_target"]) return cls.power_tuning(
return cls.power_tuning() autotuning_conf["power_target"],
scaling=ScalingConfig.from_bosminer(toml_conf, mode="power"),
)
return cls.power_tuning(
scaling=ScalingConfig.from_bosminer(toml_conf, mode="power"),
)
if mode == "hashrate_target": if mode == "hashrate_target":
if autotuning_conf.get("hashrate_target") is not None: if autotuning_conf.get("hashrate_target") is not None:
return cls.hashrate_tuning(autotuning_conf["hashrate_target"]) return cls.hashrate_tuning(
return cls.hashrate_tuning() autotuning_conf["hashrate_target"],
scaling=ScalingConfig.from_bosminer(toml_conf, mode="hashrate"),
)
return cls.hashrate_tuning(
scaling=ScalingConfig.from_bosminer(toml_conf, mode="hashrate"),
)
@classmethod @classmethod
def from_vnish(cls, web_settings: dict): def from_vnish(cls, web_settings: dict):
@@ -502,22 +561,36 @@ class MiningModeConfig(MinerConfigOption):
if tuner_conf.get("tunerMode") is not None: if tuner_conf.get("tunerMode") is not None:
if tuner_conf["tunerMode"] == 1: if tuner_conf["tunerMode"] == 1:
if tuner_conf.get("powerTarget") is not None: if tuner_conf.get("powerTarget") is not None:
return cls.power_tuning(tuner_conf["powerTarget"]["watt"]) return cls.power_tuning(
return cls.power_tuning() tuner_conf["powerTarget"]["watt"],
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="power"),
)
return cls.power_tuning(
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="power")
)
if tuner_conf["tunerMode"] == 2: if tuner_conf["tunerMode"] == 2:
if tuner_conf.get("hashrateTarget") is not None: if tuner_conf.get("hashrateTarget") is not None:
return cls.hashrate_tuning( return cls.hashrate_tuning(
int(tuner_conf["hashrateTarget"]["terahashPerSecond"]) int(tuner_conf["hashrateTarget"]["terahashPerSecond"]),
scaling=ScalingConfig.from_boser(
grpc_miner_conf, mode="hashrate"
),
) )
return cls.hashrate_tuning() return cls.hashrate_tuning(
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="hashrate"),
)
if tuner_conf.get("powerTarget") is not None: if tuner_conf.get("powerTarget") is not None:
return cls.power_tuning(tuner_conf["powerTarget"]["watt"]) return cls.power_tuning(
tuner_conf["powerTarget"]["watt"],
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="power"),
)
if tuner_conf.get("hashrateTarget") is not None: if tuner_conf.get("hashrateTarget") is not None:
return cls.hashrate_tuning( return cls.hashrate_tuning(
int(tuner_conf["hashrateTarget"]["terahashPerSecond"]) int(tuner_conf["hashrateTarget"]["terahashPerSecond"]),
scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="hashrate"),
) )
@classmethod @classmethod

View File

@@ -0,0 +1,57 @@
from dataclasses import dataclass, field
from pyasic.config.base import MinerConfigOption, MinerConfigValue
@dataclass
class StandardTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="standard")
def as_epic(self) -> str:
return VOptAlgo().as_epic()
@dataclass
class VOptAlgo(MinerConfigValue):
mode: str = field(init=False, default="voltage_optimizer")
def as_epic(self) -> str:
return "VoltageOptimizer"
@dataclass
class BoardTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="board_tune")
def as_epic(self) -> str:
return "BoardTune"
@dataclass
class ChipTuneAlgo(MinerConfigValue):
mode: str = field(init=False, default="chip_tune")
def as_epic(self) -> str:
return "ChipTune"
@dataclass
class TunerAlgo(MinerConfigOption):
standard = StandardTuneAlgo
voltage_optimizer = VOptAlgo
board_tune = BoardTuneAlgo
chip_tune = ChipTuneAlgo
@classmethod
def default(cls):
return cls.standard()
@classmethod
def from_dict(cls, dict_conf: dict | None):
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
cls_attr = getattr(cls, mode)
if cls_attr is not None:
return cls_attr().from_dict(dict_conf)

View File

@@ -0,0 +1,128 @@
# ------------------------------------------------------------------------------
# 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 __future__ import annotations
from dataclasses import dataclass
from pyasic.config.base import MinerConfigValue
@dataclass
class ScalingShutdown(MinerConfigValue):
enabled: bool = False
duration: int = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "ScalingShutdown":
return cls(
enabled=dict_conf.get("enabled", False), duration=dict_conf.get("duration")
)
@classmethod
def from_bosminer(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdown_enabled")
if sd_enabled is not None:
return cls(sd_enabled, power_scaling_conf.get("shutdown_duration"))
return None
@classmethod
def from_boser(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdownEnabled")
if sd_enabled is not None:
try:
return cls(sd_enabled, power_scaling_conf["shutdownDuration"]["hours"])
except KeyError:
return cls(sd_enabled)
return None
def as_bosminer(self) -> dict:
cfg = {"shutdown_enabled": self.enabled}
if self.duration is not None:
cfg["shutdown_duration"] = self.duration
return cfg
def as_boser(self) -> dict:
return {"enable_shutdown": self.enabled, "shutdown_duration": self.duration}
@dataclass
class ScalingConfig(MinerConfigValue):
step: int = None
minimum: int = None
shutdown: ScalingShutdown = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "ScalingConfig":
cls_conf = {
"step": dict_conf.get("step"),
"minimum": dict_conf.get("minimum"),
}
shutdown = dict_conf.get("shutdown")
if shutdown is not None:
cls_conf["shutdown"] = ScalingShutdown.from_dict(shutdown)
return cls(**cls_conf)
@classmethod
def from_bosminer(cls, toml_conf: dict, mode: str = None):
if mode == "power":
return cls._from_bosminer_power(toml_conf)
if mode == "hashrate":
# not implemented yet
pass
@classmethod
def _from_bosminer_power(cls, toml_conf: dict):
power_scaling = toml_conf.get("power_scaling")
if power_scaling is None:
power_scaling = toml_conf.get("performance_scaling")
if power_scaling is not None:
enabled = power_scaling.get("enabled")
if not enabled:
return None
power_step = power_scaling.get("power_step")
min_power = power_scaling.get("min_psu_power_limit")
if min_power is None:
min_power = power_scaling.get("min_power_target")
sd_mode = ScalingShutdown.from_bosminer(power_scaling)
return cls(step=power_step, minimum=min_power, shutdown=sd_mode)
@classmethod
def from_boser(cls, grpc_miner_conf: dict, mode: str = None):
if mode == "power":
return cls._from_boser_power(grpc_miner_conf)
if mode == "hashrate":
# not implemented yet
pass
@classmethod
def _from_boser_power(cls, grpc_miner_conf: dict):
try:
dps_conf = grpc_miner_conf["dps"]
if not dps_conf.get("enabled", False):
return None
except LookupError:
return None
conf = {"shutdown": ScalingShutdown.from_boser(dps_conf)}
if dps_conf.get("minPowerTarget") is not None:
conf["minimum"] = dps_conf["minPowerTarget"]["watt"]
if dps_conf.get("powerStep") is not None:
conf["step"] = dps_conf["powerStep"]["watt"]
return cls(**conf)

View File

@@ -1,253 +0,0 @@
# ------------------------------------------------------------------------------
# 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 __future__ import annotations
from dataclasses import dataclass, field
from pyasic.config.base import MinerConfigOption, MinerConfigValue
from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
DpsHashrateTarget,
DpsPowerTarget,
DpsTarget,
Power,
SetDpsRequest,
TeraHashrate,
)
@dataclass
class ScalingShutdownEnabled(MinerConfigValue):
mode: str = field(init=False, default="enabled")
duration: int = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "ScalingShutdownEnabled":
return cls(duration=dict_conf.get("duration"))
def as_bosminer(self) -> dict:
cfg = {"shutdown_enabled": True}
if self.duration is not None:
cfg["shutdown_duration"] = self.duration
return cfg
def as_boser(self) -> dict:
return {"enable_shutdown": True, "shutdown_duration": self.duration}
@dataclass
class ScalingShutdownDisabled(MinerConfigValue):
mode: str = field(init=False, default="disabled")
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "ScalingShutdownDisabled":
return cls()
def as_bosminer(self) -> dict:
return {"shutdown_enabled": False}
def as_boser(self) -> dict:
return {"enable_shutdown ": False}
class ScalingShutdown(MinerConfigOption):
enabled = ScalingShutdownEnabled
disabled = ScalingShutdownDisabled
@classmethod
def from_dict(cls, dict_conf: dict | None):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
clsattr = getattr(cls, mode)
if clsattr is not None:
return clsattr().from_dict(dict_conf)
@classmethod
def from_bosminer(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdown_enabled")
if sd_enabled is not None:
if sd_enabled:
return cls.enabled(power_scaling_conf.get("shutdown_duration"))
else:
return cls.disabled()
return None
@classmethod
def from_boser(cls, power_scaling_conf: dict):
sd_enabled = power_scaling_conf.get("shutdownEnabled")
if sd_enabled is not None:
if sd_enabled:
try:
return cls.enabled(power_scaling_conf["shutdownDuration"]["hours"])
except KeyError:
return cls.enabled()
else:
return cls.disabled()
return None
@dataclass
class PowerScaling(MinerConfigValue):
mode: str = field(init=False, default="power")
step: int = None
minimum: int = None
shutdown: ScalingShutdownEnabled | ScalingShutdownDisabled = None
@classmethod
def from_bosminer(cls, power_scaling_conf: dict) -> "PowerScaling":
power_step = power_scaling_conf.get("power_step")
min_power = power_scaling_conf.get("min_psu_power_limit")
if min_power is None:
min_power = power_scaling_conf.get("min_power_target")
sd_mode = ScalingShutdown.from_bosminer(power_scaling_conf)
return cls(step=power_step, minimum=min_power, shutdown=sd_mode)
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "PowerScaling":
cls_conf = {
"step": dict_conf.get("step"),
"minimum": dict_conf.get("minimum"),
}
shutdown = dict_conf.get("shutdown")
if shutdown is not None:
cls_conf["shutdown"] = ScalingShutdown.from_dict(shutdown)
return cls(**cls_conf)
def as_bosminer(self) -> dict:
cfg = {"enabled": True}
if self.step is not None:
cfg["power_step"] = self.step
if self.minimum is not None:
cfg["min_power_target"] = self.minimum
if self.shutdown is not None:
cfg = {**cfg, **self.shutdown.as_bosminer()}
return {"performance_scaling": cfg}
def as_boser(self) -> dict:
return {
"set_dps": SetDpsRequest(
enable=True,
**self.shutdown.as_boser(),
target=DpsTarget(
power_target=DpsPowerTarget(
power_step=Power(self.step),
min_power_target=Power(self.minimum),
)
),
),
}
@dataclass
class HashrateScaling(MinerConfigValue):
mode: str = field(init=False, default="hashrate")
step: int = None
minimum: int = None
shutdown: ScalingShutdownEnabled | ScalingShutdownDisabled = None
@classmethod
def from_dict(cls, dict_conf: dict | None) -> "HashrateScaling":
cls_conf = {
"step": dict_conf.get("step"),
"minimum": dict_conf.get("minimum"),
}
shutdown = dict_conf.get("shutdown")
if shutdown is not None:
cls_conf["shutdown"] = ScalingShutdown.from_dict(shutdown)
return cls(**cls_conf)
def as_boser(self) -> dict:
return {
"set_dps": SetDpsRequest(
enable=True,
**self.shutdown.as_boser(),
target=DpsTarget(
hashrate_target=DpsHashrateTarget(
hashrate_step=TeraHashrate(self.step),
min_hashrate_target=TeraHashrate(self.minimum),
)
),
),
}
@dataclass
class ScalingDisabled(MinerConfigValue):
mode: str = field(init=False, default="disabled")
class ScalingConfig(MinerConfigOption):
power = PowerScaling
hashrate = HashrateScaling
disabled = ScalingDisabled
@classmethod
def default(cls):
return cls.disabled()
@classmethod
def from_dict(cls, dict_conf: dict | None):
if dict_conf is None:
return cls.default()
mode = dict_conf.get("mode")
if mode is None:
return cls.default()
clsattr = getattr(cls, mode)
if clsattr is not None:
return clsattr().from_dict(dict_conf)
@classmethod
def from_bosminer(cls, toml_conf: dict):
power_scaling = toml_conf.get("power_scaling")
if power_scaling is None:
power_scaling = toml_conf.get("performance_scaling")
if power_scaling is not None:
enabled = power_scaling.get("enabled")
if enabled is not None:
if enabled:
return cls.power().from_bosminer(power_scaling)
else:
return cls.disabled()
return cls.default()
@classmethod
def from_boser(cls, grpc_miner_conf: dict):
try:
dps_conf = grpc_miner_conf["dps"]
if not dps_conf.get("enabled", False):
return cls.disabled()
except LookupError:
return cls.default()
conf = {"shutdown_enabled": ScalingShutdown.from_boser(dps_conf)}
if dps_conf.get("minPowerTarget") is not None:
conf["minimum_power"] = dps_conf["minPowerTarget"]["watt"]
if dps_conf.get("powerStep") is not None:
conf["power_step"] = dps_conf["powerStep"]["watt"]
return cls.power(**conf)

View File

@@ -33,6 +33,9 @@ class HashBoard:
expected_chips: The expected chip count of the board as an int. expected_chips: The expected chip count of the board as an int.
serial_number: The serial number of the board. serial_number: The serial number of the board.
missing: Whether the board is returned from the miners data as a bool. missing: Whether the board is returned from the miners data as a bool.
tuned: Whether the board is tuned as a bool.
active: Whether the board is currently tuning as a bool.
voltage: Current input voltage of the board as a float.
""" """
slot: int = 0 slot: int = 0
@@ -43,6 +46,9 @@ class HashBoard:
expected_chips: int = None expected_chips: int = None
serial_number: str = None serial_number: str = None
missing: bool = True missing: bool = True
tuned: bool = None
active: bool = None
voltage: float = None
def get(self, __key: str, default: Any = None): def get(self, __key: str, default: Any = None):
try: try:

View File

@@ -39,6 +39,7 @@ from pyasic.rpc.bosminer import BOSMinerRPCAPI
from pyasic.ssh.braiins_os import BOSMinerSSH from pyasic.ssh.braiins_os import BOSMinerSSH
from pyasic.web.braiins_os import BOSerWebAPI, BOSMinerWebAPI from pyasic.web.braiins_os import BOSerWebAPI, BOSMinerWebAPI
from pyasic.web.braiins_os.proto.braiins.bos.v1 import SaveAction from pyasic.web.braiins_os.proto.braiins.bos.v1 import SaveAction
from pyasic.data.pools import PoolMetrics
BOSMINER_DATA_LOC = DataLocations( BOSMINER_DATA_LOC = DataLocations(
**{ **{
@@ -94,6 +95,10 @@ BOSMINER_DATA_LOC = DataLocations(
"_get_uptime", "_get_uptime",
[RPCAPICommand("rpc_summary", "summary")], [RPCAPICommand("rpc_summary", "summary")],
), ),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
} }
) )
@@ -573,6 +578,36 @@ class BOSMiner(BraiinsOSFirmware):
except LookupError: except LookupError:
pass 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:
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_info.get("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): async def upgrade_firmware(self, file: Path):
""" """
Upgrade the firmware of the BOSMiner device. Upgrade the firmware of the BOSMiner device.

View File

@@ -16,6 +16,8 @@
import logging import logging
from typing import List, Optional from typing import List, Optional
import aiofiles
from pathlib import Path
from pyasic.config import MinerConfig, MiningModeConfig from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
@@ -649,3 +651,41 @@ class BTMiner(StockFirmware):
return int(rpc_summary["SUMMARY"][0]["Elapsed"]) return int(rpc_summary["SUMMARY"][0]["Elapsed"])
except LookupError: except LookupError:
pass pass
async def upgrade_firmware(self, file: Path, token: str):
"""
Upgrade the firmware of the Whatsminer device.
Args:
file (Path): The local file path of the firmware to be uploaded.
token (str): The authentication token for the firmware upgrade.
Returns:
str: Confirmation message after upgrading the firmware.
"""
try:
logging.info("Starting firmware upgrade process for Whatsminer.")
if not file:
raise ValueError("File location must be provided for firmware upgrade.")
# Read the firmware file contents
async with aiofiles.open(file, "rb") as f:
upgrade_contents = await f.read()
result = await self.rpc.update_firmware(upgrade_contents)
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}")
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)
raise

View File

@@ -21,6 +21,7 @@ from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.error_codes import MinerErrorData, X19Error from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.logger import logger from pyasic.logger import logger
from pyasic.data.pools import PoolMetrics
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.miners.device.firmware import ePICFirmware from pyasic.miners.device.firmware import ePICFirmware
from pyasic.web.epic import ePICWebAPI from pyasic.web.epic import ePICWebAPI
@@ -58,10 +59,6 @@ EPIC_DATA_LOC = DataLocations(
"_get_wattage", "_get_wattage",
[WebAPICommand("web_summary", "summary")], [WebAPICommand("web_summary", "summary")],
), ),
str(DataOptions.VOLTAGE): DataFunction(
"_get_voltage",
[WebAPICommand("web_summary", "summary")],
),
str(DataOptions.FANS): DataFunction( str(DataOptions.FANS): DataFunction(
"_get_fans", "_get_fans",
[WebAPICommand("web_summary", "summary")], [WebAPICommand("web_summary", "summary")],
@@ -82,6 +79,10 @@ EPIC_DATA_LOC = DataLocations(
"_is_mining", "_is_mining",
[WebAPICommand("web_summary", "summary")], [WebAPICommand("web_summary", "summary")],
), ),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[WebAPICommand("web_summary", "summary")],
),
} }
) )
@@ -219,20 +220,6 @@ class ePIC(ePICFirmware):
except KeyError: except KeyError:
pass pass
async def _get_voltage(self, web_summary: dict = None) -> Optional[float]:
if web_summary is None:
try:
web_summary = await self.web.summary()
except APIError:
pass
if web_summary is not None:
try:
voltage = web_summary["Power Supply Stats"]["Output Voltage"]
return voltage
except KeyError:
pass
async def _get_hashrate(self, web_summary: dict = None) -> Optional[float]: async def _get_hashrate(self, web_summary: dict = None) -> Optional[float]:
if web_summary is None: if web_summary is None:
try: try:
@@ -323,6 +310,24 @@ class ePIC(ePICFirmware):
except APIError: except APIError:
pass pass
tuned = True
active = False
if web_summary is not None:
tuner_running = web_summary["PerpetualTune"]["Running"]
if tuner_running:
active = True
algo_info = web_summary["PerpetualTune"]["Algorithm"]
if algo_info.get("VoltageOptimizer") is not None:
tuned = algo_info["VoltageOptimizer"].get("Optimized")
elif algo_info.get("BoardTune") is not None:
tuned = algo_info["BoardTune"].get("Optimized")
else:
tuned = algo_info["ChipTune"].get("Optimized")
# To be extra detailed, also ensure the miner is in "Mining" state
tuned = tuned and web_summary["Status"]["Operating State"] == "Mining"
active = active and web_summary["Status"]["Operating State"] == "Mining"
hb_list = [ hb_list = [
HashBoard(slot=i, expected_chips=self.expected_chips) HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards) for i in range(self.expected_hashboards)
@@ -349,6 +354,9 @@ class ePIC(ePICFirmware):
).into(self.algo.unit.default) ).into(self.algo.unit.default)
hb_list[hb["Index"]].chips = num_of_chips hb_list[hb["Index"]].chips = num_of_chips
hb_list[hb["Index"]].temp = hb["Temperature"] hb_list[hb["Index"]].temp = hb["Temperature"]
hb_list[hb["Index"]].tuned = tuned
hb_list[hb["Index"]].active = active
hb_list[hb["Index"]].voltage = hb["Input Voltage"]
return hb_list return hb_list
async def _is_mining(self, web_summary, *args, **kwargs) -> Optional[bool]: async def _is_mining(self, web_summary, *args, **kwargs) -> Optional[bool]:
@@ -411,3 +419,34 @@ class ePIC(ePICFirmware):
except KeyError: except KeyError:
pass pass
return errors return errors
async def _get_pools(self, web_summary: dict = None) -> List[PoolMetrics]:
if web_summary is None:
try:
web_summary = await self.web.summary()
except APIError:
pass
pool_data = []
try:
if web_summary is not None:
if (
web_summary.get("Session") is not None
and web_summary.get("Stratum") is not None
):
pool_data.append(
PoolMetrics(
accepted=web_summary["Session"].get("Accepted"),
rejected=web_summary["Session"].get("Rejected"),
get_failures=0,
remote_failures=0,
active=web_summary["Stratum"].get("IsPoolConnected"),
alive=web_summary["Stratum"].get("IsPoolConnected"),
url=web_summary["Stratum"].get("Current Pool"),
user=web_summary["Stratum"].get("Current User"),
index=web_summary["Stratum"].get("Config Id"),
)
)
return pool_data
except LookupError:
pass

View File

@@ -37,7 +37,6 @@ class DataOptions(Enum):
IS_MINING = "is_mining" IS_MINING = "is_mining"
UPTIME = "uptime" UPTIME = "uptime"
CONFIG = "config" CONFIG = "config"
VOLTAGE = "voltage"
POOLS = "pools" POOLS = "pools"
def __str__(self): def __str__(self):

View File

@@ -23,6 +23,7 @@ import json
import logging import logging
import re import re
from typing import Literal, Union from typing import Literal, Union
import struct
import httpx import httpx
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@@ -559,11 +560,24 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
""" """
return await self.send_privileged_command("set_normal_power") return await self.send_privileged_command("set_normal_power")
async def update_firmware(self): # noqa - static async def update_firmware(self, firmware: bytes):
"""Not implemented.""" """Upgrade the firmware running on the miner and using the firmware passed in bytes.
# to be determined if this will be added later
# requires a file stream in bytes Parameters:
return NotImplementedError firmware (bytes): The firmware binary data to be uploaded.
Returns:
bool: A boolean indicating the success of the firmware upgrade.
Raises:
APIError: If the miner is not ready for firmware update.
"""
ready = await self.send_privileged_command("upgrade_firmware")
if not ready.get("Msg") == "ready":
raise APIError(f"Not ready for firmware update: {self}")
file_size = struct.pack("<I", len(firmware))
await self._send_bytes(file_size + firmware)
return True
async def reboot(self, timeout: int = 10) -> dict: async def reboot(self, timeout: int = 10) -> dict:
"""Reboot the miner using the API. """Reboot the miner using the API.

View File

@@ -23,7 +23,7 @@ from pyasic.config import (
ScalingConfig, ScalingConfig,
TemperatureConfig, TemperatureConfig,
) )
from pyasic.config.scaling import ScalingShutdown from pyasic.config.mining.scaling import ScalingShutdown
class TestConfig(unittest.TestCase): class TestConfig(unittest.TestCase):
@@ -40,11 +40,13 @@ class TestConfig(unittest.TestCase):
), ),
fan_mode=FanModeConfig.manual(speed=90, minimum_fans=2), fan_mode=FanModeConfig.manual(speed=90, minimum_fans=2),
temperature=TemperatureConfig(target=70, danger=120), temperature=TemperatureConfig(target=70, danger=120),
mining_mode=MiningModeConfig.power_tuning(power=3000), mining_mode=MiningModeConfig.power_tuning(
scaling=ScalingConfig.power( power=3000,
step=100, scaling=ScalingConfig(
minimum=2000, step=100,
shutdown=ScalingShutdown.enabled(duration=3), minimum=2000,
shutdown=ScalingShutdown(enabled=True, duration=3),
),
), ),
) )
@@ -76,12 +78,11 @@ class TestConfig(unittest.TestCase):
"mode": "power_tuning", "mode": "power_tuning",
"power": 3000, "power": 3000,
"algo": {"mode": "standard"}, "algo": {"mode": "standard"},
}, "scaling": {
"scaling": { "step": 100,
"mode": "power", "minimum": 2000,
"step": 100, "shutdown": {"enabled": True, "duration": 3},
"minimum": 2000, },
"shutdown": {"mode": "enabled", "duration": 3},
}, },
} }

View File

@@ -71,5 +71,5 @@ class TestFanConfig(unittest.TestCase):
fan_mode=fan_mode, fan_mode=fan_mode,
): ):
conf = fan_mode() conf = fan_mode()
boser_conf = conf.as_boser() boser_conf = conf.as_boser
self.assertEqual(conf, FanModeConfig.from_boser(boser_conf)) self.assertEqual(conf, FanModeConfig.from_boser(boser_conf))