feature: add mypy type checking for better type consistency

This commit is contained in:
James Hilliard
2025-09-26 11:24:38 -06:00
committed by GitHub
parent cd52d3aeaf
commit 1f4054bf38
100 changed files with 2813 additions and 2203 deletions

View File

@@ -22,16 +22,24 @@ repos:
name: check-yaml for other YAML files name: check-yaml for other YAML files
exclude: ^mkdocs\.yml$ exclude: ^mkdocs\.yml$
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/psf/black - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 25.9.0 rev: v0.13.2
hooks: hooks:
- id: black - id: ruff-check
- repo: https://github.com/pycqa/isort args: [--fix]
rev: 6.0.1 - id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
hooks: hooks:
- id: isort - id: mypy
name: isort (python) 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 - repo: local
hooks: hooks:
- id: unittest - id: unittest

View File

@@ -1,8 +1,8 @@
import asyncio
import importlib import importlib
import os import os
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import Any
from pyasic.miners.factory import MINER_CLASSES, MinerTypes from pyasic.miners.factory import MINER_CLASSES, MinerTypes
@@ -128,7 +128,7 @@ BACKEND_TYPE_CLOSER = """
</ul> </ul>
</details>""" </details>"""
m_data = {} m_data: dict[str, dict[str, list[type[Any]]]] = {}
done = [] done = []
for m in MINER_CLASSES: for m in MINER_CLASSES:

129
poetry.lock generated
View File

@@ -638,46 +638,6 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "importlib-metadata"
version = "8.7.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.9"
groups = ["docs"]
markers = "python_version < \"3.10\""
files = [
{file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"},
{file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"},
]
[package.dependencies]
zipp = ">=3.20"
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
perf = ["ipython"]
test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
type = ["pytest-mypy"]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
groups = ["dev"]
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.6" version = "3.1.6"
@@ -708,9 +668,6 @@ files = [
{file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"},
] ]
[package.dependencies]
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"] testing = ["coverage", "pyyaml"]
@@ -814,7 +771,6 @@ files = [
click = ">=7.0" click = ">=7.0"
colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
ghp-import = ">=1.0" ghp-import = ">=1.0"
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
jinja2 = ">=2.11.1" jinja2 = ">=2.11.1"
markdown = ">=3.3.6" markdown = ">=3.3.6"
markupsafe = ">=2.0.1" markupsafe = ">=2.0.1"
@@ -860,7 +816,6 @@ files = [
] ]
[package.dependencies] [package.dependencies]
importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
mergedeep = ">=1.3.4" mergedeep = ">=1.3.4"
platformdirs = ">=2.2.0" platformdirs = ">=2.2.0"
pyyaml = ">=5.1" pyyaml = ">=5.1"
@@ -922,7 +877,6 @@ files = [
[package.dependencies] [package.dependencies]
click = ">=7.0" click = ">=7.0"
importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
Jinja2 = ">=2.11.1" Jinja2 = ">=2.11.1"
Markdown = ">=3.6" Markdown = ">=3.6"
MarkupSafe = ">=1.1" MarkupSafe = ">=1.1"
@@ -931,7 +885,6 @@ mkdocs-autorefs = ">=1.2"
mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
platformdirs = ">=2.2" platformdirs = ">=2.2"
pymdown-extensions = ">=6.3" pymdown-extensions = ">=6.3"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
crystal = ["mkdocstrings-crystal (>=0.3.4)"] crystal = ["mkdocstrings-crystal (>=0.3.4)"]
@@ -1498,6 +1451,35 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "ruff"
version = "0.13.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3"},
{file = "ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2"},
{file = "ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46"},
{file = "ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6"},
{file = "ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07"},
{file = "ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8"},
{file = "ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89"},
{file = "ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0"},
{file = "ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa"},
{file = "ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3"},
{file = "ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d"},
{file = "ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b"},
{file = "ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22"},
{file = "ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736"},
{file = "ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2"},
{file = "ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac"},
{file = "ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585"},
{file = "ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7"},
{file = "ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff"},
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "3.0.4" version = "3.0.4"
@@ -1589,18 +1571,42 @@ files = [
{file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"},
] ]
[[package]]
name = "types-aiofiles"
version = "24.1.0.20250822"
description = "Typing stubs for aiofiles"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "types_aiofiles-24.1.0.20250822-py3-none-any.whl", hash = "sha256:0ec8f8909e1a85a5a79aed0573af7901f53120dd2a29771dd0b3ef48e12328b0"},
{file = "types_aiofiles-24.1.0.20250822.tar.gz", hash = "sha256:9ab90d8e0c307fe97a7cf09338301e3f01a163e39f3b529ace82466355c84a7b"},
]
[[package]]
name = "types-passlib"
version = "1.7.7.20250602"
description = "Typing stubs for passlib"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "types_passlib-1.7.7.20250602-py3-none-any.whl", hash = "sha256:ed73a91be9a22484ebd62cc0d127675ded542b892b99776db92dab760bbfe274"},
{file = "types_passlib-1.7.7.20250602.tar.gz", hash = "sha256:cf2350e78d36b6b09e4db44284d96651b57285f499cfabf111b616065abab7b3"},
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+" description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main", "dev", "docs"] groups = ["main", "dev"]
files = [ files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
] ]
markers = {dev = "python_version < \"3.11\"", docs = "python_version < \"3.10\""} markers = {dev = "python_version < \"3.11\""}
[[package]] [[package]]
name = "typing-inspection" name = "typing-inspection"
@@ -1700,28 +1706,7 @@ files = [
[package.extras] [package.extras]
watchmedo = ["PyYAML (>=3.10)"] watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "zipp"
version = "3.23.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.9"
groups = ["docs"]
markers = "python_version < \"3.10\""
files = [
{file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"},
{file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">3.9, <4.0" python-versions = ">3.10, <4.0"
content-hash = "81ec4faceddb41badda1649e77ddcfba03b0275021ba37ba69290b7e6a326829" content-hash = "ab448bfd6e29c1017aa7bc9713f6d2a3a2985106d023122729bb74f4f6e0a609"

View File

@@ -14,10 +14,41 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pyasic.config.fans import FanMode, FanModeConfig, FanModeNormal from pyasic.config.fans import (
from pyasic.config.mining import MiningMode, MiningModeConfig 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.mining.scaling import ScalingConfig
from pyasic.config.pools import PoolConfig from pyasic.config.pools import PoolConfig
from pyasic.config.temperature import TemperatureConfig from pyasic.config.temperature import TemperatureConfig
@@ -32,11 +63,11 @@ class MinerConfig(BaseModel):
arbitrary_types_allowed = True arbitrary_types_allowed = True
pools: PoolConfig = Field(default_factory=PoolConfig.default) 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) 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: try:
return getattr(self, item) return getattr(self, item)
except AttributeError: except AttributeError:
@@ -88,8 +119,8 @@ class MinerConfig(BaseModel):
def as_btminer_v3(self, user_suffix: str | None = None) -> dict: def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
"""Generates the configuration in the format suitable for Whatsminers running BTMiner V3.""" """Generates the configuration in the format suitable for Whatsminers running BTMiner V3."""
return { return {
"set.miner.pools": self.pools.as_btminer_v3() "set.miner.pools": self.pools.as_btminer_v3(),
** self.mining_mode.as_btminer_v3() **self.mining_mode.as_btminer_v3(),
} }
def as_am_old(self, user_suffix: str | None = None) -> dict: 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)) return cls(pools=PoolConfig.from_goldshell(web_conf))
@classmethod @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.""" """Constructs a MinerConfig object from web configuration for Goldshell Byte miners."""
return cls(pools=PoolConfig.from_goldshell_byte(web_conf)) return cls(pools=PoolConfig.from_goldshell_byte(web_conf))

View File

@@ -16,6 +16,7 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
@@ -89,61 +90,61 @@ class MinerConfigOption(Enum):
class MinerConfigValue(BaseModel): class MinerConfigValue(BaseModel):
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None): def from_dict(cls, dict_conf: dict):
return cls() return cls()
def as_dict(self) -> dict: def as_dict(self) -> dict:
return self.model_dump() return self.model_dump()
def as_am_modern(self) -> dict: def as_am_modern(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_hiveon_modern(self) -> dict: def as_hiveon_modern(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_am_old(self) -> dict: def as_am_old(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_wm(self) -> dict: def as_wm(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_btminer_v3(self) -> dict: def as_btminer_v3(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_inno(self) -> dict: def as_inno(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_goldshell(self) -> dict: def as_goldshell(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_avalon(self) -> dict: def as_avalon(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_bosminer(self) -> dict: def as_bosminer(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_boser(self) -> dict: def as_boser(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_epic(self) -> dict: def as_epic(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_vnish(self) -> dict: def as_vnish(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_auradine(self) -> dict: def as_auradine(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_mara(self) -> dict: def as_mara(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_espminer(self) -> dict: def as_espminer(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_luxos(self) -> dict: def as_luxos(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def as_elphapex(self) -> dict: def as_elphapex(self, *args: Any, **kwargs: Any) -> Any:
return {} return {}
def __getitem__(self, item): def __getitem__(self, item):

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from typing import TypeVar, Union from typing import TypeVar
from pydantic import Field from pydantic import Field
@@ -28,7 +28,7 @@ class FanModeNormal(MinerConfigValue):
minimum_speed: int = 0 minimum_speed: int = 0
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "FanModeNormal": def from_dict(cls, dict_conf: dict) -> FanModeNormal:
cls_conf = {} cls_conf = {}
if dict_conf.get("minimum_fans") is not None: if dict_conf.get("minimum_fans") is not None:
cls_conf["minimum_fans"] = dict_conf["minimum_fans"] cls_conf["minimum_fans"] = dict_conf["minimum_fans"]
@@ -37,7 +37,7 @@ class FanModeNormal(MinerConfigValue):
return cls(**cls_conf) return cls(**cls_conf)
@classmethod @classmethod
def from_vnish(cls, web_cooling_settings: dict) -> "FanModeNormal": def from_vnish(cls, web_cooling_settings: dict) -> FanModeNormal:
cls_conf = {} cls_conf = {}
if web_cooling_settings.get("fan_min_count") is not None: if web_cooling_settings.get("fan_min_count") is not None:
cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"] cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"]
@@ -112,7 +112,7 @@ class FanModeManual(MinerConfigValue):
minimum_fans: int = 1 minimum_fans: int = 1
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "FanModeManual": def from_dict(cls, dict_conf: dict) -> FanModeManual:
cls_conf = {} cls_conf = {}
if dict_conf.get("speed") is not None: if dict_conf.get("speed") is not None:
cls_conf["speed"] = dict_conf["speed"] cls_conf["speed"] = dict_conf["speed"]
@@ -121,7 +121,7 @@ class FanModeManual(MinerConfigValue):
return cls(**cls_conf) return cls(**cls_conf)
@classmethod @classmethod
def from_bosminer(cls, toml_fan_conf: dict) -> "FanModeManual": def from_bosminer(cls, toml_fan_conf: dict) -> FanModeManual:
cls_conf = {} cls_conf = {}
if toml_fan_conf.get("min_fans") is not None: if toml_fan_conf.get("min_fans") is not None:
cls_conf["minimum_fans"] = toml_fan_conf["min_fans"] cls_conf["minimum_fans"] = toml_fan_conf["min_fans"]
@@ -130,7 +130,7 @@ class FanModeManual(MinerConfigValue):
return cls(**cls_conf) return cls(**cls_conf)
@classmethod @classmethod
def from_vnish(cls, web_cooling_settings: dict) -> "FanModeManual": def from_vnish(cls, web_cooling_settings: dict) -> FanModeManual:
cls_conf = {} cls_conf = {}
if web_cooling_settings.get("fan_min_count") is not None: if web_cooling_settings.get("fan_min_count") is not None:
cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"] cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"]
@@ -191,7 +191,7 @@ class FanModeImmersion(MinerConfigValue):
mode: str = Field(init=False, default="immersion") mode: str = Field(init=False, default="immersion")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "FanModeImmersion": def from_dict(cls, dict_conf: dict | None) -> FanModeImmersion:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -417,5 +417,5 @@ class FanModeConfig(MinerConfigOption):
FanMode = TypeVar( FanMode = TypeVar(
"FanMode", "FanMode",
bound=Union[FanModeNormal, FanModeManual, FanModeImmersion], bound=FanModeNormal | FanModeManual | FanModeImmersion,
) )

View File

@@ -16,7 +16,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import field from dataclasses import field
from typing import TypeVar, Union from typing import Any, TypeVar
from pyasic import settings from pyasic import settings
from pyasic.config.base import MinerConfigOption, MinerConfigValue from pyasic.config.base import MinerConfigOption, MinerConfigValue
@@ -35,7 +35,14 @@ from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
TunerPerformanceMode, TunerPerformanceMode,
) )
from .algo import TunerAlgo, TunerAlgoType from .algo import (
BoardTuneAlgo,
ChipTuneAlgo,
StandardTuneAlgo,
TunerAlgo,
TunerAlgoType,
VOptAlgo,
)
from .presets import MiningPreset from .presets import MiningPreset
from .scaling import ScalingConfig from .scaling import ScalingConfig
@@ -44,7 +51,7 @@ class MiningModeNormal(MinerConfigValue):
mode: str = field(init=False, default="normal") mode: str = field(init=False, default="normal")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeNormal": def from_dict(cls, dict_conf: dict | None) -> MiningModeNormal:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -93,7 +100,7 @@ class MiningModeSleep(MinerConfigValue):
mode: str = field(init=False, default="sleep") mode: str = field(init=False, default="sleep")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeSleep": def from_dict(cls, dict_conf: dict | None) -> MiningModeSleep:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -136,7 +143,7 @@ class MiningModeLPM(MinerConfigValue):
mode: str = field(init=False, default="low") mode: str = field(init=False, default="low")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeLPM": def from_dict(cls, dict_conf: dict | None) -> MiningModeLPM:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -169,7 +176,7 @@ class MiningModeHPM(MinerConfigValue):
mode: str = field(init=False, default="high") mode: str = field(init=False, default="high")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHPM": def from_dict(cls, dict_conf: dict | None) -> MiningModeHPM:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -201,11 +208,15 @@ class MiningModePowerTune(MinerConfigValue):
mode: str = field(init=False, default="power_tuning") mode: str = field(init=False, default="power_tuning")
power: int | None = None 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 scaling: ScalingConfig | None = None
@classmethod @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 = {} cls_conf = {}
if dict_conf.get("power"): if dict_conf.get("power"):
cls_conf["power"] = dict_conf["power"] cls_conf["power"] = dict_conf["power"]
@@ -245,25 +256,27 @@ class MiningModePowerTune(MinerConfigValue):
cfg = {"autotuning": tuning_cfg} cfg = {"autotuning": tuning_cfg}
if self.scaling is not None: if self.scaling is not None:
scaling_cfg = {"enabled": True} scaling_cfg: dict[str, Any] = {"enabled": True}
if self.scaling.step is not None: if self.scaling.step is not None:
scaling_cfg["power_step"] = self.scaling.step scaling_cfg["power_step"] = self.scaling.step
if self.scaling.minimum is not None: if self.scaling.minimum is not None:
scaling_cfg["min_power_target"] = self.scaling.minimum scaling_cfg["min_power_target"] = self.scaling.minimum
if self.scaling.shutdown is not None: 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 cfg["performance_scaling"] = scaling_cfg
return cfg return cfg
def as_boser(self) -> dict: def as_boser(self) -> dict:
cfg = { cfg: dict[str, Any] = {
"set_performance_mode": SetPerformanceModeRequest( "set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_AND_APPLY, save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
mode=PerformanceMode( mode=PerformanceMode(
tuner_mode=TunerPerformanceMode( tuner_mode=TunerPerformanceMode(
power_target=PowerTargetMode( power_target=PowerTargetMode(
power_target=Power(watt=self.power) 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 = {} sd_cfg = {}
if self.scaling.shutdown is not None: if self.scaling.shutdown is not None:
sd_cfg = self.scaling.shutdown.as_boser() sd_cfg = self.scaling.shutdown.as_boser()
power_target_kwargs = {} power_target_kwargs: dict[str, Any] = {}
if self.scaling.step is not None: 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: 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( cfg["set_dps"] = SetDpsRequest(
save_action=SaveAction.SAVE_AND_APPLY, save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
enable=True, enable=True,
**sd_cfg, **sd_cfg,
target=DpsTarget(power_target=DpsPowerTarget(**power_target_kwargs)), target=DpsTarget(power_target=DpsPowerTarget(**power_target_kwargs)),
@@ -311,11 +326,15 @@ class MiningModeHashrateTune(MinerConfigValue):
mode: str = field(init=False, default="hashrate_tuning") mode: str = field(init=False, default="hashrate_tuning")
hashrate: int | None = None 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 scaling: ScalingConfig | None = None
@classmethod @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 = {} cls_conf = {}
if dict_conf.get("hashrate"): if dict_conf.get("hashrate"):
cls_conf["hashrate"] = dict_conf["hashrate"] cls_conf["hashrate"] = dict_conf["hashrate"]
@@ -346,14 +365,16 @@ class MiningModeHashrateTune(MinerConfigValue):
return {"autotuning": conf} return {"autotuning": conf}
def as_boser(self) -> dict: def as_boser(self) -> dict:
cfg = { cfg: dict[str, Any] = {
"set_performance_mode": SetPerformanceModeRequest( "set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_AND_APPLY, save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
mode=PerformanceMode( mode=PerformanceMode(
tuner_mode=TunerPerformanceMode( tuner_mode=TunerPerformanceMode(
hashrate_target=HashrateTargetMode( hashrate_target=HashrateTargetMode(
hashrate_target=TeraHashrate( 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 = {} sd_cfg = {}
if self.scaling.shutdown is not None: if self.scaling.shutdown is not None:
sd_cfg = self.scaling.shutdown.as_boser() sd_cfg = self.scaling.shutdown.as_boser()
hashrate_target_kwargs = {} hashrate_target_kwargs: dict[str, Any] = {}
if self.scaling.step is not None: if self.scaling.step is not None:
hashrate_target_kwargs["hashrate_step"] = TeraHashrate( hashrate_target_kwargs["hashrate_step"] = TeraHashrate(
self.scaling.step terahash_per_second=float(self.scaling.step)
) )
if self.scaling.minimum is not None: if self.scaling.minimum is not None:
hashrate_target_kwargs["min_hashrate_target"] = TeraHashrate( hashrate_target_kwargs["min_hashrate_target"] = TeraHashrate(
self.scaling.minimum terahash_per_second=float(self.scaling.minimum)
) )
cfg["set_dps"] = SetDpsRequest( cfg["set_dps"] = SetDpsRequest(
save_action=SaveAction.SAVE_AND_APPLY, save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
enable=True, enable=True,
**sd_cfg, **sd_cfg,
target=DpsTarget( target=DpsTarget(
@@ -390,7 +411,11 @@ class MiningModeHashrateTune(MinerConfigValue):
def as_epic(self) -> dict: def as_epic(self) -> dict:
mode = { mode = {
"ptune": { "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, "target": self.hashrate,
} }
} }
@@ -431,7 +456,7 @@ class MiningModePreset(MinerConfigValue):
web_overclock_settings: dict, web_overclock_settings: dict,
web_presets: list[dict], web_presets: list[dict],
web_perf_summary: dict, web_perf_summary: dict,
) -> "MiningModePreset": ) -> MiningModePreset:
active_preset = web_perf_summary.get("current_preset") active_preset = web_perf_summary.get("current_preset")
if active_preset is None: if active_preset is None:
@@ -440,12 +465,12 @@ class MiningModePreset(MinerConfigValue):
active_preset = preset active_preset = preset
return cls( 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], available_presets=[MiningPreset.from_vnish(p) for p in web_presets],
) )
@classmethod @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) active_preset = cls.get_active_preset_from_luxos(rpc_config, rpc_profiles)
return cls( return cls(
active_preset=active_preset, active_preset=active_preset,
@@ -463,7 +488,7 @@ class MiningModePreset(MinerConfigValue):
for profile in rpc_profiles["PROFILES"]: for profile in rpc_profiles["PROFILES"]:
if profile["Profile Name"] == active_profile: if profile["Profile Name"] == active_profile:
active_preset = profile active_preset = profile
return MiningPreset.from_luxos(active_preset) return MiningPreset.from_luxos(active_preset or {})
class ManualBoardSettings(MinerConfigValue): class ManualBoardSettings(MinerConfigValue):
@@ -471,7 +496,7 @@ class ManualBoardSettings(MinerConfigValue):
volt: float volt: float
@classmethod @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"]) return cls(freq=dict_conf["freq"], volt=dict_conf["volt"])
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -499,11 +524,15 @@ class MiningModeManual(MinerConfigValue):
boards: dict[int, ManualBoardSettings] = field(default_factory=dict) boards: dict[int, ManualBoardSettings] = field(default_factory=dict)
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeManual": def from_dict(cls, dict_conf: dict) -> MiningModeManual:
return cls( return cls(
global_freq=dict_conf["global_freq"], global_freq=dict_conf["global_freq"],
global_volt=dict_conf["global_volt"], 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: def as_am_modern(self) -> dict:
@@ -527,7 +556,7 @@ class MiningModeManual(MinerConfigValue):
} }
@classmethod @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 # will raise KeyError if it cant find the settings, values cannot be empty
voltage = web_overclock_settings["globals"]["volt"] voltage = web_overclock_settings["globals"]["volt"]
freq = web_overclock_settings["globals"]["freq"] freq = web_overclock_settings["globals"]["freq"]
@@ -541,7 +570,7 @@ class MiningModeManual(MinerConfigValue):
return cls(global_freq=freq, global_volt=voltage, boards=boards) return cls(global_freq=freq, global_volt=voltage, boards=boards)
@classmethod @classmethod
def from_epic(cls, epic_conf: dict) -> "MiningModeManual": def from_epic(cls, epic_conf: dict) -> MiningModeManual:
voltage = 0 voltage = 0
freq = 0 freq = 0
if epic_conf.get("HwConfig") is not None: if epic_conf.get("HwConfig") is not None:
@@ -581,11 +610,11 @@ class MiningModeConfig(MinerConfigOption):
manual = MiningModeManual manual = MiningModeManual
@classmethod @classmethod
def default(cls): def default(cls) -> MiningModeConfig:
return cls.normal() return cls.normal()
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None): def from_dict(cls, dict_conf: dict | None) -> MiningModeConfig:
if dict_conf is None: if dict_conf is None:
return cls.default() return cls.default()
@@ -593,12 +622,13 @@ class MiningModeConfig(MinerConfigOption):
if mode is None: if mode is None:
return cls.default() return cls.default()
cls_attr = getattr(cls, mode) cls_attr = getattr(cls, mode, None)
if cls_attr is not None: if cls_attr is not None:
return cls_attr().from_dict(dict_conf) return cls_attr().from_dict(dict_conf)
return cls.default()
@classmethod @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: if web_conf.get("bitmain-work-mode") is not None:
work_mode = web_conf["bitmain-work-mode"] work_mode = web_conf["bitmain-work-mode"]
if work_mode == "": if work_mode == "":
@@ -612,7 +642,7 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @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: if web_conf.get("bitmain-work-mode") is not None:
work_mode = web_conf["bitmain-work-mode"] work_mode = web_conf["bitmain-work-mode"]
if work_mode == "": if work_mode == "":
@@ -626,7 +656,7 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @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: if web_conf.get("fc-work-mode") is not None:
work_mode = web_conf["fc-work-mode"] work_mode = web_conf["fc-work-mode"]
if work_mode == "": if work_mode == "":
@@ -640,7 +670,7 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @classmethod
def from_epic(cls, web_conf: dict): def from_epic(cls, web_conf: dict) -> MiningModeConfig:
try: try:
tuner_running = web_conf["PerpetualTune"]["Running"] tuner_running = web_conf["PerpetualTune"]["Running"]
if tuner_running: if tuner_running:
@@ -679,12 +709,12 @@ class MiningModeConfig(MinerConfigOption):
algo=TunerAlgo.chip_tune(), algo=TunerAlgo.chip_tune(),
) )
else: else:
return MiningModeManual.from_epic(web_conf) return cls.manual.from_epic(web_conf)
except KeyError: except KeyError:
return cls.default() return cls.default()
@classmethod @classmethod
def from_bosminer(cls, toml_conf: dict): def from_bosminer(cls, toml_conf: dict) -> MiningModeConfig:
if toml_conf.get("autotuning") is None: if toml_conf.get("autotuning") is None:
return cls.default() return cls.default()
autotuning_conf = toml_conf["autotuning"] autotuning_conf = toml_conf["autotuning"]
@@ -726,21 +756,19 @@ class MiningModeConfig(MinerConfigOption):
@classmethod @classmethod
def from_vnish( def from_vnish(
cls, web_settings: dict, web_presets: list[dict], web_perf_summary: dict cls, web_settings: dict, web_presets: list[dict], web_perf_summary: dict
): ) -> MiningModeConfig:
try: try:
mode_settings = web_settings["miner"]["overclock"] mode_settings = web_settings["miner"]["overclock"]
except KeyError: except KeyError:
return cls.default() return cls.default()
if mode_settings["preset"] == "disabled": if mode_settings["preset"] == "disabled":
return MiningModeManual.from_vnish(mode_settings) return cls.manual.from_vnish(mode_settings, web_presets, web_perf_summary)
else: else:
return MiningModePreset.from_vnish( return cls.preset.from_vnish(mode_settings, web_presets, web_perf_summary)
mode_settings, web_presets, web_perf_summary
)
@classmethod @classmethod
def from_boser(cls, grpc_miner_conf: dict): def from_boser(cls, grpc_miner_conf: dict) -> MiningModeConfig:
try: try:
tuner_conf = grpc_miner_conf["tuner"] tuner_conf = grpc_miner_conf["tuner"]
if not tuner_conf.get("enabled", False): if not tuner_conf.get("enabled", False):
@@ -786,7 +814,7 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @classmethod
def from_auradine(cls, web_mode: dict): def from_auradine(cls, web_mode: dict) -> MiningModeConfig:
try: try:
mode_data = web_mode["Mode"][0] mode_data = web_mode["Mode"][0]
if mode_data.get("Sleep") == "on": if mode_data.get("Sleep") == "on":
@@ -803,9 +831,12 @@ class MiningModeConfig(MinerConfigOption):
return cls.power_tuning(power=mode_data["Power"]) return cls.power_tuning(power=mode_data["Power"])
except LookupError: except LookupError:
return cls.default() return cls.default()
return cls.default()
@classmethod @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: try:
is_mining = rpc_device_info["msg"]["miner"]["working"] == "true" is_mining = rpc_device_info["msg"]["miner"]["working"] == "true"
if not is_mining: if not is_mining:
@@ -823,9 +854,10 @@ class MiningModeConfig(MinerConfigOption):
except LookupError: except LookupError:
return cls.default() return cls.default()
return cls.default()
@classmethod @classmethod
def from_mara(cls, web_config: dict): def from_mara(cls, web_config: dict) -> MiningModeConfig:
try: try:
mode = web_config["mode"]["work-mode-selector"] mode = web_config["mode"]["work-mode-selector"]
if mode == "Fixed": if mode == "Fixed":
@@ -850,24 +882,26 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @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) preset_info = MiningModePreset.from_luxos(rpc_config, rpc_profiles)
return cls.preset( return cls.preset(
active_preset=preset_info.active_preset, active_preset=preset_info.active_preset,
available_presets=preset_info.available_presets, 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 = TypeVar(
"MiningMode", "MiningMode",
bound=Union[ bound=MiningModeNormal
MiningModeNormal, | MiningModeHPM
MiningModeHPM, | MiningModeLPM
MiningModeLPM, | MiningModeSleep
MiningModeSleep, | MiningModeManual
MiningModeManual, | MiningModePowerTune
MiningModePowerTune, | MiningModeHashrateTune
MiningModeHashrateTune, | MiningModePreset,
MiningModePreset,
],
) )

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import field from dataclasses import field
from typing import TypeVar, Union from typing import Any, TypeVar
from pyasic.config.base import MinerConfigOption, MinerConfigValue from pyasic.config.base import MinerConfigOption, MinerConfigValue
@@ -41,26 +41,26 @@ class TunerAlgo(MinerConfigOption):
chip_tune = ChipTuneAlgo chip_tune = ChipTuneAlgo
@classmethod @classmethod
def default(cls) -> TunerAlgoType: def default(cls) -> StandardTuneAlgo:
return cls.standard() return cls.standard()
@classmethod @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") mode = dict_conf.get("mode")
if mode is None: if mode is None:
return cls.default() return cls.default()
cls_attr = getattr(cls, mode) cls_attr = getattr(cls, mode, None)
if cls_attr is not None: if cls_attr is not None:
return cls_attr().from_dict(dict_conf) return cls_attr().from_dict(dict_conf)
return cls.default()
TunerAlgoType = TypeVar( TunerAlgoType = TypeVar(
"TunerAlgoType", "TunerAlgoType",
bound=Union[ bound=StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo,
StandardTuneAlgo,
VOptAlgo,
BoardTuneAlgo,
ChipTuneAlgo,
],
) )

View File

@@ -23,7 +23,9 @@ class ScalingShutdown(MinerConfigValue):
duration: int | None = None duration: int | None = None
@classmethod @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( return cls(
enabled=dict_conf.get("enabled", False), duration=dict_conf.get("duration") enabled=dict_conf.get("enabled", False), duration=dict_conf.get("duration")
) )
@@ -51,7 +53,7 @@ class ScalingShutdown(MinerConfigValue):
return None return None
def as_bosminer(self) -> dict: 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: if self.duration is not None:
cfg["shutdown_duration"] = self.duration cfg["shutdown_duration"] = self.duration
@@ -68,7 +70,9 @@ class ScalingConfig(MinerConfigValue):
shutdown: ScalingShutdown | None = None shutdown: ScalingShutdown | None = None
@classmethod @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 = { cls_conf = {
"step": dict_conf.get("step"), "step": dict_conf.get("step"),
"minimum": dict_conf.get("minimum"), "minimum": dict_conf.get("minimum"),

View File

@@ -17,7 +17,7 @@ from __future__ import annotations
import random import random
import string import string
from typing import List from typing import Any
from pydantic import Field from pydantic import Field
@@ -66,12 +66,15 @@ class Pool(MinerConfigValue):
def as_btminer_v3(self, user_suffix: str | None = None) -> dict: def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
return { return {
f"pool": self.url, "pool": self.url,
f"worker": f"{self.user}{user_suffix or ''}", "worker": f"{self.user}{user_suffix or ''}",
f"passwd": self.password, "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 { return {
f"_ant_pool{idx}url": self.url, f"_ant_pool{idx}url": self.url,
f"_ant_pool{idx}user": f"{self.user}{user_suffix or ''}", 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: def as_avalon(self, user_suffix: str | None = None) -> str:
return ",".join([self.url, f"{self.user}{user_suffix or ''}", self.password]) 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 { return {
f"Pool{idx}": self.url, f"Pool{idx}": self.url,
f"UserName{idx}": f"{self.user}{user_suffix or ''}", f"UserName{idx}": f"{self.user}{user_suffix or ''}",
@@ -109,7 +115,7 @@ class Pool(MinerConfigValue):
"pass": self.password, "pass": self.password,
} }
def as_epic(self, user_suffix: str | None = None): def as_epic(self, user_suffix: str | None = None) -> dict:
return { return {
"pool": self.url, "pool": self.url,
"login": f"{self.user}{user_suffix or ''}", "login": f"{self.user}{user_suffix or ''}",
@@ -146,58 +152,60 @@ class Pool(MinerConfigValue):
} }
@classmethod @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( return cls(
url=dict_conf["url"], user=dict_conf["user"], password=dict_conf["password"] url=dict_conf["url"], user=dict_conf["user"], password=dict_conf["password"]
) )
@classmethod @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") return cls(url=api_pool["URL"], user=api_pool["User"], password="x")
@classmethod @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") return cls(url=api_pool["url"], user=api_pool["account"], password="x")
@classmethod @classmethod
def from_epic(cls, api_pool: dict) -> "Pool": def from_epic(cls, api_pool: dict) -> Pool:
return cls( return cls(
url=api_pool["pool"], user=api_pool["login"], password=api_pool["password"] url=api_pool["pool"], user=api_pool["login"], password=api_pool["password"]
) )
@classmethod @classmethod
def from_am_modern(cls, web_pool: dict) -> "Pool": def from_am_modern(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
@classmethod @classmethod
def from_hiveon_modern(cls, web_pool: dict) -> "Pool": def from_hiveon_modern(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
@classmethod @classmethod
def from_elphapex(cls, web_pool: dict) -> "Pool": def from_elphapex(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
# TODO: check if this is accurate, user/username, pass/password # TODO: check if this is accurate, user/username, pass/password
@classmethod @classmethod
def from_goldshell(cls, web_pool: dict) -> "Pool": def from_goldshell(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
@classmethod @classmethod
def from_inno(cls, web_pool: dict) -> "Pool": def from_inno(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
@classmethod @classmethod
def from_bosminer(cls, toml_pool_conf: dict) -> "Pool": def from_bosminer(cls, toml_pool_conf: dict) -> Pool:
return cls( return cls(
url=toml_pool_conf["url"], url=toml_pool_conf["url"],
user=toml_pool_conf["user"], user=toml_pool_conf["user"],
@@ -205,7 +213,7 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_vnish(cls, web_pool: dict) -> "Pool": def from_vnish(cls, web_pool: dict) -> Pool:
return cls( return cls(
url="stratum+tcp://" + web_pool["url"], url="stratum+tcp://" + web_pool["url"],
user=web_pool["user"], user=web_pool["user"],
@@ -213,7 +221,7 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_boser(cls, grpc_pool: dict) -> "Pool": def from_boser(cls, grpc_pool: dict) -> Pool:
return cls( return cls(
url=grpc_pool["url"], url=grpc_pool["url"],
user=grpc_pool["user"], user=grpc_pool["user"],
@@ -221,7 +229,7 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_mara(cls, web_pool: dict) -> "Pool": def from_mara(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], url=web_pool["url"],
user=web_pool["user"], user=web_pool["user"],
@@ -229,7 +237,7 @@ class Pool(MinerConfigValue):
) )
@classmethod @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']}" url = f"stratum+tcp://{web_system_info['stratumURL']}:{web_system_info['stratumPort']}"
return cls( return cls(
url=url, url=url,
@@ -238,11 +246,11 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_luxos(cls, rpc_pools: dict) -> "Pool": def from_luxos(cls, rpc_pools: dict) -> Pool:
return cls.from_api(rpc_pools) return cls.from_api(rpc_pools)
@classmethod @classmethod
def from_iceriver(cls, web_pool: dict) -> "Pool": def from_iceriver(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["addr"], url=web_pool["addr"],
user=web_pool["user"], user=web_pool["user"],
@@ -294,34 +302,32 @@ class PoolGroup(MinerConfigValue):
idx += 1 idx += 1
return pools return pools
def as_wm(self, user_suffix: str | None = None) -> dict: def as_wm(self, *args: Any, user_suffix: str | None = None, **kwargs: Any) -> dict:
pools = {} pools: dict[str, str] = {}
idx = 0 idx = 0
while idx < 3: while idx < 3:
if len(self.pools) > idx: if len(self.pools) > idx:
pools.update( pools.update(**self.pools[idx].as_wm(idx + 1, user_suffix=user_suffix))
**self.pools[idx].as_wm(idx=idx + 1, user_suffix=user_suffix)
)
else: else:
pools.update(**Pool(url="", user="", password="").as_wm(idx=idx + 1)) pools.update(**Pool(url="", user="", password="").as_wm(idx + 1))
idx += 1 idx += 1
return pools return pools
def as_btminer_v3(self, user_suffix: str | None = None) -> list: def as_btminer_v3(self, user_suffix: str | None = None) -> list:
return [pool.as_btminer_v3(user_suffix) for pool in self.pools[:3]] return [pool.as_btminer_v3(user_suffix) for pool in self.pools[:3]]
def as_am_old(self, user_suffix: str | None = None) -> dict: def as_am_old(
pools = {} self, *args: Any, user_suffix: str | None = None, **kwargs: Any
) -> dict:
pools: dict[str, str] = {}
idx = 0 idx = 0
while idx < 3: while idx < 3:
if len(self.pools) > idx: if len(self.pools) > idx:
pools.update( 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: else:
pools.update( pools.update(**Pool(url="", user="", password="").as_am_old(idx + 1))
**Pool(url="", user="", password="").as_am_old(idx=idx + 1)
)
idx += 1 idx += 1
return pools return pools
@@ -333,22 +339,24 @@ class PoolGroup(MinerConfigValue):
return self.pools[0].as_avalon(user_suffix=user_suffix) return self.pools[0].as_avalon(user_suffix=user_suffix)
return Pool(url="", user="", password="").as_avalon() return Pool(url="", user="", password="").as_avalon()
def as_inno(self, user_suffix: str | None = None) -> dict: def as_inno(
pools = {} self, *args: Any, user_suffix: str | None = None, **kwargs: Any
) -> dict:
pools: dict[str, str] = {}
idx = 0 idx = 0
while idx < 3: while idx < 3:
if len(self.pools) > idx: if len(self.pools) > idx:
pools.update( 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: else:
pools.update(**Pool(url="", user="", password="").as_inno(idx=idx + 1)) pools.update(**Pool(url="", user="", password="").as_inno(idx + 1))
idx += 1 idx += 1
return pools return pools
def as_bosminer(self, user_suffix: str | None = None) -> dict: def as_bosminer(self, user_suffix: str | None = None) -> dict:
if len(self.pools) > 0: if len(self.pools) > 0:
conf = { conf: dict[str, Any] = {
"name": self.name, "name": self.name,
"pool": [ "pool": [
pool.as_bosminer(user_suffix=user_suffix) for pool in self.pools 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: def as_boser(self, user_suffix: str | None = None) -> PoolGroupConfiguration:
return PoolGroupConfiguration( return PoolGroupConfiguration(
name=self.name, name=self.name or "",
quota=Quota(value=self.quota), quota=Quota(value=self.quota),
pools=[p.as_boser() for p in self.pools], 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]} return {"pools": [p.as_vnish(user_suffix=user_suffix) for p in self.pools]}
@classmethod @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 = {} cls_conf = {}
if dict_conf.get("quota") is not None: if dict_conf.get("quota") is not None:
@@ -393,57 +404,57 @@ class PoolGroup(MinerConfigValue):
return cls(**cls_conf) return cls(**cls_conf)
@classmethod @classmethod
def from_api(cls, api_pool_list: list) -> "PoolGroup": def from_api(cls, api_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in api_pool_list: for pool in api_pool_list:
pools.append(Pool.from_api(pool)) pools.append(Pool.from_api(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_btminer_v3(cls, api_pool_list: list) -> "PoolGroup": def from_btminer_v3(cls, api_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in api_pool_list: for pool in api_pool_list:
pools.append(Pool.from_btminer_v3(pool)) pools.append(Pool.from_btminer_v3(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_epic(cls, api_pool_list: list) -> "PoolGroup": def from_epic(cls, api_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in api_pool_list: for pool in api_pool_list:
pools.append(Pool.from_epic(pool)) pools.append(Pool.from_epic(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_am_modern(cls, web_pool_list: list) -> "PoolGroup": def from_am_modern(cls, web_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in web_pool_list: for pool in web_pool_list:
pools.append(Pool.from_am_modern(pool)) pools.append(Pool.from_am_modern(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_hiveon_modern(cls, web_pool_list: list) -> "PoolGroup": def from_hiveon_modern(cls, web_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in web_pool_list: for pool in web_pool_list:
pools.append(Pool.from_hiveon_modern(pool)) pools.append(Pool.from_hiveon_modern(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_elphapex(cls, web_pool_list: list) -> "PoolGroup": def from_elphapex(cls, web_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in web_pool_list: for pool in web_pool_list:
pools.append(Pool.from_elphapex(pool)) pools.append(Pool.from_elphapex(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @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]) return cls(pools=[Pool.from_goldshell(p) for p in web_pools])
@classmethod @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]) return cls(pools=[Pool.from_inno(p) for p in web_pools])
@classmethod @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: if toml_group_conf.get("pool") is not None:
return cls( return cls(
name=toml_group_conf["name"], name=toml_group_conf["name"],
@@ -453,13 +464,13 @@ class PoolGroup(MinerConfigValue):
return cls() return cls()
@classmethod @classmethod
def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup": def from_vnish(cls, web_settings_pools: dict) -> PoolGroup:
return cls( return cls(
pools=[Pool.from_vnish(p) for p in web_settings_pools if p["url"] != ""] pools=[Pool.from_vnish(p) for p in web_settings_pools if p["url"] != ""]
) )
@classmethod @classmethod
def from_boser(cls, grpc_pool_group: dict) -> "PoolGroup": def from_boser(cls, grpc_pool_group: dict) -> PoolGroup:
try: try:
return cls( return cls(
pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]], pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]],
@@ -474,15 +485,15 @@ class PoolGroup(MinerConfigValue):
return cls() return cls()
@classmethod @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]) return cls(pools=[Pool.from_mara(pool_conf) for pool_conf in web_config_pools])
@classmethod @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)]) return cls(pools=[Pool.from_espminer(web_system_info)])
@classmethod @classmethod
def from_iceriver(cls, web_userpanel: dict) -> "PoolGroup": def from_iceriver(cls, web_userpanel: dict) -> PoolGroup:
return cls( return cls(
pools=[ pools=[
Pool.from_iceriver(web_pool) Pool.from_iceriver(web_pool)
@@ -492,21 +503,21 @@ class PoolGroup(MinerConfigValue):
class PoolConfig(MinerConfigValue): class PoolConfig(MinerConfigValue):
groups: List[PoolGroup] = Field(default_factory=list) groups: list[PoolGroup] = Field(default_factory=list)
@classmethod @classmethod
def default(cls) -> "PoolConfig": def default(cls) -> PoolConfig:
return cls() return cls()
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolConfig": def from_dict(cls, dict_conf: dict | None) -> PoolConfig:
if dict_conf is None: if dict_conf is None:
return cls.default() return cls.default()
return cls(groups=[PoolGroup.from_dict(g) for g in dict_conf["groups"]]) return cls(groups=[PoolGroup.from_dict(g) for g in dict_conf["groups"]])
@classmethod @classmethod
def simple(cls, pools: list[Pool | dict[str, str]]) -> "PoolConfig": def simple(cls, pools: list[Pool | dict[str, str]]) -> PoolConfig:
group_pools = [] group_pools = []
for pool in pools: for pool in pools:
if isinstance(pool, dict): if isinstance(pool, dict):
@@ -529,7 +540,7 @@ class PoolConfig(MinerConfigValue):
return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)} return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_elphapex()} 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: if len(self.groups) > 0:
return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)} return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_wm()} 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": self.groups[0].as_btminer_v3(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_btminer_v3()} 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: if len(self.groups) > 0:
return self.groups[0].as_am_old(user_suffix=user_suffix) return self.groups[0].as_am_old(user_suffix=user_suffix)
return PoolGroup().as_am_old() 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": self.groups[0].as_avalon(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_avalon()} 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: if len(self.groups) > 0:
return self.groups[0].as_inno(user_suffix=user_suffix) return self.groups[0].as_inno(user_suffix=user_suffix)
return PoolGroup().as_inno() return PoolGroup().as_inno()
@@ -569,7 +584,7 @@ class PoolConfig(MinerConfigValue):
def as_boser(self, user_suffix: str | None = None) -> dict: def as_boser(self, user_suffix: str | None = None) -> dict:
return { return {
"set_pool_groups": SetPoolGroupsRequest( "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], 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) return self.groups[0].as_vnish(user_suffix=user_suffix)
@classmethod @classmethod
def from_api(cls, api_pools: dict) -> "PoolConfig": def from_api(cls, api_pools: dict) -> PoolConfig:
try: try:
pool_data = api_pools["POOLS"] pool_data = api_pools["POOLS"]
except KeyError: except KeyError:
@@ -625,7 +640,7 @@ class PoolConfig(MinerConfigValue):
return cls(groups=[PoolGroup.from_api(pool_data)]) return cls(groups=[PoolGroup.from_api(pool_data)])
@classmethod @classmethod
def from_btminer_v3(cls, rpc_pools: dict) -> "PoolConfig": def from_btminer_v3(cls, rpc_pools: dict) -> PoolConfig:
try: try:
pool_data = rpc_pools["pools"] pool_data = rpc_pools["pools"]
except KeyError: except KeyError:
@@ -635,12 +650,12 @@ class PoolConfig(MinerConfigValue):
return cls(groups=[PoolGroup.from_btminer_v3(pool_data)]) return cls(groups=[PoolGroup.from_btminer_v3(pool_data)])
@classmethod @classmethod
def from_epic(cls, web_conf: dict) -> "PoolConfig": def from_epic(cls, web_conf: dict) -> PoolConfig:
pool_data = web_conf["StratumConfigs"] pool_data = web_conf["StratumConfigs"]
return cls(groups=[PoolGroup.from_epic(pool_data)]) return cls(groups=[PoolGroup.from_epic(pool_data)])
@classmethod @classmethod
def from_am_modern(cls, web_conf: dict) -> "PoolConfig": def from_am_modern(cls, web_conf: dict) -> PoolConfig:
try: try:
pool_data = web_conf["pools"] pool_data = web_conf["pools"]
except KeyError: except KeyError:
@@ -649,7 +664,7 @@ class PoolConfig(MinerConfigValue):
return cls(groups=[PoolGroup.from_am_modern(pool_data)]) return cls(groups=[PoolGroup.from_am_modern(pool_data)])
@classmethod @classmethod
def from_hiveon_modern(cls, web_conf: dict) -> "PoolConfig": def from_hiveon_modern(cls, web_conf: dict) -> PoolConfig:
try: try:
pool_data = web_conf["pools"] pool_data = web_conf["pools"]
except KeyError: except KeyError:
@@ -658,17 +673,17 @@ class PoolConfig(MinerConfigValue):
return cls(groups=[PoolGroup.from_hiveon_modern(pool_data)]) return cls(groups=[PoolGroup.from_hiveon_modern(pool_data)])
@classmethod @classmethod
def from_elphapex(cls, web_conf: dict) -> "PoolConfig": def from_elphapex(cls, web_conf: dict) -> PoolConfig:
pool_data = web_conf["pools"] pool_data = web_conf["pools"]
return cls(groups=[PoolGroup.from_elphapex(pool_data)]) return cls(groups=[PoolGroup.from_elphapex(pool_data)])
@classmethod @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)]) return cls(groups=[PoolGroup.from_goldshell(web_pools)])
@classmethod @classmethod
def from_goldshell_byte(cls, web_pools: list) -> "PoolConfig": def from_goldshell_byte(cls, web_pools: list) -> PoolConfig:
return cls( return cls(
groups=[ groups=[
PoolGroup.from_goldshell(g["pools"]) PoolGroup.from_goldshell(g["pools"])
@@ -678,25 +693,25 @@ class PoolConfig(MinerConfigValue):
) )
@classmethod @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)]) return cls(groups=[PoolGroup.from_inno(web_pools)])
@classmethod @classmethod
def from_bosminer(cls, toml_conf: dict) -> "PoolConfig": def from_bosminer(cls, toml_conf: dict) -> PoolConfig:
if toml_conf.get("group") is None: if toml_conf.get("group") is None:
return cls() return cls()
return cls(groups=[PoolGroup.from_bosminer(g) for g in toml_conf["group"]]) return cls(groups=[PoolGroup.from_bosminer(g) for g in toml_conf["group"]])
@classmethod @classmethod
def from_vnish(cls, web_settings: dict) -> "PoolConfig": def from_vnish(cls, web_settings: dict) -> PoolConfig:
try: try:
return cls(groups=[PoolGroup.from_vnish(web_settings["miner"]["pools"])]) return cls(groups=[PoolGroup.from_vnish(web_settings["miner"]["pools"])])
except LookupError: except LookupError:
return cls() return cls()
@classmethod @classmethod
def from_boser(cls, grpc_miner_conf: dict) -> "PoolConfig": def from_boser(cls, grpc_miner_conf: dict) -> PoolConfig:
try: try:
return cls( return cls(
groups=[ groups=[
@@ -708,19 +723,19 @@ class PoolConfig(MinerConfigValue):
return cls() return cls()
@classmethod @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"])]) return cls(groups=[PoolGroup.from_mara(web_config["pools"])])
@classmethod @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)]) return cls(groups=[PoolGroup.from_espminer(web_system_info)])
@classmethod @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)]) return cls(groups=[PoolGroup.from_iceriver(web_userpanel)])
@classmethod @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( return cls(
groups=[ groups=[
PoolGroup( PoolGroup(

View File

@@ -40,7 +40,7 @@ class TemperatureConfig(MinerConfigValue):
return {"temp_control": temp_cfg} return {"temp_control": temp_cfg}
def as_epic(self) -> dict: def as_epic(self) -> dict:
temps_config = {"temps": {}, "fans": {"Auto": {}}} temps_config: dict = {"temps": {}, "fans": {"Auto": {}}}
if self.target is not None: if self.target is not None:
temps_config["fans"]["Auto"]["Target Temperature"] = self.target temps_config["fans"]["Auto"]["Target Temperature"] = self.target
else: else:
@@ -58,7 +58,9 @@ class TemperatureConfig(MinerConfigValue):
return {"misc": {"restart_temp": self.danger}} return {"misc": {"restart_temp": self.danger}}
@classmethod @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( return cls(
target=dict_conf.get("target"), target=dict_conf.get("target"),
hot=dict_conf.get("hot"), hot=dict_conf.get("hot"),
@@ -66,7 +68,7 @@ class TemperatureConfig(MinerConfigValue):
) )
@classmethod @classmethod
def from_bosminer(cls, toml_conf: dict) -> "TemperatureConfig": def from_bosminer(cls, toml_conf: dict) -> TemperatureConfig:
temp_control = toml_conf.get("temp_control") temp_control = toml_conf.get("temp_control")
if temp_control is not None: if temp_control is not None:
return cls( return cls(
@@ -77,7 +79,7 @@ class TemperatureConfig(MinerConfigValue):
return cls() return cls()
@classmethod @classmethod
def from_epic(cls, web_conf: dict) -> "TemperatureConfig": def from_epic(cls, web_conf: dict) -> TemperatureConfig:
try: try:
dangerous_temp = web_conf["Misc"]["Critical Temp"] dangerous_temp = web_conf["Misc"]["Critical Temp"]
except KeyError: except KeyError:
@@ -95,7 +97,7 @@ class TemperatureConfig(MinerConfigValue):
return cls(target=target_temp, hot=hot_temp, danger=dangerous_temp) return cls(target=target_temp, hot=hot_temp, danger=dangerous_temp)
@classmethod @classmethod
def from_vnish(cls, web_settings: dict) -> "TemperatureConfig": def from_vnish(cls, web_settings: dict) -> TemperatureConfig:
try: try:
dangerous_temp = web_settings["misc"]["restart_temp"] dangerous_temp = web_settings["misc"]["restart_temp"]
except KeyError: except KeyError:
@@ -111,7 +113,7 @@ class TemperatureConfig(MinerConfigValue):
return cls() return cls()
@classmethod @classmethod
def from_boser(cls, grpc_miner_conf: dict) -> "TemperatureConfig": def from_boser(cls, grpc_miner_conf: dict) -> TemperatureConfig:
try: try:
temperature_conf = grpc_miner_conf["temperature"] temperature_conf = grpc_miner_conf["temperature"]
except KeyError: except KeyError:
@@ -142,7 +144,7 @@ class TemperatureConfig(MinerConfigValue):
return cls.default() return cls.default()
@classmethod @classmethod
def from_luxos(cls, rpc_tempctrl: dict) -> "TemperatureConfig": def from_luxos(cls, rpc_tempctrl: dict) -> TemperatureConfig:
try: try:
tempctrl_config = rpc_tempctrl["TEMPCTRL"][0] tempctrl_config = rpc_tempctrl["TEMPCTRL"][0]
return cls( return cls(

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import copy import copy
import time import time
from collections.abc import Callable
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any
@@ -24,6 +25,7 @@ from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune from pyasic.config.mining import MiningModePowerTune
from pyasic.data.pools import PoolMetrics, Scheme from pyasic.data.pools import PoolMetrics, Scheme
from pyasic.device.algorithm.hashrate import AlgoHashRateType from pyasic.device.algorithm.hashrate import AlgoHashRateType
from pyasic.device.algorithm.hashrate.base import GenericHashrate
from .boards import HashBoard from .boards import HashBoard
from .device import DeviceInfo from .device import DeviceInfo
@@ -90,7 +92,9 @@ class MinerData(BaseModel):
hostname: str | None = None hostname: str | None = None
# hashrate # hashrate
raw_hashrate: AlgoHashRateType = Field(exclude=True, default=None, repr=False) raw_hashrate: AlgoHashRateType | None = Field(
exclude=True, default=None, repr=False
)
# sticker # sticker
sticker_hashrate: AlgoHashRateType | None = None sticker_hashrate: AlgoHashRateType | None = None
@@ -194,7 +198,7 @@ class MinerData(BaseModel):
setattr(cp, key, item & other_item) setattr(cp, key, item & other_item)
return cp return cp
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def hashrate(self) -> AlgoHashRateType | None: def hashrate(self) -> AlgoHashRateType | None:
if len(self.hashboards) > 0: if len(self.hashboards) > 0:
@@ -203,14 +207,24 @@ class MinerData(BaseModel):
if item.hashrate is not None: if item.hashrate is not None:
hr_data.append(item.hashrate) hr_data.append(item.hashrate)
if len(hr_data) > 0: 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 return self.raw_hashrate
@hashrate.setter @hashrate.setter
def hashrate(self, val): def hashrate(self, val):
self.raw_hashrate = val self.raw_hashrate = val
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def wattage_limit(self) -> int | None: def wattage_limit(self) -> int | None:
if self.config is not None: if self.config is not None:
@@ -222,7 +236,7 @@ class MinerData(BaseModel):
def wattage_limit(self, val: int): def wattage_limit(self, val: int):
self.raw_wattage_limit = val self.raw_wattage_limit = val
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def total_chips(self) -> int | None: def total_chips(self) -> int | None:
if len(self.hashboards) > 0: if len(self.hashboards) > 0:
@@ -233,15 +247,16 @@ class MinerData(BaseModel):
if len(chip_data) > 0: if len(chip_data) > 0:
return sum(chip_data) return sum(chip_data)
return None return None
return 0
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def nominal(self) -> bool | None: def nominal(self) -> bool | None:
if self.total_chips is None or self.expected_chips is None: if self.total_chips is None or self.expected_chips is None:
return None return None
return self.expected_chips == self.total_chips return self.expected_chips == self.total_chips
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def percent_expected_chips(self) -> int | None: def percent_expected_chips(self) -> int | None:
if self.total_chips is None or self.expected_chips is None: if self.total_chips is None or self.expected_chips is None:
@@ -250,7 +265,7 @@ class MinerData(BaseModel):
return 0 return 0
return round((self.total_chips / self.expected_chips) * 100) return round((self.total_chips / self.expected_chips) * 100)
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def percent_expected_hashrate(self) -> int | None: def percent_expected_hashrate(self) -> int | None:
if self.hashrate is None or self.expected_hashrate is None: if self.hashrate is None or self.expected_hashrate is None:
@@ -260,7 +275,7 @@ class MinerData(BaseModel):
except ZeroDivisionError: except ZeroDivisionError:
return 0 return 0
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def percent_expected_wattage(self) -> int | None: def percent_expected_wattage(self) -> int | None:
if self.wattage_limit is None or self.wattage is None: if self.wattage_limit is None or self.wattage is None:
@@ -270,10 +285,10 @@ class MinerData(BaseModel):
except ZeroDivisionError: except ZeroDivisionError:
return 0 return 0
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def temperature_avg(self) -> int | None: def temperature_avg(self) -> int | None:
total_temp = 0 total_temp: float = 0
temp_count = 0 temp_count = 0
for hb in self.hashboards: for hb in self.hashboards:
if hb.temp is not None: if hb.temp is not None:
@@ -283,7 +298,7 @@ class MinerData(BaseModel):
return None return None
return round(total_temp / temp_count) return round(total_temp / temp_count)
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def efficiency(self) -> int | None: def efficiency(self) -> int | None:
efficiency = self._efficiency(0) efficiency = self._efficiency(0)
@@ -292,7 +307,7 @@ class MinerData(BaseModel):
else: else:
return int(efficiency) return int(efficiency)
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def efficiency_fract(self) -> float | None: def efficiency_fract(self) -> float | None:
return self._efficiency(2) return self._efficiency(2)
@@ -305,39 +320,43 @@ class MinerData(BaseModel):
except ZeroDivisionError: except ZeroDivisionError:
return 0.0 return 0.0
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def datetime(self) -> str: def datetime(self) -> str:
return self.raw_datetime.isoformat() return self.raw_datetime.isoformat()
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def timestamp(self) -> int: def timestamp(self) -> int:
return int(time.mktime(self.raw_datetime.timetuple())) return int(time.mktime(self.raw_datetime.timetuple()))
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def make(self) -> str | None: 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 str(self.device_info.make)
return ""
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def model(self) -> str | None: 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 str(self.device_info.model)
return ""
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def firmware(self) -> str | None: 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 str(self.device_info.firmware)
return ""
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def algo(self) -> str | None: 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 str(self.device_info.algo)
return ""
def keys(self) -> list: def keys(self) -> list:
return list(self.model_fields.keys()) return list(self.model_fields.keys())
@@ -417,7 +436,8 @@ class MinerData(BaseModel):
for dt in serialization_map_instance: for dt in serialization_map_instance:
if item_serialized is None: if item_serialized is None:
if isinstance(list_field_val, dt): 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 f"{key}{level_delimiter}{idx}", list_field_val
) )
if item_serialized is not None: if item_serialized is not None:
@@ -461,11 +481,11 @@ class MinerData(BaseModel):
"pools", "pools",
] ]
serialization_map_instance = { serialization_map_instance: dict[type, Callable[[str, Any], str | None]] = {
AlgoHashRateType: serialize_algo_hash_rate, AlgoHashRateType: serialize_algo_hash_rate,
BaseMinerError: serialize_miner_error, BaseMinerError: serialize_miner_error,
} }
serialization_map = { serialization_map: dict[type, Callable[[str, Any], str | None]] = {
int: serialize_int, int: serialize_int,
float: serialize_float, float: serialize_float,
str: serialize_str, str: serialize_str,
@@ -499,9 +519,8 @@ class MinerData(BaseModel):
for datatype in serialization_map_instance: for datatype in serialization_map_instance:
if serialized is None: if serialized is None:
if isinstance(field_val, datatype): if isinstance(field_val, datatype):
serialized = serialization_map_instance[datatype]( func = serialization_map_instance[datatype]
field, field_val serialized = func(field, field_val)
)
if serialized is not None: if serialized is not None:
field_data.append(serialized) field_data.append(serialized)

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from typing import Any from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
@@ -89,7 +90,7 @@ class HashBoard(BaseModel):
def serialize_algo_hash_rate(key: str, value: AlgoHashRateType) -> str: def serialize_algo_hash_rate(key: str, value: AlgoHashRateType) -> str:
return f"{key}={round(float(value), 2)}" 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()}" return f"{key}={str(value).lower()}"
serialization_map_instance = { serialization_map_instance = {
@@ -116,8 +117,11 @@ class HashBoard(BaseModel):
field_data = [] field_data = []
for field in include: for field in include:
field_val = getattr(self, field) field_val = getattr(self, field)
serialization_func = serialization_map.get( serialization_func: Callable[[str, Any], str | None] = (
type(field_val), lambda _k, _v: None serialization_map.get(
type(field_val),
lambda _k, _v: None, # type: ignore
)
) )
serialized = serialization_func( serialized = serialization_func(
f"{key_root}{level_delimiter}{field}", field_val f"{key_root}{level_delimiter}{field}", field_val

View File

@@ -2,6 +2,8 @@ from pydantic import BaseModel
class BaseMinerError(BaseModel): class BaseMinerError(BaseModel):
error_code: int | None = None
@classmethod @classmethod
def fields(cls): def fields(cls):
return list(cls.model_fields.keys()) return list(cls.model_fields.keys())
@@ -24,9 +26,13 @@ class BaseMinerError(BaseModel):
field_data.append( field_data.append(
f"{root_key}{level_delimiter}error_code={self.error_code}" f"{root_key}{level_delimiter}error_code={self.error_code}"
) )
if self.error_message is not None:
field_data.append( # Check if error_message exists as an attribute (either regular or computed field)
f'{root_key}{level_delimiter}error_message="{self.error_message}"' 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="{error_message}"'
)
return ",".join(field_data) return ",".join(field_data)

View File

@@ -30,7 +30,7 @@ class InnosiliconError(BaseMinerError):
error_code: int error_code: int
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def error_message(self) -> str: # noqa - Skip PyCharm inspection def error_message(self) -> str: # noqa - Skip PyCharm inspection
if self.error_code in ERROR_CODES: if self.error_code in ERROR_CODES:

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pydantic import computed_field from pydantic import computed_field
from pyasic.data.error_codes.base import BaseMinerError from pyasic.data.error_codes.base import BaseMinerError
@@ -28,50 +29,69 @@ class WhatsminerError(BaseMinerError):
error_code: int error_code: int
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def error_message(self) -> str: # noqa - Skip PyCharm inspection def error_message(self) -> str: # noqa - Skip PyCharm inspection
if len(str(self.error_code)) == 6 and not str(self.error_code)[:1] == "1": error_str = str(self.error_code)
err_type = int(str(self.error_code)[:2])
err_subtype = int(str(self.error_code)[2:3]) # Handle edge cases for short error codes
err_value = int(str(self.error_code)[3:]) 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: else:
err_type = int(str(self.error_code)[:-2]) err_type = int(error_str[:-2])
err_subtype = int(str(self.error_code)[-2:-1]) err_subtype = int(error_str[-2:-1])
err_value = int(str(self.error_code)[-1:]) err_value = int(error_str[-1:])
try: 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: if err_subtype in select_err_type:
select_err_subtype = select_err_type[err_subtype] select_err_subtype = select_err_type[err_subtype]
if err_value in select_err_subtype: if isinstance(select_err_subtype, dict):
return select_err_subtype[err_value] if err_value in select_err_subtype:
elif "n" in select_err_subtype: result = select_err_subtype[err_value]
return select_err_subtype[ return str(result) if not isinstance(result, str) else result
"n" # noqa: picks up `select_err_subtype["n"]` as not being numeric? elif "n" in select_err_subtype:
].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: else:
return "Unknown error type." return "Unknown error type."
elif "n" in select_err_type: elif "n" in select_err_type:
select_err_subtype = select_err_type[ select_err_subtype = select_err_type["n"]
"n" # noqa: picks up `select_err_subtype["n"]` as not being numeric? if isinstance(select_err_subtype, dict):
] if err_value in select_err_subtype:
if err_value in select_err_subtype: result = select_err_subtype[err_value]
return select_err_subtype[err_value] return str(result) if not isinstance(result, str) else result
elif "c" in select_err_subtype: elif "c" in select_err_subtype:
return ( template = select_err_subtype["c"]
select_err_subtype["c"] if isinstance(template, str):
.replace( # noqa: picks up `select_err_subtype["n"]` as not being numeric? return template.replace("{n}", str(err_subtype)).replace(
"{n}", str(err_subtype) "{c}", str(err_value)
) )
.replace("{c}", str(err_value)) else:
) return "Unknown error type."
else:
return "Unknown error type."
else:
return "Unknown error type."
else: else:
return "Unknown error type." return "Unknown error type."
except KeyError: except (KeyError, TypeError):
return "Unknown error type." return "Unknown error type."
ERROR_CODES = { ERROR_CODES: dict[int, dict[int | str, str | dict[int | str, str]]] = {
1: { # Fan error 1: { # Fan error
0: { 0: {
0: "Fan unknown.", 0: "Fan unknown.",

View File

@@ -26,7 +26,7 @@ class Fan(BaseModel):
speed: The speed of the fan. speed: The speed of the fan.
""" """
speed: int = None speed: int | None = None
def get(self, __key: str, default: Any = None): def get(self, __key: str, default: Any = None):
try: try:

View File

@@ -1,5 +1,6 @@
from collections.abc import Callable
from enum import Enum from enum import Enum
from typing import Optional from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
from pydantic import BaseModel, computed_field, model_serializer from pydantic import BaseModel, computed_field, model_serializer
@@ -16,7 +17,7 @@ class PoolUrl(BaseModel):
scheme: Scheme scheme: Scheme
host: str host: str
port: int port: int
pubkey: Optional[str] = None pubkey: str | None = None
@model_serializer @model_serializer
def serialize(self): def serialize(self):
@@ -39,6 +40,8 @@ class PoolUrl(BaseModel):
scheme = Scheme.STRATUM_V1 scheme = Scheme.STRATUM_V1
host = parsed_url.hostname host = parsed_url.hostname
port = parsed_url.port port = parsed_url.port
if port is None:
return None
pubkey = parsed_url.path.lstrip("/") if scheme == Scheme.STRATUM_V2 else None pubkey = parsed_url.path.lstrip("/") if scheme == Scheme.STRATUM_V2 else None
return cls(scheme=scheme, host=host, port=port, pubkey=pubkey) return cls(scheme=scheme, host=host, port=port, pubkey=pubkey)
@@ -70,16 +73,20 @@ class PoolMetrics(BaseModel):
index: int | None = None index: int | None = None
user: str | None = None user: str | None = None
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def pool_rejected_percent(self) -> float: # noqa - Skip PyCharm inspection def pool_rejected_percent(self) -> float: # noqa - Skip PyCharm inspection
"""Calculate and return the percentage of rejected shares""" """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) return self._calculate_percentage(self.rejected, self.accepted + self.rejected)
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def pool_stale_percent(self) -> float: # noqa - Skip PyCharm inspection def pool_stale_percent(self) -> float: # noqa - Skip PyCharm inspection
"""Calculate and return the percentage of stale shares.""" """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( return self._calculate_percentage(
self.get_failures, self.accepted + self.rejected self.get_failures, self.accepted + self.rejected
) )
@@ -87,10 +94,8 @@ class PoolMetrics(BaseModel):
@staticmethod @staticmethod
def _calculate_percentage(value: int, total: int) -> float: def _calculate_percentage(value: int, total: int) -> float:
"""Calculate the percentage.""" """Calculate the percentage."""
if value is None or total is None:
return 0
if total == 0: if total == 0:
return 0 return 0.0
return (value / total) * 100 return (value / total) * 100
def as_influxdb(self, key_root: str, level_delimiter: str = ".") -> str: 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: def serialize_str(key: str, value: str) -> str:
return f'{key}="{value}"' 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)}"' 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()}" return f"{key}={str(value).lower()}"
serialization_map = { serialization_map: dict[type, Callable[[str, Any], str]] = {
int: serialize_int, int: serialize_int,
float: serialize_float, float: serialize_float,
str: serialize_str, str: serialize_str,
@@ -129,13 +134,14 @@ class PoolMetrics(BaseModel):
field_data = [] field_data = []
for field in include: for field in include:
field_val = getattr(self, field) field_val = getattr(self, field)
serialization_func = serialization_map.get( if field_val is None:
type(field_val), lambda _k, _v: None continue
) serialization_func = serialization_map.get(type(field_val))
serialized = serialization_func( if serialization_func is not None:
f"{key_root}{level_delimiter}{field}", field_val serialized = serialization_func(
) f"{key_root}{level_delimiter}{field}", field_val
if serialized is not None: )
field_data.append(serialized) if serialized is not None:
field_data.append(serialized)
return ",".join(field_data) return ",".join(field_data)

View File

@@ -1,23 +1,26 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Generic, TypeVar
from pydantic import BaseModel, field_serializer from pydantic import BaseModel, field_serializer
from typing_extensions import Self from typing_extensions import Self
from .unit.base import AlgoHashRateUnitType, GenericUnit 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 rate: float
@field_serializer("unit") @field_serializer("unit")
def serialize_unit(self, unit: AlgoHashRateUnitType): def serialize_unit(self, unit: UnitType):
return unit.model_dump() return unit.model_dump()
@abstractmethod @abstractmethod
def into(self, other: "AlgoHashRateUnitType"): def into(self, other: UnitType) -> Self:
pass pass
def auto_unit(self): def auto_unit(self):
@@ -46,7 +49,7 @@ class AlgoHashRateType(BaseModel, ABC):
def __repr__(self): def __repr__(self):
return f"{self.rate} {str(self.unit)}" 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) return round(self.rate, n)
def __add__(self, other: Self | int | float) -> Self: 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) return self.__class__(rate=self.rate * other, unit=self.unit)
class GenericHashrate(AlgoHashRateType): class GenericHashrate(AlgoHashRateType[GenericUnit]):
rate: float = 0 rate: float = 0
unit: GenericUnit = GenericUnit.H unit: GenericUnit = GenericUnit.H
def into(self, other: GenericUnit): def into(self, other: GenericUnit) -> Self:
return self.__class__( return self.__class__(
rate=self.rate / (other.value / self.unit.value), unit=other rate=self.rate / (other.value / self.unit.value), unit=other
) )

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.blake256 import Blake256Unit
from .unit import HashUnit from .unit import HashUnit
class Blake256HashRate(AlgoHashRateType): class Blake256HashRate(AlgoHashRateType[Blake256Unit]):
rate: float rate: float
unit: Blake256Unit = HashUnit.BLAKE256.default unit: Blake256Unit = HashUnit.BLAKE256.default

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.blockflow import BlockFlowUnit
from .unit import HashUnit from .unit import HashUnit
class BlockFlowHashRate(AlgoHashRateType): class BlockFlowHashRate(AlgoHashRateType[BlockFlowUnit]):
rate: float rate: float
unit: BlockFlowUnit = HashUnit.BLOCKFLOW.default unit: BlockFlowUnit = HashUnit.BLOCKFLOW.default

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.eaglesong import EaglesongUnit
from .unit import HashUnit from .unit import HashUnit
class EaglesongHashRate(AlgoHashRateType): class EaglesongHashRate(AlgoHashRateType[EaglesongUnit]):
rate: float rate: float
unit: EaglesongUnit = HashUnit.EAGLESONG.default unit: EaglesongUnit = HashUnit.EAGLESONG.default

View File

@@ -8,9 +8,9 @@ from pyasic.device.algorithm.hashrate.unit.equihash import EquihashUnit
from .unit import HashUnit from .unit import HashUnit
class EquihashHashRate(AlgoHashRateType): class EquihashHashRate(AlgoHashRateType[EquihashUnit]):
rate: float rate: float
unit: EquihashUnit = HashUnit.ETHASH.default unit: EquihashUnit = HashUnit.EQUIHASH.default
def into(self, other: EquihashUnit) -> Self: def into(self, other: EquihashUnit) -> Self:
return self.__class__( return self.__class__(

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.ethash import EtHashUnit
from .unit import HashUnit from .unit import HashUnit
class EtHashHashRate(AlgoHashRateType): class EtHashHashRate(AlgoHashRateType[EtHashUnit]):
rate: float rate: float
unit: EtHashUnit = HashUnit.ETHASH.default unit: EtHashUnit = HashUnit.ETHASH.default

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.handshake import HandshakeUnit
from .unit import HashUnit from .unit import HashUnit
class HandshakeHashRate(AlgoHashRateType): class HandshakeHashRate(AlgoHashRateType[HandshakeUnit]):
rate: float rate: float
unit: HandshakeUnit = HashUnit.HANDSHAKE.default unit: HandshakeUnit = HashUnit.HANDSHAKE.default

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.kadena import KadenaUnit
from .unit import HashUnit from .unit import HashUnit
class KadenaHashRate(AlgoHashRateType): class KadenaHashRate(AlgoHashRateType[KadenaUnit]):
rate: float rate: float
unit: KadenaUnit = HashUnit.KADENA.default unit: KadenaUnit = HashUnit.KADENA.default

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.kheavyhash import KHeavyHashUnit
from .unit import HashUnit from .unit import HashUnit
class KHeavyHashHashRate(AlgoHashRateType): class KHeavyHashHashRate(AlgoHashRateType[KHeavyHashUnit]):
rate: float rate: float
unit: KHeavyHashUnit = HashUnit.KHEAVYHASH.default unit: KHeavyHashUnit = HashUnit.KHEAVYHASH.default

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.scrypt import ScryptUnit
from .unit import HashUnit from .unit import HashUnit
class ScryptHashRate(AlgoHashRateType): class ScryptHashRate(AlgoHashRateType[ScryptUnit]):
rate: float rate: float
unit: ScryptUnit = HashUnit.SCRYPT.default unit: ScryptUnit = HashUnit.SCRYPT.default

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.sha256 import SHA256Unit
from .unit import HashUnit from .unit import HashUnit
class SHA256HashRate(AlgoHashRateType): class SHA256HashRate(AlgoHashRateType[SHA256Unit]):
rate: float rate: float
unit: SHA256Unit = HashUnit.SHA256.default unit: SHA256Unit = HashUnit.SHA256.default

View File

@@ -2,54 +2,46 @@ from enum import IntEnum
class AlgoHashRateUnitType(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): def __str__(self):
if self.value == self.H: if hasattr(self.__class__, "H") and self.value == self.__class__.H:
return "H/s" return "H/s"
if self.value == self.KH: if hasattr(self.__class__, "KH") and self.value == self.__class__.KH:
return "KH/s" return "KH/s"
if self.value == self.MH: if hasattr(self.__class__, "MH") and self.value == self.__class__.MH:
return "MH/s" return "MH/s"
if self.value == self.GH: if hasattr(self.__class__, "GH") and self.value == self.__class__.GH:
return "GH/s" return "GH/s"
if self.value == self.TH: if hasattr(self.__class__, "TH") and self.value == self.__class__.TH:
return "TH/s" return "TH/s"
if self.value == self.PH: if hasattr(self.__class__, "PH") and self.value == self.__class__.PH:
return "PH/s" return "PH/s"
if self.value == self.EH: if hasattr(self.__class__, "EH") and self.value == self.__class__.EH:
return "EH/s" return "EH/s"
if self.value == self.ZH: if hasattr(self.__class__, "ZH") and self.value == self.__class__.ZH:
return "ZH/s" return "ZH/s"
return ""
@classmethod @classmethod
def from_str(cls, value: str): def from_str(cls, value: str):
if value == "H": if value == "H" and hasattr(cls, "H"):
return cls.H return cls.H
elif value == "KH": elif value == "KH" and hasattr(cls, "KH"):
return cls.KH return cls.KH
elif value == "MH": elif value == "MH" and hasattr(cls, "MH"):
return cls.MH return cls.MH
elif value == "GH": elif value == "GH" and hasattr(cls, "GH"):
return cls.GH return cls.GH
elif value == "TH": elif value == "TH" and hasattr(cls, "TH"):
return cls.TH return cls.TH
elif value == "PH": elif value == "PH" and hasattr(cls, "PH"):
return cls.PH return cls.PH
elif value == "EH": elif value == "EH" and hasattr(cls, "EH"):
return cls.EH return cls.EH
elif value == "ZH": elif value == "ZH" and hasattr(cls, "ZH"):
return cls.ZH return cls.ZH
return cls.default if hasattr(cls, "default"):
return cls.default
return None
def __repr__(self): def __repr__(self):
return str(self) return str(self)

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.x11 import X11Unit
from .unit import HashUnit from .unit import HashUnit
class X11HashRate(AlgoHashRateType): class X11HashRate(AlgoHashRateType[X11Unit]):
rate: float rate: float
unit: X11Unit = HashUnit.X11.default unit: X11Unit = HashUnit.X11.default

View File

@@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.zksnark import ZkSnarkUnit
from .unit import HashUnit from .unit import HashUnit
class ZkSnarkHashRate(AlgoHashRateType): class ZkSnarkHashRate(AlgoHashRateType[ZkSnarkUnit]):
rate: float rate: float
unit: ZkSnarkUnit = HashUnit.ZKSNARK.default unit: ZkSnarkUnit = HashUnit.ZKSNARK.default

View File

@@ -224,6 +224,7 @@ class WhatsminerModels(MinerModelType):
M31V20 = "M31 V20" M31V20 = "M31 V20"
M32V10 = "M32 V10" M32V10 = "M32 V10"
M32V20 = "M32 V20" M32V20 = "M32 V20"
M32S = "M32S"
M33SPlusPlusVG40 = "M33S++ VG40" M33SPlusPlusVG40 = "M33S++ VG40"
M33SPlusPlusVH20 = "M33S++ VH20" M33SPlusPlusVH20 = "M33S++ VH20"
M33SPlusPlusVH30 = "M33S++ VH30" M33SPlusPlusVH30 = "M33S++ VH30"
@@ -562,12 +563,18 @@ class BraiinsModels(MinerModelType):
BMM100 = "BMM100" BMM100 = "BMM100"
BMM101 = "BMM101" BMM101 = "BMM101"
def __str__(self):
return self.value
class ElphapexModels(MinerModelType): class ElphapexModels(MinerModelType):
DG1 = "DG1" DG1 = "DG1"
DG1Plus = "DG1+" DG1Plus = "DG1+"
DG1Home = "DG1Home" DG1Home = "DG1Home"
def __str__(self):
return self.value
class MinerModel: class MinerModel:
ANTMINER = AntminerModels ANTMINER = AntminerModels

View File

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

View File

@@ -14,7 +14,7 @@
# limitations under the License. - # 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.backends import ePIC
from pyasic.miners.device.models import ( from pyasic.miners.device.models import (
S19, S19,
@@ -56,12 +56,12 @@ class ePICS19XP(ePIC, S19XP):
class ePICS19jProDual(ePIC, S19jPro): class ePICS19jProDual(ePIC, S19jPro):
raw_model = MinerModel.EPIC.S19jProDual raw_model: MinerModelType = MinerModel.EPIC.S19jProDual
expected_fans = S19jPro.expected_fans * 2 expected_fans = S19jPro.expected_fans * 2
expected_hashboards = S19jPro.expected_hashboards * 2 expected_hashboards = S19jPro.expected_hashboards * 2
class ePICS19kProDual(ePIC, S19kPro): class ePICS19kProDual(ePIC, S19kPro):
raw_model = MinerModel.EPIC.S19kProDual raw_model: MinerModelType = MinerModel.EPIC.S19kProDual
expected_fans = S19kPro.expected_fans * 2 expected_fans = S19kPro.expected_fans * 2
expected_hashboards = S19kPro.expected_hashboards * 2 expected_hashboards = S19kPro.expected_hashboards * 2

View File

@@ -14,7 +14,6 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic.data import HashBoard from pyasic.data import HashBoard
from pyasic.device.algorithm import AlgoHashRate, HashUnit from pyasic.device.algorithm import AlgoHashRate, HashUnit
@@ -76,7 +75,7 @@ class HiveonT9(HiveonOld, T9):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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 = [ hashboards = [
HashBoard(slot=board, expected_chips=self.expected_chips) HashBoard(slot=board, expected_chips=self.expected_chips)
for board in range(self.expected_hashboards) for board in range(self.expected_hashboards)
@@ -84,7 +83,7 @@ class HiveonT9(HiveonOld, T9):
if rpc_stats is None: if rpc_stats is None:
try: try:
rpc_stats = self.rpc.stats() rpc_stats = await self.rpc.stats()
except APIError: except APIError:
return [] return []
@@ -98,7 +97,7 @@ class HiveonT9(HiveonOld, T9):
hashrate = 0 hashrate = 0
chips = 0 chips = 0
for chipset in board_map[board]: 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: try:
hashboards[board].temp = rpc_stats["STATS"][1][f"temp{chipset}"] hashboards[board].temp = rpc_stats["STATS"][1][f"temp{chipset}"]
hashboards[board].chip_temp = rpc_stats["STATS"][1][ hashboards[board].chip_temp = rpc_stats["STATS"][1][
@@ -108,11 +107,12 @@ class HiveonT9(HiveonOld, T9):
pass pass
else: else:
hashboards[board].missing = False hashboards[board].missing = False
try: if rpc_stats is not None:
hashrate += rpc_stats["STATS"][1][f"chain_rate{chipset}"] try:
chips += rpc_stats["STATS"][1][f"chain_acn{chipset}"] hashrate += rpc_stats["STATS"][1][f"chain_rate{chipset}"]
except (KeyError, IndexError): chips += rpc_stats["STATS"][1][f"chain_acn{chipset}"]
pass except (KeyError, IndexError):
pass
hashboards[board].hashrate = AlgoHashRate.SHA256( hashboards[board].hashrate = AlgoHashRate.SHA256(
rate=float(hashrate), unit=HashUnit.SHA256.GH rate=float(hashrate), unit=HashUnit.SHA256.GH
).into(self.algo.unit.default) ).into(self.algo.unit.default)
@@ -120,8 +120,8 @@ class HiveonT9(HiveonOld, T9):
return hashboards return hashboards
async def _get_env_temp(self, rpc_stats: dict = None) -> Optional[float]: async def _get_env_temp(self, rpc_stats: dict | None = None) -> float | None:
env_temp_list = [] env_temp_list: list[int] = []
board_map = { board_map = {
0: [2, 9, 10], 0: [2, 9, 10],
1: [3, 11, 12], 1: [3, 11, 12],
@@ -144,3 +144,4 @@ class HiveonT9(HiveonOld, T9):
if not env_temp_list == []: if not env_temp_list == []:
return round(sum(env_temp_list) / len(env_temp_list)) return round(sum(env_temp_list) / len(env_temp_list))
return None

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import LUXMiner 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): class LUXMinerT21(LUXMiner, T21):

View File

@@ -87,7 +87,3 @@ class VNishS19ProHydro(VNish, S19ProHydro):
class VNishS19kPro(VNish, S19kPro): class VNishS19kPro(VNish, S19kPro):
pass pass
class VNishS19ProA(VNish, S19ProA):
pass

View File

@@ -13,11 +13,11 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional from typing import Any
from pyasic import APIError from pyasic import APIError
from pyasic.data.boards import HashBoard 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.backends import AvalonMiner
from pyasic.miners.data import ( from pyasic.miners.data import (
DataFunction, DataFunction,
@@ -150,12 +150,12 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3):
data_locations = AVALON_NANO_DATA_LOC 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: if web_minerinfo is None:
try: try:
web_minerinfo = await self.web.minerinfo() web_minerinfo = await self.web.minerinfo()
except APIError: except APIError:
pass return None
if web_minerinfo is not None: if web_minerinfo is not None:
try: try:
@@ -164,17 +164,18 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3):
return mac.upper() return mac.upper()
except (KeyError, ValueError): except (KeyError, ValueError):
pass pass
return None
class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s): class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
data_locations = AVALON_NANO3S_DATA_LOC 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: if rpc_estats is None:
try: try:
rpc_estats = await self.rpc.estats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass return None
if rpc_estats is not None: if rpc_estats is not None:
try: try:
@@ -182,13 +183,16 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
return int(parsed_estats["PS"][6]) return int(parsed_estats["PS"][6])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass 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: if rpc_estats is None:
try: try:
rpc_estats = await self.rpc.estats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass return None
if rpc_estats is not None: if rpc_estats is not None:
try: try:
@@ -198,15 +202,16 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
).into(self.algo.unit.default) ).into(self.algo.unit.default)
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass 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) hashboards = await AvalonMiner._get_hashboards(self, rpc_estats)
if rpc_estats is None: if rpc_estats is None:
try: try:
rpc_estats = await self.rpc.estats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass return hashboards
if rpc_estats is not None: if rpc_estats is not None:
try: try:

View File

@@ -16,13 +16,12 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List, Optional
from pyasic.config import MinerConfig, MiningModeConfig from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import Fan, HashBoard 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.data.pools import PoolMetrics, PoolUrl
from pyasic.device.algorithm import AlgoHashRate from pyasic.device.algorithm import AlgoHashRateType
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.miners.backends.bmminer import BMMiner from pyasic.miners.backends.bmminer import BMMiner
from pyasic.miners.backends.cgminer import CGMiner from pyasic.miners.backends.cgminer import CGMiner
@@ -120,9 +119,11 @@ class AntminerModern(BMMiner):
data = await self.web.get_miner_conf() data = await self.web.get_miner_conf()
if data: if data:
self.config = MinerConfig.from_am_modern(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 self.config = config
await self.web.set_miner_conf(config.as_am_modern(user_suffix=user_suffix)) await self.web.set_miner_conf(config.as_am_modern(user_suffix=user_suffix))
# if data: # if data:
@@ -135,54 +136,77 @@ class AntminerModern(BMMiner):
# break # break
# await asyncio.sleep(1) # 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. Upgrade the firmware of the AntMiner device.
Args: Args:
file (Path): Path to the firmware file. file: Path to the firmware file as a string.
keep_settings (bool): Whether to keep the current settings after the update. 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: Returns:
str: Result of the upgrade process. bool: True if upgrade was successful, False otherwise.
""" """
if not file: 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: 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( result = await self.web.update_firmware(
file=file, keep_settings=keep_settings file=file_path, keep_settings=keep_settings
) )
if result.get("success"): if result.get("success"):
logging.info( logging.info(
"Firmware upgrade process completed successfully for AntMiner." "Firmware upgrade process completed successfully for AntMiner."
) )
return "Firmware upgrade completed successfully." return True
else: else:
error_message = result.get("message", "Unknown error") error_message = result.get("message", "Unknown error")
logging.error(f"Firmware upgrade failed. Response: {error_message}") logging.error(f"Firmware upgrade failed. Response: {error_message}")
return f"Firmware upgrade failed. Response: {error_message}" return False
except Exception as e: except Exception as e:
logging.error( logging.error(
f"An error occurred during the firmware upgrade process: {e}", f"An error occurred during the firmware upgrade process: {e}",
exc_info=True, exc_info=True,
) )
raise return False
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
data = await self.web.blink(blink=True) data = await self.web.blink(blink=True)
if data: if data:
if data.get("code") == "B000": if data.get("code") == "B000":
self.light = True self.light = True
return self.light return self.light or False
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
data = await self.web.blink(blink=False) data = await self.web.blink(blink=False)
if data: if data:
if data.get("code") == "B100": if data.get("code") == "B100":
self.light = False self.light = False
return self.light return self.light or False
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.web.reboot() data = await self.web.reboot()
@@ -202,7 +226,9 @@ class AntminerModern(BMMiner):
await self.send_config(cfg) await self.send_config(cfg)
return True 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -214,8 +240,9 @@ class AntminerModern(BMMiner):
return web_get_system_info["hostname"] return web_get_system_info["hostname"]
except KeyError: except KeyError:
pass 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -234,8 +261,11 @@ class AntminerModern(BMMiner):
return data["macaddr"] return data["macaddr"]
except KeyError: except KeyError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -255,7 +285,7 @@ class AntminerModern(BMMiner):
pass pass
return errors 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: if self.expected_hashboards is None:
return [] return []
@@ -273,8 +303,11 @@ class AntminerModern(BMMiner):
try: try:
for board in rpc_stats["STATS"][0]["chain"]: for board in rpc_stats["STATS"][0]["chain"]:
hashboards[board["index"]].hashrate = self.algo.hashrate( hashboards[board["index"]].hashrate = self.algo.hashrate(
rate=board["rate_real"], unit=self.algo.unit.GH rate=board["rate_real"],
).into(self.algo.unit.default) 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"] hashboards[board["index"]].chips = board["asic_num"]
if "S21+ Hyd" in self.model: if "S21+ Hyd" in self.model:
@@ -324,8 +357,8 @@ class AntminerModern(BMMiner):
return hashboards return hashboards
async def _get_fault_light( async def _get_fault_light(
self, web_get_blink_status: dict = None self, web_get_blink_status: dict | None = None
) -> Optional[bool]: ) -> bool | None:
if self.light: if self.light:
return self.light return self.light
@@ -343,8 +376,8 @@ class AntminerModern(BMMiner):
return self.light return self.light
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_stats: dict = None self, rpc_stats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_stats is None: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -360,13 +393,14 @@ class AntminerModern(BMMiner):
rate_unit = "GH" rate_unit = "GH"
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) 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: except LookupError:
pass pass
return None
async def _get_serial_number( async def _get_serial_number(
self, web_get_system_info: dict = None self, web_get_system_info: dict | None = None
) -> Optional[str]: ) -> str | None:
if web_get_system_info is None: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -378,6 +412,7 @@ class AntminerModern(BMMiner):
return web_get_system_info["serinum"] return web_get_system_info["serinum"]
except LookupError: except LookupError:
pass pass
return None
async def set_static_ip( async def set_static_ip(
self, self,
@@ -385,10 +420,10 @@ class AntminerModern(BMMiner):
dns: str, dns: str,
gateway: str, gateway: str,
subnet_mask: str = "255.255.255.0", subnet_mask: str = "255.255.255.0",
hostname: str = None, hostname: str | None = None,
): ):
if not hostname: if not hostname:
hostname = await self.get_hostname() hostname = await self.get_hostname() or ""
await self.web.set_network_conf( await self.web.set_network_conf(
ip=ip, ip=ip,
dns=dns, dns=dns,
@@ -398,9 +433,9 @@ class AntminerModern(BMMiner):
protocol=2, protocol=2,
) )
async def set_dhcp(self, hostname: str = None): async def set_dhcp(self, hostname: str | None = None):
if not hostname: if not hostname:
hostname = await self.get_hostname() hostname = await self.get_hostname() or ""
await self.web.set_network_conf( await self.web.set_network_conf(
ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1 ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1
) )
@@ -421,7 +456,7 @@ class AntminerModern(BMMiner):
protocol=protocol, 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: if web_get_conf is None:
try: try:
web_get_conf = await self.web.get_miner_conf() web_get_conf = await self.web.get_miner_conf()
@@ -437,8 +472,9 @@ class AntminerModern(BMMiner):
return False return False
except LookupError: except LookupError:
pass 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: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -450,8 +486,9 @@ class AntminerModern(BMMiner):
return int(rpc_stats["STATS"][1]["Elapsed"]) return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()
@@ -540,19 +577,22 @@ class AntminerOld(CGMiner):
data = await self.web.get_miner_conf() data = await self.web.get_miner_conf()
if data: if data:
self.config = MinerConfig.from_am_old(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 self.config = config
await self.web.set_miner_conf(config.as_am_old(user_suffix=user_suffix)) 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: try:
data = await self.web.get_system_info() data = await self.web.get_system_info()
if data: if data:
return data["macaddr"] return data["macaddr"]
except KeyError: except KeyError:
pass pass
return None
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
# this should time out, after it does do a check # this should time out, after it does do a check
@@ -564,7 +604,7 @@ class AntminerOld(CGMiner):
self.light = True self.light = True
except KeyError: except KeyError:
pass pass
return self.light return self.light or False
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
await self.web.blink(blink=False) await self.web.blink(blink=False)
@@ -575,7 +615,7 @@ class AntminerOld(CGMiner):
self.light = False self.light = False
except KeyError: except KeyError:
pass pass
return self.light return self.light or False
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.web.reboot() data = await self.web.reboot()
@@ -584,8 +624,8 @@ class AntminerOld(CGMiner):
return False return False
async def _get_fault_light( async def _get_fault_light(
self, web_get_blink_status: dict = None self, web_get_blink_status: dict | None = None
) -> Optional[bool]: ) -> bool | None:
if self.light: if self.light:
return self.light return self.light
@@ -602,7 +642,9 @@ class AntminerOld(CGMiner):
pass pass
return self.light 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -614,8 +656,9 @@ class AntminerOld(CGMiner):
return web_get_system_info["hostname"] return web_get_system_info["hostname"]
except KeyError: except KeyError:
pass 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: if self.expected_fans is None:
return [] return []
@@ -640,16 +683,16 @@ class AntminerOld(CGMiner):
for fan in range(self.expected_fans): for fan in range(self.expected_fans):
fans_data[fan].speed = rpc_stats["STATS"][1].get( fans_data[fan].speed = rpc_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0 f"fan{fan_offset + fan}", 0
) )
except LookupError: except LookupError:
pass pass
return fans_data 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: if self.expected_hashboards is None:
return [] return []
hashboards = [] hashboards: list[HashBoard] = []
if rpc_stats is None: if rpc_stats is None:
try: try:
@@ -689,8 +732,11 @@ class AntminerOld(CGMiner):
hashrate = boards[1].get(f"chain_rate{i}") hashrate = boards[1].get(f"chain_rate{i}")
if hashrate: if hashrate:
hashboard.hashrate = self.algo.hashrate( hashboard.hashrate = self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.GH rate=float(hashrate),
).into(self.algo.unit.default) 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}") chips = boards[1].get(f"chain_acn{i}")
if chips: if chips:
@@ -707,7 +753,7 @@ class AntminerOld(CGMiner):
return hashboards 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: if web_get_conf is None:
try: try:
web_get_conf = await self.web.get_miner_conf() web_get_conf = await self.web.get_miner_conf()
@@ -732,7 +778,9 @@ class AntminerOld(CGMiner):
else: else:
return False 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: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -744,3 +792,4 @@ class AntminerOld(CGMiner):
return int(rpc_stats["STATS"][1]["Elapsed"]) return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass pass
return None

View File

@@ -15,11 +15,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import logging import logging
from enum import Enum from enum import Enum
from typing import List, Optional
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard 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.errors import APIError
from pyasic.miners.data import ( from pyasic.miners.data import (
DataFunction, DataFunction,
@@ -187,7 +186,9 @@ class Auradine(StockFirmware):
pass pass
return MinerConfig() 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 self.config = config
conf = config.as_auradine(user_suffix=user_suffix) conf = config.as_auradine(user_suffix=user_suffix)
@@ -197,8 +198,8 @@ class Auradine(StockFirmware):
async def upgrade_firmware( async def upgrade_firmware(
self, self,
*, *,
url: str = None, url: str | None = None,
version: str = "latest", version: str | None = "latest",
keep_settings: bool = False, keep_settings: bool = False,
**kwargs, **kwargs,
) -> bool: ) -> bool:
@@ -223,8 +224,12 @@ class Auradine(StockFirmware):
if url: if url:
result = await self.web.firmware_upgrade(url=url) result = await self.web.firmware_upgrade(url=url)
else: elif version:
result = await self.web.firmware_upgrade(version=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": if result.get("STATUS", [{}])[0].get("STATUS") == "S":
logging.info("Firmware upgrade process completed successfully.") logging.info("Firmware upgrade process completed successfully.")
@@ -245,7 +250,7 @@ class Auradine(StockFirmware):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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: if web_ipreport is None:
try: try:
web_ipreport = await self.web.ipreport() web_ipreport = await self.web.ipreport()
@@ -257,8 +262,9 @@ class Auradine(StockFirmware):
return web_ipreport["IPReport"][0]["mac"].upper() return web_ipreport["IPReport"][0]["mac"].upper()
except (LookupError, AttributeError): except (LookupError, AttributeError):
pass 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: if web_ipreport is None:
try: try:
web_ipreport = await self.web.ipreport() web_ipreport = await self.web.ipreport()
@@ -270,8 +276,9 @@ class Auradine(StockFirmware):
return web_ipreport["IPReport"][0]["version"] return web_ipreport["IPReport"][0]["version"]
except LookupError: except LookupError:
pass 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: if web_ipreport is None:
try: try:
web_ipreport = await self.web.ipreport() web_ipreport = await self.web.ipreport()
@@ -283,8 +290,11 @@ class Auradine(StockFirmware):
return web_ipreport["IPReport"][0]["hostname"] return web_ipreport["IPReport"][0]["hostname"]
except LookupError: except LookupError:
pass 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -295,14 +305,15 @@ class Auradine(StockFirmware):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]), rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass pass
return None
async def _get_hashboards( async def _get_hashboards(
self, rpc_devs: dict = None, web_ipreport: dict = None self, rpc_devs: dict | None = None, web_ipreport: dict | None = None
) -> List[HashBoard]: ) -> list[HashBoard]:
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
@@ -327,8 +338,11 @@ class Auradine(StockFirmware):
for board in rpc_devs["DEVS"]: for board in rpc_devs["DEVS"]:
b_id = board["ID"] - 1 b_id = board["ID"] - 1
hashboards[b_id].hashrate = self.algo.hashrate( hashboards[b_id].hashrate = self.algo.hashrate(
rate=float(board["MHS 5s"]), unit=self.algo.unit.MH rate=float(board["MHS 5s"]),
).into(self.algo.unit.default) 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].temp = round(float(board["Temperature"]))
hashboards[b_id].missing = False hashboards[b_id].missing = False
except LookupError: except LookupError:
@@ -344,7 +358,7 @@ class Auradine(StockFirmware):
return hashboards 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: if web_psu is None:
try: try:
web_psu = await self.web.get_psu() web_psu = await self.web.get_psu()
@@ -356,10 +370,11 @@ class Auradine(StockFirmware):
return int(float(web_psu["PSU"][0]["PowerIn"].replace("W", ""))) return int(float(web_psu["PSU"][0]["PowerIn"].replace("W", "")))
except (LookupError, TypeError, ValueError): except (LookupError, TypeError, ValueError):
pass pass
return None
async def _get_wattage_limit( async def _get_wattage_limit(
self, web_mode: dict = None, web_psu: dict = None self, web_mode: dict | None = None, web_psu: dict | None = None
) -> Optional[int]: ) -> int | None:
if web_mode is None: if web_mode is None:
try: try:
web_mode = await self.web.get_mode() web_mode = await self.web.get_mode()
@@ -383,8 +398,9 @@ class Auradine(StockFirmware):
return int(float(web_psu["PSU"][0]["PoutMax"].replace("W", ""))) return int(float(web_psu["PSU"][0]["PoutMax"].replace("W", "")))
except (LookupError, TypeError, ValueError): except (LookupError, TypeError, ValueError):
pass 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: if self.expected_fans is None:
return [] return []
@@ -403,7 +419,7 @@ class Auradine(StockFirmware):
pass pass
return fans 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: if web_led is None:
try: try:
web_led = await self.web.get_led() web_led = await self.web.get_led()
@@ -415,8 +431,9 @@ class Auradine(StockFirmware):
return web_led["LED"][0]["Code"] == int(AuradineLEDCodes.LOCATE_MINER) return web_led["LED"][0]["Code"] == int(AuradineLEDCodes.LOCATE_MINER)
except LookupError: except LookupError:
pass 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: if web_mode is None:
try: try:
web_mode = await self.web.get_mode() web_mode = await self.web.get_mode()
@@ -428,8 +445,9 @@ class Auradine(StockFirmware):
return web_mode["Mode"][0]["Sleep"] == "off" return web_mode["Mode"][0]["Sleep"] == "off"
except (LookupError, TypeError, ValueError): except (LookupError, TypeError, ValueError):
pass 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -441,3 +459,4 @@ class Auradine(StockFirmware):
return rpc_summary["SUMMARY"][0]["Elapsed"] return rpc_summary["SUMMARY"][0]["Elapsed"]
except LookupError: except LookupError:
pass pass
return None

View File

@@ -16,10 +16,9 @@
import copy import copy
import re import re
import time import time
from typing import List, Optional
from pyasic.data import Fan, HashBoard 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.errors import APIError
from pyasic.miners.backends.cgminer import CGMiner from pyasic.miners.backends.cgminer import CGMiner
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
@@ -119,7 +118,7 @@ class AvalonMiner(CGMiner):
limit = 1 limit = 1
else: else:
limit = 0 limit = 0
data = await self.rpc.ascset(0, "worklevel,set", 1) data = await self.rpc.ascset(0, "worklevel,set", limit)
except APIError: except APIError:
return False return False
if data["STATUS"][0]["Msg"] == "ASC 0 set OK": if data["STATUS"][0]["Msg"] == "ASC 0 set OK":
@@ -143,7 +142,7 @@ class AvalonMiner(CGMiner):
try: try:
# Shut off 5 seconds from now # Shut off 5 seconds from now
timestamp = int(time.time()) + 5 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: except APIError:
return False return False
if "success" in data["STATUS"][0]["Msg"]: if "success" in data["STATUS"][0]["Msg"]:
@@ -154,7 +153,7 @@ class AvalonMiner(CGMiner):
try: try:
# Shut off 5 seconds from now # Shut off 5 seconds from now
timestamp = int(time.time()) + 5 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: except APIError:
return False return False
if "success" in data["STATUS"][0]["Msg"]: if "success" in data["STATUS"][0]["Msg"]:
@@ -232,7 +231,7 @@ class AvalonMiner(CGMiner):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -249,23 +248,28 @@ class AvalonMiner(CGMiner):
return mac return mac
except (KeyError, ValueError): except (KeyError, ValueError):
pass 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: if rpc_devs is None:
try: try:
rpc_devs = await self.rpc.devs() rpc_devs = await self.rpc.devs()
except APIError: except APIError:
pass return None
if rpc_devs is not None: if rpc_devs is not None:
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_devs["DEVS"][0]["MHS 1m"]), unit=self.algo.unit.MH rate=float(rpc_devs["DEVS"][0]["MHS 1m"]),
).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): except (KeyError, IndexError, ValueError, TypeError):
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -291,12 +295,18 @@ class AvalonMiner(CGMiner):
board_hr = parsed_estats["STATS"][0]["MM ID0"]["MGHS"] board_hr = parsed_estats["STATS"][0]["MM ID0"]["MGHS"]
if isinstance(board_hr, list): if isinstance(board_hr, list):
hashboards[board].hashrate = self.algo.hashrate( hashboards[board].hashrate = self.algo.hashrate(
rate=float(board_hr[board]), unit=self.algo.unit.GH rate=float(board_hr[board]),
).into(self.algo.unit.default) unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
else: else:
hashboards[board].hashrate = self.algo.hashrate( hashboards[board].hashrate = self.algo.hashrate(
rate=float(board_hr), unit=self.algo.unit.GH rate=float(board_hr),
).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: except LookupError:
pass pass
@@ -376,24 +386,26 @@ class AvalonMiner(CGMiner):
return hashboards return hashboards
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_estats: dict = None self, rpc_estats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_estats is None: if rpc_estats is None:
try: try:
rpc_estats = await self.rpc.estats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass return None
if rpc_estats is not None: if rpc_estats is not None:
try: try:
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"] parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
return self.algo.hashrate( return self.algo.hashrate(
rate=float(parsed_estats["GHSmm"]), unit=self.algo.unit.GH rate=float(parsed_estats["GHSmm"]),
).into(self.algo.unit.default) unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) # type: ignore[attr-defined]
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass 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: if rpc_estats is None:
try: try:
rpc_estats = await self.rpc.estats() rpc_estats = await self.rpc.estats()
@@ -406,13 +418,14 @@ class AvalonMiner(CGMiner):
return float(parsed_estats["Temp"]) return float(parsed_estats["Temp"])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass 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: if rpc_estats is None:
try: try:
rpc_estats = await self.rpc.estats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass return None
if rpc_estats is not None: if rpc_estats is not None:
try: try:
@@ -420,8 +433,9 @@ class AvalonMiner(CGMiner):
return int(parsed_estats["MPO"]) return int(parsed_estats["MPO"])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass 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: if rpc_estats is None:
try: try:
rpc_estats = await self.rpc.estats() rpc_estats = await self.rpc.estats()
@@ -434,8 +448,9 @@ class AvalonMiner(CGMiner):
return int(parsed_estats["WALLPOWER"]) return int(parsed_estats["WALLPOWER"])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass 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: if self.expected_fans is None:
return [] return []
@@ -459,7 +474,7 @@ class AvalonMiner(CGMiner):
pass pass
return fans_data 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: if self.light:
return self.light return self.light
if rpc_estats is None: if rpc_estats is None:

View File

@@ -14,12 +14,10 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware from pyasic.miners.device.firmware import StockFirmware
@@ -72,7 +70,8 @@ class BFGMiner(StockFirmware):
try: try:
pools = await self.rpc.pools() pools = await self.rpc.pools()
except APIError: except APIError:
return self.config if self.config is not None:
return self.config
self.config = MinerConfig.from_api(pools) self.config = MinerConfig.from_api(pools)
return self.config return self.config
@@ -81,7 +80,7 @@ class BFGMiner(StockFirmware):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -96,7 +95,7 @@ class BFGMiner(StockFirmware):
return self.api_ver 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -111,7 +110,9 @@ class BFGMiner(StockFirmware):
return self.fw_ver 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 # get hr from API
if rpc_summary is None: if rpc_summary is None:
try: try:
@@ -123,12 +124,15 @@ class BFGMiner(StockFirmware):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["MHS 20s"]), rate=float(rpc_summary["SUMMARY"][0]["MHS 20s"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -172,8 +176,11 @@ class BFGMiner(StockFirmware):
hashrate = boards[1].get(f"chain_rate{i}") hashrate = boards[1].get(f"chain_rate{i}")
if hashrate: if hashrate:
hashboard.hashrate = self.algo.hashrate( hashboard.hashrate = self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.GH rate=float(hashrate),
).into(self.algo.unit.default) 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}") chips = boards[1].get(f"chain_acn{i}")
if chips: if chips:
@@ -187,7 +194,7 @@ class BFGMiner(StockFirmware):
return hashboards 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: if self.expected_fans is None:
return [] return []
@@ -212,7 +219,7 @@ class BFGMiner(StockFirmware):
for fan in range(self.expected_fans): for fan in range(self.expected_fans):
fans_data[fan] = rpc_stats["STATS"][1].get( fans_data[fan] = rpc_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0 f"fan{fan_offset + fan}", 0
) )
except LookupError: except LookupError:
pass pass
@@ -220,7 +227,7 @@ class BFGMiner(StockFirmware):
return fans 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()
@@ -251,8 +258,8 @@ class BFGMiner(StockFirmware):
return pools_data return pools_data
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_stats: dict = None self, rpc_stats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
# X19 method, not sure compatibility # X19 method, not sure compatibility
if rpc_stats is None: if rpc_stats is None:
try: try:
@@ -269,6 +276,7 @@ class BFGMiner(StockFirmware):
rate_unit = "GH" rate_unit = "GH"
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) 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: except LookupError:
pass pass
return None

View File

@@ -14,12 +14,11 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware from pyasic.miners.device.firmware import StockFirmware
@@ -76,7 +75,8 @@ class BMMiner(StockFirmware):
try: try:
pools = await self.rpc.pools() pools = await self.rpc.pools()
except APIError: except APIError:
return self.config if self.config is not None:
return self.config
self.config = MinerConfig.from_api(pools) self.config = MinerConfig.from_api(pools)
return self.config return self.config
@@ -85,7 +85,7 @@ class BMMiner(StockFirmware):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -100,7 +100,7 @@ class BMMiner(StockFirmware):
return self.api_ver 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -115,7 +115,9 @@ class BMMiner(StockFirmware):
return self.fw_ver 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 # get hr from API
if rpc_summary is None: if rpc_summary is None:
try: try:
@@ -127,12 +129,15 @@ class BMMiner(StockFirmware):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -189,8 +194,11 @@ class BMMiner(StockFirmware):
hashrate = boards[1].get(f"chain_rate{i}") hashrate = boards[1].get(f"chain_rate{i}")
if hashrate: if hashrate:
hashboard.hashrate = self.algo.hashrate( hashboard.hashrate = self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.GH rate=float(hashrate),
).into(self.algo.unit.default) 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}") chips = boards[1].get(f"chain_acn{i}")
if chips: if chips:
@@ -204,7 +212,7 @@ class BMMiner(StockFirmware):
return hashboards 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: if self.expected_fans is None:
return [] return []
@@ -229,7 +237,7 @@ class BMMiner(StockFirmware):
for fan in range(self.expected_fans): for fan in range(self.expected_fans):
fans[fan].speed = rpc_stats["STATS"][1].get( fans[fan].speed = rpc_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0 f"fan{fan_offset + fan}", 0
) )
except LookupError: except LookupError:
pass pass
@@ -237,8 +245,8 @@ class BMMiner(StockFirmware):
return fans return fans
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_stats: dict = None self, rpc_stats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
# X19 method, not sure compatibility # X19 method, not sure compatibility
if rpc_stats is None: if rpc_stats is None:
try: try:
@@ -255,11 +263,14 @@ class BMMiner(StockFirmware):
rate_unit = "GH" rate_unit = "GH"
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) 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: except LookupError:
pass 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: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -271,8 +282,9 @@ class BMMiner(StockFirmware):
return int(rpc_stats["STATS"][1]["Elapsed"]) return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()

View File

@@ -16,8 +16,6 @@
import base64 import base64
import logging import logging
import time import time
from pathlib import Path
from typing import List, Optional, Union
import aiofiles import aiofiles
import tomli_w import tomli_w
@@ -25,14 +23,14 @@ import tomli_w
try: try:
import tomllib import tomllib
except ImportError: except ImportError:
import tomli as tomllib import tomli as tomllib # type: ignore
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune from pyasic.config.mining import MiningModePowerTune
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import ( from pyasic.miners.data import (
DataFunction, DataFunction,
@@ -193,7 +191,9 @@ class BOSMiner(BraiinsOSFirmware):
return self.config 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 self.config = config
parsed_cfg = config.as_bosminer(user_suffix=user_suffix) parsed_cfg = config.as_bosminer(user_suffix=user_suffix)
@@ -202,21 +202,18 @@ class BOSMiner(BraiinsOSFirmware):
"format": { "format": {
"version": "2.0", "version": "2.0",
"generator": "pyasic", "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()), "timestamp": int(time.time()),
}, },
**parsed_cfg, **parsed_cfg,
} }
) )
try: try:
conn = await self.ssh._get_connection() await self.ssh.send_command("/etc/init.d/bosminer stop")
except ConnectionError as e: await self.ssh.send_command("echo '" + toml_conf + "' > /etc/bosminer.toml")
raise APIError("SSH connection failed when sending config.") from e await self.ssh.send_command("/etc/init.d/bosminer start")
except Exception as e:
async with conn: raise APIError("SSH command failed when sending config.") from e
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")
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> bool:
try: try:
@@ -285,12 +282,12 @@ class BOSMiner(BraiinsOSFirmware):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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: if web_net_conf is None:
try: try:
web_net_conf = await self.web.get_net_conf() web_net_conf = await self.web.get_net_conf()
except APIError: except APIError:
pass return None
if isinstance(web_net_conf, dict): if isinstance(web_net_conf, dict):
if "admin/network/iface_status/lan" in web_net_conf.keys(): if "admin/network/iface_status/lan" in web_net_conf.keys():
@@ -301,17 +298,18 @@ class BOSMiner(BraiinsOSFirmware):
return web_net_conf[0]["macaddr"] return web_net_conf[0]["macaddr"]
except LookupError: except LookupError:
pass pass
return None
# could use ssh, but its slow and buggy # could use ssh, but its slow and buggy
# result = await self.send_ssh_command("cat /sys/class/net/eth0/address") # result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
# if result: # if result:
# return result.upper().strip() # 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
except APIError: except APIError:
pass return None
# Now get the API version # Now get the API version
if rpc_version is not None: if rpc_version is not None:
@@ -320,17 +318,20 @@ class BOSMiner(BraiinsOSFirmware):
except LookupError: except LookupError:
rpc_ver = None rpc_ver = None
self.api_ver = rpc_ver 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 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: if web_bos_info is None:
try: try:
web_bos_info = await self.web.get_bos_info() web_bos_info = await self.web.get_bos_info()
except APIError: except APIError:
return None return None
if web_bos_info is None:
return None
if isinstance(web_bos_info, dict): if isinstance(web_bos_info, dict):
if "bos/info" in web_bos_info.keys(): if "bos/info" in web_bos_info.keys():
web_bos_info = web_bos_info["bos/info"] web_bos_info = web_bos_info["bos/info"]
@@ -344,7 +345,7 @@ class BOSMiner(BraiinsOSFirmware):
return self.fw_ver return self.fw_ver
async def _get_hostname(self) -> Union[str, None]: async def _get_hostname(self) -> str | None:
try: try:
hostname = (await self.ssh.get_hostname()).strip() hostname = (await self.ssh.get_hostname()).strip()
except AttributeError: except AttributeError:
@@ -354,28 +355,31 @@ class BOSMiner(BraiinsOSFirmware):
return None return None
return hostname 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
except APIError: except APIError:
pass return None
if rpc_summary is not None: if rpc_summary is not None:
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except (KeyError, IndexError, ValueError, TypeError): except (KeyError, IndexError, ValueError, TypeError):
pass pass
return None
async def _get_hashboards( async def _get_hashboards(
self, self,
rpc_temps: dict = None, rpc_temps: dict | None = None,
rpc_devdetails: dict = None, rpc_devdetails: dict | None = None,
rpc_devs: dict = None, rpc_devs: dict | None = None,
) -> List[HashBoard]: ) -> list[HashBoard]:
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
@@ -440,19 +444,22 @@ class BOSMiner(BraiinsOSFirmware):
for board in rpc_devs["DEVS"]: for board in rpc_devs["DEVS"]:
_id = board["ID"] - offset _id = board["ID"] - offset
hashboards[_id].hashrate = self.algo.hashrate( hashboards[_id].hashrate = self.algo.hashrate(
rate=float(board["MHS 1m"]), unit=self.algo.unit.MH rate=float(board["MHS 1m"]),
).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): except (IndexError, KeyError):
pass pass
return hashboards 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: if rpc_tunerstatus is None:
try: try:
rpc_tunerstatus = await self.rpc.tunerstatus() rpc_tunerstatus = await self.rpc.tunerstatus()
except APIError: except APIError:
pass return None
if rpc_tunerstatus is not None: if rpc_tunerstatus is not None:
try: try:
@@ -461,21 +468,25 @@ class BOSMiner(BraiinsOSFirmware):
] ]
except LookupError: except LookupError:
pass 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: if rpc_tunerstatus is None:
try: try:
rpc_tunerstatus = await self.rpc.tunerstatus() rpc_tunerstatus = await self.rpc.tunerstatus()
except APIError: except APIError:
pass return None
if rpc_tunerstatus is not None: if rpc_tunerstatus is not None:
try: try:
return rpc_tunerstatus["TUNERSTATUS"][0]["PowerLimit"] return rpc_tunerstatus["TUNERSTATUS"][0]["PowerLimit"]
except LookupError: except LookupError:
pass 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: if self.expected_fans is None:
return [] return []
@@ -483,7 +494,7 @@ class BOSMiner(BraiinsOSFirmware):
try: try:
rpc_fans = await self.rpc.fans() rpc_fans = await self.rpc.fans()
except APIError: except APIError:
pass return [Fan() for _ in range(self.expected_fans)]
if rpc_fans is not None: if rpc_fans is not None:
fans = [] fans = []
@@ -495,12 +506,14 @@ class BOSMiner(BraiinsOSFirmware):
return fans return fans
return [Fan() for _ in range(self.expected_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: if rpc_tunerstatus is None:
try: try:
rpc_tunerstatus = await self.rpc.tunerstatus() rpc_tunerstatus = await self.rpc.tunerstatus()
except APIError: except APIError:
pass return []
if rpc_tunerstatus is not None: if rpc_tunerstatus is not None:
errors = [] errors = []
@@ -523,9 +536,10 @@ class BOSMiner(BraiinsOSFirmware):
errors.append( errors.append(
BraiinsOSError(error_message=f"Slot {_id} {_error}") BraiinsOSError(error_message=f"Slot {_id} {_error}")
) )
return errors return errors # type: ignore
except (KeyError, IndexError): except (KeyError, IndexError):
pass pass
return []
async def _get_fault_light(self) -> bool: async def _get_fault_light(self) -> bool:
if self.light: if self.light:
@@ -537,16 +551,16 @@ class BOSMiner(BraiinsOSFirmware):
self.light = True self.light = True
return self.light return self.light
except (TypeError, AttributeError): except (TypeError, AttributeError):
return self.light return self.light or False
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_devs: dict = None self, rpc_devs: dict | None = None
) -> Optional[AlgoHashRateType]: ) -> AlgoHashRateType | None:
if rpc_devs is None: if rpc_devs is None:
try: try:
rpc_devs = await self.rpc.devs() rpc_devs = await self.rpc.devs()
except APIError: except APIError:
pass return None
if rpc_devs is not None: if rpc_devs is not None:
try: try:
@@ -559,52 +573,57 @@ class BOSMiner(BraiinsOSFirmware):
if len(hr_list) == 0: if len(hr_list) == 0:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(0), unit=self.algo.unit.default rate=float(0),
unit=self.algo.unit.default, # type: ignore
) )
else: else:
return self.algo.hashrate( return self.algo.hashrate(
rate=float( 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, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except (IndexError, KeyError): except (IndexError, KeyError):
pass 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: if rpc_devdetails is None:
try: try:
rpc_devdetails = await self.rpc.send_command( rpc_devdetails = await self.rpc.send_command(
"devdetails", ignore_errors=True, allow_warning=False "devdetails", ignore_errors=True, allow_warning=False
) )
except APIError: except APIError:
pass return None
if rpc_devdetails is not None: if rpc_devdetails is not None:
try: try:
return not rpc_devdetails["STATUS"][0]["Msg"] == "Unavailable" return not rpc_devdetails["STATUS"][0]["Msg"] == "Unavailable"
except LookupError: except LookupError:
pass 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
except APIError: except APIError:
pass return None
if rpc_summary is not None: if rpc_summary is not None:
try: try:
return int(rpc_summary["SUMMARY"][0]["Elapsed"]) return int(rpc_summary["SUMMARY"][0]["Elapsed"])
except LookupError: except LookupError:
pass 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()
except APIError: except APIError:
pass return []
pools_data = [] pools_data = []
if rpc_pools is not None: if rpc_pools is not None:
@@ -629,15 +648,25 @@ class BOSMiner(BraiinsOSFirmware):
pass pass
return pools_data 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. Upgrade the firmware of the BOSMiner device.
Args: 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: Returns:
Confirmation message after upgrading the firmware. True if upgrade was successful, False otherwise.
""" """
try: try:
logging.info("Starting firmware upgrade process.") logging.info("Starting firmware upgrade process.")
@@ -659,24 +688,24 @@ class BOSMiner(BraiinsOSFirmware):
) )
logging.info("Firmware upgrade process completed successfully.") logging.info("Firmware upgrade process completed successfully.")
return "Firmware upgrade completed successfully." return True
except FileNotFoundError as e: except FileNotFoundError as e:
logging.error(f"File not found during the firmware upgrade process: {e}") logging.error(f"File not found during the firmware upgrade process: {e}")
raise return False
except ValueError as e: except ValueError as e:
logging.error( logging.error(
f"Validation error occurred during the firmware upgrade process: {e}" f"Validation error occurred during the firmware upgrade process: {e}"
) )
raise return False
except OSError as e: except OSError as e:
logging.error(f"OS error occurred during the firmware upgrade process: {e}") logging.error(f"OS error occurred during the firmware upgrade process: {e}")
raise return False
except Exception as e: except Exception as e:
logging.error( logging.error(
f"An unexpected error occurred during the firmware upgrade process: {e}", f"An unexpected error occurred during the firmware upgrade process: {e}",
exc_info=True, exc_info=True,
) )
raise return False
BOSER_DATA_LOC = DataLocations( BOSER_DATA_LOC = DataLocations(
@@ -805,7 +834,9 @@ class BOSer(BraiinsOSFirmware):
return MinerConfig.from_boser(grpc_conf) 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) boser_cfg = config.as_boser(user_suffix=user_suffix)
for key in boser_cfg: for key in boser_cfg:
await self.web.send_command(key, message=boser_cfg[key]) 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: async def set_power_limit(self, wattage: int) -> bool:
try: try:
result = await self.web.set_power_target( 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: except APIError:
return False return False
@@ -829,25 +861,26 @@ class BOSer(BraiinsOSFirmware):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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: if grpc_miner_details is None:
try: try:
grpc_miner_details = await self.web.get_miner_details() grpc_miner_details = await self.web.get_miner_details()
except APIError: except APIError:
pass return None
if grpc_miner_details is not None: if grpc_miner_details is not None:
try: try:
return grpc_miner_details["macAddress"].upper() return grpc_miner_details["macAddress"].upper()
except (LookupError, TypeError): except (LookupError, TypeError):
pass 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
except APIError: except APIError:
pass return None
if rpc_version is not None: if rpc_version is not None:
try: try:
@@ -855,16 +888,16 @@ class BOSer(BraiinsOSFirmware):
except LookupError: except LookupError:
rpc_ver = None rpc_ver = None
self.api_ver = rpc_ver 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 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: if grpc_miner_details is None:
try: try:
grpc_miner_details = await self.web.get_miner_details() grpc_miner_details = await self.web.get_miner_details()
except APIError: except APIError:
pass return None
fw_ver = None fw_ver = None
@@ -882,43 +915,47 @@ class BOSer(BraiinsOSFirmware):
return self.fw_ver 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: if grpc_miner_details is None:
try: try:
grpc_miner_details = await self.web.get_miner_details() grpc_miner_details = await self.web.get_miner_details()
except APIError: except APIError:
pass return None
if grpc_miner_details is not None: if grpc_miner_details is not None:
try: try:
return grpc_miner_details["hostname"] return grpc_miner_details["hostname"]
except LookupError: except LookupError:
pass 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
except APIError: except APIError:
pass return None
if rpc_summary is not None: if rpc_summary is not None:
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except (KeyError, IndexError, ValueError, TypeError): except (KeyError, IndexError, ValueError, TypeError):
pass pass
return None
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, grpc_miner_details: dict = None self, grpc_miner_details: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if grpc_miner_details is None: if grpc_miner_details is None:
try: try:
grpc_miner_details = await self.web.get_miner_details() grpc_miner_details = await self.web.get_miner_details()
except APIError: except APIError:
pass return None
if grpc_miner_details is not None: if grpc_miner_details is not None:
try: try:
@@ -926,12 +963,15 @@ class BOSer(BraiinsOSFirmware):
rate=float( rate=float(
grpc_miner_details["stickerHashrate"]["gigahashPerSecond"] grpc_miner_details["stickerHashrate"]["gigahashPerSecond"]
), ),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except LookupError: except LookupError:
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -944,7 +984,7 @@ class BOSer(BraiinsOSFirmware):
try: try:
grpc_hashboards = await self.web.get_hashboards() grpc_hashboards = await self.web.get_hashboards()
except APIError: except APIError:
pass return hashboards
if grpc_hashboards is not None: if grpc_hashboards is not None:
grpc_boards = sorted( grpc_boards = sorted(
@@ -967,35 +1007,38 @@ class BOSer(BraiinsOSFirmware):
"gigahashPerSecond" "gigahashPerSecond"
] ]
), ),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
hashboards[idx].missing = False hashboards[idx].missing = False
return hashboards 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: if grpc_miner_stats is None:
try: try:
grpc_miner_stats = await self.web.get_miner_stats() grpc_miner_stats = await self.web.get_miner_stats()
except APIError: except APIError:
pass return None
if grpc_miner_stats is not None: if grpc_miner_stats is not None:
try: try:
return grpc_miner_stats["powerStats"]["approximatedConsumption"]["watt"] return grpc_miner_stats["powerStats"]["approximatedConsumption"]["watt"]
except KeyError: except KeyError:
pass pass
return None
async def _get_wattage_limit( async def _get_wattage_limit(
self, grpc_active_performance_mode: dict = None self, grpc_active_performance_mode: dict | None = None
) -> Optional[int]: ) -> int | None:
if grpc_active_performance_mode is None: if grpc_active_performance_mode is None:
try: try:
grpc_active_performance_mode = ( grpc_active_performance_mode = (
await self.web.get_active_performance_mode() await self.web.get_active_performance_mode()
) )
except APIError: except APIError:
pass return None
if grpc_active_performance_mode is not None: if grpc_active_performance_mode is not None:
try: try:
@@ -1004,8 +1047,9 @@ class BOSer(BraiinsOSFirmware):
]["watt"] ]["watt"]
except KeyError: except KeyError:
pass 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: if self.expected_fans is None:
return [] return []
@@ -1013,7 +1057,7 @@ class BOSer(BraiinsOSFirmware):
try: try:
grpc_cooling_state = await self.web.get_cooling_state() grpc_cooling_state = await self.web.get_cooling_state()
except APIError: except APIError:
pass return [Fan() for _ in range(self.expected_fans)]
if grpc_cooling_state is not None: if grpc_cooling_state is not None:
fans = [] fans = []
@@ -1025,12 +1069,14 @@ class BOSer(BraiinsOSFirmware):
return fans return fans
return [Fan() for _ in range(self.expected_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: if rpc_tunerstatus is None:
try: try:
rpc_tunerstatus = await self.rpc.tunerstatus() rpc_tunerstatus = await self.rpc.tunerstatus()
except APIError: except APIError:
pass return []
if rpc_tunerstatus is not None: if rpc_tunerstatus is not None:
errors = [] errors = []
@@ -1053,11 +1099,14 @@ class BOSer(BraiinsOSFirmware):
errors.append( errors.append(
BraiinsOSError(error_message=f"Slot {_id} {_error}") BraiinsOSError(error_message=f"Slot {_id} {_error}")
) )
return errors return errors # type: ignore
except LookupError: except LookupError:
pass 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: if self.light is not None:
return self.light return self.light
@@ -1065,7 +1114,7 @@ class BOSer(BraiinsOSFirmware):
try: try:
grpc_locate_device_status = await self.web.get_locate_device_status() grpc_locate_device_status = await self.web.get_locate_device_status()
except APIError: except APIError:
pass return False
if grpc_locate_device_status is not None: if grpc_locate_device_status is not None:
if grpc_locate_device_status == {}: if grpc_locate_device_status == {}:
@@ -1074,36 +1123,41 @@ class BOSer(BraiinsOSFirmware):
return grpc_locate_device_status["enabled"] return grpc_locate_device_status["enabled"]
except LookupError: except LookupError:
pass 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: if rpc_devdetails is None:
try: try:
rpc_devdetails = await self.rpc.send_command( rpc_devdetails = await self.rpc.send_command(
"devdetails", ignore_errors=True, allow_warning=False "devdetails", ignore_errors=True, allow_warning=False
) )
except APIError: except APIError:
pass return None
if rpc_devdetails is not None: if rpc_devdetails is not None:
try: try:
return not rpc_devdetails["STATUS"][0]["Msg"] == "Unavailable" return not rpc_devdetails["STATUS"][0]["Msg"] == "Unavailable"
except LookupError: except LookupError:
pass 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
except APIError: except APIError:
pass return None
if rpc_summary is not None: if rpc_summary is not None:
try: try:
return int(rpc_summary["SUMMARY"][0]["Elapsed"]) return int(rpc_summary["SUMMARY"][0]["Elapsed"])
except LookupError: except LookupError:
pass 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: if grpc_pool_groups is None:
try: try:
grpc_pool_groups = await self.web.get_pool_groups() grpc_pool_groups = await self.web.get_pool_groups()

View File

@@ -15,16 +15,15 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio import asyncio
import logging import logging
from pathlib import Path
import aiofiles import aiofiles
import semver import semver
from pyasic.config import MinerConfig, MiningModeConfig, PoolConfig from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, WhatsminerError from pyasic.data.error_codes import MinerErrorData, WhatsminerError
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware from pyasic.miners.device.firmware import StockFirmware
@@ -237,7 +236,9 @@ class BTMinerV2(StockFirmware):
return True return True
return False 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 self.config = config
conf = config.as_wm(user_suffix=user_suffix) conf = config.as_wm(user_suffix=user_suffix)
@@ -308,6 +309,9 @@ class BTMinerV2(StockFirmware):
self.config = cfg self.config = cfg
return self.config return self.config
cfg.mining_mode = MiningModeConfig.normal()
return cfg
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> bool:
try: try:
await self.rpc.adjust_power_limit(wattage) await self.rpc.adjust_power_limit(wattage)
@@ -316,13 +320,14 @@ class BTMinerV2(StockFirmware):
return False return False
else: else:
return True return True
return False
################################################## ##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
################################################## ##################################################
async def _get_mac( 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: ) -> str | None:
if rpc_get_miner_info is None: if rpc_get_miner_info is None:
try: try:
@@ -350,7 +355,9 @@ class BTMinerV2(StockFirmware):
except LookupError: except LookupError:
pass 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: if rpc_get_version is None:
try: try:
rpc_get_version = await self.rpc.get_version() rpc_get_version = await self.rpc.get_version()
@@ -368,13 +375,12 @@ class BTMinerV2(StockFirmware):
except (KeyError, TypeError): except (KeyError, TypeError):
pass pass
else: else:
self.rpc.rpc_ver = self.api_ver
return self.api_ver return self.api_ver
return self.api_ver return self.api_ver
async def _get_fw_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: ) -> str | None:
if rpc_get_version is None: if rpc_get_version is None:
try: try:
@@ -408,7 +414,7 @@ class BTMinerV2(StockFirmware):
return self.fw_ver 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 hostname = None
if rpc_get_miner_info is None: if rpc_get_miner_info is None:
try: try:
@@ -424,7 +430,9 @@ class BTMinerV2(StockFirmware):
return hostname 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -435,13 +443,13 @@ class BTMinerV2(StockFirmware):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except LookupError: except LookupError:
pass pass
return None 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: if self.expected_hashboards is None:
return [] return []
@@ -470,8 +478,11 @@ class BTMinerV2(StockFirmware):
hashboards[asc].chip_temp = round(board["Chip Temp Avg"]) hashboards[asc].chip_temp = round(board["Chip Temp Avg"])
hashboards[asc].temp = round(board["Temperature"]) hashboards[asc].temp = round(board["Temperature"])
hashboards[asc].hashrate = self.algo.hashrate( hashboards[asc].hashrate = self.algo.hashrate(
rate=float(board["MHS 1m"]), unit=self.algo.unit.MH rate=float(board["MHS 1m"]),
).into(self.algo.unit.default) 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].chips = board["Effective Chips"]
hashboards[asc].serial_number = board["PCB SN"] hashboards[asc].serial_number = board["PCB SN"]
hashboards[asc].missing = False hashboards[asc].missing = False
@@ -480,7 +491,7 @@ class BTMinerV2(StockFirmware):
return hashboards 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -494,7 +505,7 @@ class BTMinerV2(StockFirmware):
pass pass
return None 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -509,7 +520,7 @@ class BTMinerV2(StockFirmware):
pass pass
return None 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -524,7 +535,7 @@ class BTMinerV2(StockFirmware):
return None return None
async def _get_fans( 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]: ) -> list[Fan]:
if self.expected_fans is None: if self.expected_fans is None:
return [] return []
@@ -549,7 +560,7 @@ class BTMinerV2(StockFirmware):
return fans return fans
async def _get_fan_psu( 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: ) -> int | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
@@ -577,7 +588,7 @@ class BTMinerV2(StockFirmware):
return None return None
async def _get_errors( 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]: ) -> list[MinerErrorData]:
errors = [] errors = []
if rpc_get_error_code is None and rpc_summary is None: 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)) errors.append(WhatsminerError(error_code=err))
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass pass
return errors return errors # type: ignore[return-value]
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_summary: dict = None self, rpc_summary: dict | None = None
) -> AlgoHashRate | None: ) -> AlgoHashRateType | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -627,14 +638,17 @@ class BTMinerV2(StockFirmware):
expected_hashrate = rpc_summary["SUMMARY"][0]["Factory GHS"] expected_hashrate = rpc_summary["SUMMARY"][0]["Factory GHS"]
if expected_hashrate: if expected_hashrate:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_hashrate), unit=self.algo.unit.GH rate=float(expected_hashrate),
).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: except LookupError:
pass pass
return None 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: if rpc_get_miner_info is None:
try: try:
rpc_get_miner_info = await self.rpc.get_miner_info() rpc_get_miner_info = await self.rpc.get_miner_info()
@@ -656,15 +670,17 @@ class BTMinerV2(StockFirmware):
dns: str, dns: str,
gateway: str, gateway: str,
subnet_mask: str = "255.255.255.0", subnet_mask: str = "255.255.255.0",
hostname: str = None, hostname: str | None = None,
): ):
if not hostname: if not hostname:
hostname = await self.get_hostname() hostname = await self.get_hostname()
if hostname is None:
hostname = str(self.ip)
await self.rpc.net_config( await self.rpc.net_config(
ip=ip, mask=subnet_mask, dns=dns, gate=gateway, host=hostname, dhcp=False 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: if hostname:
await self.set_hostname(hostname) await self.set_hostname(hostname)
await self.rpc.net_config() await self.rpc.net_config()
@@ -672,7 +688,7 @@ class BTMinerV2(StockFirmware):
async def set_hostname(self, hostname: str): async def set_hostname(self, hostname: str):
await self.rpc.set_hostname(hostname) 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: if rpc_status is None:
try: try:
rpc_status = await self.rpc.status() rpc_status = await self.rpc.status()
@@ -692,7 +708,7 @@ class BTMinerV2(StockFirmware):
pass pass
return False 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -704,8 +720,9 @@ class BTMinerV2(StockFirmware):
return int(rpc_summary["SUMMARY"][0]["Elapsed"]) return int(rpc_summary["SUMMARY"][0]["Elapsed"])
except LookupError: except LookupError:
pass 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()
@@ -735,15 +752,25 @@ class BTMinerV2(StockFirmware):
pass pass
return pools_data 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. Upgrade the firmware of the Whatsminer device.
Args: 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: Returns:
str: Confirmation message after upgrading the firmware. bool: True if firmware upgrade was successful.
""" """
try: try:
logging.info("Starting firmware upgrade process for Whatsminer.") logging.info("Starting firmware upgrade process for Whatsminer.")
@@ -755,12 +782,12 @@ class BTMinerV2(StockFirmware):
async with aiofiles.open(file, "rb") as f: async with aiofiles.open(file, "rb") as f:
upgrade_contents = await f.read() upgrade_contents = await f.read()
result = await self.rpc.update_firmware(upgrade_contents) await self.rpc.update_firmware(upgrade_contents)
logging.info( logging.info(
"Firmware upgrade process completed successfully for Whatsminer." "Firmware upgrade process completed successfully for Whatsminer."
) )
return result return True
except FileNotFoundError as e: except FileNotFoundError as e:
logging.error(f"File not found during the firmware upgrade process: {e}") logging.error(f"File not found during the firmware upgrade process: {e}")
raise raise
@@ -872,13 +899,18 @@ class BTMinerV3(StockFirmware):
except LookupError: except LookupError:
pass pass
self.config = MinerConfig.from_btminer_v3( if pools is not None and settings is not None and device_info is not None:
rpc_pools=pools, rpc_settings=settings, rpc_device_info=device_info self.config = MinerConfig.from_btminer_v3(
) rpc_pools=pools, rpc_settings=settings, rpc_device_info=device_info
)
else:
self.config = MinerConfig()
return self.config 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 self.config = config
conf = config.as_btminer_v3(user_suffix=user_suffix) conf = config.as_btminer_v3(user_suffix=user_suffix)
@@ -902,11 +934,7 @@ class BTMinerV3(StockFirmware):
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
try: try:
data = await self.rpc.set_system_led( data = await self.rpc.set_system_led(
leds=[ leds=[{"color": "red", "period": 60, "duration": 20, "start": 0}]
{
{"color": "red", "period": 60, "duration": 20, "start": 0},
}
],
) )
except APIError: except APIError:
return False return False
@@ -922,7 +950,7 @@ class BTMinerV3(StockFirmware):
data = await self.rpc.set_system_reboot() data = await self.rpc.set_system_reboot()
except APIError: except APIError:
return False return False
if data.get("msg"): if data and data.get("msg"):
if data["msg"] == "ok": if data["msg"] == "ok":
return True return True
return False return False
@@ -932,7 +960,7 @@ class BTMinerV3(StockFirmware):
data = await self.rpc.set_miner_service("restart") data = await self.rpc.set_miner_service("restart")
except APIError: except APIError:
return False return False
if data.get("msg"): if data and data.get("msg"):
if data["msg"] == "ok": if data["msg"] == "ok":
return True return True
return False return False
@@ -942,7 +970,7 @@ class BTMinerV3(StockFirmware):
data = await self.rpc.set_miner_service("stop") data = await self.rpc.set_miner_service("stop")
except APIError: except APIError:
return False return False
if data.get("msg"): if data and data.get("msg"):
if data["msg"] == "ok": if data["msg"] == "ok":
return True return True
return False return False
@@ -952,7 +980,7 @@ class BTMinerV3(StockFirmware):
data = await self.rpc.set_miner_service("start") data = await self.rpc.set_miner_service("start")
except APIError: except APIError:
return False return False
if data.get("msg"): if data and data.get("msg"):
if data["msg"] == "ok": if data["msg"] == "ok":
return True return True
return False return False
@@ -966,74 +994,94 @@ class BTMinerV3(StockFirmware):
else: else:
return True 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: if rpc_get_device_info is None:
try: try:
rpc_get_device_info = await self.rpc.get_device_info() rpc_get_device_info = await self.rpc.get_device_info()
except APIError: except APIError:
return None return None
if rpc_get_device_info is None:
return None
return rpc_get_device_info.get("msg", {}).get("network", {}).get("mac") 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: async def _get_api_version(
if rpc_get_device_info is None: self, rpc_get_device_info: dict | None = 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
) -> str | None: ) -> str | None:
if rpc_get_device_info is None: if rpc_get_device_info is None:
try: try:
rpc_get_device_info = await self.rpc.get_device_info() rpc_get_device_info = await self.rpc.get_device_info()
except APIError: except APIError:
return None 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: if rpc_get_device_info is None:
try: try:
rpc_get_device_info = await self.rpc.get_device_info() rpc_get_device_info = await self.rpc.get_device_info()
except APIError: except APIError:
return None 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") return rpc_get_device_info.get("msg", {}).get("network", {}).get("hostname")
async def _get_light_flashing( async def _get_light_flashing(
self, rpc_get_device_info: dict = None self, rpc_get_device_info: dict | None = None
) -> bool | None: ) -> bool | None:
if rpc_get_device_info is None: if rpc_get_device_info is None:
try: try:
rpc_get_device_info = await self.rpc.get_device_info() rpc_get_device_info = await self.rpc.get_device_info()
except APIError: except APIError:
return None return None
if rpc_get_device_info is None:
return None
val = rpc_get_device_info.get("msg", {}).get("system", {}).get("ledstatus") val = rpc_get_device_info.get("msg", {}).get("system", {}).get("ledstatus")
if isinstance(val, str): if isinstance(val, str):
return val != "auto" return val != "auto"
return None return None
async def _get_wattage_limit( async def _get_wattage_limit(
self, rpc_get_device_info: dict = None self, rpc_get_device_info: dict | None = None
) -> float | None: ) -> int | None:
if rpc_get_device_info is None: if rpc_get_device_info is None:
try: try:
rpc_get_device_info = await self.rpc.get_device_info() rpc_get_device_info = await self.rpc.get_device_info()
except APIError: except APIError:
return None return None
if rpc_get_device_info is None:
return None
val = rpc_get_device_info.get("msg", {}).get("miner", {}).get("power-limit-set") val = rpc_get_device_info.get("msg", {}).get("miner", {}).get("power-limit-set")
try: try:
return float(val) return int(float(val))
except (ValueError, TypeError): except (ValueError, TypeError):
return None 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: if rpc_get_miner_status_summary is None:
try: try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError: except APIError:
return [] return []
fans = [] fans = []
if rpc_get_miner_status_summary is None:
return []
summary = rpc_get_miner_status_summary.get("msg", {}).get("summary", {}) summary = rpc_get_miner_status_summary.get("msg", {}).get("summary", {})
for idx, direction in enumerate(["in", "out"]): for idx, direction in enumerate(["in", "out"]):
rpm = summary.get(f"fan-speed-{direction}") rpm = summary.get(f"fan-speed-{direction}")
@@ -1041,19 +1089,21 @@ class BTMinerV3(StockFirmware):
fans.append(Fan(speed=rpm)) fans.append(Fan(speed=rpm))
return fans 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: if rpc_get_device_info is None:
try: try:
rpc_get_device_info = await self.rpc.get_device_info() rpc_get_device_info = await self.rpc.get_device_info()
except APIError: except APIError:
return [] return []
if rpc_get_device_info is None:
return []
rpm = rpc_get_device_info.get("msg", {}).get("power", {}).get("fanspeed") rpm = rpc_get_device_info.get("msg", {}).get("power", {}).get("fanspeed")
return [Fan(speed=rpm)] if rpm is not None else [] return [Fan(speed=rpm)] if rpm is not None else []
async def _get_hashboards( async def _get_hashboards(
self, self,
rpc_get_device_info: dict = None, rpc_get_device_info: dict | None = None,
rpc_get_miner_status_edevs: dict = None, rpc_get_miner_status_edevs: dict | None = None,
) -> list[HashBoard]: ) -> list[HashBoard]:
if rpc_get_device_info is None: if rpc_get_device_info is None:
try: try:
@@ -1067,6 +1117,8 @@ class BTMinerV3(StockFirmware):
return [] return []
boards = [] boards = []
if rpc_get_device_info is None or rpc_get_miner_status_edevs is None:
return []
board_count = ( board_count = (
rpc_get_device_info.get("msg", {}).get("hardware", {}).get("boards", 3) rpc_get_device_info.get("msg", {}).get("hardware", {}).get("boards", 3)
) )
@@ -1077,8 +1129,11 @@ class BTMinerV3(StockFirmware):
HashBoard( HashBoard(
slot=idx, slot=idx,
hashrate=self.algo.hashrate( hashrate=self.algo.hashrate(
rate=board_data.get("hash-average", 0), unit=self.algo.unit.TH rate=board_data.get("hash-average", 0),
).into(self.algo.unit.default), 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"), temp=board_data.get("chip-temp-min"),
inlet_temp=board_data.get("chip-temp-min"), inlet_temp=board_data.get("chip-temp-min"),
outlet_temp=board_data.get("chip-temp-max"), outlet_temp=board_data.get("chip-temp-max"),
@@ -1095,7 +1150,7 @@ class BTMinerV3(StockFirmware):
return boards return boards
async def _get_pools( async def _get_pools(
self, rpc_get_miner_status_summary: dict = None self, rpc_get_miner_status_summary: dict | None = None
) -> list[PoolMetrics]: ) -> list[PoolMetrics]:
if rpc_get_miner_status_summary is None: if rpc_get_miner_status_summary is None:
try: try:
@@ -1103,6 +1158,8 @@ class BTMinerV3(StockFirmware):
except APIError: except APIError:
return [] return []
pools = [] pools = []
if rpc_get_miner_status_summary is None:
return []
msg_pools = rpc_get_miner_status_summary.get("msg", {}).get("pools", []) msg_pools = rpc_get_miner_status_summary.get("msg", {}).get("pools", [])
for idx, pool in enumerate(msg_pools): for idx, pool in enumerate(msg_pools):
pools.append( pools.append(
@@ -1117,13 +1174,15 @@ class BTMinerV3(StockFirmware):
return pools return pools
async def _get_uptime( async def _get_uptime(
self, rpc_get_miner_status_summary: dict = None self, rpc_get_miner_status_summary: dict | None = None
) -> int | None: ) -> int | None:
if rpc_get_miner_status_summary is None: if rpc_get_miner_status_summary is None:
try: try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError: except APIError:
return None return None
if rpc_get_miner_status_summary is None:
return None
return ( return (
rpc_get_miner_status_summary.get("msg", {}) rpc_get_miner_status_summary.get("msg", {})
.get("summary", {}) .get("summary", {})
@@ -1131,27 +1190,37 @@ class BTMinerV3(StockFirmware):
) )
async def _get_wattage( async def _get_wattage(
self, rpc_get_miner_status_summary: dict = None self, rpc_get_miner_status_summary: dict | None = None
) -> float | None: ) -> int | None:
if rpc_get_miner_status_summary is None: if rpc_get_miner_status_summary is None:
try: try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError: except APIError:
return None return None
return ( if rpc_get_miner_status_summary is None:
return None
power_val = (
rpc_get_miner_status_summary.get("msg", {}) rpc_get_miner_status_summary.get("msg", {})
.get("summary", {}) .get("summary", {})
.get("power-realtime") .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( async def _get_hashrate(
self, rpc_get_miner_status_summary: dict = None self, rpc_get_miner_status_summary: dict | None = None
) -> float | None: ) -> AlgoHashRateType | None:
if rpc_get_miner_status_summary is None: if rpc_get_miner_status_summary is None:
try: try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError: except APIError:
return None return None
if rpc_get_miner_status_summary is None:
return None
return ( return (
rpc_get_miner_status_summary.get("msg", {}) rpc_get_miner_status_summary.get("msg", {})
.get("summary", {}) .get("summary", {})
@@ -1159,31 +1228,37 @@ class BTMinerV3(StockFirmware):
) )
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_get_miner_status_summary: dict = None self, rpc_get_miner_status_summary: dict | None = None
) -> float | None: ) -> AlgoHashRateType | None:
if rpc_get_miner_status_summary is None: if rpc_get_miner_status_summary is None:
try: try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError: except APIError:
return None return None
if rpc_get_miner_status_summary is None:
return None
res = ( res = (
rpc_get_miner_status_summary.get("msg", {}) rpc_get_miner_status_summary.get("msg", {})
.get("summary", {}) .get("summary", {})
.get("factory-hash") .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 None
return res return res
async def _get_env_temp( async def _get_env_temp(
self, rpc_get_miner_status_summary: dict = None self, rpc_get_miner_status_summary: dict | None = None
) -> float | None: ) -> float | None:
if rpc_get_miner_status_summary is None: if rpc_get_miner_status_summary is None:
try: try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError: except APIError:
return None return None
if rpc_get_miner_status_summary is None:
return None
return ( return (
rpc_get_miner_status_summary.get("msg", {}) rpc_get_miner_status_summary.get("msg", {})
.get("summary", {}) .get("summary", {})

View File

@@ -14,11 +14,10 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware from pyasic.miners.device.firmware import StockFirmware
@@ -75,7 +74,7 @@ class CGMiner(StockFirmware):
try: try:
pools = await self.rpc.pools() pools = await self.rpc.pools()
except APIError: except APIError:
return self.config return self.config or MinerConfig()
self.config = MinerConfig.from_api(pools) self.config = MinerConfig.from_api(pools)
return self.config return self.config
@@ -84,7 +83,7 @@ class CGMiner(StockFirmware):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -99,7 +98,7 @@ class CGMiner(StockFirmware):
return self.api_ver 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -114,7 +113,9 @@ class CGMiner(StockFirmware):
return self.fw_ver 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -125,12 +126,15 @@ class CGMiner(StockFirmware):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -142,8 +146,9 @@ class CGMiner(StockFirmware):
return int(rpc_stats["STATS"][1]["Elapsed"]) return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()

View File

@@ -13,13 +13,11 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic import APIError, MinerConfig from pyasic import APIError, MinerConfig
from pyasic.data import Fan, HashBoard, X19Error from pyasic.data import Fan, HashBoard, X19Error
from pyasic.data.error_codes import MinerErrorData
from pyasic.data.pools import PoolMetrics, PoolUrl from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.device.algorithm import AlgoHashRate from pyasic.device.algorithm import AlgoHashRateType
from pyasic.miners.data import ( from pyasic.miners.data import (
DataFunction, DataFunction,
DataLocations, DataLocations,
@@ -95,9 +93,13 @@ class ElphapexMiner(StockFirmware):
data = await self.web.get_miner_conf() data = await self.web.get_miner_conf()
if data: if data:
self.config = MinerConfig.from_elphapex(data) self.config = MinerConfig.from_elphapex(data)
if self.config is None:
self.config = MinerConfig()
return self.config 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 self.config = config
await self.web.set_miner_conf(config.as_elphapex(user_suffix=user_suffix)) await self.web.set_miner_conf(config.as_elphapex(user_suffix=user_suffix))
@@ -106,14 +108,14 @@ class ElphapexMiner(StockFirmware):
if data: if data:
if data.get("code") == "B000": if data.get("code") == "B000":
self.light = True self.light = True
return self.light return self.light if self.light is not None else False
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
data = await self.web.blink(blink=False) data = await self.web.blink(blink=False)
if data: if data:
if data.get("code") == "B100": if data.get("code") == "B100":
self.light = False self.light = False
return self.light return self.light if self.light is not None else False
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.web.reboot() data = await self.web.reboot()
@@ -121,7 +123,7 @@ class ElphapexMiner(StockFirmware):
return True return True
return False 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -136,7 +138,7 @@ class ElphapexMiner(StockFirmware):
return self.api_ver 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -155,7 +157,9 @@ class ElphapexMiner(StockFirmware):
return self.fw_ver 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -167,8 +171,9 @@ class ElphapexMiner(StockFirmware):
return web_get_system_info["hostname"] return web_get_system_info["hostname"]
except KeyError: except KeyError:
pass 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -187,8 +192,11 @@ class ElphapexMiner(StockFirmware):
return data["macaddr"] return data["macaddr"]
except KeyError: except KeyError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -208,7 +216,7 @@ class ElphapexMiner(StockFirmware):
pass pass
return errors 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: if self.expected_hashboards is None:
return [] return []
@@ -227,8 +235,11 @@ class ElphapexMiner(StockFirmware):
try: try:
for board in web_stats["STATS"][0]["chain"]: for board in web_stats["STATS"][0]["chain"]:
hashboards[board["index"]].hashrate = self.algo.hashrate( hashboards[board["index"]].hashrate = self.algo.hashrate(
rate=board["rate_real"], unit=self.algo.unit.MH rate=board["rate_real"],
).into(self.algo.unit.default) 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"] hashboards[board["index"]].chips = board["asic_num"]
board_temp_data = list( board_temp_data = list(
filter(lambda x: not x == 0, board["temp_pcb"]) filter(lambda x: not x == 0, board["temp_pcb"])
@@ -250,8 +261,8 @@ class ElphapexMiner(StockFirmware):
return hashboards return hashboards
async def _get_fault_light( async def _get_fault_light(
self, web_get_blink_status: dict = None self, web_get_blink_status: dict | None = None
) -> Optional[bool]: ) -> bool | None:
if self.light: if self.light:
return self.light return self.light
@@ -269,8 +280,8 @@ class ElphapexMiner(StockFirmware):
return self.light return self.light
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, web_stats: dict = None self, web_stats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if web_stats is None: if web_stats is None:
try: try:
web_stats = await self.web.stats() web_stats = await self.web.stats()
@@ -286,11 +297,12 @@ class ElphapexMiner(StockFirmware):
rate_unit = "MH" rate_unit = "MH"
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) 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: except LookupError:
pass 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: if web_get_miner_conf is None:
try: try:
web_get_miner_conf = await self.web.get_miner_conf() web_get_miner_conf = await self.web.get_miner_conf()
@@ -306,8 +318,9 @@ class ElphapexMiner(StockFirmware):
return False return False
except LookupError: except LookupError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -319,8 +332,9 @@ class ElphapexMiner(StockFirmware):
return int(web_summary["SUMMARY"][1]["elapsed"]) return int(web_summary["SUMMARY"][1]["elapsed"])
except LookupError: except LookupError:
pass 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: if self.expected_fans is None:
return [] return []
@@ -340,13 +354,16 @@ class ElphapexMiner(StockFirmware):
return fans 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: if web_pools is None:
try: try:
web_pools = await self.web.pools() web_pools = await self.web.pools()
except APIError: except APIError:
return [] return []
if web_pools is None:
return []
active_pool_index = None active_pool_index = None
highest_priority = float("inf") highest_priority = float("inf")
@@ -359,23 +376,22 @@ class ElphapexMiner(StockFirmware):
active_pool_index = pool_info["index"] active_pool_index = pool_info["index"]
pools_data = [] pools_data = []
if web_pools is not None: try:
try: for pool_info in web_pools["POOLS"]:
for pool_info in web_pools["POOLS"]: url = pool_info.get("url")
url = pool_info.get("url") pool_url = PoolUrl.from_str(url) if url else None
pool_url = PoolUrl.from_str(url) if url else None pool_data = PoolMetrics(
pool_data = PoolMetrics( accepted=pool_info.get("accepted"),
accepted=pool_info.get("accepted"), rejected=pool_info.get("rejected"),
rejected=pool_info.get("rejected"), get_failures=pool_info.get("stale"),
get_failures=pool_info.get("stale"), remote_failures=pool_info.get("discarded"),
remote_failures=pool_info.get("discarded"), active=pool_info.get("index") == active_pool_index,
active=pool_info.get("index") == active_pool_index, alive=pool_info.get("status") == "Alive",
alive=pool_info.get("status") == "Alive", url=pool_url,
url=pool_url, user=pool_info.get("user"),
user=pool_info.get("user"), index=pool_info.get("index"),
index=pool_info.get("index"), )
) pools_data.append(pool_data)
pools_data.append(pool_data) except LookupError:
except LookupError: pass
pass
return pools_data return pools_data

View File

@@ -14,14 +14,12 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pathlib import Path
from typing import List, Optional
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, X19Error from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.logger import logger from pyasic.logger import logger
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
@@ -116,7 +114,9 @@ class ePIC(ePICFirmware):
self.config = cfg self.config = cfg
return self.config 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 self.config = config
conf = self.config.as_epic(user_suffix=user_suffix) conf = self.config.as_epic(user_suffix=user_suffix)
@@ -180,7 +180,7 @@ class ePIC(ePICFirmware):
pass pass
return False 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: if web_network is None:
try: try:
web_network = await self.web.network() web_network = await self.web.network()
@@ -194,8 +194,9 @@ class ePIC(ePICFirmware):
return mac return mac
except KeyError: except KeyError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -208,8 +209,9 @@ class ePIC(ePICFirmware):
return hostname return hostname
except KeyError: except KeyError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -223,8 +225,11 @@ class ePIC(ePICFirmware):
return wattage return wattage
except KeyError: except KeyError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -238,14 +243,16 @@ class ePIC(ePICFirmware):
for hb in web_summary["HBs"]: for hb in web_summary["HBs"]:
hashrate += hb["Hashrate"][0] hashrate += hb["Hashrate"][0]
return self.algo.hashrate( return self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.MH rate=float(hashrate),
).into(self.algo.unit.TH) unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.TH) # type: ignore[attr-defined]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass pass
return None
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, web_summary: dict = None self, web_summary: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if web_summary is None: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -264,12 +271,14 @@ class ePIC(ePICFirmware):
hashrate += hb["Hashrate"][0] / ideal hashrate += hb["Hashrate"][0] / ideal
return self.algo.hashrate( return self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.MH rate=float(hashrate),
).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): except (LookupError, ValueError, TypeError):
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -283,8 +292,9 @@ class ePIC(ePICFirmware):
return fw_ver return fw_ver
except KeyError: except KeyError:
pass 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: if self.expected_fans is None:
return [] return []
@@ -305,8 +315,8 @@ class ePIC(ePICFirmware):
return fans return fans
async def _get_hashboards( async def _get_hashboards(
self, web_summary: dict = None, web_capabilities: dict = None self, web_summary: dict | None = None, web_capabilities: dict | None = None
) -> List[HashBoard]: ) -> list[HashBoard]:
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
@@ -362,16 +372,18 @@ class ePIC(ePICFirmware):
# Update the Hashboard object # Update the Hashboard object
hb_list[hb["Index"]].missing = False hb_list[hb["Index"]].missing = False
hb_list[hb["Index"]].hashrate = self.algo.hashrate( hb_list[hb["Index"]].hashrate = self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.MH rate=float(hashrate),
).into(self.algo.unit.default) 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"]].chips = num_of_chips
hb_list[hb["Index"]].temp = int(hb["Temperature"]) hb_list[hb["Index"]].temp = int(hb["Temperature"])
hb_list[hb["Index"]].tuned = tuned hb_list[hb["Index"]].tuned = tuned
hb_list[hb["Index"]].active = active hb_list[hb["Index"]].active = active
hb_list[hb["Index"]].voltage = hb["Input Voltage"] hb_list[hb["Index"]].voltage = hb["Input Voltage"]
return hb_list 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -383,8 +395,9 @@ class ePIC(ePICFirmware):
return not op_state == "Idling" return not op_state == "Idling"
except KeyError: except KeyError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -399,7 +412,7 @@ class ePIC(ePICFirmware):
pass pass
return None 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -414,7 +427,9 @@ class ePIC(ePICFirmware):
pass pass
return False 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: if not web_summary:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -427,12 +442,11 @@ class ePIC(ePICFirmware):
error = web_summary["Status"]["Last Error"] error = web_summary["Status"]["Last Error"]
if error is not None: if error is not None:
errors.append(X19Error(error_message=str(error))) errors.append(X19Error(error_message=str(error)))
return errors
except KeyError: except KeyError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -466,18 +480,29 @@ class ePIC(ePICFirmware):
return pool_data return pool_data
except LookupError: except LookupError:
pass pass
return []
async def upgrade_firmware( 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: ) -> bool:
""" """
Upgrade the firmware of the ePIC miner device. Upgrade the firmware of the ePIC miner device.
Args: Args:
file (Path | str): The local file path of the firmware to be uploaded. file: The local file path of the firmware to be uploaded.
keep_settings (bool): Whether to keep the current settings after the update. 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: Returns:
bool: Whether the firmware update succeeded. 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

View File

@@ -1,8 +1,6 @@
from typing import List, Optional
from pyasic import APIError, MinerConfig from pyasic import APIError, MinerConfig
from pyasic.data import Fan, HashBoard 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.device.firmware import MinerFirmware
from pyasic.miners.base import BaseMiner from pyasic.miners.base import BaseMiner
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
@@ -72,24 +70,28 @@ class ESPMiner(BaseMiner):
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
return MinerConfig.from_espminer(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()) 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: if web_system_info is None:
try: try:
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
except APIError: except APIError:
pass pass
if web_system_info is not None: if web_system_info is not None:
try: try:
return round(web_system_info["power"]) return round(web_system_info["power"])
except KeyError: except KeyError:
pass pass
return None
async def _get_hashrate( async def _get_hashrate(
self, web_system_info: dict = None self, web_system_info: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if web_system_info is None: if web_system_info is None:
try: try:
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
@@ -99,14 +101,18 @@ class ESPMiner(BaseMiner):
if web_system_info is not None: if web_system_info is not None:
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(web_system_info["hashRate"]), unit=self.algo.unit.GH rate=float(web_system_info["hashRate"]),
).into(self.algo.unit.default) unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except KeyError: except KeyError:
pass pass
return None
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, web_system_info: dict = None self, web_system_info: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if web_system_info is None: if web_system_info is None:
try: try:
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
@@ -126,15 +132,23 @@ class ESPMiner(BaseMiner):
except APIError: except APIError:
pass pass
expected_hashrate = small_core_count * asic_count * frequency if (
small_core_count is not None
return self.algo.hashrate( and asic_count is not None
rate=float(expected_hashrate), unit=self.algo.unit.MH and frequency is not None
).into(self.algo.unit.default) ):
expected_hashrate = small_core_count * asic_count * frequency
return self.algo.hashrate(
rate=float(expected_hashrate),
unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except KeyError: except KeyError:
pass 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: if web_system_info is None:
try: try:
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
@@ -146,8 +160,11 @@ class ESPMiner(BaseMiner):
return web_system_info["uptimeSeconds"] return web_system_info["uptimeSeconds"]
except KeyError: except KeyError:
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -163,8 +180,10 @@ class ESPMiner(BaseMiner):
HashBoard( HashBoard(
hashrate=self.algo.hashrate( hashrate=self.algo.hashrate(
rate=float(web_system_info["hashRate"]), rate=float(web_system_info["hashRate"]),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default), ).into(
self.algo.unit.default # type: ignore[attr-defined]
),
chip_temp=web_system_info.get("temp"), chip_temp=web_system_info.get("temp"),
temp=web_system_info.get("vrTemp"), temp=web_system_info.get("vrTemp"),
chips=web_system_info.get("asicCount", 1), chips=web_system_info.get("asicCount", 1),
@@ -178,7 +197,7 @@ class ESPMiner(BaseMiner):
pass pass
return [] 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: if self.expected_fans is None:
return [] return []
@@ -195,7 +214,7 @@ class ESPMiner(BaseMiner):
pass pass
return [] 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: if web_system_info is None:
try: try:
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
@@ -207,8 +226,9 @@ class ESPMiner(BaseMiner):
return web_system_info["hostname"] return web_system_info["hostname"]
except KeyError: except KeyError:
pass 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: if web_system_info is None:
try: try:
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
@@ -220,8 +240,9 @@ class ESPMiner(BaseMiner):
return web_system_info["version"] return web_system_info["version"]
except KeyError: except KeyError:
pass 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: if web_system_info is None:
try: try:
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
@@ -233,8 +254,9 @@ class ESPMiner(BaseMiner):
return web_system_info["version"] return web_system_info["version"]
except KeyError: except KeyError:
pass 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: if web_system_info is None:
try: try:
web_system_info = await self.web.system_info() web_system_info = await self.web.system_info()
@@ -246,3 +268,4 @@ class ESPMiner(BaseMiner):
return web_system_info["macAddr"].upper() return web_system_info["macAddr"].upper()
except KeyError: except KeyError:
pass pass
return None

View File

@@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List
from pyasic.config import MinerConfig, MiningModeConfig from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import HashBoard from pyasic.data import HashBoard
@@ -86,12 +85,15 @@ class GoldshellMiner(BFGMiner):
try: try:
pools = await self.web.pools() pools = await self.web.pools()
except APIError: except APIError:
return self.config if self.config is not None:
return self.config
self.config = MinerConfig.from_goldshell(pools) self.config = MinerConfig.from_goldshell(pools)
return self.config 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() pools_data = await self.web.pools()
# have to delete all the pools one at a time first # have to delete all the pools one at a time first
for pool in pools_data: for pool in pools_data:
@@ -116,35 +118,37 @@ class GoldshellMiner(BFGMiner):
settings["select"] = idx settings["select"] = idx
await self.web.set_setting(settings) 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: if web_setting is None:
try: try:
web_setting = await self.web.setting() web_setting = await self.web.setting()
except APIError: except APIError:
pass return None
if web_setting is not None: if web_setting is not None:
try: try:
return web_setting["name"] return web_setting["name"]
except KeyError: except KeyError:
pass 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: if web_status is None:
try: try:
web_status = await self.web.setting() web_status = await self.web.setting()
except APIError: except APIError:
pass return None
if web_status is not None: if web_status is not None:
try: try:
return web_status["firmware"] return web_status["firmware"]
except KeyError: except KeyError:
pass pass
return None
async def _get_hashboards( async def _get_hashboards(
self, rpc_devs: dict = None, rpc_devdetails: dict = None self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None
) -> List[HashBoard]: ) -> list[HashBoard]:
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
@@ -166,8 +170,11 @@ class GoldshellMiner(BFGMiner):
try: try:
b_id = board["ID"] b_id = board["ID"]
hashboards[b_id].hashrate = self.algo.hashrate( hashboards[b_id].hashrate = self.algo.hashrate(
rate=float(board["MHS 20s"]), unit=self.algo.unit.MH rate=float(board["MHS 20s"]),
).into(self.algo.unit.default) 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].temp = board["tstemp-2"]
hashboards[b_id].missing = False hashboards[b_id].missing = False
except KeyError: except KeyError:

View File

@@ -14,13 +14,13 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional from typing import cast
from pyasic import MinerConfig from pyasic import MinerConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, X19Error from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import ( from pyasic.miners.data import (
DataFunction, DataFunction,
@@ -106,9 +106,11 @@ class BlackMiner(StockFirmware):
data = await self.web.get_miner_conf() data = await self.web.get_miner_conf()
if data: if data:
self.config = MinerConfig.from_hammer(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 self.config = config
await self.web.set_miner_conf(config.as_hammer(user_suffix=user_suffix)) await self.web.set_miner_conf(config.as_hammer(user_suffix=user_suffix))
@@ -117,14 +119,14 @@ class BlackMiner(StockFirmware):
if data: if data:
if data.get("code") == "B000": if data.get("code") == "B000":
self.light = True self.light = True
return self.light return self.light or False
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
data = await self.web.blink(blink=False) data = await self.web.blink(blink=False)
if data: if data:
if data.get("code") == "B100": if data.get("code") == "B100":
self.light = False self.light = False
return self.light return self.light or False
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.web.reboot() data = await self.web.reboot()
@@ -132,7 +134,7 @@ class BlackMiner(StockFirmware):
return True return True
return False 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -147,7 +149,7 @@ class BlackMiner(StockFirmware):
return self.api_ver 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -162,7 +164,9 @@ class BlackMiner(StockFirmware):
return self.fw_ver 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 # get hr from API
if rpc_summary is None: if rpc_summary is None:
try: try:
@@ -174,12 +178,13 @@ class BlackMiner(StockFirmware):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]), rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -232,8 +237,11 @@ class BlackMiner(StockFirmware):
hashrate = boards[1].get(f"chain_rate{i}") hashrate = boards[1].get(f"chain_rate{i}")
if hashrate: if hashrate:
hashboard.hashrate = self.algo.hashrate( hashboard.hashrate = self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.MH rate=float(hashrate),
).into(self.algo.unit.default) 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}") chips = boards[1].get(f"chain_acn{i}")
if chips: if chips:
@@ -247,7 +255,7 @@ class BlackMiner(StockFirmware):
return hashboards 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: if self.expected_fans is None:
return [] return []
@@ -272,14 +280,16 @@ class BlackMiner(StockFirmware):
for fan in range(self.expected_fans): for fan in range(self.expected_fans):
fans[fan].speed = rpc_stats["STATS"][1].get( fans[fan].speed = rpc_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0 f"fan{fan_offset + fan}", 0
) )
except LookupError: except LookupError:
pass pass
return fans 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -291,8 +301,9 @@ class BlackMiner(StockFirmware):
return web_get_system_info["hostname"] return web_get_system_info["hostname"]
except KeyError: except KeyError:
pass 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -311,8 +322,11 @@ class BlackMiner(StockFirmware):
return data["macaddr"] return data["macaddr"]
except KeyError: except KeyError:
pass 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -330,11 +344,11 @@ class BlackMiner(StockFirmware):
continue continue
except LookupError: except LookupError:
pass pass
return errors return cast(list[MinerErrorData], errors)
async def _get_fault_light( async def _get_fault_light(
self, web_get_blink_status: dict = None self, web_get_blink_status: dict | None = None
) -> Optional[bool]: ) -> bool | None:
if self.light: if self.light:
return self.light return self.light
@@ -352,8 +366,8 @@ class BlackMiner(StockFirmware):
return self.light return self.light
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_stats: dict = None self, rpc_stats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_stats is None: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -364,16 +378,22 @@ class BlackMiner(StockFirmware):
try: try:
expected_rate = rpc_stats["STATS"][1].get("total_rateideal") expected_rate = rpc_stats["STATS"][1].get("total_rateideal")
if expected_rate is None: 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: try:
rate_unit = rpc_stats["STATS"][1]["rate_unit"] rate_unit = rpc_stats["STATS"][1]["rate_unit"]
except KeyError: except KeyError:
rate_unit = "MH" rate_unit = "MH"
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) 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: except LookupError:
pass pass
return None
async def set_static_ip( async def set_static_ip(
self, self,
@@ -381,10 +401,10 @@ class BlackMiner(StockFirmware):
dns: str, dns: str,
gateway: str, gateway: str,
subnet_mask: str = "255.255.255.0", subnet_mask: str = "255.255.255.0",
hostname: str = None, hostname: str | None = None,
): ):
if not hostname: if not hostname:
hostname = await self.get_hostname() hostname = await self.get_hostname() or ""
await self.web.set_network_conf( await self.web.set_network_conf(
ip=ip, ip=ip,
dns=dns, dns=dns,
@@ -394,9 +414,9 @@ class BlackMiner(StockFirmware):
protocol=2, protocol=2,
) )
async def set_dhcp(self, hostname: str = None): async def set_dhcp(self, hostname: str | None = None):
if not hostname: if not hostname:
hostname = await self.get_hostname() hostname = await self.get_hostname() or ""
await self.web.set_network_conf( await self.web.set_network_conf(
ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1 ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1
) )
@@ -417,7 +437,7 @@ class BlackMiner(StockFirmware):
protocol=protocol, 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: if web_get_conf is None:
try: try:
web_get_conf = await self.web.get_miner_conf() web_get_conf = await self.web.get_miner_conf()
@@ -433,8 +453,9 @@ class BlackMiner(StockFirmware):
return False return False
except LookupError: except LookupError:
pass 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: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -446,8 +467,9 @@ class BlackMiner(StockFirmware):
return int(rpc_stats["STATS"][1]["Elapsed"]) return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()

View File

@@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import Optional
from pyasic import APIError from pyasic import APIError
from pyasic.config import MinerConfig, MiningModeConfig from pyasic.config import MinerConfig, MiningModeConfig
@@ -92,7 +91,7 @@ class HiveonModern(HiveonFirmware, BMMiner):
web: HiveonWebAPI web: HiveonWebAPI
_web_cls = 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() data = await self.web.get_miner_conf()
if data: if data:
self.config = MinerConfig.from_hiveon_modern(data) self.config = MinerConfig.from_hiveon_modern(data)
@@ -103,14 +102,16 @@ class HiveonModern(HiveonFirmware, BMMiner):
if data: if data:
if data.get("code") == "B000": if data.get("code") == "B000":
self.light = True self.light = True
return self.light return True
return False
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
data = await self.web.blink(blink=False) data = await self.web.blink(blink=False)
if data: if data:
if data.get("code") == "B100": if data.get("code") == "B100":
self.light = False self.light = False
return self.light return True
return False
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.web.reboot() data = await self.web.reboot()
@@ -120,17 +121,21 @@ class HiveonModern(HiveonFirmware, BMMiner):
async def stop_mining(self) -> bool: async def stop_mining(self) -> bool:
cfg = await self.get_config() cfg = await self.get_config()
cfg.mining_mode = MiningModeConfig.sleep() if cfg is not None:
await self.send_config(cfg) cfg.mining_mode = MiningModeConfig.sleep()
return True await self.send_config(cfg)
return True
return False
async def resume_mining(self) -> bool: async def resume_mining(self) -> bool:
cfg = await self.get_config() cfg = await self.get_config()
cfg.mining_mode = MiningModeConfig.normal() if cfg is not None:
await self.send_config(cfg) cfg.mining_mode = MiningModeConfig.normal()
return True 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: if not rpc_stats:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -139,15 +144,19 @@ class HiveonModern(HiveonFirmware, BMMiner):
if rpc_stats: if rpc_stats:
boards = rpc_stats.get("STATS") boards = rpc_stats.get("STATS")
try: if boards:
wattage_raw = boards[1]["chain_power"] try:
except (KeyError, IndexError): wattage_raw = boards[1]["chain_power"]
pass except (KeyError, IndexError):
else: pass
# parse wattage position out of raw data else:
return round(float(wattage_raw.split(" ")[0])) # 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -159,8 +168,9 @@ class HiveonModern(HiveonFirmware, BMMiner):
return web_get_system_info["hostname"] return web_get_system_info["hostname"]
except KeyError: except KeyError:
pass 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: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -179,10 +189,11 @@ class HiveonModern(HiveonFirmware, BMMiner):
return data["macaddr"] return data["macaddr"]
except KeyError: except KeyError:
pass pass
return None
async def _get_fault_light( async def _get_fault_light(
self, web_get_blink_status: dict = None self, web_get_blink_status: dict | None = None
) -> Optional[bool]: ) -> bool | None:
if self.light: if self.light:
return self.light return self.light
@@ -199,7 +210,7 @@ class HiveonModern(HiveonFirmware, BMMiner):
pass pass
return self.light 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: if web_get_conf is None:
try: try:
web_get_conf = await self.web.get_miner_conf() web_get_conf = await self.web.get_miner_conf()
@@ -215,6 +226,7 @@ class HiveonModern(HiveonFirmware, BMMiner):
return False return False
except LookupError: except LookupError:
pass pass
return None
HIVEON_OLD_DATA_LOC = DataLocations( HIVEON_OLD_DATA_LOC = DataLocations(
@@ -262,7 +274,7 @@ HIVEON_OLD_DATA_LOC = DataLocations(
class HiveonOld(HiveonFirmware, BMMiner): class HiveonOld(HiveonFirmware, BMMiner):
data_locations = HIVEON_OLD_DATA_LOC 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: if not rpc_stats:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -271,10 +283,12 @@ class HiveonOld(HiveonFirmware, BMMiner):
if rpc_stats: if rpc_stats:
boards = rpc_stats.get("STATS") boards = rpc_stats.get("STATS")
try: if boards:
wattage_raw = boards[1]["chain_power"] try:
except (KeyError, IndexError): wattage_raw = boards[1]["chain_power"]
pass except (KeyError, IndexError):
else: pass
# parse wattage position out of raw data else:
return round(float(wattage_raw.split(" ")[0])) # parse wattage position out of raw data
return round(float(wattage_raw.split(" ")[0]))
return None

View File

@@ -1,9 +1,7 @@
from typing import List, Optional
from pyasic import MinerConfig from pyasic import MinerConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.miners.device.firmware import StockFirmware from pyasic.miners.device.firmware import StockFirmware
@@ -78,7 +76,7 @@ class IceRiver(StockFirmware):
return MinerConfig.from_iceriver(web_userpanel) 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: if self.expected_fans is None:
return [] return []
@@ -86,7 +84,7 @@ class IceRiver(StockFirmware):
try: try:
web_userpanel = await self.web.userpanel() web_userpanel = await self.web.userpanel()
except APIError: except APIError:
pass return []
if web_userpanel is not None: if web_userpanel is not None:
try: try:
@@ -95,13 +93,14 @@ class IceRiver(StockFirmware):
] ]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if web_userpanel is None:
try: try:
web_userpanel = await self.web.userpanel() web_userpanel = await self.web.userpanel()
except APIError: except APIError:
pass return None
if web_userpanel is not None: if web_userpanel is not None:
try: try:
@@ -110,26 +109,30 @@ class IceRiver(StockFirmware):
) )
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if web_userpanel is None:
try: try:
web_userpanel = await self.web.userpanel() web_userpanel = await self.web.userpanel()
except APIError: except APIError:
pass return None
if web_userpanel is not None: if web_userpanel is not None:
try: try:
return web_userpanel["userpanel"]["data"]["host"] return web_userpanel["userpanel"]["data"]["host"]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if web_userpanel is None:
try: try:
web_userpanel = await self.web.userpanel() web_userpanel = await self.web.userpanel()
except APIError: except APIError:
pass return None
if web_userpanel is not None: if web_userpanel is not None:
try: try:
@@ -144,8 +147,9 @@ class IceRiver(StockFirmware):
).into(MinerAlgo.SHA256.unit.default) ).into(MinerAlgo.SHA256.unit.default)
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if web_userpanel is None:
try: try:
web_userpanel = await self.web.userpanel() web_userpanel = await self.web.userpanel()
@@ -159,20 +163,23 @@ class IceRiver(StockFirmware):
pass pass
return False 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: if web_userpanel is None:
try: try:
web_userpanel = await self.web.userpanel() web_userpanel = await self.web.userpanel()
except APIError: except APIError:
pass return False
if web_userpanel is not None: if web_userpanel is not None:
try: try:
return web_userpanel["userpanel"]["data"]["powstate"] return web_userpanel["userpanel"]["data"]["powstate"]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -195,15 +202,17 @@ class IceRiver(StockFirmware):
hb_list[idx].temp = round(board["intmp"]) hb_list[idx].temp = round(board["intmp"])
hb_list[idx].hashrate = self.algo.hashrate( hb_list[idx].hashrate = self.algo.hashrate(
rate=float(board["rtpow"].replace("G", "")), rate=float(board["rtpow"].replace("G", "")),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
hb_list[idx].chips = int(board["chipnum"]) hb_list[idx].chips = int(board["chipnum"])
hb_list[idx].missing = False hb_list[idx].missing = False
except LookupError: except LookupError:
pass pass
return hb_list 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: if web_userpanel is None:
try: try:
web_userpanel = await self.web.userpanel() web_userpanel = await self.web.userpanel()
@@ -222,8 +231,9 @@ class IceRiver(StockFirmware):
) )
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if web_userpanel is None:
try: try:
web_userpanel = await self.web.userpanel() web_userpanel = await self.web.userpanel()

View File

@@ -13,14 +13,12 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData
from pyasic.data.error_codes.innosilicon import InnosiliconError from pyasic.data.error_codes.innosilicon import InnosiliconError
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.backends import CGMiner from pyasic.miners.backends import CGMiner
from pyasic.miners.data import ( from pyasic.miners.data import (
@@ -113,17 +111,17 @@ class Innosilicon(CGMiner):
# get pool data # get pool data
try: try:
pools = await self.web.pools() pools = await self.web.pools()
if pools and "pools" in pools:
self.config = MinerConfig.from_inno(pools["pools"])
except APIError: except APIError:
return self.config pass
return self.config or MinerConfig()
self.config = MinerConfig.from_inno(pools["pools"])
return self.config
async def reboot(self) -> bool: async def reboot(self) -> bool:
try: try:
data = await self.web.reboot() data = await self.web.reboot()
except APIError: except APIError:
pass return False
else: else:
return data["success"] return data["success"]
@@ -131,14 +129,16 @@ class Innosilicon(CGMiner):
try: try:
data = await self.web.restart_cgminer() data = await self.web.restart_cgminer()
except APIError: except APIError:
pass return False
else: else:
return data["success"] return data["success"]
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
return await self.restart_cgminer() 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 self.config = config
await self.web.update_pools(config.as_inno(user_suffix=user_suffix)) await self.web.update_pools(config.as_inno(user_suffix=user_suffix))
@@ -147,8 +147,8 @@ class Innosilicon(CGMiner):
################################################## ##################################################
async def _get_mac( async def _get_mac(
self, web_get_all: dict = None, web_overview: dict = None self, web_get_all: dict | None = None, web_overview: dict | None = None
) -> Optional[str]: ) -> str | None:
if web_get_all: if web_get_all:
web_get_all = web_get_all["all"] web_get_all = web_get_all["all"]
@@ -171,10 +171,11 @@ class Innosilicon(CGMiner):
return mac.upper() return mac.upper()
except KeyError: except KeyError:
pass pass
return None
async def _get_hashrate( async def _get_hashrate(
self, rpc_summary: dict = None, web_get_all: dict = None self, rpc_summary: dict | None = None, web_get_all: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if web_get_all: if web_get_all:
web_get_all = web_get_all["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(): if "Hash Rate H" in web_get_all["total_hash"].keys():
return self.algo.hashrate( return self.algo.hashrate(
rate=float(web_get_all["total_hash"]["Hash Rate H"]), rate=float(web_get_all["total_hash"]["Hash Rate H"]),
unit=self.algo.unit.H, unit=self.algo.unit.H, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
elif "Hash Rate" in web_get_all["total_hash"].keys(): elif "Hash Rate" in web_get_all["total_hash"].keys():
return self.algo.hashrate( return self.algo.hashrate(
rate=float(web_get_all["total_hash"]["Hash Rate"]), rate=float(web_get_all["total_hash"]["Hash Rate"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except KeyError: except KeyError:
pass pass
@@ -203,14 +208,17 @@ class Innosilicon(CGMiner):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except (KeyError, IndexError): except (KeyError, IndexError):
pass pass
return None
async def _get_hashboards( async def _get_hashboards(
self, rpc_stats: dict = None, web_get_all: dict = None self, rpc_stats: dict | None = None, web_get_all: dict | None = None
) -> List[HashBoard]: ) -> list[HashBoard]:
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
@@ -260,8 +268,11 @@ class Innosilicon(CGMiner):
hashrate = board.get("Hash Rate H") hashrate = board.get("Hash Rate H")
if hashrate: if hashrate:
hashboards[idx].hashrate = self.algo.hashrate( hashboards[idx].hashrate = self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.H rate=float(hashrate),
).into(self.algo.unit.default) unit=self.algo.unit.H, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
chip_temp = board.get("Temp max") chip_temp = board.get("Temp max")
if chip_temp: if chip_temp:
@@ -270,8 +281,8 @@ class Innosilicon(CGMiner):
return hashboards return hashboards
async def _get_wattage( async def _get_wattage(
self, web_get_all: dict = None, rpc_stats: dict = None self, web_get_all: dict | None = None, rpc_stats: dict | None = None
) -> Optional[int]: ) -> int | None:
if web_get_all: if web_get_all:
web_get_all = web_get_all["all"] web_get_all = web_get_all["all"]
@@ -305,8 +316,9 @@ class Innosilicon(CGMiner):
else: else:
wattage = int(wattage) wattage = int(wattage)
return 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: if self.expected_fans is None:
return [] return []
@@ -328,15 +340,15 @@ class Innosilicon(CGMiner):
except KeyError: except KeyError:
pass pass
else: else:
round((int(spd) * 6000) / 100) spd_converted = round((int(spd) * 6000) / 100)
for i in range(self.expected_fans): for i in range(self.expected_fans):
fans[i].speed = spd fans[i].speed = spd_converted
return fans return fans
async def _get_errors( async def _get_errors( # type: ignore[override]
self, web_get_error_detail: dict = None self, web_get_error_detail: dict | None = None
) -> List[MinerErrorData]: ) -> list[InnosiliconError]:
errors = [] errors = []
if web_get_error_detail is None: if web_get_error_detail is None:
try: try:
@@ -357,7 +369,7 @@ class Innosilicon(CGMiner):
errors.append(InnosiliconError(error_code=err)) errors.append(InnosiliconError(error_code=err))
return errors 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: if web_get_all:
web_get_all = web_get_all["all"] web_get_all = web_get_all["all"]
@@ -379,8 +391,9 @@ class Innosilicon(CGMiner):
level = int(level) level = int(level)
limit = 1250 + (250 * level) limit = 1250 + (250 * level)
return limit 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()

View File

@@ -14,13 +14,12 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import logging import logging
from typing import List, Optional
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePreset from pyasic.config.mining import MiningModePreset
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import LuxOSFirmware from pyasic.miners.device.firmware import LuxOSFirmware
@@ -131,6 +130,7 @@ class LUXMiner(LuxOSFirmware):
return True return True
except (APIError, LookupError): except (APIError, LookupError):
pass pass
return False
async def reboot(self) -> bool: async def reboot(self) -> bool:
try: try:
@@ -169,24 +169,40 @@ class LUXMiner(LuxOSFirmware):
return False return False
async def atm_enabled(self) -> Optional[bool]: async def atm_enabled(self) -> bool | None:
try: try:
result = await self.rpc.atm() result = await self.rpc.atm()
return result["ATM"][0]["Enabled"] return result["ATM"][0]["Enabled"]
except (APIError, LookupError): except (APIError, LookupError):
pass pass
return None
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> bool:
config = await self.get_config() 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 = { valid_presets = {
preset.name: preset.power preset.name: preset.power
for preset in config.mining_mode.available_presets for preset in available_presets
if preset.power <= wattage 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 # Set power to highest preset <= wattage
# If ATM enabled, must disable it before setting power limit # 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 re_enable_atm = False
try: try:
@@ -211,12 +227,13 @@ class LUXMiner(LuxOSFirmware):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### 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: if rpc_config is None:
try: try:
rpc_config = await self.rpc.config() rpc_config = await self.rpc.config()
except APIError: except APIError:
pass pass
return None
if rpc_config is not None: if rpc_config is not None:
try: try:
@@ -224,12 +241,15 @@ class LUXMiner(LuxOSFirmware):
except KeyError: except KeyError:
pass 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: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
except APIError: except APIError:
pass pass
return None
if rpc_summary is not None: if rpc_summary is not None:
try: try:
@@ -240,7 +260,7 @@ class LUXMiner(LuxOSFirmware):
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -262,8 +282,10 @@ class LUXMiner(LuxOSFirmware):
board_n = idx + 1 board_n = idx + 1
hashboards[idx].hashrate = self.algo.hashrate( hashboards[idx].hashrate = self.algo.hashrate(
rate=float(board_stats[f"chain_rate{board_n}"]), rate=float(board_stats[f"chain_rate{board_n}"]),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
hashboards[idx].chips = int(board_stats[f"chain_acn{board_n}"]) hashboards[idx].chips = int(board_stats[f"chain_acn{board_n}"])
chip_temp_data = list( chip_temp_data = list(
filter( filter(
@@ -288,22 +310,26 @@ class LUXMiner(LuxOSFirmware):
pass pass
return hashboards 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: if rpc_power is None:
try: try:
rpc_power = await self.rpc.power() rpc_power = await self.rpc.power()
except APIError: except APIError:
pass pass
return None
if rpc_power is not None: if rpc_power is not None:
try: try:
return rpc_power["POWER"][0]["Watts"] return rpc_power["POWER"][0]["Watts"]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass pass
return None
async def _get_wattage_limit( async def _get_wattage_limit(
self, rpc_config: dict = None, rpc_profiles: list[dict] = None self, rpc_config: dict | None = None, rpc_profiles: dict | None = None
) -> Optional[int]: ) -> int | None:
if rpc_config is None or rpc_profiles is None:
return None
try: try:
active_preset = MiningModePreset.get_active_preset_from_luxos( active_preset = MiningModePreset.get_active_preset_from_luxos(
rpc_config, rpc_profiles rpc_config, rpc_profiles
@@ -311,8 +337,9 @@ class LUXMiner(LuxOSFirmware):
return active_preset.power return active_preset.power
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if self.expected_fans is None:
return [] return []
@@ -333,8 +360,8 @@ class LUXMiner(LuxOSFirmware):
return fans return fans
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_stats: dict = None self, rpc_stats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_stats is None: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -350,11 +377,12 @@ class LUXMiner(LuxOSFirmware):
rate_unit = "GH" rate_unit = "GH"
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) 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: except LookupError:
pass 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: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -366,8 +394,9 @@ class LUXMiner(LuxOSFirmware):
return int(rpc_stats["STATS"][1]["Elapsed"]) return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -379,8 +408,9 @@ class LUXMiner(LuxOSFirmware):
return rpc_version["VERSION"][0]["Miner"] return rpc_version["VERSION"][0]["Miner"]
except LookupError: except LookupError:
pass 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: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -392,8 +422,9 @@ class LUXMiner(LuxOSFirmware):
return rpc_version["VERSION"][0]["API"] return rpc_version["VERSION"][0]["API"]
except LookupError: except LookupError:
pass 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: if rpc_config is None:
try: try:
rpc_config = await self.rpc.config() rpc_config = await self.rpc.config()
@@ -405,8 +436,9 @@ class LUXMiner(LuxOSFirmware):
return not rpc_config["CONFIG"][0]["RedLed"] == "off" return not rpc_config["CONFIG"][0]["RedLed"] == "off"
except LookupError: except LookupError:
pass 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()

View File

@@ -1,10 +1,8 @@
from typing import List, Optional
from pyasic import MinerConfig from pyasic import MinerConfig
from pyasic.config import MiningModeConfig from pyasic.config import MiningModeConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.miners.device.firmware import MaraFirmware from pyasic.miners.device.firmware import MaraFirmware
@@ -90,9 +88,12 @@ class MaraMiner(MaraFirmware):
data = await self.web.get_miner_config() data = await self.web.get_miner_config()
if data: if data:
self.config = MinerConfig.from_mara(data) self.config = MinerConfig.from_mara(data)
return self.config 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() data = await self.web.get_miner_config()
cfg_data = config.as_mara(user_suffix=user_suffix) cfg_data = config.as_mara(user_suffix=user_suffix)
merged_cfg = merge_dicts(data, cfg_data) merged_cfg = merge_dicts(data, cfg_data)
@@ -124,12 +125,13 @@ class MaraMiner(MaraFirmware):
await self.web.reload() await self.web.reload()
return True 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: if web_brief is None:
try: try:
web_brief = await self.web.brief() web_brief = await self.web.brief()
except APIError: except APIError:
pass pass
return None
if web_brief is not None: if web_brief is not None:
try: try:
@@ -137,12 +139,13 @@ class MaraMiner(MaraFirmware):
except LookupError: except LookupError:
pass 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: if web_brief is None:
try: try:
web_brief = await self.web.brief() web_brief = await self.web.brief()
except APIError: except APIError:
pass pass
return None
if web_brief is not None: if web_brief is not None:
try: try:
@@ -150,12 +153,13 @@ class MaraMiner(MaraFirmware):
except LookupError: except LookupError:
pass 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: if web_brief is None:
try: try:
web_brief = await self.web.brief() web_brief = await self.web.brief()
except APIError: except APIError:
pass pass
return None
if web_brief is not None: if web_brief is not None:
try: try:
@@ -163,7 +167,9 @@ class MaraMiner(MaraFirmware):
except LookupError: except LookupError:
pass 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: if self.expected_hashboards is None:
return [] return []
@@ -183,8 +189,11 @@ class MaraMiner(MaraFirmware):
for hb in web_hashboards["hashboards"]: for hb in web_hashboards["hashboards"]:
idx = hb["index"] idx = hb["index"]
hashboards[idx].hashrate = self.algo.hashrate( hashboards[idx].hashrate = self.algo.hashrate(
rate=float(hb["hashrate_average"]), unit=self.algo.unit.GH rate=float(hb["hashrate_average"]),
).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].temp = round( hashboards[idx].temp = round(
sum(hb["temperature_pcb"]) / len(hb["temperature_pcb"]) sum(hb["temperature_pcb"]) / len(hb["temperature_pcb"])
) )
@@ -198,7 +207,7 @@ class MaraMiner(MaraFirmware):
pass pass
return hashboards 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: if web_overview is None:
try: try:
web_overview = await self.web.overview() web_overview = await self.web.overview()
@@ -210,8 +219,9 @@ class MaraMiner(MaraFirmware):
return web_overview["mac"].upper() return web_overview["mac"].upper()
except LookupError: except LookupError:
pass 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: if web_overview is None:
try: try:
web_overview = await self.web.overview() web_overview = await self.web.overview()
@@ -223,8 +233,9 @@ class MaraMiner(MaraFirmware):
return web_overview["version_firmware"] return web_overview["version_firmware"]
except LookupError: except LookupError:
pass 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: if web_network_config is None:
try: try:
web_network_config = await self.web.get_network_config() web_network_config = await self.web.get_network_config()
@@ -236,8 +247,11 @@ class MaraMiner(MaraFirmware):
return web_network_config["hostname"] return web_network_config["hostname"]
except LookupError: except LookupError:
pass 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: if web_brief is None:
try: try:
web_brief = await self.web.brief() web_brief = await self.web.brief()
@@ -247,12 +261,14 @@ class MaraMiner(MaraFirmware):
if web_brief is not None: if web_brief is not None:
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(web_brief["hashrate_realtime"]), unit=self.algo.unit.TH rate=float(web_brief["hashrate_realtime"]),
).into(self.algo.unit.default) unit=self.algo.unit.TH, # type: ignore[attr-defined]
).into(self.algo.unit.default) # type: ignore[attr-defined]
except LookupError: except LookupError:
pass 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: if self.expected_fans is None:
return [] return []
@@ -272,7 +288,7 @@ class MaraMiner(MaraFirmware):
return fans return fans
return [Fan() for _ in range(self.expected_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: if web_locate_miner is None:
try: try:
web_locate_miner = await self.web.get_locate_miner() web_locate_miner = await self.web.get_locate_miner()
@@ -287,8 +303,8 @@ class MaraMiner(MaraFirmware):
return False return False
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, web_brief: dict = None self, web_brief: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if web_brief is None: if web_brief is None:
try: try:
web_brief = await self.web.brief() web_brief = await self.web.brief()
@@ -298,14 +314,16 @@ class MaraMiner(MaraFirmware):
if web_brief is not None: if web_brief is not None:
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(web_brief["hashrate_ideal"]), unit=self.algo.unit.GH rate=float(web_brief["hashrate_ideal"]),
).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: except LookupError:
pass pass
return None
async def _get_wattage_limit( async def _get_wattage_limit(
self, web_miner_config: dict = None self, web_miner_config: dict | None = None
) -> Optional[AlgoHashRate]: ) -> int | None:
if web_miner_config is None: if web_miner_config is None:
try: try:
web_miner_config = await self.web.get_miner_config() web_miner_config = await self.web.get_miner_config()
@@ -317,8 +335,9 @@ class MaraMiner(MaraFirmware):
return web_miner_config["mode"]["concorde"]["power-target"] return web_miner_config["mode"]["concorde"]["power-target"]
except LookupError: except LookupError:
pass 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: if web_pools is None:
try: try:
web_pools = await self.web.pools() web_pools = await self.web.pools()

View File

@@ -1,7 +1,5 @@
from typing import Optional
from pyasic import APIError 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.backends import BMMiner
from pyasic.miners.data import ( from pyasic.miners.data import (
DataFunction, DataFunction,
@@ -66,7 +64,9 @@ class MSKMiner(BMMiner):
web: MSKMinerWebAPI web: MSKMinerWebAPI
_web_cls = 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 # get hr from API
if rpc_stats is None: if rpc_stats is None:
try: try:
@@ -78,12 +78,15 @@ class MSKMiner(BMMiner):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_stats["STATS"][0]["total_rate"]), rate=float(rpc_stats["STATS"][0]["total_rate"]),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -95,8 +98,9 @@ class MSKMiner(BMMiner):
return rpc_stats["STATS"][0]["total_power"] return rpc_stats["STATS"][0]["total_power"]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if web_info_v1 is None:
try: try:
web_info_v1 = await self.web.info_v1() web_info_v1 = await self.web.info_v1()
@@ -108,3 +112,4 @@ class MSKMiner(BMMiner):
return web_info_v1["network_info"]["result"]["macaddr"].upper() return web_info_v1["network_info"]["result"]["macaddr"].upper()
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass pass
return None

View File

@@ -12,12 +12,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import List, Optional, Tuple
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData 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.miners.base import BaseMiner
from pyasic.rpc.unknown import UnknownRPCAPI from pyasic.rpc.unknown import UnknownRPCAPI
@@ -47,8 +47,8 @@ class UnknownMiner(BaseMiner):
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
return False return False
async def get_config(self) -> None: async def get_config(self) -> MinerConfig:
return None return MinerConfig()
async def reboot(self) -> bool: async def reboot(self) -> bool:
return False return False
@@ -62,7 +62,9 @@ class UnknownMiner(BaseMiner):
async def resume_mining(self) -> bool: async def resume_mining(self) -> bool:
return False 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 return None
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> bool:
@@ -72,53 +74,62 @@ class UnknownMiner(BaseMiner):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
################################################## ##################################################
async def _get_mac(self) -> Optional[str]: async def _get_mac(self) -> str | None:
return 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 return None, None
async def _get_hostname(self) -> Optional[str]: async def _get_hostname(self) -> str | None:
return None return None
async def _get_hashrate(self) -> Optional[AlgoHashRate]: async def _get_hashrate(self) -> AlgoHashRateType | None:
return None return None
async def _get_hashboards(self) -> List[HashBoard]: async def _get_hashboards(self) -> list[HashBoard]:
return [] return []
async def _get_env_temp(self) -> Optional[float]: async def _get_env_temp(self) -> float | None:
return None return None
async def _get_wattage(self) -> Optional[int]: async def _get_wattage(self) -> int | None:
return None return None
async def _get_wattage_limit(self) -> Optional[int]: async def _get_wattage_limit(self) -> int | None:
return None return None
async def _get_fans(self) -> List[Fan]: async def _get_fans(self) -> list[Fan]:
return [] return []
async def _get_fan_psu(self) -> Optional[int]: async def _get_fan_psu(self) -> int | None:
return None return None
async def _get_api_ver(self) -> Optional[str]: async def _get_api_ver(self) -> str | None:
return None return None
async def _get_fw_ver(self) -> Optional[str]: async def _get_fw_ver(self) -> str | None:
return None return None
async def _get_errors(self) -> List[MinerErrorData]: async def _get_errors(self) -> list[MinerErrorData]:
return [] return []
async def _get_fault_light(self) -> bool: async def _get_fault_light(self) -> bool:
return False return False
async def _get_expected_hashrate(self) -> Optional[AlgoHashRate]: async def _get_expected_hashrate(self) -> AlgoHashRateType | None:
return None return None
async def _is_mining(self, *args, **kwargs) -> Optional[bool]: async def _is_mining(self, *args, **kwargs) -> bool | None:
return 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 return None

View File

@@ -15,11 +15,11 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import logging import logging
from typing import List, Optional
from pyasic import MinerConfig from pyasic import MinerConfig
from pyasic.data.error_codes import MinerErrorData, VnishError from pyasic.config.mining import MiningModePreset
from pyasic.device.algorithm import AlgoHashRate from pyasic.data.error_codes import VnishError
from pyasic.device.algorithm import AlgoHashRateType
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.miners.backends.bmminer import BMMiner from pyasic.miners.backends.bmminer import BMMiner
from pyasic.miners.data import ( from pyasic.miners.data import (
@@ -106,7 +106,9 @@ class VNish(VNishFirmware, BMMiner):
data_locations = VNISH_DATA_LOC 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( await self.web.post_settings(
miner_settings=config.as_vnish(user_suffix=user_suffix) miner_settings=config.as_vnish(user_suffix=user_suffix)
) )
@@ -147,7 +149,7 @@ class VNish(VNishFirmware, BMMiner):
pass pass
return False 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: if web_summary is not None:
try: try:
mac = web_summary["system"]["network_status"]["mac"] mac = web_summary["system"]["network_status"]["mac"]
@@ -164,6 +166,8 @@ class VNish(VNishFirmware, BMMiner):
except KeyError: except KeyError:
pass pass
return None
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
result = await self.web.find_miner() result = await self.web.find_miner()
if result is not None: if result is not None:
@@ -171,6 +175,7 @@ class VNish(VNishFirmware, BMMiner):
return True return True
else: else:
await self.web.find_miner() await self.web.find_miner()
return False
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
result = await self.web.find_miner() result = await self.web.find_miner()
@@ -179,26 +184,27 @@ class VNish(VNishFirmware, BMMiner):
return True return True
else: else:
await self.web.find_miner() 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: if web_summary is None:
web_info = await self.web.info() web_info = await self.web.info()
if web_info is not None: if web_info is not None:
try: try:
hostname = web_info["system"]["network_status"]["hostname"] hostname = web_info["system"]["network_status"]["hostname"]
return hostname return hostname
except KeyError: except KeyError:
pass pass
else:
if web_summary is not None:
try: try:
hostname = web_summary["system"]["network_status"]["hostname"] hostname = web_summary["system"]["network_status"]["hostname"]
return hostname return hostname
except KeyError: except KeyError:
pass 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: if web_summary is None:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -209,25 +215,30 @@ class VNish(VNishFirmware, BMMiner):
return wattage return wattage
except KeyError: except KeyError:
pass 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 # get hr from API
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
except APIError: except APIError:
pass return None
if rpc_summary is not None: if rpc_summary is not None:
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]),
unit=self.algo.unit.GH, unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass 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: if web_settings is None:
web_settings = await self.web.summary() web_settings = await self.web.summary()
@@ -240,7 +251,9 @@ class VNish(VNishFirmware, BMMiner):
except (KeyError, TypeError): except (KeyError, TypeError):
pass 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: if web_summary is None:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -253,16 +266,16 @@ class VNish(VNishFirmware, BMMiner):
except LookupError: except LookupError:
return fw_ver 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: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
except APIError: except APIError:
pass return None
if web_summary is not None: if web_summary is not None:
try: try:
is_mining = not web_summary["miner"]["miner_status"]["miner_state"] in [ is_mining = web_summary["miner"]["miner_status"]["miner_state"] not in [
"stopped", "stopped",
"shutting-down", "shutting-down",
"failure", "failure",
@@ -271,8 +284,13 @@ class VNish(VNishFirmware, BMMiner):
except LookupError: except LookupError:
pass pass
async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]: return None
errors = []
async def _get_errors( # type: ignore[override]
self, web_summary: dict | None = None
) -> list[VnishError]:
errors: list[VnishError] = []
if web_summary is None: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -292,10 +310,13 @@ class VNish(VNishFirmware, BMMiner):
async def get_config(self) -> MinerConfig: async def get_config(self) -> MinerConfig:
try: try:
web_settings = await self.web.settings() 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 {} web_perf_summary = (await self.web.perf_summary()) or {}
except APIError: except APIError:
return self.config return self.config or MinerConfig()
self.config = MinerConfig.from_vnish( self.config = MinerConfig.from_vnish(
web_settings, web_presets, web_perf_summary web_settings, web_presets, web_perf_summary
) )
@@ -303,11 +324,20 @@ class VNish(VNishFirmware, BMMiner):
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> bool:
config = await self.get_config() 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 = [ valid_presets = [
preset.power preset.power
for preset in config.mining_mode.available_presets 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) new_wattage = max(valid_presets)
# Set power to highest preset <= wattage # Set power to highest preset <= wattage

View File

@@ -16,54 +16,53 @@
import asyncio import asyncio
import ipaddress import ipaddress
import warnings 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.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data.device import DeviceInfo from pyasic.data.device import DeviceInfo
from pyasic.data.error_codes import MinerErrorData from pyasic.data.error_codes import MinerErrorData
from pyasic.data.pools import PoolMetrics 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.base import GenericAlgo
from pyasic.device.algorithm.hashrate import AlgoHashRate
from pyasic.device.firmware import MinerFirmware from pyasic.device.firmware import MinerFirmware
from pyasic.device.makes import MinerMake from pyasic.device.makes import MinerMake
from pyasic.device.models import MinerModelType from pyasic.device.models import MinerModelType
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.logger import logger 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): class MinerProtocol(Protocol):
_rpc_cls: Type = None _rpc_cls: type[Any] | None = None
_web_cls: Type = None _web_cls: type[Any] | None = None
_ssh_cls: Type = None _ssh_cls: type[Any] | None = None
ip: str = None ip: str | None = None
rpc: _rpc_cls = None rpc: Any | None = None
web: _web_cls = None web: Any | None = None
ssh: _ssh_cls = None ssh: Any | None = None
make: MinerMake = None make: MinerMake | None = None
raw_model: MinerModelType = None raw_model: MinerModelType | None = None
firmware: MinerFirmware = None firmware: MinerFirmware | None = None
algo: type[MinerAlgoType] = GenericAlgo algo: type[MinerAlgoType] = GenericAlgo
expected_hashboards: int = None expected_hashboards: int | None = None
expected_chips: int = None expected_chips: int | None = None
expected_fans: int = None expected_fans: int | None = None
data_locations: DataLocations = None data_locations: Any | None = None
supports_shutdown: bool = False supports_shutdown: bool = False
supports_power_modes: bool = False supports_power_modes: bool = False
supports_presets: bool = False supports_presets: bool = False
supports_autotuning: bool = False supports_autotuning: bool = False
api_ver: str = None api_ver: str | None = None
fw_ver: str = None fw_ver: str | None = None
light: bool = None light: bool | None = None
config: MinerConfig = None config: MinerConfig | None = None
def __repr__(self): def __repr__(self):
return f"{self.model}: {str(self.ip)}" return f"{self.model}: {str(self.ip)}"
@@ -80,9 +79,9 @@ class MinerProtocol(Protocol):
@property @property
def model(self) -> str: def model(self) -> str:
if self.raw_model is not None: if self.raw_model is not None:
model_data = [self.raw_model] model_data = [str(self.raw_model)]
elif self.make is not None: elif self.make is not None:
model_data = [self.make] model_data = [str(self.make)]
else: else:
model_data = ["Unknown"] model_data = ["Unknown"]
if self.firmware is not None: if self.firmware is not None:
@@ -148,7 +147,9 @@ class MinerProtocol(Protocol):
""" """
return False 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. """Set the mining configuration of the miner.
Parameters: Parameters:
@@ -187,9 +188,9 @@ class MinerProtocol(Protocol):
async def upgrade_firmware( async def upgrade_firmware(
self, self,
*, *,
file: str = None, file: str | None = None,
url: str = None, url: str | None = None,
version: str = None, version: str | None = None,
keep_settings: bool = True, keep_settings: bool = True,
) -> bool: ) -> bool:
"""Upgrade the firmware of the miner. """Upgrade the firmware of the miner.
@@ -209,7 +210,7 @@ class MinerProtocol(Protocol):
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
################################################## ##################################################
async def get_serial_number(self) -> Optional[str]: async def get_serial_number(self) -> str | None:
"""Get the serial number of the miner and return it as a string. """Get the serial number of the miner and return it as a string.
Returns: Returns:
@@ -217,7 +218,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_serial_number() return await self._get_serial_number()
async def get_mac(self) -> Optional[str]: async def get_mac(self) -> str | None:
"""Get the MAC address of the miner and return it as a string. """Get the MAC address of the miner and return it as a string.
Returns: Returns:
@@ -225,7 +226,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_mac() 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. """Get the model of the miner and return it as a string.
Returns: Returns:
@@ -233,7 +234,7 @@ class MinerProtocol(Protocol):
""" """
return self.model 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. """Get device information, including model, make, and firmware.
Returns: Returns:
@@ -241,7 +242,7 @@ class MinerProtocol(Protocol):
""" """
return self.device_info 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. """Get the API version of the miner and is as a string.
Returns: Returns:
@@ -249,7 +250,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_api_ver() 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. """Get the firmware version of the miner and is as a string.
Returns: Returns:
@@ -257,7 +258,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_fw_ver() 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. """Get the API version and firmware version of the miner and return them as strings.
Returns: Returns:
@@ -267,7 +268,7 @@ class MinerProtocol(Protocol):
fw_ver = await self.get_fw_ver() fw_ver = await self.get_fw_ver()
return api_ver, 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. """Get the hostname of the miner and return it as a string.
Returns: Returns:
@@ -275,7 +276,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_hostname() 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. """Get the hashrate of the miner and return it as a float in TH/s.
Returns: Returns:
@@ -283,7 +284,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_hashrate() 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]. """Get hashboard data from the miner in the form of [`HashBoard`][pyasic.data.HashBoard].
Returns: Returns:
@@ -291,7 +292,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_hashboards() 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. """Get environment temp from the miner as a float.
Returns: Returns:
@@ -299,7 +300,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_env_temp() 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. """Get wattage from the miner as an int.
Returns: Returns:
@@ -307,7 +308,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_wattage() 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. """Get output voltage of the PSU as a float.
Returns: Returns:
@@ -315,7 +316,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_voltage() 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. """Get wattage limit from the miner as an int.
Returns: Returns:
@@ -323,7 +324,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_wattage_limit() 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]. """Get fan data from the miner in the form [fan_1, fan_2, fan_3, fan_4].
Returns: Returns:
@@ -331,7 +332,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_fans() 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. """Get PSU fan speed from the miner.
Returns: Returns:
@@ -339,7 +340,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_fan_psu() 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. """Get a list of the errors the miner is experiencing.
Returns: Returns:
@@ -353,9 +354,10 @@ class MinerProtocol(Protocol):
Returns: Returns:
A boolean value where `True` represents on and `False` represents off. 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. """Get the nominal hashrate from factory if available.
Returns: Returns:
@@ -363,7 +365,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_expected_hashrate() 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. """Check whether the miner is mining.
Returns: Returns:
@@ -371,7 +373,7 @@ class MinerProtocol(Protocol):
""" """
return await self._is_mining() 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. """Get the uptime of the miner in seconds.
Returns: Returns:
@@ -379,7 +381,7 @@ class MinerProtocol(Protocol):
""" """
return await self._get_uptime() 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. """Get the pools information from Miner.
Returns: Returns:
@@ -387,68 +389,68 @@ class MinerProtocol(Protocol):
""" """
return await self._get_pools() return await self._get_pools()
async def _get_serial_number(self) -> Optional[str]: async def _get_serial_number(self) -> str | None:
pass pass
async def _get_mac(self) -> Optional[str]: async def _get_mac(self) -> str | None:
pass return None
async def _get_api_ver(self) -> Optional[str]: async def _get_api_ver(self) -> str | None:
pass return None
async def _get_fw_ver(self) -> Optional[str]: async def _get_fw_ver(self) -> str | None:
pass return None
async def _get_hostname(self) -> Optional[str]: async def _get_hostname(self) -> str | None:
pass return None
async def _get_hashrate(self) -> Optional[AlgoHashRate]: async def _get_hashrate(self) -> AlgoHashRateType | None:
pass return None
async def _get_hashboards(self) -> List[HashBoard]: async def _get_hashboards(self) -> list[HashBoard]:
return [] return []
async def _get_env_temp(self) -> Optional[float]: async def _get_env_temp(self) -> float | None:
pass return None
async def _get_wattage(self) -> Optional[int]: async def _get_wattage(self) -> int | None:
pass return None
async def _get_voltage(self) -> Optional[float]: async def _get_voltage(self) -> float | None:
pass return None
async def _get_wattage_limit(self) -> Optional[int]: async def _get_wattage_limit(self) -> int | None:
pass return None
async def _get_fans(self) -> List[Fan]: async def _get_fans(self) -> list[Fan]:
return [] return []
async def _get_fan_psu(self) -> Optional[int]: async def _get_fan_psu(self) -> int | None:
pass return None
async def _get_errors(self) -> List[MinerErrorData]: async def _get_errors(self) -> list[MinerErrorData]:
return [] return []
async def _get_fault_light(self) -> Optional[bool]: async def _get_fault_light(self) -> bool | None:
pass return None
async def _get_expected_hashrate(self) -> Optional[AlgoHashRate]: async def _get_expected_hashrate(self) -> AlgoHashRateType | None:
pass return None
async def _is_mining(self) -> Optional[bool]: async def _is_mining(self) -> bool | None:
pass return None
async def _get_uptime(self) -> Optional[int]: async def _get_uptime(self) -> int | None:
pass return None
async def _get_pools(self) -> List[PoolMetrics]: async def _get_pools(self) -> list[PoolMetrics]:
pass return []
async def _get_data( async def _get_data(
self, self,
allow_warning: bool, allow_warning: bool,
include: List[Union[str, DataOptions]] = None, include: list[str | DataOptions] | None = None,
exclude: List[Union[str, DataOptions]] = None, exclude: list[str | DataOptions] | None = None,
) -> dict: ) -> dict:
# handle include # handle include
if include is not None: if include is not None:
@@ -470,7 +472,7 @@ class MinerProtocol(Protocol):
for data_name in include: for data_name in include:
try: try:
# get kwargs needed for the _get_xyz function # 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 # keep track of which RPC/Web commands need to be sent
for arg in fn_args: for arg in fn_args:
@@ -483,13 +485,21 @@ class MinerProtocol(Protocol):
continue continue
# create tasks for all commands that need to be sent, or no-op with sleep(0) -> None # 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( rpc_command_task = asyncio.create_task(
self.rpc.multicommand(*rpc_multicommand, allow_warning=allow_warning) self.rpc.multicommand(*rpc_multicommand, allow_warning=allow_warning)
) )
else: else:
rpc_command_task = asyncio.create_task(asyncio.sleep(0)) 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( web_command_task = asyncio.create_task(
self.web.multicommand(*web_multicommand, allow_warning=allow_warning) self.web.multicommand(*web_multicommand, allow_warning=allow_warning)
) )
@@ -511,7 +521,7 @@ class MinerProtocol(Protocol):
for data_name in include: for data_name in include:
try: 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} args_to_send = {k.name: None for k in fn_args}
for arg in fn_args: for arg in fn_args:
try: try:
@@ -532,7 +542,9 @@ class MinerProtocol(Protocol):
except LookupError: except LookupError:
continue continue
try: 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) miner_data[data_name] = await function(**args_to_send)
except Exception as e: except Exception as e:
raise APIError( raise APIError(
@@ -543,8 +555,8 @@ class MinerProtocol(Protocol):
async def get_data( async def get_data(
self, self,
allow_warning: bool = False, allow_warning: bool = False,
include: List[Union[str, DataOptions]] = None, include: list[str | DataOptions] | None = None,
exclude: List[Union[str, DataOptions]] = None, exclude: list[str | DataOptions] | None = None,
) -> MinerData: ) -> MinerData:
"""Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData]. """Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData].
@@ -562,6 +574,7 @@ class MinerProtocol(Protocol):
expected_chips=( expected_chips=(
self.expected_chips * self.expected_hashboards self.expected_chips * self.expected_hashboards
if self.expected_chips is not None if self.expected_chips is not None
and self.expected_hashboards is not None
else 0 else 0
), ),
expected_hashboards=self.expected_hashboards, expected_hashboards=self.expected_hashboards,

View File

@@ -16,7 +16,6 @@
from dataclasses import dataclass, field, make_dataclass from dataclasses import dataclass, field, make_dataclass
from enum import Enum from enum import Enum
from typing import List, Union
class DataOptions(Enum): class DataOptions(Enum):
@@ -67,7 +66,7 @@ class WebAPICommand:
@dataclass @dataclass
class DataFunction: class DataFunction:
cmd: str cmd: str
kwargs: List[Union[RPCAPICommand, WebAPICommand]] = field(default_factory=list) kwargs: list[RPCAPICommand | WebAPICommand] = field(default_factory=list)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self return self

View File

@@ -14,7 +14,7 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.device.algorithm import MinerAlgo 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 from pyasic.miners.device.makes import AntMinerMake
@@ -118,7 +118,7 @@ class S19jNoPIC(AntMinerMake):
class S19jPro(AntMinerMake): class S19jPro(AntMinerMake):
raw_model = MinerModel.ANTMINER.S19jPro raw_model: MinerModelType = MinerModel.ANTMINER.S19jPro
expected_chips = 126 expected_chips = 126
expected_fans = 4 expected_fans = 4
@@ -163,7 +163,7 @@ class S19jProPlusNoPIC(AntMinerMake):
class S19kPro(AntMinerMake): class S19kPro(AntMinerMake):
raw_model = MinerModel.ANTMINER.S19kPro raw_model: MinerModelType = MinerModel.ANTMINER.S19kPro
expected_chips = 77 expected_chips = 77
expected_fans = 4 expected_fans = 4

View File

@@ -30,8 +30,8 @@ from .S19 import (
S19jProPlus, S19jProPlus,
S19jProPlusNoPIC, S19jProPlusNoPIC,
S19jXP, S19jXP,
S19kPro,
S19KPro, S19KPro,
S19kPro,
S19kProNoPIC, S19kProNoPIC,
S19NoPIC, S19NoPIC,
S19Plus, S19Plus,

View File

@@ -1,2 +1,18 @@
from .ALX import * from .ALX import *
from .KSX 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
]

View File

@@ -141,6 +141,7 @@ from .M31S_Plus import (
) )
from .M31SE import M31SEV10, M31SEV20, M31SEV30 from .M31SE import M31SEV10, M31SEV20, M31SEV30
from .M32 import M32V10, M32V20 from .M32 import M32V10, M32V20
from .M32S import M32S
from .M33 import M33V10, M33V20, M33V30 from .M33 import M33V10, M33V20, M33V30
from .M33S import M33SVG30 from .M33S import M33SVG30
from .M33S_Plus import M33SPlusVG20, M33SPlusVG30, M33SPlusVH20, M33SPlusVH30 from .M33S_Plus import M33SPlusVG20, M33SPlusVG30, M33SPlusVH20, M33SPlusVH30

View File

@@ -21,7 +21,8 @@ import ipaddress
import json import json
import re import re
import warnings import warnings
from typing import Any, AsyncGenerator, Callable from collections.abc import AsyncGenerator, Callable
from typing import Any, cast
import anyio import anyio
import httpx import httpx
@@ -70,7 +71,7 @@ class MinerTypes(enum.Enum):
MSKMINER = 18 MSKMINER = 18
MINER_CLASSES = { MINER_CLASSES: dict[MinerTypes, dict[str | None, Any]] = {
MinerTypes.ANTMINER: { MinerTypes.ANTMINER: {
None: type("AntminerUnknown", (BMMiner, AntMinerMake), {}), None: type("AntminerUnknown", (BMMiner, AntMinerMake), {}),
"ANTMINER D3": CGMinerD3, "ANTMINER D3": CGMinerD3,
@@ -731,8 +732,9 @@ class MinerFactory:
async def get_multiple_miners( async def get_multiple_miners(
self, ips: list[str], limit: int = 200 self, ips: list[str], limit: int = 200
) -> list[AnyMiner]: ) -> list[AnyMiner]:
results = [] results: list[AnyMiner] = []
miner: AnyMiner
async for miner in self.get_miner_generator(ips, limit): async for miner in self.get_miner_generator(ips, limit):
results.append(miner) results.append(miner)
@@ -740,7 +742,7 @@ class MinerFactory:
async def get_miner_generator( async def get_miner_generator(
self, ips: list, limit: int = 200 self, ips: list, limit: int = 200
) -> AsyncGenerator[AnyMiner]: ) -> AsyncGenerator[AnyMiner, None]:
tasks = [] tasks = []
semaphore = asyncio.Semaphore(limit) semaphore = asyncio.Semaphore(limit)
@@ -749,11 +751,13 @@ class MinerFactory:
for task in tasks: for task in tasks:
async with semaphore: async with semaphore:
result = await task result = await task # type: ignore[func-returns-value]
if result is not None: if result is not None:
yield result 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) ip = str(ip)
miner_type = None miner_type = None
@@ -771,7 +775,7 @@ class MinerFactory:
break break
if miner_type is not None: if miner_type is not None:
miner_model = None miner_model: str | None = None
miner_model_fns = { miner_model_fns = {
MinerTypes.ANTMINER: self.get_miner_model_antminer, MinerTypes.ANTMINER: self.get_miner_model_antminer,
MinerTypes.WHATSMINER: self.get_miner_model_whatsminer, MinerTypes.WHATSMINER: self.get_miner_model_whatsminer,
@@ -792,7 +796,7 @@ class MinerFactory:
MinerTypes.VOLCMINER: self.get_miner_model_volcminer, MinerTypes.VOLCMINER: self.get_miner_model_volcminer,
MinerTypes.ELPHAPEX: self.get_miner_model_elphapex, MinerTypes.ELPHAPEX: self.get_miner_model_elphapex,
} }
version = None version: str | None = None
miner_version_fns = { miner_version_fns = {
MinerTypes.WHATSMINER: self.get_miner_version_whatsminer, MinerTypes.WHATSMINER: self.get_miner_version_whatsminer,
} }
@@ -801,18 +805,18 @@ class MinerFactory:
if model_fn is not None: if model_fn is not None:
# noinspection PyArgumentList # noinspection PyArgumentList
task = asyncio.create_task(model_fn(ip)) model_task = asyncio.create_task(model_fn(ip))
try: try:
miner_model = await asyncio.wait_for( 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: except asyncio.TimeoutError:
pass pass
if version_fn is not None: if version_fn is not None:
task = asyncio.create_task(version_fn(ip)) version_task = asyncio.create_task(version_fn(ip))
try: try:
version = await asyncio.wait_for( 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: except asyncio.TimeoutError:
pass pass
@@ -820,6 +824,7 @@ class MinerFactory:
ip, miner_type=miner_type, miner_model=miner_model, version=version ip, miner_type=miner_type, miner_model=miner_model, version=version
) )
return miner return miner
return None
async def _get_miner_type(self, ip: str) -> MinerTypes | None: async def _get_miner_type(self, ip: str) -> MinerTypes | None:
tasks = [ tasks = [
@@ -850,14 +855,15 @@ class MinerFactory:
if res is not None: if res is not None:
mtype = MinerTypes.MARATHON mtype = MinerTypes.MARATHON
if mtype == MinerTypes.HAMMER: if mtype == MinerTypes.HAMMER:
res = await self.get_miner_model_hammer(ip) hammer_model = await self.get_miner_model_hammer(ip)
if res is None: if hammer_model is None:
return MinerTypes.HAMMER return MinerTypes.HAMMER
if "HAMMER" in res.upper(): if "HAMMER" in hammer_model.upper():
mtype = MinerTypes.HAMMER mtype = MinerTypes.HAMMER
else: else:
mtype = MinerTypes.VOLCMINER mtype = MinerTypes.VOLCMINER
return mtype return mtype
return None
@staticmethod @staticmethod
async def _web_ping( async def _web_ping(
@@ -919,6 +925,7 @@ class MinerFactory:
return MinerTypes.INNOSILICON return MinerTypes.INNOSILICON
if "Miner UI" in web_text: if "Miner UI" in web_text:
return MinerTypes.AURADINE return MinerTypes.AURADINE
return None
async def _get_miner_socket(self, ip: str) -> MinerTypes | None: async def _get_miner_socket(self, ip: str) -> MinerTypes | None:
commands = ["version", "devdetails"] commands = ["version", "devdetails"]
@@ -931,6 +938,7 @@ class MinerFactory:
if data is not None: if data is not None:
d = self._parse_socket_type(data) d = self._parse_socket_type(data)
return d return d
return None
@staticmethod @staticmethod
async def _socket_ping(ip: str, cmd: str) -> str | None: async def _socket_ping(ip: str, cmd: str) -> str | None:
@@ -941,13 +949,13 @@ class MinerFactory:
timeout=settings.get("factory_get_timeout", 3), timeout=settings.get("factory_get_timeout", 3),
) )
except (ConnectionError, OSError, asyncio.TimeoutError): except (ConnectionError, OSError, asyncio.TimeoutError):
return return None
cmd = {"command": cmd} command_dict = {"command": cmd}
try: try:
# send the command # send the command
writer.write(json.dumps(cmd).encode("utf-8")) writer.write(json.dumps(command_dict).encode("utf-8"))
await writer.drain() await writer.drain()
# loop to receive all the data # loop to receive all the data
@@ -964,11 +972,11 @@ class MinerFactory:
logger.warning(f"{ip}: Socket ping timeout.") logger.warning(f"{ip}: Socket ping timeout.")
break break
except ConnectionResetError: except ConnectionResetError:
return return None
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
except (ConnectionError, OSError): except (ConnectionError, OSError):
return return None
finally: finally:
# Handle cancellation explicitly # Handle cancellation explicitly
if writer.transport.is_closing(): if writer.transport.is_closing():
@@ -978,9 +986,10 @@ class MinerFactory:
try: try:
await writer.wait_closed() await writer.wait_closed()
except (ConnectionError, OSError): except (ConnectionError, OSError):
return return None
if data: if data:
return data.decode("utf-8") return data.decode("utf-8")
return None
@staticmethod @staticmethod
def _parse_socket_type(data: str) -> MinerTypes | None: def _parse_socket_type(data: str) -> MinerTypes | None:
@@ -1013,12 +1022,13 @@ class MinerFactory:
return MinerTypes.AURADINE return MinerTypes.AURADINE
if "VNISH" in upper_data: if "VNISH" in upper_data:
return MinerTypes.VNISH return MinerTypes.VNISH
return None
async def send_web_command( async def send_web_command(
self, self,
ip: str, ip: str,
location: str, location: str,
auth: httpx.DigestAuth = None, auth: httpx.DigestAuth | None = None,
) -> dict | None: ) -> dict | None:
async with httpx.AsyncClient(transport=settings.transport()) as session: async with httpx.AsyncClient(transport=settings.transport()) as session:
try: try:
@@ -1029,16 +1039,16 @@ class MinerFactory:
) )
except (httpx.HTTPError, asyncio.TimeoutError): except (httpx.HTTPError, asyncio.TimeoutError):
logger.info(f"{ip}: Web command timeout.") logger.info(f"{ip}: Web command timeout.")
return return None
if data is None: if data is None:
return return None
try: try:
json_data = data.json() json_data = data.json()
except (json.JSONDecodeError, asyncio.TimeoutError): except (json.JSONDecodeError, asyncio.TimeoutError):
try: try:
return json.loads(data.text) return json.loads(data.text)
except (json.JSONDecodeError, httpx.HTTPError): except (json.JSONDecodeError, httpx.HTTPError):
return return None
else: else:
return json_data return json_data
@@ -1047,7 +1057,7 @@ class MinerFactory:
try: try:
reader, writer = await asyncio.open_connection(ip, 4028) reader, writer = await asyncio.open_connection(ip, 4028)
except (ConnectionError, OSError): except (ConnectionError, OSError):
return return None
cmd = {"command": command} cmd = {"command": command}
try: try:
@@ -1067,26 +1077,26 @@ class MinerFactory:
except asyncio.CancelledError: except asyncio.CancelledError:
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
return return None
except (ConnectionError, OSError): except (ConnectionError, OSError):
return return None
if data == b"Socket connect failed: Connection refused\n": 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: try:
data = json.loads(data) data_dict = json.loads(data_str)
except json.JSONDecodeError: except json.JSONDecodeError:
return {} return {}
return data return data_dict
async def send_btminer_v3_api_command(self, ip, command): async def send_btminer_v3_api_command(self, ip, command):
try: try:
reader, writer = await asyncio.open_connection(ip, 4433) reader, writer = await asyncio.open_connection(ip, 4433)
except (ConnectionError, OSError): except (ConnectionError, OSError):
return return None
cmd = {"cmd": command} cmd = {"cmd": command}
try: try:
@@ -1108,11 +1118,11 @@ class MinerFactory:
except asyncio.CancelledError: except asyncio.CancelledError:
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
return return None
except (ConnectionError, OSError): except (ConnectionError, OSError):
return return None
if data == b"Socket connect failed: Connection refused\n": if data == b"Socket connect failed: Connection refused\n":
return return None
try: try:
data = json.loads(data) data = json.loads(data)
@@ -1156,7 +1166,7 @@ class MinerFactory:
@staticmethod @staticmethod
def _select_miner_from_classes( def _select_miner_from_classes(
ip: ipaddress.ip_address, ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address,
miner_model: str | None, miner_model: str | None,
miner_type: MinerTypes | None, miner_type: MinerTypes | None,
version: str | None = None, version: str | None = None,
@@ -1165,6 +1175,10 @@ class MinerFactory:
if "HIVEON" in str(miner_model).upper(): if "HIVEON" in str(miner_model).upper():
miner_model = str(miner_model).upper().replace(" HIVEON", "") miner_model = str(miner_model).upper().replace(" HIVEON", "")
miner_type = MinerTypes.HIVEON miner_type = MinerTypes.HIVEON
if miner_type is None:
return cast(AnyMiner, UnknownMiner(str(ip), version))
try: try:
return MINER_CLASSES[miner_type][str(miner_model).upper()](ip, version) return MINER_CLASSES[miner_type][str(miner_model).upper()](ip, version)
except LookupError: except LookupError:
@@ -1175,7 +1189,7 @@ class MinerFactory:
f"and this model on GitHub (https://github.com/UpstreamData/pyasic/issues)." f"and this model on GitHub (https://github.com/UpstreamData/pyasic/issues)."
) )
return MINER_CLASSES[miner_type][None](ip, version) 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: async def get_miner_model_antminer(self, ip: str) -> str | None:
tasks = [ tasks = [
@@ -1194,25 +1208,28 @@ class MinerFactory:
ip, "/cgi-bin/get_system_info.cgi", auth=auth ip, "/cgi-bin/get_system_info.cgi", auth=auth
) )
try: if web_json_data is not None:
miner_model = web_json_data["minertype"] try:
miner_model = web_json_data["minertype"]
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def _get_model_antminer_sock(self, ip: str) -> str | None: async def _get_model_antminer_sock(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "version") sock_json_data = await self.send_api_command(ip, "version")
try: if sock_json_data is not None:
miner_model = sock_json_data["VERSION"][0]["Type"] try:
miner_model = sock_json_data["VERSION"][0]["Type"]
if " (" in miner_model: if " (" in miner_model:
split_miner_model = miner_model.split(" (") split_miner_model = miner_model.split(" (")
miner_model = split_miner_model[0] miner_model = split_miner_model[0]
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
sock_json_data = await self.send_api_command(ip, "stats") sock_json_data = await self.send_api_command(ip, "stats")
try: try:
@@ -1225,24 +1242,29 @@ class MinerFactory:
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_goldshell(self, ip: str) -> str | None: async def get_miner_model_goldshell(self, ip: str) -> str | None:
json_data = await self.send_web_command(ip, "/mcb/status") json_data = await self.send_web_command(ip, "/mcb/status")
try: if json_data is not None:
miner_model = json_data["model"].replace("-", " ") try:
miner_model = json_data["model"].replace("-", " ")
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_whatsminer(self, ip: str) -> str | None: async def get_miner_model_whatsminer(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "devdetails") sock_json_data = await self.send_api_command(ip, "devdetails")
try: if sock_json_data is not None:
miner_model = sock_json_data["DEVDETAILS"][0]["Model"].replace("_", "") try:
miner_model = miner_model[:-1] + "0" miner_model = sock_json_data["DEVDETAILS"][0]["Model"].replace("_", "")
return miner_model miner_model = miner_model[:-1] + "0"
except (TypeError, LookupError): return miner_model
except (TypeError, LookupError):
pass
else:
sock_json_data_v3 = await self.send_btminer_v3_api_command( sock_json_data_v3 = await self.send_btminer_v3_api_command(
ip, "get.device.info" ip, "get.device.info"
) )
@@ -1253,27 +1275,32 @@ class MinerFactory:
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_version_whatsminer(self, ip: str) -> str | None: async def get_miner_version_whatsminer(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "get_version") sock_json_data = await self.send_api_command(ip, "get_version")
try: if sock_json_data is not None:
version = sock_json_data["Msg"]["fw_ver"] try:
return version version = sock_json_data["Msg"]["fw_ver"]
except LookupError: return version
pass except LookupError:
pass
return None
async def get_miner_model_avalonminer(self, ip: str) -> str | None: async def get_miner_model_avalonminer(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "version") sock_json_data = await self.send_api_command(ip, "version")
try: if sock_json_data is not None:
miner_model = sock_json_data["VERSION"][0]["PROD"].upper() try:
if "-" in miner_model: miner_model = sock_json_data["VERSION"][0]["PROD"].upper()
miner_model = miner_model.split("-")[0] if "-" in miner_model:
if miner_model in ["AVALONNANO", "AVALON0O", "AVALONMINER 15"]: miner_model = miner_model.split("-")[0]
subtype = sock_json_data["VERSION"][0]["MODEL"].upper() if miner_model in ["AVALONNANO", "AVALON0O", "AVALONMINER 15"]:
miner_model = f"AVALONMINER {subtype}" subtype = sock_json_data["VERSION"][0]["MODEL"].upper()
return miner_model miner_model = f"AVALONMINER {subtype}"
except (TypeError, LookupError): return miner_model
pass except (TypeError, LookupError):
pass
return None
async def get_miner_model_innosilicon(self, ip: str) -> str | None: async def get_miner_model_innosilicon(self, ip: str) -> str | None:
try: try:
@@ -1289,7 +1316,7 @@ class MinerFactory:
) )
auth = auth_req.json()["jwt"] auth = auth_req.json()["jwt"]
except (httpx.HTTPError, LookupError): except (httpx.HTTPError, LookupError):
return return None
try: try:
async with httpx.AsyncClient(transport=settings.transport()) as session: async with httpx.AsyncClient(transport=settings.transport()) as session:
@@ -1303,6 +1330,7 @@ class MinerFactory:
return web_data["type"] return web_data["type"]
except (httpx.HTTPError, LookupError): except (httpx.HTTPError, LookupError):
pass pass
return None
try: try:
async with httpx.AsyncClient(transport=settings.transport()) as session: async with httpx.AsyncClient(transport=settings.transport()) as session:
web_data = ( web_data = (
@@ -1315,18 +1343,21 @@ class MinerFactory:
return web_data["type"] return web_data["type"]
except (httpx.HTTPError, LookupError): except (httpx.HTTPError, LookupError):
pass pass
return None
async def get_miner_model_braiins_os(self, ip: str) -> str | None: async def get_miner_model_braiins_os(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "devdetails") sock_json_data = await self.send_api_command(ip, "devdetails")
try: if sock_json_data is not None:
miner_model = ( try:
sock_json_data["DEVDETAILS"][0]["Model"] miner_model = (
.replace("Bitmain ", "") sock_json_data["DEVDETAILS"][0]["Model"]
.replace("S19XP", "S19 XP") .replace("Bitmain ", "")
) .replace("S19XP", "S19 XP")
return miner_model )
except (TypeError, LookupError): return miner_model
pass except (TypeError, LookupError):
pass
return None
try: try:
async with httpx.AsyncClient(transport=settings.transport()) as session: async with httpx.AsyncClient(transport=settings.transport()) as session:
@@ -1342,64 +1373,75 @@ class MinerFactory:
return miner_model return miner_model
except (httpx.HTTPError, LookupError): except (httpx.HTTPError, LookupError):
pass pass
return None
async def get_miner_model_vnish(self, ip: str) -> str | None: async def get_miner_model_vnish(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "stats") sock_json_data = await self.send_api_command(ip, "stats")
try: if sock_json_data is not None:
miner_model = sock_json_data["STATS"][0]["Type"] try:
if " (" in miner_model: miner_model = sock_json_data["STATS"][0]["Type"]
split_miner_model = miner_model.split(" (") if " (" in miner_model:
miner_model = split_miner_model[0] split_miner_model = miner_model.split(" (")
miner_model = split_miner_model[0]
if "(88)" in miner_model: if "(88)" in miner_model:
miner_model = miner_model.replace("(88)", "NOPIC") miner_model = miner_model.replace("(88)", "NOPIC")
if " AML" in miner_model: if " AML" in miner_model:
miner_model = miner_model.replace(" AML", "") miner_model = miner_model.replace(" AML", "")
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_epic(self, ip: str) -> str | None: async def get_miner_model_epic(self, ip: str) -> str | None:
for retry_cnt in range(settings.get("get_data_retries", 1)): for retry_cnt in range(settings.get("get_data_retries", 1)):
sock_json_data = await self.send_web_command(ip, ":4028/capabilities") sock_json_data = await self.send_web_command(ip, ":4028/capabilities")
try: if sock_json_data is not None:
miner_model = sock_json_data["Model"] try:
return miner_model miner_model = sock_json_data["Model"]
except (TypeError, LookupError): return miner_model
except (TypeError, LookupError):
pass
else:
if retry_cnt < settings.get("get_data_retries", 1) - 1: if retry_cnt < settings.get("get_data_retries", 1) - 1:
continue continue
else: else:
pass pass
return None
async def get_miner_model_hiveon(self, ip: str) -> str | None: async def get_miner_model_hiveon(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "version") sock_json_data = await self.send_api_command(ip, "version")
try: if sock_json_data is not None:
miner_type = sock_json_data["VERSION"][0]["Type"] try:
miner_type = sock_json_data["VERSION"][0]["Type"]
return miner_type.replace(" HIVEON", "") return miner_type.replace(" HIVEON", "")
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_luxos(self, ip: str) -> str | None: async def get_miner_model_luxos(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "version") sock_json_data = await self.send_api_command(ip, "version")
try: if sock_json_data is not None:
miner_model = sock_json_data["VERSION"][0]["Type"] try:
miner_model = sock_json_data["VERSION"][0]["Type"]
if " (" in miner_model: if " (" in miner_model:
split_miner_model = miner_model.split(" (") split_miner_model = miner_model.split(" (")
miner_model = split_miner_model[0] miner_model = split_miner_model[0]
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_auradine(self, ip: str) -> str | None: async def get_miner_model_auradine(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "devdetails") sock_json_data = await self.send_api_command(ip, "devdetails")
try: if sock_json_data is not None:
return sock_json_data["DEVDETAILS"][0]["Model"] try:
except LookupError: return sock_json_data["DEVDETAILS"][0]["Model"]
pass except LookupError:
pass
return None
async def get_miner_model_marathon(self, ip: str) -> str | None: async def get_miner_model_marathon(self, ip: str) -> str | None:
auth = httpx.DigestAuth("root", "root") auth = httpx.DigestAuth("root", "root")
@@ -1407,38 +1449,41 @@ class MinerFactory:
ip, "/kaonsu/v1/overview", auth=auth ip, "/kaonsu/v1/overview", auth=auth
) )
try: if web_json_data is not None:
miner_model = web_json_data["model"] try:
if miner_model == "": miner_model = web_json_data["model"]
return None if miner_model == "":
return None
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_bitaxe(self, ip: str) -> str | None: async def get_miner_model_bitaxe(self, ip: str) -> str | None:
web_json_data = await self.send_web_command(ip, "/api/system/info") web_json_data = await self.send_web_command(ip, "/api/system/info")
try: if web_json_data is not None:
miner_model = web_json_data["ASICModel"] try:
if miner_model == "": miner_model = web_json_data["ASICModel"]
return None if miner_model == "":
return None
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_luckyminer(self, ip: str) -> str | None: async def get_miner_model_luckyminer(self, ip: str) -> str | None:
web_json_data = await self.send_web_command(ip, "/api/system/info") web_json_data = await self.send_web_command(ip, "/api/system/info")
try: if web_json_data is not None:
miner_model = web_json_data["minerModel"] try:
if miner_model == "": miner_model = web_json_data["minerModel"]
return None if miner_model == "":
return None
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_iceriver(self, ip: str) -> str | None: async def get_miner_model_iceriver(self, ip: str) -> str | None:
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
@@ -1461,7 +1506,7 @@ class MinerFactory:
f"http://{ip}:/user/userpanel", params={"post": "4"} f"http://{ip}:/user/userpanel", params={"post": "4"}
) )
if not resp.status_code == 200: if not resp.status_code == 200:
return return None
result = resp.json() result = resp.json()
software_ver = result["data"]["softver1"] software_ver = result["data"]["softver1"]
split_ver = software_ver.split("_") split_ver = software_ver.split("_")
@@ -1472,6 +1517,7 @@ class MinerFactory:
return miner_ver.upper() return miner_ver.upper()
except httpx.HTTPError: except httpx.HTTPError:
pass pass
return None
async def get_miner_model_hammer(self, ip: str) -> str | None: async def get_miner_model_hammer(self, ip: str) -> str | None:
auth = httpx.DigestAuth( auth = httpx.DigestAuth(
@@ -1481,12 +1527,13 @@ class MinerFactory:
ip, "/cgi-bin/get_system_info.cgi", auth=auth ip, "/cgi-bin/get_system_info.cgi", auth=auth
) )
try: if web_json_data is not None:
miner_model = web_json_data["minertype"] try:
miner_model = web_json_data["minertype"]
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_volcminer(self, ip: str) -> str | None: async def get_miner_model_volcminer(self, ip: str) -> str | None:
auth = httpx.DigestAuth( auth = httpx.DigestAuth(
@@ -1496,12 +1543,13 @@ class MinerFactory:
ip, "/cgi-bin/get_system_info.cgi", auth=auth ip, "/cgi-bin/get_system_info.cgi", auth=auth
) )
try: if web_json_data is not None:
miner_model = web_json_data["minertype"] try:
miner_model = web_json_data["minertype"]
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_elphapex(self, ip: str) -> str | None: async def get_miner_model_elphapex(self, ip: str) -> str | None:
auth = httpx.DigestAuth( auth = httpx.DigestAuth(
@@ -1511,24 +1559,29 @@ class MinerFactory:
ip, "/cgi-bin/get_system_info.cgi", auth=auth ip, "/cgi-bin/get_system_info.cgi", auth=auth
) )
try: if web_json_data is not None:
miner_model = web_json_data["minertype"] try:
miner_model = web_json_data["minertype"]
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
return None
async def get_miner_model_mskminer(self, ip: str) -> str | None: async def get_miner_model_mskminer(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "version") sock_json_data = await self.send_api_command(ip, "version")
try: if sock_json_data is not None:
return sock_json_data["VERSION"][0]["Type"].split(" ")[0] try:
except LookupError: return sock_json_data["VERSION"][0]["Type"].split(" ")[0]
pass except LookupError:
pass
return None
miner_factory = MinerFactory() miner_factory = MinerFactory()
# abstracted version of get miner that is easier to access # abstracted version of get miner that is easier to access
async def get_miner(ip: ipaddress.ip_address | str) -> AnyMiner: async def get_miner(
return await miner_factory.get_miner(ip) ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address,
) -> AnyMiner | None:
return await miner_factory.get_miner(ip) # type: ignore[func-returns-value]

View File

@@ -13,13 +13,12 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional, Union
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data import Fan, MinerData from pyasic.data import Fan, MinerData
from pyasic.data.boards import HashBoard from pyasic.data.boards import HashBoard
from pyasic.data.pools import PoolMetrics, PoolUrl 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.errors import APIError
from pyasic.miners.backends import GoldshellMiner from pyasic.miners.backends import GoldshellMiner
from pyasic.miners.data import ( from pyasic.miners.data import (
@@ -94,8 +93,8 @@ class GoldshellByte(GoldshellMiner, Byte):
async def get_data( async def get_data(
self, self,
allow_warning: bool = False, allow_warning: bool = False,
include: List[Union[str, DataOptions]] = None, include: list[str | DataOptions] | None = None,
exclude: List[Union[str, DataOptions]] = None, exclude: list[str | DataOptions] | None = None,
) -> MinerData: ) -> MinerData:
if self.web_devs is None: if self.web_devs is None:
try: try:
@@ -106,7 +105,7 @@ class GoldshellByte(GoldshellMiner, Byte):
scrypt_board_count = 0 scrypt_board_count = 0
zksnark_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") algo_name = minfo.get("name")
for _ in minfo.get("infos", []): for _ in minfo.get("infos", []):
@@ -123,9 +122,9 @@ class GoldshellByte(GoldshellMiner, Byte):
) )
if scrypt_board_count > 0 and zksnark_board_count == 0: 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: 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 = await super().get_data(allow_warning, include, exclude)
data.expected_chips = self.expected_chips data.expected_chips = self.expected_chips
@@ -136,12 +135,12 @@ class GoldshellByte(GoldshellMiner, Byte):
try: try:
pools = await self.web.pools() pools = await self.web.pools()
except APIError: except APIError:
return self.config return self.config or MinerConfig()
self.config = MinerConfig.from_goldshell_byte(pools) self.config = MinerConfig.from_goldshell_byte(pools.get("groups", []))
return self.config 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: if web_setting is None:
try: try:
web_setting = await self.web.setting() web_setting = await self.web.setting()
@@ -160,15 +159,15 @@ class GoldshellByte(GoldshellMiner, Byte):
return self.api_ver return self.api_ver
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_devs: dict = None self, rpc_devs: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_devs is None: if rpc_devs is None:
try: try:
rpc_devs = await self.rpc.devs() rpc_devs = await self.rpc.devs()
except APIError: except APIError:
pass pass
total_hash_rate_mh = 0 total_hash_rate_mh = 0.0
if rpc_devs is not None: if rpc_devs is not None:
for board in rpc_devs.get("DEVS", []): for board in rpc_devs.get("DEVS", []):
@@ -192,14 +191,16 @@ class GoldshellByte(GoldshellMiner, Byte):
return hash_rate 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: if rpc_devs is None:
try: try:
rpc_devs = await self.rpc.devs() rpc_devs = await self.rpc.devs()
except APIError: except APIError:
pass pass
total_hash_rate_mh = 0 total_hash_rate_mh = 0.0
if rpc_devs is not None: if rpc_devs is not None:
for board in rpc_devs.get("DEVS", []): for board in rpc_devs.get("DEVS", []):
@@ -211,7 +212,7 @@ class GoldshellByte(GoldshellMiner, Byte):
return hash_rate 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: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()
@@ -240,8 +241,8 @@ class GoldshellByte(GoldshellMiner, Byte):
return pools_data return pools_data
async def _get_hashboards( async def _get_hashboards(
self, rpc_devs: dict = None, rpc_devdetails: dict = None self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None
) -> List[HashBoard]: ) -> list[HashBoard]:
if rpc_devs is None: if rpc_devs is None:
try: try:
rpc_devs = await self.rpc.devs() rpc_devs = await self.rpc.devs()
@@ -285,7 +286,7 @@ class GoldshellByte(GoldshellMiner, Byte):
return hashboards 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: if self.expected_fans is None:
return [] return []
@@ -312,7 +313,7 @@ class GoldshellByte(GoldshellMiner, Byte):
return fans 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: if web_devs is None:
try: try:
web_devs = await self.web.devs() web_devs = await self.web.devs()
@@ -321,7 +322,7 @@ class GoldshellByte(GoldshellMiner, Byte):
if web_devs is not None: if web_devs is not None:
try: try:
for minfo in self.web_devs.get("minfos", []): for minfo in (self.web_devs or {}).get("minfos", []):
for info in minfo.get("infos", []): for info in minfo.get("infos", []):
uptime = int(float(info["time"])) uptime = int(float(info["time"]))
return uptime return uptime
@@ -330,7 +331,7 @@ class GoldshellByte(GoldshellMiner, Byte):
return None 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: if web_devs is None:
try: try:
web_devs = await self.web.devs() web_devs = await self.web.devs()
@@ -339,7 +340,7 @@ class GoldshellByte(GoldshellMiner, Byte):
if web_devs is not None: if web_devs is not None:
try: try:
for minfo in self.web_devs.get("minfos", []): for minfo in (self.web_devs or {}).get("minfos", []):
for info in minfo.get("infos", []): for info in minfo.get("infos", []):
wattage = int(float(info["power"])) wattage = int(float(info["power"]))
return wattage return wattage

View File

@@ -13,11 +13,10 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.data.boards import HashBoard 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.errors import APIError
from pyasic.logger import logger from pyasic.logger import logger
from pyasic.miners.backends import GoldshellMiner from pyasic.miners.backends import GoldshellMiner
@@ -80,18 +79,19 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge):
supports_shutdown = False supports_shutdown = False
supports_power_modes = False supports_power_modes = False
async def get_config(self) -> MinerConfig: async def get_config(self) -> MinerConfig | None: # type: ignore[override]
try: try:
pools = await self.web.pools() pools = await self.web.pools()
except APIError: except APIError:
return self.config 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 return self.config
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_devs: dict = None self, rpc_devs: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_devs is None: if rpc_devs is None:
try: try:
rpc_devs = await self.rpc.devs() rpc_devs = await self.rpc.devs()
@@ -110,8 +110,8 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge):
return None return None
async def _get_hashboards( async def _get_hashboards(
self, rpc_devs: dict = None, rpc_devdetails: dict = None self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None
) -> List[HashBoard]: ) -> list[HashBoard]:
if rpc_devs is None: if rpc_devs is None:
try: try:
rpc_devs = await self.rpc.devs() rpc_devs = await self.rpc.devs()
@@ -162,7 +162,7 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge):
return hashboards 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: if web_devs is None:
try: try:
web_devs = await self.web.devs() web_devs = await self.web.devs()

View File

@@ -55,7 +55,7 @@ class MinerListenerProtocol(asyncio.Protocol):
class MinerListener: class MinerListener:
def __init__(self, bind_addr: str = "0.0.0.0"): 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.stop = asyncio.Event()
self.bind_addr = bind_addr self.bind_addr = bind_addr

View File

@@ -17,7 +17,8 @@
import asyncio import asyncio
import ipaddress import ipaddress
import logging import logging
from typing import AsyncIterator, List, Union from collections.abc import AsyncIterator
from typing import cast
from pyasic import settings from pyasic import settings
from pyasic.miners.factory import AnyMiner, miner_factory 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. 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 self.hosts = hosts
semaphore_limit = settings.get("network_scan_semaphore", 255) semaphore_limit = settings.get("network_scan_semaphore", 255)
if semaphore_limit is None: if semaphore_limit is None:
semaphore_limit = 255 semaphore_limit = 255
self.semaphore = asyncio.Semaphore(semaphore_limit) self.semaphore = asyncio.Semaphore(semaphore_limit)
def __len__(self): def __len__(self) -> int:
return len(self.hosts) return len(self.hosts)
@classmethod @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. """Parse a list of address constructors into a MinerNetwork.
Parameters: Parameters:
addresses: A list of address constructors, such as `["10.1-2.1.1-50", "10.4.1-2.1-50"]`. 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: for address in addresses:
hosts = [*hosts, *cls.from_address(address).hosts] hosts = [*hosts, *cls.from_address(address).hosts]
return cls(sorted(list(set(hosts)))) return cls(sorted(list(set(hosts))))
@@ -79,7 +80,7 @@ class MinerNetwork:
oct_4: An octet constructor, such as `"1-50"`. 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) 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): 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): for oct_4_idx in range((abs(oct_4_end - oct_4_start)) + 1):
oct_4_val = str(oct_4_idx + oct_4_start) oct_4_val = str(oct_4_idx + oct_4_start)
hosts.append( ip_addr = ipaddress.ip_address(
ipaddress.ip_address( ".".join([oct_1_val, oct_2_val, oct_3_val, oct_4_val])
".".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)) return cls(sorted(hosts))
@classmethod @classmethod
@@ -111,9 +112,13 @@ class MinerNetwork:
Parameters: Parameters:
subnet: A subnet string, such as `"10.0.0.1/24"`. 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. """Scan the network for miners.
Returns: Returns:
@@ -121,15 +126,17 @@ class MinerNetwork:
""" """
return await self.scan_network_for_miners() 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") 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] *[self.ping_and_get_miner(host) for host in self.hosts]
) )
# remove all None from the miner list # 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( logging.debug(
f"{self} - (Scan Network For Miners) - Found {len(miners)} miners" f"{self} - (Scan Network For Miners) - Found {len(miners)} miners"
) )
@@ -137,7 +144,7 @@ class MinerNetwork:
# return the miner objects # return the miner objects
return miners 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. Scan the network for miners using an async generator.
@@ -145,39 +152,47 @@ class MinerNetwork:
An asynchronous generator containing found miners. An asynchronous generator containing found miners.
""" """
# create a list of scan tasks # create a list of scan tasks
miners = asyncio.as_completed( tasks: list[asyncio.Task[AnyMiner | None]] = [
[asyncio.create_task(self.ping_and_get_miner(host)) for host in self.hosts] asyncio.create_task(self.ping_and_get_miner(host)) for host in self.hosts
) ]
for miner in miners: for miner in asyncio.as_completed(tasks):
try: try:
yield await miner result = await miner
yield result
except TimeoutError: except TimeoutError:
yield None yield None
return
async def ping_and_get_miner( async def ping_and_get_miner(
self, ip: ipaddress.ip_address self, ip: ipaddress.IPv4Address | ipaddress.IPv6Address
) -> Union[None, AnyMiner]: ) -> AnyMiner | None:
if settings.get("network_scan_semaphore") is 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: 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 @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: try:
return await ping_and_get_miner(ip) return await ping_and_get_miner(ip) # type: ignore[func-returns-value]
except ConnectionRefusedError: 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): for miner in asyncio.as_completed(tasks):
try: try:
return await miner return await miner
except ConnectionRefusedError: except ConnectionRefusedError:
pass pass
return None
async def ping_and_get_miner( async def ping_and_get_miner(
ip: ipaddress.ip_address, port=80 ip: ipaddress.IPv4Address | ipaddress.IPv6Address, port: int = 80
) -> Union[None, AnyMiner]: ) -> AnyMiner | None:
for _ in range(settings.get("network_ping_retries", 1)): for _ in range(settings.get("network_ping_retries", 1)):
try: try:
connection_fut = asyncio.open_connection(str(ip), port) connection_fut = asyncio.open_connection(str(ip), port)
@@ -190,7 +205,7 @@ async def ping_and_get_miner(
# make sure the writer is closed # make sure the writer is closed
await writer.wait_closed() await writer.wait_closed()
# ping was successful # 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: except asyncio.exceptions.TimeoutError:
# ping failed if we time out # ping failed if we time out
continue continue
@@ -198,11 +213,11 @@ async def ping_and_get_miner(
raise ConnectionRefusedError from e raise ConnectionRefusedError from e
except Exception as e: except Exception as e:
logging.warning(f"{str(ip)}: Unhandled ping exception: {e}") logging.warning(f"{str(ip)}: Unhandled ping exception: {e}")
return return None
return return None
def compute_oct_range(octet: str) -> tuple: def compute_oct_range(octet: str) -> tuple[int, int]:
octet_split = octet.split("-") octet_split = octet.split("-")
octet_start = int(octet_split[0]) octet_start = int(octet_split[0])
octet_end = None octet_end = None

View File

@@ -20,7 +20,6 @@ import json
import logging import logging
import re import re
import warnings import warnings
from typing import Union
from pyasic.errors import APIError, APIWarning from pyasic.errors import APIError, APIWarning
from pyasic.misc import validate_command_output from pyasic.misc import validate_command_output
@@ -35,7 +34,7 @@ class BaseMinerRPCAPI:
# api version if known # api version if known
self.api_ver = api_ver self.api_ver = api_ver
self.pwd = None self.pwd: str | None = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls is BaseMinerRPCAPI: if cls is BaseMinerRPCAPI:
@@ -47,8 +46,8 @@ class BaseMinerRPCAPI:
async def send_command( async def send_command(
self, self,
command: Union[str, bytes], command: str,
parameters: Union[str, int, bool] = None, parameters: str | int | bool | None = None,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
**kwargs, **kwargs,
@@ -86,10 +85,10 @@ class BaseMinerRPCAPI:
raise APIError(data.decode("utf-8")) raise APIError(data.decode("utf-8"))
return {} 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 # 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 validation[0]:
if not ignore_errors: if not ignore_errors:
# validate the command succeeded # validate the command succeeded
@@ -100,7 +99,7 @@ class BaseMinerRPCAPI:
) )
logging.debug(f"{self} - (Send Command) - Received data.") logging.debug(f"{self} - (Send Command) - Received data.")
return data return api_data
# Privileged command handler, only used by whatsminers, defined here for consistency. # Privileged command handler, only used by whatsminers, defined here for consistency.
async def send_privileged_command(self, *args, **kwargs) -> dict: 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 # 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" # standard multicommand format is "command1+command2"
# doesn't work for S19 which uses the backup _send_split_multicommand # doesn't work for S19 which uses the backup _send_split_multicommand
command = "+".join(commands) command = "+".join(valid_commands)
try: try:
data = await self.send_command(command, allow_warning=allow_warning) data = await self.send_command(command, allow_warning=allow_warning)
except APIError: except APIError:
@@ -164,10 +163,13 @@ class BaseMinerRPCAPI:
for func in for func in
# each function in self # each function in self
dir(self) dir(self)
if not func in ["commands", "open_api"] if func not in ["commands", "open_api"]
if callable(getattr(self, func)) and if callable(getattr(self, func))
and
# no __ or _ methods # 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 # remove all functions that are in this base class
func func
not in [ not in [
@@ -196,7 +198,7 @@ If you are sure you want to use this command please use API.send_command("{comma
self, self,
data: bytes, data: bytes,
*, *,
port: int = None, port: int | None = None,
timeout: int = 100, timeout: int = 100,
) -> bytes: ) -> bytes:
if port is None: if port is None:

View File

@@ -151,7 +151,7 @@ class BFGMinerRPCAPI(CGMinerRPCAPI):
""" """
return await self.send_command("procidentify", parameters=n) 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. """Set processor option opt to val on processor n.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>

View File

@@ -23,13 +23,15 @@ import json
import logging import logging
import re import re
import struct import struct
import typing
import warnings import warnings
from asyncio import Future, StreamReader, StreamWriter from collections.abc import AsyncGenerator
from typing import Any, AsyncGenerator, Callable, Literal, Union from typing import Any, Literal
import httpx import httpx
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from passlib.handlers.md5_crypt import md5_crypt from passlib.handlers.md5_crypt import md5_crypt
from pydantic import BaseModel, Field
from pyasic import settings from pyasic import settings
from pyasic.errors import APIError, APIWarning 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, # 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. # or add it as the Whatsminer_pwd in the settings.toml file.
PrePowerOnMessage = Union[
Literal["wait for adjust temp"], class TokenResponse(BaseModel):
Literal["adjust complete"], salt: str
Literal["adjust continue"], 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: def _crypt(word: str, salt: str) -> str:
@@ -93,7 +138,7 @@ def _add_to_16(string: str) -> bytes:
return str.encode(string) # return 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 returned from the BTMiner privileged API.
Parses data from the BTMiner privileged API using the token 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 # get the encoded data from the dict
enc_data = data["enc"] enc_data = data["enc"]
# get the aes key from the token data # 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 # unhexlify the aes key
aeskey = binascii.unhexlify(aeskey.encode()) aeskey = binascii.unhexlify(aeskey_hex.encode())
# create the required decryptor # create the required decryptor
aes = Cipher(algorithms.AES(aeskey), modes.ECB()) aes = Cipher(algorithms.AES(aeskey), modes.ECB())
decryptor = aes.decryptor() decryptor = aes.decryptor()
@@ -124,7 +169,7 @@ def parse_btminer_priviledge_data(token_data: dict, data: dict) -> dict:
return ret_msg 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. """Create a privileged command to send to the BTMiner API.
Creates a privileged command using the token from the API and the 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: Returns:
The encrypted privileged command to be sent to the miner. 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 # add token to command
command["token"] = token_data["host_sign"] command["token"] = token_data.host_sign
# encode host_passwd data and get hexdigest # 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 # unhexlify the encoded host_passwd
aeskey = binascii.unhexlify(aeskey.encode()) aeskey = binascii.unhexlify(aeskey_hex.encode())
# create a new AES key # create a new AES key
aes = Cipher(algorithms.AES(aeskey), modes.ECB()) aes = Cipher(algorithms.AES(aeskey), modes.ECB())
encryptor = aes.encryptor() 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: def __init__(self, ip: str, port: int = 4028, api_ver: str = "0.0.0") -> None:
super().__init__(ip, port, api_ver) super().__init__(ip, port, api_ver)
self.pwd = settings.get("default_whatsminer_rpc_password", "admin") self.pwd: str = settings.get("default_whatsminer_rpc_password", "admin")
self.token = None self.token: TokenData | None = None
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict: async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner. """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. allow_warning: A boolean to supress APIWarnings.
""" """
# make sure we can actually run each command, otherwise they will fail # 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" # standard multicommand format is "command1+command2"
# commands starting with "get_" and the "status" command aren't supported, but we can fake that # commands starting with "get_" and the "status" command aren't supported, but we can fake that
split_commands = [] split_commands = []
for command in list(commands): for command in commands_list:
if command.startswith("get_") or command == "status": if command.startswith("get_") or command == "status":
commands.remove(command) commands_list.remove(command)
# send seperately and append later # send seperately and append later
split_commands.append(command) split_commands.append(command)
command = "+".join(commands) command = "+".join(commands_list)
tasks = [] tasks = []
if len(split_commands) > 0: if len(split_commands) > 0:
@@ -240,7 +285,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
async def send_privileged_command( async def send_privileged_command(
self, self,
command: Union[str, bytes], command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
timeout: int = 10, timeout: int = 10,
**kwargs, **kwargs,
@@ -252,6 +297,8 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
except APIError as e: except APIError as e:
if not e.message == "can't access write cmd": if not e.message == "can't access write cmd":
raise raise
# If we get here, we caught the specific error but didn't handle it
raise
# try: # try:
# await self.open_api() # await self.open_api()
# except Exception as e: # except Exception as e:
@@ -262,7 +309,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
async def _send_privileged_command( async def _send_privileged_command(
self, self,
command: Union[str, bytes], command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
timeout: int = 10, timeout: int = 10,
**kwargs, **kwargs,
@@ -272,10 +319,10 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
if len(kwargs) > 0 if len(kwargs) > 0
else "" else ""
) )
command = {"cmd": command, **kwargs} cmd = {"cmd": command, **kwargs}
token_data = await self.get_token() 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") logging.debug(f"{self} - (Send Privileged Command) - Sending")
try: try:
@@ -289,24 +336,23 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
if ignore_errors: if ignore_errors:
return {} return {}
raise APIError("No data was returned from the API.") 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: try:
data = parse_btminer_priviledge_data(self.token, data) data_dict = parse_btminer_priviledge_data(token_data, data_dict)
print(data)
except Exception as e: except Exception as e:
logging.info(f"{str(self.ip)}: {e}") logging.info(f"{str(self.ip)}: {e}")
if not ignore_errors: if not ignore_errors:
# if it fails to validate, it is likely an error # 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]: if not validation[0]:
raise APIError(validation[1]) raise APIError(validation[1])
# return the parsed json as a dict # 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. """Gets token information from the API.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>
@@ -317,7 +363,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
""" """
logging.debug(f"{self} - (Get Token) - Getting token") logging.debug(f"{self} - (Get Token) - Getting token")
if self.token: if self.token:
if self.token["timestamp"] > datetime.datetime.now() - datetime.timedelta( if self.token.timestamp > datetime.datetime.now() - datetime.timedelta(
minutes=30 minutes=30
): ):
return self.token return self.token
@@ -325,26 +371,30 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
# get the token # get the token
data = await self.send_command("get_token") data = await self.send_command("get_token")
token_response = TokenResponse.model_validate(data["Msg"])
# encrypt the admin password with the salt # encrypt the admin password with the salt
pwd = _crypt(self.pwd, "$1$" + data["Msg"]["salt"] + "$") pwd_str = _crypt(self.pwd, "$1$" + token_response.salt + "$")
pwd = pwd.split("$") pwd_parts = pwd_str.split("$")
# take the 4th item from the pwd 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 # encrypt the pwd with the time and new salt
tmp = _crypt(pwd[3] + data["Msg"]["time"], "$1$" + data["Msg"]["newsalt"] + "$") tmp_str = _crypt(
tmp = tmp.split("$") pwd_parts[3] + token_response.time, "$1$" + token_response.newsalt + "$"
)
tmp_parts = tmp_str.split("$")
# take the 4th item from the encrypted pwd split # take the 4th item from the encrypted pwd split
host_sign = tmp[3] host_sign = tmp_parts[3]
# set the current token # set the current token
self.token = { self.token = TokenData(
"host_sign": host_sign, host_sign=host_sign,
"host_passwd_md5": host_passwd_md5, host_passwd_md5=host_passwd_md5,
"timestamp": datetime.datetime.now(), timestamp=datetime.datetime.now(),
} )
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}") logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}")
return self.token return self.token
@@ -388,12 +438,12 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
pool_1: str, pool_1: str,
worker_1: str, worker_1: str,
passwd_1: str, passwd_1: str,
pool_2: str = None, pool_2: str | None = None,
worker_2: str = None, worker_2: str | None = None,
passwd_2: str = None, passwd_2: str | None = None,
pool_3: str = None, pool_3: str | None = None,
worker_3: str = None, worker_3: str | None = None,
passwd_3: str = None, passwd_3: str | None = None,
) -> dict: ) -> dict:
"""Update the pools of the miner using the API. """Update the pools of the miner using the API.
<details> <details>
@@ -650,11 +700,11 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
async def net_config( async def net_config(
self, self,
ip: str = None, ip: str | None = None,
mask: str = None, mask: str | None = None,
gate: str = None, gate: str | None = None,
dns: str = None, dns: str | None = None,
host: str = None, host: str | None = None,
dhcp: bool = True, dhcp: bool = True,
): ):
if dhcp: if dhcp:
@@ -683,9 +733,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
""" """
if not -100 < percent < 100: if not -100 < percent < 100:
raise APIError( raise APIError(
f"Frequency % is outside of the allowed " "Frequency % is outside of the allowed "
f"range. Please set a % between -100 and " "range. Please set a % between -100 and "
f"100" "100"
) )
return await self.send_privileged_command( return await self.send_privileged_command(
"set_target_freq", percent=str(percent) "set_target_freq", percent=str(percent)
@@ -786,9 +836,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
if not 0 < percent < 100: if not 0 < percent < 100:
raise APIError( raise APIError(
f"Power PCT % is outside of the allowed " "Power PCT % is outside of the allowed "
f"range. Please set a % between 0 and " "range. Please set a % between 0 and "
f"100" "100"
) )
return await self.send_privileged_command("set_power_pct", percent=str(percent)) return await self.send_privileged_command("set_power_pct", percent=str(percent))
@@ -846,9 +896,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
if not 0 < percent < 100: if not 0 < percent < 100:
raise APIError( raise APIError(
f"Power PCT % is outside of the allowed " "Power PCT % is outside of the allowed "
f"range. Please set a % between 0 and " "range. Please set a % between 0 and "
f"100" "100"
) )
return await self.send_privileged_command( return await self.send_privileged_command(
"set_power_pct_v2", percent=str(percent) "set_power_pct_v2", percent=str(percent)
@@ -873,9 +923,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
""" """
if not -30 < temp_offset < 0: if not -30 < temp_offset < 0:
raise APIError( raise APIError(
f"Temp offset is outside of the allowed " "Temp offset is outside of the allowed "
f"range. Please set a number between -30 and " "range. Please set a number between -30 and "
f"0." "0."
) )
return await self.send_privileged_command( return await self.send_privileged_command(
@@ -924,9 +974,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
""" """
if not 0 < upfreq_speed < 9: if not 0 < upfreq_speed < 9:
raise APIError( raise APIError(
f"Upfreq speed is outside of the allowed " "Upfreq speed is outside of the allowed "
f"range. Please set a number between 0 (Normal) and " "range. Please set a number between 0 (Normal) and "
f"9 (Fastest)." "9 (Fastest)."
) )
return await self.send_privileged_command( return await self.send_privileged_command(
"adjust_upfreq_speed", upfreq_speed=upfreq_speed "adjust_upfreq_speed", upfreq_speed=upfreq_speed
@@ -1109,8 +1159,8 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
def __init__(self, ip: str, port: int = 4433, api_ver: str = "0.0.0"): def __init__(self, ip: str, port: int = 4433, api_ver: str = "0.0.0"):
super().__init__(ip, port, api_ver=api_ver) super().__init__(ip, port, api_ver=api_ver)
self.salt = None self.salt: str | None = None
self.pwd = "super" self.pwd: str = "super"
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict: async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner. """Creates and sends multiple commands as one command to the miner.
@@ -1120,47 +1170,52 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
allow_warning: A boolean to supress APIWarnings. allow_warning: A boolean to supress APIWarnings.
""" """
commands = self._check_commands(*commands) checked_commands = self._check_commands(*commands)
data = await self._send_split_multicommand(*commands) data = await self._send_split_multicommand(*checked_commands)
data["multicommand"] = True data["multicommand"] = True
return data return data
async def send_command( 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: ) -> dict:
if ":" in command: if ":" in command:
parameters = command.split(":")[1] parameters = command.split(":")[1]
command = command.split(":")[0] command = command.split(":")[0]
cmd = {"cmd": command}
if parameters is not None: cmd: BTMinerV3Command | BTMinerV3PrivilegedCommand
cmd["param"] = parameters
if command.startswith("set."): if command.startswith("set."):
salt = await self.get_salt() salt = await self.get_salt()
ts = int(datetime.datetime.now().timestamp()) ts = int(datetime.datetime.now().timestamp())
cmd["ts"] = ts token_str = command + self.pwd + salt + str(ts)
token_str = cmd["cmd"] + self.pwd + salt + str(ts)
token_hashed = bytearray( token_hashed = bytearray(
base64.b64encode(hashlib.sha256(token_str.encode("utf-8")).digest()) base64.b64encode(hashlib.sha256(token_str.encode("utf-8")).digest())
) )
b_arr = bytearray(token_hashed) b_arr = bytearray(token_hashed)
b_arr[8] = 0 b_arr[8] = 0
str_token = b_arr.split(b"\x00")[0].decode("utf-8") str_token = b_arr.split(b"\x00")[0].decode("utf-8")
cmd["account"] = "super"
cmd["token"] = str_token
# send the command cmd = BTMinerV3PrivilegedCommand(
ser = json.dumps(cmd).encode("utf-8") 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)) header = struct.pack("<I", len(ser))
return json.loads( return json.loads(await self._send_bytes(header + ser))
await self._send_bytes(header + json.dumps(cmd).encode("utf-8"))
)
async def _send_bytes( async def _send_bytes(
self, self,
data: bytes, data: bytes,
*, *,
port: int = None, port: int | None = None,
timeout: int = 100, timeout: int = 100,
) -> bytes: ) -> bytes:
if port is None: if port is None:
@@ -1234,6 +1289,7 @@ If you are sure you want to use this command please use API.send_command("{comma
self.salt = data["msg"]["salt"] self.salt = data["msg"]["salt"]
return self.salt return self.salt
@typing.no_type_check
async def get_miner_report(self) -> AsyncGenerator[dict, None]: async def get_miner_report(self) -> AsyncGenerator[dict, None]:
if self.writer is None: if self.writer is None:
await self.connect() await self.connect()

View File

@@ -243,7 +243,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
""" """
return await self.send_command("removepool", parameters=n) 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. """Save the config.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>
@@ -484,7 +484,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
""" """
return await self.send_command("usbstats") 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. """Set PGA option opt to val on PGA n.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>
@@ -611,7 +611,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI):
""" """
return await self.send_command("asccount") 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. """Set ASC n option opt to value val.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import Literal, Optional, Union from typing import Literal
from pyasic import APIError from pyasic import APIError
from pyasic.rpc.base import BaseMinerRPCAPI from pyasic.rpc.base import BaseMinerRPCAPI
@@ -37,9 +37,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.session_token = None self.session_token = None
async def send_privileged_command( async def send_privileged_command(self, command: str, *args, **kwargs) -> dict:
self, command: Union[str, bytes], *args, **kwargs
) -> dict:
if self.session_token is None: if self.session_token is None:
await self.auth() await self.auth()
return await self.send_command( return await self.send_command(
@@ -51,7 +49,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
async def send_command( async def send_command(
self, self,
command: Union[str, bytes], command: str,
*args, *args,
**kwargs, **kwargs,
) -> dict: ) -> dict:
@@ -59,7 +57,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
return await super().send_command(command, **kwargs) return await super().send_command(command, **kwargs)
return await super().send_command(command, parameters=",".join(args), **kwargs) return await super().send_command(command, parameters=",".join(args), **kwargs)
async def auth(self) -> Optional[str]: async def auth(self) -> str | None:
try: try:
data = await self.session() data = await self.session()
if not data["SESSION"][0]["SessionID"] == "": if not data["SESSION"][0]["SessionID"] == "":
@@ -74,6 +72,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
return self.session_token return self.session_token
except (LookupError, APIError): except (LookupError, APIError):
pass pass
return None
async def addgroup(self, name: str, quota: int) -> dict: async def addgroup(self, name: str, quota: int) -> dict:
"""Add a pool group. """Add a pool group.
@@ -91,7 +90,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
return await self.send_command("addgroup", name, quota) return await self.send_command("addgroup", name, quota)
async def addpool( 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: ) -> dict:
"""Add a pool. """Add a pool.
<details> <details>
@@ -163,13 +162,13 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
async def atmset( async def atmset(
self, self,
enabled: bool = None, enabled: bool | None = None,
startup_minutes: int = None, startup_minutes: int | None = None,
post_ramp_minutes: int = None, post_ramp_minutes: int | None = None,
temp_window: int = None, temp_window: int | None = None,
min_profile: str = None, min_profile: str | None = None,
max_profile: str = None, max_profile: str | None = None,
prevent_oc: bool = None, prevent_oc: bool | None = None,
) -> dict: ) -> dict:
"""Sets the ATM configuration. """Sets the ATM configuration.
<details> <details>
@@ -357,7 +356,10 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
return await self.send_command("fans") return await self.send_command("fans")
async def fanset( 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: ) -> dict:
"""Set fan control. """Set fan control.
<details> <details>
@@ -380,7 +382,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
fanset_data.append(f"power_off_speed={power_off_speed}") fanset_data.append(f"power_off_speed={power_off_speed}")
return await self.send_privileged_command("fanset", *fanset_data) 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. """Get frequency data for a board and chips.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>
@@ -453,7 +455,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
""" """
return await self.send_command("groups") 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. """Get chip health.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>
@@ -471,7 +473,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
healthchipget_data.append(str(chip_n)) healthchipget_data.append(str(chip_n))
return await self.send_command("healthchipget", *healthchipget_data) 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. """Select the next chip to have its health checked.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>
@@ -641,7 +643,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
""" """
return await self.send_privileged_command("profileset", profile) 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. """Reboot a board.
<details> <details>
<summary>Expand</summary> <summary>Expand</summary>
@@ -721,7 +723,10 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
return await self.send_command("session") return await self.send_command("session")
async def tempctrlset( 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: ) -> dict:
"""Set temp control values. """Set temp control values.
<details> <details>

View File

@@ -14,37 +14,45 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from ssl import SSLContext from ssl import SSLContext
from typing import Any, Union from typing import Any
import httpx import httpx
from httpx import AsyncHTTPTransport from httpx import AsyncHTTPTransport
from pydantic import BaseModel, Field
_settings = { # defaults
"network_ping_retries": 1, class Settings(BaseModel):
"network_ping_timeout": 3, network_ping_retries: int = Field(default=1)
"network_scan_semaphore": None, network_ping_timeout: int = Field(default=3)
"factory_get_retries": 1, network_scan_semaphore: int | None = Field(default=None)
"factory_get_timeout": 3, factory_get_retries: int = Field(default=1)
"get_data_retries": 1, factory_get_timeout: int = Field(default=3)
"api_function_timeout": 5, get_data_retries: int = Field(default=1)
"antminer_mining_mode_as_str": False, api_function_timeout: int = Field(default=5)
"default_whatsminer_rpc_password": "admin", antminer_mining_mode_as_str: bool = Field(default=False)
"default_innosilicon_web_password": "admin", default_whatsminer_rpc_password: str = Field(default="admin")
"default_antminer_web_password": "root", default_innosilicon_web_password: str = Field(default="admin")
"default_hammer_web_password": "root", default_antminer_web_password: str = Field(default="root")
"default_volcminer_web_password": "ltc@dog", default_hammer_web_password: str = Field(default="root")
"default_bosminer_web_password": "root", default_volcminer_web_password: str = Field(default="ltc@dog")
"default_vnish_web_password": "admin", default_bosminer_web_password: str = Field(default="root")
"default_goldshell_web_password": "123456789", default_vnish_web_password: str = Field(default="admin")
"default_auradine_web_password": "admin", default_goldshell_web_password: str = Field(default="123456789")
"default_epic_web_password": "letmein", default_auradine_web_password: str = Field(default="admin")
"default_hive_web_password": "root", default_epic_web_password: str = Field(default="letmein")
"default_iceriver_web_password": "12345678", default_hive_web_password: str = Field(default="root")
"default_elphapex_web_password": "root", default_iceriver_web_password: str = Field(default="12345678")
"default_mskminer_web_password": "root", default_elphapex_web_password: str = Field(default="root")
"default_antminer_ssh_password": "miner", default_mskminer_web_password: str = Field(default="root")
"default_bosminer_ssh_password": "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() 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 # this function returns an AsyncHTTPTransport instance to perform asynchronous HTTP requests
# using those options. # using those options.
def transport(verify: Union[str, bool, SSLContext] = ssl_cxt): def transport(verify: str | bool | SSLContext = ssl_cxt):
return AsyncHTTPTransport(verify=verify) return AsyncHTTPTransport(verify=verify)
def get(key: str, other: Any = None) -> Any: 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: def update(key: str, val: Any) -> None:
_settings[key] = val if hasattr(_settings, key):
setattr(_settings, key, val)
else:
_settings.__dict__[key] = val

View File

@@ -1,6 +1,5 @@
import asyncio import asyncio
import logging import logging
from typing import Optional
import asyncssh import asyncssh
@@ -8,9 +7,9 @@ import asyncssh
class BaseSSH: class BaseSSH:
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
self.ip = ip self.ip = ip
self.pwd = None self.pwd: str | None = None
self.username = "root" self.username: str = "root"
self.port = 22 self.port: int = 22
async def _get_connection(self) -> asyncssh.connect: async def _get_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection""" """Create a new asyncssh connection"""
@@ -29,7 +28,7 @@ class BaseSSH:
except Exception as e: except Exception as e:
raise ConnectionError from 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""" """Send an ssh command to the miner"""
try: try:
conn = await asyncio.wait_for(self._get_connection(), timeout=10) conn = await asyncio.wait_for(self._get_connection(), timeout=10)

View File

@@ -24,6 +24,7 @@ import aiofiles
import httpx import httpx
from pyasic import settings from pyasic import settings
from pyasic.errors import APIError
from pyasic.web.base import BaseWebAPI from pyasic.web.base import BaseWebAPI
@@ -35,12 +36,12 @@ class AntminerModernWebAPI(BaseWebAPI):
ip (str): IP address of the Antminer device. ip (str): IP address of the Antminer device.
""" """
super().__init__(ip) super().__init__(ip)
self.username = "root" self.username: str = "root"
self.pwd = settings.get("default_antminer_web_password", "root") self.pwd: str = settings.get("default_antminer_web_password", "root")
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -49,7 +50,7 @@ class AntminerModernWebAPI(BaseWebAPI):
"""Send a command to the Antminer device using HTTP digest authentication. """Send a command to the Antminer device using HTTP digest authentication.
Args: 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. ignore_errors (bool): If True, ignore any HTTP errors.
allow_warning (bool): If True, proceed with warnings. allow_warning (bool): If True, proceed with warnings.
privileged (bool): If set to True, requires elevated privileges. privileged (bool): If set to True, requires elevated privileges.
@@ -249,12 +250,12 @@ class AntminerOldWebAPI(BaseWebAPI):
ip (str): IP address of the Antminer device. ip (str): IP address of the Antminer device.
""" """
super().__init__(ip) super().__init__(ip)
self.username = "root" self.username: str = "root"
self.pwd = settings.get("default_antminer_web_password", "root") self.pwd: str = settings.get("default_antminer_web_password", "root")
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -263,7 +264,7 @@ class AntminerOldWebAPI(BaseWebAPI):
"""Send a command to the Antminer device using HTTP digest authentication. """Send a command to the Antminer device using HTTP digest authentication.
Args: 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. ignore_errors (bool): If True, ignore any HTTP errors.
allow_warning (bool): If True, proceed with warnings. allow_warning (bool): If True, proceed with warnings.
privileged (bool): If set to True, requires elevated privileges. privileged (bool): If set to True, requires elevated privileges.
@@ -293,6 +294,7 @@ class AntminerOldWebAPI(BaseWebAPI):
return data.json() return data.json()
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
pass pass
raise APIError(f"Failed to send command to miner: {self}")
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True 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: async with aiofiles.open(file, "rb") as firmware:
file_content = await firmware.read() file_content = await firmware.read()
parameters = { return await self.send_command(
"file": (file.name, file_content, "application/octet-stream"), command="upgrade",
"filename": file.name, file=(file.name, file_content, "application/octet-stream"),
"keep_settings": keep_settings, filename=file.name,
} keep_settings=keep_settings,
)
return await self.send_command(command="upgrade", **parameters)

View File

@@ -69,7 +69,7 @@ class AuradineWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -78,7 +78,7 @@ class AuradineWebAPI(BaseWebAPI):
"""Send a command to the Auradine miner, handling authentication and retries. """Send a command to the Auradine miner, handling authentication and retries.
Args: Args:
command (str | bytes): The specific command to execute. command (str): The specific command to execute.
ignore_errors (bool): Whether to ignore HTTP errors. ignore_errors (bool): Whether to ignore HTTP errors.
allow_warning (bool): Whether to proceed with warnings. allow_warning (bool): Whether to proceed with warnings.
privileged (bool): Whether the command requires privileged access. privileged (bool): Whether the command requires privileged access.
@@ -95,6 +95,10 @@ class AuradineWebAPI(BaseWebAPI):
await self.auth() await self.auth()
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
for i in range(settings.get("get_data_retries", 1)): 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: try:
if post: if post:
response = await client.post( response = await client.post(
@@ -120,6 +124,7 @@ class AuradineWebAPI(BaseWebAPI):
return json_data return json_data
except (httpx.HTTPError, json.JSONDecodeError): except (httpx.HTTPError, json.JSONDecodeError):
pass pass
raise APIError(f"Failed to send command to miner: {self}")
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
@@ -145,11 +150,9 @@ class AuradineWebAPI(BaseWebAPI):
*[tasks[cmd] for cmd in tasks], return_exceptions=True *[tasks[cmd] for cmd in tasks], return_exceptions=True
) )
data = {"multicommand": True} data: dict[str, Any] = {"multicommand": True}
for cmd, result in zip(tasks.keys(), results): for cmd, result in zip(tasks.keys(), results):
if not isinstance(result, (APIError, Exception)): if isinstance(result, dict):
if result is None or result == {}:
result = {}
data[cmd] = result data[cmd] = result
return data return data
@@ -182,7 +185,9 @@ class AuradineWebAPI(BaseWebAPI):
""" """
return await self.send_command("fan", index=fan, percentage=speed_pct) 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. """Upgrade the firmware of the Auradine miner.
Args: Args:

View File

@@ -34,20 +34,21 @@ class AvalonMinerWebAPI(BaseWebAPI):
ip (str): IP address of the Avalonminer device. ip (str): IP address of the Avalonminer device.
""" """
super().__init__(ip) super().__init__(ip)
self.username = "root" self.username: str = "root"
self.pwd = settings.get("default_avalonminer_web_password", "root") self.pwd: str = settings.get("default_avalonminer_web_password", "root")
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False,
**parameters: Any, **parameters: Any,
) -> dict: ) -> dict:
"""Send a command to the Avalonminer device using HTTP digest authentication. """Send a command to the Avalonminer device using HTTP digest authentication.
Args: 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. ignore_errors (bool): If True, ignore any HTTP errors.
allow_warning (bool): If True, proceed with warnings. allow_warning (bool): If True, proceed with warnings.
**parameters: Arbitrary keyword arguments to be sent as parameters in the request. **parameters: Arbitrary keyword arguments to be sent as parameters in the request.

View File

@@ -26,24 +26,24 @@ class BaseWebAPI(ABC):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
# ip address of the miner # ip address of the miner
self.ip = ip self.ip = ip
self.username = None self.username: str | None = None
self.pwd = None self.pwd: str | None = None
self.port = 80 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: if cls is BaseWebAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated") raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls) return object.__new__(cls)
def __repr__(self): def __repr__(self) -> str:
return f"{self.__class__.__name__}: {str(self.ip)}" return f"{self.__class__.__name__}: {str(self.ip)}"
@abstractmethod @abstractmethod
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -57,7 +57,7 @@ class BaseWebAPI(ABC):
) -> dict: ) -> dict:
pass pass
def _check_commands(self, *commands): def _check_commands(self, *commands: str) -> list[str]:
allowed_commands = self.get_commands() allowed_commands = self.get_commands()
return_commands = [] return_commands = []
for command in [*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 return return_commands
@property @property
def commands(self) -> list: def commands(self) -> list[str]:
return self.get_commands() 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. """Get a list of command accessible to a specific type of web API on the miner.
Returns: 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 # each function in self
dir(self) dir(self)
if not func == "commands" if not func == "commands"
if callable(getattr(self, func)) and if callable(getattr(self, func))
and
# no __ or _ methods # 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 # remove all functions that are in this base class
func func
not in [ not in [

View File

@@ -1,5 +1,5 @@
from datetime import datetime, timedelta 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 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 # https://github.com/danielgtaylor/python-betterproto/pull/609
def to_pydict( def to_pydict(
self, casing: Casing = Casing.CAMEL, include_default_values: bool = False self, casing: Casing = Casing.CAMEL, include_default_values: bool = False
) -> Dict[str, Any]: ) -> dict[str, Any]:
""" """
Returns a python dict representation of this object. Returns a python dict representation of this object.
@@ -23,10 +23,10 @@ def to_pydict(
Returns Returns
-------- --------
Dict[:class:`str`, Any] dict[:class:`str`, Any]
The python dict representation of this object. The python dict representation of this object.
""" """
output: Dict[str, Any] = {} output: dict[str, Any] = {}
defaults = self._betterproto.default_gen defaults = self._betterproto.default_gen
for field_name, meta in self._betterproto.meta_by_field_name.items(): for field_name, meta in self._betterproto.meta_by_field_name.items():
field_is_repeated = defaults[field_name] is list field_is_repeated = defaults[field_name] is list

View File

@@ -51,10 +51,10 @@ class BOSMinerGRPCStub(
class BOSerWebAPI(BaseWebAPI): class BOSerWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.username = "root" self.username: str = "root"
self.pwd = settings.get("default_bosminer_password", "root") self.pwd: str = settings.get("default_bosminer_password", "root")
self.port = 50051 self.port = 50051
self._auth_time = None self._auth_time: datetime | None = None
@property @property
def commands(self) -> list: def commands(self) -> list:
@@ -68,15 +68,17 @@ class BOSerWebAPI(BaseWebAPI):
dir(self) dir(self)
if func if func
not in ["send_command", "multicommand", "auth", "commands", "get_commands"] not in ["send_command", "multicommand", "auth", "commands", "get_commands"]
if callable(getattr(self, func)) and if callable(getattr(self, func))
and
# no __ or _ methods # no __ or _ methods
not func.startswith("__") and not func.startswith("_") not func.startswith("__")
and not func.startswith("_")
] ]
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict: ) -> dict:
result = {"multicommand": True} result: dict[str, Any] = {"multicommand": True}
tasks = {} tasks = {}
for command in commands: for command in commands:
try: try:
@@ -89,14 +91,14 @@ class BOSerWebAPI(BaseWebAPI):
) )
for cmd, task_result in zip(tasks.keys(), results): for cmd, task_result in zip(tasks.keys(), results):
if not isinstance(task_result, (GRPCError, APIError, ConnectionError)): if isinstance(task_result, dict):
result[cmd] = task_result result[cmd] = task_result
return result return result
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -125,14 +127,16 @@ class BOSerWebAPI(BaseWebAPI):
raise APIError(f"gRPC command failed - {endpoint}") from e raise APIError(f"gRPC command failed - {endpoint}") from e
async def auth(self) -> str | None: async def auth(self) -> str | None:
if self.token is not None and self._auth_time - datetime.now() < timedelta( if (
seconds=3540 self.token is not None
and self._auth_time is not None
and datetime.now() - self._auth_time < timedelta(seconds=3540)
): ):
return self.token return self.token
await self._get_auth() await self._get_auth()
return self.token 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: async with Channel(self.ip, self.port) as c:
req = LoginRequest(username=self.username, password=self.pwd) req = LoginRequest(username=self.username, password=self.pwd)
async with c.request( async with c.request(
@@ -143,11 +147,13 @@ class BOSerWebAPI(BaseWebAPI):
) as stream: ) as stream:
await stream.send_message(req, end=True) await stream.send_message(req, end=True)
await stream.recv_initial_metadata() await stream.recv_initial_metadata()
auth = stream.initial_metadata.get("authorization") if stream.initial_metadata is not None:
if auth is not None: auth = stream.initial_metadata.get("authorization")
self.token = auth if auth is not None and isinstance(auth, str):
self._auth_time = datetime.now() self.token = auth
return self.token self._auth_time = datetime.now()
return self.token
return None
async def get_api_version(self) -> dict: async def get_api_version(self) -> dict:
return await self.send_command( return await self.send_command(
@@ -194,7 +200,7 @@ class BOSerWebAPI(BaseWebAPI):
privileged=True, 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( return await self.send_command(
"set_password", "set_password",
message=SetPasswordRequest(password=password), message=SetPasswordRequest(password=password),
@@ -209,7 +215,7 @@ class BOSerWebAPI(BaseWebAPI):
async def set_immersion_mode( async def set_immersion_mode(
self, self,
enable: bool, enable: bool,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_immersion_mode", "set_immersion_mode",
@@ -230,7 +236,7 @@ class BOSerWebAPI(BaseWebAPI):
) )
async def set_default_power_target( async def set_default_power_target(
self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY self, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY)
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_default_power_target", "set_default_power_target",
@@ -241,7 +247,7 @@ class BOSerWebAPI(BaseWebAPI):
async def set_power_target( async def set_power_target(
self, self,
power_target: int, power_target: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_power_target", "set_power_target",
@@ -254,7 +260,7 @@ class BOSerWebAPI(BaseWebAPI):
async def increment_power_target( async def increment_power_target(
self, self,
power_target_increment: int, power_target_increment: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"increment_power_target", "increment_power_target",
@@ -268,7 +274,7 @@ class BOSerWebAPI(BaseWebAPI):
async def decrement_power_target( async def decrement_power_target(
self, self,
power_target_decrement: int, power_target_decrement: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"decrement_power_target", "decrement_power_target",
@@ -280,7 +286,7 @@ class BOSerWebAPI(BaseWebAPI):
) )
async def set_default_hashrate_target( async def set_default_hashrate_target(
self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY self, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY)
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_default_hashrate_target", "set_default_hashrate_target",
@@ -291,7 +297,7 @@ class BOSerWebAPI(BaseWebAPI):
async def set_hashrate_target( async def set_hashrate_target(
self, self,
hashrate_target: float, hashrate_target: float,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_hashrate_target", "set_hashrate_target",
@@ -305,7 +311,7 @@ class BOSerWebAPI(BaseWebAPI):
async def increment_hashrate_target( async def increment_hashrate_target(
self, self,
hashrate_target_increment: int, hashrate_target_increment: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"increment_hashrate_target", "increment_hashrate_target",
@@ -321,7 +327,7 @@ class BOSerWebAPI(BaseWebAPI):
async def decrement_hashrate_target( async def decrement_hashrate_target(
self, self,
hashrate_target_decrement: int, hashrate_target_decrement: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"decrement_hashrate_target", "decrement_hashrate_target",
@@ -339,15 +345,19 @@ class BOSerWebAPI(BaseWebAPI):
enable: bool, enable: bool,
power_step: int, power_step: int,
min_power_target: int, min_power_target: int,
enable_shutdown: bool = None, enable_shutdown: bool | None = None,
shutdown_duration: int = None, shutdown_duration: int | None = None,
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_dps", "set_dps",
message=SetDpsRequest( message=SetDpsRequest(
enable=enable, enable=enable,
enable_shutdown=enable_shutdown, enable_shutdown=enable_shutdown,
shutdown_duration=shutdown_duration, shutdown_duration=(
Hours(hours=shutdown_duration)
if shutdown_duration is not None
else None
),
target=DpsTarget( target=DpsTarget(
power_target=DpsPowerTarget( power_target=DpsPowerTarget(
power_step=Power(power_step), power_step=Power(power_step),
@@ -360,50 +370,40 @@ class BOSerWebAPI(BaseWebAPI):
async def set_performance_mode( async def set_performance_mode(
self, self,
wattage_target: int = None, wattage_target: int | None = None,
hashrate_target: int = None, hashrate_target: int | None = None,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
if wattage_target is not None and hashrate_target is not None: if wattage_target is not None and hashrate_target is not None:
logging.error( logging.error(
"Cannot use both wattage_target and hashrate_target, using wattage_target." "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( raise APIError(
"No target supplied, please supply either wattage_target or hashrate_target." "No target supplied, please supply either wattage_target or hashrate_target."
) )
if wattage_target is not None:
return await self.send_command( return await self.send_command(
"set_performance_mode", "set_performance_mode",
message=SetPerformanceModeRequest( message=SetPerformanceModeRequest(
save_action=save_action, save_action=save_action,
mode=PerformanceMode( mode=PerformanceMode(tuner_mode=tuner_mode),
tuner_mode=TunerPerformanceMode( ),
power_target=PowerTargetMode( privileged=True,
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
)
)
)
),
),
privileged=True,
)
async def get_active_performance_mode(self) -> dict: async def get_active_performance_mode(self) -> dict:
return await self.send_command( return await self.send_command(
@@ -461,8 +461,8 @@ class BOSerWebAPI(BaseWebAPI):
async def enable_hashboards( async def enable_hashboards(
self, self,
hashboard_ids: List[str], hashboard_ids: list[str],
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"enable_hashboards", "enable_hashboards",
@@ -474,8 +474,8 @@ class BOSerWebAPI(BaseWebAPI):
async def disable_hashboards( async def disable_hashboards(
self, self,
hashboard_ids: List[str], hashboard_ids: list[str],
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"disable_hashboards", "disable_hashboards",
@@ -487,8 +487,8 @@ class BOSerWebAPI(BaseWebAPI):
async def set_pool_groups( async def set_pool_groups(
self, self,
pool_groups: List[PoolGroupConfiguration], pool_groups: list[PoolGroupConfiguration],
save_action: SaveAction = SaveAction.SAVE_AND_APPLY, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY),
) -> dict: ) -> dict:
return await self.send_command( return await self.send_command(
"set_pool_groups", "set_pool_groups",

View File

@@ -34,7 +34,7 @@ class BOSMinerWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,

View File

@@ -33,12 +33,12 @@ class ElphapexWebAPI(BaseWebAPI):
ip (str): IP address of the Elphapex device. ip (str): IP address of the Elphapex device.
""" """
super().__init__(ip) super().__init__(ip)
self.username = "root" self.username: str = "root"
self.pwd = settings.get("default_elphapex_web_password", "root") self.pwd: str = settings.get("default_elphapex_web_password", "root")
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -47,7 +47,7 @@ class ElphapexWebAPI(BaseWebAPI):
"""Send a command to the Elphapex device using HTTP digest authentication. """Send a command to the Elphapex device using HTTP digest authentication.
Args: 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. ignore_errors (bool): If True, ignore any HTTP errors.
allow_warning (bool): If True, proceed with warnings. allow_warning (bool): If True, proceed with warnings.
privileged (bool): If set to True, requires elevated privileges. privileged (bool): If set to True, requires elevated privileges.

View File

@@ -38,7 +38,7 @@ class ePICWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -49,15 +49,17 @@ class ePICWebAPI(BaseWebAPI):
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
for retry_cnt in range(settings.get("get_data_retries", 1)): for retry_cnt in range(settings.get("get_data_retries", 1)):
try: try:
if parameters.get("form") is not None: if parameters.get("files") is not None:
form_data = parameters["form"] files = parameters["files"]
form_data.add_field("password", self.pwd) data_fields = parameters.get("data", {})
data_fields["password"] = self.pwd
response = await client.post( response = await client.post(
f"http://{self.ip}:{self.port}/{command}", f"http://{self.ip}:{self.port}/{command}",
timeout=5, timeout=5,
data=form_data, files=files,
data=data_fields,
) )
if post: elif post:
response = await client.post( response = await client.post(
f"http://{self.ip}:{self.port}/{command}", f"http://{self.ip}:{self.port}/{command}",
timeout=5, timeout=5,
@@ -89,11 +91,12 @@ class ePICWebAPI(BaseWebAPI):
return {"success": True} return {"success": True}
except (httpx.HTTPError, json.JSONDecodeError, AttributeError): except (httpx.HTTPError, json.JSONDecodeError, AttributeError):
pass pass
raise APIError(f"Failed to send command to miner: {self}")
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict: ) -> dict:
data = {k: None for k in commands} data: dict[str, Any] = {k: None for k in commands}
data["multicommand"] = True data["multicommand"] = True
for command in commands: for command in commands:
data[command] = await self.send_command(command) data[command] = await self.send_command(command)
@@ -147,7 +150,7 @@ class ePICWebAPI(BaseWebAPI):
async def capabilities(self) -> dict: async def capabilities(self) -> dict:
return await self.send_command("capabilities") 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 """Perform a system update by uploading a firmware file and sending a
command to initiate the update.""" command to initiate the update."""
@@ -159,9 +162,7 @@ class ePICWebAPI(BaseWebAPI):
checksum = sha256_hash.hexdigest() checksum = sha256_hash.hexdigest()
# prepare the multipart/form-data request # prepare the multipart/form-data request
form_data = aiohttp.FormData() with open(file, "rb") as f:
form_data.add_field("checksum", checksum) files = {"update.zip": ("update.zip", f, "application/zip")}
form_data.add_field("keepsettings", str(keep_settings).lower()) data = {"checksum": checksum, "keepsettings": str(keep_settings).lower()}
form_data.add_field("update.zip", open(file, "rb"), filename="update.zip") await self.send_command("systemupdate", files=files, data=data)
await self.send_command("systemupdate", form=form_data)

View File

@@ -13,7 +13,7 @@ from pyasic.web.base import BaseWebAPI
class ESPMinerWebAPI(BaseWebAPI): class ESPMinerWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -21,7 +21,8 @@ class ESPMinerWebAPI(BaseWebAPI):
) -> dict: ) -> dict:
url = f"http://{self.ip}:{self.port}/api/{command}" url = f"http://{self.ip}:{self.port}/api/{command}"
async with httpx.AsyncClient(transport=settings.transport()) as client: 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: try:
if parameters.get("post", False): if parameters.get("post", False):
parameters.pop("post") parameters.pop("post")
@@ -42,14 +43,22 @@ class ESPMinerWebAPI(BaseWebAPI):
url, url,
timeout=settings.get("api_function_timeout", 5), timeout=settings.get("api_function_timeout", 5),
) )
except httpx.HTTPError: except httpx.HTTPError as e:
pass if attempt == retries - 1:
raise APIError(
f"HTTP error sending '{command}' to {self.ip}: {e}"
)
else: else:
if data.status_code == 200: if data.status_code == 200:
try: try:
return data.json() return data.json()
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError as e:
pass 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( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
@@ -75,26 +84,27 @@ class ESPMinerWebAPI(BaseWebAPI):
*[tasks[cmd] for cmd in tasks], return_exceptions=True *[tasks[cmd] for cmd in tasks], return_exceptions=True
) )
data = {"multicommand": True} data: dict[str, Any] = {"multicommand": True}
for cmd, result in zip(tasks.keys(), results): for cmd, result in zip(tasks.keys(), results):
if not isinstance(result, (APIError, Exception)): if not isinstance(result, (APIError, Exception)):
if result is None or result == {}: if result is None or result == {}:
result = {} data[cmd] = {}
data[cmd] = result else:
data[cmd] = result
return data return data
async def system_info(self): async def system_info(self) -> dict:
return await self.send_command("system/info") 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") 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) 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) 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") return await self.send_command("system/asic")

View File

@@ -17,20 +17,23 @@ from __future__ import annotations
import json import json
import warnings import warnings
from typing import Any from typing import Any, TypedDict
import httpx import httpx
from pyasic import settings from pyasic import settings
from pyasic.errors import APIError
from pyasic.web.base import BaseWebAPI from pyasic.web.base import BaseWebAPI
PoolPass = TypedDict("PoolPass", {"pass": str})
class GoldshellWebAPI(BaseWebAPI): class GoldshellWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.username = "admin" self.username: str = "admin"
self.pwd = settings.get("default_goldshell_web_password", "123456789") self.pwd: str = settings.get("default_goldshell_web_password", "123456789")
self.token = None self.token: str | None = None
async def auth(self) -> str | None: async def auth(self) -> str | None:
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
@@ -63,7 +66,7 @@ class GoldshellWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -72,7 +75,12 @@ class GoldshellWebAPI(BaseWebAPI):
if self.token is None: if self.token is None:
await self.auth() await self.auth()
async with httpx.AsyncClient(transport=settings.transport()) as client: 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: try:
if not parameters == {}: if not parameters == {}:
response = await client.put( response = await client.put(
@@ -91,17 +99,33 @@ class GoldshellWebAPI(BaseWebAPI):
return json_data return json_data
except TypeError: except TypeError:
await self.auth() await self.auth()
except (httpx.HTTPError, json.JSONDecodeError): except httpx.HTTPError as e:
pass 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( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict: ) -> dict:
data = {k: None for k in commands} data: dict[str, Any] = {k: None for k in commands}
data["multicommand"] = True data["multicommand"] = True
await self.auth() await self.auth()
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
for command in commands: for command in commands:
if self.token is None:
raise APIError(
f"Could not authenticate web token with miner: {self}"
)
try: try:
uri_commnand = command uri_commnand = command
if command == "devs": if command == "devs":
@@ -127,22 +151,22 @@ class GoldshellWebAPI(BaseWebAPI):
async def newpool(self, url: str, user: str, password: str) -> dict: async def newpool(self, url: str, user: str, password: str) -> dict:
# looks dumb, but cant pass `pass` since it is a built in type # looks dumb, but cant pass `pass` since it is a built in type
return await self.send_command( poolpass: PoolPass = {"pass": password}
"newpool", **{"url": url, "user": user, "pass": password} return await self.send_command("newpool", url=url, user=user, **poolpass)
)
async def delpool( async def delpool(
self, url: str, user: str, password: str, dragid: int = 0 self, url: str, user: str, password: str, dragid: int = 0
) -> dict: ) -> dict:
# looks dumb, but cant pass `pass` since it is a built in type # looks dumb, but cant pass `pass` since it is a built in type
poolpass: PoolPass = {"pass": password}
return await self.send_command( 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: async def setting(self) -> dict:
return await self.send_command("setting") 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) await self.send_command("setting", **values)
async def status(self) -> dict: async def status(self) -> dict:

View File

@@ -33,12 +33,12 @@ class HammerWebAPI(BaseWebAPI):
ip (str): IP address of the Hammer device. ip (str): IP address of the Hammer device.
""" """
super().__init__(ip) super().__init__(ip)
self.username = "root" self.username: str = "root"
self.pwd = settings.get("default_hammer_web_password", "root") self.pwd: str = settings.get("default_hammer_web_password", "root")
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -47,7 +47,7 @@ class HammerWebAPI(BaseWebAPI):
"""Send a command to the Hammer device using HTTP digest authentication. """Send a command to the Hammer device using HTTP digest authentication.
Args: 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. ignore_errors (bool): If True, ignore any HTTP errors.
allow_warning (bool): If True, proceed with warnings. allow_warning (bool): If True, proceed with warnings.
privileged (bool): If set to True, requires elevated privileges. privileged (bool): If set to True, requires elevated privileges.

View File

@@ -22,7 +22,7 @@ from typing import Any
import aiofiles import aiofiles
import httpx import httpx
from pyasic import settings from pyasic import APIError, settings
from pyasic.web.base import BaseWebAPI from pyasic.web.base import BaseWebAPI
@@ -34,12 +34,12 @@ class HiveonWebAPI(BaseWebAPI):
ip (str): IP address of the Antminer device. ip (str): IP address of the Antminer device.
""" """
super().__init__(ip) super().__init__(ip)
self.username = "root" self.username: str = "root"
self.pwd = settings.get("default_hive_web_password", "root") self.pwd: str = settings.get("default_hive_web_password", "root")
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -48,7 +48,7 @@ class HiveonWebAPI(BaseWebAPI):
"""Send a command to the Antminer device using HTTP digest authentication. """Send a command to the Antminer device using HTTP digest authentication.
Args: 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. ignore_errors (bool): If True, ignore any HTTP errors.
allow_warning (bool): If True, proceed with warnings. allow_warning (bool): If True, proceed with warnings.
privileged (bool): If set to True, requires elevated privileges. privileged (bool): If set to True, requires elevated privileges.
@@ -62,22 +62,26 @@ class HiveonWebAPI(BaseWebAPI):
try: try:
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
if parameters: if parameters:
data = await client.post( response = await client.post(
url, url,
data=parameters, data=parameters,
auth=auth, auth=auth,
timeout=settings.get("api_function_timeout", 3), timeout=settings.get("api_function_timeout", 3),
) )
else: else:
data = await client.get(url, auth=auth) response = await client.get(url, auth=auth)
except httpx.HTTPError: except httpx.HTTPError as e:
pass raise APIError(f"HTTP error sending '{command}' to {self.ip}: {e}")
else: else:
if data.status_code == 200: if response.status_code == 200:
try: try:
return data.json() return response.json()
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError as e:
pass 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( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True 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: async with aiofiles.open(file, "rb") as firmware:
file_content = await firmware.read() file_content = await firmware.read()
parameters = { return await self.send_command(
"file": (file.name, file_content, "application/octet-stream"), "upgrade",
"filename": file.name, file=(file.name, file_content, "application/octet-stream"),
"keep_settings": keep_settings, filename=file.name,
} keep_settings=keep_settings,
)
return await self.send_command(command="upgrade", **parameters)

View File

@@ -41,7 +41,7 @@ class IceRiverWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,

View File

@@ -29,9 +29,9 @@ from pyasic.web.base import BaseWebAPI
class InnosiliconWebAPI(BaseWebAPI): class InnosiliconWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.username = "admin" self.username: str = "admin"
self.pwd = settings.get("default_innosilicon_web_password", "admin") self.pwd: str = settings.get("default_innosilicon_web_password", "admin")
self.token = None self.token: str | None = None
async def auth(self) -> str | None: async def auth(self) -> str | None:
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
@@ -49,7 +49,7 @@ class InnosiliconWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -58,7 +58,12 @@ class InnosiliconWebAPI(BaseWebAPI):
if self.token is None: if self.token is None:
await self.auth() await self.auth()
async with httpx.AsyncClient(transport=settings.transport()) as client: 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: try:
response = await client.post( response = await client.post(
f"http://{self.ip}:{self.port}/api/{command}", f"http://{self.ip}:{self.port}/api/{command}",
@@ -82,17 +87,33 @@ class InnosiliconWebAPI(BaseWebAPI):
raise APIError(json_data["message"]) raise APIError(json_data["message"])
raise APIError("Innosilicon web api command failed.") raise APIError("Innosilicon web api command failed.")
return json_data return json_data
except (httpx.HTTPError, json.JSONDecodeError): except httpx.HTTPError as e:
pass 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( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict: ) -> dict:
data = {k: None for k in commands} data: dict[str, Any] = {k: None for k in commands}
data["multicommand"] = True data["multicommand"] = True
await self.auth() await self.auth()
async with httpx.AsyncClient(transport=settings.transport()) as client: async with httpx.AsyncClient(transport=settings.transport()) as client:
for command in commands: for command in commands:
if self.token is None:
raise APIError(
f"Could not authenticate web token with miner: {self}"
)
try: try:
response = await client.post( response = await client.post(
f"http://{self.ip}:{self.port}/api/{command}", f"http://{self.ip}:{self.port}/api/{command}",

View File

@@ -6,13 +6,15 @@ from typing import Any
import httpx import httpx
from pyasic import settings from pyasic import APIError, settings
from pyasic.web.base import BaseWebAPI from pyasic.web.base import BaseWebAPI
class MaraWebAPI(BaseWebAPI): class MaraWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.username: str = "root"
self.pwd: str = "root"
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
@@ -52,7 +54,7 @@ class MaraWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
@@ -65,22 +67,26 @@ class MaraWebAPI(BaseWebAPI):
transport=settings.transport(), transport=settings.transport(),
) as client: ) as client:
if parameters: if parameters:
data = await client.post( response = await client.post(
url, url,
auth=auth, auth=auth,
timeout=settings.get("api_function_timeout", 3), timeout=settings.get("api_function_timeout", 3),
json=parameters, json=parameters,
) )
else: else:
data = await client.get(url, auth=auth) response = await client.get(url, auth=auth)
except httpx.HTTPError: except httpx.HTTPError:
pass pass
else: else:
if data.status_code == 200: if response.status_code == 200:
try: try:
return data.json() return response.json()
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError as e:
pass 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): async def brief(self):
return await self.send_command("brief") return await self.send_command("brief")

View File

@@ -41,7 +41,7 @@ class MSKMinerWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,

View File

@@ -22,6 +22,7 @@ from typing import Any
import httpx import httpx
from pyasic import settings from pyasic import settings
from pyasic.errors import APIError
from pyasic.web.base import BaseWebAPI from pyasic.web.base import BaseWebAPI
@@ -53,21 +54,26 @@ class VNishWebAPI(BaseWebAPI):
async def send_command( async def send_command(
self, self,
command: str | bytes, command: str,
ignore_errors: bool = False, ignore_errors: bool = False,
allow_warning: bool = True, allow_warning: bool = True,
privileged: bool = False, privileged: bool = False,
**parameters: Any, **parameters: Any,
) -> dict | None: ) -> dict:
post = privileged or not parameters == {} post = privileged or not parameters == {}
if self.token is None: if self.token is None:
await self.auth() await self.auth()
async with httpx.AsyncClient(transport=settings.transport()) as client: 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: try:
auth = self.token auth = self.token
if auth is None:
raise APIError(
f"Could not authenticate web token with miner: {self}"
)
if command.startswith("system"): if command.startswith("system"):
auth = "Bearer " + self.token auth = "Bearer " + auth
if post: if post:
response = await client.post( response = await client.post(
@@ -90,13 +96,30 @@ class VNishWebAPI(BaseWebAPI):
if json_data: if json_data:
return json_data return json_data
return {"success": True} return {"success": True}
except (httpx.HTTPError, json.JSONDecodeError, AttributeError): except httpx.HTTPError as e:
pass 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}"
)
except AttributeError as e:
if attempt == retries - 1:
raise APIError(
f"Attribute error sending '{command}' to {self.ip}: {e}"
)
raise APIError(f"Failed to send command to miner: {self}")
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict: ) -> dict:
data = {k: None for k in commands} data: dict[str, Any] = {k: None for k in commands}
data["multicommand"] = True data["multicommand"] = True
for command in commands: for command in commands:
data[command] = await self.send_command(command) data[command] = await self.send_command(command)
@@ -141,14 +164,14 @@ class VNishWebAPI(BaseWebAPI):
async def settings(self) -> dict: async def settings(self) -> dict:
return await self.send_command("settings") return await self.send_command("settings")
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> dict:
# Can only set power limit to tuned preset # Can only set power limit to tuned preset
settings = await self.settings() settings = await self.settings()
settings["miner"]["overclock"]["preset"] = str(wattage) settings["miner"]["overclock"]["preset"] = str(wattage)
new_settings = {"miner": {"overclock": settings["miner"]["overclock"]}} miner = {"overclock": settings["miner"]["overclock"]}
# response will always be {"restart_required":false,"reboot_required":false} even if unsuccessful # response will always be {"restart_required":false,"reboot_required":false} even if unsuccessful
return await self.send_command("settings", privileged=True, **new_settings) return await self.send_command("settings", privileged=True, miner=miner)
async def autotune_presets(self) -> dict: async def autotune_presets(self) -> dict:
return await self.send_command("autotune/presets") return await self.send_command("autotune/presets")

View File

@@ -40,7 +40,7 @@ classifiers = [
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
] ]
requires-python = ">3.9, <4.0" requires-python = ">3.10, <4.0"
dependencies = [ dependencies = [
"httpx>=0.26.0", "httpx>=0.26.0",
"asyncssh>=2.20.0", "asyncssh>=2.20.0",
@@ -59,8 +59,10 @@ dependencies = [
optional = true optional = true
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pre-commit = "^4.0.1" pre-commit = "^4.3.0"
isort = "^5.12.0" ruff = "^0.13.2"
types-passlib = "^1.7.7.20250602"
types-aiofiles = "^24.1.0.20250822"
[tool.poetry.group.docs] [tool.poetry.group.docs]
optional = true optional = true
@@ -75,5 +77,46 @@ mkdocs-material = "^9.5.39"
requires = ["poetry-core>=2.0.0"] requires = ["poetry-core>=2.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.isort] [tool.mypy]
profile = "black" warn_unused_ignores = true
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]]
module = "pyasic.web.braiins_os.proto.*"
disable_error_code = "arg-type"
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
[tool.ruff]
line-length = 88
indent-width = 4
target-version = "py310"
fix = true
unsafe-fixes = true
extend-exclude = ["pyasic/web/braiins_os/proto"]
[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # Pyflakes
"UP", # pyupgrade
"I", # isort
]
fixable = ["ALL"]
ignore = [
"E402",
"E501",
"F401",
"F403",
"F405",
"F601",
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

View File

@@ -7,7 +7,7 @@ class TestFanConfig(unittest.TestCase):
def test_serialize_and_deserialize(self): def test_serialize_and_deserialize(self):
for fan_mode in FanModeConfig: for fan_mode in FanModeConfig:
with self.subTest( with self.subTest(
msg=f"Test serialization and deserialization of fan config", msg="Test serialization and deserialization of fan config",
fan_mode=fan_mode, fan_mode=fan_mode,
): ):
conf = fan_mode() conf = fan_mode()
@@ -17,7 +17,7 @@ class TestFanConfig(unittest.TestCase):
def test_bosminer_deserialize_and_serialize(self): def test_bosminer_deserialize_and_serialize(self):
for fan_mode in FanModeConfig: for fan_mode in FanModeConfig:
with self.subTest( with self.subTest(
msg=f"Test serialization and deserialization of bosminer fan config", msg="Test serialization and deserialization of bosminer fan config",
fan_mode=fan_mode, fan_mode=fan_mode,
): ):
conf = fan_mode() conf = fan_mode()
@@ -27,7 +27,7 @@ class TestFanConfig(unittest.TestCase):
def test_am_modern_deserialize_and_serialize(self): def test_am_modern_deserialize_and_serialize(self):
for fan_mode in FanModeConfig: for fan_mode in FanModeConfig:
with self.subTest( with self.subTest(
msg=f"Test serialization and deserialization of antminer modern fan config", msg="Test serialization and deserialization of antminer modern fan config",
fan_mode=fan_mode, fan_mode=fan_mode,
): ):
conf = fan_mode() conf = fan_mode()
@@ -37,7 +37,7 @@ class TestFanConfig(unittest.TestCase):
def test_epic_deserialize_and_serialize(self): def test_epic_deserialize_and_serialize(self):
for fan_mode in FanModeConfig: for fan_mode in FanModeConfig:
with self.subTest( with self.subTest(
msg=f"Test serialization and deserialization of epic fan config", msg="Test serialization and deserialization of epic fan config",
fan_mode=fan_mode, fan_mode=fan_mode,
): ):
conf = fan_mode() conf = fan_mode()
@@ -47,7 +47,7 @@ class TestFanConfig(unittest.TestCase):
def test_vnish_deserialize_and_serialize(self): def test_vnish_deserialize_and_serialize(self):
for fan_mode in FanModeConfig: for fan_mode in FanModeConfig:
with self.subTest( with self.subTest(
msg=f"Test serialization and deserialization of vnish fan config", msg="Test serialization and deserialization of vnish fan config",
fan_mode=fan_mode, fan_mode=fan_mode,
): ):
conf = fan_mode() conf = fan_mode()
@@ -57,7 +57,7 @@ class TestFanConfig(unittest.TestCase):
def test_auradine_deserialize_and_serialize(self): def test_auradine_deserialize_and_serialize(self):
for fan_mode in FanModeConfig: for fan_mode in FanModeConfig:
with self.subTest( with self.subTest(
msg=f"Test serialization and deserialization of auradine fan config", msg="Test serialization and deserialization of auradine fan config",
fan_mode=fan_mode, fan_mode=fan_mode,
): ):
conf = fan_mode() conf = fan_mode()
@@ -67,7 +67,7 @@ class TestFanConfig(unittest.TestCase):
def test_boser_deserialize_and_serialize(self): def test_boser_deserialize_and_serialize(self):
for fan_mode in FanModeConfig: for fan_mode in FanModeConfig:
with self.subTest( with self.subTest(
msg=f"Test serialization and deserialization of boser fan config", msg="Test serialization and deserialization of boser fan config",
fan_mode=fan_mode, fan_mode=fan_mode,
): ):
conf = fan_mode() conf = fan_mode()

View File

@@ -29,7 +29,7 @@ class MinersTest(unittest.TestCase):
for miner_type in MINER_CLASSES.keys(): for miner_type in MINER_CLASSES.keys():
for miner_model in MINER_CLASSES[miner_type].keys(): for miner_model in MINER_CLASSES[miner_type].keys():
with self.subTest( with self.subTest(
msg=f"Test creation of miner", msg="Test creation of miner",
miner_type=miner_type, miner_type=miner_type,
miner_model=miner_model, miner_model=miner_model,
): ):
@@ -42,7 +42,7 @@ class MinersTest(unittest.TestCase):
if miner_model is None: if miner_model is None:
continue continue
with self.subTest( with self.subTest(
msg=f"Test miner has defined hashboards", msg="Test miner has defined hashboards",
miner_type=miner_type, miner_type=miner_type,
miner_model=miner_model, miner_model=miner_model,
): ):
@@ -56,7 +56,7 @@ class MinersTest(unittest.TestCase):
if miner_model is None: if miner_model is None:
continue continue
with self.subTest( with self.subTest(
msg=f"Test miner has defined fans", msg="Test miner has defined fans",
miner_type=miner_type, miner_type=miner_type,
miner_model=miner_model, miner_model=miner_model,
): ):
@@ -70,7 +70,7 @@ class MinersTest(unittest.TestCase):
if miner_model is None: if miner_model is None:
continue continue
with self.subTest( with self.subTest(
msg=f"Test miner has defined algo", msg="Test miner has defined algo",
miner_type=miner_type, miner_type=miner_type,
miner_model=miner_model, miner_model=miner_model,
): ):
@@ -105,7 +105,7 @@ class MinersTest(unittest.TestCase):
for miner_type in MINER_CLASSES.keys(): for miner_type in MINER_CLASSES.keys():
for miner_model in MINER_CLASSES[miner_type].keys(): for miner_model in MINER_CLASSES[miner_type].keys():
with self.subTest( with self.subTest(
msg=f"Data map key check", msg="Data map key check",
miner_type=miner_type, miner_type=miner_type,
miner_model=miner_model, miner_model=miner_model,
): ):

View File

@@ -50,23 +50,39 @@ class TestAPIBase(unittest.IsolatedAsyncioTestCase):
).encode("utf-8") ).encode("utf-8")
def get_success_value(self, command: str): def get_success_value(self, command: str):
if self.api_str == "BTMiner" and command == "status": if self.api_str == "BTMiner":
return json.dumps( if command == "status":
{ return json.dumps(
"STATUS": "S", {
"When": 1706287567, "STATUS": "S",
"Code": 131, "When": 1706287567,
"Msg": { "Code": 131,
"mineroff": "false", "Msg": {
"mineroff_reason": "", "mineroff": "false",
"mineroff_time": "", "mineroff_reason": "",
"FirmwareVersion": "20230911.12.Rel", "mineroff_time": "",
"power_mode": "", "FirmwareVersion": "20230911.12.Rel",
"hash_percent": "", "power_mode": "",
}, "hash_percent": "",
"Description": "", },
} "Description": "",
).encode("utf-8") }
).encode("utf-8")
elif command == "get_token":
# Return proper token response for BTMiner matching real miner format
return json.dumps(
{
"STATUS": "S",
"When": int(time.time()),
"Code": 134,
"Msg": {
"time": str(int(time.time())),
"salt": "D6w5gVOb", # Valid salt format (alphanumeric only)
"newsalt": "zU4gvW30", # Valid salt format (alphanumeric only)
},
"Description": "",
}
).encode("utf-8")
return json.dumps( return json.dumps(
{ {
"STATUS": [ "STATUS": [
@@ -119,7 +135,33 @@ class TestAPIBase(unittest.IsolatedAsyncioTestCase):
command=command, command=command,
): ):
api_func = getattr(self.api, command) api_func = getattr(self.api, command)
mock_send_bytes.return_value = self.get_success_value(command)
# For BTMiner, we need to handle multiple calls for privileged commands
# Use a list to track calls and return different values
if self.api_str == "BTMiner":
def btminer_side_effect(data):
# Parse the command from the sent data
try:
# data is already bytes
if isinstance(data, bytes):
cmd_str = data.decode("utf-8")
cmd_data = json.loads(cmd_str)
if "cmd" in cmd_data:
sent_cmd = cmd_data["cmd"]
if sent_cmd == "get_token":
# Return proper token response
return self.get_success_value("get_token")
except Exception:
# If we can't parse it, it might be encrypted privileged command
pass
# Default return for the actual command
return self.get_success_value(command)
mock_send_bytes.side_effect = btminer_side_effect
else:
mock_send_bytes.return_value = self.get_success_value(command)
try: try:
await api_func() await api_func()
except APIError: except APIError: