Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f4054bf38 | ||
|
|
cd52d3aeaf | ||
|
|
66c9b3663e | ||
|
|
5f0e1da938 | ||
|
|
2bd031b33d | ||
|
|
e2f07818cc | ||
|
|
75056cfff5 | ||
|
|
7fbcb0dbd2 | ||
|
|
7329aeace2 | ||
|
|
e8c3953106 | ||
|
|
a1a7562bdb |
@@ -5,13 +5,13 @@ ci:
|
||||
- generate-docs
|
||||
repos:
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: 2.1.2
|
||||
rev: 2.2.1
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
- id: poetry-lock
|
||||
- id: poetry-install
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
@@ -22,16 +22,24 @@ repos:
|
||||
name: check-yaml for other YAML files
|
||||
exclude: ^mkdocs\.yml$
|
||||
- id: check-added-large-files
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 25.1.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.13.2
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 6.0.1
|
||||
- id: ruff-check
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.18.2
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
[
|
||||
betterproto==2.0.0b7,
|
||||
httpx==0.28.1,
|
||||
types-aiofiles==24.1.0.20250822,
|
||||
types-passlib==1.7.7.20250602,
|
||||
pydantic==2.11.9,
|
||||
]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: unittest
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
import os
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pyasic.miners.factory import MINER_CLASSES, MinerTypes
|
||||
|
||||
@@ -128,7 +128,7 @@ BACKEND_TYPE_CLOSER = """
|
||||
</ul>
|
||||
</details>"""
|
||||
|
||||
m_data = {}
|
||||
m_data: dict[str, dict[str, list[type[Any]]]] = {}
|
||||
done = []
|
||||
|
||||
for m in MINER_CLASSES:
|
||||
|
||||
16
docs/miners/avalonminer/Q.md
Normal file
16
docs/miners/avalonminer/Q.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# pyasic
|
||||
## Q Models
|
||||
|
||||
## Avalon Q Home (Stock)
|
||||
|
||||
- [ ] Shutdowns
|
||||
- [ ] Power Modes
|
||||
- [ ] Setpoints
|
||||
- [ ] Presets
|
||||
|
||||
::: pyasic.miners.avalonminer.cgminer.Q.Q.CGMinerAvalonQHome
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# pyasic
|
||||
## Byte Models
|
||||
## byte Models
|
||||
|
||||
## Byte (Stock)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- [ ] Setpoints
|
||||
- [ ] Presets
|
||||
|
||||
::: pyasic.miners.goldshell.bfgminer.Byte.Byte.GoldshellByte
|
||||
::: pyasic.miners.goldshell.bfgminer.byte.byte.GoldshellByte
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
|
||||
16
docs/miners/goldshell/mini_doge.md
Normal file
16
docs/miners/goldshell/mini_doge.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# pyasic
|
||||
## mini_doge Models
|
||||
|
||||
## Mini Doge (Stock)
|
||||
|
||||
- [ ] Shutdowns
|
||||
- [ ] Power Modes
|
||||
- [ ] Setpoints
|
||||
- [ ] Presets
|
||||
|
||||
::: pyasic.miners.goldshell.bfgminer.mini_doge.mini_doge.GoldshellMiniDoge
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 0
|
||||
|
||||
16
docs/miners/iceriver/ALX.md
Normal file
16
docs/miners/iceriver/ALX.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# pyasic
|
||||
## ALX Models
|
||||
|
||||
## AL3 (Stock)
|
||||
|
||||
- [ ] Shutdowns
|
||||
- [ ] Power Modes
|
||||
- [ ] Setpoints
|
||||
- [ ] Presets
|
||||
|
||||
::: pyasic.miners.iceriver.iceminer.ALX.AL3.IceRiverAL3
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 0
|
||||
|
||||
@@ -603,12 +603,6 @@ details {
|
||||
<details>
|
||||
<summary>Stock Firmware Goldshells:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary>Mini Doge Series:</summary>
|
||||
<ul>
|
||||
<li><a href="../goldshell/MiniDoge#mini-doge-stock">Mini Doge (Stock)</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>X5 Series:</summary>
|
||||
<ul>
|
||||
@@ -631,9 +625,15 @@ details {
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Byte Series:</summary>
|
||||
<summary>byte Series:</summary>
|
||||
<ul>
|
||||
<li><a href="../goldshell/Byte#byte-stock">Byte (Stock)</a></li>
|
||||
<li><a href="../goldshell/byte#byte-stock">Byte (Stock)</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>mini_doge Series:</summary>
|
||||
<ul>
|
||||
<li><a href="../goldshell/mini_doge#mini-doge-stock">Mini Doge (Stock)</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
|
||||
1085
poetry.lock
generated
1085
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,10 +14,41 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from pyasic.config.fans import FanMode, FanModeConfig, FanModeNormal
|
||||
from pyasic.config.mining import MiningMode, MiningModeConfig
|
||||
from pyasic.config.fans import (
|
||||
FanModeConfig,
|
||||
FanModeImmersion,
|
||||
FanModeManual,
|
||||
FanModeNormal,
|
||||
)
|
||||
from pyasic.config.mining import (
|
||||
MiningModeConfig,
|
||||
MiningModeHashrateTune,
|
||||
MiningModeHPM,
|
||||
MiningModeLPM,
|
||||
MiningModeManual,
|
||||
MiningModeNormal,
|
||||
MiningModePowerTune,
|
||||
MiningModePreset,
|
||||
MiningModeSleep,
|
||||
)
|
||||
|
||||
# Type aliases for config field types
|
||||
FanModeType = FanModeNormal | FanModeManual | FanModeImmersion | FanModeConfig
|
||||
MiningModeType = (
|
||||
MiningModeNormal
|
||||
| MiningModeHPM
|
||||
| MiningModeLPM
|
||||
| MiningModeSleep
|
||||
| MiningModeManual
|
||||
| MiningModePowerTune
|
||||
| MiningModeHashrateTune
|
||||
| MiningModePreset
|
||||
| MiningModeConfig
|
||||
)
|
||||
from pyasic.config.mining.scaling import ScalingConfig
|
||||
from pyasic.config.pools import PoolConfig
|
||||
from pyasic.config.temperature import TemperatureConfig
|
||||
@@ -32,11 +63,11 @@ class MinerConfig(BaseModel):
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
pools: PoolConfig = Field(default_factory=PoolConfig.default)
|
||||
fan_mode: FanMode = Field(default_factory=FanModeConfig.default)
|
||||
fan_mode: FanModeType = Field(default_factory=FanModeConfig.default)
|
||||
temperature: TemperatureConfig = Field(default_factory=TemperatureConfig.default)
|
||||
mining_mode: MiningMode = Field(default_factory=MiningModeConfig.default)
|
||||
mining_mode: MiningModeType = Field(default_factory=MiningModeConfig.default)
|
||||
|
||||
def __getitem__(self, item):
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
try:
|
||||
return getattr(self, item)
|
||||
except AttributeError:
|
||||
@@ -88,8 +119,8 @@ class MinerConfig(BaseModel):
|
||||
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()
|
||||
"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:
|
||||
@@ -260,7 +291,7 @@ class MinerConfig(BaseModel):
|
||||
return cls(pools=PoolConfig.from_goldshell(web_conf))
|
||||
|
||||
@classmethod
|
||||
def from_goldshell_byte(cls, web_conf: dict) -> "MinerConfig":
|
||||
def from_goldshell_byte(cls, web_conf: list) -> "MinerConfig":
|
||||
"""Constructs a MinerConfig object from web configuration for Goldshell Byte miners."""
|
||||
return cls(pools=PoolConfig.from_goldshell_byte(web_conf))
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -89,61 +90,61 @@ class MinerConfigOption(Enum):
|
||||
|
||||
class MinerConfigValue(BaseModel):
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None):
|
||||
def from_dict(cls, dict_conf: dict):
|
||||
return cls()
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
return self.model_dump()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
def as_am_modern(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_hiveon_modern(self) -> dict:
|
||||
def as_hiveon_modern(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_am_old(self) -> dict:
|
||||
def as_am_old(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_wm(self) -> dict:
|
||||
def as_wm(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_btminer_v3(self) -> dict:
|
||||
def as_btminer_v3(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_inno(self) -> dict:
|
||||
def as_inno(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_goldshell(self) -> dict:
|
||||
def as_goldshell(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_avalon(self) -> dict:
|
||||
def as_avalon(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_bosminer(self) -> dict:
|
||||
def as_bosminer(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_boser(self) -> dict:
|
||||
def as_boser(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_epic(self) -> dict:
|
||||
def as_epic(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_vnish(self) -> dict:
|
||||
def as_vnish(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_auradine(self) -> dict:
|
||||
def as_auradine(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_mara(self) -> dict:
|
||||
def as_mara(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_espminer(self) -> dict:
|
||||
def as_espminer(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_luxos(self) -> dict:
|
||||
def as_luxos(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def as_elphapex(self) -> dict:
|
||||
def as_elphapex(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return {}
|
||||
|
||||
def __getitem__(self, item):
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar, Union
|
||||
from typing import TypeVar
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -28,7 +28,7 @@ class FanModeNormal(MinerConfigValue):
|
||||
minimum_speed: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "FanModeNormal":
|
||||
def from_dict(cls, dict_conf: dict) -> FanModeNormal:
|
||||
cls_conf = {}
|
||||
if dict_conf.get("minimum_fans") is not None:
|
||||
cls_conf["minimum_fans"] = dict_conf["minimum_fans"]
|
||||
@@ -37,7 +37,7 @@ class FanModeNormal(MinerConfigValue):
|
||||
return cls(**cls_conf)
|
||||
|
||||
@classmethod
|
||||
def from_vnish(cls, web_cooling_settings: dict) -> "FanModeNormal":
|
||||
def from_vnish(cls, web_cooling_settings: dict) -> FanModeNormal:
|
||||
cls_conf = {}
|
||||
if web_cooling_settings.get("fan_min_count") is not None:
|
||||
cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"]
|
||||
@@ -112,7 +112,7 @@ class FanModeManual(MinerConfigValue):
|
||||
minimum_fans: int = 1
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "FanModeManual":
|
||||
def from_dict(cls, dict_conf: dict) -> FanModeManual:
|
||||
cls_conf = {}
|
||||
if dict_conf.get("speed") is not None:
|
||||
cls_conf["speed"] = dict_conf["speed"]
|
||||
@@ -121,7 +121,7 @@ class FanModeManual(MinerConfigValue):
|
||||
return cls(**cls_conf)
|
||||
|
||||
@classmethod
|
||||
def from_bosminer(cls, toml_fan_conf: dict) -> "FanModeManual":
|
||||
def from_bosminer(cls, toml_fan_conf: dict) -> FanModeManual:
|
||||
cls_conf = {}
|
||||
if toml_fan_conf.get("min_fans") is not None:
|
||||
cls_conf["minimum_fans"] = toml_fan_conf["min_fans"]
|
||||
@@ -130,7 +130,7 @@ class FanModeManual(MinerConfigValue):
|
||||
return cls(**cls_conf)
|
||||
|
||||
@classmethod
|
||||
def from_vnish(cls, web_cooling_settings: dict) -> "FanModeManual":
|
||||
def from_vnish(cls, web_cooling_settings: dict) -> FanModeManual:
|
||||
cls_conf = {}
|
||||
if web_cooling_settings.get("fan_min_count") is not None:
|
||||
cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"]
|
||||
@@ -191,7 +191,7 @@ class FanModeImmersion(MinerConfigValue):
|
||||
mode: str = Field(init=False, default="immersion")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "FanModeImmersion":
|
||||
def from_dict(cls, dict_conf: dict | None) -> FanModeImmersion:
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
@@ -417,5 +417,5 @@ class FanModeConfig(MinerConfigOption):
|
||||
|
||||
FanMode = TypeVar(
|
||||
"FanMode",
|
||||
bound=Union[FanModeNormal, FanModeManual, FanModeImmersion],
|
||||
bound=FanModeNormal | FanModeManual | FanModeImmersion,
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import field
|
||||
from typing import TypeVar, Union
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic.config.base import MinerConfigOption, MinerConfigValue
|
||||
@@ -35,7 +35,14 @@ from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
|
||||
TunerPerformanceMode,
|
||||
)
|
||||
|
||||
from .algo import TunerAlgo, TunerAlgoType
|
||||
from .algo import (
|
||||
BoardTuneAlgo,
|
||||
ChipTuneAlgo,
|
||||
StandardTuneAlgo,
|
||||
TunerAlgo,
|
||||
TunerAlgoType,
|
||||
VOptAlgo,
|
||||
)
|
||||
from .presets import MiningPreset
|
||||
from .scaling import ScalingConfig
|
||||
|
||||
@@ -44,7 +51,7 @@ class MiningModeNormal(MinerConfigValue):
|
||||
mode: str = field(init=False, default="normal")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModeNormal":
|
||||
def from_dict(cls, dict_conf: dict | None) -> MiningModeNormal:
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
@@ -93,7 +100,7 @@ class MiningModeSleep(MinerConfigValue):
|
||||
mode: str = field(init=False, default="sleep")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModeSleep":
|
||||
def from_dict(cls, dict_conf: dict | None) -> MiningModeSleep:
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
@@ -136,7 +143,7 @@ class MiningModeLPM(MinerConfigValue):
|
||||
mode: str = field(init=False, default="low")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModeLPM":
|
||||
def from_dict(cls, dict_conf: dict | None) -> MiningModeLPM:
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
@@ -169,7 +176,7 @@ class MiningModeHPM(MinerConfigValue):
|
||||
mode: str = field(init=False, default="high")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHPM":
|
||||
def from_dict(cls, dict_conf: dict | None) -> MiningModeHPM:
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
@@ -201,11 +208,15 @@ class MiningModePowerTune(MinerConfigValue):
|
||||
|
||||
mode: str = field(init=False, default="power_tuning")
|
||||
power: int | None = None
|
||||
algo: TunerAlgoType = field(default_factory=TunerAlgo.default)
|
||||
algo: StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo = field(
|
||||
default_factory=TunerAlgo.default
|
||||
)
|
||||
scaling: ScalingConfig | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune":
|
||||
def from_dict(cls, dict_conf: dict | None) -> MiningModePowerTune:
|
||||
if dict_conf is None:
|
||||
return cls()
|
||||
cls_conf = {}
|
||||
if dict_conf.get("power"):
|
||||
cls_conf["power"] = dict_conf["power"]
|
||||
@@ -245,25 +256,27 @@ class MiningModePowerTune(MinerConfigValue):
|
||||
cfg = {"autotuning": tuning_cfg}
|
||||
|
||||
if self.scaling is not None:
|
||||
scaling_cfg = {"enabled": True}
|
||||
scaling_cfg: dict[str, Any] = {"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()}
|
||||
scaling_cfg.update(self.scaling.shutdown.as_bosminer())
|
||||
cfg["performance_scaling"] = scaling_cfg
|
||||
|
||||
return cfg
|
||||
|
||||
def as_boser(self) -> dict:
|
||||
cfg = {
|
||||
cfg: dict[str, Any] = {
|
||||
"set_performance_mode": SetPerformanceModeRequest(
|
||||
save_action=SaveAction.SAVE_AND_APPLY,
|
||||
save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
mode=PerformanceMode(
|
||||
tuner_mode=TunerPerformanceMode(
|
||||
power_target=PowerTargetMode(
|
||||
power_target=Power(watt=self.power)
|
||||
if self.power is not None
|
||||
else None # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -273,13 +286,15 @@ class MiningModePowerTune(MinerConfigValue):
|
||||
sd_cfg = {}
|
||||
if self.scaling.shutdown is not None:
|
||||
sd_cfg = self.scaling.shutdown.as_boser()
|
||||
power_target_kwargs = {}
|
||||
power_target_kwargs: dict[str, Any] = {}
|
||||
if self.scaling.step is not None:
|
||||
power_target_kwargs["power_step"] = Power(self.scaling.step)
|
||||
power_target_kwargs["power_step"] = Power(watt=self.scaling.step)
|
||||
if self.scaling.minimum is not None:
|
||||
power_target_kwargs["min_power_target"] = Power(self.scaling.minimum)
|
||||
power_target_kwargs["min_power_target"] = Power(
|
||||
watt=self.scaling.minimum
|
||||
)
|
||||
cfg["set_dps"] = SetDpsRequest(
|
||||
save_action=SaveAction.SAVE_AND_APPLY,
|
||||
save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
enable=True,
|
||||
**sd_cfg,
|
||||
target=DpsTarget(power_target=DpsPowerTarget(**power_target_kwargs)),
|
||||
@@ -311,11 +326,15 @@ class MiningModeHashrateTune(MinerConfigValue):
|
||||
|
||||
mode: str = field(init=False, default="hashrate_tuning")
|
||||
hashrate: int | None = None
|
||||
algo: TunerAlgoType = field(default_factory=TunerAlgo.default)
|
||||
algo: StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo = field(
|
||||
default_factory=TunerAlgo.default
|
||||
)
|
||||
scaling: ScalingConfig | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune":
|
||||
def from_dict(cls, dict_conf: dict | None) -> MiningModeHashrateTune:
|
||||
if dict_conf is None:
|
||||
return cls()
|
||||
cls_conf = {}
|
||||
if dict_conf.get("hashrate"):
|
||||
cls_conf["hashrate"] = dict_conf["hashrate"]
|
||||
@@ -346,14 +365,16 @@ class MiningModeHashrateTune(MinerConfigValue):
|
||||
return {"autotuning": conf}
|
||||
|
||||
def as_boser(self) -> dict:
|
||||
cfg = {
|
||||
cfg: dict[str, Any] = {
|
||||
"set_performance_mode": SetPerformanceModeRequest(
|
||||
save_action=SaveAction.SAVE_AND_APPLY,
|
||||
save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
mode=PerformanceMode(
|
||||
tuner_mode=TunerPerformanceMode(
|
||||
hashrate_target=HashrateTargetMode(
|
||||
hashrate_target=TeraHashrate(
|
||||
terahash_per_second=self.hashrate
|
||||
terahash_per_second=float(self.hashrate)
|
||||
if self.hashrate is not None
|
||||
else None # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -364,17 +385,17 @@ class MiningModeHashrateTune(MinerConfigValue):
|
||||
sd_cfg = {}
|
||||
if self.scaling.shutdown is not None:
|
||||
sd_cfg = self.scaling.shutdown.as_boser()
|
||||
hashrate_target_kwargs = {}
|
||||
hashrate_target_kwargs: dict[str, Any] = {}
|
||||
if self.scaling.step is not None:
|
||||
hashrate_target_kwargs["hashrate_step"] = TeraHashrate(
|
||||
self.scaling.step
|
||||
terahash_per_second=float(self.scaling.step)
|
||||
)
|
||||
if self.scaling.minimum is not None:
|
||||
hashrate_target_kwargs["min_hashrate_target"] = TeraHashrate(
|
||||
self.scaling.minimum
|
||||
terahash_per_second=float(self.scaling.minimum)
|
||||
)
|
||||
cfg["set_dps"] = SetDpsRequest(
|
||||
save_action=SaveAction.SAVE_AND_APPLY,
|
||||
save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
enable=True,
|
||||
**sd_cfg,
|
||||
target=DpsTarget(
|
||||
@@ -390,7 +411,11 @@ class MiningModeHashrateTune(MinerConfigValue):
|
||||
def as_epic(self) -> dict:
|
||||
mode = {
|
||||
"ptune": {
|
||||
"algo": self.algo.as_epic(),
|
||||
"algo": (
|
||||
self.algo.as_epic()
|
||||
if hasattr(self.algo, "as_epic")
|
||||
else TunerAlgo.default().as_epic()
|
||||
),
|
||||
"target": self.hashrate,
|
||||
}
|
||||
}
|
||||
@@ -431,7 +456,7 @@ class MiningModePreset(MinerConfigValue):
|
||||
web_overclock_settings: dict,
|
||||
web_presets: list[dict],
|
||||
web_perf_summary: dict,
|
||||
) -> "MiningModePreset":
|
||||
) -> MiningModePreset:
|
||||
active_preset = web_perf_summary.get("current_preset")
|
||||
|
||||
if active_preset is None:
|
||||
@@ -440,12 +465,12 @@ class MiningModePreset(MinerConfigValue):
|
||||
active_preset = preset
|
||||
|
||||
return cls(
|
||||
active_preset=MiningPreset.from_vnish(active_preset),
|
||||
active_preset=MiningPreset.from_vnish(active_preset or {}),
|
||||
available_presets=[MiningPreset.from_vnish(p) for p in web_presets],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> "MiningModePreset":
|
||||
def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> MiningModePreset:
|
||||
active_preset = cls.get_active_preset_from_luxos(rpc_config, rpc_profiles)
|
||||
return cls(
|
||||
active_preset=active_preset,
|
||||
@@ -463,7 +488,7 @@ class MiningModePreset(MinerConfigValue):
|
||||
for profile in rpc_profiles["PROFILES"]:
|
||||
if profile["Profile Name"] == active_profile:
|
||||
active_preset = profile
|
||||
return MiningPreset.from_luxos(active_preset)
|
||||
return MiningPreset.from_luxos(active_preset or {})
|
||||
|
||||
|
||||
class ManualBoardSettings(MinerConfigValue):
|
||||
@@ -471,7 +496,7 @@ class ManualBoardSettings(MinerConfigValue):
|
||||
volt: float
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "ManualBoardSettings":
|
||||
def from_dict(cls, dict_conf: dict) -> ManualBoardSettings:
|
||||
return cls(freq=dict_conf["freq"], volt=dict_conf["volt"])
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
@@ -499,11 +524,15 @@ class MiningModeManual(MinerConfigValue):
|
||||
boards: dict[int, ManualBoardSettings] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "MiningModeManual":
|
||||
def from_dict(cls, dict_conf: dict) -> MiningModeManual:
|
||||
return cls(
|
||||
global_freq=dict_conf["global_freq"],
|
||||
global_volt=dict_conf["global_volt"],
|
||||
boards={i: ManualBoardSettings.from_dict(dict_conf[i]) for i in dict_conf},
|
||||
boards={
|
||||
i: ManualBoardSettings.from_dict(dict_conf[i])
|
||||
for i in dict_conf
|
||||
if isinstance(i, int)
|
||||
},
|
||||
)
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
@@ -527,7 +556,7 @@ class MiningModeManual(MinerConfigValue):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_vnish(cls, web_overclock_settings: dict) -> "MiningModeManual":
|
||||
def from_vnish(cls, web_overclock_settings: dict) -> MiningModeManual:
|
||||
# will raise KeyError if it cant find the settings, values cannot be empty
|
||||
voltage = web_overclock_settings["globals"]["volt"]
|
||||
freq = web_overclock_settings["globals"]["freq"]
|
||||
@@ -541,7 +570,7 @@ class MiningModeManual(MinerConfigValue):
|
||||
return cls(global_freq=freq, global_volt=voltage, boards=boards)
|
||||
|
||||
@classmethod
|
||||
def from_epic(cls, epic_conf: dict) -> "MiningModeManual":
|
||||
def from_epic(cls, epic_conf: dict) -> MiningModeManual:
|
||||
voltage = 0
|
||||
freq = 0
|
||||
if epic_conf.get("HwConfig") is not None:
|
||||
@@ -581,11 +610,11 @@ class MiningModeConfig(MinerConfigOption):
|
||||
manual = MiningModeManual
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
def default(cls) -> MiningModeConfig:
|
||||
return cls.normal()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None):
|
||||
def from_dict(cls, dict_conf: dict | None) -> MiningModeConfig:
|
||||
if dict_conf is None:
|
||||
return cls.default()
|
||||
|
||||
@@ -593,12 +622,13 @@ class MiningModeConfig(MinerConfigOption):
|
||||
if mode is None:
|
||||
return cls.default()
|
||||
|
||||
cls_attr = getattr(cls, mode)
|
||||
cls_attr = getattr(cls, mode, None)
|
||||
if cls_attr is not None:
|
||||
return cls_attr().from_dict(dict_conf)
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_am_modern(cls, web_conf: dict):
|
||||
def from_am_modern(cls, web_conf: dict) -> MiningModeConfig:
|
||||
if web_conf.get("bitmain-work-mode") is not None:
|
||||
work_mode = web_conf["bitmain-work-mode"]
|
||||
if work_mode == "":
|
||||
@@ -612,7 +642,7 @@ class MiningModeConfig(MinerConfigOption):
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_hiveon_modern(cls, web_conf: dict):
|
||||
def from_hiveon_modern(cls, web_conf: dict) -> MiningModeConfig:
|
||||
if web_conf.get("bitmain-work-mode") is not None:
|
||||
work_mode = web_conf["bitmain-work-mode"]
|
||||
if work_mode == "":
|
||||
@@ -626,7 +656,7 @@ class MiningModeConfig(MinerConfigOption):
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_elphapex(cls, web_conf: dict):
|
||||
def from_elphapex(cls, web_conf: dict) -> MiningModeConfig:
|
||||
if web_conf.get("fc-work-mode") is not None:
|
||||
work_mode = web_conf["fc-work-mode"]
|
||||
if work_mode == "":
|
||||
@@ -640,7 +670,7 @@ class MiningModeConfig(MinerConfigOption):
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_epic(cls, web_conf: dict):
|
||||
def from_epic(cls, web_conf: dict) -> MiningModeConfig:
|
||||
try:
|
||||
tuner_running = web_conf["PerpetualTune"]["Running"]
|
||||
if tuner_running:
|
||||
@@ -679,12 +709,12 @@ class MiningModeConfig(MinerConfigOption):
|
||||
algo=TunerAlgo.chip_tune(),
|
||||
)
|
||||
else:
|
||||
return MiningModeManual.from_epic(web_conf)
|
||||
return cls.manual.from_epic(web_conf)
|
||||
except KeyError:
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_bosminer(cls, toml_conf: dict):
|
||||
def from_bosminer(cls, toml_conf: dict) -> MiningModeConfig:
|
||||
if toml_conf.get("autotuning") is None:
|
||||
return cls.default()
|
||||
autotuning_conf = toml_conf["autotuning"]
|
||||
@@ -726,21 +756,19 @@ class MiningModeConfig(MinerConfigOption):
|
||||
@classmethod
|
||||
def from_vnish(
|
||||
cls, web_settings: dict, web_presets: list[dict], web_perf_summary: dict
|
||||
):
|
||||
) -> MiningModeConfig:
|
||||
try:
|
||||
mode_settings = web_settings["miner"]["overclock"]
|
||||
except KeyError:
|
||||
return cls.default()
|
||||
|
||||
if mode_settings["preset"] == "disabled":
|
||||
return MiningModeManual.from_vnish(mode_settings)
|
||||
return cls.manual.from_vnish(mode_settings, web_presets, web_perf_summary)
|
||||
else:
|
||||
return MiningModePreset.from_vnish(
|
||||
mode_settings, web_presets, web_perf_summary
|
||||
)
|
||||
return cls.preset.from_vnish(mode_settings, web_presets, web_perf_summary)
|
||||
|
||||
@classmethod
|
||||
def from_boser(cls, grpc_miner_conf: dict):
|
||||
def from_boser(cls, grpc_miner_conf: dict) -> MiningModeConfig:
|
||||
try:
|
||||
tuner_conf = grpc_miner_conf["tuner"]
|
||||
if not tuner_conf.get("enabled", False):
|
||||
@@ -786,7 +814,7 @@ class MiningModeConfig(MinerConfigOption):
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_auradine(cls, web_mode: dict):
|
||||
def from_auradine(cls, web_mode: dict) -> MiningModeConfig:
|
||||
try:
|
||||
mode_data = web_mode["Mode"][0]
|
||||
if mode_data.get("Sleep") == "on":
|
||||
@@ -803,9 +831,12 @@ class MiningModeConfig(MinerConfigOption):
|
||||
return cls.power_tuning(power=mode_data["Power"])
|
||||
except LookupError:
|
||||
return cls.default()
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_btminer_v3(cls, rpc_device_info: dict, rpc_settings: dict):
|
||||
def from_btminer_v3(
|
||||
cls, rpc_device_info: dict, rpc_settings: dict
|
||||
) -> MiningModeConfig:
|
||||
try:
|
||||
is_mining = rpc_device_info["msg"]["miner"]["working"] == "true"
|
||||
if not is_mining:
|
||||
@@ -823,9 +854,10 @@ class MiningModeConfig(MinerConfigOption):
|
||||
|
||||
except LookupError:
|
||||
return cls.default()
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_mara(cls, web_config: dict):
|
||||
def from_mara(cls, web_config: dict) -> MiningModeConfig:
|
||||
try:
|
||||
mode = web_config["mode"]["work-mode-selector"]
|
||||
if mode == "Fixed":
|
||||
@@ -850,24 +882,26 @@ class MiningModeConfig(MinerConfigOption):
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_luxos(cls, rpc_config: dict, rpc_profiles: dict):
|
||||
def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> MiningModeConfig:
|
||||
preset_info = MiningModePreset.from_luxos(rpc_config, rpc_profiles)
|
||||
return cls.preset(
|
||||
active_preset=preset_info.active_preset,
|
||||
available_presets=preset_info.available_presets,
|
||||
)
|
||||
|
||||
def as_btminer_v3(self) -> dict:
|
||||
"""Delegate to the default instance for btminer v3 configuration."""
|
||||
return self.default().as_btminer_v3()
|
||||
|
||||
|
||||
MiningMode = TypeVar(
|
||||
"MiningMode",
|
||||
bound=Union[
|
||||
MiningModeNormal,
|
||||
MiningModeHPM,
|
||||
MiningModeLPM,
|
||||
MiningModeSleep,
|
||||
MiningModeManual,
|
||||
MiningModePowerTune,
|
||||
MiningModeHashrateTune,
|
||||
MiningModePreset,
|
||||
],
|
||||
bound=MiningModeNormal
|
||||
| MiningModeHPM
|
||||
| MiningModeLPM
|
||||
| MiningModeSleep
|
||||
| MiningModeManual
|
||||
| MiningModePowerTune
|
||||
| MiningModeHashrateTune
|
||||
| MiningModePreset,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import field
|
||||
from typing import TypeVar, Union
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from pyasic.config.base import MinerConfigOption, MinerConfigValue
|
||||
|
||||
@@ -41,26 +41,26 @@ class TunerAlgo(MinerConfigOption):
|
||||
chip_tune = ChipTuneAlgo
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> TunerAlgoType:
|
||||
def default(cls) -> StandardTuneAlgo:
|
||||
return cls.standard()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> TunerAlgoType:
|
||||
def from_dict(
|
||||
cls, dict_conf: dict[Any, Any] | None
|
||||
) -> StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo:
|
||||
if dict_conf is None:
|
||||
return cls.default()
|
||||
mode = dict_conf.get("mode")
|
||||
if mode is None:
|
||||
return cls.default()
|
||||
|
||||
cls_attr = getattr(cls, mode)
|
||||
cls_attr = getattr(cls, mode, None)
|
||||
if cls_attr is not None:
|
||||
return cls_attr().from_dict(dict_conf)
|
||||
return cls.default()
|
||||
|
||||
|
||||
TunerAlgoType = TypeVar(
|
||||
"TunerAlgoType",
|
||||
bound=Union[
|
||||
StandardTuneAlgo,
|
||||
VOptAlgo,
|
||||
BoardTuneAlgo,
|
||||
ChipTuneAlgo,
|
||||
],
|
||||
bound=StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo,
|
||||
)
|
||||
|
||||
@@ -23,7 +23,9 @@ class ScalingShutdown(MinerConfigValue):
|
||||
duration: int | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "ScalingShutdown":
|
||||
def from_dict(cls, dict_conf: dict | None) -> ScalingShutdown:
|
||||
if dict_conf is None:
|
||||
return cls()
|
||||
return cls(
|
||||
enabled=dict_conf.get("enabled", False), duration=dict_conf.get("duration")
|
||||
)
|
||||
@@ -51,7 +53,7 @@ class ScalingShutdown(MinerConfigValue):
|
||||
return None
|
||||
|
||||
def as_bosminer(self) -> dict:
|
||||
cfg = {"shutdown_enabled": self.enabled}
|
||||
cfg: dict[str, bool | int] = {"shutdown_enabled": self.enabled}
|
||||
|
||||
if self.duration is not None:
|
||||
cfg["shutdown_duration"] = self.duration
|
||||
@@ -68,7 +70,9 @@ class ScalingConfig(MinerConfigValue):
|
||||
shutdown: ScalingShutdown | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "ScalingConfig":
|
||||
def from_dict(cls, dict_conf: dict | None) -> ScalingConfig:
|
||||
if dict_conf is None:
|
||||
return cls()
|
||||
cls_conf = {
|
||||
"step": dict_conf.get("step"),
|
||||
"minimum": dict_conf.get("minimum"),
|
||||
|
||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import random
|
||||
import string
|
||||
from typing import List
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -66,12 +66,15 @@ class Pool(MinerConfigValue):
|
||||
|
||||
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,
|
||||
"pool": self.url,
|
||||
"worker": f"{self.user}{user_suffix or ''}",
|
||||
"passwd": self.password,
|
||||
}
|
||||
|
||||
def as_am_old(self, idx: int = 1, user_suffix: str | None = None) -> dict:
|
||||
def as_am_old(
|
||||
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
|
||||
) -> dict:
|
||||
idx = args[0] if args else kwargs.get("idx", 1)
|
||||
return {
|
||||
f"_ant_pool{idx}url": self.url,
|
||||
f"_ant_pool{idx}user": f"{self.user}{user_suffix or ''}",
|
||||
@@ -88,7 +91,10 @@ class Pool(MinerConfigValue):
|
||||
def as_avalon(self, user_suffix: str | None = None) -> str:
|
||||
return ",".join([self.url, f"{self.user}{user_suffix or ''}", self.password])
|
||||
|
||||
def as_inno(self, idx: int = 1, user_suffix: str | None = None) -> dict:
|
||||
def as_inno(
|
||||
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
|
||||
) -> dict:
|
||||
idx = args[0] if args else kwargs.get("idx", 1)
|
||||
return {
|
||||
f"Pool{idx}": self.url,
|
||||
f"UserName{idx}": f"{self.user}{user_suffix or ''}",
|
||||
@@ -109,7 +115,7 @@ class Pool(MinerConfigValue):
|
||||
"pass": self.password,
|
||||
}
|
||||
|
||||
def as_epic(self, user_suffix: str | None = None):
|
||||
def as_epic(self, user_suffix: str | None = None) -> dict:
|
||||
return {
|
||||
"pool": self.url,
|
||||
"login": f"{self.user}{user_suffix or ''}",
|
||||
@@ -146,58 +152,60 @@ class Pool(MinerConfigValue):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "Pool":
|
||||
def from_dict(cls, dict_conf: dict | None) -> Pool:
|
||||
if dict_conf is None:
|
||||
raise ValueError("dict_conf cannot be None")
|
||||
return cls(
|
||||
url=dict_conf["url"], user=dict_conf["user"], password=dict_conf["password"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_api(cls, api_pool: dict) -> "Pool":
|
||||
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":
|
||||
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":
|
||||
def from_epic(cls, api_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=api_pool["pool"], user=api_pool["login"], password=api_pool["password"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_am_modern(cls, web_pool: dict) -> "Pool":
|
||||
def from_am_modern(cls, web_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_hiveon_modern(cls, web_pool: dict) -> "Pool":
|
||||
def from_hiveon_modern(cls, web_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_elphapex(cls, web_pool: dict) -> "Pool":
|
||||
def from_elphapex(cls, web_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
|
||||
)
|
||||
|
||||
# TODO: check if this is accurate, user/username, pass/password
|
||||
@classmethod
|
||||
def from_goldshell(cls, web_pool: dict) -> "Pool":
|
||||
def from_goldshell(cls, web_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_inno(cls, web_pool: dict) -> "Pool":
|
||||
def from_inno(cls, web_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_bosminer(cls, toml_pool_conf: dict) -> "Pool":
|
||||
def from_bosminer(cls, toml_pool_conf: dict) -> Pool:
|
||||
return cls(
|
||||
url=toml_pool_conf["url"],
|
||||
user=toml_pool_conf["user"],
|
||||
@@ -205,7 +213,7 @@ class Pool(MinerConfigValue):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_vnish(cls, web_pool: dict) -> "Pool":
|
||||
def from_vnish(cls, web_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url="stratum+tcp://" + web_pool["url"],
|
||||
user=web_pool["user"],
|
||||
@@ -213,7 +221,7 @@ class Pool(MinerConfigValue):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_boser(cls, grpc_pool: dict) -> "Pool":
|
||||
def from_boser(cls, grpc_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=grpc_pool["url"],
|
||||
user=grpc_pool["user"],
|
||||
@@ -221,7 +229,7 @@ class Pool(MinerConfigValue):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_mara(cls, web_pool: dict) -> "Pool":
|
||||
def from_mara(cls, web_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=web_pool["url"],
|
||||
user=web_pool["user"],
|
||||
@@ -229,7 +237,7 @@ class Pool(MinerConfigValue):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_espminer(cls, web_system_info: dict) -> "Pool":
|
||||
def from_espminer(cls, web_system_info: dict) -> Pool:
|
||||
url = f"stratum+tcp://{web_system_info['stratumURL']}:{web_system_info['stratumPort']}"
|
||||
return cls(
|
||||
url=url,
|
||||
@@ -238,11 +246,11 @@ class Pool(MinerConfigValue):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_luxos(cls, rpc_pools: dict) -> "Pool":
|
||||
def from_luxos(cls, rpc_pools: dict) -> Pool:
|
||||
return cls.from_api(rpc_pools)
|
||||
|
||||
@classmethod
|
||||
def from_iceriver(cls, web_pool: dict) -> "Pool":
|
||||
def from_iceriver(cls, web_pool: dict) -> Pool:
|
||||
return cls(
|
||||
url=web_pool["addr"],
|
||||
user=web_pool["user"],
|
||||
@@ -294,34 +302,32 @@ class PoolGroup(MinerConfigValue):
|
||||
idx += 1
|
||||
return pools
|
||||
|
||||
def as_wm(self, user_suffix: str | None = None) -> dict:
|
||||
pools = {}
|
||||
def as_wm(self, *args: Any, user_suffix: str | None = None, **kwargs: Any) -> dict:
|
||||
pools: dict[str, str] = {}
|
||||
idx = 0
|
||||
while idx < 3:
|
||||
if len(self.pools) > idx:
|
||||
pools.update(
|
||||
**self.pools[idx].as_wm(idx=idx + 1, user_suffix=user_suffix)
|
||||
)
|
||||
pools.update(**self.pools[idx].as_wm(idx + 1, user_suffix=user_suffix))
|
||||
else:
|
||||
pools.update(**Pool(url="", user="", password="").as_wm(idx=idx + 1))
|
||||
pools.update(**Pool(url="", user="", password="").as_wm(idx + 1))
|
||||
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 = {}
|
||||
def as_am_old(
|
||||
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
|
||||
) -> dict:
|
||||
pools: dict[str, str] = {}
|
||||
idx = 0
|
||||
while idx < 3:
|
||||
if len(self.pools) > idx:
|
||||
pools.update(
|
||||
**self.pools[idx].as_am_old(idx=idx + 1, user_suffix=user_suffix)
|
||||
**self.pools[idx].as_am_old(idx + 1, user_suffix=user_suffix)
|
||||
)
|
||||
else:
|
||||
pools.update(
|
||||
**Pool(url="", user="", password="").as_am_old(idx=idx + 1)
|
||||
)
|
||||
pools.update(**Pool(url="", user="", password="").as_am_old(idx + 1))
|
||||
idx += 1
|
||||
return pools
|
||||
|
||||
@@ -333,22 +339,24 @@ class PoolGroup(MinerConfigValue):
|
||||
return self.pools[0].as_avalon(user_suffix=user_suffix)
|
||||
return Pool(url="", user="", password="").as_avalon()
|
||||
|
||||
def as_inno(self, user_suffix: str | None = None) -> dict:
|
||||
pools = {}
|
||||
def as_inno(
|
||||
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
|
||||
) -> dict:
|
||||
pools: dict[str, str] = {}
|
||||
idx = 0
|
||||
while idx < 3:
|
||||
if len(self.pools) > idx:
|
||||
pools.update(
|
||||
**self.pools[idx].as_inno(idx=idx + 1, user_suffix=user_suffix)
|
||||
**self.pools[idx].as_inno(idx + 1, user_suffix=user_suffix)
|
||||
)
|
||||
else:
|
||||
pools.update(**Pool(url="", user="", password="").as_inno(idx=idx + 1))
|
||||
pools.update(**Pool(url="", user="", password="").as_inno(idx + 1))
|
||||
idx += 1
|
||||
return pools
|
||||
|
||||
def as_bosminer(self, user_suffix: str | None = None) -> dict:
|
||||
if len(self.pools) > 0:
|
||||
conf = {
|
||||
conf: dict[str, Any] = {
|
||||
"name": self.name,
|
||||
"pool": [
|
||||
pool.as_bosminer(user_suffix=user_suffix) for pool in self.pools
|
||||
@@ -373,7 +381,7 @@ class PoolGroup(MinerConfigValue):
|
||||
|
||||
def as_boser(self, user_suffix: str | None = None) -> PoolGroupConfiguration:
|
||||
return PoolGroupConfiguration(
|
||||
name=self.name,
|
||||
name=self.name or "",
|
||||
quota=Quota(value=self.quota),
|
||||
pools=[p.as_boser() for p in self.pools],
|
||||
)
|
||||
@@ -382,7 +390,10 @@ class PoolGroup(MinerConfigValue):
|
||||
return {"pools": [p.as_vnish(user_suffix=user_suffix) for p in self.pools]}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "PoolGroup":
|
||||
def from_dict(cls, dict_conf: dict | None) -> PoolGroup:
|
||||
if dict_conf is None:
|
||||
return cls()
|
||||
|
||||
cls_conf = {}
|
||||
|
||||
if dict_conf.get("quota") is not None:
|
||||
@@ -393,57 +404,57 @@ class PoolGroup(MinerConfigValue):
|
||||
return cls(**cls_conf)
|
||||
|
||||
@classmethod
|
||||
def from_api(cls, api_pool_list: list) -> "PoolGroup":
|
||||
def from_api(cls, api_pool_list: list) -> PoolGroup:
|
||||
pools = []
|
||||
for pool in api_pool_list:
|
||||
pools.append(Pool.from_api(pool))
|
||||
return cls(pools=pools)
|
||||
|
||||
@classmethod
|
||||
def from_btminer_v3(cls, api_pool_list: list) -> "PoolGroup":
|
||||
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":
|
||||
def from_epic(cls, api_pool_list: list) -> PoolGroup:
|
||||
pools = []
|
||||
for pool in api_pool_list:
|
||||
pools.append(Pool.from_epic(pool))
|
||||
return cls(pools=pools)
|
||||
|
||||
@classmethod
|
||||
def from_am_modern(cls, web_pool_list: list) -> "PoolGroup":
|
||||
def from_am_modern(cls, web_pool_list: list) -> PoolGroup:
|
||||
pools = []
|
||||
for pool in web_pool_list:
|
||||
pools.append(Pool.from_am_modern(pool))
|
||||
return cls(pools=pools)
|
||||
|
||||
@classmethod
|
||||
def from_hiveon_modern(cls, web_pool_list: list) -> "PoolGroup":
|
||||
def from_hiveon_modern(cls, web_pool_list: list) -> PoolGroup:
|
||||
pools = []
|
||||
for pool in web_pool_list:
|
||||
pools.append(Pool.from_hiveon_modern(pool))
|
||||
return cls(pools=pools)
|
||||
|
||||
@classmethod
|
||||
def from_elphapex(cls, web_pool_list: list) -> "PoolGroup":
|
||||
def from_elphapex(cls, web_pool_list: list) -> PoolGroup:
|
||||
pools = []
|
||||
for pool in web_pool_list:
|
||||
pools.append(Pool.from_elphapex(pool))
|
||||
return cls(pools=pools)
|
||||
|
||||
@classmethod
|
||||
def from_goldshell(cls, web_pools: list) -> "PoolGroup":
|
||||
def from_goldshell(cls, web_pools: list) -> PoolGroup:
|
||||
return cls(pools=[Pool.from_goldshell(p) for p in web_pools])
|
||||
|
||||
@classmethod
|
||||
def from_inno(cls, web_pools: list) -> "PoolGroup":
|
||||
def from_inno(cls, web_pools: list) -> PoolGroup:
|
||||
return cls(pools=[Pool.from_inno(p) for p in web_pools])
|
||||
|
||||
@classmethod
|
||||
def from_bosminer(cls, toml_group_conf: dict) -> "PoolGroup":
|
||||
def from_bosminer(cls, toml_group_conf: dict) -> PoolGroup:
|
||||
if toml_group_conf.get("pool") is not None:
|
||||
return cls(
|
||||
name=toml_group_conf["name"],
|
||||
@@ -453,13 +464,13 @@ class PoolGroup(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup":
|
||||
def from_vnish(cls, web_settings_pools: dict) -> PoolGroup:
|
||||
return cls(
|
||||
pools=[Pool.from_vnish(p) for p in web_settings_pools if p["url"] != ""]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_boser(cls, grpc_pool_group: dict) -> "PoolGroup":
|
||||
def from_boser(cls, grpc_pool_group: dict) -> PoolGroup:
|
||||
try:
|
||||
return cls(
|
||||
pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]],
|
||||
@@ -474,15 +485,15 @@ class PoolGroup(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_mara(cls, web_config_pools: dict) -> "PoolGroup":
|
||||
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_espminer(cls, web_system_info: dict) -> "PoolGroup":
|
||||
def from_espminer(cls, web_system_info: dict) -> PoolGroup:
|
||||
return cls(pools=[Pool.from_espminer(web_system_info)])
|
||||
|
||||
@classmethod
|
||||
def from_iceriver(cls, web_userpanel: dict) -> "PoolGroup":
|
||||
def from_iceriver(cls, web_userpanel: dict) -> PoolGroup:
|
||||
return cls(
|
||||
pools=[
|
||||
Pool.from_iceriver(web_pool)
|
||||
@@ -492,21 +503,21 @@ class PoolGroup(MinerConfigValue):
|
||||
|
||||
|
||||
class PoolConfig(MinerConfigValue):
|
||||
groups: List[PoolGroup] = Field(default_factory=list)
|
||||
groups: list[PoolGroup] = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> "PoolConfig":
|
||||
def default(cls) -> PoolConfig:
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "PoolConfig":
|
||||
def from_dict(cls, dict_conf: dict | None) -> PoolConfig:
|
||||
if dict_conf is None:
|
||||
return cls.default()
|
||||
|
||||
return cls(groups=[PoolGroup.from_dict(g) for g in dict_conf["groups"]])
|
||||
|
||||
@classmethod
|
||||
def simple(cls, pools: list[Pool | dict[str, str]]) -> "PoolConfig":
|
||||
def simple(cls, pools: list[Pool | dict[str, str]]) -> PoolConfig:
|
||||
group_pools = []
|
||||
for pool in pools:
|
||||
if isinstance(pool, dict):
|
||||
@@ -529,7 +540,7 @@ class PoolConfig(MinerConfigValue):
|
||||
return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)}
|
||||
return {"pools": PoolGroup().as_elphapex()}
|
||||
|
||||
def as_wm(self, user_suffix: str | None = None) -> dict:
|
||||
def as_wm(self, *args: Any, user_suffix: str | None = None, **kwargs: Any) -> dict:
|
||||
if len(self.groups) > 0:
|
||||
return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)}
|
||||
return {"pools": PoolGroup().as_wm()}
|
||||
@@ -539,7 +550,9 @@ class PoolConfig(MinerConfigValue):
|
||||
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:
|
||||
def as_am_old(
|
||||
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
|
||||
) -> dict:
|
||||
if len(self.groups) > 0:
|
||||
return self.groups[0].as_am_old(user_suffix=user_suffix)
|
||||
return PoolGroup().as_am_old()
|
||||
@@ -554,7 +567,9 @@ class PoolConfig(MinerConfigValue):
|
||||
return {"pools": self.groups[0].as_avalon(user_suffix=user_suffix)}
|
||||
return {"pools": PoolGroup().as_avalon()}
|
||||
|
||||
def as_inno(self, user_suffix: str | None = None) -> dict:
|
||||
def as_inno(
|
||||
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
|
||||
) -> dict:
|
||||
if len(self.groups) > 0:
|
||||
return self.groups[0].as_inno(user_suffix=user_suffix)
|
||||
return PoolGroup().as_inno()
|
||||
@@ -569,7 +584,7 @@ class PoolConfig(MinerConfigValue):
|
||||
def as_boser(self, user_suffix: str | None = None) -> dict:
|
||||
return {
|
||||
"set_pool_groups": SetPoolGroupsRequest(
|
||||
save_action=SaveAction.SAVE_AND_APPLY,
|
||||
save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
pool_groups=[g.as_boser(user_suffix=user_suffix) for g in self.groups],
|
||||
)
|
||||
}
|
||||
@@ -615,7 +630,7 @@ class PoolConfig(MinerConfigValue):
|
||||
return self.groups[0].as_vnish(user_suffix=user_suffix)
|
||||
|
||||
@classmethod
|
||||
def from_api(cls, api_pools: dict) -> "PoolConfig":
|
||||
def from_api(cls, api_pools: dict) -> PoolConfig:
|
||||
try:
|
||||
pool_data = api_pools["POOLS"]
|
||||
except KeyError:
|
||||
@@ -625,7 +640,7 @@ class PoolConfig(MinerConfigValue):
|
||||
return cls(groups=[PoolGroup.from_api(pool_data)])
|
||||
|
||||
@classmethod
|
||||
def from_btminer_v3(cls, rpc_pools: dict) -> "PoolConfig":
|
||||
def from_btminer_v3(cls, rpc_pools: dict) -> PoolConfig:
|
||||
try:
|
||||
pool_data = rpc_pools["pools"]
|
||||
except KeyError:
|
||||
@@ -635,12 +650,12 @@ class PoolConfig(MinerConfigValue):
|
||||
return cls(groups=[PoolGroup.from_btminer_v3(pool_data)])
|
||||
|
||||
@classmethod
|
||||
def from_epic(cls, web_conf: dict) -> "PoolConfig":
|
||||
def from_epic(cls, web_conf: dict) -> PoolConfig:
|
||||
pool_data = web_conf["StratumConfigs"]
|
||||
return cls(groups=[PoolGroup.from_epic(pool_data)])
|
||||
|
||||
@classmethod
|
||||
def from_am_modern(cls, web_conf: dict) -> "PoolConfig":
|
||||
def from_am_modern(cls, web_conf: dict) -> PoolConfig:
|
||||
try:
|
||||
pool_data = web_conf["pools"]
|
||||
except KeyError:
|
||||
@@ -649,7 +664,7 @@ class PoolConfig(MinerConfigValue):
|
||||
return cls(groups=[PoolGroup.from_am_modern(pool_data)])
|
||||
|
||||
@classmethod
|
||||
def from_hiveon_modern(cls, web_conf: dict) -> "PoolConfig":
|
||||
def from_hiveon_modern(cls, web_conf: dict) -> PoolConfig:
|
||||
try:
|
||||
pool_data = web_conf["pools"]
|
||||
except KeyError:
|
||||
@@ -658,17 +673,17 @@ class PoolConfig(MinerConfigValue):
|
||||
return cls(groups=[PoolGroup.from_hiveon_modern(pool_data)])
|
||||
|
||||
@classmethod
|
||||
def from_elphapex(cls, web_conf: dict) -> "PoolConfig":
|
||||
def from_elphapex(cls, web_conf: dict) -> PoolConfig:
|
||||
pool_data = web_conf["pools"]
|
||||
|
||||
return cls(groups=[PoolGroup.from_elphapex(pool_data)])
|
||||
|
||||
@classmethod
|
||||
def from_goldshell(cls, web_pools: list) -> "PoolConfig":
|
||||
def from_goldshell(cls, web_pools: list) -> PoolConfig:
|
||||
return cls(groups=[PoolGroup.from_goldshell(web_pools)])
|
||||
|
||||
@classmethod
|
||||
def from_goldshell_byte(cls, web_pools: list) -> "PoolConfig":
|
||||
def from_goldshell_byte(cls, web_pools: list) -> PoolConfig:
|
||||
return cls(
|
||||
groups=[
|
||||
PoolGroup.from_goldshell(g["pools"])
|
||||
@@ -678,25 +693,25 @@ class PoolConfig(MinerConfigValue):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_inno(cls, web_pools: list) -> "PoolConfig":
|
||||
def from_inno(cls, web_pools: list) -> PoolConfig:
|
||||
return cls(groups=[PoolGroup.from_inno(web_pools)])
|
||||
|
||||
@classmethod
|
||||
def from_bosminer(cls, toml_conf: dict) -> "PoolConfig":
|
||||
def from_bosminer(cls, toml_conf: dict) -> PoolConfig:
|
||||
if toml_conf.get("group") is None:
|
||||
return cls()
|
||||
|
||||
return cls(groups=[PoolGroup.from_bosminer(g) for g in toml_conf["group"]])
|
||||
|
||||
@classmethod
|
||||
def from_vnish(cls, web_settings: dict) -> "PoolConfig":
|
||||
def from_vnish(cls, web_settings: dict) -> PoolConfig:
|
||||
try:
|
||||
return cls(groups=[PoolGroup.from_vnish(web_settings["miner"]["pools"])])
|
||||
except LookupError:
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_boser(cls, grpc_miner_conf: dict) -> "PoolConfig":
|
||||
def from_boser(cls, grpc_miner_conf: dict) -> PoolConfig:
|
||||
try:
|
||||
return cls(
|
||||
groups=[
|
||||
@@ -708,19 +723,19 @@ class PoolConfig(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_mara(cls, web_config: dict) -> "PoolConfig":
|
||||
def from_mara(cls, web_config: dict) -> PoolConfig:
|
||||
return cls(groups=[PoolGroup.from_mara(web_config["pools"])])
|
||||
|
||||
@classmethod
|
||||
def from_espminer(cls, web_system_info: dict) -> "PoolConfig":
|
||||
def from_espminer(cls, web_system_info: dict) -> PoolConfig:
|
||||
return cls(groups=[PoolGroup.from_espminer(web_system_info)])
|
||||
|
||||
@classmethod
|
||||
def from_iceriver(cls, web_userpanel: dict) -> "PoolConfig":
|
||||
def from_iceriver(cls, web_userpanel: dict) -> PoolConfig:
|
||||
return cls(groups=[PoolGroup.from_iceriver(web_userpanel)])
|
||||
|
||||
@classmethod
|
||||
def from_luxos(cls, rpc_groups: dict, rpc_pools: dict) -> "PoolConfig":
|
||||
def from_luxos(cls, rpc_groups: dict, rpc_pools: dict) -> PoolConfig:
|
||||
return cls(
|
||||
groups=[
|
||||
PoolGroup(
|
||||
|
||||
@@ -40,7 +40,7 @@ class TemperatureConfig(MinerConfigValue):
|
||||
return {"temp_control": temp_cfg}
|
||||
|
||||
def as_epic(self) -> dict:
|
||||
temps_config = {"temps": {}, "fans": {"Auto": {}}}
|
||||
temps_config: dict = {"temps": {}, "fans": {"Auto": {}}}
|
||||
if self.target is not None:
|
||||
temps_config["fans"]["Auto"]["Target Temperature"] = self.target
|
||||
else:
|
||||
@@ -58,7 +58,9 @@ class TemperatureConfig(MinerConfigValue):
|
||||
return {"misc": {"restart_temp": self.danger}}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, dict_conf: dict | None) -> "TemperatureConfig":
|
||||
def from_dict(cls, dict_conf: dict | None) -> TemperatureConfig:
|
||||
if dict_conf is None:
|
||||
return cls()
|
||||
return cls(
|
||||
target=dict_conf.get("target"),
|
||||
hot=dict_conf.get("hot"),
|
||||
@@ -66,7 +68,7 @@ class TemperatureConfig(MinerConfigValue):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_bosminer(cls, toml_conf: dict) -> "TemperatureConfig":
|
||||
def from_bosminer(cls, toml_conf: dict) -> TemperatureConfig:
|
||||
temp_control = toml_conf.get("temp_control")
|
||||
if temp_control is not None:
|
||||
return cls(
|
||||
@@ -77,7 +79,7 @@ class TemperatureConfig(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_epic(cls, web_conf: dict) -> "TemperatureConfig":
|
||||
def from_epic(cls, web_conf: dict) -> TemperatureConfig:
|
||||
try:
|
||||
dangerous_temp = web_conf["Misc"]["Critical Temp"]
|
||||
except KeyError:
|
||||
@@ -95,7 +97,7 @@ class TemperatureConfig(MinerConfigValue):
|
||||
return cls(target=target_temp, hot=hot_temp, danger=dangerous_temp)
|
||||
|
||||
@classmethod
|
||||
def from_vnish(cls, web_settings: dict) -> "TemperatureConfig":
|
||||
def from_vnish(cls, web_settings: dict) -> TemperatureConfig:
|
||||
try:
|
||||
dangerous_temp = web_settings["misc"]["restart_temp"]
|
||||
except KeyError:
|
||||
@@ -111,7 +113,7 @@ class TemperatureConfig(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_boser(cls, grpc_miner_conf: dict) -> "TemperatureConfig":
|
||||
def from_boser(cls, grpc_miner_conf: dict) -> TemperatureConfig:
|
||||
try:
|
||||
temperature_conf = grpc_miner_conf["temperature"]
|
||||
except KeyError:
|
||||
@@ -142,7 +144,7 @@ class TemperatureConfig(MinerConfigValue):
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def from_luxos(cls, rpc_tempctrl: dict) -> "TemperatureConfig":
|
||||
def from_luxos(cls, rpc_tempctrl: dict) -> TemperatureConfig:
|
||||
try:
|
||||
tempctrl_config = rpc_tempctrl["TEMPCTRL"][0]
|
||||
return cls(
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
import copy
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
@@ -24,6 +25,7 @@ from pyasic.config import MinerConfig
|
||||
from pyasic.config.mining import MiningModePowerTune
|
||||
from pyasic.data.pools import PoolMetrics, Scheme
|
||||
from pyasic.device.algorithm.hashrate import AlgoHashRateType
|
||||
from pyasic.device.algorithm.hashrate.base import GenericHashrate
|
||||
|
||||
from .boards import HashBoard
|
||||
from .device import DeviceInfo
|
||||
@@ -83,13 +85,16 @@ class MinerData(BaseModel):
|
||||
|
||||
# about
|
||||
device_info: DeviceInfo | None = None
|
||||
serial_number: str | None = None
|
||||
mac: str | None = None
|
||||
api_ver: str | None = None
|
||||
fw_ver: str | None = None
|
||||
hostname: str | None = None
|
||||
|
||||
# hashrate
|
||||
raw_hashrate: AlgoHashRateType = Field(exclude=True, default=None, repr=False)
|
||||
raw_hashrate: AlgoHashRateType | None = Field(
|
||||
exclude=True, default=None, repr=False
|
||||
)
|
||||
|
||||
# sticker
|
||||
sticker_hashrate: AlgoHashRateType | None = None
|
||||
@@ -193,7 +198,7 @@ class MinerData(BaseModel):
|
||||
setattr(cp, key, item & other_item)
|
||||
return cp
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def hashrate(self) -> AlgoHashRateType | None:
|
||||
if len(self.hashboards) > 0:
|
||||
@@ -202,14 +207,24 @@ class MinerData(BaseModel):
|
||||
if item.hashrate is not None:
|
||||
hr_data.append(item.hashrate)
|
||||
if len(hr_data) > 0:
|
||||
return sum(hr_data, start=self.device_info.algo.hashrate(rate=0))
|
||||
if self.device_info is not None and self.device_info.algo is not None:
|
||||
from pyasic.device.algorithm.hashrate.unit.base import GenericUnit
|
||||
|
||||
return sum(
|
||||
hr_data,
|
||||
start=self.device_info.algo.hashrate(
|
||||
rate=0, unit=GenericUnit.H
|
||||
),
|
||||
)
|
||||
else:
|
||||
return sum(hr_data, start=GenericHashrate(rate=0))
|
||||
return self.raw_hashrate
|
||||
|
||||
@hashrate.setter
|
||||
def hashrate(self, val):
|
||||
self.raw_hashrate = val
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def wattage_limit(self) -> int | None:
|
||||
if self.config is not None:
|
||||
@@ -221,7 +236,7 @@ class MinerData(BaseModel):
|
||||
def wattage_limit(self, val: int):
|
||||
self.raw_wattage_limit = val
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def total_chips(self) -> int | None:
|
||||
if len(self.hashboards) > 0:
|
||||
@@ -232,15 +247,16 @@ class MinerData(BaseModel):
|
||||
if len(chip_data) > 0:
|
||||
return sum(chip_data)
|
||||
return None
|
||||
return 0
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def nominal(self) -> bool | None:
|
||||
if self.total_chips is None or self.expected_chips is None:
|
||||
return None
|
||||
return self.expected_chips == self.total_chips
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def percent_expected_chips(self) -> int | None:
|
||||
if self.total_chips is None or self.expected_chips is None:
|
||||
@@ -249,7 +265,7 @@ class MinerData(BaseModel):
|
||||
return 0
|
||||
return round((self.total_chips / self.expected_chips) * 100)
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def percent_expected_hashrate(self) -> int | None:
|
||||
if self.hashrate is None or self.expected_hashrate is None:
|
||||
@@ -259,7 +275,7 @@ class MinerData(BaseModel):
|
||||
except ZeroDivisionError:
|
||||
return 0
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def percent_expected_wattage(self) -> int | None:
|
||||
if self.wattage_limit is None or self.wattage is None:
|
||||
@@ -269,10 +285,10 @@ class MinerData(BaseModel):
|
||||
except ZeroDivisionError:
|
||||
return 0
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def temperature_avg(self) -> int | None:
|
||||
total_temp = 0
|
||||
total_temp: float = 0
|
||||
temp_count = 0
|
||||
for hb in self.hashboards:
|
||||
if hb.temp is not None:
|
||||
@@ -282,7 +298,7 @@ class MinerData(BaseModel):
|
||||
return None
|
||||
return round(total_temp / temp_count)
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def efficiency(self) -> int | None:
|
||||
efficiency = self._efficiency(0)
|
||||
@@ -291,7 +307,7 @@ class MinerData(BaseModel):
|
||||
else:
|
||||
return int(efficiency)
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def efficiency_fract(self) -> float | None:
|
||||
return self._efficiency(2)
|
||||
@@ -304,39 +320,43 @@ class MinerData(BaseModel):
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def datetime(self) -> str:
|
||||
return self.raw_datetime.isoformat()
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def timestamp(self) -> int:
|
||||
return int(time.mktime(self.raw_datetime.timetuple()))
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def make(self) -> str | None:
|
||||
if self.device_info.make is not None:
|
||||
if self.device_info is not None and self.device_info.make is not None:
|
||||
return str(self.device_info.make)
|
||||
return ""
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def model(self) -> str | None:
|
||||
if self.device_info.model is not None:
|
||||
if self.device_info is not None and self.device_info.model is not None:
|
||||
return str(self.device_info.model)
|
||||
return ""
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def firmware(self) -> str | None:
|
||||
if self.device_info.firmware is not None:
|
||||
if self.device_info is not None and self.device_info.firmware is not None:
|
||||
return str(self.device_info.firmware)
|
||||
return ""
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def algo(self) -> str | None:
|
||||
if self.device_info.algo is not None:
|
||||
if self.device_info is not None and self.device_info.algo is not None:
|
||||
return str(self.device_info.algo)
|
||||
return ""
|
||||
|
||||
def keys(self) -> list:
|
||||
return list(self.model_fields.keys())
|
||||
@@ -416,7 +436,8 @@ class MinerData(BaseModel):
|
||||
for dt in serialization_map_instance:
|
||||
if item_serialized is None:
|
||||
if isinstance(list_field_val, dt):
|
||||
item_serialized = serialization_map_instance[dt](
|
||||
func = serialization_map_instance[dt]
|
||||
item_serialized = func(
|
||||
f"{key}{level_delimiter}{idx}", list_field_val
|
||||
)
|
||||
if item_serialized is not None:
|
||||
@@ -460,11 +481,11 @@ class MinerData(BaseModel):
|
||||
"pools",
|
||||
]
|
||||
|
||||
serialization_map_instance = {
|
||||
serialization_map_instance: dict[type, Callable[[str, Any], str | None]] = {
|
||||
AlgoHashRateType: serialize_algo_hash_rate,
|
||||
BaseMinerError: serialize_miner_error,
|
||||
}
|
||||
serialization_map = {
|
||||
serialization_map: dict[type, Callable[[str, Any], str | None]] = {
|
||||
int: serialize_int,
|
||||
float: serialize_float,
|
||||
str: serialize_str,
|
||||
@@ -498,9 +519,8 @@ class MinerData(BaseModel):
|
||||
for datatype in serialization_map_instance:
|
||||
if serialized is None:
|
||||
if isinstance(field_val, datatype):
|
||||
serialized = serialization_map_instance[datatype](
|
||||
field, field_val
|
||||
)
|
||||
func = serialization_map_instance[datatype]
|
||||
serialized = func(field, field_val)
|
||||
if serialized is not None:
|
||||
field_data.append(serialized)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -89,7 +90,7 @@ class HashBoard(BaseModel):
|
||||
def serialize_algo_hash_rate(key: str, value: AlgoHashRateType) -> str:
|
||||
return f"{key}={round(float(value), 2)}"
|
||||
|
||||
def serialize_bool(key: str, value: bool):
|
||||
def serialize_bool(key: str, value: bool) -> str:
|
||||
return f"{key}={str(value).lower()}"
|
||||
|
||||
serialization_map_instance = {
|
||||
@@ -116,8 +117,11 @@ class HashBoard(BaseModel):
|
||||
field_data = []
|
||||
for field in include:
|
||||
field_val = getattr(self, field)
|
||||
serialization_func = serialization_map.get(
|
||||
type(field_val), lambda _k, _v: None
|
||||
serialization_func: Callable[[str, Any], str | None] = (
|
||||
serialization_map.get(
|
||||
type(field_val),
|
||||
lambda _k, _v: None, # type: ignore
|
||||
)
|
||||
)
|
||||
serialized = serialization_func(
|
||||
f"{key_root}{level_delimiter}{field}", field_val
|
||||
|
||||
@@ -2,6 +2,8 @@ from pydantic import BaseModel
|
||||
|
||||
|
||||
class BaseMinerError(BaseModel):
|
||||
error_code: int | None = None
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return list(cls.model_fields.keys())
|
||||
@@ -24,9 +26,13 @@ class BaseMinerError(BaseModel):
|
||||
field_data.append(
|
||||
f"{root_key}{level_delimiter}error_code={self.error_code}"
|
||||
)
|
||||
if self.error_message is not None:
|
||||
|
||||
# Check if error_message exists as an attribute (either regular or computed field)
|
||||
if hasattr(self, "error_message"):
|
||||
error_message = getattr(self, "error_message")
|
||||
if error_message is not None:
|
||||
field_data.append(
|
||||
f'{root_key}{level_delimiter}error_message="{self.error_message}"'
|
||||
f'{root_key}{level_delimiter}error_message="{error_message}"'
|
||||
)
|
||||
|
||||
return ",".join(field_data)
|
||||
|
||||
@@ -30,7 +30,7 @@ class InnosiliconError(BaseMinerError):
|
||||
|
||||
error_code: int
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def error_message(self) -> str: # noqa - Skip PyCharm inspection
|
||||
if self.error_code in ERROR_CODES:
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pydantic import computed_field
|
||||
|
||||
from pyasic.data.error_codes.base import BaseMinerError
|
||||
@@ -28,50 +29,69 @@ class WhatsminerError(BaseMinerError):
|
||||
|
||||
error_code: int
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def error_message(self) -> str: # noqa - Skip PyCharm inspection
|
||||
if len(str(self.error_code)) == 6 and not str(self.error_code)[:1] == "1":
|
||||
err_type = int(str(self.error_code)[:2])
|
||||
err_subtype = int(str(self.error_code)[2:3])
|
||||
err_value = int(str(self.error_code)[3:])
|
||||
error_str = str(self.error_code)
|
||||
|
||||
# Handle edge cases for short error codes
|
||||
if len(error_str) < 3:
|
||||
return "Unknown error type."
|
||||
|
||||
if len(error_str) == 6 and not error_str[:1] == "1":
|
||||
err_type = int(error_str[:2])
|
||||
err_subtype = int(error_str[2:3])
|
||||
err_value = int(error_str[3:])
|
||||
else:
|
||||
err_type = int(str(self.error_code)[:-2])
|
||||
err_subtype = int(str(self.error_code)[-2:-1])
|
||||
err_value = int(str(self.error_code)[-1:])
|
||||
err_type = int(error_str[:-2])
|
||||
err_subtype = int(error_str[-2:-1])
|
||||
err_value = int(error_str[-1:])
|
||||
try:
|
||||
select_err_type = ERROR_CODES[err_type]
|
||||
select_err_type = ERROR_CODES.get(err_type)
|
||||
if select_err_type is None:
|
||||
return "Unknown error type."
|
||||
|
||||
if err_subtype in select_err_type:
|
||||
select_err_subtype = select_err_type[err_subtype]
|
||||
if isinstance(select_err_subtype, dict):
|
||||
if err_value in select_err_subtype:
|
||||
return select_err_subtype[err_value]
|
||||
result = select_err_subtype[err_value]
|
||||
return str(result) if not isinstance(result, str) else result
|
||||
elif "n" in select_err_subtype:
|
||||
return select_err_subtype[
|
||||
"n" # noqa: picks up `select_err_subtype["n"]` as not being numeric?
|
||||
].replace("{n}", str(err_value))
|
||||
template = select_err_subtype["n"]
|
||||
if isinstance(template, str):
|
||||
return template.replace("{n}", str(err_value))
|
||||
else:
|
||||
return "Unknown error type."
|
||||
else:
|
||||
return "Unknown error type."
|
||||
else:
|
||||
return "Unknown error type."
|
||||
elif "n" in select_err_type:
|
||||
select_err_subtype = select_err_type[
|
||||
"n" # noqa: picks up `select_err_subtype["n"]` as not being numeric?
|
||||
]
|
||||
select_err_subtype = select_err_type["n"]
|
||||
if isinstance(select_err_subtype, dict):
|
||||
if err_value in select_err_subtype:
|
||||
return select_err_subtype[err_value]
|
||||
result = select_err_subtype[err_value]
|
||||
return str(result) if not isinstance(result, str) else result
|
||||
elif "c" in select_err_subtype:
|
||||
return (
|
||||
select_err_subtype["c"]
|
||||
.replace( # noqa: picks up `select_err_subtype["n"]` as not being numeric?
|
||||
"{n}", str(err_subtype)
|
||||
)
|
||||
.replace("{c}", str(err_value))
|
||||
template = select_err_subtype["c"]
|
||||
if isinstance(template, str):
|
||||
return template.replace("{n}", str(err_subtype)).replace(
|
||||
"{c}", str(err_value)
|
||||
)
|
||||
else:
|
||||
return "Unknown error type."
|
||||
except KeyError:
|
||||
else:
|
||||
return "Unknown error type."
|
||||
else:
|
||||
return "Unknown error type."
|
||||
else:
|
||||
return "Unknown error type."
|
||||
except (KeyError, TypeError):
|
||||
return "Unknown error type."
|
||||
|
||||
|
||||
ERROR_CODES = {
|
||||
ERROR_CODES: dict[int, dict[int | str, str | dict[int | str, str]]] = {
|
||||
1: { # Fan error
|
||||
0: {
|
||||
0: "Fan unknown.",
|
||||
|
||||
@@ -26,7 +26,7 @@ class Fan(BaseModel):
|
||||
speed: The speed of the fan.
|
||||
"""
|
||||
|
||||
speed: int = None
|
||||
speed: int | None = None
|
||||
|
||||
def get(self, __key: str, default: Any = None):
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import BaseModel, computed_field, model_serializer
|
||||
@@ -16,7 +17,7 @@ class PoolUrl(BaseModel):
|
||||
scheme: Scheme
|
||||
host: str
|
||||
port: int
|
||||
pubkey: Optional[str] = None
|
||||
pubkey: str | None = None
|
||||
|
||||
@model_serializer
|
||||
def serialize(self):
|
||||
@@ -39,6 +40,8 @@ class PoolUrl(BaseModel):
|
||||
scheme = Scheme.STRATUM_V1
|
||||
host = parsed_url.hostname
|
||||
port = parsed_url.port
|
||||
if port is None:
|
||||
return None
|
||||
pubkey = parsed_url.path.lstrip("/") if scheme == Scheme.STRATUM_V2 else None
|
||||
return cls(scheme=scheme, host=host, port=port, pubkey=pubkey)
|
||||
|
||||
@@ -70,16 +73,20 @@ class PoolMetrics(BaseModel):
|
||||
index: int | None = None
|
||||
user: str | None = None
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def pool_rejected_percent(self) -> float: # noqa - Skip PyCharm inspection
|
||||
"""Calculate and return the percentage of rejected shares"""
|
||||
if self.rejected is None or self.accepted is None:
|
||||
return 0.0
|
||||
return self._calculate_percentage(self.rejected, self.accepted + self.rejected)
|
||||
|
||||
@computed_field # type: ignore[misc]
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def pool_stale_percent(self) -> float: # noqa - Skip PyCharm inspection
|
||||
"""Calculate and return the percentage of stale shares."""
|
||||
if self.get_failures is None or self.accepted is None or self.rejected is None:
|
||||
return 0.0
|
||||
return self._calculate_percentage(
|
||||
self.get_failures, self.accepted + self.rejected
|
||||
)
|
||||
@@ -87,10 +94,8 @@ class PoolMetrics(BaseModel):
|
||||
@staticmethod
|
||||
def _calculate_percentage(value: int, total: int) -> float:
|
||||
"""Calculate the percentage."""
|
||||
if value is None or total is None:
|
||||
return 0
|
||||
if total == 0:
|
||||
return 0
|
||||
return 0.0
|
||||
return (value / total) * 100
|
||||
|
||||
def as_influxdb(self, key_root: str, level_delimiter: str = ".") -> str:
|
||||
@@ -103,13 +108,13 @@ class PoolMetrics(BaseModel):
|
||||
def serialize_str(key: str, value: str) -> str:
|
||||
return f'{key}="{value}"'
|
||||
|
||||
def serialize_pool_url(key: str, value: str) -> str:
|
||||
def serialize_pool_url(key: str, value: PoolUrl) -> str:
|
||||
return f'{key}="{str(value)}"'
|
||||
|
||||
def serialize_bool(key: str, value: bool):
|
||||
def serialize_bool(key: str, value: bool) -> str:
|
||||
return f"{key}={str(value).lower()}"
|
||||
|
||||
serialization_map = {
|
||||
serialization_map: dict[type, Callable[[str, Any], str]] = {
|
||||
int: serialize_int,
|
||||
float: serialize_float,
|
||||
str: serialize_str,
|
||||
@@ -129,9 +134,10 @@ class PoolMetrics(BaseModel):
|
||||
field_data = []
|
||||
for field in include:
|
||||
field_val = getattr(self, field)
|
||||
serialization_func = serialization_map.get(
|
||||
type(field_val), lambda _k, _v: None
|
||||
)
|
||||
if field_val is None:
|
||||
continue
|
||||
serialization_func = serialization_map.get(type(field_val))
|
||||
if serialization_func is not None:
|
||||
serialized = serialization_func(
|
||||
f"{key_root}{level_delimiter}{field}", field_val
|
||||
)
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel, field_serializer
|
||||
from typing_extensions import Self
|
||||
|
||||
from .unit.base import AlgoHashRateUnitType, GenericUnit
|
||||
|
||||
UnitType = TypeVar("UnitType", bound=AlgoHashRateUnitType)
|
||||
|
||||
class AlgoHashRateType(BaseModel, ABC):
|
||||
unit: AlgoHashRateUnitType
|
||||
|
||||
class AlgoHashRateType(BaseModel, ABC, Generic[UnitType]):
|
||||
unit: UnitType
|
||||
rate: float
|
||||
|
||||
@field_serializer("unit")
|
||||
def serialize_unit(self, unit: AlgoHashRateUnitType):
|
||||
def serialize_unit(self, unit: UnitType):
|
||||
return unit.model_dump()
|
||||
|
||||
@abstractmethod
|
||||
def into(self, other: "AlgoHashRateUnitType"):
|
||||
def into(self, other: UnitType) -> Self:
|
||||
pass
|
||||
|
||||
def auto_unit(self):
|
||||
@@ -46,7 +49,7 @@ class AlgoHashRateType(BaseModel, ABC):
|
||||
def __repr__(self):
|
||||
return f"{self.rate} {str(self.unit)}"
|
||||
|
||||
def __round__(self, n: int = None):
|
||||
def __round__(self, n: int | None = None):
|
||||
return round(self.rate, n)
|
||||
|
||||
def __add__(self, other: Self | int | float) -> Self:
|
||||
@@ -85,11 +88,11 @@ class AlgoHashRateType(BaseModel, ABC):
|
||||
return self.__class__(rate=self.rate * other, unit=self.unit)
|
||||
|
||||
|
||||
class GenericHashrate(AlgoHashRateType):
|
||||
class GenericHashrate(AlgoHashRateType[GenericUnit]):
|
||||
rate: float = 0
|
||||
unit: GenericUnit = GenericUnit.H
|
||||
|
||||
def into(self, other: GenericUnit):
|
||||
def into(self, other: GenericUnit) -> Self:
|
||||
return self.__class__(
|
||||
rate=self.rate / (other.value / self.unit.value), unit=other
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.blake256 import Blake256Unit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class Blake256HashRate(AlgoHashRateType):
|
||||
class Blake256HashRate(AlgoHashRateType[Blake256Unit]):
|
||||
rate: float
|
||||
unit: Blake256Unit = HashUnit.BLAKE256.default
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.blockflow import BlockFlowUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class BlockFlowHashRate(AlgoHashRateType):
|
||||
class BlockFlowHashRate(AlgoHashRateType[BlockFlowUnit]):
|
||||
rate: float
|
||||
unit: BlockFlowUnit = HashUnit.BLOCKFLOW.default
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.eaglesong import EaglesongUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class EaglesongHashRate(AlgoHashRateType):
|
||||
class EaglesongHashRate(AlgoHashRateType[EaglesongUnit]):
|
||||
rate: float
|
||||
unit: EaglesongUnit = HashUnit.EAGLESONG.default
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ from pyasic.device.algorithm.hashrate.unit.equihash import EquihashUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class EquihashHashRate(AlgoHashRateType):
|
||||
class EquihashHashRate(AlgoHashRateType[EquihashUnit]):
|
||||
rate: float
|
||||
unit: EquihashUnit = HashUnit.ETHASH.default
|
||||
unit: EquihashUnit = HashUnit.EQUIHASH.default
|
||||
|
||||
def into(self, other: EquihashUnit) -> Self:
|
||||
return self.__class__(
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.ethash import EtHashUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class EtHashHashRate(AlgoHashRateType):
|
||||
class EtHashHashRate(AlgoHashRateType[EtHashUnit]):
|
||||
rate: float
|
||||
unit: EtHashUnit = HashUnit.ETHASH.default
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.handshake import HandshakeUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class HandshakeHashRate(AlgoHashRateType):
|
||||
class HandshakeHashRate(AlgoHashRateType[HandshakeUnit]):
|
||||
rate: float
|
||||
unit: HandshakeUnit = HashUnit.HANDSHAKE.default
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.kadena import KadenaUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class KadenaHashRate(AlgoHashRateType):
|
||||
class KadenaHashRate(AlgoHashRateType[KadenaUnit]):
|
||||
rate: float
|
||||
unit: KadenaUnit = HashUnit.KADENA.default
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.kheavyhash import KHeavyHashUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class KHeavyHashHashRate(AlgoHashRateType):
|
||||
class KHeavyHashHashRate(AlgoHashRateType[KHeavyHashUnit]):
|
||||
rate: float
|
||||
unit: KHeavyHashUnit = HashUnit.KHEAVYHASH.default
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.scrypt import ScryptUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class ScryptHashRate(AlgoHashRateType):
|
||||
class ScryptHashRate(AlgoHashRateType[ScryptUnit]):
|
||||
rate: float
|
||||
unit: ScryptUnit = HashUnit.SCRYPT.default
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.sha256 import SHA256Unit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class SHA256HashRate(AlgoHashRateType):
|
||||
class SHA256HashRate(AlgoHashRateType[SHA256Unit]):
|
||||
rate: float
|
||||
unit: SHA256Unit = HashUnit.SHA256.default
|
||||
|
||||
|
||||
@@ -2,54 +2,46 @@ from enum import IntEnum
|
||||
|
||||
|
||||
class AlgoHashRateUnitType(IntEnum):
|
||||
H: int
|
||||
KH: int
|
||||
MH: int
|
||||
GH: int
|
||||
TH: int
|
||||
PH: int
|
||||
EH: int
|
||||
ZH: int
|
||||
|
||||
default: int
|
||||
|
||||
def __str__(self):
|
||||
if self.value == self.H:
|
||||
if hasattr(self.__class__, "H") and self.value == self.__class__.H:
|
||||
return "H/s"
|
||||
if self.value == self.KH:
|
||||
if hasattr(self.__class__, "KH") and self.value == self.__class__.KH:
|
||||
return "KH/s"
|
||||
if self.value == self.MH:
|
||||
if hasattr(self.__class__, "MH") and self.value == self.__class__.MH:
|
||||
return "MH/s"
|
||||
if self.value == self.GH:
|
||||
if hasattr(self.__class__, "GH") and self.value == self.__class__.GH:
|
||||
return "GH/s"
|
||||
if self.value == self.TH:
|
||||
if hasattr(self.__class__, "TH") and self.value == self.__class__.TH:
|
||||
return "TH/s"
|
||||
if self.value == self.PH:
|
||||
if hasattr(self.__class__, "PH") and self.value == self.__class__.PH:
|
||||
return "PH/s"
|
||||
if self.value == self.EH:
|
||||
if hasattr(self.__class__, "EH") and self.value == self.__class__.EH:
|
||||
return "EH/s"
|
||||
if self.value == self.ZH:
|
||||
if hasattr(self.__class__, "ZH") and self.value == self.__class__.ZH:
|
||||
return "ZH/s"
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, value: str):
|
||||
if value == "H":
|
||||
if value == "H" and hasattr(cls, "H"):
|
||||
return cls.H
|
||||
elif value == "KH":
|
||||
elif value == "KH" and hasattr(cls, "KH"):
|
||||
return cls.KH
|
||||
elif value == "MH":
|
||||
elif value == "MH" and hasattr(cls, "MH"):
|
||||
return cls.MH
|
||||
elif value == "GH":
|
||||
elif value == "GH" and hasattr(cls, "GH"):
|
||||
return cls.GH
|
||||
elif value == "TH":
|
||||
elif value == "TH" and hasattr(cls, "TH"):
|
||||
return cls.TH
|
||||
elif value == "PH":
|
||||
elif value == "PH" and hasattr(cls, "PH"):
|
||||
return cls.PH
|
||||
elif value == "EH":
|
||||
elif value == "EH" and hasattr(cls, "EH"):
|
||||
return cls.EH
|
||||
elif value == "ZH":
|
||||
elif value == "ZH" and hasattr(cls, "ZH"):
|
||||
return cls.ZH
|
||||
if hasattr(cls, "default"):
|
||||
return cls.default
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.x11 import X11Unit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class X11HashRate(AlgoHashRateType):
|
||||
class X11HashRate(AlgoHashRateType[X11Unit]):
|
||||
rate: float
|
||||
unit: X11Unit = HashUnit.X11.default
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.zksnark import ZkSnarkUnit
|
||||
from .unit import HashUnit
|
||||
|
||||
|
||||
class ZkSnarkHashRate(AlgoHashRateType):
|
||||
class ZkSnarkHashRate(AlgoHashRateType[ZkSnarkUnit]):
|
||||
rate: float
|
||||
unit: ZkSnarkUnit = HashUnit.ZKSNARK.default
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@ class WhatsminerModels(MinerModelType):
|
||||
M31V20 = "M31 V20"
|
||||
M32V10 = "M32 V10"
|
||||
M32V20 = "M32 V20"
|
||||
M32S = "M32S"
|
||||
M33SPlusPlusVG40 = "M33S++ VG40"
|
||||
M33SPlusPlusVH20 = "M33S++ VH20"
|
||||
M33SPlusPlusVH30 = "M33S++ VH30"
|
||||
@@ -562,12 +563,18 @@ class BraiinsModels(MinerModelType):
|
||||
BMM100 = "BMM100"
|
||||
BMM101 = "BMM101"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class ElphapexModels(MinerModelType):
|
||||
DG1 = "DG1"
|
||||
DG1Plus = "DG1+"
|
||||
DG1Home = "DG1Home"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class MinerModel:
|
||||
ANTMINER = AntminerModels
|
||||
|
||||
@@ -1,352 +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. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
from typing import List, Union
|
||||
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners import AnyMiner
|
||||
from pyasic.miners.backends import AntminerModern, BOSMiner, BTMiner
|
||||
from pyasic.miners.device.models import (
|
||||
S9,
|
||||
S17,
|
||||
T17,
|
||||
S17e,
|
||||
S17Plus,
|
||||
S17Pro,
|
||||
T17e,
|
||||
T17Plus,
|
||||
)
|
||||
|
||||
FAN_USAGE = 50 # 50 W per fan
|
||||
|
||||
|
||||
class MinerLoadBalancer:
|
||||
"""A load balancer for miners. Can be passed a list of `AnyMiner`, or a list of phases (lists of `AnyMiner`)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
phases: Union[List[List[AnyMiner]], None] = None,
|
||||
):
|
||||
self.phases = [_MinerPhaseBalancer(phase) for phase in phases]
|
||||
|
||||
async def balance(self, wattage: int) -> int:
|
||||
phase_wattage = wattage // len(self.phases)
|
||||
setpoints = await asyncio.gather(
|
||||
*[phase.get_balance_setpoints(phase_wattage) for phase in self.phases]
|
||||
)
|
||||
tasks = []
|
||||
total_wattage = 0
|
||||
for setpoint in setpoints:
|
||||
wattage_set = 0
|
||||
for miner in setpoint:
|
||||
if setpoint[miner]["set"] == "on":
|
||||
wattage_set += setpoint[miner]["max"]
|
||||
tasks.append(setpoint[miner]["miner"].resume_mining())
|
||||
elif setpoint[miner]["set"] == "off":
|
||||
wattage_set += setpoint[miner]["min"]
|
||||
tasks.append(setpoint[miner]["miner"].stop_mining())
|
||||
else:
|
||||
wattage_set += setpoint[miner]["set"]
|
||||
tasks.append(
|
||||
setpoint[miner]["miner"].set_power_limit(setpoint[miner]["set"])
|
||||
)
|
||||
total_wattage += wattage_set
|
||||
await asyncio.gather(*tasks)
|
||||
return total_wattage
|
||||
|
||||
|
||||
class _MinerPhaseBalancer:
|
||||
def __init__(self, miners: List[AnyMiner]):
|
||||
self.miners = {
|
||||
str(miner.ip): {
|
||||
"miner": miner,
|
||||
"set": 0,
|
||||
"min": miner.expected_fans * FAN_USAGE,
|
||||
}
|
||||
for miner in miners
|
||||
}
|
||||
for miner in miners:
|
||||
if (
|
||||
isinstance(miner, BTMiner)
|
||||
and not (miner.raw_model.startswith("M2") if miner.raw_model else True)
|
||||
) or isinstance(miner, BOSMiner):
|
||||
if isinstance(miner, S9):
|
||||
self.miners[str(miner.ip)]["tune"] = True
|
||||
self.miners[str(miner.ip)]["shutdown"] = True
|
||||
self.miners[str(miner.ip)]["max"] = 1400
|
||||
elif True in [
|
||||
isinstance(miner, x)
|
||||
for x in [S17, S17Plus, S17Pro, S17e, T17, T17Plus, T17e]
|
||||
]:
|
||||
self.miners[str(miner.ip)]["tune"] = True
|
||||
self.miners[str(miner.ip)]["shutdown"] = True
|
||||
self.miners[str(miner.ip)]["max"] = 2400
|
||||
else:
|
||||
self.miners[str(miner.ip)]["tune"] = True
|
||||
self.miners[str(miner.ip)]["shutdown"] = True
|
||||
self.miners[str(miner.ip)]["max"] = 3600
|
||||
elif isinstance(miner, AntminerModern):
|
||||
self.miners[str(miner.ip)]["tune"] = False
|
||||
self.miners[str(miner.ip)]["shutdown"] = True
|
||||
self.miners[str(miner.ip)]["max"] = 3600
|
||||
elif isinstance(miner, BTMiner):
|
||||
self.miners[str(miner.ip)]["tune"] = False
|
||||
self.miners[str(miner.ip)]["shutdown"] = True
|
||||
self.miners[str(miner.ip)]["max"] = 3600
|
||||
if miner.raw_model:
|
||||
if miner.raw_model.startswith("M2"):
|
||||
self.miners[str(miner.ip)]["tune"] = False
|
||||
self.miners[str(miner.ip)]["shutdown"] = True
|
||||
self.miners[str(miner.ip)]["max"] = 2400
|
||||
else:
|
||||
self.miners[str(miner.ip)]["tune"] = False
|
||||
self.miners[str(miner.ip)]["shutdown"] = False
|
||||
self.miners[str(miner.ip)]["max"] = 3600
|
||||
self.miners[str(miner.ip)]["min"] = 3600
|
||||
|
||||
async def balance(self, wattage: int) -> int:
|
||||
setpoint = await self.get_balance_setpoints(wattage)
|
||||
wattage_set = 0
|
||||
tasks = []
|
||||
for miner in setpoint:
|
||||
if setpoint[miner]["set"] == "on":
|
||||
wattage_set += setpoint[miner]["max"]
|
||||
tasks.append(setpoint[miner]["miner"].resume_mining())
|
||||
elif setpoint[miner]["set"] == "off":
|
||||
wattage_set += setpoint[miner]["min"]
|
||||
tasks.append(setpoint[miner]["miner"].stop_mining())
|
||||
else:
|
||||
wattage_set += setpoint[miner]["set"]
|
||||
tasks.append(
|
||||
setpoint[miner]["miner"].set_power_limit(setpoint[miner]["set"])
|
||||
)
|
||||
await asyncio.gather(*tasks)
|
||||
return wattage_set
|
||||
|
||||
async def get_balance_setpoints(self, wattage: int) -> dict:
|
||||
# gather data needed to optimize shutdown only miners
|
||||
dp = ["hashrate", "wattage", "wattage_limit", "hashboards"]
|
||||
data = await asyncio.gather(
|
||||
*[
|
||||
self.miners[miner]["miner"].get_data(data_to_get=dp)
|
||||
for miner in self.miners
|
||||
]
|
||||
)
|
||||
pct_expected_list = [d.percent_ideal for d in data]
|
||||
pct_ideal = 0
|
||||
if len(pct_expected_list) > 0:
|
||||
pct_ideal = sum(pct_expected_list) / len(pct_expected_list)
|
||||
|
||||
wattage = round(wattage * 1 / (pct_ideal / 100))
|
||||
|
||||
for data_point in data:
|
||||
if (not self.miners[data_point.ip]["tune"]) and (
|
||||
not self.miners[data_point.ip]["shutdown"]
|
||||
):
|
||||
# cant do anything with it so need to find a semi-accurate power limit
|
||||
if data_point.wattage_limit is not None:
|
||||
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
|
||||
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
|
||||
elif data_point.wattage is not None:
|
||||
self.miners[data_point.ip]["max"] = int(data_point.wattage)
|
||||
self.miners[data_point.ip]["min"] = int(data_point.wattage)
|
||||
|
||||
max_tune_wattage = sum(
|
||||
[miner["max"] for miner in self.miners.values() if miner["tune"]]
|
||||
)
|
||||
max_shutdown_wattage = sum(
|
||||
[
|
||||
miner["max"]
|
||||
for miner in self.miners.values()
|
||||
if (not miner["tune"]) and (miner["shutdown"])
|
||||
]
|
||||
)
|
||||
max_other_wattage = sum(
|
||||
[
|
||||
miner["max"]
|
||||
for miner in self.miners.values()
|
||||
if (not miner["tune"]) and (not miner["shutdown"])
|
||||
]
|
||||
)
|
||||
min_tune_wattage = sum(
|
||||
[miner["min"] for miner in self.miners.values() if miner["tune"]]
|
||||
)
|
||||
min_shutdown_wattage = sum(
|
||||
[
|
||||
miner["min"]
|
||||
for miner in self.miners.values()
|
||||
if (not miner["tune"]) and (miner["shutdown"])
|
||||
]
|
||||
)
|
||||
# min_other_wattage = sum(
|
||||
# [
|
||||
# miner["min"]
|
||||
# for miner in self.miners.values()
|
||||
# if (not miner["tune"]) and (not miner["shutdown"])
|
||||
# ]
|
||||
# )
|
||||
|
||||
# make sure wattage isnt set too high
|
||||
if wattage > (max_tune_wattage + max_shutdown_wattage + max_other_wattage):
|
||||
raise APIError(
|
||||
f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W"
|
||||
)
|
||||
|
||||
# should now know wattage limits and which can be tuned/shutdown
|
||||
# check if 1/2 max of the miners which can be tuned is low enough
|
||||
if (max_tune_wattage / 2) + max_shutdown_wattage + max_other_wattage < wattage:
|
||||
useable_wattage = wattage - (max_other_wattage + max_shutdown_wattage)
|
||||
useable_miners = len(
|
||||
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
|
||||
)
|
||||
if not useable_miners == 0:
|
||||
watts_per_miner = useable_wattage // useable_miners
|
||||
# loop through and set useable miners to wattage
|
||||
for miner in self.miners:
|
||||
if (self.miners[miner]["set"] == 0) and (
|
||||
self.miners[miner]["tune"]
|
||||
):
|
||||
self.miners[miner]["set"] = watts_per_miner
|
||||
elif self.miners[miner]["set"] == 0 and (
|
||||
self.miners[miner]["shutdown"]
|
||||
):
|
||||
self.miners[miner]["set"] = "on"
|
||||
|
||||
# check if shutting down miners will help
|
||||
elif (
|
||||
max_tune_wattage / 2
|
||||
) + min_shutdown_wattage + max_other_wattage < wattage:
|
||||
# tuneable inclusive since could be S9 BOS+ and S19 Stock, would rather shut down the S9, tuneable should always support shutdown
|
||||
useable_wattage = wattage - (
|
||||
min_tune_wattage + max_other_wattage + min_shutdown_wattage
|
||||
)
|
||||
for miner in sorted(
|
||||
[miner for miner in self.miners.values() if miner["shutdown"]],
|
||||
key=lambda x: x["max"],
|
||||
reverse=True,
|
||||
):
|
||||
if miner["tune"]:
|
||||
miner_min_watt_use = miner["max"] / 2
|
||||
useable_wattage -= miner_min_watt_use - miner["min"]
|
||||
if useable_wattage < 0:
|
||||
useable_wattage += miner_min_watt_use - miner["min"]
|
||||
self.miners[str(miner["miner"].ip)]["set"] = "off"
|
||||
else:
|
||||
miner_min_watt_use = miner["max"]
|
||||
useable_wattage -= miner_min_watt_use - miner["min"]
|
||||
if useable_wattage < 0:
|
||||
useable_wattage += miner_min_watt_use - miner["min"]
|
||||
self.miners[str(miner["miner"].ip)]["set"] = "off"
|
||||
|
||||
new_shutdown_wattage = sum(
|
||||
[
|
||||
miner["max"] if miner["set"] == 0 else miner["min"]
|
||||
for miner in self.miners.values()
|
||||
if miner["shutdown"] and not miner["tune"]
|
||||
]
|
||||
)
|
||||
new_tune_wattage = sum(
|
||||
[
|
||||
miner["min"]
|
||||
for miner in self.miners.values()
|
||||
if miner["tune"] and miner["set"] == "off"
|
||||
]
|
||||
)
|
||||
|
||||
useable_wattage = wattage - (
|
||||
new_tune_wattage + max_other_wattage + new_shutdown_wattage
|
||||
)
|
||||
useable_miners = len(
|
||||
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
|
||||
)
|
||||
|
||||
if not useable_miners == 0:
|
||||
watts_per_miner = useable_wattage // useable_miners
|
||||
# loop through and set useable miners to wattage
|
||||
for miner in self.miners:
|
||||
if (self.miners[miner]["set"] == 0) and (
|
||||
self.miners[miner]["tune"]
|
||||
):
|
||||
self.miners[miner]["set"] = watts_per_miner
|
||||
elif self.miners[miner]["set"] == 0 and (
|
||||
self.miners[miner]["shutdown"]
|
||||
):
|
||||
self.miners[miner]["set"] = "on"
|
||||
|
||||
# check if shutting down tuneable miners will do it
|
||||
elif min_tune_wattage + min_shutdown_wattage + max_other_wattage < wattage:
|
||||
# all miners that can be shutdown need to be
|
||||
for miner in self.miners:
|
||||
if (not self.miners[miner]["tune"]) and (
|
||||
self.miners[miner]["shutdown"]
|
||||
):
|
||||
self.miners[miner]["set"] = "off"
|
||||
# calculate wattage usable by tuneable miners
|
||||
useable_wattage = wattage - (
|
||||
min_tune_wattage + max_other_wattage + min_shutdown_wattage
|
||||
)
|
||||
|
||||
# loop through miners to see how much is actually useable
|
||||
# sort the largest first
|
||||
for miner in sorted(
|
||||
[
|
||||
miner
|
||||
for miner in self.miners.values()
|
||||
if miner["tune"] and miner["shutdown"]
|
||||
],
|
||||
key=lambda x: x["max"],
|
||||
reverse=True,
|
||||
):
|
||||
# add min to useable wattage since it was removed earlier, and remove 1/2 tuner max
|
||||
useable_wattage -= (miner["max"] / 2) - miner["min"]
|
||||
if useable_wattage < 0:
|
||||
useable_wattage += (miner["max"] / 2) - miner["min"]
|
||||
self.miners[str(miner["miner"].ip)]["set"] = "off"
|
||||
|
||||
new_tune_wattage = sum(
|
||||
[
|
||||
miner["min"]
|
||||
for miner in self.miners.values()
|
||||
if miner["tune"] and miner["set"] == "off"
|
||||
]
|
||||
)
|
||||
|
||||
useable_wattage = wattage - (
|
||||
new_tune_wattage + max_other_wattage + min_shutdown_wattage
|
||||
)
|
||||
useable_miners = len(
|
||||
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
|
||||
)
|
||||
|
||||
if not useable_miners == 0:
|
||||
watts_per_miner = useable_wattage // useable_miners
|
||||
# loop through and set useable miners to wattage
|
||||
for miner in self.miners:
|
||||
if (self.miners[miner]["set"] == 0) and (
|
||||
self.miners[miner]["tune"]
|
||||
):
|
||||
self.miners[miner]["set"] = watts_per_miner
|
||||
elif self.miners[miner]["set"] == 0 and (
|
||||
self.miners[miner]["shutdown"]
|
||||
):
|
||||
self.miners[miner]["set"] = "on"
|
||||
else:
|
||||
raise APIError(
|
||||
f"Wattage setpoint is too low, setpoint: {wattage}W, min: {min_tune_wattage + min_shutdown_wattage + max_other_wattage}W"
|
||||
) # PhaseBalancingError(f"Wattage setpoint is too low, setpoint: {wattage}W, min: {min_tune_wattage + min_shutdown_wattage + max_other_wattage}W")
|
||||
|
||||
return self.miners
|
||||
@@ -14,7 +14,7 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.device.models import MinerModel
|
||||
from pyasic.device.models import MinerModel, MinerModelType
|
||||
from pyasic.miners.backends import ePIC
|
||||
from pyasic.miners.device.models import (
|
||||
S19,
|
||||
@@ -56,12 +56,12 @@ class ePICS19XP(ePIC, S19XP):
|
||||
|
||||
|
||||
class ePICS19jProDual(ePIC, S19jPro):
|
||||
raw_model = MinerModel.EPIC.S19jProDual
|
||||
raw_model: MinerModelType = MinerModel.EPIC.S19jProDual
|
||||
expected_fans = S19jPro.expected_fans * 2
|
||||
expected_hashboards = S19jPro.expected_hashboards * 2
|
||||
|
||||
|
||||
class ePICS19kProDual(ePIC, S19kPro):
|
||||
raw_model = MinerModel.EPIC.S19kProDual
|
||||
raw_model: MinerModelType = MinerModel.EPIC.S19kProDual
|
||||
expected_fans = S19kPro.expected_fans * 2
|
||||
expected_hashboards = S19kPro.expected_hashboards * 2
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.data import HashBoard
|
||||
from pyasic.device.algorithm import AlgoHashRate, HashUnit
|
||||
@@ -76,7 +75,7 @@ class HiveonT9(HiveonOld, T9):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]:
|
||||
hashboards = [
|
||||
HashBoard(slot=board, expected_chips=self.expected_chips)
|
||||
for board in range(self.expected_hashboards)
|
||||
@@ -84,7 +83,7 @@ class HiveonT9(HiveonOld, T9):
|
||||
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = self.rpc.stats()
|
||||
rpc_stats = await self.rpc.stats()
|
||||
except APIError:
|
||||
return []
|
||||
|
||||
@@ -98,7 +97,7 @@ class HiveonT9(HiveonOld, T9):
|
||||
hashrate = 0
|
||||
chips = 0
|
||||
for chipset in board_map[board]:
|
||||
if hashboards[board].chip_temp is None:
|
||||
if hashboards[board].chip_temp is None and rpc_stats is not None:
|
||||
try:
|
||||
hashboards[board].temp = rpc_stats["STATS"][1][f"temp{chipset}"]
|
||||
hashboards[board].chip_temp = rpc_stats["STATS"][1][
|
||||
@@ -108,6 +107,7 @@ class HiveonT9(HiveonOld, T9):
|
||||
pass
|
||||
else:
|
||||
hashboards[board].missing = False
|
||||
if rpc_stats is not None:
|
||||
try:
|
||||
hashrate += rpc_stats["STATS"][1][f"chain_rate{chipset}"]
|
||||
chips += rpc_stats["STATS"][1][f"chain_acn{chipset}"]
|
||||
@@ -120,8 +120,8 @@ class HiveonT9(HiveonOld, T9):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_env_temp(self, rpc_stats: dict = None) -> Optional[float]:
|
||||
env_temp_list = []
|
||||
async def _get_env_temp(self, rpc_stats: dict | None = None) -> float | None:
|
||||
env_temp_list: list[int] = []
|
||||
board_map = {
|
||||
0: [2, 9, 10],
|
||||
1: [3, 11, 12],
|
||||
@@ -144,3 +144,4 @@ class HiveonT9(HiveonOld, T9):
|
||||
|
||||
if not env_temp_list == []:
|
||||
return round(sum(env_temp_list) / len(env_temp_list))
|
||||
return None
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.miners.backends import LUXMiner
|
||||
from pyasic.miners.device.models import S21, T21
|
||||
from pyasic.miners.device.models import T21
|
||||
|
||||
|
||||
class LUXMinerT21(LUXMiner, T21):
|
||||
|
||||
@@ -87,7 +87,3 @@ class VNishS19ProHydro(VNish, S19ProHydro):
|
||||
|
||||
class VNishS19kPro(VNish, S19kPro):
|
||||
pass
|
||||
|
||||
|
||||
class VNishS19ProA(VNish, S19ProA):
|
||||
pass
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import List, Optional
|
||||
from typing import Any
|
||||
|
||||
from pyasic import APIError
|
||||
from pyasic.data.boards import HashBoard
|
||||
from pyasic.device.algorithm.hashrate import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.miners.backends import AvalonMiner
|
||||
from pyasic.miners.data import (
|
||||
DataFunction,
|
||||
@@ -150,12 +150,12 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3):
|
||||
|
||||
data_locations = AVALON_NANO_DATA_LOC
|
||||
|
||||
async def _get_mac(self, web_minerinfo: dict) -> Optional[dict]:
|
||||
async def _get_mac(self, web_minerinfo: dict[Any, Any] | None = None) -> str | None:
|
||||
if web_minerinfo is None:
|
||||
try:
|
||||
web_minerinfo = await self.web.minerinfo()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_minerinfo is not None:
|
||||
try:
|
||||
@@ -164,17 +164,18 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3):
|
||||
return mac.upper()
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
|
||||
data_locations = AVALON_NANO3S_DATA_LOC
|
||||
|
||||
async def _get_wattage(self, rpc_estats: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, rpc_estats: dict | None = None) -> int | None:
|
||||
if rpc_estats is None:
|
||||
try:
|
||||
rpc_estats = await self.rpc.estats()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_estats is not None:
|
||||
try:
|
||||
@@ -182,13 +183,16 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
|
||||
return int(parsed_estats["PS"][6])
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self, rpc_estats: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_estats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_estats is None:
|
||||
try:
|
||||
rpc_estats = await self.rpc.estats()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_estats is not None:
|
||||
try:
|
||||
@@ -198,15 +202,16 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
|
||||
).into(self.algo.unit.default)
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self, rpc_estats: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_estats: dict | None = None) -> list[HashBoard]:
|
||||
hashboards = await AvalonMiner._get_hashboards(self, rpc_estats)
|
||||
|
||||
if rpc_estats is None:
|
||||
try:
|
||||
rpc_estats = await self.rpc.estats()
|
||||
except APIError:
|
||||
pass
|
||||
return hashboards
|
||||
|
||||
if rpc_estats is not None:
|
||||
try:
|
||||
|
||||
@@ -16,13 +16,12 @@
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig, MiningModeConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import MinerErrorData, X19Error
|
||||
from pyasic.data.error_codes import X19Error
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.backends.bmminer import BMMiner
|
||||
from pyasic.miners.backends.cgminer import CGMiner
|
||||
@@ -39,6 +38,10 @@ from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
|
||||
|
||||
ANTMINER_MODERN_DATA_LOC = DataLocations(
|
||||
**{
|
||||
str(DataOptions.SERIAL_NUMBER): DataFunction(
|
||||
"_get_serial_number",
|
||||
[WebAPICommand("web_get_system_info", "get_system_info")],
|
||||
),
|
||||
str(DataOptions.MAC): DataFunction(
|
||||
"_get_mac",
|
||||
[WebAPICommand("web_get_system_info", "get_system_info")],
|
||||
@@ -116,9 +119,11 @@ class AntminerModern(BMMiner):
|
||||
data = await self.web.get_miner_conf()
|
||||
if data:
|
||||
self.config = MinerConfig.from_am_modern(data)
|
||||
return self.config
|
||||
return self.config or MinerConfig()
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
await self.web.set_miner_conf(config.as_am_modern(user_suffix=user_suffix))
|
||||
# if data:
|
||||
@@ -131,54 +136,77 @@ class AntminerModern(BMMiner):
|
||||
# break
|
||||
# await asyncio.sleep(1)
|
||||
|
||||
async def upgrade_firmware(self, file: Path, keep_settings: bool = True) -> str:
|
||||
async def upgrade_firmware(
|
||||
self,
|
||||
*,
|
||||
file: str | None = None,
|
||||
url: str | None = None,
|
||||
version: str | None = None,
|
||||
keep_settings: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
Upgrade the firmware of the AntMiner device.
|
||||
|
||||
Args:
|
||||
file (Path): Path to the firmware file.
|
||||
keep_settings (bool): Whether to keep the current settings after the update.
|
||||
file: Path to the firmware file as a string.
|
||||
url: URL to download firmware from (not implemented).
|
||||
version: Version to upgrade to (not implemented).
|
||||
keep_settings: Whether to keep the current settings after the update.
|
||||
|
||||
Returns:
|
||||
str: Result of the upgrade process.
|
||||
bool: True if upgrade was successful, False otherwise.
|
||||
"""
|
||||
if not file:
|
||||
raise ValueError("File location must be provided for firmware upgrade.")
|
||||
logging.error("File location must be provided for firmware upgrade.")
|
||||
return False
|
||||
|
||||
if url or version:
|
||||
logging.warning(
|
||||
"URL and version parameters are not implemented for Antminer."
|
||||
)
|
||||
|
||||
try:
|
||||
file_path = Path(file)
|
||||
|
||||
if not hasattr(self.web, "update_firmware"):
|
||||
logging.error(
|
||||
"Firmware upgrade not supported via web API for this Antminer model."
|
||||
)
|
||||
return False
|
||||
|
||||
result = await self.web.update_firmware(
|
||||
file=file, keep_settings=keep_settings
|
||||
file=file_path, keep_settings=keep_settings
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
logging.info(
|
||||
"Firmware upgrade process completed successfully for AntMiner."
|
||||
)
|
||||
return "Firmware upgrade completed successfully."
|
||||
return True
|
||||
else:
|
||||
error_message = result.get("message", "Unknown error")
|
||||
logging.error(f"Firmware upgrade failed. Response: {error_message}")
|
||||
return f"Firmware upgrade failed. Response: {error_message}"
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"An error occurred during the firmware upgrade process: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
return False
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
data = await self.web.blink(blink=True)
|
||||
if data:
|
||||
if data.get("code") == "B000":
|
||||
self.light = True
|
||||
return self.light
|
||||
return self.light or False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
data = await self.web.blink(blink=False)
|
||||
if data:
|
||||
if data.get("code") == "B100":
|
||||
self.light = False
|
||||
return self.light
|
||||
return self.light or False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
data = await self.web.reboot()
|
||||
@@ -198,7 +226,9 @@ class AntminerModern(BMMiner):
|
||||
await self.send_config(cfg)
|
||||
return True
|
||||
|
||||
async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(
|
||||
self, web_get_system_info: dict | None = None
|
||||
) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -210,8 +240,9 @@ class AntminerModern(BMMiner):
|
||||
return web_get_system_info["hostname"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -230,8 +261,11 @@ class AntminerModern(BMMiner):
|
||||
return data["macaddr"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
|
||||
async def _get_errors( # type: ignore[override]
|
||||
self, web_summary: dict | None = None
|
||||
) -> list[X19Error]:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -251,7 +285,7 @@ class AntminerModern(BMMiner):
|
||||
pass
|
||||
return errors
|
||||
|
||||
async def _get_hashboards(self) -> List[HashBoard]:
|
||||
async def _get_hashboards(self) -> list[HashBoard]: # type: ignore[override]
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -269,8 +303,11 @@ class AntminerModern(BMMiner):
|
||||
try:
|
||||
for board in rpc_stats["STATS"][0]["chain"]:
|
||||
hashboards[board["index"]].hashrate = self.algo.hashrate(
|
||||
rate=board["rate_real"], unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=board["rate_real"],
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hashboards[board["index"]].chips = board["asic_num"]
|
||||
|
||||
if "S21+ Hyd" in self.model:
|
||||
@@ -320,8 +357,8 @@ class AntminerModern(BMMiner):
|
||||
return hashboards
|
||||
|
||||
async def _get_fault_light(
|
||||
self, web_get_blink_status: dict = None
|
||||
) -> Optional[bool]:
|
||||
self, web_get_blink_status: dict | None = None
|
||||
) -> bool | None:
|
||||
if self.light:
|
||||
return self.light
|
||||
|
||||
@@ -339,8 +376,8 @@ class AntminerModern(BMMiner):
|
||||
return self.light
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_stats: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_stats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -356,9 +393,26 @@ class AntminerModern(BMMiner):
|
||||
rate_unit = "GH"
|
||||
return self.algo.hashrate(
|
||||
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit)
|
||||
).into(self.algo.unit.default)
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_serial_number(
|
||||
self, web_get_system_info: dict | None = None
|
||||
) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if web_get_system_info is not None:
|
||||
try:
|
||||
return web_get_system_info["serinum"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def set_static_ip(
|
||||
self,
|
||||
@@ -366,10 +420,10 @@ class AntminerModern(BMMiner):
|
||||
dns: str,
|
||||
gateway: str,
|
||||
subnet_mask: str = "255.255.255.0",
|
||||
hostname: str = None,
|
||||
hostname: str | None = None,
|
||||
):
|
||||
if not hostname:
|
||||
hostname = await self.get_hostname()
|
||||
hostname = await self.get_hostname() or ""
|
||||
await self.web.set_network_conf(
|
||||
ip=ip,
|
||||
dns=dns,
|
||||
@@ -379,9 +433,9 @@ class AntminerModern(BMMiner):
|
||||
protocol=2,
|
||||
)
|
||||
|
||||
async def set_dhcp(self, hostname: str = None):
|
||||
async def set_dhcp(self, hostname: str | None = None):
|
||||
if not hostname:
|
||||
hostname = await self.get_hostname()
|
||||
hostname = await self.get_hostname() or ""
|
||||
await self.web.set_network_conf(
|
||||
ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1
|
||||
)
|
||||
@@ -402,7 +456,7 @@ class AntminerModern(BMMiner):
|
||||
protocol=protocol,
|
||||
)
|
||||
|
||||
async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None:
|
||||
if web_get_conf is None:
|
||||
try:
|
||||
web_get_conf = await self.web.get_miner_conf()
|
||||
@@ -418,8 +472,9 @@ class AntminerModern(BMMiner):
|
||||
return False
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -431,8 +486,9 @@ class AntminerModern(BMMiner):
|
||||
return int(rpc_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
@@ -521,19 +577,22 @@ class AntminerOld(CGMiner):
|
||||
data = await self.web.get_miner_conf()
|
||||
if data:
|
||||
self.config = MinerConfig.from_am_old(data)
|
||||
return self.config
|
||||
return self.config or MinerConfig()
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
await self.web.set_miner_conf(config.as_am_old(user_suffix=user_suffix))
|
||||
|
||||
async def _get_mac(self) -> Optional[str]:
|
||||
async def _get_mac(self) -> str | None:
|
||||
try:
|
||||
data = await self.web.get_system_info()
|
||||
if data:
|
||||
return data["macaddr"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
# this should time out, after it does do a check
|
||||
@@ -545,7 +604,7 @@ class AntminerOld(CGMiner):
|
||||
self.light = True
|
||||
except KeyError:
|
||||
pass
|
||||
return self.light
|
||||
return self.light or False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
await self.web.blink(blink=False)
|
||||
@@ -556,7 +615,7 @@ class AntminerOld(CGMiner):
|
||||
self.light = False
|
||||
except KeyError:
|
||||
pass
|
||||
return self.light
|
||||
return self.light or False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
data = await self.web.reboot()
|
||||
@@ -565,8 +624,8 @@ class AntminerOld(CGMiner):
|
||||
return False
|
||||
|
||||
async def _get_fault_light(
|
||||
self, web_get_blink_status: dict = None
|
||||
) -> Optional[bool]:
|
||||
self, web_get_blink_status: dict | None = None
|
||||
) -> bool | None:
|
||||
if self.light:
|
||||
return self.light
|
||||
|
||||
@@ -583,7 +642,9 @@ class AntminerOld(CGMiner):
|
||||
pass
|
||||
return self.light
|
||||
|
||||
async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(
|
||||
self, web_get_system_info: dict | None = None
|
||||
) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -595,8 +656,9 @@ class AntminerOld(CGMiner):
|
||||
return web_get_system_info["hostname"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -627,10 +689,10 @@ class AntminerOld(CGMiner):
|
||||
pass
|
||||
return fans_data
|
||||
|
||||
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
hashboards = []
|
||||
hashboards: list[HashBoard] = []
|
||||
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
@@ -670,8 +732,11 @@ class AntminerOld(CGMiner):
|
||||
hashrate = boards[1].get(f"chain_rate{i}")
|
||||
if hashrate:
|
||||
hashboard.hashrate = self.algo.hashrate(
|
||||
rate=float(hashrate), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(hashrate),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
chips = boards[1].get(f"chain_acn{i}")
|
||||
if chips:
|
||||
@@ -688,7 +753,7 @@ class AntminerOld(CGMiner):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None:
|
||||
if web_get_conf is None:
|
||||
try:
|
||||
web_get_conf = await self.web.get_miner_conf()
|
||||
@@ -713,7 +778,9 @@ class AntminerOld(CGMiner):
|
||||
else:
|
||||
return False
|
||||
|
||||
async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -725,3 +792,4 @@ class AntminerOld(CGMiner):
|
||||
return int(rpc_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -15,11 +15,10 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import (
|
||||
DataFunction,
|
||||
@@ -187,7 +186,9 @@ class Auradine(StockFirmware):
|
||||
pass
|
||||
return MinerConfig()
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
|
||||
conf = config.as_auradine(user_suffix=user_suffix)
|
||||
@@ -197,8 +198,8 @@ class Auradine(StockFirmware):
|
||||
async def upgrade_firmware(
|
||||
self,
|
||||
*,
|
||||
url: str = None,
|
||||
version: str = "latest",
|
||||
url: str | None = None,
|
||||
version: str | None = "latest",
|
||||
keep_settings: bool = False,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
@@ -223,8 +224,12 @@ class Auradine(StockFirmware):
|
||||
|
||||
if url:
|
||||
result = await self.web.firmware_upgrade(url=url)
|
||||
else:
|
||||
elif version:
|
||||
result = await self.web.firmware_upgrade(version=version)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Either URL or version must be provided for firmware upgrade."
|
||||
)
|
||||
|
||||
if result.get("STATUS", [{}])[0].get("STATUS") == "S":
|
||||
logging.info("Firmware upgrade process completed successfully.")
|
||||
@@ -245,7 +250,7 @@ class Auradine(StockFirmware):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_mac(self, web_ipreport: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_ipreport: dict | None = None) -> str | None:
|
||||
if web_ipreport is None:
|
||||
try:
|
||||
web_ipreport = await self.web.ipreport()
|
||||
@@ -257,8 +262,9 @@ class Auradine(StockFirmware):
|
||||
return web_ipreport["IPReport"][0]["mac"].upper()
|
||||
except (LookupError, AttributeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self, web_ipreport: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, web_ipreport: dict | None = None) -> str | None:
|
||||
if web_ipreport is None:
|
||||
try:
|
||||
web_ipreport = await self.web.ipreport()
|
||||
@@ -270,8 +276,9 @@ class Auradine(StockFirmware):
|
||||
return web_ipreport["IPReport"][0]["version"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hostname(self, web_ipreport: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(self, web_ipreport: dict | None = None) -> str | None:
|
||||
if web_ipreport is None:
|
||||
try:
|
||||
web_ipreport = await self.web.ipreport()
|
||||
@@ -283,8 +290,11 @@ class Auradine(StockFirmware):
|
||||
return web_ipreport["IPReport"][0]["hostname"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -295,14 +305,15 @@ class Auradine(StockFirmware):
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(
|
||||
self, rpc_devs: dict = None, web_ipreport: dict = None
|
||||
) -> List[HashBoard]:
|
||||
self, rpc_devs: dict | None = None, web_ipreport: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -327,8 +338,11 @@ class Auradine(StockFirmware):
|
||||
for board in rpc_devs["DEVS"]:
|
||||
b_id = board["ID"] - 1
|
||||
hashboards[b_id].hashrate = self.algo.hashrate(
|
||||
rate=float(board["MHS 5s"]), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(board["MHS 5s"]),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hashboards[b_id].temp = round(float(board["Temperature"]))
|
||||
hashboards[b_id].missing = False
|
||||
except LookupError:
|
||||
@@ -344,7 +358,7 @@ class Auradine(StockFirmware):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_wattage(self, web_psu: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, web_psu: dict | None = None) -> int | None:
|
||||
if web_psu is None:
|
||||
try:
|
||||
web_psu = await self.web.get_psu()
|
||||
@@ -356,10 +370,11 @@ class Auradine(StockFirmware):
|
||||
return int(float(web_psu["PSU"][0]["PowerIn"].replace("W", "")))
|
||||
except (LookupError, TypeError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(
|
||||
self, web_mode: dict = None, web_psu: dict = None
|
||||
) -> Optional[int]:
|
||||
self, web_mode: dict | None = None, web_psu: dict | None = None
|
||||
) -> int | None:
|
||||
if web_mode is None:
|
||||
try:
|
||||
web_mode = await self.web.get_mode()
|
||||
@@ -383,8 +398,9 @@ class Auradine(StockFirmware):
|
||||
return int(float(web_psu["PSU"][0]["PoutMax"].replace("W", "")))
|
||||
except (LookupError, TypeError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, web_fan: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, web_fan: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -403,7 +419,7 @@ class Auradine(StockFirmware):
|
||||
pass
|
||||
return fans
|
||||
|
||||
async def _get_fault_light(self, web_led: dict = None) -> Optional[bool]:
|
||||
async def _get_fault_light(self, web_led: dict | None = None) -> bool | None:
|
||||
if web_led is None:
|
||||
try:
|
||||
web_led = await self.web.get_led()
|
||||
@@ -415,8 +431,9 @@ class Auradine(StockFirmware):
|
||||
return web_led["LED"][0]["Code"] == int(AuradineLEDCodes.LOCATE_MINER)
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _is_mining(self, web_mode: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_mode: dict | None = None) -> bool | None:
|
||||
if web_mode is None:
|
||||
try:
|
||||
web_mode = await self.web.get_mode()
|
||||
@@ -428,8 +445,9 @@ class Auradine(StockFirmware):
|
||||
return web_mode["Mode"][0]["Sleep"] == "off"
|
||||
except (LookupError, TypeError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_summary: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -441,3 +459,4 @@ class Auradine(StockFirmware):
|
||||
return rpc_summary["SUMMARY"][0]["Elapsed"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
import copy
|
||||
import re
|
||||
import time
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.backends.cgminer import CGMiner
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
|
||||
@@ -119,7 +118,7 @@ class AvalonMiner(CGMiner):
|
||||
limit = 1
|
||||
else:
|
||||
limit = 0
|
||||
data = await self.rpc.ascset(0, "worklevel,set", 1)
|
||||
data = await self.rpc.ascset(0, "worklevel,set", limit)
|
||||
except APIError:
|
||||
return False
|
||||
if data["STATUS"][0]["Msg"] == "ASC 0 set OK":
|
||||
@@ -143,7 +142,7 @@ class AvalonMiner(CGMiner):
|
||||
try:
|
||||
# Shut off 5 seconds from now
|
||||
timestamp = int(time.time()) + 5
|
||||
data = await self.rpc.ascset(0, f"softoff", f"1:{timestamp}")
|
||||
data = await self.rpc.ascset(0, "softoff", f"1:{timestamp}")
|
||||
except APIError:
|
||||
return False
|
||||
if "success" in data["STATUS"][0]["Msg"]:
|
||||
@@ -154,7 +153,7 @@ class AvalonMiner(CGMiner):
|
||||
try:
|
||||
# Shut off 5 seconds from now
|
||||
timestamp = int(time.time()) + 5
|
||||
data = await self.rpc.ascset(0, f"softon", f"1:{timestamp}")
|
||||
data = await self.rpc.ascset(0, "softon", f"1:{timestamp}")
|
||||
except APIError:
|
||||
return False
|
||||
if "success" in data["STATUS"][0]["Msg"]:
|
||||
@@ -232,7 +231,7 @@ class AvalonMiner(CGMiner):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_mac(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -249,23 +248,28 @@ class AvalonMiner(CGMiner):
|
||||
return mac
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self, rpc_devs: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_devs: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_devs is None:
|
||||
try:
|
||||
rpc_devs = await self.rpc.devs()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_devs is not None:
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_devs["DEVS"][0]["MHS 1m"]), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(rpc_devs["DEVS"][0]["MHS 1m"]),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (KeyError, IndexError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self, rpc_estats: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_estats: dict | None = None) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -291,12 +295,18 @@ class AvalonMiner(CGMiner):
|
||||
board_hr = parsed_estats["STATS"][0]["MM ID0"]["MGHS"]
|
||||
if isinstance(board_hr, list):
|
||||
hashboards[board].hashrate = self.algo.hashrate(
|
||||
rate=float(board_hr[board]), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(board_hr[board]),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
else:
|
||||
hashboards[board].hashrate = self.algo.hashrate(
|
||||
rate=float(board_hr), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(board_hr),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
except LookupError:
|
||||
pass
|
||||
@@ -376,24 +386,26 @@ class AvalonMiner(CGMiner):
|
||||
return hashboards
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_estats: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_estats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_estats is None:
|
||||
try:
|
||||
rpc_estats = await self.rpc.estats()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_estats is not None:
|
||||
try:
|
||||
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
|
||||
return self.algo.hashrate(
|
||||
rate=float(parsed_estats["GHSmm"]), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(parsed_estats["GHSmm"]),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_env_temp(self, rpc_estats: dict = None) -> Optional[float]:
|
||||
async def _get_env_temp(self, rpc_estats: dict | None = None) -> float | None:
|
||||
if rpc_estats is None:
|
||||
try:
|
||||
rpc_estats = await self.rpc.estats()
|
||||
@@ -406,13 +418,14 @@ class AvalonMiner(CGMiner):
|
||||
return float(parsed_estats["Temp"])
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(self, rpc_estats: dict = None) -> Optional[int]:
|
||||
async def _get_wattage_limit(self, rpc_estats: dict | None = None) -> int | None:
|
||||
if rpc_estats is None:
|
||||
try:
|
||||
rpc_estats = await self.rpc.estats()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_estats is not None:
|
||||
try:
|
||||
@@ -420,8 +433,9 @@ class AvalonMiner(CGMiner):
|
||||
return int(parsed_estats["MPO"])
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage(self, rpc_estats: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, rpc_estats: dict | None = None) -> int | None:
|
||||
if rpc_estats is None:
|
||||
try:
|
||||
rpc_estats = await self.rpc.estats()
|
||||
@@ -434,8 +448,9 @@ class AvalonMiner(CGMiner):
|
||||
return int(parsed_estats["WALLPOWER"])
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, rpc_estats: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, rpc_estats: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -459,7 +474,7 @@ class AvalonMiner(CGMiner):
|
||||
pass
|
||||
return fans_data
|
||||
|
||||
async def _get_fault_light(self, rpc_estats: dict = None) -> Optional[bool]:
|
||||
async def _get_fault_light(self, rpc_estats: dict | None = None) -> bool | None:
|
||||
if self.light:
|
||||
return self.light
|
||||
if rpc_estats is None:
|
||||
|
||||
@@ -14,12 +14,10 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
|
||||
from pyasic.miners.device.firmware import StockFirmware
|
||||
@@ -72,6 +70,7 @@ class BFGMiner(StockFirmware):
|
||||
try:
|
||||
pools = await self.rpc.pools()
|
||||
except APIError:
|
||||
if self.config is not None:
|
||||
return self.config
|
||||
|
||||
self.config = MinerConfig.from_api(pools)
|
||||
@@ -81,7 +80,7 @@ class BFGMiner(StockFirmware):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -96,7 +95,7 @@ class BFGMiner(StockFirmware):
|
||||
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -111,7 +110,9 @@ class BFGMiner(StockFirmware):
|
||||
|
||||
return self.fw_ver
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
# get hr from API
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
@@ -123,12 +124,15 @@ class BFGMiner(StockFirmware):
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["MHS 20s"]),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -172,8 +176,11 @@ class BFGMiner(StockFirmware):
|
||||
hashrate = boards[1].get(f"chain_rate{i}")
|
||||
if hashrate:
|
||||
hashboard.hashrate = self.algo.hashrate(
|
||||
rate=float(hashrate), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(hashrate),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
chips = boards[1].get(f"chain_acn{i}")
|
||||
if chips:
|
||||
@@ -187,7 +194,7 @@ class BFGMiner(StockFirmware):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -220,7 +227,7 @@ class BFGMiner(StockFirmware):
|
||||
|
||||
return fans
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
@@ -251,8 +258,8 @@ class BFGMiner(StockFirmware):
|
||||
return pools_data
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_stats: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_stats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
# X19 method, not sure compatibility
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
@@ -269,6 +276,7 @@ class BFGMiner(StockFirmware):
|
||||
rate_unit = "GH"
|
||||
return self.algo.hashrate(
|
||||
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit)
|
||||
).into(self.algo.unit.default)
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
|
||||
from pyasic.miners.device.firmware import StockFirmware
|
||||
@@ -76,6 +75,7 @@ class BMMiner(StockFirmware):
|
||||
try:
|
||||
pools = await self.rpc.pools()
|
||||
except APIError:
|
||||
if self.config is not None:
|
||||
return self.config
|
||||
|
||||
self.config = MinerConfig.from_api(pools)
|
||||
@@ -85,7 +85,7 @@ class BMMiner(StockFirmware):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -100,7 +100,7 @@ class BMMiner(StockFirmware):
|
||||
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -115,7 +115,9 @@ class BMMiner(StockFirmware):
|
||||
|
||||
return self.fw_ver
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
# get hr from API
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
@@ -127,12 +129,15 @@ class BMMiner(StockFirmware):
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -189,8 +194,11 @@ class BMMiner(StockFirmware):
|
||||
hashrate = boards[1].get(f"chain_rate{i}")
|
||||
if hashrate:
|
||||
hashboard.hashrate = self.algo.hashrate(
|
||||
rate=float(hashrate), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(hashrate),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
chips = boards[1].get(f"chain_acn{i}")
|
||||
if chips:
|
||||
@@ -204,7 +212,7 @@ class BMMiner(StockFirmware):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -237,8 +245,8 @@ class BMMiner(StockFirmware):
|
||||
return fans
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_stats: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_stats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
# X19 method, not sure compatibility
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
@@ -255,11 +263,14 @@ class BMMiner(StockFirmware):
|
||||
rate_unit = "GH"
|
||||
return self.algo.hashrate(
|
||||
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit)
|
||||
).into(self.algo.unit.default)
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -271,8 +282,9 @@ class BMMiner(StockFirmware):
|
||||
return int(rpc_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
import base64
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import aiofiles
|
||||
import tomli_w
|
||||
@@ -25,14 +23,14 @@ import tomli_w
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError:
|
||||
import tomli as tomllib
|
||||
import tomli as tomllib # type: ignore
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.config.mining import MiningModePowerTune
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate, AlgoHashRateType
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import (
|
||||
DataFunction,
|
||||
@@ -193,7 +191,9 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
|
||||
return self.config
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
parsed_cfg = config.as_bosminer(user_suffix=user_suffix)
|
||||
|
||||
@@ -202,21 +202,18 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
"format": {
|
||||
"version": "2.0",
|
||||
"generator": "pyasic",
|
||||
"model": f"{self.make.replace('Miner', 'miner')} {self.raw_model.replace('j', 'J')}",
|
||||
"model": f"{self.make.replace('Miner', 'miner') if self.make else ''} {self.raw_model.replace('j', 'J') if self.raw_model else ''}",
|
||||
"timestamp": int(time.time()),
|
||||
},
|
||||
**parsed_cfg,
|
||||
}
|
||||
)
|
||||
try:
|
||||
conn = await self.ssh._get_connection()
|
||||
except ConnectionError as e:
|
||||
raise APIError("SSH connection failed when sending config.") from e
|
||||
|
||||
async with conn:
|
||||
await conn.run("/etc/init.d/bosminer stop")
|
||||
await conn.run("echo '" + toml_conf + "' > /etc/bosminer.toml")
|
||||
await conn.run("/etc/init.d/bosminer start")
|
||||
await self.ssh.send_command("/etc/init.d/bosminer stop")
|
||||
await self.ssh.send_command("echo '" + toml_conf + "' > /etc/bosminer.toml")
|
||||
await self.ssh.send_command("/etc/init.d/bosminer start")
|
||||
except Exception as e:
|
||||
raise APIError("SSH command failed when sending config.") from e
|
||||
|
||||
async def set_power_limit(self, wattage: int) -> bool:
|
||||
try:
|
||||
@@ -285,12 +282,12 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_mac(self, web_net_conf: Union[dict, list] = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_net_conf: dict | list | None = None) -> str | None:
|
||||
if web_net_conf is None:
|
||||
try:
|
||||
web_net_conf = await self.web.get_net_conf()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if isinstance(web_net_conf, dict):
|
||||
if "admin/network/iface_status/lan" in web_net_conf.keys():
|
||||
@@ -301,17 +298,18 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
return web_net_conf[0]["macaddr"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
# could use ssh, but its slow and buggy
|
||||
# result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
||||
# if result:
|
||||
# return result.upper().strip()
|
||||
|
||||
async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
# Now get the API version
|
||||
if rpc_version is not None:
|
||||
@@ -320,17 +318,20 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
except LookupError:
|
||||
rpc_ver = None
|
||||
self.api_ver = rpc_ver
|
||||
self.rpc.rpc_ver = self.api_ver
|
||||
self.rpc.rpc_ver = self.api_ver # type: ignore
|
||||
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, web_bos_info: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, web_bos_info: dict | None = None) -> str | None:
|
||||
if web_bos_info is None:
|
||||
try:
|
||||
web_bos_info = await self.web.get_bos_info()
|
||||
except APIError:
|
||||
return None
|
||||
|
||||
if web_bos_info is None:
|
||||
return None
|
||||
|
||||
if isinstance(web_bos_info, dict):
|
||||
if "bos/info" in web_bos_info.keys():
|
||||
web_bos_info = web_bos_info["bos/info"]
|
||||
@@ -344,7 +345,7 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
|
||||
return self.fw_ver
|
||||
|
||||
async def _get_hostname(self) -> Union[str, None]:
|
||||
async def _get_hostname(self) -> str | None:
|
||||
try:
|
||||
hostname = (await self.ssh.get_hostname()).strip()
|
||||
except AttributeError:
|
||||
@@ -354,28 +355,31 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
return None
|
||||
return hostname
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_summary is not None:
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (KeyError, IndexError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(
|
||||
self,
|
||||
rpc_temps: dict = None,
|
||||
rpc_devdetails: dict = None,
|
||||
rpc_devs: dict = None,
|
||||
) -> List[HashBoard]:
|
||||
rpc_temps: dict | None = None,
|
||||
rpc_devdetails: dict | None = None,
|
||||
rpc_devs: dict | None = None,
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -440,19 +444,22 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
for board in rpc_devs["DEVS"]:
|
||||
_id = board["ID"] - offset
|
||||
hashboards[_id].hashrate = self.algo.hashrate(
|
||||
rate=float(board["MHS 1m"]), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(board["MHS 1m"]),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except (IndexError, KeyError):
|
||||
pass
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_wattage(self, rpc_tunerstatus: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, rpc_tunerstatus: dict | None = None) -> int | None:
|
||||
if rpc_tunerstatus is None:
|
||||
try:
|
||||
rpc_tunerstatus = await self.rpc.tunerstatus()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_tunerstatus is not None:
|
||||
try:
|
||||
@@ -461,21 +468,25 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(self, rpc_tunerstatus: dict = None) -> Optional[int]:
|
||||
async def _get_wattage_limit(
|
||||
self, rpc_tunerstatus: dict | None = None
|
||||
) -> int | None:
|
||||
if rpc_tunerstatus is None:
|
||||
try:
|
||||
rpc_tunerstatus = await self.rpc.tunerstatus()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_tunerstatus is not None:
|
||||
try:
|
||||
return rpc_tunerstatus["TUNERSTATUS"][0]["PowerLimit"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, rpc_fans: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, rpc_fans: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -483,7 +494,7 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
try:
|
||||
rpc_fans = await self.rpc.fans()
|
||||
except APIError:
|
||||
pass
|
||||
return [Fan() for _ in range(self.expected_fans)]
|
||||
|
||||
if rpc_fans is not None:
|
||||
fans = []
|
||||
@@ -495,12 +506,14 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
return fans
|
||||
return [Fan() for _ in range(self.expected_fans)]
|
||||
|
||||
async def _get_errors(self, rpc_tunerstatus: dict = None) -> List[MinerErrorData]:
|
||||
async def _get_errors(
|
||||
self, rpc_tunerstatus: dict | None = None
|
||||
) -> list[MinerErrorData]:
|
||||
if rpc_tunerstatus is None:
|
||||
try:
|
||||
rpc_tunerstatus = await self.rpc.tunerstatus()
|
||||
except APIError:
|
||||
pass
|
||||
return []
|
||||
|
||||
if rpc_tunerstatus is not None:
|
||||
errors = []
|
||||
@@ -523,9 +536,10 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
errors.append(
|
||||
BraiinsOSError(error_message=f"Slot {_id} {_error}")
|
||||
)
|
||||
return errors
|
||||
return errors # type: ignore
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
return []
|
||||
|
||||
async def _get_fault_light(self) -> bool:
|
||||
if self.light:
|
||||
@@ -537,16 +551,16 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
self.light = True
|
||||
return self.light
|
||||
except (TypeError, AttributeError):
|
||||
return self.light
|
||||
return self.light or False
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_devs: dict = None
|
||||
) -> Optional[AlgoHashRateType]:
|
||||
self, rpc_devs: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_devs is None:
|
||||
try:
|
||||
rpc_devs = await self.rpc.devs()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_devs is not None:
|
||||
try:
|
||||
@@ -559,52 +573,57 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
|
||||
if len(hr_list) == 0:
|
||||
return self.algo.hashrate(
|
||||
rate=float(0), unit=self.algo.unit.default
|
||||
rate=float(0),
|
||||
unit=self.algo.unit.default, # type: ignore
|
||||
)
|
||||
else:
|
||||
return self.algo.hashrate(
|
||||
rate=float(
|
||||
(sum(hr_list) / len(hr_list)) * self.expected_hashboards
|
||||
(sum(hr_list) / len(hr_list))
|
||||
* (self.expected_hashboards or 1)
|
||||
),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (IndexError, KeyError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _is_mining(self, rpc_devdetails: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, rpc_devdetails: dict | None = None) -> bool | None:
|
||||
if rpc_devdetails is None:
|
||||
try:
|
||||
rpc_devdetails = await self.rpc.send_command(
|
||||
"devdetails", ignore_errors=True, allow_warning=False
|
||||
)
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_devdetails is not None:
|
||||
try:
|
||||
return not rpc_devdetails["STATUS"][0]["Msg"] == "Unavailable"
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_summary: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_summary is not None:
|
||||
try:
|
||||
return int(rpc_summary["SUMMARY"][0]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
except APIError:
|
||||
pass
|
||||
return []
|
||||
|
||||
pools_data = []
|
||||
if rpc_pools is not None:
|
||||
@@ -629,15 +648,25 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
pass
|
||||
return pools_data
|
||||
|
||||
async def upgrade_firmware(self, file: Path) -> str:
|
||||
async def upgrade_firmware(
|
||||
self,
|
||||
*,
|
||||
file: str | None = None,
|
||||
url: str | None = None,
|
||||
version: str | None = None,
|
||||
keep_settings: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
Upgrade the firmware of the BOSMiner device.
|
||||
|
||||
Args:
|
||||
file (Path): The local file path of the firmware to be uploaded.
|
||||
file: The local file path of the firmware to be uploaded.
|
||||
url: URL of firmware to download (not used in this implementation).
|
||||
version: Specific version to upgrade to (not used in this implementation).
|
||||
keep_settings: Whether to keep current settings (not used in this implementation).
|
||||
|
||||
Returns:
|
||||
Confirmation message after upgrading the firmware.
|
||||
True if upgrade was successful, False otherwise.
|
||||
"""
|
||||
try:
|
||||
logging.info("Starting firmware upgrade process.")
|
||||
@@ -659,24 +688,24 @@ class BOSMiner(BraiinsOSFirmware):
|
||||
)
|
||||
|
||||
logging.info("Firmware upgrade process completed successfully.")
|
||||
return "Firmware upgrade completed successfully."
|
||||
return True
|
||||
except FileNotFoundError as e:
|
||||
logging.error(f"File not found during the firmware upgrade process: {e}")
|
||||
raise
|
||||
return False
|
||||
except ValueError as e:
|
||||
logging.error(
|
||||
f"Validation error occurred during the firmware upgrade process: {e}"
|
||||
)
|
||||
raise
|
||||
return False
|
||||
except OSError as e:
|
||||
logging.error(f"OS error occurred during the firmware upgrade process: {e}")
|
||||
raise
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"An unexpected error occurred during the firmware upgrade process: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
return False
|
||||
|
||||
|
||||
BOSER_DATA_LOC = DataLocations(
|
||||
@@ -805,7 +834,9 @@ class BOSer(BraiinsOSFirmware):
|
||||
|
||||
return MinerConfig.from_boser(grpc_conf)
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
boser_cfg = config.as_boser(user_suffix=user_suffix)
|
||||
for key in boser_cfg:
|
||||
await self.web.send_command(key, message=boser_cfg[key])
|
||||
@@ -813,7 +844,8 @@ class BOSer(BraiinsOSFirmware):
|
||||
async def set_power_limit(self, wattage: int) -> bool:
|
||||
try:
|
||||
result = await self.web.set_power_target(
|
||||
wattage, save_action=SaveAction.SAVE_AND_FORCE_APPLY
|
||||
wattage,
|
||||
save_action=SaveAction(SaveAction.SAVE_AND_FORCE_APPLY),
|
||||
)
|
||||
except APIError:
|
||||
return False
|
||||
@@ -829,25 +861,26 @@ class BOSer(BraiinsOSFirmware):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_mac(self, grpc_miner_details: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, grpc_miner_details: dict | None = None) -> str | None:
|
||||
if grpc_miner_details is None:
|
||||
try:
|
||||
grpc_miner_details = await self.web.get_miner_details()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if grpc_miner_details is not None:
|
||||
try:
|
||||
return grpc_miner_details["macAddress"].upper()
|
||||
except (LookupError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_version is not None:
|
||||
try:
|
||||
@@ -855,16 +888,16 @@ class BOSer(BraiinsOSFirmware):
|
||||
except LookupError:
|
||||
rpc_ver = None
|
||||
self.api_ver = rpc_ver
|
||||
self.rpc.rpc_ver = self.api_ver
|
||||
self.rpc.rpc_ver = self.api_ver # type: ignore
|
||||
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, grpc_miner_details: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, grpc_miner_details: dict | None = None) -> str | None:
|
||||
if grpc_miner_details is None:
|
||||
try:
|
||||
grpc_miner_details = await self.web.get_miner_details()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
fw_ver = None
|
||||
|
||||
@@ -882,43 +915,47 @@ class BOSer(BraiinsOSFirmware):
|
||||
|
||||
return self.fw_ver
|
||||
|
||||
async def _get_hostname(self, grpc_miner_details: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(self, grpc_miner_details: dict | None = None) -> str | None:
|
||||
if grpc_miner_details is None:
|
||||
try:
|
||||
grpc_miner_details = await self.web.get_miner_details()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if grpc_miner_details is not None:
|
||||
try:
|
||||
return grpc_miner_details["hostname"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_summary is not None:
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (KeyError, IndexError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, grpc_miner_details: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, grpc_miner_details: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if grpc_miner_details is None:
|
||||
try:
|
||||
grpc_miner_details = await self.web.get_miner_details()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if grpc_miner_details is not None:
|
||||
try:
|
||||
@@ -926,12 +963,15 @@ class BOSer(BraiinsOSFirmware):
|
||||
rate=float(
|
||||
grpc_miner_details["stickerHashrate"]["gigahashPerSecond"]
|
||||
),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self, grpc_hashboards: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(
|
||||
self, grpc_hashboards: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -944,7 +984,7 @@ class BOSer(BraiinsOSFirmware):
|
||||
try:
|
||||
grpc_hashboards = await self.web.get_hashboards()
|
||||
except APIError:
|
||||
pass
|
||||
return hashboards
|
||||
|
||||
if grpc_hashboards is not None:
|
||||
grpc_boards = sorted(
|
||||
@@ -967,35 +1007,38 @@ class BOSer(BraiinsOSFirmware):
|
||||
"gigahashPerSecond"
|
||||
]
|
||||
),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hashboards[idx].missing = False
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_wattage(self, grpc_miner_stats: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, grpc_miner_stats: dict | None = None) -> int | None:
|
||||
if grpc_miner_stats is None:
|
||||
try:
|
||||
grpc_miner_stats = await self.web.get_miner_stats()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if grpc_miner_stats is not None:
|
||||
try:
|
||||
return grpc_miner_stats["powerStats"]["approximatedConsumption"]["watt"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(
|
||||
self, grpc_active_performance_mode: dict = None
|
||||
) -> Optional[int]:
|
||||
self, grpc_active_performance_mode: dict | None = None
|
||||
) -> int | None:
|
||||
if grpc_active_performance_mode is None:
|
||||
try:
|
||||
grpc_active_performance_mode = (
|
||||
await self.web.get_active_performance_mode()
|
||||
)
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if grpc_active_performance_mode is not None:
|
||||
try:
|
||||
@@ -1004,8 +1047,9 @@ class BOSer(BraiinsOSFirmware):
|
||||
]["watt"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, grpc_cooling_state: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, grpc_cooling_state: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -1013,7 +1057,7 @@ class BOSer(BraiinsOSFirmware):
|
||||
try:
|
||||
grpc_cooling_state = await self.web.get_cooling_state()
|
||||
except APIError:
|
||||
pass
|
||||
return [Fan() for _ in range(self.expected_fans)]
|
||||
|
||||
if grpc_cooling_state is not None:
|
||||
fans = []
|
||||
@@ -1025,12 +1069,14 @@ class BOSer(BraiinsOSFirmware):
|
||||
return fans
|
||||
return [Fan() for _ in range(self.expected_fans)]
|
||||
|
||||
async def _get_errors(self, rpc_tunerstatus: dict = None) -> List[MinerErrorData]:
|
||||
async def _get_errors(
|
||||
self, rpc_tunerstatus: dict | None = None
|
||||
) -> list[MinerErrorData]:
|
||||
if rpc_tunerstatus is None:
|
||||
try:
|
||||
rpc_tunerstatus = await self.rpc.tunerstatus()
|
||||
except APIError:
|
||||
pass
|
||||
return []
|
||||
|
||||
if rpc_tunerstatus is not None:
|
||||
errors = []
|
||||
@@ -1053,11 +1099,14 @@ class BOSer(BraiinsOSFirmware):
|
||||
errors.append(
|
||||
BraiinsOSError(error_message=f"Slot {_id} {_error}")
|
||||
)
|
||||
return errors
|
||||
return errors # type: ignore
|
||||
except LookupError:
|
||||
pass
|
||||
return []
|
||||
|
||||
async def _get_fault_light(self, grpc_locate_device_status: dict = None) -> bool:
|
||||
async def _get_fault_light(
|
||||
self, grpc_locate_device_status: dict | None = None
|
||||
) -> bool:
|
||||
if self.light is not None:
|
||||
return self.light
|
||||
|
||||
@@ -1065,7 +1114,7 @@ class BOSer(BraiinsOSFirmware):
|
||||
try:
|
||||
grpc_locate_device_status = await self.web.get_locate_device_status()
|
||||
except APIError:
|
||||
pass
|
||||
return False
|
||||
|
||||
if grpc_locate_device_status is not None:
|
||||
if grpc_locate_device_status == {}:
|
||||
@@ -1074,36 +1123,41 @@ class BOSer(BraiinsOSFirmware):
|
||||
return grpc_locate_device_status["enabled"]
|
||||
except LookupError:
|
||||
pass
|
||||
return False
|
||||
|
||||
async def _is_mining(self, rpc_devdetails: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, rpc_devdetails: dict | None = None) -> bool | None:
|
||||
if rpc_devdetails is None:
|
||||
try:
|
||||
rpc_devdetails = await self.rpc.send_command(
|
||||
"devdetails", ignore_errors=True, allow_warning=False
|
||||
)
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_devdetails is not None:
|
||||
try:
|
||||
return not rpc_devdetails["STATUS"][0]["Msg"] == "Unavailable"
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_summary: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_summary is not None:
|
||||
try:
|
||||
return int(rpc_summary["SUMMARY"][0]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, grpc_pool_groups: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(
|
||||
self, grpc_pool_groups: dict | None = None
|
||||
) -> list[PoolMetrics]:
|
||||
if grpc_pool_groups is None:
|
||||
try:
|
||||
grpc_pool_groups = await self.web.get_pool_groups()
|
||||
|
||||
@@ -15,16 +15,15 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
import semver
|
||||
|
||||
from pyasic.config import MinerConfig, MiningModeConfig, PoolConfig
|
||||
from pyasic.config import MinerConfig, MiningModeConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import MinerErrorData, WhatsminerError
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
|
||||
from pyasic.miners.device.firmware import StockFirmware
|
||||
@@ -237,7 +236,9 @@ class BTMinerV2(StockFirmware):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
|
||||
conf = config.as_wm(user_suffix=user_suffix)
|
||||
@@ -308,6 +309,9 @@ class BTMinerV2(StockFirmware):
|
||||
self.config = cfg
|
||||
return self.config
|
||||
|
||||
cfg.mining_mode = MiningModeConfig.normal()
|
||||
return cfg
|
||||
|
||||
async def set_power_limit(self, wattage: int) -> bool:
|
||||
try:
|
||||
await self.rpc.adjust_power_limit(wattage)
|
||||
@@ -316,13 +320,14 @@ class BTMinerV2(StockFirmware):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
##################################################
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_mac(
|
||||
self, rpc_summary: dict = None, rpc_get_miner_info: dict = None
|
||||
self, rpc_summary: dict | None = None, rpc_get_miner_info: dict | None = None
|
||||
) -> str | None:
|
||||
if rpc_get_miner_info is None:
|
||||
try:
|
||||
@@ -350,7 +355,9 @@ class BTMinerV2(StockFirmware):
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
async def _get_api_ver(self, rpc_get_version: dict = None) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_api_ver(self, rpc_get_version: dict | None = None) -> str | None:
|
||||
if rpc_get_version is None:
|
||||
try:
|
||||
rpc_get_version = await self.rpc.get_version()
|
||||
@@ -368,13 +375,12 @@ class BTMinerV2(StockFirmware):
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
else:
|
||||
self.rpc.rpc_ver = self.api_ver
|
||||
return self.api_ver
|
||||
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(
|
||||
self, rpc_get_version: dict = None, rpc_summary: dict = None
|
||||
self, rpc_get_version: dict | None = None, rpc_summary: dict | None = None
|
||||
) -> str | None:
|
||||
if rpc_get_version is None:
|
||||
try:
|
||||
@@ -408,7 +414,7 @@ class BTMinerV2(StockFirmware):
|
||||
|
||||
return self.fw_ver
|
||||
|
||||
async def _get_hostname(self, rpc_get_miner_info: dict = None) -> str | None:
|
||||
async def _get_hostname(self, rpc_get_miner_info: dict | None = None) -> str | None:
|
||||
hostname = None
|
||||
if rpc_get_miner_info is None:
|
||||
try:
|
||||
@@ -424,7 +430,9 @@ class BTMinerV2(StockFirmware):
|
||||
|
||||
return hostname
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> AlgoHashRate | None:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -435,13 +443,13 @@ class BTMinerV2(StockFirmware):
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self, rpc_devs: dict = None) -> list[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_devs: dict | None = None) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -470,8 +478,11 @@ class BTMinerV2(StockFirmware):
|
||||
hashboards[asc].chip_temp = round(board["Chip Temp Avg"])
|
||||
hashboards[asc].temp = round(board["Temperature"])
|
||||
hashboards[asc].hashrate = self.algo.hashrate(
|
||||
rate=float(board["MHS 1m"]), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(board["MHS 1m"]),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hashboards[asc].chips = board["Effective Chips"]
|
||||
hashboards[asc].serial_number = board["PCB SN"]
|
||||
hashboards[asc].missing = False
|
||||
@@ -480,7 +491,7 @@ class BTMinerV2(StockFirmware):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_env_temp(self, rpc_summary: dict = None) -> float | None:
|
||||
async def _get_env_temp(self, rpc_summary: dict | None = None) -> float | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -494,7 +505,7 @@ class BTMinerV2(StockFirmware):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage(self, rpc_summary: dict = None) -> int | None:
|
||||
async def _get_wattage(self, rpc_summary: dict | None = None) -> int | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -509,7 +520,7 @@ class BTMinerV2(StockFirmware):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(self, rpc_summary: dict = None) -> int | None:
|
||||
async def _get_wattage_limit(self, rpc_summary: dict | None = None) -> int | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -524,7 +535,7 @@ class BTMinerV2(StockFirmware):
|
||||
return None
|
||||
|
||||
async def _get_fans(
|
||||
self, rpc_summary: dict = None, rpc_get_psu: dict = None
|
||||
self, rpc_summary: dict | None = None, rpc_get_psu: dict | None = None
|
||||
) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
@@ -549,7 +560,7 @@ class BTMinerV2(StockFirmware):
|
||||
return fans
|
||||
|
||||
async def _get_fan_psu(
|
||||
self, rpc_summary: dict = None, rpc_get_psu: dict = None
|
||||
self, rpc_summary: dict | None = None, rpc_get_psu: dict | None = None
|
||||
) -> int | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
@@ -577,7 +588,7 @@ class BTMinerV2(StockFirmware):
|
||||
return None
|
||||
|
||||
async def _get_errors(
|
||||
self, rpc_summary: dict = None, rpc_get_error_code: dict = None
|
||||
self, rpc_summary: dict | None = None, rpc_get_error_code: dict | None = None
|
||||
) -> list[MinerErrorData]:
|
||||
errors = []
|
||||
if rpc_get_error_code is None and rpc_summary is None:
|
||||
@@ -611,11 +622,11 @@ class BTMinerV2(StockFirmware):
|
||||
errors.append(WhatsminerError(error_code=err))
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return errors
|
||||
return errors # type: ignore[return-value]
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_summary: dict = None
|
||||
) -> AlgoHashRate | None:
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -627,14 +638,17 @@ class BTMinerV2(StockFirmware):
|
||||
expected_hashrate = rpc_summary["SUMMARY"][0]["Factory GHS"]
|
||||
if expected_hashrate:
|
||||
return self.algo.hashrate(
|
||||
rate=float(expected_hashrate), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(expected_hashrate),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fault_light(self, rpc_get_miner_info: dict = None) -> bool | None:
|
||||
async def _get_fault_light(
|
||||
self, rpc_get_miner_info: dict | None = None
|
||||
) -> bool | None:
|
||||
if rpc_get_miner_info is None:
|
||||
try:
|
||||
rpc_get_miner_info = await self.rpc.get_miner_info()
|
||||
@@ -656,15 +670,17 @@ class BTMinerV2(StockFirmware):
|
||||
dns: str,
|
||||
gateway: str,
|
||||
subnet_mask: str = "255.255.255.0",
|
||||
hostname: str = None,
|
||||
hostname: str | None = None,
|
||||
):
|
||||
if not hostname:
|
||||
hostname = await self.get_hostname()
|
||||
if hostname is None:
|
||||
hostname = str(self.ip)
|
||||
await self.rpc.net_config(
|
||||
ip=ip, mask=subnet_mask, dns=dns, gate=gateway, host=hostname, dhcp=False
|
||||
)
|
||||
|
||||
async def set_dhcp(self, hostname: str = None):
|
||||
async def set_dhcp(self, hostname: str | None = None):
|
||||
if hostname:
|
||||
await self.set_hostname(hostname)
|
||||
await self.rpc.net_config()
|
||||
@@ -672,7 +688,7 @@ class BTMinerV2(StockFirmware):
|
||||
async def set_hostname(self, hostname: str):
|
||||
await self.rpc.set_hostname(hostname)
|
||||
|
||||
async def _is_mining(self, rpc_status: dict = None) -> bool | None:
|
||||
async def _is_mining(self, rpc_status: dict | None = None) -> bool | None:
|
||||
if rpc_status is None:
|
||||
try:
|
||||
rpc_status = await self.rpc.status()
|
||||
@@ -692,7 +708,7 @@ class BTMinerV2(StockFirmware):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def _get_uptime(self, rpc_summary: dict = None) -> int | None:
|
||||
async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -704,8 +720,9 @@ class BTMinerV2(StockFirmware):
|
||||
return int(rpc_summary["SUMMARY"][0]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> list[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
@@ -735,15 +752,25 @@ class BTMinerV2(StockFirmware):
|
||||
pass
|
||||
return pools_data
|
||||
|
||||
async def upgrade_firmware(self, file: Path) -> str:
|
||||
async def upgrade_firmware(
|
||||
self,
|
||||
*,
|
||||
file: str | None = None,
|
||||
url: str | None = None,
|
||||
version: str | None = None,
|
||||
keep_settings: bool = True,
|
||||
) -> bool:
|
||||
"""
|
||||
Upgrade the firmware of the Whatsminer device.
|
||||
|
||||
Args:
|
||||
file (Path): The local file path of the firmware to be uploaded.
|
||||
file: The local file path of the firmware to be uploaded.
|
||||
url: URL to download firmware from (not supported).
|
||||
version: Specific version to upgrade to (not supported).
|
||||
keep_settings: Whether to keep settings after upgrade.
|
||||
|
||||
Returns:
|
||||
str: Confirmation message after upgrading the firmware.
|
||||
bool: True if firmware upgrade was successful.
|
||||
"""
|
||||
try:
|
||||
logging.info("Starting firmware upgrade process for Whatsminer.")
|
||||
@@ -755,12 +782,12 @@ class BTMinerV2(StockFirmware):
|
||||
async with aiofiles.open(file, "rb") as f:
|
||||
upgrade_contents = await f.read()
|
||||
|
||||
result = await self.rpc.update_firmware(upgrade_contents)
|
||||
await self.rpc.update_firmware(upgrade_contents)
|
||||
|
||||
logging.info(
|
||||
"Firmware upgrade process completed successfully for Whatsminer."
|
||||
)
|
||||
return result
|
||||
return True
|
||||
except FileNotFoundError as e:
|
||||
logging.error(f"File not found during the firmware upgrade process: {e}")
|
||||
raise
|
||||
@@ -872,13 +899,18 @@ class BTMinerV3(StockFirmware):
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
if pools is not None and settings is not None and device_info is not None:
|
||||
self.config = MinerConfig.from_btminer_v3(
|
||||
rpc_pools=pools, rpc_settings=settings, rpc_device_info=device_info
|
||||
)
|
||||
else:
|
||||
self.config = MinerConfig()
|
||||
|
||||
return self.config
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
|
||||
conf = config.as_btminer_v3(user_suffix=user_suffix)
|
||||
@@ -902,11 +934,7 @@ class BTMinerV3(StockFirmware):
|
||||
async def fault_light_on(self) -> bool:
|
||||
try:
|
||||
data = await self.rpc.set_system_led(
|
||||
leds=[
|
||||
{
|
||||
{"color": "red", "period": 60, "duration": 20, "start": 0},
|
||||
}
|
||||
],
|
||||
leds=[{"color": "red", "period": 60, "duration": 20, "start": 0}]
|
||||
)
|
||||
except APIError:
|
||||
return False
|
||||
@@ -922,7 +950,7 @@ class BTMinerV3(StockFirmware):
|
||||
data = await self.rpc.set_system_reboot()
|
||||
except APIError:
|
||||
return False
|
||||
if data.get("msg"):
|
||||
if data and data.get("msg"):
|
||||
if data["msg"] == "ok":
|
||||
return True
|
||||
return False
|
||||
@@ -932,7 +960,7 @@ class BTMinerV3(StockFirmware):
|
||||
data = await self.rpc.set_miner_service("restart")
|
||||
except APIError:
|
||||
return False
|
||||
if data.get("msg"):
|
||||
if data and data.get("msg"):
|
||||
if data["msg"] == "ok":
|
||||
return True
|
||||
return False
|
||||
@@ -942,7 +970,7 @@ class BTMinerV3(StockFirmware):
|
||||
data = await self.rpc.set_miner_service("stop")
|
||||
except APIError:
|
||||
return False
|
||||
if data.get("msg"):
|
||||
if data and data.get("msg"):
|
||||
if data["msg"] == "ok":
|
||||
return True
|
||||
return False
|
||||
@@ -952,7 +980,7 @@ class BTMinerV3(StockFirmware):
|
||||
data = await self.rpc.set_miner_service("start")
|
||||
except APIError:
|
||||
return False
|
||||
if data.get("msg"):
|
||||
if data and data.get("msg"):
|
||||
if data["msg"] == "ok":
|
||||
return True
|
||||
return False
|
||||
@@ -966,74 +994,94 @@ class BTMinerV3(StockFirmware):
|
||||
else:
|
||||
return True
|
||||
|
||||
async def _get_mac(self, rpc_get_device_info: dict = None) -> str | None:
|
||||
async def _get_mac(self, rpc_get_device_info: dict | None = None) -> str | None:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
rpc_get_device_info = await self.rpc.get_device_info()
|
||||
except APIError:
|
||||
return None
|
||||
if rpc_get_device_info is None:
|
||||
return None
|
||||
return rpc_get_device_info.get("msg", {}).get("network", {}).get("mac")
|
||||
|
||||
async def _get_api_version(self, rpc_get_device_info: dict = None) -> str | None:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
rpc_get_device_info = await self.rpc.get_device_info()
|
||||
except APIError:
|
||||
return None
|
||||
return rpc_get_device_info.get("msg", {}).get("system", {}).get("api")
|
||||
|
||||
async def _get_firmware_version(
|
||||
self, rpc_get_device_info: dict = None
|
||||
async def _get_api_version(
|
||||
self, rpc_get_device_info: dict | None = None
|
||||
) -> str | None:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
rpc_get_device_info = await self.rpc.get_device_info()
|
||||
except APIError:
|
||||
return None
|
||||
return rpc_get_device_info.get("msg", {}).get("system", {}).get("fwversion")
|
||||
if rpc_get_device_info is None:
|
||||
return None
|
||||
return rpc_get_device_info.get("msg", {}).get("system", {}).get("api")
|
||||
|
||||
async def _get_hostname(self, rpc_get_device_info: dict = None) -> str | None:
|
||||
async def _get_firmware_version(
|
||||
self, rpc_get_device_info: dict | None = None
|
||||
) -> str | None:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
rpc_get_device_info = await self.rpc.get_device_info()
|
||||
except APIError:
|
||||
return None
|
||||
if rpc_get_device_info is None:
|
||||
return None
|
||||
return rpc_get_device_info.get("msg", {}).get("system", {}).get("fwversion")
|
||||
|
||||
async def _get_hostname(
|
||||
self, rpc_get_device_info: dict | None = None
|
||||
) -> str | None:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
rpc_get_device_info = await self.rpc.get_device_info()
|
||||
except APIError:
|
||||
return None
|
||||
if rpc_get_device_info is None:
|
||||
return None
|
||||
return rpc_get_device_info.get("msg", {}).get("network", {}).get("hostname")
|
||||
|
||||
async def _get_light_flashing(
|
||||
self, rpc_get_device_info: dict = None
|
||||
self, rpc_get_device_info: dict | None = None
|
||||
) -> bool | None:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
rpc_get_device_info = await self.rpc.get_device_info()
|
||||
except APIError:
|
||||
return None
|
||||
if rpc_get_device_info is None:
|
||||
return None
|
||||
val = rpc_get_device_info.get("msg", {}).get("system", {}).get("ledstatus")
|
||||
if isinstance(val, str):
|
||||
return val != "auto"
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(
|
||||
self, rpc_get_device_info: dict = None
|
||||
) -> float | None:
|
||||
self, rpc_get_device_info: dict | None = None
|
||||
) -> int | None:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
rpc_get_device_info = await self.rpc.get_device_info()
|
||||
except APIError:
|
||||
return None
|
||||
if rpc_get_device_info is None:
|
||||
return None
|
||||
val = rpc_get_device_info.get("msg", {}).get("miner", {}).get("power-limit-set")
|
||||
try:
|
||||
return float(val)
|
||||
return int(float(val))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
async def _get_fans(self, rpc_get_miner_status_summary: dict = None) -> list[Fan]:
|
||||
async def _get_fans(
|
||||
self, rpc_get_miner_status_summary: dict | None = None
|
||||
) -> list[Fan]:
|
||||
if rpc_get_miner_status_summary is None:
|
||||
try:
|
||||
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
|
||||
except APIError:
|
||||
return []
|
||||
fans = []
|
||||
if rpc_get_miner_status_summary is None:
|
||||
return []
|
||||
summary = rpc_get_miner_status_summary.get("msg", {}).get("summary", {})
|
||||
for idx, direction in enumerate(["in", "out"]):
|
||||
rpm = summary.get(f"fan-speed-{direction}")
|
||||
@@ -1041,19 +1089,21 @@ class BTMinerV3(StockFirmware):
|
||||
fans.append(Fan(speed=rpm))
|
||||
return fans
|
||||
|
||||
async def _get_psu_fans(self, rpc_get_device_info: dict = None) -> list[Fan]:
|
||||
async def _get_psu_fans(self, rpc_get_device_info: dict | None = None) -> list[Fan]:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
rpc_get_device_info = await self.rpc.get_device_info()
|
||||
except APIError:
|
||||
return []
|
||||
if rpc_get_device_info is None:
|
||||
return []
|
||||
rpm = rpc_get_device_info.get("msg", {}).get("power", {}).get("fanspeed")
|
||||
return [Fan(speed=rpm)] if rpm is not None else []
|
||||
|
||||
async def _get_hashboards(
|
||||
self,
|
||||
rpc_get_device_info: dict = None,
|
||||
rpc_get_miner_status_edevs: dict = None,
|
||||
rpc_get_device_info: dict | None = None,
|
||||
rpc_get_miner_status_edevs: dict | None = None,
|
||||
) -> list[HashBoard]:
|
||||
if rpc_get_device_info is None:
|
||||
try:
|
||||
@@ -1067,6 +1117,8 @@ class BTMinerV3(StockFirmware):
|
||||
return []
|
||||
|
||||
boards = []
|
||||
if rpc_get_device_info is None or rpc_get_miner_status_edevs is None:
|
||||
return []
|
||||
board_count = (
|
||||
rpc_get_device_info.get("msg", {}).get("hardware", {}).get("boards", 3)
|
||||
)
|
||||
@@ -1077,8 +1129,11 @@ class BTMinerV3(StockFirmware):
|
||||
HashBoard(
|
||||
slot=idx,
|
||||
hashrate=self.algo.hashrate(
|
||||
rate=board_data.get("hash-average", 0), unit=self.algo.unit.TH
|
||||
).into(self.algo.unit.default),
|
||||
rate=board_data.get("hash-average", 0),
|
||||
unit=self.algo.unit.TH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
),
|
||||
temp=board_data.get("chip-temp-min"),
|
||||
inlet_temp=board_data.get("chip-temp-min"),
|
||||
outlet_temp=board_data.get("chip-temp-max"),
|
||||
@@ -1095,7 +1150,7 @@ class BTMinerV3(StockFirmware):
|
||||
return boards
|
||||
|
||||
async def _get_pools(
|
||||
self, rpc_get_miner_status_summary: dict = None
|
||||
self, rpc_get_miner_status_summary: dict | None = None
|
||||
) -> list[PoolMetrics]:
|
||||
if rpc_get_miner_status_summary is None:
|
||||
try:
|
||||
@@ -1103,6 +1158,8 @@ class BTMinerV3(StockFirmware):
|
||||
except APIError:
|
||||
return []
|
||||
pools = []
|
||||
if rpc_get_miner_status_summary is None:
|
||||
return []
|
||||
msg_pools = rpc_get_miner_status_summary.get("msg", {}).get("pools", [])
|
||||
for idx, pool in enumerate(msg_pools):
|
||||
pools.append(
|
||||
@@ -1117,13 +1174,15 @@ class BTMinerV3(StockFirmware):
|
||||
return pools
|
||||
|
||||
async def _get_uptime(
|
||||
self, rpc_get_miner_status_summary: dict = None
|
||||
self, rpc_get_miner_status_summary: dict | None = None
|
||||
) -> int | None:
|
||||
if rpc_get_miner_status_summary is None:
|
||||
try:
|
||||
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
|
||||
except APIError:
|
||||
return None
|
||||
if rpc_get_miner_status_summary is None:
|
||||
return None
|
||||
return (
|
||||
rpc_get_miner_status_summary.get("msg", {})
|
||||
.get("summary", {})
|
||||
@@ -1131,27 +1190,37 @@ class BTMinerV3(StockFirmware):
|
||||
)
|
||||
|
||||
async def _get_wattage(
|
||||
self, rpc_get_miner_status_summary: dict = None
|
||||
) -> float | None:
|
||||
self, rpc_get_miner_status_summary: dict | None = None
|
||||
) -> int | None:
|
||||
if rpc_get_miner_status_summary is None:
|
||||
try:
|
||||
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
|
||||
except APIError:
|
||||
return None
|
||||
return (
|
||||
if rpc_get_miner_status_summary is None:
|
||||
return None
|
||||
power_val = (
|
||||
rpc_get_miner_status_summary.get("msg", {})
|
||||
.get("summary", {})
|
||||
.get("power-realtime")
|
||||
)
|
||||
try:
|
||||
return int(float(power_val)) if power_val is not None else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
async def _get_hashrate(
|
||||
self, rpc_get_miner_status_summary: dict = None
|
||||
) -> float | None:
|
||||
self, rpc_get_miner_status_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_get_miner_status_summary is None:
|
||||
try:
|
||||
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
|
||||
except APIError:
|
||||
return None
|
||||
|
||||
if rpc_get_miner_status_summary is None:
|
||||
return None
|
||||
|
||||
return (
|
||||
rpc_get_miner_status_summary.get("msg", {})
|
||||
.get("summary", {})
|
||||
@@ -1159,31 +1228,37 @@ class BTMinerV3(StockFirmware):
|
||||
)
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_get_miner_status_summary: dict = None
|
||||
) -> float | None:
|
||||
self, rpc_get_miner_status_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_get_miner_status_summary is None:
|
||||
try:
|
||||
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
|
||||
except APIError:
|
||||
return None
|
||||
if rpc_get_miner_status_summary is None:
|
||||
return None
|
||||
res = (
|
||||
rpc_get_miner_status_summary.get("msg", {})
|
||||
.get("summary", {})
|
||||
.get("factory-hash")
|
||||
)
|
||||
|
||||
if res == (-0.001 * self.expected_hashboards):
|
||||
if self.expected_hashboards is not None and res == (
|
||||
-0.001 * self.expected_hashboards
|
||||
):
|
||||
return None
|
||||
return res
|
||||
|
||||
async def _get_env_temp(
|
||||
self, rpc_get_miner_status_summary: dict = None
|
||||
self, rpc_get_miner_status_summary: dict | None = None
|
||||
) -> float | None:
|
||||
if rpc_get_miner_status_summary is None:
|
||||
try:
|
||||
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
|
||||
except APIError:
|
||||
return None
|
||||
if rpc_get_miner_status_summary is None:
|
||||
return None
|
||||
return (
|
||||
rpc_get_miner_status_summary.get("msg", {})
|
||||
.get("summary", {})
|
||||
|
||||
@@ -14,11 +14,10 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
|
||||
from pyasic.miners.device.firmware import StockFirmware
|
||||
@@ -75,7 +74,7 @@ class CGMiner(StockFirmware):
|
||||
try:
|
||||
pools = await self.rpc.pools()
|
||||
except APIError:
|
||||
return self.config
|
||||
return self.config or MinerConfig()
|
||||
|
||||
self.config = MinerConfig.from_api(pools)
|
||||
return self.config
|
||||
@@ -84,7 +83,7 @@ class CGMiner(StockFirmware):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -99,7 +98,7 @@ class CGMiner(StockFirmware):
|
||||
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -114,7 +113,9 @@ class CGMiner(StockFirmware):
|
||||
|
||||
return self.fw_ver
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
@@ -125,12 +126,15 @@ class CGMiner(StockFirmware):
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -142,8 +146,9 @@ class CGMiner(StockFirmware):
|
||||
return int(rpc_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
|
||||
@@ -13,13 +13,11 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic import APIError, MinerConfig
|
||||
from pyasic.data import Fan, HashBoard, X19Error
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.miners.data import (
|
||||
DataFunction,
|
||||
DataLocations,
|
||||
@@ -95,9 +93,13 @@ class ElphapexMiner(StockFirmware):
|
||||
data = await self.web.get_miner_conf()
|
||||
if data:
|
||||
self.config = MinerConfig.from_elphapex(data)
|
||||
if self.config is None:
|
||||
self.config = MinerConfig()
|
||||
return self.config
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
await self.web.set_miner_conf(config.as_elphapex(user_suffix=user_suffix))
|
||||
|
||||
@@ -106,14 +108,14 @@ class ElphapexMiner(StockFirmware):
|
||||
if data:
|
||||
if data.get("code") == "B000":
|
||||
self.light = True
|
||||
return self.light
|
||||
return self.light if self.light is not None else False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
data = await self.web.blink(blink=False)
|
||||
if data:
|
||||
if data.get("code") == "B100":
|
||||
self.light = False
|
||||
return self.light
|
||||
return self.light if self.light is not None else False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
data = await self.web.reboot()
|
||||
@@ -121,7 +123,7 @@ class ElphapexMiner(StockFirmware):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _get_api_ver(self, web_summary: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, web_summary: dict | None = None) -> str | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -136,7 +138,7 @@ class ElphapexMiner(StockFirmware):
|
||||
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, web_get_system_info: dict | None = None) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -155,7 +157,9 @@ class ElphapexMiner(StockFirmware):
|
||||
|
||||
return self.fw_ver
|
||||
|
||||
async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(
|
||||
self, web_get_system_info: dict | None = None
|
||||
) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -167,8 +171,9 @@ class ElphapexMiner(StockFirmware):
|
||||
return web_get_system_info["hostname"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -187,8 +192,11 @@ class ElphapexMiner(StockFirmware):
|
||||
return data["macaddr"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
|
||||
async def _get_errors( # type: ignore[override]
|
||||
self, web_summary: dict | None = None
|
||||
) -> list[X19Error]:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -208,7 +216,7 @@ class ElphapexMiner(StockFirmware):
|
||||
pass
|
||||
return errors
|
||||
|
||||
async def _get_hashboards(self, web_stats: dict | None = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, web_stats: dict | None = None) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -227,8 +235,11 @@ class ElphapexMiner(StockFirmware):
|
||||
try:
|
||||
for board in web_stats["STATS"][0]["chain"]:
|
||||
hashboards[board["index"]].hashrate = self.algo.hashrate(
|
||||
rate=board["rate_real"], unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=board["rate_real"],
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hashboards[board["index"]].chips = board["asic_num"]
|
||||
board_temp_data = list(
|
||||
filter(lambda x: not x == 0, board["temp_pcb"])
|
||||
@@ -250,8 +261,8 @@ class ElphapexMiner(StockFirmware):
|
||||
return hashboards
|
||||
|
||||
async def _get_fault_light(
|
||||
self, web_get_blink_status: dict = None
|
||||
) -> Optional[bool]:
|
||||
self, web_get_blink_status: dict | None = None
|
||||
) -> bool | None:
|
||||
if self.light:
|
||||
return self.light
|
||||
|
||||
@@ -269,8 +280,8 @@ class ElphapexMiner(StockFirmware):
|
||||
return self.light
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, web_stats: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, web_stats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_stats is None:
|
||||
try:
|
||||
web_stats = await self.web.stats()
|
||||
@@ -286,11 +297,12 @@ class ElphapexMiner(StockFirmware):
|
||||
rate_unit = "MH"
|
||||
return self.algo.hashrate(
|
||||
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit)
|
||||
).into(self.algo.unit.default)
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _is_mining(self, web_get_miner_conf: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_get_miner_conf: dict | None = None) -> bool | None:
|
||||
if web_get_miner_conf is None:
|
||||
try:
|
||||
web_get_miner_conf = await self.web.get_miner_conf()
|
||||
@@ -306,8 +318,9 @@ class ElphapexMiner(StockFirmware):
|
||||
return False
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, web_summary: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, web_summary: dict | None = None) -> int | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -319,8 +332,9 @@ class ElphapexMiner(StockFirmware):
|
||||
return int(web_summary["SUMMARY"][1]["elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, web_stats: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, web_stats: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -340,13 +354,16 @@ class ElphapexMiner(StockFirmware):
|
||||
|
||||
return fans
|
||||
|
||||
async def _get_pools(self, web_pools: list = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, web_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if web_pools is None:
|
||||
try:
|
||||
web_pools = await self.web.pools()
|
||||
except APIError:
|
||||
return []
|
||||
|
||||
if web_pools is None:
|
||||
return []
|
||||
|
||||
active_pool_index = None
|
||||
highest_priority = float("inf")
|
||||
|
||||
@@ -359,7 +376,6 @@ class ElphapexMiner(StockFirmware):
|
||||
active_pool_index = pool_info["index"]
|
||||
|
||||
pools_data = []
|
||||
if web_pools is not None:
|
||||
try:
|
||||
for pool_info in web_pools["POOLS"]:
|
||||
url = pool_info.get("url")
|
||||
|
||||
@@ -14,14 +14,12 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import MinerErrorData, X19Error
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate, ScryptAlgo
|
||||
from pyasic.device.algorithm import AlgoHashRateType, ScryptAlgo
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.logger import logger
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
|
||||
@@ -116,7 +114,9 @@ class ePIC(ePICFirmware):
|
||||
self.config = cfg
|
||||
return self.config
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
conf = self.config.as_epic(user_suffix=user_suffix)
|
||||
|
||||
@@ -180,7 +180,7 @@ class ePIC(ePICFirmware):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def _get_mac(self, web_network: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_network: dict | None = None) -> str | None:
|
||||
if web_network is None:
|
||||
try:
|
||||
web_network = await self.web.network()
|
||||
@@ -194,8 +194,9 @@ class ePIC(ePICFirmware):
|
||||
return mac
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hostname(self, web_summary: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(self, web_summary: dict | None = None) -> str | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -208,8 +209,9 @@ class ePIC(ePICFirmware):
|
||||
return hostname
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage(self, web_summary: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, web_summary: dict | None = None) -> int | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -223,8 +225,11 @@ class ePIC(ePICFirmware):
|
||||
return wattage
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self, web_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, web_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -238,14 +243,16 @@ class ePIC(ePICFirmware):
|
||||
for hb in web_summary["HBs"]:
|
||||
hashrate += hb["Hashrate"][0]
|
||||
return self.algo.hashrate(
|
||||
rate=float(hashrate), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.TH)
|
||||
rate=float(hashrate),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.TH) # type: ignore[attr-defined]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, web_summary: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, web_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -264,12 +271,14 @@ class ePIC(ePICFirmware):
|
||||
|
||||
hashrate += hb["Hashrate"][0] / ideal
|
||||
return self.algo.hashrate(
|
||||
rate=float(hashrate), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(hashrate),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self, web_summary: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, web_summary: dict | None = None) -> str | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -283,8 +292,9 @@ class ePIC(ePICFirmware):
|
||||
return fw_ver
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, web_summary: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, web_summary: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -305,8 +315,8 @@ class ePIC(ePICFirmware):
|
||||
return fans
|
||||
|
||||
async def _get_hashboards(
|
||||
self, web_summary: dict = None, web_capabilities: dict = None
|
||||
) -> List[HashBoard]:
|
||||
self, web_summary: dict | None = None, web_capabilities: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -362,16 +372,18 @@ class ePIC(ePICFirmware):
|
||||
# Update the Hashboard object
|
||||
hb_list[hb["Index"]].missing = False
|
||||
hb_list[hb["Index"]].hashrate = self.algo.hashrate(
|
||||
rate=float(hashrate), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(hashrate),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
hb_list[hb["Index"]].chips = num_of_chips
|
||||
hb_list[hb["Index"]].temp = int(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: dict | None = None) -> bool | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -383,8 +395,9 @@ class ePIC(ePICFirmware):
|
||||
return not op_state == "Idling"
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, web_summary: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, web_summary: dict | None = None) -> int | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -399,7 +412,7 @@ class ePIC(ePICFirmware):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fault_light(self, web_summary: dict = None) -> Optional[bool]:
|
||||
async def _get_fault_light(self, web_summary: dict | None = None) -> bool | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -414,7 +427,9 @@ class ePIC(ePICFirmware):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
|
||||
async def _get_errors(
|
||||
self, web_summary: dict | None = None
|
||||
) -> list[MinerErrorData]:
|
||||
if not web_summary:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -427,12 +442,11 @@ class ePIC(ePICFirmware):
|
||||
error = web_summary["Status"]["Last Error"]
|
||||
if error is not None:
|
||||
errors.append(X19Error(error_message=str(error)))
|
||||
return errors
|
||||
except KeyError:
|
||||
pass
|
||||
return errors
|
||||
return errors # type: ignore[return-value]
|
||||
|
||||
async def _get_pools(self, web_summary: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, web_summary: dict | None = None) -> list[PoolMetrics]:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -466,18 +480,29 @@ class ePIC(ePICFirmware):
|
||||
return pool_data
|
||||
except LookupError:
|
||||
pass
|
||||
return []
|
||||
|
||||
async def upgrade_firmware(
|
||||
self, file: Path | str, keep_settings: bool = True
|
||||
self,
|
||||
*,
|
||||
file: str | None = None,
|
||||
url: str | None = None,
|
||||
version: str | None = None,
|
||||
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.
|
||||
file: The local file path of the firmware to be uploaded.
|
||||
url: The URL to download the firmware from. Must be a valid URL if provided.
|
||||
version: The version of the firmware to upgrade to. If None, the version will be inferred from the file or URL.
|
||||
keep_settings: 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)
|
||||
if file is not None:
|
||||
await self.web.system_update(file=file, keep_settings=keep_settings)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic import APIError, MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.device.firmware import MinerFirmware
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
|
||||
@@ -72,24 +70,28 @@ class ESPMiner(BaseMiner):
|
||||
web_system_info = await self.web.system_info()
|
||||
return MinerConfig.from_espminer(web_system_info)
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
await self.web.update_settings(**config.as_espminer())
|
||||
|
||||
async def _get_wattage(self, web_system_info: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, web_system_info: dict | None = None) -> int | None:
|
||||
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
|
||||
return None
|
||||
|
||||
async def _get_hashrate(
|
||||
self, web_system_info: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, web_system_info: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_system_info is None:
|
||||
try:
|
||||
web_system_info = await self.web.system_info()
|
||||
@@ -99,14 +101,18 @@ class ESPMiner(BaseMiner):
|
||||
if web_system_info is not None:
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(web_system_info["hashRate"]), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(web_system_info["hashRate"]),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, web_system_info: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, web_system_info: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_system_info is None:
|
||||
try:
|
||||
web_system_info = await self.web.system_info()
|
||||
@@ -126,15 +132,23 @@ class ESPMiner(BaseMiner):
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if (
|
||||
small_core_count is not None
|
||||
and asic_count is not None
|
||||
and frequency is not None
|
||||
):
|
||||
expected_hashrate = small_core_count * asic_count * frequency
|
||||
|
||||
return self.algo.hashrate(
|
||||
rate=float(expected_hashrate), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(expected_hashrate),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, web_system_info: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, web_system_info: dict | None = None) -> int | None:
|
||||
if web_system_info is None:
|
||||
try:
|
||||
web_system_info = await self.web.system_info()
|
||||
@@ -146,8 +160,11 @@ class ESPMiner(BaseMiner):
|
||||
return web_system_info["uptimeSeconds"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self, web_system_info: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(
|
||||
self, web_system_info: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -163,8 +180,10 @@ class ESPMiner(BaseMiner):
|
||||
HashBoard(
|
||||
hashrate=self.algo.hashrate(
|
||||
rate=float(web_system_info["hashRate"]),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
),
|
||||
chip_temp=web_system_info.get("temp"),
|
||||
temp=web_system_info.get("vrTemp"),
|
||||
chips=web_system_info.get("asicCount", 1),
|
||||
@@ -178,7 +197,7 @@ class ESPMiner(BaseMiner):
|
||||
pass
|
||||
return []
|
||||
|
||||
async def _get_fans(self, web_system_info: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, web_system_info: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -195,7 +214,7 @@ class ESPMiner(BaseMiner):
|
||||
pass
|
||||
return []
|
||||
|
||||
async def _get_hostname(self, web_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(self, web_system_info: dict | None = None) -> str | None:
|
||||
if web_system_info is None:
|
||||
try:
|
||||
web_system_info = await self.web.system_info()
|
||||
@@ -207,8 +226,9 @@ class ESPMiner(BaseMiner):
|
||||
return web_system_info["hostname"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_api_ver(self, web_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, web_system_info: dict | None = None) -> str | None:
|
||||
if web_system_info is None:
|
||||
try:
|
||||
web_system_info = await self.web.system_info()
|
||||
@@ -220,8 +240,9 @@ class ESPMiner(BaseMiner):
|
||||
return web_system_info["version"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self, web_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, web_system_info: dict | None = None) -> str | None:
|
||||
if web_system_info is None:
|
||||
try:
|
||||
web_system_info = await self.web.system_info()
|
||||
@@ -233,8 +254,9 @@ class ESPMiner(BaseMiner):
|
||||
return web_system_info["version"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_mac(self, web_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_system_info: dict | None = None) -> str | None:
|
||||
if web_system_info is None:
|
||||
try:
|
||||
web_system_info = await self.web.system_info()
|
||||
@@ -246,3 +268,4 @@ class ESPMiner(BaseMiner):
|
||||
return web_system_info["macAddr"].upper()
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import List
|
||||
|
||||
from pyasic.config import MinerConfig, MiningModeConfig
|
||||
from pyasic.data import HashBoard
|
||||
@@ -86,12 +85,15 @@ class GoldshellMiner(BFGMiner):
|
||||
try:
|
||||
pools = await self.web.pools()
|
||||
except APIError:
|
||||
if self.config is not None:
|
||||
return self.config
|
||||
|
||||
self.config = MinerConfig.from_goldshell(pools)
|
||||
return self.config
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
pools_data = await self.web.pools()
|
||||
# have to delete all the pools one at a time first
|
||||
for pool in pools_data:
|
||||
@@ -116,35 +118,37 @@ class GoldshellMiner(BFGMiner):
|
||||
settings["select"] = idx
|
||||
await self.web.set_setting(settings)
|
||||
|
||||
async def _get_mac(self, web_setting: dict = None) -> str:
|
||||
async def _get_mac(self, web_setting: dict | None = None) -> str | None:
|
||||
if web_setting is None:
|
||||
try:
|
||||
web_setting = await self.web.setting()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_setting is not None:
|
||||
try:
|
||||
return web_setting["name"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self, web_status: dict = None) -> str:
|
||||
async def _get_fw_ver(self, web_status: dict | None = None) -> str | None:
|
||||
if web_status is None:
|
||||
try:
|
||||
web_status = await self.web.setting()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_status is not None:
|
||||
try:
|
||||
return web_status["firmware"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(
|
||||
self, rpc_devs: dict = None, rpc_devdetails: dict = None
|
||||
) -> List[HashBoard]:
|
||||
self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -166,8 +170,11 @@ class GoldshellMiner(BFGMiner):
|
||||
try:
|
||||
b_id = board["ID"]
|
||||
hashboards[b_id].hashrate = self.algo.hashrate(
|
||||
rate=float(board["MHS 20s"]), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(board["MHS 20s"]),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hashboards[b_id].temp = board["tstemp-2"]
|
||||
hashboards[b_id].missing = False
|
||||
except KeyError:
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import cast
|
||||
|
||||
from pyasic import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import MinerErrorData, X19Error
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import (
|
||||
DataFunction,
|
||||
@@ -106,9 +106,11 @@ class BlackMiner(StockFirmware):
|
||||
data = await self.web.get_miner_conf()
|
||||
if data:
|
||||
self.config = MinerConfig.from_hammer(data)
|
||||
return self.config
|
||||
return self.config or MinerConfig()
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
await self.web.set_miner_conf(config.as_hammer(user_suffix=user_suffix))
|
||||
|
||||
@@ -117,14 +119,14 @@ class BlackMiner(StockFirmware):
|
||||
if data:
|
||||
if data.get("code") == "B000":
|
||||
self.light = True
|
||||
return self.light
|
||||
return self.light or False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
data = await self.web.blink(blink=False)
|
||||
if data:
|
||||
if data.get("code") == "B100":
|
||||
self.light = False
|
||||
return self.light
|
||||
return self.light or False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
data = await self.web.reboot()
|
||||
@@ -132,7 +134,7 @@ class BlackMiner(StockFirmware):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -147,7 +149,7 @@ class BlackMiner(StockFirmware):
|
||||
|
||||
return self.api_ver
|
||||
|
||||
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -162,7 +164,9 @@ class BlackMiner(StockFirmware):
|
||||
|
||||
return self.fw_ver
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
# get hr from API
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
@@ -174,12 +178,13 @@ class BlackMiner(StockFirmware):
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -232,8 +237,11 @@ class BlackMiner(StockFirmware):
|
||||
hashrate = boards[1].get(f"chain_rate{i}")
|
||||
if hashrate:
|
||||
hashboard.hashrate = self.algo.hashrate(
|
||||
rate=float(hashrate), unit=self.algo.unit.MH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(hashrate),
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
chips = boards[1].get(f"chain_acn{i}")
|
||||
if chips:
|
||||
@@ -247,7 +255,7 @@ class BlackMiner(StockFirmware):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -279,7 +287,9 @@ class BlackMiner(StockFirmware):
|
||||
|
||||
return fans
|
||||
|
||||
async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(
|
||||
self, web_get_system_info: dict | None = None
|
||||
) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -291,8 +301,9 @@ class BlackMiner(StockFirmware):
|
||||
return web_get_system_info["hostname"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -311,8 +322,11 @@ class BlackMiner(StockFirmware):
|
||||
return data["macaddr"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
|
||||
async def _get_errors(
|
||||
self, web_summary: dict | None = None
|
||||
) -> list[MinerErrorData]:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -330,11 +344,11 @@ class BlackMiner(StockFirmware):
|
||||
continue
|
||||
except LookupError:
|
||||
pass
|
||||
return errors
|
||||
return cast(list[MinerErrorData], errors)
|
||||
|
||||
async def _get_fault_light(
|
||||
self, web_get_blink_status: dict = None
|
||||
) -> Optional[bool]:
|
||||
self, web_get_blink_status: dict | None = None
|
||||
) -> bool | None:
|
||||
if self.light:
|
||||
return self.light
|
||||
|
||||
@@ -352,8 +366,8 @@ class BlackMiner(StockFirmware):
|
||||
return self.light
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_stats: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_stats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -364,16 +378,22 @@ class BlackMiner(StockFirmware):
|
||||
try:
|
||||
expected_rate = rpc_stats["STATS"][1].get("total_rateideal")
|
||||
if expected_rate is None:
|
||||
return self.sticker_hashrate.into(self.algo.unit.default)
|
||||
if (
|
||||
hasattr(self, "sticker_hashrate")
|
||||
and self.sticker_hashrate is not None
|
||||
):
|
||||
return self.sticker_hashrate.into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
return None
|
||||
try:
|
||||
rate_unit = rpc_stats["STATS"][1]["rate_unit"]
|
||||
except KeyError:
|
||||
rate_unit = "MH"
|
||||
return self.algo.hashrate(
|
||||
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit)
|
||||
).into(self.algo.unit.default)
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def set_static_ip(
|
||||
self,
|
||||
@@ -381,10 +401,10 @@ class BlackMiner(StockFirmware):
|
||||
dns: str,
|
||||
gateway: str,
|
||||
subnet_mask: str = "255.255.255.0",
|
||||
hostname: str = None,
|
||||
hostname: str | None = None,
|
||||
):
|
||||
if not hostname:
|
||||
hostname = await self.get_hostname()
|
||||
hostname = await self.get_hostname() or ""
|
||||
await self.web.set_network_conf(
|
||||
ip=ip,
|
||||
dns=dns,
|
||||
@@ -394,9 +414,9 @@ class BlackMiner(StockFirmware):
|
||||
protocol=2,
|
||||
)
|
||||
|
||||
async def set_dhcp(self, hostname: str = None):
|
||||
async def set_dhcp(self, hostname: str | None = None):
|
||||
if not hostname:
|
||||
hostname = await self.get_hostname()
|
||||
hostname = await self.get_hostname() or ""
|
||||
await self.web.set_network_conf(
|
||||
ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1
|
||||
)
|
||||
@@ -417,7 +437,7 @@ class BlackMiner(StockFirmware):
|
||||
protocol=protocol,
|
||||
)
|
||||
|
||||
async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None:
|
||||
if web_get_conf is None:
|
||||
try:
|
||||
web_get_conf = await self.web.get_miner_conf()
|
||||
@@ -433,8 +453,9 @@ class BlackMiner(StockFirmware):
|
||||
return False
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -446,8 +467,9 @@ class BlackMiner(StockFirmware):
|
||||
return int(rpc_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import Optional
|
||||
|
||||
from pyasic import APIError
|
||||
from pyasic.config import MinerConfig, MiningModeConfig
|
||||
@@ -92,7 +91,7 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
web: HiveonWebAPI
|
||||
_web_cls = HiveonWebAPI
|
||||
|
||||
async def get_config(self) -> MinerConfig:
|
||||
async def get_config(self) -> MinerConfig | None: # type: ignore[override]
|
||||
data = await self.web.get_miner_conf()
|
||||
if data:
|
||||
self.config = MinerConfig.from_hiveon_modern(data)
|
||||
@@ -103,14 +102,16 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
if data:
|
||||
if data.get("code") == "B000":
|
||||
self.light = True
|
||||
return self.light
|
||||
return True
|
||||
return False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
data = await self.web.blink(blink=False)
|
||||
if data:
|
||||
if data.get("code") == "B100":
|
||||
self.light = False
|
||||
return self.light
|
||||
return True
|
||||
return False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
data = await self.web.reboot()
|
||||
@@ -120,17 +121,21 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
cfg = await self.get_config()
|
||||
if cfg is not None:
|
||||
cfg.mining_mode = MiningModeConfig.sleep()
|
||||
await self.send_config(cfg)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
cfg = await self.get_config()
|
||||
if cfg is not None:
|
||||
cfg.mining_mode = MiningModeConfig.normal()
|
||||
await self.send_config(cfg)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if not rpc_stats:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -139,6 +144,7 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
|
||||
if rpc_stats:
|
||||
boards = rpc_stats.get("STATS")
|
||||
if boards:
|
||||
try:
|
||||
wattage_raw = boards[1]["chain_power"]
|
||||
except (KeyError, IndexError):
|
||||
@@ -146,8 +152,11 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
else:
|
||||
# parse wattage position out of raw data
|
||||
return round(float(wattage_raw.split(" ")[0]))
|
||||
return None
|
||||
|
||||
async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(
|
||||
self, web_get_system_info: dict | None = None
|
||||
) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -159,8 +168,9 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
return web_get_system_info["hostname"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None:
|
||||
if web_get_system_info is None:
|
||||
try:
|
||||
web_get_system_info = await self.web.get_system_info()
|
||||
@@ -179,10 +189,11 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
return data["macaddr"]
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fault_light(
|
||||
self, web_get_blink_status: dict = None
|
||||
) -> Optional[bool]:
|
||||
self, web_get_blink_status: dict | None = None
|
||||
) -> bool | None:
|
||||
if self.light:
|
||||
return self.light
|
||||
|
||||
@@ -199,7 +210,7 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
pass
|
||||
return self.light
|
||||
|
||||
async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None:
|
||||
if web_get_conf is None:
|
||||
try:
|
||||
web_get_conf = await self.web.get_miner_conf()
|
||||
@@ -215,6 +226,7 @@ class HiveonModern(HiveonFirmware, BMMiner):
|
||||
return False
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
HIVEON_OLD_DATA_LOC = DataLocations(
|
||||
@@ -262,7 +274,7 @@ HIVEON_OLD_DATA_LOC = DataLocations(
|
||||
class HiveonOld(HiveonFirmware, BMMiner):
|
||||
data_locations = HIVEON_OLD_DATA_LOC
|
||||
|
||||
async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if not rpc_stats:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -271,6 +283,7 @@ class HiveonOld(HiveonFirmware, BMMiner):
|
||||
|
||||
if rpc_stats:
|
||||
boards = rpc_stats.get("STATS")
|
||||
if boards:
|
||||
try:
|
||||
wattage_raw = boards[1]["chain_power"]
|
||||
except (KeyError, IndexError):
|
||||
@@ -278,3 +291,4 @@ class HiveonOld(HiveonFirmware, BMMiner):
|
||||
else:
|
||||
# parse wattage position out of raw data
|
||||
return round(float(wattage_raw.split(" ")[0]))
|
||||
return None
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate, MinerAlgo
|
||||
from pyasic.device.algorithm import AlgoHashRateType, MinerAlgo
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
|
||||
from pyasic.miners.device.firmware import StockFirmware
|
||||
@@ -78,7 +76,7 @@ class IceRiver(StockFirmware):
|
||||
|
||||
return MinerConfig.from_iceriver(web_userpanel)
|
||||
|
||||
async def _get_fans(self, web_userpanel: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, web_userpanel: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -86,7 +84,7 @@ class IceRiver(StockFirmware):
|
||||
try:
|
||||
web_userpanel = await self.web.userpanel()
|
||||
except APIError:
|
||||
pass
|
||||
return []
|
||||
|
||||
if web_userpanel is not None:
|
||||
try:
|
||||
@@ -95,13 +93,14 @@ class IceRiver(StockFirmware):
|
||||
]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return []
|
||||
|
||||
async def _get_mac(self, web_userpanel: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_userpanel: dict | None = None) -> str | None:
|
||||
if web_userpanel is None:
|
||||
try:
|
||||
web_userpanel = await self.web.userpanel()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_userpanel is not None:
|
||||
try:
|
||||
@@ -110,26 +109,30 @@ class IceRiver(StockFirmware):
|
||||
)
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hostname(self, web_userpanel: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(self, web_userpanel: dict | None = None) -> str | None:
|
||||
if web_userpanel is None:
|
||||
try:
|
||||
web_userpanel = await self.web.userpanel()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_userpanel is not None:
|
||||
try:
|
||||
return web_userpanel["userpanel"]["data"]["host"]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self, web_userpanel: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, web_userpanel: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_userpanel is None:
|
||||
try:
|
||||
web_userpanel = await self.web.userpanel()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_userpanel is not None:
|
||||
try:
|
||||
@@ -144,8 +147,9 @@ class IceRiver(StockFirmware):
|
||||
).into(MinerAlgo.SHA256.unit.default)
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fault_light(self, web_userpanel: dict = None) -> bool:
|
||||
async def _get_fault_light(self, web_userpanel: dict | None = None) -> bool:
|
||||
if web_userpanel is None:
|
||||
try:
|
||||
web_userpanel = await self.web.userpanel()
|
||||
@@ -159,20 +163,23 @@ class IceRiver(StockFirmware):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def _is_mining(self, web_userpanel: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_userpanel: dict | None = None) -> bool | None:
|
||||
if web_userpanel is None:
|
||||
try:
|
||||
web_userpanel = await self.web.userpanel()
|
||||
except APIError:
|
||||
pass
|
||||
return False
|
||||
|
||||
if web_userpanel is not None:
|
||||
try:
|
||||
return web_userpanel["userpanel"]["data"]["powstate"]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def _get_hashboards(self, web_userpanel: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(
|
||||
self, web_userpanel: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -195,15 +202,17 @@ class IceRiver(StockFirmware):
|
||||
hb_list[idx].temp = round(board["intmp"])
|
||||
hb_list[idx].hashrate = self.algo.hashrate(
|
||||
rate=float(board["rtpow"].replace("G", "")),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hb_list[idx].chips = int(board["chipnum"])
|
||||
hb_list[idx].missing = False
|
||||
except LookupError:
|
||||
pass
|
||||
return hb_list
|
||||
|
||||
async def _get_uptime(self, web_userpanel: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, web_userpanel: dict | None = None) -> int | None:
|
||||
if web_userpanel is None:
|
||||
try:
|
||||
web_userpanel = await self.web.userpanel()
|
||||
@@ -222,8 +231,9 @@ class IceRiver(StockFirmware):
|
||||
)
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, web_userpanel: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, web_userpanel: dict | None = None) -> list[PoolMetrics]:
|
||||
if web_userpanel is None:
|
||||
try:
|
||||
web_userpanel = await self.web.userpanel()
|
||||
|
||||
@@ -13,14 +13,12 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
from pyasic.data.error_codes.innosilicon import InnosiliconError
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.backends import CGMiner
|
||||
from pyasic.miners.data import (
|
||||
@@ -113,17 +111,17 @@ class Innosilicon(CGMiner):
|
||||
# get pool data
|
||||
try:
|
||||
pools = await self.web.pools()
|
||||
except APIError:
|
||||
return self.config
|
||||
|
||||
if pools and "pools" in pools:
|
||||
self.config = MinerConfig.from_inno(pools["pools"])
|
||||
return self.config
|
||||
except APIError:
|
||||
pass
|
||||
return self.config or MinerConfig()
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
try:
|
||||
data = await self.web.reboot()
|
||||
except APIError:
|
||||
pass
|
||||
return False
|
||||
else:
|
||||
return data["success"]
|
||||
|
||||
@@ -131,14 +129,16 @@ class Innosilicon(CGMiner):
|
||||
try:
|
||||
data = await self.web.restart_cgminer()
|
||||
except APIError:
|
||||
pass
|
||||
return False
|
||||
else:
|
||||
return data["success"]
|
||||
|
||||
async def restart_backend(self) -> bool:
|
||||
return await self.restart_cgminer()
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
self.config = config
|
||||
await self.web.update_pools(config.as_inno(user_suffix=user_suffix))
|
||||
|
||||
@@ -147,8 +147,8 @@ class Innosilicon(CGMiner):
|
||||
##################################################
|
||||
|
||||
async def _get_mac(
|
||||
self, web_get_all: dict = None, web_overview: dict = None
|
||||
) -> Optional[str]:
|
||||
self, web_get_all: dict | None = None, web_overview: dict | None = None
|
||||
) -> str | None:
|
||||
if web_get_all:
|
||||
web_get_all = web_get_all["all"]
|
||||
|
||||
@@ -171,10 +171,11 @@ class Innosilicon(CGMiner):
|
||||
return mac.upper()
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict = None, web_get_all: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_summary: dict | None = None, web_get_all: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_get_all:
|
||||
web_get_all = web_get_all["all"]
|
||||
|
||||
@@ -189,13 +190,17 @@ class Innosilicon(CGMiner):
|
||||
if "Hash Rate H" in web_get_all["total_hash"].keys():
|
||||
return self.algo.hashrate(
|
||||
rate=float(web_get_all["total_hash"]["Hash Rate H"]),
|
||||
unit=self.algo.unit.H,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.H, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
elif "Hash Rate" in web_get_all["total_hash"].keys():
|
||||
return self.algo.hashrate(
|
||||
rate=float(web_get_all["total_hash"]["Hash Rate"]),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -203,14 +208,17 @@ class Innosilicon(CGMiner):
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
|
||||
unit=self.algo.unit.MH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.MH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashboards(
|
||||
self, rpc_stats: dict = None, web_get_all: dict = None
|
||||
) -> List[HashBoard]:
|
||||
self, rpc_stats: dict | None = None, web_get_all: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -260,8 +268,11 @@ class Innosilicon(CGMiner):
|
||||
hashrate = board.get("Hash Rate H")
|
||||
if hashrate:
|
||||
hashboards[idx].hashrate = self.algo.hashrate(
|
||||
rate=float(hashrate), unit=self.algo.unit.H
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(hashrate),
|
||||
unit=self.algo.unit.H, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
chip_temp = board.get("Temp max")
|
||||
if chip_temp:
|
||||
@@ -270,8 +281,8 @@ class Innosilicon(CGMiner):
|
||||
return hashboards
|
||||
|
||||
async def _get_wattage(
|
||||
self, web_get_all: dict = None, rpc_stats: dict = None
|
||||
) -> Optional[int]:
|
||||
self, web_get_all: dict | None = None, rpc_stats: dict | None = None
|
||||
) -> int | None:
|
||||
if web_get_all:
|
||||
web_get_all = web_get_all["all"]
|
||||
|
||||
@@ -305,8 +316,9 @@ class Innosilicon(CGMiner):
|
||||
else:
|
||||
wattage = int(wattage)
|
||||
return wattage
|
||||
return None
|
||||
|
||||
async def _get_fans(self, web_get_all: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, web_get_all: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -328,15 +340,15 @@ class Innosilicon(CGMiner):
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
round((int(spd) * 6000) / 100)
|
||||
spd_converted = round((int(spd) * 6000) / 100)
|
||||
for i in range(self.expected_fans):
|
||||
fans[i].speed = spd
|
||||
fans[i].speed = spd_converted
|
||||
|
||||
return fans
|
||||
|
||||
async def _get_errors(
|
||||
self, web_get_error_detail: dict = None
|
||||
) -> List[MinerErrorData]:
|
||||
async def _get_errors( # type: ignore[override]
|
||||
self, web_get_error_detail: dict | None = None
|
||||
) -> list[InnosiliconError]:
|
||||
errors = []
|
||||
if web_get_error_detail is None:
|
||||
try:
|
||||
@@ -357,7 +369,7 @@ class Innosilicon(CGMiner):
|
||||
errors.append(InnosiliconError(error_code=err))
|
||||
return errors
|
||||
|
||||
async def _get_wattage_limit(self, web_get_all: dict = None) -> Optional[int]:
|
||||
async def _get_wattage_limit(self, web_get_all: dict | None = None) -> int | None:
|
||||
if web_get_all:
|
||||
web_get_all = web_get_all["all"]
|
||||
|
||||
@@ -379,8 +391,9 @@ class Innosilicon(CGMiner):
|
||||
level = int(level)
|
||||
limit = 1250 + (250 * level)
|
||||
return limit
|
||||
return None
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.config.mining import MiningModePreset
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
|
||||
from pyasic.miners.device.firmware import LuxOSFirmware
|
||||
@@ -131,6 +130,7 @@ class LUXMiner(LuxOSFirmware):
|
||||
return True
|
||||
except (APIError, LookupError):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
try:
|
||||
@@ -169,24 +169,40 @@ class LUXMiner(LuxOSFirmware):
|
||||
|
||||
return False
|
||||
|
||||
async def atm_enabled(self) -> Optional[bool]:
|
||||
async def atm_enabled(self) -> bool | None:
|
||||
try:
|
||||
result = await self.rpc.atm()
|
||||
return result["ATM"][0]["Enabled"]
|
||||
except (APIError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def set_power_limit(self, wattage: int) -> bool:
|
||||
config = await self.get_config()
|
||||
|
||||
# Check if we have preset mode with available presets
|
||||
if not hasattr(config.mining_mode, "available_presets"):
|
||||
logging.warning(f"{self} - Mining mode does not support presets")
|
||||
return False
|
||||
|
||||
available_presets = getattr(config.mining_mode, "available_presets", [])
|
||||
if not available_presets:
|
||||
logging.warning(f"{self} - No available presets found")
|
||||
return False
|
||||
|
||||
valid_presets = {
|
||||
preset.name: preset.power
|
||||
for preset in config.mining_mode.available_presets
|
||||
if preset.power <= wattage
|
||||
for preset in available_presets
|
||||
if preset.power is not None and preset.power <= wattage
|
||||
}
|
||||
|
||||
if not valid_presets:
|
||||
logging.warning(f"{self} - No valid presets found for wattage {wattage}")
|
||||
return False
|
||||
|
||||
# Set power to highest preset <= wattage
|
||||
# If ATM enabled, must disable it before setting power limit
|
||||
new_preset = max(valid_presets, key=valid_presets.get)
|
||||
new_preset = max(valid_presets, key=lambda x: valid_presets[x])
|
||||
|
||||
re_enable_atm = False
|
||||
try:
|
||||
@@ -211,12 +227,13 @@ class LUXMiner(LuxOSFirmware):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_mac(self, rpc_config: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, rpc_config: dict | None = None) -> str | None:
|
||||
if rpc_config is None:
|
||||
try:
|
||||
rpc_config = await self.rpc.config()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_config is not None:
|
||||
try:
|
||||
@@ -224,12 +241,15 @@ class LUXMiner(LuxOSFirmware):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_summary is not None:
|
||||
try:
|
||||
@@ -240,7 +260,7 @@ class LUXMiner(LuxOSFirmware):
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -262,8 +282,10 @@ class LUXMiner(LuxOSFirmware):
|
||||
board_n = idx + 1
|
||||
hashboards[idx].hashrate = self.algo.hashrate(
|
||||
rate=float(board_stats[f"chain_rate{board_n}"]),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hashboards[idx].chips = int(board_stats[f"chain_acn{board_n}"])
|
||||
chip_temp_data = list(
|
||||
filter(
|
||||
@@ -288,22 +310,26 @@ class LUXMiner(LuxOSFirmware):
|
||||
pass
|
||||
return hashboards
|
||||
|
||||
async def _get_wattage(self, rpc_power: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, rpc_power: dict | None = None) -> int | None:
|
||||
if rpc_power is None:
|
||||
try:
|
||||
rpc_power = await self.rpc.power()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_power is not None:
|
||||
try:
|
||||
return rpc_power["POWER"][0]["Watts"]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(
|
||||
self, rpc_config: dict = None, rpc_profiles: list[dict] = None
|
||||
) -> Optional[int]:
|
||||
self, rpc_config: dict | None = None, rpc_profiles: dict | None = None
|
||||
) -> int | None:
|
||||
if rpc_config is None or rpc_profiles is None:
|
||||
return None
|
||||
try:
|
||||
active_preset = MiningModePreset.get_active_preset_from_luxos(
|
||||
rpc_config, rpc_profiles
|
||||
@@ -311,8 +337,9 @@ class LUXMiner(LuxOSFirmware):
|
||||
return active_preset.power
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, rpc_fans: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, rpc_fans: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -333,8 +360,8 @@ class LUXMiner(LuxOSFirmware):
|
||||
return fans
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_stats: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_stats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -350,11 +377,12 @@ class LUXMiner(LuxOSFirmware):
|
||||
rate_unit = "GH"
|
||||
return self.algo.hashrate(
|
||||
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit)
|
||||
).into(self.algo.unit.default)
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -366,8 +394,9 @@ class LUXMiner(LuxOSFirmware):
|
||||
return int(rpc_stats["STATS"][1]["Elapsed"])
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -379,8 +408,9 @@ class LUXMiner(LuxOSFirmware):
|
||||
return rpc_version["VERSION"][0]["Miner"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None:
|
||||
if rpc_version is None:
|
||||
try:
|
||||
rpc_version = await self.rpc.version()
|
||||
@@ -392,8 +422,9 @@ class LUXMiner(LuxOSFirmware):
|
||||
return rpc_version["VERSION"][0]["API"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fault_light(self, rpc_config: dict = None) -> Optional[bool]:
|
||||
async def _get_fault_light(self, rpc_config: dict | None = None) -> bool | None:
|
||||
if rpc_config is None:
|
||||
try:
|
||||
rpc_config = await self.rpc.config()
|
||||
@@ -405,8 +436,9 @@ class LUXMiner(LuxOSFirmware):
|
||||
return not rpc_config["CONFIG"][0]["RedLed"] == "off"
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic import MinerConfig
|
||||
from pyasic.config import MiningModeConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
|
||||
from pyasic.miners.device.firmware import MaraFirmware
|
||||
@@ -91,8 +89,11 @@ class MaraMiner(MaraFirmware):
|
||||
if data:
|
||||
self.config = MinerConfig.from_mara(data)
|
||||
return self.config
|
||||
return MinerConfig()
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
data = await self.web.get_miner_config()
|
||||
cfg_data = config.as_mara(user_suffix=user_suffix)
|
||||
merged_cfg = merge_dicts(data, cfg_data)
|
||||
@@ -124,12 +125,13 @@ class MaraMiner(MaraFirmware):
|
||||
await self.web.reload()
|
||||
return True
|
||||
|
||||
async def _get_wattage(self, web_brief: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, web_brief: dict | None = None) -> int | None:
|
||||
if web_brief is None:
|
||||
try:
|
||||
web_brief = await self.web.brief()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_brief is not None:
|
||||
try:
|
||||
@@ -137,12 +139,13 @@ class MaraMiner(MaraFirmware):
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
async def _is_mining(self, web_brief: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_brief: dict | None = None) -> bool | None:
|
||||
if web_brief is None:
|
||||
try:
|
||||
web_brief = await self.web.brief()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_brief is not None:
|
||||
try:
|
||||
@@ -150,12 +153,13 @@ class MaraMiner(MaraFirmware):
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
async def _get_uptime(self, web_brief: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, web_brief: dict | None = None) -> int | None:
|
||||
if web_brief is None:
|
||||
try:
|
||||
web_brief = await self.web.brief()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_brief is not None:
|
||||
try:
|
||||
@@ -163,7 +167,9 @@ class MaraMiner(MaraFirmware):
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
async def _get_hashboards(self, web_hashboards: dict = None) -> List[HashBoard]:
|
||||
async def _get_hashboards(
|
||||
self, web_hashboards: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if self.expected_hashboards is None:
|
||||
return []
|
||||
|
||||
@@ -183,8 +189,11 @@ class MaraMiner(MaraFirmware):
|
||||
for hb in web_hashboards["hashboards"]:
|
||||
idx = hb["index"]
|
||||
hashboards[idx].hashrate = self.algo.hashrate(
|
||||
rate=float(hb["hashrate_average"]), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(hb["hashrate_average"]),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
hashboards[idx].temp = round(
|
||||
sum(hb["temperature_pcb"]) / len(hb["temperature_pcb"])
|
||||
)
|
||||
@@ -198,7 +207,7 @@ class MaraMiner(MaraFirmware):
|
||||
pass
|
||||
return hashboards
|
||||
|
||||
async def _get_mac(self, web_overview: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_overview: dict | None = None) -> str | None:
|
||||
if web_overview is None:
|
||||
try:
|
||||
web_overview = await self.web.overview()
|
||||
@@ -210,8 +219,9 @@ class MaraMiner(MaraFirmware):
|
||||
return web_overview["mac"].upper()
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self, web_overview: dict = None) -> Optional[str]:
|
||||
async def _get_fw_ver(self, web_overview: dict | None = None) -> str | None:
|
||||
if web_overview is None:
|
||||
try:
|
||||
web_overview = await self.web.overview()
|
||||
@@ -223,8 +233,9 @@ class MaraMiner(MaraFirmware):
|
||||
return web_overview["version_firmware"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hostname(self, web_network_config: dict = None) -> Optional[str]:
|
||||
async def _get_hostname(self, web_network_config: dict | None = None) -> str | None:
|
||||
if web_network_config is None:
|
||||
try:
|
||||
web_network_config = await self.web.get_network_config()
|
||||
@@ -236,8 +247,11 @@ class MaraMiner(MaraFirmware):
|
||||
return web_network_config["hostname"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self, web_brief: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, web_brief: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_brief is None:
|
||||
try:
|
||||
web_brief = await self.web.brief()
|
||||
@@ -247,12 +261,14 @@ class MaraMiner(MaraFirmware):
|
||||
if web_brief is not None:
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(web_brief["hashrate_realtime"]), unit=self.algo.unit.TH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(web_brief["hashrate_realtime"]),
|
||||
unit=self.algo.unit.TH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_fans(self, web_fans: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, web_fans: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -272,7 +288,7 @@ class MaraMiner(MaraFirmware):
|
||||
return fans
|
||||
return [Fan() for _ in range(self.expected_fans)]
|
||||
|
||||
async def _get_fault_light(self, web_locate_miner: dict = None) -> bool:
|
||||
async def _get_fault_light(self, web_locate_miner: dict | None = None) -> bool:
|
||||
if web_locate_miner is None:
|
||||
try:
|
||||
web_locate_miner = await self.web.get_locate_miner()
|
||||
@@ -287,8 +303,8 @@ class MaraMiner(MaraFirmware):
|
||||
return False
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, web_brief: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, web_brief: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if web_brief is None:
|
||||
try:
|
||||
web_brief = await self.web.brief()
|
||||
@@ -298,14 +314,16 @@ class MaraMiner(MaraFirmware):
|
||||
if web_brief is not None:
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(web_brief["hashrate_ideal"]), unit=self.algo.unit.GH
|
||||
).into(self.algo.unit.default)
|
||||
rate=float(web_brief["hashrate_ideal"]),
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(
|
||||
self, web_miner_config: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, web_miner_config: dict | None = None
|
||||
) -> int | None:
|
||||
if web_miner_config is None:
|
||||
try:
|
||||
web_miner_config = await self.web.get_miner_config()
|
||||
@@ -317,8 +335,9 @@ class MaraMiner(MaraFirmware):
|
||||
return web_miner_config["mode"]["concorde"]["power-target"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_pools(self, web_pools: list = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, web_pools: list | None = None) -> list[PoolMetrics]:
|
||||
if web_pools is None:
|
||||
try:
|
||||
web_pools = await self.web.pools()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from pyasic import APIError
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.miners.backends import BMMiner
|
||||
from pyasic.miners.data import (
|
||||
DataFunction,
|
||||
@@ -66,7 +64,9 @@ class MSKMiner(BMMiner):
|
||||
web: MSKMinerWebAPI
|
||||
_web_cls = MSKMinerWebAPI
|
||||
|
||||
async def _get_hashrate(self, rpc_stats: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_stats: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
# get hr from API
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
@@ -78,12 +78,15 @@ class MSKMiner(BMMiner):
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_stats["STATS"][0]["total_rate"]),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(
|
||||
self.algo.unit.default # type: ignore[attr-defined]
|
||||
)
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, rpc_stats: dict | None = None) -> int | None:
|
||||
if rpc_stats is None:
|
||||
try:
|
||||
rpc_stats = await self.rpc.stats()
|
||||
@@ -95,8 +98,9 @@ class MSKMiner(BMMiner):
|
||||
return rpc_stats["STATS"][0]["total_power"]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_mac(self, web_info_v1: dict = None) -> Optional[str]:
|
||||
async def _get_mac(self, web_info_v1: dict | None = None) -> str | None:
|
||||
if web_info_v1 is None:
|
||||
try:
|
||||
web_info_v1 = await self.web.info_v1()
|
||||
@@ -108,3 +112,4 @@ class MSKMiner(BMMiner):
|
||||
return web_info_v1["network_info"]["result"]["macaddr"].upper()
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.data.pools import PoolMetrics
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.rpc.unknown import UnknownRPCAPI
|
||||
|
||||
@@ -47,8 +47,8 @@ class UnknownMiner(BaseMiner):
|
||||
async def fault_light_on(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_config(self) -> None:
|
||||
return None
|
||||
async def get_config(self) -> MinerConfig:
|
||||
return MinerConfig()
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
return False
|
||||
@@ -62,7 +62,9 @@ class UnknownMiner(BaseMiner):
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
async def set_power_limit(self, wattage: int) -> bool:
|
||||
@@ -72,53 +74,62 @@ class UnknownMiner(BaseMiner):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def _get_mac(self) -> Optional[str]:
|
||||
async def _get_mac(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_version(self) -> Tuple[Optional[str], Optional[str]]:
|
||||
async def _get_serial_number(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_version(self) -> tuple[str | None, str | None]:
|
||||
return None, None
|
||||
|
||||
async def _get_hostname(self) -> Optional[str]:
|
||||
async def _get_hostname(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(self) -> AlgoHashRateType | None:
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self) -> List[HashBoard]:
|
||||
async def _get_hashboards(self) -> list[HashBoard]:
|
||||
return []
|
||||
|
||||
async def _get_env_temp(self) -> Optional[float]:
|
||||
async def _get_env_temp(self) -> float | None:
|
||||
return None
|
||||
|
||||
async def _get_wattage(self) -> Optional[int]:
|
||||
async def _get_wattage(self) -> int | None:
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(self) -> Optional[int]:
|
||||
async def _get_wattage_limit(self) -> int | None:
|
||||
return None
|
||||
|
||||
async def _get_fans(self) -> List[Fan]:
|
||||
async def _get_fans(self) -> list[Fan]:
|
||||
return []
|
||||
|
||||
async def _get_fan_psu(self) -> Optional[int]:
|
||||
async def _get_fan_psu(self) -> int | None:
|
||||
return None
|
||||
|
||||
async def _get_api_ver(self) -> Optional[str]:
|
||||
async def _get_api_ver(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self) -> Optional[str]:
|
||||
async def _get_fw_ver(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_errors(self) -> List[MinerErrorData]:
|
||||
async def _get_errors(self) -> list[MinerErrorData]:
|
||||
return []
|
||||
|
||||
async def _get_fault_light(self) -> bool:
|
||||
return False
|
||||
|
||||
async def _get_expected_hashrate(self) -> Optional[AlgoHashRate]:
|
||||
async def _get_expected_hashrate(self) -> AlgoHashRateType | None:
|
||||
return None
|
||||
|
||||
async def _is_mining(self, *args, **kwargs) -> Optional[bool]:
|
||||
async def _is_mining(self, *args, **kwargs) -> bool | None:
|
||||
return None
|
||||
|
||||
async def _get_uptime(self, *args, **kwargs) -> Optional[int]:
|
||||
async def _get_uptime(self, *args, **kwargs) -> int | None:
|
||||
return None
|
||||
|
||||
async def _get_pools(self) -> list[PoolMetrics]:
|
||||
return []
|
||||
|
||||
async def _get_voltage(self) -> float | None:
|
||||
return None
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic import MinerConfig
|
||||
from pyasic.data.error_codes import MinerErrorData, VnishError
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.config.mining import MiningModePreset
|
||||
from pyasic.data.error_codes import VnishError
|
||||
from pyasic.device.algorithm import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.backends.bmminer import BMMiner
|
||||
from pyasic.miners.data import (
|
||||
@@ -106,7 +106,9 @@ class VNish(VNishFirmware, BMMiner):
|
||||
|
||||
data_locations = VNISH_DATA_LOC
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
await self.web.post_settings(
|
||||
miner_settings=config.as_vnish(user_suffix=user_suffix)
|
||||
)
|
||||
@@ -147,7 +149,7 @@ class VNish(VNishFirmware, BMMiner):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def _get_mac(self, web_summary: dict = None) -> str:
|
||||
async def _get_mac(self, web_summary: dict | None = None) -> str | None:
|
||||
if web_summary is not None:
|
||||
try:
|
||||
mac = web_summary["system"]["network_status"]["mac"]
|
||||
@@ -164,6 +166,8 @@ class VNish(VNishFirmware, BMMiner):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
result = await self.web.find_miner()
|
||||
if result is not None:
|
||||
@@ -171,6 +175,7 @@ class VNish(VNishFirmware, BMMiner):
|
||||
return True
|
||||
else:
|
||||
await self.web.find_miner()
|
||||
return False
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
result = await self.web.find_miner()
|
||||
@@ -179,26 +184,27 @@ class VNish(VNishFirmware, BMMiner):
|
||||
return True
|
||||
else:
|
||||
await self.web.find_miner()
|
||||
return False
|
||||
|
||||
async def _get_hostname(self, web_summary: dict = None) -> str:
|
||||
async def _get_hostname(self, web_summary: dict | None = None) -> str | None:
|
||||
if web_summary is None:
|
||||
web_info = await self.web.info()
|
||||
|
||||
if web_info is not None:
|
||||
try:
|
||||
hostname = web_info["system"]["network_status"]["hostname"]
|
||||
return hostname
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if web_summary is not None:
|
||||
else:
|
||||
try:
|
||||
hostname = web_summary["system"]["network_status"]["hostname"]
|
||||
return hostname
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
async def _get_wattage(self, web_summary: dict = None) -> Optional[int]:
|
||||
return None
|
||||
|
||||
async def _get_wattage(self, web_summary: dict | None = None) -> int | None:
|
||||
if web_summary is None:
|
||||
web_summary = await self.web.summary()
|
||||
|
||||
@@ -209,25 +215,30 @@ class VNish(VNishFirmware, BMMiner):
|
||||
return wattage
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_summary: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
# get hr from API
|
||||
if rpc_summary is None:
|
||||
try:
|
||||
rpc_summary = await self.rpc.summary()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if rpc_summary is not None:
|
||||
try:
|
||||
return self.algo.hashrate(
|
||||
rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]),
|
||||
unit=self.algo.unit.GH,
|
||||
).into(self.algo.unit.default)
|
||||
unit=self.algo.unit.GH, # type: ignore[attr-defined]
|
||||
).into(self.algo.unit.default) # type: ignore[attr-defined]
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
async def _get_wattage_limit(self, web_settings: dict = None) -> Optional[int]:
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(self, web_settings: dict | None = None) -> int | None:
|
||||
if web_settings is None:
|
||||
web_settings = await self.web.summary()
|
||||
|
||||
@@ -240,7 +251,9 @@ class VNish(VNishFirmware, BMMiner):
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
async def _get_fw_ver(self, web_summary: dict = None) -> Optional[str]:
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self, web_summary: dict | None = None) -> str | None:
|
||||
if web_summary is None:
|
||||
web_summary = await self.web.summary()
|
||||
|
||||
@@ -253,16 +266,16 @@ class VNish(VNishFirmware, BMMiner):
|
||||
except LookupError:
|
||||
return fw_ver
|
||||
|
||||
async def _is_mining(self, web_summary: dict = None) -> Optional[bool]:
|
||||
async def _is_mining(self, web_summary: dict | None = None) -> bool | None:
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
except APIError:
|
||||
pass
|
||||
return None
|
||||
|
||||
if web_summary is not None:
|
||||
try:
|
||||
is_mining = not web_summary["miner"]["miner_status"]["miner_state"] in [
|
||||
is_mining = web_summary["miner"]["miner_status"]["miner_state"] not in [
|
||||
"stopped",
|
||||
"shutting-down",
|
||||
"failure",
|
||||
@@ -271,8 +284,13 @@ class VNish(VNishFirmware, BMMiner):
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
|
||||
errors = []
|
||||
return None
|
||||
|
||||
async def _get_errors( # type: ignore[override]
|
||||
self, web_summary: dict | None = None
|
||||
) -> list[VnishError]:
|
||||
errors: list[VnishError] = []
|
||||
|
||||
if web_summary is None:
|
||||
try:
|
||||
web_summary = await self.web.summary()
|
||||
@@ -292,10 +310,13 @@ class VNish(VNishFirmware, BMMiner):
|
||||
async def get_config(self) -> MinerConfig:
|
||||
try:
|
||||
web_settings = await self.web.settings()
|
||||
web_presets = await self.web.autotune_presets()
|
||||
web_presets_dict = await self.web.autotune_presets()
|
||||
web_presets = (
|
||||
web_presets_dict.get("presets", []) if web_presets_dict else []
|
||||
)
|
||||
web_perf_summary = (await self.web.perf_summary()) or {}
|
||||
except APIError:
|
||||
return self.config
|
||||
return self.config or MinerConfig()
|
||||
self.config = MinerConfig.from_vnish(
|
||||
web_settings, web_presets, web_perf_summary
|
||||
)
|
||||
@@ -303,11 +324,20 @@ class VNish(VNishFirmware, BMMiner):
|
||||
|
||||
async def set_power_limit(self, wattage: int) -> bool:
|
||||
config = await self.get_config()
|
||||
|
||||
# Check if mining mode is preset mode and has available presets
|
||||
if not isinstance(config.mining_mode, MiningModePreset):
|
||||
return False
|
||||
|
||||
valid_presets = [
|
||||
preset.power
|
||||
for preset in config.mining_mode.available_presets
|
||||
if preset.tuned and preset.power <= wattage
|
||||
if (preset.tuned and preset.power is not None and preset.power <= wattage)
|
||||
]
|
||||
|
||||
if not valid_presets:
|
||||
return False
|
||||
|
||||
new_wattage = max(valid_presets)
|
||||
|
||||
# Set power to highest preset <= wattage
|
||||
|
||||
@@ -16,54 +16,53 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import warnings
|
||||
from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union
|
||||
from typing import Any, Protocol, TypeVar
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, HashBoard, MinerData
|
||||
from pyasic.data.device import DeviceInfo
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
from pyasic.data.pools import PoolMetrics
|
||||
from pyasic.device.algorithm import MinerAlgoType
|
||||
from pyasic.device.algorithm import AlgoHashRateType, MinerAlgoType
|
||||
from pyasic.device.algorithm.base import GenericAlgo
|
||||
from pyasic.device.algorithm.hashrate import AlgoHashRate
|
||||
from pyasic.device.firmware import MinerFirmware
|
||||
from pyasic.device.makes import MinerMake
|
||||
from pyasic.device.models import MinerModelType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.logger import logger
|
||||
from pyasic.miners.data import DataLocations, DataOptions, RPCAPICommand, WebAPICommand
|
||||
from pyasic.miners.data import DataOptions, RPCAPICommand, WebAPICommand
|
||||
|
||||
|
||||
class MinerProtocol(Protocol):
|
||||
_rpc_cls: Type = None
|
||||
_web_cls: Type = None
|
||||
_ssh_cls: Type = None
|
||||
_rpc_cls: type[Any] | None = None
|
||||
_web_cls: type[Any] | None = None
|
||||
_ssh_cls: type[Any] | None = None
|
||||
|
||||
ip: str = None
|
||||
rpc: _rpc_cls = None
|
||||
web: _web_cls = None
|
||||
ssh: _ssh_cls = None
|
||||
ip: str | None = None
|
||||
rpc: Any | None = None
|
||||
web: Any | None = None
|
||||
ssh: Any | None = None
|
||||
|
||||
make: MinerMake = None
|
||||
raw_model: MinerModelType = None
|
||||
firmware: MinerFirmware = None
|
||||
make: MinerMake | None = None
|
||||
raw_model: MinerModelType | None = None
|
||||
firmware: MinerFirmware | None = None
|
||||
algo: type[MinerAlgoType] = GenericAlgo
|
||||
|
||||
expected_hashboards: int = None
|
||||
expected_chips: int = None
|
||||
expected_fans: int = None
|
||||
expected_hashboards: int | None = None
|
||||
expected_chips: int | None = None
|
||||
expected_fans: int | None = None
|
||||
|
||||
data_locations: DataLocations = None
|
||||
data_locations: Any | None = None
|
||||
|
||||
supports_shutdown: bool = False
|
||||
supports_power_modes: bool = False
|
||||
supports_presets: bool = False
|
||||
supports_autotuning: bool = False
|
||||
|
||||
api_ver: str = None
|
||||
fw_ver: str = None
|
||||
light: bool = None
|
||||
config: MinerConfig = None
|
||||
api_ver: str | None = None
|
||||
fw_ver: str | None = None
|
||||
light: bool | None = None
|
||||
config: MinerConfig | None = None
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.model}: {str(self.ip)}"
|
||||
@@ -80,9 +79,9 @@ class MinerProtocol(Protocol):
|
||||
@property
|
||||
def model(self) -> str:
|
||||
if self.raw_model is not None:
|
||||
model_data = [self.raw_model]
|
||||
model_data = [str(self.raw_model)]
|
||||
elif self.make is not None:
|
||||
model_data = [self.make]
|
||||
model_data = [str(self.make)]
|
||||
else:
|
||||
model_data = ["Unknown"]
|
||||
if self.firmware is not None:
|
||||
@@ -148,7 +147,9 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
async def send_config(
|
||||
self, config: MinerConfig, user_suffix: str | None = None
|
||||
) -> None:
|
||||
"""Set the mining configuration of the miner.
|
||||
|
||||
Parameters:
|
||||
@@ -187,9 +188,9 @@ class MinerProtocol(Protocol):
|
||||
async def upgrade_firmware(
|
||||
self,
|
||||
*,
|
||||
file: str = None,
|
||||
url: str = None,
|
||||
version: str = None,
|
||||
file: str | None = None,
|
||||
url: str | None = None,
|
||||
version: str | None = None,
|
||||
keep_settings: bool = True,
|
||||
) -> bool:
|
||||
"""Upgrade the firmware of the miner.
|
||||
@@ -209,7 +210,15 @@ class MinerProtocol(Protocol):
|
||||
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
|
||||
##################################################
|
||||
|
||||
async def get_mac(self) -> Optional[str]:
|
||||
async def get_serial_number(self) -> str | None:
|
||||
"""Get the serial number of the miner and return it as a string.
|
||||
|
||||
Returns:
|
||||
A string representing the serial number of the miner.
|
||||
"""
|
||||
return await self._get_serial_number()
|
||||
|
||||
async def get_mac(self) -> str | None:
|
||||
"""Get the MAC address of the miner and return it as a string.
|
||||
|
||||
Returns:
|
||||
@@ -217,7 +226,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_mac()
|
||||
|
||||
async def get_model(self) -> Optional[str]:
|
||||
async def get_model(self) -> str | None:
|
||||
"""Get the model of the miner and return it as a string.
|
||||
|
||||
Returns:
|
||||
@@ -225,7 +234,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return self.model
|
||||
|
||||
async def get_device_info(self) -> Optional[DeviceInfo]:
|
||||
async def get_device_info(self) -> DeviceInfo | None:
|
||||
"""Get device information, including model, make, and firmware.
|
||||
|
||||
Returns:
|
||||
@@ -233,7 +242,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return self.device_info
|
||||
|
||||
async def get_api_ver(self) -> Optional[str]:
|
||||
async def get_api_ver(self) -> str | None:
|
||||
"""Get the API version of the miner and is as a string.
|
||||
|
||||
Returns:
|
||||
@@ -241,7 +250,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_api_ver()
|
||||
|
||||
async def get_fw_ver(self) -> Optional[str]:
|
||||
async def get_fw_ver(self) -> str | None:
|
||||
"""Get the firmware version of the miner and is as a string.
|
||||
|
||||
Returns:
|
||||
@@ -249,7 +258,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_fw_ver()
|
||||
|
||||
async def get_version(self) -> Tuple[Optional[str], Optional[str]]:
|
||||
async def get_version(self) -> tuple[str | None, str | None]:
|
||||
"""Get the API version and firmware version of the miner and return them as strings.
|
||||
|
||||
Returns:
|
||||
@@ -259,7 +268,7 @@ class MinerProtocol(Protocol):
|
||||
fw_ver = await self.get_fw_ver()
|
||||
return api_ver, fw_ver
|
||||
|
||||
async def get_hostname(self) -> Optional[str]:
|
||||
async def get_hostname(self) -> str | None:
|
||||
"""Get the hostname of the miner and return it as a string.
|
||||
|
||||
Returns:
|
||||
@@ -267,7 +276,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_hostname()
|
||||
|
||||
async def get_hashrate(self) -> Optional[AlgoHashRate]:
|
||||
async def get_hashrate(self) -> AlgoHashRateType | None:
|
||||
"""Get the hashrate of the miner and return it as a float in TH/s.
|
||||
|
||||
Returns:
|
||||
@@ -275,7 +284,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_hashrate()
|
||||
|
||||
async def get_hashboards(self) -> List[HashBoard]:
|
||||
async def get_hashboards(self) -> list[HashBoard]:
|
||||
"""Get hashboard data from the miner in the form of [`HashBoard`][pyasic.data.HashBoard].
|
||||
|
||||
Returns:
|
||||
@@ -283,7 +292,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_hashboards()
|
||||
|
||||
async def get_env_temp(self) -> Optional[float]:
|
||||
async def get_env_temp(self) -> float | None:
|
||||
"""Get environment temp from the miner as a float.
|
||||
|
||||
Returns:
|
||||
@@ -291,7 +300,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_env_temp()
|
||||
|
||||
async def get_wattage(self) -> Optional[int]:
|
||||
async def get_wattage(self) -> int | None:
|
||||
"""Get wattage from the miner as an int.
|
||||
|
||||
Returns:
|
||||
@@ -299,7 +308,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_wattage()
|
||||
|
||||
async def get_voltage(self) -> Optional[float]:
|
||||
async def get_voltage(self) -> float | None:
|
||||
"""Get output voltage of the PSU as a float.
|
||||
|
||||
Returns:
|
||||
@@ -307,7 +316,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_voltage()
|
||||
|
||||
async def get_wattage_limit(self) -> Optional[int]:
|
||||
async def get_wattage_limit(self) -> int | None:
|
||||
"""Get wattage limit from the miner as an int.
|
||||
|
||||
Returns:
|
||||
@@ -315,7 +324,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_wattage_limit()
|
||||
|
||||
async def get_fans(self) -> List[Fan]:
|
||||
async def get_fans(self) -> list[Fan]:
|
||||
"""Get fan data from the miner in the form [fan_1, fan_2, fan_3, fan_4].
|
||||
|
||||
Returns:
|
||||
@@ -323,7 +332,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_fans()
|
||||
|
||||
async def get_fan_psu(self) -> Optional[int]:
|
||||
async def get_fan_psu(self) -> int | None:
|
||||
"""Get PSU fan speed from the miner.
|
||||
|
||||
Returns:
|
||||
@@ -331,7 +340,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_fan_psu()
|
||||
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
async def get_errors(self) -> list[MinerErrorData]:
|
||||
"""Get a list of the errors the miner is experiencing.
|
||||
|
||||
Returns:
|
||||
@@ -345,9 +354,10 @@ class MinerProtocol(Protocol):
|
||||
Returns:
|
||||
A boolean value where `True` represents on and `False` represents off.
|
||||
"""
|
||||
return await self._get_fault_light()
|
||||
result = await self._get_fault_light()
|
||||
return result if result is not None else False
|
||||
|
||||
async def get_expected_hashrate(self) -> Optional[AlgoHashRate]:
|
||||
async def get_expected_hashrate(self) -> AlgoHashRateType | None:
|
||||
"""Get the nominal hashrate from factory if available.
|
||||
|
||||
Returns:
|
||||
@@ -355,7 +365,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_expected_hashrate()
|
||||
|
||||
async def is_mining(self) -> Optional[bool]:
|
||||
async def is_mining(self) -> bool | None:
|
||||
"""Check whether the miner is mining.
|
||||
|
||||
Returns:
|
||||
@@ -363,7 +373,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._is_mining()
|
||||
|
||||
async def get_uptime(self) -> Optional[int]:
|
||||
async def get_uptime(self) -> int | None:
|
||||
"""Get the uptime of the miner in seconds.
|
||||
|
||||
Returns:
|
||||
@@ -371,7 +381,7 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_uptime()
|
||||
|
||||
async def get_pools(self) -> List[PoolMetrics]:
|
||||
async def get_pools(self) -> list[PoolMetrics]:
|
||||
"""Get the pools information from Miner.
|
||||
|
||||
Returns:
|
||||
@@ -379,65 +389,68 @@ class MinerProtocol(Protocol):
|
||||
"""
|
||||
return await self._get_pools()
|
||||
|
||||
async def _get_mac(self) -> Optional[str]:
|
||||
async def _get_serial_number(self) -> str | None:
|
||||
pass
|
||||
|
||||
async def _get_api_ver(self) -> Optional[str]:
|
||||
pass
|
||||
async def _get_mac(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_fw_ver(self) -> Optional[str]:
|
||||
pass
|
||||
async def _get_api_ver(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_hostname(self) -> Optional[str]:
|
||||
pass
|
||||
async def _get_fw_ver(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_hashrate(self) -> Optional[AlgoHashRate]:
|
||||
pass
|
||||
async def _get_hostname(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self) -> List[HashBoard]:
|
||||
async def _get_hashrate(self) -> AlgoHashRateType | None:
|
||||
return None
|
||||
|
||||
async def _get_hashboards(self) -> list[HashBoard]:
|
||||
return []
|
||||
|
||||
async def _get_env_temp(self) -> Optional[float]:
|
||||
pass
|
||||
async def _get_env_temp(self) -> float | None:
|
||||
return None
|
||||
|
||||
async def _get_wattage(self) -> Optional[int]:
|
||||
pass
|
||||
async def _get_wattage(self) -> int | None:
|
||||
return None
|
||||
|
||||
async def _get_voltage(self) -> Optional[float]:
|
||||
pass
|
||||
async def _get_voltage(self) -> float | None:
|
||||
return None
|
||||
|
||||
async def _get_wattage_limit(self) -> Optional[int]:
|
||||
pass
|
||||
async def _get_wattage_limit(self) -> int | None:
|
||||
return None
|
||||
|
||||
async def _get_fans(self) -> List[Fan]:
|
||||
async def _get_fans(self) -> list[Fan]:
|
||||
return []
|
||||
|
||||
async def _get_fan_psu(self) -> Optional[int]:
|
||||
pass
|
||||
async def _get_fan_psu(self) -> int | None:
|
||||
return None
|
||||
|
||||
async def _get_errors(self) -> List[MinerErrorData]:
|
||||
async def _get_errors(self) -> list[MinerErrorData]:
|
||||
return []
|
||||
|
||||
async def _get_fault_light(self) -> Optional[bool]:
|
||||
pass
|
||||
async def _get_fault_light(self) -> bool | None:
|
||||
return None
|
||||
|
||||
async def _get_expected_hashrate(self) -> Optional[AlgoHashRate]:
|
||||
pass
|
||||
async def _get_expected_hashrate(self) -> AlgoHashRateType | None:
|
||||
return None
|
||||
|
||||
async def _is_mining(self) -> Optional[bool]:
|
||||
pass
|
||||
async def _is_mining(self) -> bool | None:
|
||||
return None
|
||||
|
||||
async def _get_uptime(self) -> Optional[int]:
|
||||
pass
|
||||
async def _get_uptime(self) -> int | None:
|
||||
return None
|
||||
|
||||
async def _get_pools(self) -> List[PoolMetrics]:
|
||||
pass
|
||||
async def _get_pools(self) -> list[PoolMetrics]:
|
||||
return []
|
||||
|
||||
async def _get_data(
|
||||
self,
|
||||
allow_warning: bool,
|
||||
include: List[Union[str, DataOptions]] = None,
|
||||
exclude: List[Union[str, DataOptions]] = None,
|
||||
include: list[str | DataOptions] | None = None,
|
||||
exclude: list[str | DataOptions] | None = None,
|
||||
) -> dict:
|
||||
# handle include
|
||||
if include is not None:
|
||||
@@ -459,7 +472,7 @@ class MinerProtocol(Protocol):
|
||||
for data_name in include:
|
||||
try:
|
||||
# get kwargs needed for the _get_xyz function
|
||||
fn_args = getattr(self.data_locations, data_name).kwargs
|
||||
fn_args = getattr(self.data_locations, str(data_name)).kwargs
|
||||
|
||||
# keep track of which RPC/Web commands need to be sent
|
||||
for arg in fn_args:
|
||||
@@ -472,13 +485,21 @@ class MinerProtocol(Protocol):
|
||||
continue
|
||||
|
||||
# create tasks for all commands that need to be sent, or no-op with sleep(0) -> None
|
||||
if len(rpc_multicommand) > 0:
|
||||
if (
|
||||
len(rpc_multicommand) > 0
|
||||
and self.rpc is not None
|
||||
and hasattr(self.rpc, "multicommand")
|
||||
):
|
||||
rpc_command_task = asyncio.create_task(
|
||||
self.rpc.multicommand(*rpc_multicommand, allow_warning=allow_warning)
|
||||
)
|
||||
else:
|
||||
rpc_command_task = asyncio.create_task(asyncio.sleep(0))
|
||||
if len(web_multicommand) > 0:
|
||||
if (
|
||||
len(web_multicommand) > 0
|
||||
and self.web is not None
|
||||
and hasattr(self.web, "multicommand")
|
||||
):
|
||||
web_command_task = asyncio.create_task(
|
||||
self.web.multicommand(*web_multicommand, allow_warning=allow_warning)
|
||||
)
|
||||
@@ -500,7 +521,7 @@ class MinerProtocol(Protocol):
|
||||
|
||||
for data_name in include:
|
||||
try:
|
||||
fn_args = getattr(self.data_locations, data_name).kwargs
|
||||
fn_args = getattr(self.data_locations, str(data_name)).kwargs
|
||||
args_to_send = {k.name: None for k in fn_args}
|
||||
for arg in fn_args:
|
||||
try:
|
||||
@@ -521,7 +542,9 @@ class MinerProtocol(Protocol):
|
||||
except LookupError:
|
||||
continue
|
||||
try:
|
||||
function = getattr(self, getattr(self.data_locations, data_name).cmd)
|
||||
function = getattr(
|
||||
self, getattr(self.data_locations, str(data_name)).cmd
|
||||
)
|
||||
miner_data[data_name] = await function(**args_to_send)
|
||||
except Exception as e:
|
||||
raise APIError(
|
||||
@@ -532,8 +555,8 @@ class MinerProtocol(Protocol):
|
||||
async def get_data(
|
||||
self,
|
||||
allow_warning: bool = False,
|
||||
include: List[Union[str, DataOptions]] = None,
|
||||
exclude: List[Union[str, DataOptions]] = None,
|
||||
include: list[str | DataOptions] | None = None,
|
||||
exclude: list[str | DataOptions] | None = None,
|
||||
) -> MinerData:
|
||||
"""Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData].
|
||||
|
||||
@@ -551,6 +574,7 @@ class MinerProtocol(Protocol):
|
||||
expected_chips=(
|
||||
self.expected_chips * self.expected_hashboards
|
||||
if self.expected_chips is not None
|
||||
and self.expected_hashboards is not None
|
||||
else 0
|
||||
),
|
||||
expected_hashboards=self.expected_hashboards,
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
from dataclasses import dataclass, field, make_dataclass
|
||||
from enum import Enum
|
||||
from typing import List, Union
|
||||
|
||||
|
||||
class DataOptions(Enum):
|
||||
SERIAL_NUMBER = "serial_number"
|
||||
MAC = "mac"
|
||||
API_VERSION = "api_ver"
|
||||
FW_VERSION = "fw_ver"
|
||||
@@ -66,7 +66,7 @@ class WebAPICommand:
|
||||
@dataclass
|
||||
class DataFunction:
|
||||
cmd: str
|
||||
kwargs: List[Union[RPCAPICommand, WebAPICommand]] = field(default_factory=list)
|
||||
kwargs: list[RPCAPICommand | WebAPICommand] = field(default_factory=list)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from pyasic.device.algorithm import MinerAlgo
|
||||
from pyasic.device.models import MinerModel
|
||||
from pyasic.device.models import MinerModel, MinerModelType
|
||||
from pyasic.miners.device.makes import AntMinerMake
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ class S19jNoPIC(AntMinerMake):
|
||||
|
||||
|
||||
class S19jPro(AntMinerMake):
|
||||
raw_model = MinerModel.ANTMINER.S19jPro
|
||||
raw_model: MinerModelType = MinerModel.ANTMINER.S19jPro
|
||||
|
||||
expected_chips = 126
|
||||
expected_fans = 4
|
||||
@@ -163,7 +163,7 @@ class S19jProPlusNoPIC(AntMinerMake):
|
||||
|
||||
|
||||
class S19kPro(AntMinerMake):
|
||||
raw_model = MinerModel.ANTMINER.S19kPro
|
||||
raw_model: MinerModelType = MinerModel.ANTMINER.S19kPro
|
||||
|
||||
expected_chips = 77
|
||||
expected_fans = 4
|
||||
|
||||
@@ -30,8 +30,8 @@ from .S19 import (
|
||||
S19jProPlus,
|
||||
S19jProPlusNoPIC,
|
||||
S19jXP,
|
||||
S19kPro,
|
||||
S19KPro,
|
||||
S19kPro,
|
||||
S19kProNoPIC,
|
||||
S19NoPIC,
|
||||
S19Plus,
|
||||
|
||||
@@ -1,2 +1,18 @@
|
||||
from .ALX import *
|
||||
from .KSX import *
|
||||
|
||||
# Define what gets exported with wildcard to exclude KS3 and KS5
|
||||
# which conflict with antminer models
|
||||
__all__ = [
|
||||
# From ALX
|
||||
"AL3",
|
||||
# From KSX
|
||||
"KS0",
|
||||
"KS1",
|
||||
"KS2",
|
||||
"KS3L",
|
||||
"KS3M",
|
||||
"KS5L",
|
||||
"KS5M",
|
||||
# Note: KS3 and KS5 are excluded to avoid conflicts with antminer
|
||||
]
|
||||
|
||||
@@ -141,6 +141,7 @@ from .M31S_Plus import (
|
||||
)
|
||||
from .M31SE import M31SEV10, M31SEV20, M31SEV30
|
||||
from .M32 import M32V10, M32V20
|
||||
from .M32S import M32S
|
||||
from .M33 import M33V10, M33V20, M33V30
|
||||
from .M33S import M33SVG30
|
||||
from .M33S_Plus import M33SPlusVG20, M33SPlusVG30, M33SPlusVH20, M33SPlusVH30
|
||||
|
||||
@@ -21,7 +21,8 @@ import ipaddress
|
||||
import json
|
||||
import re
|
||||
import warnings
|
||||
from typing import Any, AsyncGenerator, Callable
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from typing import Any, cast
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
@@ -70,7 +71,7 @@ class MinerTypes(enum.Enum):
|
||||
MSKMINER = 18
|
||||
|
||||
|
||||
MINER_CLASSES = {
|
||||
MINER_CLASSES: dict[MinerTypes, dict[str | None, Any]] = {
|
||||
MinerTypes.ANTMINER: {
|
||||
None: type("AntminerUnknown", (BMMiner, AntMinerMake), {}),
|
||||
"ANTMINER D3": CGMinerD3,
|
||||
@@ -731,8 +732,9 @@ class MinerFactory:
|
||||
async def get_multiple_miners(
|
||||
self, ips: list[str], limit: int = 200
|
||||
) -> list[AnyMiner]:
|
||||
results = []
|
||||
results: list[AnyMiner] = []
|
||||
|
||||
miner: AnyMiner
|
||||
async for miner in self.get_miner_generator(ips, limit):
|
||||
results.append(miner)
|
||||
|
||||
@@ -740,7 +742,7 @@ class MinerFactory:
|
||||
|
||||
async def get_miner_generator(
|
||||
self, ips: list, limit: int = 200
|
||||
) -> AsyncGenerator[AnyMiner]:
|
||||
) -> AsyncGenerator[AnyMiner, None]:
|
||||
tasks = []
|
||||
semaphore = asyncio.Semaphore(limit)
|
||||
|
||||
@@ -749,11 +751,13 @@ class MinerFactory:
|
||||
|
||||
for task in tasks:
|
||||
async with semaphore:
|
||||
result = await task
|
||||
result = await task # type: ignore[func-returns-value]
|
||||
if result is not None:
|
||||
yield result
|
||||
|
||||
async def get_miner(self, ip: str | ipaddress.ip_address) -> AnyMiner | None:
|
||||
async def get_miner(
|
||||
self, ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address
|
||||
) -> AnyMiner | None:
|
||||
ip = str(ip)
|
||||
|
||||
miner_type = None
|
||||
@@ -771,7 +775,7 @@ class MinerFactory:
|
||||
break
|
||||
|
||||
if miner_type is not None:
|
||||
miner_model = None
|
||||
miner_model: str | None = None
|
||||
miner_model_fns = {
|
||||
MinerTypes.ANTMINER: self.get_miner_model_antminer,
|
||||
MinerTypes.WHATSMINER: self.get_miner_model_whatsminer,
|
||||
@@ -792,7 +796,7 @@ class MinerFactory:
|
||||
MinerTypes.VOLCMINER: self.get_miner_model_volcminer,
|
||||
MinerTypes.ELPHAPEX: self.get_miner_model_elphapex,
|
||||
}
|
||||
version = None
|
||||
version: str | None = None
|
||||
miner_version_fns = {
|
||||
MinerTypes.WHATSMINER: self.get_miner_version_whatsminer,
|
||||
}
|
||||
@@ -801,18 +805,18 @@ class MinerFactory:
|
||||
|
||||
if model_fn is not None:
|
||||
# noinspection PyArgumentList
|
||||
task = asyncio.create_task(model_fn(ip))
|
||||
model_task = asyncio.create_task(model_fn(ip))
|
||||
try:
|
||||
miner_model = await asyncio.wait_for(
|
||||
task, timeout=settings.get("factory_get_timeout", 3)
|
||||
model_task, timeout=settings.get("factory_get_timeout", 3)
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
if version_fn is not None:
|
||||
task = asyncio.create_task(version_fn(ip))
|
||||
version_task = asyncio.create_task(version_fn(ip))
|
||||
try:
|
||||
version = await asyncio.wait_for(
|
||||
task, timeout=settings.get("factory_get_timeout", 3)
|
||||
version_task, timeout=settings.get("factory_get_timeout", 3)
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
@@ -820,6 +824,7 @@ class MinerFactory:
|
||||
ip, miner_type=miner_type, miner_model=miner_model, version=version
|
||||
)
|
||||
return miner
|
||||
return None
|
||||
|
||||
async def _get_miner_type(self, ip: str) -> MinerTypes | None:
|
||||
tasks = [
|
||||
@@ -850,14 +855,15 @@ class MinerFactory:
|
||||
if res is not None:
|
||||
mtype = MinerTypes.MARATHON
|
||||
if mtype == MinerTypes.HAMMER:
|
||||
res = await self.get_miner_model_hammer(ip)
|
||||
if res is None:
|
||||
hammer_model = await self.get_miner_model_hammer(ip)
|
||||
if hammer_model is None:
|
||||
return MinerTypes.HAMMER
|
||||
if "HAMMER" in res.upper():
|
||||
if "HAMMER" in hammer_model.upper():
|
||||
mtype = MinerTypes.HAMMER
|
||||
else:
|
||||
mtype = MinerTypes.VOLCMINER
|
||||
return mtype
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _web_ping(
|
||||
@@ -919,6 +925,7 @@ class MinerFactory:
|
||||
return MinerTypes.INNOSILICON
|
||||
if "Miner UI" in web_text:
|
||||
return MinerTypes.AURADINE
|
||||
return None
|
||||
|
||||
async def _get_miner_socket(self, ip: str) -> MinerTypes | None:
|
||||
commands = ["version", "devdetails"]
|
||||
@@ -931,6 +938,7 @@ class MinerFactory:
|
||||
if data is not None:
|
||||
d = self._parse_socket_type(data)
|
||||
return d
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _socket_ping(ip: str, cmd: str) -> str | None:
|
||||
@@ -941,13 +949,13 @@ class MinerFactory:
|
||||
timeout=settings.get("factory_get_timeout", 3),
|
||||
)
|
||||
except (ConnectionError, OSError, asyncio.TimeoutError):
|
||||
return
|
||||
return None
|
||||
|
||||
cmd = {"command": cmd}
|
||||
command_dict = {"command": cmd}
|
||||
|
||||
try:
|
||||
# send the command
|
||||
writer.write(json.dumps(cmd).encode("utf-8"))
|
||||
writer.write(json.dumps(command_dict).encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
# loop to receive all the data
|
||||
@@ -964,11 +972,11 @@ class MinerFactory:
|
||||
logger.warning(f"{ip}: Socket ping timeout.")
|
||||
break
|
||||
except ConnectionResetError:
|
||||
return
|
||||
return None
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except (ConnectionError, OSError):
|
||||
return
|
||||
return None
|
||||
finally:
|
||||
# Handle cancellation explicitly
|
||||
if writer.transport.is_closing():
|
||||
@@ -978,9 +986,10 @@ class MinerFactory:
|
||||
try:
|
||||
await writer.wait_closed()
|
||||
except (ConnectionError, OSError):
|
||||
return
|
||||
return None
|
||||
if data:
|
||||
return data.decode("utf-8")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_socket_type(data: str) -> MinerTypes | None:
|
||||
@@ -1013,12 +1022,13 @@ class MinerFactory:
|
||||
return MinerTypes.AURADINE
|
||||
if "VNISH" in upper_data:
|
||||
return MinerTypes.VNISH
|
||||
return None
|
||||
|
||||
async def send_web_command(
|
||||
self,
|
||||
ip: str,
|
||||
location: str,
|
||||
auth: httpx.DigestAuth = None,
|
||||
auth: httpx.DigestAuth | None = None,
|
||||
) -> dict | None:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as session:
|
||||
try:
|
||||
@@ -1029,16 +1039,16 @@ class MinerFactory:
|
||||
)
|
||||
except (httpx.HTTPError, asyncio.TimeoutError):
|
||||
logger.info(f"{ip}: Web command timeout.")
|
||||
return
|
||||
return None
|
||||
if data is None:
|
||||
return
|
||||
return None
|
||||
try:
|
||||
json_data = data.json()
|
||||
except (json.JSONDecodeError, asyncio.TimeoutError):
|
||||
try:
|
||||
return json.loads(data.text)
|
||||
except (json.JSONDecodeError, httpx.HTTPError):
|
||||
return
|
||||
return None
|
||||
else:
|
||||
return json_data
|
||||
|
||||
@@ -1047,7 +1057,7 @@ class MinerFactory:
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(ip, 4028)
|
||||
except (ConnectionError, OSError):
|
||||
return
|
||||
return None
|
||||
cmd = {"command": command}
|
||||
|
||||
try:
|
||||
@@ -1067,26 +1077,26 @@ class MinerFactory:
|
||||
except asyncio.CancelledError:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
return
|
||||
return None
|
||||
except (ConnectionError, OSError):
|
||||
return
|
||||
return None
|
||||
if data == b"Socket connect failed: Connection refused\n":
|
||||
return
|
||||
return None
|
||||
|
||||
data = await self._fix_api_data(data)
|
||||
data_str = await self._fix_api_data(data)
|
||||
|
||||
try:
|
||||
data = json.loads(data)
|
||||
data_dict = json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
return data
|
||||
return data_dict
|
||||
|
||||
async def send_btminer_v3_api_command(self, ip, command):
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(ip, 4433)
|
||||
except (ConnectionError, OSError):
|
||||
return
|
||||
return None
|
||||
cmd = {"cmd": command}
|
||||
|
||||
try:
|
||||
@@ -1108,11 +1118,11 @@ class MinerFactory:
|
||||
except asyncio.CancelledError:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
return
|
||||
return None
|
||||
except (ConnectionError, OSError):
|
||||
return
|
||||
return None
|
||||
if data == b"Socket connect failed: Connection refused\n":
|
||||
return
|
||||
return None
|
||||
|
||||
try:
|
||||
data = json.loads(data)
|
||||
@@ -1156,7 +1166,7 @@ class MinerFactory:
|
||||
|
||||
@staticmethod
|
||||
def _select_miner_from_classes(
|
||||
ip: ipaddress.ip_address,
|
||||
ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address,
|
||||
miner_model: str | None,
|
||||
miner_type: MinerTypes | None,
|
||||
version: str | None = None,
|
||||
@@ -1165,6 +1175,10 @@ class MinerFactory:
|
||||
if "HIVEON" in str(miner_model).upper():
|
||||
miner_model = str(miner_model).upper().replace(" HIVEON", "")
|
||||
miner_type = MinerTypes.HIVEON
|
||||
|
||||
if miner_type is None:
|
||||
return cast(AnyMiner, UnknownMiner(str(ip), version))
|
||||
|
||||
try:
|
||||
return MINER_CLASSES[miner_type][str(miner_model).upper()](ip, version)
|
||||
except LookupError:
|
||||
@@ -1175,7 +1189,7 @@ class MinerFactory:
|
||||
f"and this model on GitHub (https://github.com/UpstreamData/pyasic/issues)."
|
||||
)
|
||||
return MINER_CLASSES[miner_type][None](ip, version)
|
||||
return UnknownMiner(str(ip), version)
|
||||
return cast(AnyMiner, UnknownMiner(str(ip), version))
|
||||
|
||||
async def get_miner_model_antminer(self, ip: str) -> str | None:
|
||||
tasks = [
|
||||
@@ -1194,15 +1208,17 @@ class MinerFactory:
|
||||
ip, "/cgi-bin/get_system_info.cgi", auth=auth
|
||||
)
|
||||
|
||||
if web_json_data is not None:
|
||||
try:
|
||||
miner_model = web_json_data["minertype"]
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def _get_model_antminer_sock(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "version")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
miner_model = sock_json_data["VERSION"][0]["Type"]
|
||||
|
||||
@@ -1213,6 +1229,7 @@ class MinerFactory:
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
sock_json_data = await self.send_api_command(ip, "stats")
|
||||
try:
|
||||
@@ -1225,24 +1242,29 @@ class MinerFactory:
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_goldshell(self, ip: str) -> str | None:
|
||||
json_data = await self.send_web_command(ip, "/mcb/status")
|
||||
|
||||
if json_data is not None:
|
||||
try:
|
||||
miner_model = json_data["model"].replace("-", " ")
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_whatsminer(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "devdetails")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
miner_model = sock_json_data["DEVDETAILS"][0]["Model"].replace("_", "")
|
||||
miner_model = miner_model[:-1] + "0"
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
else:
|
||||
sock_json_data_v3 = await self.send_btminer_v3_api_command(
|
||||
ip, "get.device.info"
|
||||
)
|
||||
@@ -1253,17 +1275,21 @@ class MinerFactory:
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_version_whatsminer(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "get_version")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
version = sock_json_data["Msg"]["fw_ver"]
|
||||
return version
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_avalonminer(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "version")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
miner_model = sock_json_data["VERSION"][0]["PROD"].upper()
|
||||
if "-" in miner_model:
|
||||
@@ -1274,6 +1300,7 @@ class MinerFactory:
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_innosilicon(self, ip: str) -> str | None:
|
||||
try:
|
||||
@@ -1289,7 +1316,7 @@ class MinerFactory:
|
||||
)
|
||||
auth = auth_req.json()["jwt"]
|
||||
except (httpx.HTTPError, LookupError):
|
||||
return
|
||||
return None
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as session:
|
||||
@@ -1303,6 +1330,7 @@ class MinerFactory:
|
||||
return web_data["type"]
|
||||
except (httpx.HTTPError, LookupError):
|
||||
pass
|
||||
return None
|
||||
try:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as session:
|
||||
web_data = (
|
||||
@@ -1315,9 +1343,11 @@ class MinerFactory:
|
||||
return web_data["type"]
|
||||
except (httpx.HTTPError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_braiins_os(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "devdetails")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
miner_model = (
|
||||
sock_json_data["DEVDETAILS"][0]["Model"]
|
||||
@@ -1327,6 +1357,7 @@ class MinerFactory:
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as session:
|
||||
@@ -1342,9 +1373,11 @@ class MinerFactory:
|
||||
return miner_model
|
||||
except (httpx.HTTPError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_vnish(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "stats")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
miner_model = sock_json_data["STATS"][0]["Type"]
|
||||
if " (" in miner_model:
|
||||
@@ -1360,46 +1393,55 @@ class MinerFactory:
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_epic(self, ip: str) -> str | None:
|
||||
for retry_cnt in range(settings.get("get_data_retries", 1)):
|
||||
sock_json_data = await self.send_web_command(ip, ":4028/capabilities")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
miner_model = sock_json_data["Model"]
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
else:
|
||||
if retry_cnt < settings.get("get_data_retries", 1) - 1:
|
||||
continue
|
||||
else:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_hiveon(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "version")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
miner_type = sock_json_data["VERSION"][0]["Type"]
|
||||
|
||||
return miner_type.replace(" HIVEON", "")
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_luxos(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "version")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
miner_model = sock_json_data["VERSION"][0]["Type"]
|
||||
|
||||
if " (" in miner_model:
|
||||
split_miner_model = miner_model.split(" (")
|
||||
miner_model = split_miner_model[0]
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_auradine(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "devdetails")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
return sock_json_data["DEVDETAILS"][0]["Model"]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_marathon(self, ip: str) -> str | None:
|
||||
auth = httpx.DigestAuth("root", "root")
|
||||
@@ -1407,38 +1449,41 @@ class MinerFactory:
|
||||
ip, "/kaonsu/v1/overview", auth=auth
|
||||
)
|
||||
|
||||
if web_json_data is not None:
|
||||
try:
|
||||
miner_model = web_json_data["model"]
|
||||
if miner_model == "":
|
||||
return None
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_bitaxe(self, ip: str) -> str | None:
|
||||
web_json_data = await self.send_web_command(ip, "/api/system/info")
|
||||
|
||||
if web_json_data is not None:
|
||||
try:
|
||||
miner_model = web_json_data["ASICModel"]
|
||||
if miner_model == "":
|
||||
return None
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_luckyminer(self, ip: str) -> str | None:
|
||||
web_json_data = await self.send_web_command(ip, "/api/system/info")
|
||||
|
||||
if web_json_data is not None:
|
||||
try:
|
||||
miner_model = web_json_data["minerModel"]
|
||||
if miner_model == "":
|
||||
return None
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_iceriver(self, ip: str) -> str | None:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
@@ -1461,7 +1506,7 @@ class MinerFactory:
|
||||
f"http://{ip}:/user/userpanel", params={"post": "4"}
|
||||
)
|
||||
if not resp.status_code == 200:
|
||||
return
|
||||
return None
|
||||
result = resp.json()
|
||||
software_ver = result["data"]["softver1"]
|
||||
split_ver = software_ver.split("_")
|
||||
@@ -1472,6 +1517,7 @@ class MinerFactory:
|
||||
return miner_ver.upper()
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_hammer(self, ip: str) -> str | None:
|
||||
auth = httpx.DigestAuth(
|
||||
@@ -1481,12 +1527,13 @@ class MinerFactory:
|
||||
ip, "/cgi-bin/get_system_info.cgi", auth=auth
|
||||
)
|
||||
|
||||
if web_json_data is not None:
|
||||
try:
|
||||
miner_model = web_json_data["minertype"]
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_volcminer(self, ip: str) -> str | None:
|
||||
auth = httpx.DigestAuth(
|
||||
@@ -1496,12 +1543,13 @@ class MinerFactory:
|
||||
ip, "/cgi-bin/get_system_info.cgi", auth=auth
|
||||
)
|
||||
|
||||
if web_json_data is not None:
|
||||
try:
|
||||
miner_model = web_json_data["minertype"]
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_elphapex(self, ip: str) -> str | None:
|
||||
auth = httpx.DigestAuth(
|
||||
@@ -1511,24 +1559,29 @@ class MinerFactory:
|
||||
ip, "/cgi-bin/get_system_info.cgi", auth=auth
|
||||
)
|
||||
|
||||
if web_json_data is not None:
|
||||
try:
|
||||
miner_model = web_json_data["minertype"]
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def get_miner_model_mskminer(self, ip: str) -> str | None:
|
||||
sock_json_data = await self.send_api_command(ip, "version")
|
||||
if sock_json_data is not None:
|
||||
try:
|
||||
return sock_json_data["VERSION"][0]["Type"].split(" ")[0]
|
||||
except LookupError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
miner_factory = MinerFactory()
|
||||
|
||||
|
||||
# abstracted version of get miner that is easier to access
|
||||
async def get_miner(ip: ipaddress.ip_address | str) -> AnyMiner:
|
||||
return await miner_factory.get_miner(ip)
|
||||
async def get_miner(
|
||||
ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address,
|
||||
) -> AnyMiner | None:
|
||||
return await miner_factory.get_miner(ip) # type: ignore[func-returns-value]
|
||||
|
||||
@@ -13,13 +13,12 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import Fan, MinerData
|
||||
from pyasic.data.boards import HashBoard
|
||||
from pyasic.data.pools import PoolMetrics, PoolUrl
|
||||
from pyasic.device.algorithm import AlgoHashRate, MinerAlgo
|
||||
from pyasic.device.algorithm import AlgoHashRateType, MinerAlgo
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.backends import GoldshellMiner
|
||||
from pyasic.miners.data import (
|
||||
@@ -94,8 +93,8 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
async def get_data(
|
||||
self,
|
||||
allow_warning: bool = False,
|
||||
include: List[Union[str, DataOptions]] = None,
|
||||
exclude: List[Union[str, DataOptions]] = None,
|
||||
include: list[str | DataOptions] | None = None,
|
||||
exclude: list[str | DataOptions] | None = None,
|
||||
) -> MinerData:
|
||||
if self.web_devs is None:
|
||||
try:
|
||||
@@ -106,7 +105,7 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
scrypt_board_count = 0
|
||||
zksnark_board_count = 0
|
||||
|
||||
for minfo in self.web_devs.get("minfos", []):
|
||||
for minfo in (self.web_devs or {}).get("minfos", []):
|
||||
algo_name = minfo.get("name")
|
||||
|
||||
for _ in minfo.get("infos", []):
|
||||
@@ -123,9 +122,9 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
)
|
||||
|
||||
if scrypt_board_count > 0 and zksnark_board_count == 0:
|
||||
self.algo = MinerAlgo.SCRYPT
|
||||
self.algo = MinerAlgo.SCRYPT # type: ignore[assignment]
|
||||
elif zksnark_board_count > 0 and scrypt_board_count == 0:
|
||||
self.algo = MinerAlgo.ZKSNARK
|
||||
self.algo = MinerAlgo.ZKSNARK # type: ignore[assignment]
|
||||
|
||||
data = await super().get_data(allow_warning, include, exclude)
|
||||
data.expected_chips = self.expected_chips
|
||||
@@ -136,12 +135,12 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
try:
|
||||
pools = await self.web.pools()
|
||||
except APIError:
|
||||
return self.config or MinerConfig()
|
||||
|
||||
self.config = MinerConfig.from_goldshell_byte(pools.get("groups", []))
|
||||
return self.config
|
||||
|
||||
self.config = MinerConfig.from_goldshell_byte(pools)
|
||||
return self.config
|
||||
|
||||
async def _get_api_ver(self, web_setting: dict = None) -> Optional[str]:
|
||||
async def _get_api_ver(self, web_setting: dict | None = None) -> str | None:
|
||||
if web_setting is None:
|
||||
try:
|
||||
web_setting = await self.web.setting()
|
||||
@@ -160,15 +159,15 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
return self.api_ver
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_devs: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_devs: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_devs is None:
|
||||
try:
|
||||
rpc_devs = await self.rpc.devs()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
total_hash_rate_mh = 0
|
||||
total_hash_rate_mh = 0.0
|
||||
|
||||
if rpc_devs is not None:
|
||||
for board in rpc_devs.get("DEVS", []):
|
||||
@@ -192,14 +191,16 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
return hash_rate
|
||||
|
||||
async def _get_hashrate(self, rpc_devs: dict = None) -> Optional[AlgoHashRate]:
|
||||
async def _get_hashrate(
|
||||
self, rpc_devs: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_devs is None:
|
||||
try:
|
||||
rpc_devs = await self.rpc.devs()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
total_hash_rate_mh = 0
|
||||
total_hash_rate_mh = 0.0
|
||||
|
||||
if rpc_devs is not None:
|
||||
for board in rpc_devs.get("DEVS", []):
|
||||
@@ -211,7 +212,7 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
return hash_rate
|
||||
|
||||
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]:
|
||||
async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
|
||||
if rpc_pools is None:
|
||||
try:
|
||||
rpc_pools = await self.rpc.pools()
|
||||
@@ -240,8 +241,8 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
return pools_data
|
||||
|
||||
async def _get_hashboards(
|
||||
self, rpc_devs: dict = None, rpc_devdetails: dict = None
|
||||
) -> List[HashBoard]:
|
||||
self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if rpc_devs is None:
|
||||
try:
|
||||
rpc_devs = await self.rpc.devs()
|
||||
@@ -285,7 +286,7 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_fans(self, rpc_devs: dict = None) -> List[Fan]:
|
||||
async def _get_fans(self, rpc_devs: dict | None = None) -> list[Fan]:
|
||||
if self.expected_fans is None:
|
||||
return []
|
||||
|
||||
@@ -312,7 +313,7 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
return fans
|
||||
|
||||
async def _get_uptime(self, web_devs: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, web_devs: dict | None = None) -> int | None:
|
||||
if web_devs is None:
|
||||
try:
|
||||
web_devs = await self.web.devs()
|
||||
@@ -321,7 +322,7 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
if web_devs is not None:
|
||||
try:
|
||||
for minfo in self.web_devs.get("minfos", []):
|
||||
for minfo in (self.web_devs or {}).get("minfos", []):
|
||||
for info in minfo.get("infos", []):
|
||||
uptime = int(float(info["time"]))
|
||||
return uptime
|
||||
@@ -330,7 +331,7 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
return None
|
||||
|
||||
async def _get_wattage(self, web_devs: dict = None) -> Optional[int]:
|
||||
async def _get_wattage(self, web_devs: dict | None = None) -> int | None:
|
||||
if web_devs is None:
|
||||
try:
|
||||
web_devs = await self.web.devs()
|
||||
@@ -339,7 +340,7 @@ class GoldshellByte(GoldshellMiner, Byte):
|
||||
|
||||
if web_devs is not None:
|
||||
try:
|
||||
for minfo in self.web_devs.get("minfos", []):
|
||||
for minfo in (self.web_devs or {}).get("minfos", []):
|
||||
for info in minfo.get("infos", []):
|
||||
wattage = int(float(info["power"]))
|
||||
return wattage
|
||||
|
||||
@@ -13,11 +13,10 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import List, Optional
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data.boards import HashBoard
|
||||
from pyasic.device.algorithm import AlgoHashRate
|
||||
from pyasic.device.algorithm.hashrate.base import AlgoHashRateType
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.logger import logger
|
||||
from pyasic.miners.backends import GoldshellMiner
|
||||
@@ -80,18 +79,19 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge):
|
||||
supports_shutdown = False
|
||||
supports_power_modes = False
|
||||
|
||||
async def get_config(self) -> MinerConfig:
|
||||
async def get_config(self) -> MinerConfig | None: # type: ignore[override]
|
||||
try:
|
||||
pools = await self.web.pools()
|
||||
except APIError:
|
||||
if self.config is not None:
|
||||
return self.config
|
||||
|
||||
self.config = MinerConfig.from_goldshell_list(pools)
|
||||
self.config = MinerConfig.from_goldshell(pools)
|
||||
return self.config
|
||||
|
||||
async def _get_expected_hashrate(
|
||||
self, rpc_devs: dict = None
|
||||
) -> Optional[AlgoHashRate]:
|
||||
self, rpc_devs: dict | None = None
|
||||
) -> AlgoHashRateType | None:
|
||||
if rpc_devs is None:
|
||||
try:
|
||||
rpc_devs = await self.rpc.devs()
|
||||
@@ -110,8 +110,8 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge):
|
||||
return None
|
||||
|
||||
async def _get_hashboards(
|
||||
self, rpc_devs: dict = None, rpc_devdetails: dict = None
|
||||
) -> List[HashBoard]:
|
||||
self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None
|
||||
) -> list[HashBoard]:
|
||||
if rpc_devs is None:
|
||||
try:
|
||||
rpc_devs = await self.rpc.devs()
|
||||
@@ -162,7 +162,7 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge):
|
||||
|
||||
return hashboards
|
||||
|
||||
async def _get_uptime(self, web_devs: dict = None) -> Optional[int]:
|
||||
async def _get_uptime(self, web_devs: dict | None = None) -> int | None:
|
||||
if web_devs is None:
|
||||
try:
|
||||
web_devs = await self.web.devs()
|
||||
|
||||
@@ -55,7 +55,7 @@ class MinerListenerProtocol(asyncio.Protocol):
|
||||
|
||||
class MinerListener:
|
||||
def __init__(self, bind_addr: str = "0.0.0.0"):
|
||||
self.found_miners = []
|
||||
self.found_miners: list[dict[str, str]] = []
|
||||
self.stop = asyncio.Event()
|
||||
self.bind_addr = bind_addr
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import logging
|
||||
from typing import AsyncIterator, List, Union
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import cast
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic.miners.factory import AnyMiner, miner_factory
|
||||
@@ -30,24 +31,24 @@ class MinerNetwork:
|
||||
hosts: A list of `ipaddress.IPv4Address` to be used when scanning.
|
||||
"""
|
||||
|
||||
def __init__(self, hosts: List[ipaddress.IPv4Address]):
|
||||
def __init__(self, hosts: list[ipaddress.IPv4Address]):
|
||||
self.hosts = hosts
|
||||
semaphore_limit = settings.get("network_scan_semaphore", 255)
|
||||
if semaphore_limit is None:
|
||||
semaphore_limit = 255
|
||||
self.semaphore = asyncio.Semaphore(semaphore_limit)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self.hosts)
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, addresses: list) -> "MinerNetwork":
|
||||
def from_list(cls, addresses: list[str]) -> "MinerNetwork":
|
||||
"""Parse a list of address constructors into a MinerNetwork.
|
||||
|
||||
Parameters:
|
||||
addresses: A list of address constructors, such as `["10.1-2.1.1-50", "10.4.1-2.1-50"]`.
|
||||
"""
|
||||
hosts = []
|
||||
hosts: list[ipaddress.IPv4Address] = []
|
||||
for address in addresses:
|
||||
hosts = [*hosts, *cls.from_address(address).hosts]
|
||||
return cls(sorted(list(set(hosts))))
|
||||
@@ -79,7 +80,7 @@ class MinerNetwork:
|
||||
oct_4: An octet constructor, such as `"1-50"`.
|
||||
"""
|
||||
|
||||
hosts = []
|
||||
hosts: list[ipaddress.IPv4Address] = []
|
||||
|
||||
oct_1_start, oct_1_end = compute_oct_range(oct_1)
|
||||
for oct_1_idx in range((abs(oct_1_end - oct_1_start)) + 1):
|
||||
@@ -97,11 +98,11 @@ class MinerNetwork:
|
||||
for oct_4_idx in range((abs(oct_4_end - oct_4_start)) + 1):
|
||||
oct_4_val = str(oct_4_idx + oct_4_start)
|
||||
|
||||
hosts.append(
|
||||
ipaddress.ip_address(
|
||||
ip_addr = ipaddress.ip_address(
|
||||
".".join([oct_1_val, oct_2_val, oct_3_val, oct_4_val])
|
||||
)
|
||||
)
|
||||
if isinstance(ip_addr, ipaddress.IPv4Address):
|
||||
hosts.append(ip_addr)
|
||||
return cls(sorted(hosts))
|
||||
|
||||
@classmethod
|
||||
@@ -111,9 +112,13 @@ class MinerNetwork:
|
||||
Parameters:
|
||||
subnet: A subnet string, such as `"10.0.0.1/24"`.
|
||||
"""
|
||||
return cls(list(ipaddress.ip_network(subnet, strict=False).hosts()))
|
||||
network = ipaddress.ip_network(subnet, strict=False)
|
||||
hosts = [
|
||||
host for host in network.hosts() if isinstance(host, ipaddress.IPv4Address)
|
||||
]
|
||||
return cls(hosts)
|
||||
|
||||
async def scan(self) -> List[AnyMiner]:
|
||||
async def scan(self) -> list[AnyMiner]:
|
||||
"""Scan the network for miners.
|
||||
|
||||
Returns:
|
||||
@@ -121,15 +126,17 @@ class MinerNetwork:
|
||||
"""
|
||||
return await self.scan_network_for_miners()
|
||||
|
||||
async def scan_network_for_miners(self) -> List[AnyMiner]:
|
||||
async def scan_network_for_miners(self) -> list[AnyMiner]:
|
||||
logging.debug(f"{self} - (Scan Network For Miners) - Scanning")
|
||||
|
||||
miners = await asyncio.gather(
|
||||
raw_miners: list[AnyMiner | None] = await asyncio.gather(
|
||||
*[self.ping_and_get_miner(host) for host in self.hosts]
|
||||
)
|
||||
|
||||
# remove all None from the miner list
|
||||
miners = list(filter(None, miners))
|
||||
miners: list[AnyMiner] = cast(
|
||||
list[AnyMiner], [miner for miner in raw_miners if miner is not None]
|
||||
)
|
||||
logging.debug(
|
||||
f"{self} - (Scan Network For Miners) - Found {len(miners)} miners"
|
||||
)
|
||||
@@ -137,7 +144,7 @@ class MinerNetwork:
|
||||
# return the miner objects
|
||||
return miners
|
||||
|
||||
async def scan_network_generator(self) -> AsyncIterator[AnyMiner]:
|
||||
async def scan_network_generator(self) -> AsyncIterator[AnyMiner | None]:
|
||||
"""
|
||||
Scan the network for miners using an async generator.
|
||||
|
||||
@@ -145,39 +152,47 @@ class MinerNetwork:
|
||||
An asynchronous generator containing found miners.
|
||||
"""
|
||||
# create a list of scan tasks
|
||||
miners = asyncio.as_completed(
|
||||
[asyncio.create_task(self.ping_and_get_miner(host)) for host in self.hosts]
|
||||
)
|
||||
for miner in miners:
|
||||
tasks: list[asyncio.Task[AnyMiner | None]] = [
|
||||
asyncio.create_task(self.ping_and_get_miner(host)) for host in self.hosts
|
||||
]
|
||||
for miner in asyncio.as_completed(tasks):
|
||||
try:
|
||||
yield await miner
|
||||
result = await miner
|
||||
yield result
|
||||
except TimeoutError:
|
||||
yield None
|
||||
return
|
||||
|
||||
async def ping_and_get_miner(
|
||||
self, ip: ipaddress.ip_address
|
||||
) -> Union[None, AnyMiner]:
|
||||
self, ip: ipaddress.IPv4Address | ipaddress.IPv6Address
|
||||
) -> AnyMiner | None:
|
||||
if settings.get("network_scan_semaphore") is None:
|
||||
return await self._ping_and_get_miner(ip)
|
||||
return await self._ping_and_get_miner(ip) # type: ignore[func-returns-value]
|
||||
async with self.semaphore:
|
||||
return await self._ping_and_get_miner(ip)
|
||||
return await self._ping_and_get_miner(ip) # type: ignore[func-returns-value]
|
||||
|
||||
@staticmethod
|
||||
async def _ping_and_get_miner(ip: ipaddress.ip_address) -> Union[None, AnyMiner]:
|
||||
async def _ping_and_get_miner(
|
||||
ip: ipaddress.IPv4Address | ipaddress.IPv6Address,
|
||||
) -> AnyMiner | None:
|
||||
try:
|
||||
return await ping_and_get_miner(ip)
|
||||
return await ping_and_get_miner(ip) # type: ignore[func-returns-value]
|
||||
except ConnectionRefusedError:
|
||||
tasks = [ping_and_get_miner(ip, port=port) for port in [4028, 4029, 8889]]
|
||||
tasks: list[asyncio.Task[AnyMiner | None]] = [
|
||||
asyncio.create_task(ping_and_get_miner(ip, port=port))
|
||||
for port in [4028, 4029, 8889]
|
||||
]
|
||||
for miner in asyncio.as_completed(tasks):
|
||||
try:
|
||||
return await miner
|
||||
except ConnectionRefusedError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def ping_and_get_miner(
|
||||
ip: ipaddress.ip_address, port=80
|
||||
) -> Union[None, AnyMiner]:
|
||||
ip: ipaddress.IPv4Address | ipaddress.IPv6Address, port: int = 80
|
||||
) -> AnyMiner | None:
|
||||
for _ in range(settings.get("network_ping_retries", 1)):
|
||||
try:
|
||||
connection_fut = asyncio.open_connection(str(ip), port)
|
||||
@@ -190,7 +205,7 @@ async def ping_and_get_miner(
|
||||
# make sure the writer is closed
|
||||
await writer.wait_closed()
|
||||
# ping was successful
|
||||
return await miner_factory.get_miner(ip)
|
||||
return await miner_factory.get_miner(ip) # type: ignore[func-returns-value]
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
# ping failed if we time out
|
||||
continue
|
||||
@@ -198,11 +213,11 @@ async def ping_and_get_miner(
|
||||
raise ConnectionRefusedError from e
|
||||
except Exception as e:
|
||||
logging.warning(f"{str(ip)}: Unhandled ping exception: {e}")
|
||||
return
|
||||
return
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def compute_oct_range(octet: str) -> tuple:
|
||||
def compute_oct_range(octet: str) -> tuple[int, int]:
|
||||
octet_split = octet.split("-")
|
||||
octet_start = int(octet_split[0])
|
||||
octet_end = None
|
||||
|
||||
@@ -20,7 +20,6 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
from typing import Union
|
||||
|
||||
from pyasic.errors import APIError, APIWarning
|
||||
from pyasic.misc import validate_command_output
|
||||
@@ -35,7 +34,7 @@ class BaseMinerRPCAPI:
|
||||
# api version if known
|
||||
self.api_ver = api_ver
|
||||
|
||||
self.pwd = None
|
||||
self.pwd: str | None = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is BaseMinerRPCAPI:
|
||||
@@ -47,8 +46,8 @@ class BaseMinerRPCAPI:
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: Union[str, bytes],
|
||||
parameters: Union[str, int, bool] = None,
|
||||
command: str,
|
||||
parameters: str | int | bool | None = None,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
**kwargs,
|
||||
@@ -86,10 +85,10 @@ class BaseMinerRPCAPI:
|
||||
raise APIError(data.decode("utf-8"))
|
||||
return {}
|
||||
|
||||
data = self._load_api_data(data)
|
||||
api_data = self._load_api_data(data)
|
||||
|
||||
# check for if the user wants to allow errors to return
|
||||
validation = validate_command_output(data)
|
||||
validation = validate_command_output(api_data)
|
||||
if not validation[0]:
|
||||
if not ignore_errors:
|
||||
# validate the command succeeded
|
||||
@@ -100,7 +99,7 @@ class BaseMinerRPCAPI:
|
||||
)
|
||||
|
||||
logging.debug(f"{self} - (Send Command) - Received data.")
|
||||
return data
|
||||
return api_data
|
||||
|
||||
# Privileged command handler, only used by whatsminers, defined here for consistency.
|
||||
async def send_privileged_command(self, *args, **kwargs) -> dict:
|
||||
@@ -115,10 +114,10 @@ class BaseMinerRPCAPI:
|
||||
|
||||
"""
|
||||
# make sure we can actually run each command, otherwise they will fail
|
||||
commands = self._check_commands(*commands)
|
||||
valid_commands = self._check_commands(*commands)
|
||||
# standard multicommand format is "command1+command2"
|
||||
# doesn't work for S19 which uses the backup _send_split_multicommand
|
||||
command = "+".join(commands)
|
||||
command = "+".join(valid_commands)
|
||||
try:
|
||||
data = await self.send_command(command, allow_warning=allow_warning)
|
||||
except APIError:
|
||||
@@ -136,17 +135,16 @@ class BaseMinerRPCAPI:
|
||||
self.send_command(cmd, allow_warning=allow_warning)
|
||||
)
|
||||
|
||||
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
|
||||
results = await asyncio.gather(
|
||||
*[tasks[cmd] for cmd in tasks], return_exceptions=True
|
||||
)
|
||||
|
||||
data = {}
|
||||
for cmd in tasks:
|
||||
try:
|
||||
result = tasks[cmd].result()
|
||||
for cmd, result in zip(tasks.keys(), results):
|
||||
if not isinstance(result, (APIError, Exception)):
|
||||
if result is None or result == {}:
|
||||
result = {}
|
||||
data[cmd] = [result]
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
return data
|
||||
|
||||
@@ -165,10 +163,13 @@ class BaseMinerRPCAPI:
|
||||
for func in
|
||||
# each function in self
|
||||
dir(self)
|
||||
if not func in ["commands", "open_api"]
|
||||
if callable(getattr(self, func)) and
|
||||
if func not in ["commands", "open_api"]
|
||||
if callable(getattr(self, func))
|
||||
and
|
||||
# no __ or _ methods
|
||||
not func.startswith("__") and not func.startswith("_") and
|
||||
not func.startswith("__")
|
||||
and not func.startswith("_")
|
||||
and
|
||||
# remove all functions that are in this base class
|
||||
func
|
||||
not in [
|
||||
@@ -197,7 +198,7 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
self,
|
||||
data: bytes,
|
||||
*,
|
||||
port: int = None,
|
||||
port: int | None = None,
|
||||
timeout: int = 100,
|
||||
) -> bytes:
|
||||
if port is None:
|
||||
@@ -253,10 +254,10 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
# some json from the API returns with a null byte (\x00) on the end
|
||||
if data.endswith(b"\x00"):
|
||||
# handle the null byte
|
||||
str_data = data.decode("utf-8")[:-1]
|
||||
str_data = data.decode("utf-8", errors="replace")[:-1]
|
||||
else:
|
||||
# no null byte
|
||||
str_data = data.decode("utf-8")
|
||||
str_data = data.decode("utf-8", errors="replace")
|
||||
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
||||
str_data = str_data.replace(",}", "}")
|
||||
# fix an error with a btminer return having a newline that breaks json.loads()
|
||||
|
||||
@@ -151,7 +151,7 @@ class BFGMinerRPCAPI(CGMinerRPCAPI):
|
||||
"""
|
||||
return await self.send_command("procidentify", parameters=n)
|
||||
|
||||
async def procset(self, n: int, opt: str, val: int = None) -> dict:
|
||||
async def procset(self, n: int, opt: str, val: int | None = None) -> dict:
|
||||
"""Set processor option opt to val on processor n.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
@@ -23,13 +23,15 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import struct
|
||||
import typing
|
||||
import warnings
|
||||
from asyncio import Future, StreamReader, StreamWriter
|
||||
from typing import Any, AsyncGenerator, Callable, Literal, Union
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any, Literal
|
||||
|
||||
import httpx
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from passlib.handlers.md5_crypt import md5_crypt
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic.errors import APIError, APIWarning
|
||||
@@ -44,11 +46,54 @@ from pyasic.rpc.base import BaseMinerRPCAPI
|
||||
# you change the password, you can pass that to this class as pwd,
|
||||
# or add it as the Whatsminer_pwd in the settings.toml file.
|
||||
|
||||
PrePowerOnMessage = Union[
|
||||
Literal["wait for adjust temp"],
|
||||
Literal["adjust complete"],
|
||||
Literal["adjust continue"],
|
||||
]
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
salt: str
|
||||
time: str
|
||||
newsalt: str
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
host_sign: str
|
||||
host_passwd_md5: str
|
||||
timestamp: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
||||
|
||||
|
||||
class BTMinerPrivilegedCommand(BaseModel):
|
||||
cmd: str
|
||||
token: str
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class BTMinerV3Command(BaseModel):
|
||||
cmd: str
|
||||
param: Any | None = None
|
||||
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class BTMinerV3PrivilegedCommand(BaseModel):
|
||||
cmd: str
|
||||
param: Any | None = None
|
||||
ts: int
|
||||
account: str
|
||||
token: str
|
||||
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
PrePowerOnMessage = (
|
||||
Literal["wait for adjust temp"]
|
||||
| Literal["adjust complete"]
|
||||
| Literal["adjust continue"]
|
||||
)
|
||||
|
||||
|
||||
def _crypt(word: str, salt: str) -> str:
|
||||
@@ -93,7 +138,7 @@ def _add_to_16(string: str) -> bytes:
|
||||
return str.encode(string) # return bytes
|
||||
|
||||
|
||||
def parse_btminer_priviledge_data(token_data: dict, data: dict) -> dict:
|
||||
def parse_btminer_priviledge_data(token_data: TokenData, data: dict) -> dict:
|
||||
"""Parses data returned from the BTMiner privileged API.
|
||||
|
||||
Parses data from the BTMiner privileged API using the token
|
||||
@@ -109,9 +154,9 @@ def parse_btminer_priviledge_data(token_data: dict, data: dict) -> dict:
|
||||
# get the encoded data from the dict
|
||||
enc_data = data["enc"]
|
||||
# get the aes key from the token data
|
||||
aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest()
|
||||
aeskey_hex = hashlib.sha256(token_data.host_passwd_md5.encode()).hexdigest()
|
||||
# unhexlify the aes key
|
||||
aeskey = binascii.unhexlify(aeskey.encode())
|
||||
aeskey = binascii.unhexlify(aeskey_hex.encode())
|
||||
# create the required decryptor
|
||||
aes = Cipher(algorithms.AES(aeskey), modes.ECB())
|
||||
decryptor = aes.decryptor()
|
||||
@@ -124,7 +169,7 @@ def parse_btminer_priviledge_data(token_data: dict, data: dict) -> dict:
|
||||
return ret_msg
|
||||
|
||||
|
||||
def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
|
||||
def create_privileged_cmd(token_data: TokenData, command: dict) -> bytes:
|
||||
"""Create a privileged command to send to the BTMiner API.
|
||||
|
||||
Creates a privileged command using the token from the API and the
|
||||
@@ -138,13 +183,13 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
|
||||
Returns:
|
||||
The encrypted privileged command to be sent to the miner.
|
||||
"""
|
||||
logging.debug(f"(Create Prilileged Command) - Creating Privileged Command")
|
||||
logging.debug("(Create Prilileged Command) - Creating Privileged Command")
|
||||
# add token to command
|
||||
command["token"] = token_data["host_sign"]
|
||||
command["token"] = token_data.host_sign
|
||||
# encode host_passwd data and get hexdigest
|
||||
aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest()
|
||||
aeskey_hex = hashlib.sha256(token_data.host_passwd_md5.encode()).hexdigest()
|
||||
# unhexlify the encoded host_passwd
|
||||
aeskey = binascii.unhexlify(aeskey.encode())
|
||||
aeskey = binascii.unhexlify(aeskey_hex.encode())
|
||||
# create a new AES key
|
||||
aes = Cipher(algorithms.AES(aeskey), modes.ECB())
|
||||
encryptor = aes.encryptor()
|
||||
@@ -188,8 +233,8 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
|
||||
def __init__(self, ip: str, port: int = 4028, api_ver: str = "0.0.0") -> None:
|
||||
super().__init__(ip, port, api_ver)
|
||||
self.pwd = settings.get("default_whatsminer_rpc_password", "admin")
|
||||
self.token = None
|
||||
self.pwd: str = settings.get("default_whatsminer_rpc_password", "admin")
|
||||
self.token: TokenData | None = None
|
||||
|
||||
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
|
||||
"""Creates and sends multiple commands as one command to the miner.
|
||||
@@ -199,19 +244,19 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
allow_warning: A boolean to supress APIWarnings.
|
||||
"""
|
||||
# make sure we can actually run each command, otherwise they will fail
|
||||
commands = self._check_commands(*commands)
|
||||
commands_list = self._check_commands(*commands)
|
||||
# standard multicommand format is "command1+command2"
|
||||
# commands starting with "get_" and the "status" command aren't supported, but we can fake that
|
||||
|
||||
split_commands = []
|
||||
|
||||
for command in list(commands):
|
||||
for command in commands_list:
|
||||
if command.startswith("get_") or command == "status":
|
||||
commands.remove(command)
|
||||
commands_list.remove(command)
|
||||
# send seperately and append later
|
||||
split_commands.append(command)
|
||||
|
||||
command = "+".join(commands)
|
||||
command = "+".join(commands_list)
|
||||
|
||||
tasks = []
|
||||
if len(split_commands) > 0:
|
||||
@@ -240,7 +285,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
|
||||
async def send_privileged_command(
|
||||
self,
|
||||
command: Union[str, bytes],
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
timeout: int = 10,
|
||||
**kwargs,
|
||||
@@ -252,17 +297,19 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
except APIError as e:
|
||||
if not e.message == "can't access write cmd":
|
||||
raise
|
||||
try:
|
||||
await self.open_api()
|
||||
except Exception as e:
|
||||
raise APIError("Failed to open whatsminer API.") from e
|
||||
return await self._send_privileged_command(
|
||||
command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
|
||||
)
|
||||
# If we get here, we caught the specific error but didn't handle it
|
||||
raise
|
||||
# try:
|
||||
# await self.open_api()
|
||||
# except Exception as e:
|
||||
# raise APIError("Failed to open whatsminer API.") from e
|
||||
# return await self._send_privileged_command(
|
||||
# command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
|
||||
# )
|
||||
|
||||
async def _send_privileged_command(
|
||||
self,
|
||||
command: Union[str, bytes],
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
timeout: int = 10,
|
||||
**kwargs,
|
||||
@@ -272,10 +319,10 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
if len(kwargs) > 0
|
||||
else ""
|
||||
)
|
||||
command = {"cmd": command, **kwargs}
|
||||
cmd = {"cmd": command, **kwargs}
|
||||
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
enc_command = create_privileged_cmd(token_data, cmd)
|
||||
|
||||
logging.debug(f"{self} - (Send Privileged Command) - Sending")
|
||||
try:
|
||||
@@ -289,23 +336,23 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
if ignore_errors:
|
||||
return {}
|
||||
raise APIError("No data was returned from the API.")
|
||||
data = self._load_api_data(data)
|
||||
data_dict: dict[Any, Any] = self._load_api_data(data)
|
||||
|
||||
try:
|
||||
data = parse_btminer_priviledge_data(self.token, data)
|
||||
data_dict = parse_btminer_priviledge_data(token_data, data_dict)
|
||||
except Exception as e:
|
||||
logging.info(f"{str(self.ip)}: {e}")
|
||||
|
||||
if not ignore_errors:
|
||||
# if it fails to validate, it is likely an error
|
||||
validation = validate_command_output(data)
|
||||
validation = validate_command_output(data_dict)
|
||||
if not validation[0]:
|
||||
raise APIError(validation[1])
|
||||
|
||||
# return the parsed json as a dict
|
||||
return data
|
||||
return data_dict
|
||||
|
||||
async def get_token(self) -> dict:
|
||||
async def get_token(self) -> TokenData:
|
||||
"""Gets token information from the API.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
@@ -316,7 +363,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
logging.debug(f"{self} - (Get Token) - Getting token")
|
||||
if self.token:
|
||||
if self.token["timestamp"] > datetime.datetime.now() - datetime.timedelta(
|
||||
if self.token.timestamp > datetime.datetime.now() - datetime.timedelta(
|
||||
minutes=30
|
||||
):
|
||||
return self.token
|
||||
@@ -324,26 +371,30 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
# get the token
|
||||
data = await self.send_command("get_token")
|
||||
|
||||
token_response = TokenResponse.model_validate(data["Msg"])
|
||||
|
||||
# encrypt the admin password with the salt
|
||||
pwd = _crypt(self.pwd, "$1$" + data["Msg"]["salt"] + "$")
|
||||
pwd = pwd.split("$")
|
||||
pwd_str = _crypt(self.pwd, "$1$" + token_response.salt + "$")
|
||||
pwd_parts = pwd_str.split("$")
|
||||
|
||||
# take the 4th item from the pwd split
|
||||
host_passwd_md5 = pwd[3]
|
||||
host_passwd_md5 = pwd_parts[3]
|
||||
|
||||
# encrypt the pwd with the time and new salt
|
||||
tmp = _crypt(pwd[3] + data["Msg"]["time"], "$1$" + data["Msg"]["newsalt"] + "$")
|
||||
tmp = tmp.split("$")
|
||||
tmp_str = _crypt(
|
||||
pwd_parts[3] + token_response.time, "$1$" + token_response.newsalt + "$"
|
||||
)
|
||||
tmp_parts = tmp_str.split("$")
|
||||
|
||||
# take the 4th item from the encrypted pwd split
|
||||
host_sign = tmp[3]
|
||||
host_sign = tmp_parts[3]
|
||||
|
||||
# set the current token
|
||||
self.token = {
|
||||
"host_sign": host_sign,
|
||||
"host_passwd_md5": host_passwd_md5,
|
||||
"timestamp": datetime.datetime.now(),
|
||||
}
|
||||
self.token = TokenData(
|
||||
host_sign=host_sign,
|
||||
host_passwd_md5=host_passwd_md5,
|
||||
timestamp=datetime.datetime.now(),
|
||||
)
|
||||
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}")
|
||||
return self.token
|
||||
|
||||
@@ -387,12 +438,12 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
pool_1: str,
|
||||
worker_1: str,
|
||||
passwd_1: str,
|
||||
pool_2: str = None,
|
||||
worker_2: str = None,
|
||||
passwd_2: str = None,
|
||||
pool_3: str = None,
|
||||
worker_3: str = None,
|
||||
passwd_3: str = None,
|
||||
pool_2: str | None = None,
|
||||
worker_2: str | None = None,
|
||||
passwd_2: str | None = None,
|
||||
pool_3: str | None = None,
|
||||
worker_3: str | None = None,
|
||||
passwd_3: str | None = None,
|
||||
) -> dict:
|
||||
"""Update the pools of the miner using the API.
|
||||
<details>
|
||||
@@ -649,11 +700,11 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
|
||||
async def net_config(
|
||||
self,
|
||||
ip: str = None,
|
||||
mask: str = None,
|
||||
gate: str = None,
|
||||
dns: str = None,
|
||||
host: str = None,
|
||||
ip: str | None = None,
|
||||
mask: str | None = None,
|
||||
gate: str | None = None,
|
||||
dns: str | None = None,
|
||||
host: str | None = None,
|
||||
dhcp: bool = True,
|
||||
):
|
||||
if dhcp:
|
||||
@@ -682,9 +733,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
if not -100 < percent < 100:
|
||||
raise APIError(
|
||||
f"Frequency % is outside of the allowed "
|
||||
f"range. Please set a % between -100 and "
|
||||
f"100"
|
||||
"Frequency % is outside of the allowed "
|
||||
"range. Please set a % between -100 and "
|
||||
"100"
|
||||
)
|
||||
return await self.send_privileged_command(
|
||||
"set_target_freq", percent=str(percent)
|
||||
@@ -785,9 +836,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
|
||||
if not 0 < percent < 100:
|
||||
raise APIError(
|
||||
f"Power PCT % is outside of the allowed "
|
||||
f"range. Please set a % between 0 and "
|
||||
f"100"
|
||||
"Power PCT % is outside of the allowed "
|
||||
"range. Please set a % between 0 and "
|
||||
"100"
|
||||
)
|
||||
return await self.send_privileged_command("set_power_pct", percent=str(percent))
|
||||
|
||||
@@ -845,9 +896,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
|
||||
if not 0 < percent < 100:
|
||||
raise APIError(
|
||||
f"Power PCT % is outside of the allowed "
|
||||
f"range. Please set a % between 0 and "
|
||||
f"100"
|
||||
"Power PCT % is outside of the allowed "
|
||||
"range. Please set a % between 0 and "
|
||||
"100"
|
||||
)
|
||||
return await self.send_privileged_command(
|
||||
"set_power_pct_v2", percent=str(percent)
|
||||
@@ -872,9 +923,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
if not -30 < temp_offset < 0:
|
||||
raise APIError(
|
||||
f"Temp offset is outside of the allowed "
|
||||
f"range. Please set a number between -30 and "
|
||||
f"0."
|
||||
"Temp offset is outside of the allowed "
|
||||
"range. Please set a number between -30 and "
|
||||
"0."
|
||||
)
|
||||
|
||||
return await self.send_privileged_command(
|
||||
@@ -923,9 +974,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
if not 0 < upfreq_speed < 9:
|
||||
raise APIError(
|
||||
f"Upfreq speed is outside of the allowed "
|
||||
f"range. Please set a number between 0 (Normal) and "
|
||||
f"9 (Fastest)."
|
||||
"Upfreq speed is outside of the allowed "
|
||||
"range. Please set a number between 0 (Normal) and "
|
||||
"9 (Fastest)."
|
||||
)
|
||||
return await self.send_privileged_command(
|
||||
"adjust_upfreq_speed", upfreq_speed=upfreq_speed
|
||||
@@ -1108,7 +1159,8 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
|
||||
def __init__(self, ip: str, port: int = 4433, api_ver: str = "0.0.0"):
|
||||
super().__init__(ip, port, api_ver=api_ver)
|
||||
|
||||
self.salt = None
|
||||
self.salt: str | None = None
|
||||
self.pwd: str = "super"
|
||||
|
||||
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
|
||||
"""Creates and sends multiple commands as one command to the miner.
|
||||
@@ -1118,45 +1170,52 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
|
||||
allow_warning: A boolean to supress APIWarnings.
|
||||
|
||||
"""
|
||||
commands = self._check_commands(*commands)
|
||||
data = await self._send_split_multicommand(*commands)
|
||||
checked_commands = self._check_commands(*commands)
|
||||
data = await self._send_split_multicommand(*checked_commands)
|
||||
data["multicommand"] = True
|
||||
return data
|
||||
|
||||
async def send_command(
|
||||
self, command: str, parameters: Any = None, **kwargs
|
||||
self,
|
||||
command: str,
|
||||
parameters: Any = None,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
if ":" in command:
|
||||
parameters = command.split(":")[1]
|
||||
command = command.split(":")[0]
|
||||
cmd = {"cmd": command}
|
||||
if parameters is not None:
|
||||
cmd["param"] = parameters
|
||||
|
||||
cmd: BTMinerV3Command | BTMinerV3PrivilegedCommand
|
||||
|
||||
if command.startswith("set."):
|
||||
salt = await self.get_salt()
|
||||
ts = int(datetime.datetime.now().timestamp())
|
||||
cmd["ts"] = ts
|
||||
token_str = cmd["cmd"] + self.pwd + salt + str(ts)
|
||||
token_str = command + self.pwd + salt + str(ts)
|
||||
token_hashed = bytearray(
|
||||
base64.b64encode(hashlib.sha256(token_str.encode("utf-8")).digest())
|
||||
)
|
||||
token_hashed[8] = 0
|
||||
cmd["account"] = "super"
|
||||
cmd["token"] = token_hashed.decode("ascii")
|
||||
b_arr = bytearray(token_hashed)
|
||||
b_arr[8] = 0
|
||||
str_token = b_arr.split(b"\x00")[0].decode("utf-8")
|
||||
|
||||
# send the command
|
||||
ser = json.dumps(cmd).encode("utf-8")
|
||||
header = struct.pack("<I", len(ser))
|
||||
return json.loads(
|
||||
await self._send_bytes(header + json.dumps(cmd).encode("utf-8"))
|
||||
cmd = BTMinerV3PrivilegedCommand(
|
||||
cmd=command, param=parameters, ts=ts, account="super", token=str_token
|
||||
)
|
||||
else:
|
||||
cmd = BTMinerV3Command(cmd=command, param=parameters)
|
||||
|
||||
cmd_dict = cmd.model_dump()
|
||||
ser = json.dumps(cmd_dict).encode("utf-8")
|
||||
header = struct.pack("<I", len(ser))
|
||||
return json.loads(await self._send_bytes(header + ser))
|
||||
|
||||
async def _send_bytes(
|
||||
self,
|
||||
data: bytes,
|
||||
*,
|
||||
port: int = None,
|
||||
port: int | None = None,
|
||||
timeout: int = 100,
|
||||
) -> bytes:
|
||||
if port is None:
|
||||
@@ -1230,6 +1289,7 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
self.salt = data["msg"]["salt"]
|
||||
return self.salt
|
||||
|
||||
@typing.no_type_check
|
||||
async def get_miner_report(self) -> AsyncGenerator[dict, None]:
|
||||
if self.writer is None:
|
||||
await self.connect()
|
||||
|
||||
@@ -243,7 +243,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
return await self.send_command("removepool", parameters=n)
|
||||
|
||||
async def save(self, filename: str = None) -> dict:
|
||||
async def save(self, filename: str | None = None) -> dict:
|
||||
"""Save the config.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
@@ -484,7 +484,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
return await self.send_command("usbstats")
|
||||
|
||||
async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
|
||||
async def pgaset(self, n: int, opt: str, val: int | None = None) -> dict:
|
||||
"""Set PGA option opt to val on PGA n.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
@@ -611,7 +611,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
return await self.send_command("asccount")
|
||||
|
||||
async def ascset(self, n: int, opt: str, val: int = None) -> dict:
|
||||
async def ascset(self, n: int, opt: str, val: int | str | None = None) -> dict:
|
||||
"""Set ASC n option opt to value val.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from typing import Literal, Optional, Union
|
||||
from typing import Literal
|
||||
|
||||
from pyasic import APIError
|
||||
from pyasic.rpc.base import BaseMinerRPCAPI
|
||||
@@ -37,9 +37,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.session_token = None
|
||||
|
||||
async def send_privileged_command(
|
||||
self, command: Union[str, bytes], *args, **kwargs
|
||||
) -> dict:
|
||||
async def send_privileged_command(self, command: str, *args, **kwargs) -> dict:
|
||||
if self.session_token is None:
|
||||
await self.auth()
|
||||
return await self.send_command(
|
||||
@@ -51,7 +49,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: Union[str, bytes],
|
||||
command: str,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
@@ -59,7 +57,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
return await super().send_command(command, **kwargs)
|
||||
return await super().send_command(command, parameters=",".join(args), **kwargs)
|
||||
|
||||
async def auth(self) -> Optional[str]:
|
||||
async def auth(self) -> str | None:
|
||||
try:
|
||||
data = await self.session()
|
||||
if not data["SESSION"][0]["SessionID"] == "":
|
||||
@@ -74,6 +72,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
return self.session_token
|
||||
except (LookupError, APIError):
|
||||
pass
|
||||
return None
|
||||
|
||||
async def addgroup(self, name: str, quota: int) -> dict:
|
||||
"""Add a pool group.
|
||||
@@ -91,7 +90,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
return await self.send_command("addgroup", name, quota)
|
||||
|
||||
async def addpool(
|
||||
self, url: str, user: str, pwd: str = "", group_id: str = None
|
||||
self, url: str, user: str, pwd: str = "", group_id: str | None = None
|
||||
) -> dict:
|
||||
"""Add a pool.
|
||||
<details>
|
||||
@@ -163,13 +162,13 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
|
||||
async def atmset(
|
||||
self,
|
||||
enabled: bool = None,
|
||||
startup_minutes: int = None,
|
||||
post_ramp_minutes: int = None,
|
||||
temp_window: int = None,
|
||||
min_profile: str = None,
|
||||
max_profile: str = None,
|
||||
prevent_oc: bool = None,
|
||||
enabled: bool | None = None,
|
||||
startup_minutes: int | None = None,
|
||||
post_ramp_minutes: int | None = None,
|
||||
temp_window: int | None = None,
|
||||
min_profile: str | None = None,
|
||||
max_profile: str | None = None,
|
||||
prevent_oc: bool | None = None,
|
||||
) -> dict:
|
||||
"""Sets the ATM configuration.
|
||||
<details>
|
||||
@@ -357,7 +356,10 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
return await self.send_command("fans")
|
||||
|
||||
async def fanset(
|
||||
self, speed: int = None, min_fans: int = None, power_off_speed: int = None
|
||||
self,
|
||||
speed: int | None = None,
|
||||
min_fans: int | None = None,
|
||||
power_off_speed: int | None = None,
|
||||
) -> dict:
|
||||
"""Set fan control.
|
||||
<details>
|
||||
@@ -380,7 +382,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
fanset_data.append(f"power_off_speed={power_off_speed}")
|
||||
return await self.send_privileged_command("fanset", *fanset_data)
|
||||
|
||||
async def frequencyget(self, board_n: int, chip_n: int = None) -> dict:
|
||||
async def frequencyget(self, board_n: int, chip_n: int | None = None) -> dict:
|
||||
"""Get frequency data for a board and chips.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
@@ -453,7 +455,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
return await self.send_command("groups")
|
||||
|
||||
async def healthchipget(self, board_n: int, chip_n: int = None) -> dict:
|
||||
async def healthchipget(self, board_n: int, chip_n: int | None = None) -> dict:
|
||||
"""Get chip health.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
@@ -471,7 +473,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
healthchipget_data.append(str(chip_n))
|
||||
return await self.send_command("healthchipget", *healthchipget_data)
|
||||
|
||||
async def healthchipset(self, board_n: int, chip_n: int = None) -> dict:
|
||||
async def healthchipset(self, board_n: int, chip_n: int | None = None) -> dict:
|
||||
"""Select the next chip to have its health checked.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
@@ -641,7 +643,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
"""
|
||||
return await self.send_privileged_command("profileset", profile)
|
||||
|
||||
async def reboot(self, board_n: int, delay_s: int = None) -> dict:
|
||||
async def reboot(self, board_n: int, delay_s: int | None = None) -> dict:
|
||||
"""Reboot a board.
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
@@ -721,7 +723,10 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
|
||||
return await self.send_command("session")
|
||||
|
||||
async def tempctrlset(
|
||||
self, target: int = None, hot: int = None, dangerous: int = None
|
||||
self,
|
||||
target: int | None = None,
|
||||
hot: int | None = None,
|
||||
dangerous: int | None = None,
|
||||
) -> dict:
|
||||
"""Set temp control values.
|
||||
<details>
|
||||
|
||||
@@ -14,37 +14,45 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from ssl import SSLContext
|
||||
from typing import Any, Union
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from httpx import AsyncHTTPTransport
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
_settings = { # defaults
|
||||
"network_ping_retries": 1,
|
||||
"network_ping_timeout": 3,
|
||||
"network_scan_semaphore": None,
|
||||
"factory_get_retries": 1,
|
||||
"factory_get_timeout": 3,
|
||||
"get_data_retries": 1,
|
||||
"api_function_timeout": 5,
|
||||
"antminer_mining_mode_as_str": False,
|
||||
"default_whatsminer_rpc_password": "admin",
|
||||
"default_innosilicon_web_password": "admin",
|
||||
"default_antminer_web_password": "root",
|
||||
"default_hammer_web_password": "root",
|
||||
"default_volcminer_web_password": "ltc@dog",
|
||||
"default_bosminer_web_password": "root",
|
||||
"default_vnish_web_password": "admin",
|
||||
"default_goldshell_web_password": "123456789",
|
||||
"default_auradine_web_password": "admin",
|
||||
"default_epic_web_password": "letmein",
|
||||
"default_hive_web_password": "root",
|
||||
"default_iceriver_web_password": "12345678",
|
||||
"default_elphapex_web_password": "root",
|
||||
"default_mskminer_web_password": "root",
|
||||
"default_antminer_ssh_password": "miner",
|
||||
"default_bosminer_ssh_password": "root",
|
||||
}
|
||||
|
||||
class Settings(BaseModel):
|
||||
network_ping_retries: int = Field(default=1)
|
||||
network_ping_timeout: int = Field(default=3)
|
||||
network_scan_semaphore: int | None = Field(default=None)
|
||||
factory_get_retries: int = Field(default=1)
|
||||
factory_get_timeout: int = Field(default=3)
|
||||
get_data_retries: int = Field(default=1)
|
||||
api_function_timeout: int = Field(default=5)
|
||||
antminer_mining_mode_as_str: bool = Field(default=False)
|
||||
default_whatsminer_rpc_password: str = Field(default="admin")
|
||||
default_innosilicon_web_password: str = Field(default="admin")
|
||||
default_antminer_web_password: str = Field(default="root")
|
||||
default_hammer_web_password: str = Field(default="root")
|
||||
default_volcminer_web_password: str = Field(default="ltc@dog")
|
||||
default_bosminer_web_password: str = Field(default="root")
|
||||
default_vnish_web_password: str = Field(default="admin")
|
||||
default_goldshell_web_password: str = Field(default="123456789")
|
||||
default_auradine_web_password: str = Field(default="admin")
|
||||
default_epic_web_password: str = Field(default="letmein")
|
||||
default_hive_web_password: str = Field(default="root")
|
||||
default_iceriver_web_password: str = Field(default="12345678")
|
||||
default_elphapex_web_password: str = Field(default="root")
|
||||
default_mskminer_web_password: str = Field(default="root")
|
||||
default_antminer_ssh_password: str = Field(default="miner")
|
||||
default_bosminer_ssh_password: str = Field(default="root")
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
extra = "allow"
|
||||
|
||||
|
||||
_settings = Settings()
|
||||
|
||||
|
||||
ssl_cxt = httpx.create_ssl_context()
|
||||
@@ -52,13 +60,21 @@ ssl_cxt = httpx.create_ssl_context()
|
||||
|
||||
# this function returns an AsyncHTTPTransport instance to perform asynchronous HTTP requests
|
||||
# using those options.
|
||||
def transport(verify: Union[str, bool, SSLContext] = ssl_cxt):
|
||||
def transport(verify: str | bool | SSLContext = ssl_cxt):
|
||||
return AsyncHTTPTransport(verify=verify)
|
||||
|
||||
|
||||
def get(key: str, other: Any = None) -> Any:
|
||||
return _settings.get(key, other)
|
||||
try:
|
||||
return getattr(_settings, key)
|
||||
except AttributeError:
|
||||
if hasattr(_settings, "__dict__") and key in _settings.__dict__:
|
||||
return _settings.__dict__[key]
|
||||
return other
|
||||
|
||||
|
||||
def update(key: str, val: Any) -> Any:
|
||||
_settings[key] = val
|
||||
def update(key: str, val: Any) -> None:
|
||||
if hasattr(_settings, key):
|
||||
setattr(_settings, key, val)
|
||||
else:
|
||||
_settings.__dict__[key] = val
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import asyncssh
|
||||
|
||||
@@ -8,9 +7,9 @@ import asyncssh
|
||||
class BaseSSH:
|
||||
def __init__(self, ip: str) -> None:
|
||||
self.ip = ip
|
||||
self.pwd = None
|
||||
self.username = "root"
|
||||
self.port = 22
|
||||
self.pwd: str | None = None
|
||||
self.username: str = "root"
|
||||
self.port: int = 22
|
||||
|
||||
async def _get_connection(self) -> asyncssh.connect:
|
||||
"""Create a new asyncssh connection"""
|
||||
@@ -29,7 +28,7 @@ class BaseSSH:
|
||||
except Exception as e:
|
||||
raise ConnectionError from e
|
||||
|
||||
async def send_command(self, cmd: str) -> Optional[str]:
|
||||
async def send_command(self, cmd: str) -> str | None:
|
||||
"""Send an ssh command to the miner"""
|
||||
try:
|
||||
conn = await asyncio.wait_for(self._get_connection(), timeout=10)
|
||||
|
||||
@@ -24,6 +24,7 @@ import aiofiles
|
||||
import httpx
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.web.base import BaseWebAPI
|
||||
|
||||
|
||||
@@ -35,12 +36,12 @@ class AntminerModernWebAPI(BaseWebAPI):
|
||||
ip (str): IP address of the Antminer device.
|
||||
"""
|
||||
super().__init__(ip)
|
||||
self.username = "root"
|
||||
self.pwd = settings.get("default_antminer_web_password", "root")
|
||||
self.username: str = "root"
|
||||
self.pwd: str = settings.get("default_antminer_web_password", "root")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -49,7 +50,7 @@ class AntminerModernWebAPI(BaseWebAPI):
|
||||
"""Send a command to the Antminer device using HTTP digest authentication.
|
||||
|
||||
Args:
|
||||
command (str | bytes): The CGI command to send.
|
||||
command (str): The CGI command to send.
|
||||
ignore_errors (bool): If True, ignore any HTTP errors.
|
||||
allow_warning (bool): If True, proceed with warnings.
|
||||
privileged (bool): If set to True, requires elevated privileges.
|
||||
@@ -249,12 +250,12 @@ class AntminerOldWebAPI(BaseWebAPI):
|
||||
ip (str): IP address of the Antminer device.
|
||||
"""
|
||||
super().__init__(ip)
|
||||
self.username = "root"
|
||||
self.pwd = settings.get("default_antminer_web_password", "root")
|
||||
self.username: str = "root"
|
||||
self.pwd: str = settings.get("default_antminer_web_password", "root")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -263,7 +264,7 @@ class AntminerOldWebAPI(BaseWebAPI):
|
||||
"""Send a command to the Antminer device using HTTP digest authentication.
|
||||
|
||||
Args:
|
||||
command (str | bytes): The CGI command to send.
|
||||
command (str): The CGI command to send.
|
||||
ignore_errors (bool): If True, ignore any HTTP errors.
|
||||
allow_warning (bool): If True, proceed with warnings.
|
||||
privileged (bool): If set to True, requires elevated privileges.
|
||||
@@ -293,6 +294,7 @@ class AntminerOldWebAPI(BaseWebAPI):
|
||||
return data.json()
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
raise APIError(f"Failed to send command to miner: {self}")
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
@@ -411,10 +413,9 @@ class AntminerOldWebAPI(BaseWebAPI):
|
||||
async with aiofiles.open(file, "rb") as firmware:
|
||||
file_content = await firmware.read()
|
||||
|
||||
parameters = {
|
||||
"file": (file.name, file_content, "application/octet-stream"),
|
||||
"filename": file.name,
|
||||
"keep_settings": keep_settings,
|
||||
}
|
||||
|
||||
return await self.send_command(command="upgrade", **parameters)
|
||||
return await self.send_command(
|
||||
command="upgrade",
|
||||
file=(file.name, file_content, "application/octet-stream"),
|
||||
filename=file.name,
|
||||
keep_settings=keep_settings,
|
||||
)
|
||||
|
||||
@@ -69,7 +69,7 @@ class AuradineWebAPI(BaseWebAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -78,7 +78,7 @@ class AuradineWebAPI(BaseWebAPI):
|
||||
"""Send a command to the Auradine miner, handling authentication and retries.
|
||||
|
||||
Args:
|
||||
command (str | bytes): The specific command to execute.
|
||||
command (str): The specific command to execute.
|
||||
ignore_errors (bool): Whether to ignore HTTP errors.
|
||||
allow_warning (bool): Whether to proceed with warnings.
|
||||
privileged (bool): Whether the command requires privileged access.
|
||||
@@ -95,6 +95,10 @@ class AuradineWebAPI(BaseWebAPI):
|
||||
await self.auth()
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
for i in range(settings.get("get_data_retries", 1)):
|
||||
if self.token is None:
|
||||
raise APIError(
|
||||
f"Could not authenticate web token with miner: {self}"
|
||||
)
|
||||
try:
|
||||
if post:
|
||||
response = await client.post(
|
||||
@@ -120,6 +124,7 @@ class AuradineWebAPI(BaseWebAPI):
|
||||
return json_data
|
||||
except (httpx.HTTPError, json.JSONDecodeError):
|
||||
pass
|
||||
raise APIError(f"Failed to send command to miner: {self}")
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
@@ -141,17 +146,14 @@ class AuradineWebAPI(BaseWebAPI):
|
||||
self.send_command(cmd, allow_warning=allow_warning)
|
||||
)
|
||||
|
||||
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
|
||||
results = 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: dict[str, Any] = {"multicommand": True}
|
||||
for cmd, result in zip(tasks.keys(), results):
|
||||
if isinstance(result, dict):
|
||||
data[cmd] = result
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
return data
|
||||
|
||||
@@ -183,7 +185,9 @@ class AuradineWebAPI(BaseWebAPI):
|
||||
"""
|
||||
return await self.send_command("fan", index=fan, percentage=speed_pct)
|
||||
|
||||
async def firmware_upgrade(self, url: str = None, version: str = "latest") -> dict:
|
||||
async def firmware_upgrade(
|
||||
self, url: str | None = None, version: str = "latest"
|
||||
) -> dict:
|
||||
"""Upgrade the firmware of the Auradine miner.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -34,20 +34,21 @@ class AvalonMinerWebAPI(BaseWebAPI):
|
||||
ip (str): IP address of the Avalonminer device.
|
||||
"""
|
||||
super().__init__(ip)
|
||||
self.username = "root"
|
||||
self.pwd = settings.get("default_avalonminer_web_password", "root")
|
||||
self.username: str = "root"
|
||||
self.pwd: str = settings.get("default_avalonminer_web_password", "root")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
**parameters: Any,
|
||||
) -> dict:
|
||||
"""Send a command to the Avalonminer device using HTTP digest authentication.
|
||||
|
||||
Args:
|
||||
command (str | bytes): The CGI command to send.
|
||||
command (str): The CGI command to send.
|
||||
ignore_errors (bool): If True, ignore any HTTP errors.
|
||||
allow_warning (bool): If True, proceed with warnings.
|
||||
**parameters: Arbitrary keyword arguments to be sent as parameters in the request.
|
||||
|
||||
@@ -26,24 +26,24 @@ class BaseWebAPI(ABC):
|
||||
def __init__(self, ip: str) -> None:
|
||||
# ip address of the miner
|
||||
self.ip = ip
|
||||
self.username = None
|
||||
self.pwd = None
|
||||
self.port = 80
|
||||
self.username: str | None = None
|
||||
self.pwd: str | None = None
|
||||
self.port: int = 80
|
||||
|
||||
self.token = None
|
||||
self.token: str | None = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> BaseWebAPI:
|
||||
if cls is BaseWebAPI:
|
||||
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
|
||||
return object.__new__(cls)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}: {str(self.ip)}"
|
||||
|
||||
@abstractmethod
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -57,7 +57,7 @@ class BaseWebAPI(ABC):
|
||||
) -> dict:
|
||||
pass
|
||||
|
||||
def _check_commands(self, *commands):
|
||||
def _check_commands(self, *commands: str) -> list[str]:
|
||||
allowed_commands = self.get_commands()
|
||||
return_commands = []
|
||||
for command in [*commands]:
|
||||
@@ -72,10 +72,10 @@ If you are sure you want to use this command please use WebAPI.send_command("{co
|
||||
return return_commands
|
||||
|
||||
@property
|
||||
def commands(self) -> list:
|
||||
def commands(self) -> list[str]:
|
||||
return self.get_commands()
|
||||
|
||||
def get_commands(self) -> list:
|
||||
def get_commands(self) -> list[str]:
|
||||
"""Get a list of command accessible to a specific type of web API on the miner.
|
||||
|
||||
Returns:
|
||||
@@ -87,9 +87,12 @@ If you are sure you want to use this command please use WebAPI.send_command("{co
|
||||
# each function in self
|
||||
dir(self)
|
||||
if not func == "commands"
|
||||
if callable(getattr(self, func)) and
|
||||
if callable(getattr(self, func))
|
||||
and
|
||||
# no __ or _ methods
|
||||
not func.startswith("__") and not func.startswith("_") and
|
||||
not func.startswith("__")
|
||||
and not func.startswith("_")
|
||||
and
|
||||
# remove all functions that are in this base class
|
||||
func
|
||||
not in [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from betterproto import DATETIME_ZERO, TYPE_MAP, TYPE_MESSAGE, Casing, Message
|
||||
|
||||
@@ -7,7 +7,7 @@ from betterproto import DATETIME_ZERO, TYPE_MAP, TYPE_MESSAGE, Casing, Message
|
||||
# https://github.com/danielgtaylor/python-betterproto/pull/609
|
||||
def to_pydict(
|
||||
self, casing: Casing = Casing.CAMEL, include_default_values: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Returns a python dict representation of this object.
|
||||
|
||||
@@ -23,10 +23,10 @@ def to_pydict(
|
||||
|
||||
Returns
|
||||
--------
|
||||
Dict[:class:`str`, Any]
|
||||
dict[:class:`str`, Any]
|
||||
The python dict representation of this object.
|
||||
"""
|
||||
output: Dict[str, Any] = {}
|
||||
output: dict[str, Any] = {}
|
||||
defaults = self._betterproto.default_gen
|
||||
for field_name, meta in self._betterproto.meta_by_field_name.items():
|
||||
field_is_repeated = defaults[field_name] is list
|
||||
|
||||
@@ -51,10 +51,10 @@ class BOSMinerGRPCStub(
|
||||
class BOSerWebAPI(BaseWebAPI):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.username = "root"
|
||||
self.pwd = settings.get("default_bosminer_password", "root")
|
||||
self.username: str = "root"
|
||||
self.pwd: str = settings.get("default_bosminer_password", "root")
|
||||
self.port = 50051
|
||||
self._auth_time = None
|
||||
self._auth_time: datetime | None = None
|
||||
|
||||
@property
|
||||
def commands(self) -> list:
|
||||
@@ -68,15 +68,17 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
dir(self)
|
||||
if func
|
||||
not in ["send_command", "multicommand", "auth", "commands", "get_commands"]
|
||||
if callable(getattr(self, func)) and
|
||||
if callable(getattr(self, func))
|
||||
and
|
||||
# no __ or _ methods
|
||||
not func.startswith("__") and not func.startswith("_")
|
||||
not func.startswith("__")
|
||||
and not func.startswith("_")
|
||||
]
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
) -> dict:
|
||||
result = {"multicommand": True}
|
||||
result: dict[str, Any] = {"multicommand": True}
|
||||
tasks = {}
|
||||
for command in commands:
|
||||
try:
|
||||
@@ -84,19 +86,19 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
await asyncio.gather(*[t for t in tasks.values()], return_exceptions=True)
|
||||
results = await asyncio.gather(
|
||||
*[t for t in tasks.values()], return_exceptions=True
|
||||
)
|
||||
|
||||
for cmd in tasks:
|
||||
try:
|
||||
result[cmd] = await tasks[cmd]
|
||||
except (GRPCError, APIError, ConnectionError):
|
||||
pass
|
||||
for cmd, task_result in zip(tasks.keys(), results):
|
||||
if isinstance(task_result, dict):
|
||||
result[cmd] = task_result
|
||||
|
||||
return result
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -125,14 +127,16 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
raise APIError(f"gRPC command failed - {endpoint}") from e
|
||||
|
||||
async def auth(self) -> str | None:
|
||||
if self.token is not None and self._auth_time - datetime.now() < timedelta(
|
||||
seconds=3540
|
||||
if (
|
||||
self.token is not None
|
||||
and self._auth_time is not None
|
||||
and datetime.now() - self._auth_time < timedelta(seconds=3540)
|
||||
):
|
||||
return self.token
|
||||
await self._get_auth()
|
||||
return self.token
|
||||
|
||||
async def _get_auth(self) -> str:
|
||||
async def _get_auth(self) -> str | None:
|
||||
async with Channel(self.ip, self.port) as c:
|
||||
req = LoginRequest(username=self.username, password=self.pwd)
|
||||
async with c.request(
|
||||
@@ -143,11 +147,13 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
) as stream:
|
||||
await stream.send_message(req, end=True)
|
||||
await stream.recv_initial_metadata()
|
||||
if stream.initial_metadata is not None:
|
||||
auth = stream.initial_metadata.get("authorization")
|
||||
if auth is not None:
|
||||
if auth is not None and isinstance(auth, str):
|
||||
self.token = auth
|
||||
self._auth_time = datetime.now()
|
||||
return self.token
|
||||
return None
|
||||
|
||||
async def get_api_version(self) -> dict:
|
||||
return await self.send_command(
|
||||
@@ -194,7 +200,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
privileged=True,
|
||||
)
|
||||
|
||||
async def set_password(self, password: str = None) -> dict:
|
||||
async def set_password(self, password: str | None = None) -> dict:
|
||||
return await self.send_command(
|
||||
"set_password",
|
||||
message=SetPasswordRequest(password=password),
|
||||
@@ -209,7 +215,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
async def set_immersion_mode(
|
||||
self,
|
||||
enable: bool,
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"set_immersion_mode",
|
||||
@@ -230,7 +236,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
)
|
||||
|
||||
async def set_default_power_target(
|
||||
self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
|
||||
self, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY)
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"set_default_power_target",
|
||||
@@ -241,7 +247,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
async def set_power_target(
|
||||
self,
|
||||
power_target: int,
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"set_power_target",
|
||||
@@ -254,7 +260,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
async def increment_power_target(
|
||||
self,
|
||||
power_target_increment: int,
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"increment_power_target",
|
||||
@@ -268,7 +274,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
async def decrement_power_target(
|
||||
self,
|
||||
power_target_decrement: int,
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"decrement_power_target",
|
||||
@@ -280,7 +286,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
)
|
||||
|
||||
async def set_default_hashrate_target(
|
||||
self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
|
||||
self, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY)
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"set_default_hashrate_target",
|
||||
@@ -291,7 +297,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
async def set_hashrate_target(
|
||||
self,
|
||||
hashrate_target: float,
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"set_hashrate_target",
|
||||
@@ -305,7 +311,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
async def increment_hashrate_target(
|
||||
self,
|
||||
hashrate_target_increment: int,
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"increment_hashrate_target",
|
||||
@@ -321,7 +327,7 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
async def decrement_hashrate_target(
|
||||
self,
|
||||
hashrate_target_decrement: int,
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"decrement_hashrate_target",
|
||||
@@ -339,15 +345,19 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
enable: bool,
|
||||
power_step: int,
|
||||
min_power_target: int,
|
||||
enable_shutdown: bool = None,
|
||||
shutdown_duration: int = None,
|
||||
enable_shutdown: bool | None = None,
|
||||
shutdown_duration: int | None = None,
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"set_dps",
|
||||
message=SetDpsRequest(
|
||||
enable=enable,
|
||||
enable_shutdown=enable_shutdown,
|
||||
shutdown_duration=shutdown_duration,
|
||||
shutdown_duration=(
|
||||
Hours(hours=shutdown_duration)
|
||||
if shutdown_duration is not None
|
||||
else None
|
||||
),
|
||||
target=DpsTarget(
|
||||
power_target=DpsPowerTarget(
|
||||
power_step=Power(power_step),
|
||||
@@ -360,47 +370,37 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
|
||||
async def set_performance_mode(
|
||||
self,
|
||||
wattage_target: int = None,
|
||||
hashrate_target: int = None,
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
wattage_target: int | None = None,
|
||||
hashrate_target: int | None = None,
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
if wattage_target is not None and hashrate_target is not None:
|
||||
logging.error(
|
||||
"Cannot use both wattage_target and hashrate_target, using wattage_target."
|
||||
)
|
||||
elif wattage_target is None and hashrate_target is None:
|
||||
hashrate_target = None
|
||||
|
||||
tuner_mode: TunerPerformanceMode
|
||||
if wattage_target is not None:
|
||||
tuner_mode = TunerPerformanceMode(
|
||||
power_target=PowerTargetMode(power_target=Power(watt=wattage_target))
|
||||
)
|
||||
elif hashrate_target is not None:
|
||||
tuner_mode = TunerPerformanceMode(
|
||||
hashrate_target=HashrateTargetMode(
|
||||
hashrate_target=TeraHashrate(terahash_per_second=hashrate_target)
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise APIError(
|
||||
"No target supplied, please supply either wattage_target or hashrate_target."
|
||||
)
|
||||
if wattage_target is not None:
|
||||
|
||||
return await self.send_command(
|
||||
"set_performance_mode",
|
||||
message=SetPerformanceModeRequest(
|
||||
save_action=save_action,
|
||||
mode=PerformanceMode(
|
||||
tuner_mode=TunerPerformanceMode(
|
||||
power_target=PowerTargetMode(
|
||||
power_target=Power(watt=wattage_target)
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
privileged=True,
|
||||
)
|
||||
if hashrate_target is not None:
|
||||
return await self.send_command(
|
||||
"set_performance_mode",
|
||||
message=SetPerformanceModeRequest(
|
||||
save_action=save_action,
|
||||
mode=PerformanceMode(
|
||||
tuner_mode=TunerPerformanceMode(
|
||||
hashrate_target=HashrateTargetMode(
|
||||
hashrate_target=TeraHashrate(
|
||||
terahash_per_second=hashrate_target
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
mode=PerformanceMode(tuner_mode=tuner_mode),
|
||||
),
|
||||
privileged=True,
|
||||
)
|
||||
@@ -461,8 +461,8 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
|
||||
async def enable_hashboards(
|
||||
self,
|
||||
hashboard_ids: List[str],
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
hashboard_ids: list[str],
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"enable_hashboards",
|
||||
@@ -474,8 +474,8 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
|
||||
async def disable_hashboards(
|
||||
self,
|
||||
hashboard_ids: List[str],
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
hashboard_ids: list[str],
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"disable_hashboards",
|
||||
@@ -487,8 +487,8 @@ class BOSerWebAPI(BaseWebAPI):
|
||||
|
||||
async def set_pool_groups(
|
||||
self,
|
||||
pool_groups: List[PoolGroupConfiguration],
|
||||
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
|
||||
pool_groups: list[PoolGroupConfiguration],
|
||||
save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
|
||||
) -> dict:
|
||||
return await self.send_command(
|
||||
"set_pool_groups",
|
||||
|
||||
@@ -34,7 +34,7 @@ class BOSMinerWebAPI(BaseWebAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
|
||||
@@ -33,12 +33,12 @@ class ElphapexWebAPI(BaseWebAPI):
|
||||
ip (str): IP address of the Elphapex device.
|
||||
"""
|
||||
super().__init__(ip)
|
||||
self.username = "root"
|
||||
self.pwd = settings.get("default_elphapex_web_password", "root")
|
||||
self.username: str = "root"
|
||||
self.pwd: str = settings.get("default_elphapex_web_password", "root")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -47,7 +47,7 @@ class ElphapexWebAPI(BaseWebAPI):
|
||||
"""Send a command to the Elphapex device using HTTP digest authentication.
|
||||
|
||||
Args:
|
||||
command (str | bytes): The CGI command to send.
|
||||
command (str): The CGI command to send.
|
||||
ignore_errors (bool): If True, ignore any HTTP errors.
|
||||
allow_warning (bool): If True, proceed with warnings.
|
||||
privileged (bool): If set to True, requires elevated privileges.
|
||||
|
||||
@@ -38,7 +38,7 @@ class ePICWebAPI(BaseWebAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -49,15 +49,17 @@ 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)
|
||||
if parameters.get("files") is not None:
|
||||
files = parameters["files"]
|
||||
data_fields = parameters.get("data", {})
|
||||
data_fields["password"] = self.pwd
|
||||
response = await client.post(
|
||||
f"http://{self.ip}:{self.port}/{command}",
|
||||
timeout=5,
|
||||
data=form_data,
|
||||
files=files,
|
||||
data=data_fields,
|
||||
)
|
||||
if post:
|
||||
elif post:
|
||||
response = await client.post(
|
||||
f"http://{self.ip}:{self.port}/{command}",
|
||||
timeout=5,
|
||||
@@ -89,11 +91,12 @@ class ePICWebAPI(BaseWebAPI):
|
||||
return {"success": True}
|
||||
except (httpx.HTTPError, json.JSONDecodeError, AttributeError):
|
||||
pass
|
||||
raise APIError(f"Failed to send command to miner: {self}")
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
) -> dict:
|
||||
data = {k: None for k in commands}
|
||||
data: dict[str, Any] = {k: None for k in commands}
|
||||
data["multicommand"] = True
|
||||
for command in commands:
|
||||
data[command] = await self.send_command(command)
|
||||
@@ -147,7 +150,7 @@ 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):
|
||||
async def system_update(self, file: Path | str, keep_settings: bool = True) -> None:
|
||||
"""Perform a system update by uploading a firmware file and sending a
|
||||
command to initiate the update."""
|
||||
|
||||
@@ -159,9 +162,7 @@ class ePICWebAPI(BaseWebAPI):
|
||||
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)
|
||||
with open(file, "rb") as f:
|
||||
files = {"update.zip": ("update.zip", f, "application/zip")}
|
||||
data = {"checksum": checksum, "keepsettings": str(keep_settings).lower()}
|
||||
await self.send_command("systemupdate", files=files, data=data)
|
||||
|
||||
@@ -13,7 +13,7 @@ from pyasic.web.base import BaseWebAPI
|
||||
class ESPMinerWebAPI(BaseWebAPI):
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -21,7 +21,8 @@ class ESPMinerWebAPI(BaseWebAPI):
|
||||
) -> dict:
|
||||
url = f"http://{self.ip}:{self.port}/api/{command}"
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
for _ in range(settings.get("get_data_retries", 1)):
|
||||
retries = settings.get("get_data_retries", 1)
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
if parameters.get("post", False):
|
||||
parameters.pop("post")
|
||||
@@ -42,14 +43,22 @@ class ESPMinerWebAPI(BaseWebAPI):
|
||||
url,
|
||||
timeout=settings.get("api_function_timeout", 5),
|
||||
)
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
except httpx.HTTPError as e:
|
||||
if attempt == retries - 1:
|
||||
raise APIError(
|
||||
f"HTTP error sending '{command}' to {self.ip}: {e}"
|
||||
)
|
||||
else:
|
||||
if data.status_code == 200:
|
||||
try:
|
||||
return data.json()
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
response_text = data.text if data.text else "empty response"
|
||||
if attempt == retries - 1:
|
||||
raise APIError(
|
||||
f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}"
|
||||
)
|
||||
raise APIError(f"Failed to send command to miner API: {url}")
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
@@ -71,31 +80,31 @@ class ESPMinerWebAPI(BaseWebAPI):
|
||||
self.send_command(cmd, allow_warning=allow_warning)
|
||||
)
|
||||
|
||||
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
|
||||
results = await asyncio.gather(
|
||||
*[tasks[cmd] for cmd in tasks], return_exceptions=True
|
||||
)
|
||||
|
||||
data = {"multicommand": True}
|
||||
for cmd in tasks:
|
||||
try:
|
||||
result = tasks[cmd].result()
|
||||
data: dict[str, Any] = {"multicommand": True}
|
||||
for cmd, result in zip(tasks.keys(), results):
|
||||
if not isinstance(result, (APIError, Exception)):
|
||||
if result is None or result == {}:
|
||||
result = {}
|
||||
data[cmd] = {}
|
||||
else:
|
||||
data[cmd] = result
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
return data
|
||||
|
||||
async def system_info(self):
|
||||
async def system_info(self) -> dict:
|
||||
return await self.send_command("system/info")
|
||||
|
||||
async def swarm_info(self):
|
||||
async def swarm_info(self) -> dict:
|
||||
return await self.send_command("swarm/info")
|
||||
|
||||
async def restart(self):
|
||||
async def restart(self) -> dict:
|
||||
return await self.send_command("system/restart", post=True)
|
||||
|
||||
async def update_settings(self, **config):
|
||||
async def update_settings(self, **config: Any) -> dict:
|
||||
return await self.send_command("system", patch=True, **config)
|
||||
|
||||
async def asic_info(self):
|
||||
async def asic_info(self) -> dict:
|
||||
return await self.send_command("system/asic")
|
||||
|
||||
@@ -17,20 +17,23 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import warnings
|
||||
from typing import Any
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import httpx
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.web.base import BaseWebAPI
|
||||
|
||||
PoolPass = TypedDict("PoolPass", {"pass": str})
|
||||
|
||||
|
||||
class GoldshellWebAPI(BaseWebAPI):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.username = "admin"
|
||||
self.pwd = settings.get("default_goldshell_web_password", "123456789")
|
||||
self.token = None
|
||||
self.username: str = "admin"
|
||||
self.pwd: str = settings.get("default_goldshell_web_password", "123456789")
|
||||
self.token: str | None = None
|
||||
|
||||
async def auth(self) -> str | None:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
@@ -63,7 +66,7 @@ class GoldshellWebAPI(BaseWebAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -72,7 +75,12 @@ class GoldshellWebAPI(BaseWebAPI):
|
||||
if self.token is None:
|
||||
await self.auth()
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
for _ in range(settings.get("get_data_retries", 1)):
|
||||
retries = settings.get("get_data_retries", 1)
|
||||
for attempt in range(retries):
|
||||
if self.token is None:
|
||||
raise APIError(
|
||||
f"Could not authenticate web token with miner: {self}"
|
||||
)
|
||||
try:
|
||||
if not parameters == {}:
|
||||
response = await client.put(
|
||||
@@ -91,17 +99,33 @@ class GoldshellWebAPI(BaseWebAPI):
|
||||
return json_data
|
||||
except TypeError:
|
||||
await self.auth()
|
||||
except (httpx.HTTPError, json.JSONDecodeError):
|
||||
pass
|
||||
except httpx.HTTPError as e:
|
||||
if attempt == retries - 1:
|
||||
raise APIError(
|
||||
f"HTTP error sending '{command}' to {self.ip}: {e}"
|
||||
)
|
||||
except json.JSONDecodeError as e:
|
||||
if attempt == retries - 1:
|
||||
response_text = (
|
||||
response.text if response.text else "empty response"
|
||||
)
|
||||
raise APIError(
|
||||
f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}"
|
||||
)
|
||||
raise APIError(f"Failed to send command to miner: {self}")
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
) -> dict:
|
||||
data = {k: None for k in commands}
|
||||
data: dict[str, Any] = {k: None for k in commands}
|
||||
data["multicommand"] = True
|
||||
await self.auth()
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
for command in commands:
|
||||
if self.token is None:
|
||||
raise APIError(
|
||||
f"Could not authenticate web token with miner: {self}"
|
||||
)
|
||||
try:
|
||||
uri_commnand = command
|
||||
if command == "devs":
|
||||
@@ -127,22 +151,22 @@ class GoldshellWebAPI(BaseWebAPI):
|
||||
|
||||
async def newpool(self, url: str, user: str, password: str) -> dict:
|
||||
# looks dumb, but cant pass `pass` since it is a built in type
|
||||
return await self.send_command(
|
||||
"newpool", **{"url": url, "user": user, "pass": password}
|
||||
)
|
||||
poolpass: PoolPass = {"pass": password}
|
||||
return await self.send_command("newpool", url=url, user=user, **poolpass)
|
||||
|
||||
async def delpool(
|
||||
self, url: str, user: str, password: str, dragid: int = 0
|
||||
) -> dict:
|
||||
# looks dumb, but cant pass `pass` since it is a built in type
|
||||
poolpass: PoolPass = {"pass": password}
|
||||
return await self.send_command(
|
||||
"delpool", **{"url": url, "user": user, "pass": password, "dragid": dragid}
|
||||
"delpool", url=url, user=user, dragid=dragid, **poolpass
|
||||
)
|
||||
|
||||
async def setting(self) -> dict:
|
||||
return await self.send_command("setting")
|
||||
|
||||
async def set_setting(self, values: dict):
|
||||
async def set_setting(self, values: dict) -> None:
|
||||
await self.send_command("setting", **values)
|
||||
|
||||
async def status(self) -> dict:
|
||||
|
||||
@@ -33,12 +33,12 @@ class HammerWebAPI(BaseWebAPI):
|
||||
ip (str): IP address of the Hammer device.
|
||||
"""
|
||||
super().__init__(ip)
|
||||
self.username = "root"
|
||||
self.pwd = settings.get("default_hammer_web_password", "root")
|
||||
self.username: str = "root"
|
||||
self.pwd: str = settings.get("default_hammer_web_password", "root")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -47,7 +47,7 @@ class HammerWebAPI(BaseWebAPI):
|
||||
"""Send a command to the Hammer device using HTTP digest authentication.
|
||||
|
||||
Args:
|
||||
command (str | bytes): The CGI command to send.
|
||||
command (str): The CGI command to send.
|
||||
ignore_errors (bool): If True, ignore any HTTP errors.
|
||||
allow_warning (bool): If True, proceed with warnings.
|
||||
privileged (bool): If set to True, requires elevated privileges.
|
||||
|
||||
@@ -22,7 +22,7 @@ from typing import Any
|
||||
import aiofiles
|
||||
import httpx
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic import APIError, settings
|
||||
from pyasic.web.base import BaseWebAPI
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ class HiveonWebAPI(BaseWebAPI):
|
||||
ip (str): IP address of the Antminer device.
|
||||
"""
|
||||
super().__init__(ip)
|
||||
self.username = "root"
|
||||
self.pwd = settings.get("default_hive_web_password", "root")
|
||||
self.username: str = "root"
|
||||
self.pwd: str = settings.get("default_hive_web_password", "root")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -48,7 +48,7 @@ class HiveonWebAPI(BaseWebAPI):
|
||||
"""Send a command to the Antminer device using HTTP digest authentication.
|
||||
|
||||
Args:
|
||||
command (str | bytes): The CGI command to send.
|
||||
command (str): The CGI command to send.
|
||||
ignore_errors (bool): If True, ignore any HTTP errors.
|
||||
allow_warning (bool): If True, proceed with warnings.
|
||||
privileged (bool): If set to True, requires elevated privileges.
|
||||
@@ -62,22 +62,26 @@ class HiveonWebAPI(BaseWebAPI):
|
||||
try:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
if parameters:
|
||||
data = await client.post(
|
||||
response = await client.post(
|
||||
url,
|
||||
data=parameters,
|
||||
auth=auth,
|
||||
timeout=settings.get("api_function_timeout", 3),
|
||||
)
|
||||
else:
|
||||
data = await client.get(url, auth=auth)
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
response = await client.get(url, auth=auth)
|
||||
except httpx.HTTPError as e:
|
||||
raise APIError(f"HTTP error sending '{command}' to {self.ip}: {e}")
|
||||
else:
|
||||
if data.status_code == 200:
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return data.json()
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
return response.json()
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
response_text = response.text if response.text else "empty response"
|
||||
raise APIError(
|
||||
f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}"
|
||||
)
|
||||
raise APIError(f"Failed to send command to miner API: {url}")
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
@@ -204,10 +208,9 @@ class HiveonWebAPI(BaseWebAPI):
|
||||
async with aiofiles.open(file, "rb") as firmware:
|
||||
file_content = await firmware.read()
|
||||
|
||||
parameters = {
|
||||
"file": (file.name, file_content, "application/octet-stream"),
|
||||
"filename": file.name,
|
||||
"keep_settings": keep_settings,
|
||||
}
|
||||
|
||||
return await self.send_command(command="upgrade", **parameters)
|
||||
return await self.send_command(
|
||||
"upgrade",
|
||||
file=(file.name, file_content, "application/octet-stream"),
|
||||
filename=file.name,
|
||||
keep_settings=keep_settings,
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ class IceRiverWebAPI(BaseWebAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
|
||||
@@ -29,9 +29,9 @@ from pyasic.web.base import BaseWebAPI
|
||||
class InnosiliconWebAPI(BaseWebAPI):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.username = "admin"
|
||||
self.pwd = settings.get("default_innosilicon_web_password", "admin")
|
||||
self.token = None
|
||||
self.username: str = "admin"
|
||||
self.pwd: str = settings.get("default_innosilicon_web_password", "admin")
|
||||
self.token: str | None = None
|
||||
|
||||
async def auth(self) -> str | None:
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
@@ -49,7 +49,7 @@ class InnosiliconWebAPI(BaseWebAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -58,7 +58,12 @@ class InnosiliconWebAPI(BaseWebAPI):
|
||||
if self.token is None:
|
||||
await self.auth()
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
for _ in range(settings.get("get_data_retries", 1)):
|
||||
retries = settings.get("get_data_retries", 1)
|
||||
for attempt in range(retries):
|
||||
if self.token is None:
|
||||
raise APIError(
|
||||
f"Could not authenticate web token with miner: {self}"
|
||||
)
|
||||
try:
|
||||
response = await client.post(
|
||||
f"http://{self.ip}:{self.port}/api/{command}",
|
||||
@@ -82,17 +87,33 @@ class InnosiliconWebAPI(BaseWebAPI):
|
||||
raise APIError(json_data["message"])
|
||||
raise APIError("Innosilicon web api command failed.")
|
||||
return json_data
|
||||
except (httpx.HTTPError, json.JSONDecodeError):
|
||||
pass
|
||||
except httpx.HTTPError as e:
|
||||
if attempt == retries - 1:
|
||||
raise APIError(
|
||||
f"HTTP error sending '{command}' to {self.ip}: {e}"
|
||||
)
|
||||
except json.JSONDecodeError as e:
|
||||
if attempt == retries - 1:
|
||||
response_text = (
|
||||
response.text if response.text else "empty response"
|
||||
)
|
||||
raise APIError(
|
||||
f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}"
|
||||
)
|
||||
raise APIError(f"Failed to send command to miner: {self}")
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
) -> dict:
|
||||
data = {k: None for k in commands}
|
||||
data: dict[str, Any] = {k: None for k in commands}
|
||||
data["multicommand"] = True
|
||||
await self.auth()
|
||||
async with httpx.AsyncClient(transport=settings.transport()) as client:
|
||||
for command in commands:
|
||||
if self.token is None:
|
||||
raise APIError(
|
||||
f"Could not authenticate web token with miner: {self}"
|
||||
)
|
||||
try:
|
||||
response = await client.post(
|
||||
f"http://{self.ip}:{self.port}/api/{command}",
|
||||
|
||||
@@ -6,13 +6,15 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic import APIError, settings
|
||||
from pyasic.web.base import BaseWebAPI
|
||||
|
||||
|
||||
class MaraWebAPI(BaseWebAPI):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.username: str = "root"
|
||||
self.pwd: str = "root"
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
@@ -52,7 +54,7 @@ class MaraWebAPI(BaseWebAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
@@ -65,22 +67,26 @@ class MaraWebAPI(BaseWebAPI):
|
||||
transport=settings.transport(),
|
||||
) as client:
|
||||
if parameters:
|
||||
data = await client.post(
|
||||
response = await client.post(
|
||||
url,
|
||||
auth=auth,
|
||||
timeout=settings.get("api_function_timeout", 3),
|
||||
json=parameters,
|
||||
)
|
||||
else:
|
||||
data = await client.get(url, auth=auth)
|
||||
response = await client.get(url, auth=auth)
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
else:
|
||||
if data.status_code == 200:
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
return data.json()
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
return response.json()
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
response_text = response.text if response.text else "empty response"
|
||||
raise APIError(
|
||||
f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}"
|
||||
)
|
||||
raise APIError(f"Failed to send command to miner API: {url}")
|
||||
|
||||
async def brief(self):
|
||||
return await self.send_command("brief")
|
||||
|
||||
@@ -41,7 +41,7 @@ class MSKMinerWebAPI(BaseWebAPI):
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str | bytes,
|
||||
command: str,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
privileged: bool = False,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user