From 1f4054bf389a923e0cb05ded9b071d8e78e97695 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 26 Sep 2025 11:24:38 -0600 Subject: [PATCH] feature: add mypy type checking for better type consistency --- .pre-commit-config.yaml | 24 +- docs/generate_miners.py | 4 +- poetry.lock | 129 +++--- pyasic/config/__init__.py | 47 ++- pyasic/config/base.py | 37 +- pyasic/config/fans.py | 16 +- pyasic/config/mining/__init__.py | 162 ++++--- pyasic/config/mining/algo.py | 20 +- pyasic/config/mining/scaling.py | 10 +- pyasic/config/pools.py | 175 ++++---- pyasic/config/temperature.py | 16 +- pyasic/data/__init__.py | 77 ++-- pyasic/data/boards.py | 10 +- pyasic/data/error_codes/base.py | 14 +- pyasic/data/error_codes/innosilicon.py | 2 +- pyasic/data/error_codes/whatsminer.py | 80 ++-- pyasic/data/fans.py | 2 +- pyasic/data/pools.py | 42 +- pyasic/device/algorithm/hashrate/base.py | 17 +- pyasic/device/algorithm/hashrate/blake256.py | 2 +- pyasic/device/algorithm/hashrate/blockflow.py | 2 +- pyasic/device/algorithm/hashrate/eaglesong.py | 2 +- pyasic/device/algorithm/hashrate/equihash.py | 4 +- pyasic/device/algorithm/hashrate/ethash.py | 2 +- pyasic/device/algorithm/hashrate/handshake.py | 2 +- pyasic/device/algorithm/hashrate/kadena.py | 2 +- .../device/algorithm/hashrate/kheavyhash.py | 2 +- pyasic/device/algorithm/hashrate/scrypt.py | 2 +- pyasic/device/algorithm/hashrate/sha256.py | 2 +- pyasic/device/algorithm/hashrate/unit/base.py | 48 +-- pyasic/device/algorithm/hashrate/x11.py | 2 +- pyasic/device/algorithm/hashrate/zksnark.py | 2 +- pyasic/device/models.py | 7 + pyasic/load/__init__.py | 352 ---------------- pyasic/miners/antminer/epic/X19/S19.py | 6 +- pyasic/miners/antminer/hiveon/X9/T9.py | 23 +- pyasic/miners/antminer/luxos/X21/T21.py | 2 +- pyasic/miners/antminer/vnish/X19/S19.py | 4 - .../miners/avalonminer/cgminer/nano/nano3.py | 25 +- pyasic/miners/backends/antminer.py | 153 ++++--- pyasic/miners/backends/auradine.py | 65 ++- pyasic/miners/backends/avalonminer.py | 67 +-- pyasic/miners/backends/bfgminer.py | 44 +- pyasic/miners/backends/bmminer.py | 48 ++- pyasic/miners/backends/braiins_os.py | 260 +++++++----- pyasic/miners/backends/btminer.py | 243 +++++++---- pyasic/miners/backends/cgminer.py | 25 +- pyasic/miners/backends/elphapex.py | 100 +++-- pyasic/miners/backends/epic.py | 87 ++-- pyasic/miners/backends/espminer.py | 73 ++-- pyasic/miners/backends/goldshell.py | 29 +- pyasic/miners/backends/hammer.py | 88 ++-- pyasic/miners/backends/hiveon.py | 76 ++-- pyasic/miners/backends/iceriver.py | 48 ++- pyasic/miners/backends/innosilicon.py | 81 ++-- pyasic/miners/backends/luxminer.py | 78 +++- pyasic/miners/backends/marathon.py | 71 ++-- pyasic/miners/backends/mskminer.py | 21 +- pyasic/miners/backends/unknown.py | 53 ++- pyasic/miners/backends/vnish.py | 78 ++-- pyasic/miners/base.py | 199 +++++---- pyasic/miners/data.py | 3 +- .../miners/device/models/antminer/X19/S19.py | 6 +- .../device/models/antminer/X19/__init__.py | 2 +- .../miners/device/models/iceriver/__init__.py | 16 + .../device/models/whatsminer/M3X/__init__.py | 1 + pyasic/miners/factory.py | 397 ++++++++++-------- pyasic/miners/goldshell/bfgminer/byte/byte.py | 47 ++- .../goldshell/bfgminer/mini_doge/mini_doge.py | 20 +- pyasic/miners/listener.py | 2 +- pyasic/network/__init__.py | 83 ++-- pyasic/rpc/base.py | 28 +- pyasic/rpc/bfgminer.py | 2 +- pyasic/rpc/btminer.py | 230 ++++++---- pyasic/rpc/cgminer.py | 6 +- pyasic/rpc/luxminer.py | 45 +- pyasic/settings/__init__.py | 78 ++-- pyasic/ssh/base.py | 9 +- pyasic/web/antminer.py | 31 +- pyasic/web/auradine.py | 19 +- pyasic/web/avalonminer.py | 9 +- pyasic/web/base.py | 27 +- pyasic/web/braiins_os/better_monkey.py | 8 +- pyasic/web/braiins_os/boser.py | 142 +++---- pyasic/web/braiins_os/bosminer.py | 2 +- pyasic/web/elphapex.py | 8 +- pyasic/web/epic.py | 29 +- pyasic/web/espminer.py | 38 +- pyasic/web/goldshell.py | 52 ++- pyasic/web/hammer.py | 8 +- pyasic/web/hiveon.py | 43 +- pyasic/web/iceriver.py | 2 +- pyasic/web/innosilicon.py | 37 +- pyasic/web/marathon.py | 22 +- pyasic/web/mskminer.py | 2 +- pyasic/web/vnish.py | 43 +- pyproject.toml | 53 ++- tests/config_tests/fans.py | 14 +- tests/miners_tests/__init__.py | 10 +- tests/rpc_tests/__init__.py | 78 +++- 100 files changed, 2813 insertions(+), 2203 deletions(-) delete mode 100644 pyasic/load/__init__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 884f8d08..b381cb36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,16 +22,24 @@ repos: name: check-yaml for other YAML files exclude: ^mkdocs\.yml$ - id: check-added-large-files -- repo: https://github.com/psf/black - rev: 25.9.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.2 hooks: - - id: black -- repo: https://github.com/pycqa/isort - rev: 6.0.1 + - id: ruff-check + args: [--fix] + - id: ruff-format +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.18.2 hooks: - - id: isort - name: isort (python) - + - id: mypy + additional_dependencies: + [ + betterproto==2.0.0b7, + httpx==0.28.1, + types-aiofiles==24.1.0.20250822, + types-passlib==1.7.7.20250602, + pydantic==2.11.9, + ] - repo: local hooks: - id: unittest diff --git a/docs/generate_miners.py b/docs/generate_miners.py index b41e029c..f2de9e32 100644 --- a/docs/generate_miners.py +++ b/docs/generate_miners.py @@ -1,8 +1,8 @@ -import asyncio import importlib import os import warnings from pathlib import Path +from typing import Any from pyasic.miners.factory import MINER_CLASSES, MinerTypes @@ -128,7 +128,7 @@ BACKEND_TYPE_CLOSER = """ """ -m_data = {} +m_data: dict[str, dict[str, list[type[Any]]]] = {} done = [] for m in MINER_CLASSES: diff --git a/poetry.lock b/poetry.lock index 45badd64..5aa16459 100644 --- a/poetry.lock +++ b/poetry.lock @@ -638,46 +638,6 @@ files = [ [package.extras] 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]] name = "jinja2" version = "3.1.6" @@ -708,9 +668,6 @@ files = [ {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, ] -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - [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]"] testing = ["coverage", "pyyaml"] @@ -814,7 +771,6 @@ files = [ click = ">=7.0" colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" markdown = ">=3.3.6" markupsafe = ">=2.0.1" @@ -860,7 +816,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} mergedeep = ">=1.3.4" platformdirs = ">=2.2.0" pyyaml = ">=5.1" @@ -922,7 +877,6 @@ files = [ [package.dependencies] click = ">=7.0" -importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} Jinja2 = ">=2.11.1" Markdown = ">=3.6" MarkupSafe = ">=1.1" @@ -931,7 +885,6 @@ mkdocs-autorefs = ">=1.2" mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} platformdirs = ">=2.2" pymdown-extensions = ">=6.3" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} [package.extras] crystal = ["mkdocstrings-crystal (>=0.3.4)"] @@ -1498,6 +1451,35 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] 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]] name = "semver" version = "3.0.4" @@ -1589,18 +1571,42 @@ files = [ {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]] name = "typing-extensions" version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {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]] name = "typing-inspection" @@ -1700,28 +1706,7 @@ files = [ [package.extras] 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] lock-version = "2.1" -python-versions = ">3.9, <4.0" -content-hash = "81ec4faceddb41badda1649e77ddcfba03b0275021ba37ba69290b7e6a326829" +python-versions = ">3.10, <4.0" +content-hash = "ab448bfd6e29c1017aa7bc9713f6d2a3a2985106d023122729bb74f4f6e0a609" diff --git a/pyasic/config/__init__.py b/pyasic/config/__init__.py index cc852f06..4910e2ad 100644 --- a/pyasic/config/__init__.py +++ b/pyasic/config/__init__.py @@ -14,10 +14,41 @@ # limitations under the License. - # ------------------------------------------------------------------------------ +from typing import Any + from pydantic import BaseModel, Field -from pyasic.config.fans import FanMode, FanModeConfig, FanModeNormal -from pyasic.config.mining import MiningMode, MiningModeConfig +from pyasic.config.fans import ( + FanModeConfig, + FanModeImmersion, + FanModeManual, + FanModeNormal, +) +from pyasic.config.mining import ( + MiningModeConfig, + MiningModeHashrateTune, + MiningModeHPM, + MiningModeLPM, + MiningModeManual, + MiningModeNormal, + MiningModePowerTune, + MiningModePreset, + MiningModeSleep, +) + +# Type aliases for config field types +FanModeType = FanModeNormal | FanModeManual | FanModeImmersion | FanModeConfig +MiningModeType = ( + MiningModeNormal + | MiningModeHPM + | MiningModeLPM + | MiningModeSleep + | MiningModeManual + | MiningModePowerTune + | MiningModeHashrateTune + | MiningModePreset + | MiningModeConfig +) from pyasic.config.mining.scaling import ScalingConfig from pyasic.config.pools import PoolConfig from pyasic.config.temperature import TemperatureConfig @@ -32,11 +63,11 @@ class MinerConfig(BaseModel): arbitrary_types_allowed = True pools: PoolConfig = Field(default_factory=PoolConfig.default) - fan_mode: FanMode = Field(default_factory=FanModeConfig.default) + fan_mode: FanModeType = Field(default_factory=FanModeConfig.default) temperature: TemperatureConfig = Field(default_factory=TemperatureConfig.default) - mining_mode: MiningMode = Field(default_factory=MiningModeConfig.default) + mining_mode: MiningModeType = Field(default_factory=MiningModeConfig.default) - def __getitem__(self, item): + def __getitem__(self, item: str) -> Any: try: return getattr(self, item) except AttributeError: @@ -88,8 +119,8 @@ class MinerConfig(BaseModel): def as_btminer_v3(self, user_suffix: str | None = None) -> dict: """Generates the configuration in the format suitable for Whatsminers running BTMiner V3.""" return { - "set.miner.pools": self.pools.as_btminer_v3() - ** self.mining_mode.as_btminer_v3() + "set.miner.pools": self.pools.as_btminer_v3(), + **self.mining_mode.as_btminer_v3(), } def as_am_old(self, user_suffix: str | None = None) -> dict: @@ -260,7 +291,7 @@ class MinerConfig(BaseModel): return cls(pools=PoolConfig.from_goldshell(web_conf)) @classmethod - def from_goldshell_byte(cls, web_conf: dict) -> "MinerConfig": + def from_goldshell_byte(cls, web_conf: list) -> "MinerConfig": """Constructs a MinerConfig object from web configuration for Goldshell Byte miners.""" return cls(pools=PoolConfig.from_goldshell_byte(web_conf)) diff --git a/pyasic/config/base.py b/pyasic/config/base.py index 6689cd1a..ec2dfd07 100644 --- a/pyasic/config/base.py +++ b/pyasic/config/base.py @@ -16,6 +16,7 @@ from __future__ import annotations from enum import Enum +from typing import Any from pydantic import BaseModel @@ -89,61 +90,61 @@ class MinerConfigOption(Enum): class MinerConfigValue(BaseModel): @classmethod - def from_dict(cls, dict_conf: dict | None): + def from_dict(cls, dict_conf: dict): return cls() def as_dict(self) -> dict: return self.model_dump() - def as_am_modern(self) -> dict: + def as_am_modern(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_hiveon_modern(self) -> dict: + def as_hiveon_modern(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_am_old(self) -> dict: + def as_am_old(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_wm(self) -> dict: + def as_wm(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_btminer_v3(self) -> dict: + def as_btminer_v3(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_inno(self) -> dict: + def as_inno(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_goldshell(self) -> dict: + def as_goldshell(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_avalon(self) -> dict: + def as_avalon(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_bosminer(self) -> dict: + def as_bosminer(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_boser(self) -> dict: + def as_boser(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_epic(self) -> dict: + def as_epic(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_vnish(self) -> dict: + def as_vnish(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_auradine(self) -> dict: + def as_auradine(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_mara(self) -> dict: + def as_mara(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_espminer(self) -> dict: + def as_espminer(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_luxos(self) -> dict: + def as_luxos(self, *args: Any, **kwargs: Any) -> Any: return {} - def as_elphapex(self) -> dict: + def as_elphapex(self, *args: Any, **kwargs: Any) -> Any: return {} def __getitem__(self, item): diff --git a/pyasic/config/fans.py b/pyasic/config/fans.py index aaa4af21..dc0ecee2 100644 --- a/pyasic/config/fans.py +++ b/pyasic/config/fans.py @@ -15,7 +15,7 @@ # ------------------------------------------------------------------------------ from __future__ import annotations -from typing import TypeVar, Union +from typing import TypeVar from pydantic import Field @@ -28,7 +28,7 @@ class FanModeNormal(MinerConfigValue): minimum_speed: int = 0 @classmethod - def from_dict(cls, dict_conf: dict | None) -> "FanModeNormal": + def from_dict(cls, dict_conf: dict) -> FanModeNormal: cls_conf = {} if dict_conf.get("minimum_fans") is not None: cls_conf["minimum_fans"] = dict_conf["minimum_fans"] @@ -37,7 +37,7 @@ class FanModeNormal(MinerConfigValue): return cls(**cls_conf) @classmethod - def from_vnish(cls, web_cooling_settings: dict) -> "FanModeNormal": + def from_vnish(cls, web_cooling_settings: dict) -> FanModeNormal: cls_conf = {} if web_cooling_settings.get("fan_min_count") is not None: cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"] @@ -112,7 +112,7 @@ class FanModeManual(MinerConfigValue): minimum_fans: int = 1 @classmethod - def from_dict(cls, dict_conf: dict | None) -> "FanModeManual": + def from_dict(cls, dict_conf: dict) -> FanModeManual: cls_conf = {} if dict_conf.get("speed") is not None: cls_conf["speed"] = dict_conf["speed"] @@ -121,7 +121,7 @@ class FanModeManual(MinerConfigValue): return cls(**cls_conf) @classmethod - def from_bosminer(cls, toml_fan_conf: dict) -> "FanModeManual": + def from_bosminer(cls, toml_fan_conf: dict) -> FanModeManual: cls_conf = {} if toml_fan_conf.get("min_fans") is not None: cls_conf["minimum_fans"] = toml_fan_conf["min_fans"] @@ -130,7 +130,7 @@ class FanModeManual(MinerConfigValue): return cls(**cls_conf) @classmethod - def from_vnish(cls, web_cooling_settings: dict) -> "FanModeManual": + def from_vnish(cls, web_cooling_settings: dict) -> FanModeManual: cls_conf = {} if web_cooling_settings.get("fan_min_count") is not None: cls_conf["minimum_fans"] = web_cooling_settings["fan_min_count"] @@ -191,7 +191,7 @@ class FanModeImmersion(MinerConfigValue): mode: str = Field(init=False, default="immersion") @classmethod - def from_dict(cls, dict_conf: dict | None) -> "FanModeImmersion": + def from_dict(cls, dict_conf: dict | None) -> FanModeImmersion: return cls() def as_am_modern(self) -> dict: @@ -417,5 +417,5 @@ class FanModeConfig(MinerConfigOption): FanMode = TypeVar( "FanMode", - bound=Union[FanModeNormal, FanModeManual, FanModeImmersion], + bound=FanModeNormal | FanModeManual | FanModeImmersion, ) diff --git a/pyasic/config/mining/__init__.py b/pyasic/config/mining/__init__.py index 3cbf2440..c2899ed8 100644 --- a/pyasic/config/mining/__init__.py +++ b/pyasic/config/mining/__init__.py @@ -16,7 +16,7 @@ from __future__ import annotations from dataclasses import field -from typing import TypeVar, Union +from typing import Any, TypeVar from pyasic import settings from pyasic.config.base import MinerConfigOption, MinerConfigValue @@ -35,7 +35,14 @@ from pyasic.web.braiins_os.proto.braiins.bos.v1 import ( TunerPerformanceMode, ) -from .algo import TunerAlgo, TunerAlgoType +from .algo import ( + BoardTuneAlgo, + ChipTuneAlgo, + StandardTuneAlgo, + TunerAlgo, + TunerAlgoType, + VOptAlgo, +) from .presets import MiningPreset from .scaling import ScalingConfig @@ -44,7 +51,7 @@ class MiningModeNormal(MinerConfigValue): mode: str = field(init=False, default="normal") @classmethod - def from_dict(cls, dict_conf: dict | None) -> "MiningModeNormal": + def from_dict(cls, dict_conf: dict | None) -> MiningModeNormal: return cls() def as_am_modern(self) -> dict: @@ -93,7 +100,7 @@ class MiningModeSleep(MinerConfigValue): mode: str = field(init=False, default="sleep") @classmethod - def from_dict(cls, dict_conf: dict | None) -> "MiningModeSleep": + def from_dict(cls, dict_conf: dict | None) -> MiningModeSleep: return cls() def as_am_modern(self) -> dict: @@ -136,7 +143,7 @@ class MiningModeLPM(MinerConfigValue): mode: str = field(init=False, default="low") @classmethod - def from_dict(cls, dict_conf: dict | None) -> "MiningModeLPM": + def from_dict(cls, dict_conf: dict | None) -> MiningModeLPM: return cls() def as_am_modern(self) -> dict: @@ -169,7 +176,7 @@ class MiningModeHPM(MinerConfigValue): mode: str = field(init=False, default="high") @classmethod - def from_dict(cls, dict_conf: dict | None) -> "MiningModeHPM": + def from_dict(cls, dict_conf: dict | None) -> MiningModeHPM: return cls() def as_am_modern(self) -> dict: @@ -201,11 +208,15 @@ class MiningModePowerTune(MinerConfigValue): mode: str = field(init=False, default="power_tuning") power: int | None = None - algo: TunerAlgoType = field(default_factory=TunerAlgo.default) + algo: StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo = field( + default_factory=TunerAlgo.default + ) scaling: ScalingConfig | None = None @classmethod - def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune": + def from_dict(cls, dict_conf: dict | None) -> MiningModePowerTune: + if dict_conf is None: + return cls() cls_conf = {} if dict_conf.get("power"): cls_conf["power"] = dict_conf["power"] @@ -245,25 +256,27 @@ class MiningModePowerTune(MinerConfigValue): cfg = {"autotuning": tuning_cfg} if self.scaling is not None: - scaling_cfg = {"enabled": True} + scaling_cfg: dict[str, Any] = {"enabled": True} if self.scaling.step is not None: scaling_cfg["power_step"] = self.scaling.step if self.scaling.minimum is not None: scaling_cfg["min_power_target"] = self.scaling.minimum if self.scaling.shutdown is not None: - scaling_cfg = {**scaling_cfg, **self.scaling.shutdown.as_bosminer()} + scaling_cfg.update(self.scaling.shutdown.as_bosminer()) cfg["performance_scaling"] = scaling_cfg return cfg def as_boser(self) -> dict: - cfg = { + cfg: dict[str, Any] = { "set_performance_mode": SetPerformanceModeRequest( - save_action=SaveAction.SAVE_AND_APPLY, + save_action=SaveAction(SaveAction.SAVE_AND_APPLY), mode=PerformanceMode( tuner_mode=TunerPerformanceMode( power_target=PowerTargetMode( power_target=Power(watt=self.power) + if self.power is not None + else None # type: ignore[arg-type] ) ) ), @@ -273,13 +286,15 @@ class MiningModePowerTune(MinerConfigValue): sd_cfg = {} if self.scaling.shutdown is not None: sd_cfg = self.scaling.shutdown.as_boser() - power_target_kwargs = {} + power_target_kwargs: dict[str, Any] = {} if self.scaling.step is not None: - power_target_kwargs["power_step"] = Power(self.scaling.step) + power_target_kwargs["power_step"] = Power(watt=self.scaling.step) if self.scaling.minimum is not None: - power_target_kwargs["min_power_target"] = Power(self.scaling.minimum) + power_target_kwargs["min_power_target"] = Power( + watt=self.scaling.minimum + ) cfg["set_dps"] = SetDpsRequest( - save_action=SaveAction.SAVE_AND_APPLY, + save_action=SaveAction(SaveAction.SAVE_AND_APPLY), enable=True, **sd_cfg, target=DpsTarget(power_target=DpsPowerTarget(**power_target_kwargs)), @@ -311,11 +326,15 @@ class MiningModeHashrateTune(MinerConfigValue): mode: str = field(init=False, default="hashrate_tuning") hashrate: int | None = None - algo: TunerAlgoType = field(default_factory=TunerAlgo.default) + algo: StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo = field( + default_factory=TunerAlgo.default + ) scaling: ScalingConfig | None = None @classmethod - def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune": + def from_dict(cls, dict_conf: dict | None) -> MiningModeHashrateTune: + if dict_conf is None: + return cls() cls_conf = {} if dict_conf.get("hashrate"): cls_conf["hashrate"] = dict_conf["hashrate"] @@ -346,14 +365,16 @@ class MiningModeHashrateTune(MinerConfigValue): return {"autotuning": conf} def as_boser(self) -> dict: - cfg = { + cfg: dict[str, Any] = { "set_performance_mode": SetPerformanceModeRequest( - save_action=SaveAction.SAVE_AND_APPLY, + save_action=SaveAction(SaveAction.SAVE_AND_APPLY), mode=PerformanceMode( tuner_mode=TunerPerformanceMode( hashrate_target=HashrateTargetMode( hashrate_target=TeraHashrate( - terahash_per_second=self.hashrate + terahash_per_second=float(self.hashrate) + if self.hashrate is not None + else None # type: ignore[arg-type] ) ) ) @@ -364,17 +385,17 @@ class MiningModeHashrateTune(MinerConfigValue): sd_cfg = {} if self.scaling.shutdown is not None: sd_cfg = self.scaling.shutdown.as_boser() - hashrate_target_kwargs = {} + hashrate_target_kwargs: dict[str, Any] = {} if self.scaling.step is not None: hashrate_target_kwargs["hashrate_step"] = TeraHashrate( - self.scaling.step + terahash_per_second=float(self.scaling.step) ) if self.scaling.minimum is not None: hashrate_target_kwargs["min_hashrate_target"] = TeraHashrate( - self.scaling.minimum + terahash_per_second=float(self.scaling.minimum) ) cfg["set_dps"] = SetDpsRequest( - save_action=SaveAction.SAVE_AND_APPLY, + save_action=SaveAction(SaveAction.SAVE_AND_APPLY), enable=True, **sd_cfg, target=DpsTarget( @@ -390,7 +411,11 @@ class MiningModeHashrateTune(MinerConfigValue): def as_epic(self) -> dict: mode = { "ptune": { - "algo": self.algo.as_epic(), + "algo": ( + self.algo.as_epic() + if hasattr(self.algo, "as_epic") + else TunerAlgo.default().as_epic() + ), "target": self.hashrate, } } @@ -431,7 +456,7 @@ class MiningModePreset(MinerConfigValue): web_overclock_settings: dict, web_presets: list[dict], web_perf_summary: dict, - ) -> "MiningModePreset": + ) -> MiningModePreset: active_preset = web_perf_summary.get("current_preset") if active_preset is None: @@ -440,12 +465,12 @@ class MiningModePreset(MinerConfigValue): active_preset = preset return cls( - active_preset=MiningPreset.from_vnish(active_preset), + active_preset=MiningPreset.from_vnish(active_preset or {}), available_presets=[MiningPreset.from_vnish(p) for p in web_presets], ) @classmethod - def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> "MiningModePreset": + def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> MiningModePreset: active_preset = cls.get_active_preset_from_luxos(rpc_config, rpc_profiles) return cls( active_preset=active_preset, @@ -463,7 +488,7 @@ class MiningModePreset(MinerConfigValue): for profile in rpc_profiles["PROFILES"]: if profile["Profile Name"] == active_profile: active_preset = profile - return MiningPreset.from_luxos(active_preset) + return MiningPreset.from_luxos(active_preset or {}) class ManualBoardSettings(MinerConfigValue): @@ -471,7 +496,7 @@ class ManualBoardSettings(MinerConfigValue): volt: float @classmethod - def from_dict(cls, dict_conf: dict | None) -> "ManualBoardSettings": + def from_dict(cls, dict_conf: dict) -> ManualBoardSettings: return cls(freq=dict_conf["freq"], volt=dict_conf["volt"]) def as_am_modern(self) -> dict: @@ -499,11 +524,15 @@ class MiningModeManual(MinerConfigValue): boards: dict[int, ManualBoardSettings] = field(default_factory=dict) @classmethod - def from_dict(cls, dict_conf: dict | None) -> "MiningModeManual": + def from_dict(cls, dict_conf: dict) -> MiningModeManual: return cls( global_freq=dict_conf["global_freq"], global_volt=dict_conf["global_volt"], - boards={i: ManualBoardSettings.from_dict(dict_conf[i]) for i in dict_conf}, + boards={ + i: ManualBoardSettings.from_dict(dict_conf[i]) + for i in dict_conf + if isinstance(i, int) + }, ) def as_am_modern(self) -> dict: @@ -527,7 +556,7 @@ class MiningModeManual(MinerConfigValue): } @classmethod - def from_vnish(cls, web_overclock_settings: dict) -> "MiningModeManual": + def from_vnish(cls, web_overclock_settings: dict) -> MiningModeManual: # will raise KeyError if it cant find the settings, values cannot be empty voltage = web_overclock_settings["globals"]["volt"] freq = web_overclock_settings["globals"]["freq"] @@ -541,7 +570,7 @@ class MiningModeManual(MinerConfigValue): return cls(global_freq=freq, global_volt=voltage, boards=boards) @classmethod - def from_epic(cls, epic_conf: dict) -> "MiningModeManual": + def from_epic(cls, epic_conf: dict) -> MiningModeManual: voltage = 0 freq = 0 if epic_conf.get("HwConfig") is not None: @@ -581,11 +610,11 @@ class MiningModeConfig(MinerConfigOption): manual = MiningModeManual @classmethod - def default(cls): + def default(cls) -> MiningModeConfig: return cls.normal() @classmethod - def from_dict(cls, dict_conf: dict | None): + def from_dict(cls, dict_conf: dict | None) -> MiningModeConfig: if dict_conf is None: return cls.default() @@ -593,12 +622,13 @@ class MiningModeConfig(MinerConfigOption): if mode is None: return cls.default() - cls_attr = getattr(cls, mode) + cls_attr = getattr(cls, mode, None) if cls_attr is not None: return cls_attr().from_dict(dict_conf) + return cls.default() @classmethod - def from_am_modern(cls, web_conf: dict): + def from_am_modern(cls, web_conf: dict) -> MiningModeConfig: if web_conf.get("bitmain-work-mode") is not None: work_mode = web_conf["bitmain-work-mode"] if work_mode == "": @@ -612,7 +642,7 @@ class MiningModeConfig(MinerConfigOption): return cls.default() @classmethod - def from_hiveon_modern(cls, web_conf: dict): + def from_hiveon_modern(cls, web_conf: dict) -> MiningModeConfig: if web_conf.get("bitmain-work-mode") is not None: work_mode = web_conf["bitmain-work-mode"] if work_mode == "": @@ -626,7 +656,7 @@ class MiningModeConfig(MinerConfigOption): return cls.default() @classmethod - def from_elphapex(cls, web_conf: dict): + def from_elphapex(cls, web_conf: dict) -> MiningModeConfig: if web_conf.get("fc-work-mode") is not None: work_mode = web_conf["fc-work-mode"] if work_mode == "": @@ -640,7 +670,7 @@ class MiningModeConfig(MinerConfigOption): return cls.default() @classmethod - def from_epic(cls, web_conf: dict): + def from_epic(cls, web_conf: dict) -> MiningModeConfig: try: tuner_running = web_conf["PerpetualTune"]["Running"] if tuner_running: @@ -679,12 +709,12 @@ class MiningModeConfig(MinerConfigOption): algo=TunerAlgo.chip_tune(), ) else: - return MiningModeManual.from_epic(web_conf) + return cls.manual.from_epic(web_conf) except KeyError: return cls.default() @classmethod - def from_bosminer(cls, toml_conf: dict): + def from_bosminer(cls, toml_conf: dict) -> MiningModeConfig: if toml_conf.get("autotuning") is None: return cls.default() autotuning_conf = toml_conf["autotuning"] @@ -726,21 +756,19 @@ class MiningModeConfig(MinerConfigOption): @classmethod def from_vnish( cls, web_settings: dict, web_presets: list[dict], web_perf_summary: dict - ): + ) -> MiningModeConfig: try: mode_settings = web_settings["miner"]["overclock"] except KeyError: return cls.default() if mode_settings["preset"] == "disabled": - return MiningModeManual.from_vnish(mode_settings) + return cls.manual.from_vnish(mode_settings, web_presets, web_perf_summary) else: - return MiningModePreset.from_vnish( - mode_settings, web_presets, web_perf_summary - ) + return cls.preset.from_vnish(mode_settings, web_presets, web_perf_summary) @classmethod - def from_boser(cls, grpc_miner_conf: dict): + def from_boser(cls, grpc_miner_conf: dict) -> MiningModeConfig: try: tuner_conf = grpc_miner_conf["tuner"] if not tuner_conf.get("enabled", False): @@ -786,7 +814,7 @@ class MiningModeConfig(MinerConfigOption): return cls.default() @classmethod - def from_auradine(cls, web_mode: dict): + def from_auradine(cls, web_mode: dict) -> MiningModeConfig: try: mode_data = web_mode["Mode"][0] if mode_data.get("Sleep") == "on": @@ -803,9 +831,12 @@ class MiningModeConfig(MinerConfigOption): return cls.power_tuning(power=mode_data["Power"]) except LookupError: return cls.default() + return cls.default() @classmethod - def from_btminer_v3(cls, rpc_device_info: dict, rpc_settings: dict): + def from_btminer_v3( + cls, rpc_device_info: dict, rpc_settings: dict + ) -> MiningModeConfig: try: is_mining = rpc_device_info["msg"]["miner"]["working"] == "true" if not is_mining: @@ -823,9 +854,10 @@ class MiningModeConfig(MinerConfigOption): except LookupError: return cls.default() + return cls.default() @classmethod - def from_mara(cls, web_config: dict): + def from_mara(cls, web_config: dict) -> MiningModeConfig: try: mode = web_config["mode"]["work-mode-selector"] if mode == "Fixed": @@ -850,24 +882,26 @@ class MiningModeConfig(MinerConfigOption): return cls.default() @classmethod - def from_luxos(cls, rpc_config: dict, rpc_profiles: dict): + def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> MiningModeConfig: preset_info = MiningModePreset.from_luxos(rpc_config, rpc_profiles) return cls.preset( active_preset=preset_info.active_preset, available_presets=preset_info.available_presets, ) + def as_btminer_v3(self) -> dict: + """Delegate to the default instance for btminer v3 configuration.""" + return self.default().as_btminer_v3() + MiningMode = TypeVar( "MiningMode", - bound=Union[ - MiningModeNormal, - MiningModeHPM, - MiningModeLPM, - MiningModeSleep, - MiningModeManual, - MiningModePowerTune, - MiningModeHashrateTune, - MiningModePreset, - ], + bound=MiningModeNormal + | MiningModeHPM + | MiningModeLPM + | MiningModeSleep + | MiningModeManual + | MiningModePowerTune + | MiningModeHashrateTune + | MiningModePreset, ) diff --git a/pyasic/config/mining/algo.py b/pyasic/config/mining/algo.py index a70b6398..5e7d35c9 100644 --- a/pyasic/config/mining/algo.py +++ b/pyasic/config/mining/algo.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import field -from typing import TypeVar, Union +from typing import Any, TypeVar from pyasic.config.base import MinerConfigOption, MinerConfigValue @@ -41,26 +41,26 @@ class TunerAlgo(MinerConfigOption): chip_tune = ChipTuneAlgo @classmethod - def default(cls) -> TunerAlgoType: + def default(cls) -> StandardTuneAlgo: return cls.standard() @classmethod - def from_dict(cls, dict_conf: dict | None) -> TunerAlgoType: + def from_dict( + cls, dict_conf: dict[Any, Any] | None + ) -> StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo: + if dict_conf is None: + return cls.default() mode = dict_conf.get("mode") if mode is None: return cls.default() - cls_attr = getattr(cls, mode) + cls_attr = getattr(cls, mode, None) if cls_attr is not None: return cls_attr().from_dict(dict_conf) + return cls.default() TunerAlgoType = TypeVar( "TunerAlgoType", - bound=Union[ - StandardTuneAlgo, - VOptAlgo, - BoardTuneAlgo, - ChipTuneAlgo, - ], + bound=StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo, ) diff --git a/pyasic/config/mining/scaling.py b/pyasic/config/mining/scaling.py index 710c773c..2023bf65 100644 --- a/pyasic/config/mining/scaling.py +++ b/pyasic/config/mining/scaling.py @@ -23,7 +23,9 @@ class ScalingShutdown(MinerConfigValue): duration: int | None = None @classmethod - def from_dict(cls, dict_conf: dict | None) -> "ScalingShutdown": + def from_dict(cls, dict_conf: dict | None) -> ScalingShutdown: + if dict_conf is None: + return cls() return cls( enabled=dict_conf.get("enabled", False), duration=dict_conf.get("duration") ) @@ -51,7 +53,7 @@ class ScalingShutdown(MinerConfigValue): return None def as_bosminer(self) -> dict: - cfg = {"shutdown_enabled": self.enabled} + cfg: dict[str, bool | int] = {"shutdown_enabled": self.enabled} if self.duration is not None: cfg["shutdown_duration"] = self.duration @@ -68,7 +70,9 @@ class ScalingConfig(MinerConfigValue): shutdown: ScalingShutdown | None = None @classmethod - def from_dict(cls, dict_conf: dict | None) -> "ScalingConfig": + def from_dict(cls, dict_conf: dict | None) -> ScalingConfig: + if dict_conf is None: + return cls() cls_conf = { "step": dict_conf.get("step"), "minimum": dict_conf.get("minimum"), diff --git a/pyasic/config/pools.py b/pyasic/config/pools.py index cd84faf5..e4388256 100644 --- a/pyasic/config/pools.py +++ b/pyasic/config/pools.py @@ -17,7 +17,7 @@ from __future__ import annotations import random import string -from typing import List +from typing import Any from pydantic import Field @@ -66,12 +66,15 @@ class Pool(MinerConfigValue): def as_btminer_v3(self, user_suffix: str | None = None) -> dict: return { - f"pool": self.url, - f"worker": f"{self.user}{user_suffix or ''}", - f"passwd": self.password, + "pool": self.url, + "worker": f"{self.user}{user_suffix or ''}", + "passwd": self.password, } - def as_am_old(self, idx: int = 1, user_suffix: str | None = None) -> dict: + def as_am_old( + self, *args: Any, user_suffix: str | None = None, **kwargs: Any + ) -> dict: + idx = args[0] if args else kwargs.get("idx", 1) return { f"_ant_pool{idx}url": self.url, f"_ant_pool{idx}user": f"{self.user}{user_suffix or ''}", @@ -88,7 +91,10 @@ class Pool(MinerConfigValue): def as_avalon(self, user_suffix: str | None = None) -> str: return ",".join([self.url, f"{self.user}{user_suffix or ''}", self.password]) - def as_inno(self, idx: int = 1, user_suffix: str | None = None) -> dict: + def as_inno( + self, *args: Any, user_suffix: str | None = None, **kwargs: Any + ) -> dict: + idx = args[0] if args else kwargs.get("idx", 1) return { f"Pool{idx}": self.url, f"UserName{idx}": f"{self.user}{user_suffix or ''}", @@ -109,7 +115,7 @@ class Pool(MinerConfigValue): "pass": self.password, } - def as_epic(self, user_suffix: str | None = None): + def as_epic(self, user_suffix: str | None = None) -> dict: return { "pool": self.url, "login": f"{self.user}{user_suffix or ''}", @@ -146,58 +152,60 @@ class Pool(MinerConfigValue): } @classmethod - def from_dict(cls, dict_conf: dict | None) -> "Pool": + def from_dict(cls, dict_conf: dict | None) -> Pool: + if dict_conf is None: + raise ValueError("dict_conf cannot be None") return cls( url=dict_conf["url"], user=dict_conf["user"], password=dict_conf["password"] ) @classmethod - def from_api(cls, api_pool: dict) -> "Pool": + def from_api(cls, api_pool: dict) -> Pool: return cls(url=api_pool["URL"], user=api_pool["User"], password="x") @classmethod - def from_btminer_v3(cls, api_pool: dict) -> "Pool": + def from_btminer_v3(cls, api_pool: dict) -> Pool: return cls(url=api_pool["url"], user=api_pool["account"], password="x") @classmethod - def from_epic(cls, api_pool: dict) -> "Pool": + def from_epic(cls, api_pool: dict) -> Pool: return cls( url=api_pool["pool"], user=api_pool["login"], password=api_pool["password"] ) @classmethod - def from_am_modern(cls, web_pool: dict) -> "Pool": + def from_am_modern(cls, web_pool: dict) -> Pool: return cls( url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] ) @classmethod - def from_hiveon_modern(cls, web_pool: dict) -> "Pool": + def from_hiveon_modern(cls, web_pool: dict) -> Pool: return cls( url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] ) @classmethod - def from_elphapex(cls, web_pool: dict) -> "Pool": + def from_elphapex(cls, web_pool: dict) -> Pool: return cls( url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] ) # TODO: check if this is accurate, user/username, pass/password @classmethod - def from_goldshell(cls, web_pool: dict) -> "Pool": + def from_goldshell(cls, web_pool: dict) -> Pool: return cls( url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] ) @classmethod - def from_inno(cls, web_pool: dict) -> "Pool": + def from_inno(cls, web_pool: dict) -> Pool: return cls( url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] ) @classmethod - def from_bosminer(cls, toml_pool_conf: dict) -> "Pool": + def from_bosminer(cls, toml_pool_conf: dict) -> Pool: return cls( url=toml_pool_conf["url"], user=toml_pool_conf["user"], @@ -205,7 +213,7 @@ class Pool(MinerConfigValue): ) @classmethod - def from_vnish(cls, web_pool: dict) -> "Pool": + def from_vnish(cls, web_pool: dict) -> Pool: return cls( url="stratum+tcp://" + web_pool["url"], user=web_pool["user"], @@ -213,7 +221,7 @@ class Pool(MinerConfigValue): ) @classmethod - def from_boser(cls, grpc_pool: dict) -> "Pool": + def from_boser(cls, grpc_pool: dict) -> Pool: return cls( url=grpc_pool["url"], user=grpc_pool["user"], @@ -221,7 +229,7 @@ class Pool(MinerConfigValue): ) @classmethod - def from_mara(cls, web_pool: dict) -> "Pool": + def from_mara(cls, web_pool: dict) -> Pool: return cls( url=web_pool["url"], user=web_pool["user"], @@ -229,7 +237,7 @@ class Pool(MinerConfigValue): ) @classmethod - def from_espminer(cls, web_system_info: dict) -> "Pool": + def from_espminer(cls, web_system_info: dict) -> Pool: url = f"stratum+tcp://{web_system_info['stratumURL']}:{web_system_info['stratumPort']}" return cls( url=url, @@ -238,11 +246,11 @@ class Pool(MinerConfigValue): ) @classmethod - def from_luxos(cls, rpc_pools: dict) -> "Pool": + def from_luxos(cls, rpc_pools: dict) -> Pool: return cls.from_api(rpc_pools) @classmethod - def from_iceriver(cls, web_pool: dict) -> "Pool": + def from_iceriver(cls, web_pool: dict) -> Pool: return cls( url=web_pool["addr"], user=web_pool["user"], @@ -294,34 +302,32 @@ class PoolGroup(MinerConfigValue): idx += 1 return pools - def as_wm(self, user_suffix: str | None = None) -> dict: - pools = {} + def as_wm(self, *args: Any, user_suffix: str | None = None, **kwargs: Any) -> dict: + pools: dict[str, str] = {} idx = 0 while idx < 3: if len(self.pools) > idx: - pools.update( - **self.pools[idx].as_wm(idx=idx + 1, user_suffix=user_suffix) - ) + pools.update(**self.pools[idx].as_wm(idx + 1, user_suffix=user_suffix)) else: - pools.update(**Pool(url="", user="", password="").as_wm(idx=idx + 1)) + pools.update(**Pool(url="", user="", password="").as_wm(idx + 1)) idx += 1 return pools def as_btminer_v3(self, user_suffix: str | None = None) -> list: return [pool.as_btminer_v3(user_suffix) for pool in self.pools[:3]] - def as_am_old(self, user_suffix: str | None = None) -> dict: - pools = {} + def as_am_old( + self, *args: Any, user_suffix: str | None = None, **kwargs: Any + ) -> dict: + pools: dict[str, str] = {} idx = 0 while idx < 3: if len(self.pools) > idx: pools.update( - **self.pools[idx].as_am_old(idx=idx + 1, user_suffix=user_suffix) + **self.pools[idx].as_am_old(idx + 1, user_suffix=user_suffix) ) else: - pools.update( - **Pool(url="", user="", password="").as_am_old(idx=idx + 1) - ) + pools.update(**Pool(url="", user="", password="").as_am_old(idx + 1)) idx += 1 return pools @@ -333,22 +339,24 @@ class PoolGroup(MinerConfigValue): return self.pools[0].as_avalon(user_suffix=user_suffix) return Pool(url="", user="", password="").as_avalon() - def as_inno(self, user_suffix: str | None = None) -> dict: - pools = {} + def as_inno( + self, *args: Any, user_suffix: str | None = None, **kwargs: Any + ) -> dict: + pools: dict[str, str] = {} idx = 0 while idx < 3: if len(self.pools) > idx: pools.update( - **self.pools[idx].as_inno(idx=idx + 1, user_suffix=user_suffix) + **self.pools[idx].as_inno(idx + 1, user_suffix=user_suffix) ) else: - pools.update(**Pool(url="", user="", password="").as_inno(idx=idx + 1)) + pools.update(**Pool(url="", user="", password="").as_inno(idx + 1)) idx += 1 return pools def as_bosminer(self, user_suffix: str | None = None) -> dict: if len(self.pools) > 0: - conf = { + conf: dict[str, Any] = { "name": self.name, "pool": [ pool.as_bosminer(user_suffix=user_suffix) for pool in self.pools @@ -373,7 +381,7 @@ class PoolGroup(MinerConfigValue): def as_boser(self, user_suffix: str | None = None) -> PoolGroupConfiguration: return PoolGroupConfiguration( - name=self.name, + name=self.name or "", quota=Quota(value=self.quota), pools=[p.as_boser() for p in self.pools], ) @@ -382,7 +390,10 @@ class PoolGroup(MinerConfigValue): return {"pools": [p.as_vnish(user_suffix=user_suffix) for p in self.pools]} @classmethod - def from_dict(cls, dict_conf: dict | None) -> "PoolGroup": + def from_dict(cls, dict_conf: dict | None) -> PoolGroup: + if dict_conf is None: + return cls() + cls_conf = {} if dict_conf.get("quota") is not None: @@ -393,57 +404,57 @@ class PoolGroup(MinerConfigValue): return cls(**cls_conf) @classmethod - def from_api(cls, api_pool_list: list) -> "PoolGroup": + def from_api(cls, api_pool_list: list) -> PoolGroup: pools = [] for pool in api_pool_list: pools.append(Pool.from_api(pool)) return cls(pools=pools) @classmethod - def from_btminer_v3(cls, api_pool_list: list) -> "PoolGroup": + def from_btminer_v3(cls, api_pool_list: list) -> PoolGroup: pools = [] for pool in api_pool_list: pools.append(Pool.from_btminer_v3(pool)) return cls(pools=pools) @classmethod - def from_epic(cls, api_pool_list: list) -> "PoolGroup": + def from_epic(cls, api_pool_list: list) -> PoolGroup: pools = [] for pool in api_pool_list: pools.append(Pool.from_epic(pool)) return cls(pools=pools) @classmethod - def from_am_modern(cls, web_pool_list: list) -> "PoolGroup": + def from_am_modern(cls, web_pool_list: list) -> PoolGroup: pools = [] for pool in web_pool_list: pools.append(Pool.from_am_modern(pool)) return cls(pools=pools) @classmethod - def from_hiveon_modern(cls, web_pool_list: list) -> "PoolGroup": + def from_hiveon_modern(cls, web_pool_list: list) -> PoolGroup: pools = [] for pool in web_pool_list: pools.append(Pool.from_hiveon_modern(pool)) return cls(pools=pools) @classmethod - def from_elphapex(cls, web_pool_list: list) -> "PoolGroup": + def from_elphapex(cls, web_pool_list: list) -> PoolGroup: pools = [] for pool in web_pool_list: pools.append(Pool.from_elphapex(pool)) return cls(pools=pools) @classmethod - def from_goldshell(cls, web_pools: list) -> "PoolGroup": + def from_goldshell(cls, web_pools: list) -> PoolGroup: return cls(pools=[Pool.from_goldshell(p) for p in web_pools]) @classmethod - def from_inno(cls, web_pools: list) -> "PoolGroup": + def from_inno(cls, web_pools: list) -> PoolGroup: return cls(pools=[Pool.from_inno(p) for p in web_pools]) @classmethod - def from_bosminer(cls, toml_group_conf: dict) -> "PoolGroup": + def from_bosminer(cls, toml_group_conf: dict) -> PoolGroup: if toml_group_conf.get("pool") is not None: return cls( name=toml_group_conf["name"], @@ -453,13 +464,13 @@ class PoolGroup(MinerConfigValue): return cls() @classmethod - def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup": + def from_vnish(cls, web_settings_pools: dict) -> PoolGroup: return cls( pools=[Pool.from_vnish(p) for p in web_settings_pools if p["url"] != ""] ) @classmethod - def from_boser(cls, grpc_pool_group: dict) -> "PoolGroup": + def from_boser(cls, grpc_pool_group: dict) -> PoolGroup: try: return cls( pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]], @@ -474,15 +485,15 @@ class PoolGroup(MinerConfigValue): return cls() @classmethod - def from_mara(cls, web_config_pools: dict) -> "PoolGroup": + def from_mara(cls, web_config_pools: dict) -> PoolGroup: return cls(pools=[Pool.from_mara(pool_conf) for pool_conf in web_config_pools]) @classmethod - def from_espminer(cls, web_system_info: dict) -> "PoolGroup": + def from_espminer(cls, web_system_info: dict) -> PoolGroup: return cls(pools=[Pool.from_espminer(web_system_info)]) @classmethod - def from_iceriver(cls, web_userpanel: dict) -> "PoolGroup": + def from_iceriver(cls, web_userpanel: dict) -> PoolGroup: return cls( pools=[ Pool.from_iceriver(web_pool) @@ -492,21 +503,21 @@ class PoolGroup(MinerConfigValue): class PoolConfig(MinerConfigValue): - groups: List[PoolGroup] = Field(default_factory=list) + groups: list[PoolGroup] = Field(default_factory=list) @classmethod - def default(cls) -> "PoolConfig": + def default(cls) -> PoolConfig: return cls() @classmethod - def from_dict(cls, dict_conf: dict | None) -> "PoolConfig": + def from_dict(cls, dict_conf: dict | None) -> PoolConfig: if dict_conf is None: return cls.default() return cls(groups=[PoolGroup.from_dict(g) for g in dict_conf["groups"]]) @classmethod - def simple(cls, pools: list[Pool | dict[str, str]]) -> "PoolConfig": + def simple(cls, pools: list[Pool | dict[str, str]]) -> PoolConfig: group_pools = [] for pool in pools: if isinstance(pool, dict): @@ -529,7 +540,7 @@ class PoolConfig(MinerConfigValue): return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)} return {"pools": PoolGroup().as_elphapex()} - def as_wm(self, user_suffix: str | None = None) -> dict: + def as_wm(self, *args: Any, user_suffix: str | None = None, **kwargs: Any) -> dict: if len(self.groups) > 0: return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)} return {"pools": PoolGroup().as_wm()} @@ -539,7 +550,9 @@ class PoolConfig(MinerConfigValue): return {"pools": self.groups[0].as_btminer_v3(user_suffix=user_suffix)} return {"pools": PoolGroup().as_btminer_v3()} - def as_am_old(self, user_suffix: str | None = None) -> dict: + def as_am_old( + self, *args: Any, user_suffix: str | None = None, **kwargs: Any + ) -> dict: if len(self.groups) > 0: return self.groups[0].as_am_old(user_suffix=user_suffix) return PoolGroup().as_am_old() @@ -554,7 +567,9 @@ class PoolConfig(MinerConfigValue): return {"pools": self.groups[0].as_avalon(user_suffix=user_suffix)} return {"pools": PoolGroup().as_avalon()} - def as_inno(self, user_suffix: str | None = None) -> dict: + def as_inno( + self, *args: Any, user_suffix: str | None = None, **kwargs: Any + ) -> dict: if len(self.groups) > 0: return self.groups[0].as_inno(user_suffix=user_suffix) return PoolGroup().as_inno() @@ -569,7 +584,7 @@ class PoolConfig(MinerConfigValue): def as_boser(self, user_suffix: str | None = None) -> dict: return { "set_pool_groups": SetPoolGroupsRequest( - save_action=SaveAction.SAVE_AND_APPLY, + save_action=SaveAction(SaveAction.SAVE_AND_APPLY), pool_groups=[g.as_boser(user_suffix=user_suffix) for g in self.groups], ) } @@ -615,7 +630,7 @@ class PoolConfig(MinerConfigValue): return self.groups[0].as_vnish(user_suffix=user_suffix) @classmethod - def from_api(cls, api_pools: dict) -> "PoolConfig": + def from_api(cls, api_pools: dict) -> PoolConfig: try: pool_data = api_pools["POOLS"] except KeyError: @@ -625,7 +640,7 @@ class PoolConfig(MinerConfigValue): return cls(groups=[PoolGroup.from_api(pool_data)]) @classmethod - def from_btminer_v3(cls, rpc_pools: dict) -> "PoolConfig": + def from_btminer_v3(cls, rpc_pools: dict) -> PoolConfig: try: pool_data = rpc_pools["pools"] except KeyError: @@ -635,12 +650,12 @@ class PoolConfig(MinerConfigValue): return cls(groups=[PoolGroup.from_btminer_v3(pool_data)]) @classmethod - def from_epic(cls, web_conf: dict) -> "PoolConfig": + def from_epic(cls, web_conf: dict) -> PoolConfig: pool_data = web_conf["StratumConfigs"] return cls(groups=[PoolGroup.from_epic(pool_data)]) @classmethod - def from_am_modern(cls, web_conf: dict) -> "PoolConfig": + def from_am_modern(cls, web_conf: dict) -> PoolConfig: try: pool_data = web_conf["pools"] except KeyError: @@ -649,7 +664,7 @@ class PoolConfig(MinerConfigValue): return cls(groups=[PoolGroup.from_am_modern(pool_data)]) @classmethod - def from_hiveon_modern(cls, web_conf: dict) -> "PoolConfig": + def from_hiveon_modern(cls, web_conf: dict) -> PoolConfig: try: pool_data = web_conf["pools"] except KeyError: @@ -658,17 +673,17 @@ class PoolConfig(MinerConfigValue): return cls(groups=[PoolGroup.from_hiveon_modern(pool_data)]) @classmethod - def from_elphapex(cls, web_conf: dict) -> "PoolConfig": + def from_elphapex(cls, web_conf: dict) -> PoolConfig: pool_data = web_conf["pools"] return cls(groups=[PoolGroup.from_elphapex(pool_data)]) @classmethod - def from_goldshell(cls, web_pools: list) -> "PoolConfig": + def from_goldshell(cls, web_pools: list) -> PoolConfig: return cls(groups=[PoolGroup.from_goldshell(web_pools)]) @classmethod - def from_goldshell_byte(cls, web_pools: list) -> "PoolConfig": + def from_goldshell_byte(cls, web_pools: list) -> PoolConfig: return cls( groups=[ PoolGroup.from_goldshell(g["pools"]) @@ -678,25 +693,25 @@ class PoolConfig(MinerConfigValue): ) @classmethod - def from_inno(cls, web_pools: list) -> "PoolConfig": + def from_inno(cls, web_pools: list) -> PoolConfig: return cls(groups=[PoolGroup.from_inno(web_pools)]) @classmethod - def from_bosminer(cls, toml_conf: dict) -> "PoolConfig": + def from_bosminer(cls, toml_conf: dict) -> PoolConfig: if toml_conf.get("group") is None: return cls() return cls(groups=[PoolGroup.from_bosminer(g) for g in toml_conf["group"]]) @classmethod - def from_vnish(cls, web_settings: dict) -> "PoolConfig": + def from_vnish(cls, web_settings: dict) -> PoolConfig: try: return cls(groups=[PoolGroup.from_vnish(web_settings["miner"]["pools"])]) except LookupError: return cls() @classmethod - def from_boser(cls, grpc_miner_conf: dict) -> "PoolConfig": + def from_boser(cls, grpc_miner_conf: dict) -> PoolConfig: try: return cls( groups=[ @@ -708,19 +723,19 @@ class PoolConfig(MinerConfigValue): return cls() @classmethod - def from_mara(cls, web_config: dict) -> "PoolConfig": + def from_mara(cls, web_config: dict) -> PoolConfig: return cls(groups=[PoolGroup.from_mara(web_config["pools"])]) @classmethod - def from_espminer(cls, web_system_info: dict) -> "PoolConfig": + def from_espminer(cls, web_system_info: dict) -> PoolConfig: return cls(groups=[PoolGroup.from_espminer(web_system_info)]) @classmethod - def from_iceriver(cls, web_userpanel: dict) -> "PoolConfig": + def from_iceriver(cls, web_userpanel: dict) -> PoolConfig: return cls(groups=[PoolGroup.from_iceriver(web_userpanel)]) @classmethod - def from_luxos(cls, rpc_groups: dict, rpc_pools: dict) -> "PoolConfig": + def from_luxos(cls, rpc_groups: dict, rpc_pools: dict) -> PoolConfig: return cls( groups=[ PoolGroup( diff --git a/pyasic/config/temperature.py b/pyasic/config/temperature.py index 99d2f782..e16cc00c 100644 --- a/pyasic/config/temperature.py +++ b/pyasic/config/temperature.py @@ -40,7 +40,7 @@ class TemperatureConfig(MinerConfigValue): return {"temp_control": temp_cfg} def as_epic(self) -> dict: - temps_config = {"temps": {}, "fans": {"Auto": {}}} + temps_config: dict = {"temps": {}, "fans": {"Auto": {}}} if self.target is not None: temps_config["fans"]["Auto"]["Target Temperature"] = self.target else: @@ -58,7 +58,9 @@ class TemperatureConfig(MinerConfigValue): return {"misc": {"restart_temp": self.danger}} @classmethod - def from_dict(cls, dict_conf: dict | None) -> "TemperatureConfig": + def from_dict(cls, dict_conf: dict | None) -> TemperatureConfig: + if dict_conf is None: + return cls() return cls( target=dict_conf.get("target"), hot=dict_conf.get("hot"), @@ -66,7 +68,7 @@ class TemperatureConfig(MinerConfigValue): ) @classmethod - def from_bosminer(cls, toml_conf: dict) -> "TemperatureConfig": + def from_bosminer(cls, toml_conf: dict) -> TemperatureConfig: temp_control = toml_conf.get("temp_control") if temp_control is not None: return cls( @@ -77,7 +79,7 @@ class TemperatureConfig(MinerConfigValue): return cls() @classmethod - def from_epic(cls, web_conf: dict) -> "TemperatureConfig": + def from_epic(cls, web_conf: dict) -> TemperatureConfig: try: dangerous_temp = web_conf["Misc"]["Critical Temp"] except KeyError: @@ -95,7 +97,7 @@ class TemperatureConfig(MinerConfigValue): return cls(target=target_temp, hot=hot_temp, danger=dangerous_temp) @classmethod - def from_vnish(cls, web_settings: dict) -> "TemperatureConfig": + def from_vnish(cls, web_settings: dict) -> TemperatureConfig: try: dangerous_temp = web_settings["misc"]["restart_temp"] except KeyError: @@ -111,7 +113,7 @@ class TemperatureConfig(MinerConfigValue): return cls() @classmethod - def from_boser(cls, grpc_miner_conf: dict) -> "TemperatureConfig": + def from_boser(cls, grpc_miner_conf: dict) -> TemperatureConfig: try: temperature_conf = grpc_miner_conf["temperature"] except KeyError: @@ -142,7 +144,7 @@ class TemperatureConfig(MinerConfigValue): return cls.default() @classmethod - def from_luxos(cls, rpc_tempctrl: dict) -> "TemperatureConfig": + def from_luxos(cls, rpc_tempctrl: dict) -> TemperatureConfig: try: tempctrl_config = rpc_tempctrl["TEMPCTRL"][0] return cls( diff --git a/pyasic/data/__init__.py b/pyasic/data/__init__.py index 735dc2c8..37a07dbd 100644 --- a/pyasic/data/__init__.py +++ b/pyasic/data/__init__.py @@ -15,6 +15,7 @@ # ------------------------------------------------------------------------------ import copy import time +from collections.abc import Callable from datetime import datetime, timezone from typing import Any @@ -24,6 +25,7 @@ from pyasic.config import MinerConfig from pyasic.config.mining import MiningModePowerTune from pyasic.data.pools import PoolMetrics, Scheme from pyasic.device.algorithm.hashrate import AlgoHashRateType +from pyasic.device.algorithm.hashrate.base import GenericHashrate from .boards import HashBoard from .device import DeviceInfo @@ -90,7 +92,9 @@ class MinerData(BaseModel): hostname: str | None = None # hashrate - raw_hashrate: AlgoHashRateType = Field(exclude=True, default=None, repr=False) + raw_hashrate: AlgoHashRateType | None = Field( + exclude=True, default=None, repr=False + ) # sticker sticker_hashrate: AlgoHashRateType | None = None @@ -194,7 +198,7 @@ class MinerData(BaseModel): setattr(cp, key, item & other_item) return cp - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def hashrate(self) -> AlgoHashRateType | None: if len(self.hashboards) > 0: @@ -203,14 +207,24 @@ class MinerData(BaseModel): if item.hashrate is not None: hr_data.append(item.hashrate) if len(hr_data) > 0: - return sum(hr_data, start=self.device_info.algo.hashrate(rate=0)) + if self.device_info is not None and self.device_info.algo is not None: + from pyasic.device.algorithm.hashrate.unit.base import GenericUnit + + return sum( + hr_data, + start=self.device_info.algo.hashrate( + rate=0, unit=GenericUnit.H + ), + ) + else: + return sum(hr_data, start=GenericHashrate(rate=0)) return self.raw_hashrate @hashrate.setter def hashrate(self, val): self.raw_hashrate = val - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def wattage_limit(self) -> int | None: if self.config is not None: @@ -222,7 +236,7 @@ class MinerData(BaseModel): def wattage_limit(self, val: int): self.raw_wattage_limit = val - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def total_chips(self) -> int | None: if len(self.hashboards) > 0: @@ -233,15 +247,16 @@ class MinerData(BaseModel): if len(chip_data) > 0: return sum(chip_data) return None + return 0 - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def nominal(self) -> bool | None: if self.total_chips is None or self.expected_chips is None: return None return self.expected_chips == self.total_chips - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def percent_expected_chips(self) -> int | None: if self.total_chips is None or self.expected_chips is None: @@ -250,7 +265,7 @@ class MinerData(BaseModel): return 0 return round((self.total_chips / self.expected_chips) * 100) - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def percent_expected_hashrate(self) -> int | None: if self.hashrate is None or self.expected_hashrate is None: @@ -260,7 +275,7 @@ class MinerData(BaseModel): except ZeroDivisionError: return 0 - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def percent_expected_wattage(self) -> int | None: if self.wattage_limit is None or self.wattage is None: @@ -270,10 +285,10 @@ class MinerData(BaseModel): except ZeroDivisionError: return 0 - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def temperature_avg(self) -> int | None: - total_temp = 0 + total_temp: float = 0 temp_count = 0 for hb in self.hashboards: if hb.temp is not None: @@ -283,7 +298,7 @@ class MinerData(BaseModel): return None return round(total_temp / temp_count) - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def efficiency(self) -> int | None: efficiency = self._efficiency(0) @@ -292,7 +307,7 @@ class MinerData(BaseModel): else: return int(efficiency) - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def efficiency_fract(self) -> float | None: return self._efficiency(2) @@ -305,39 +320,43 @@ class MinerData(BaseModel): except ZeroDivisionError: return 0.0 - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def datetime(self) -> str: return self.raw_datetime.isoformat() - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def timestamp(self) -> int: return int(time.mktime(self.raw_datetime.timetuple())) - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def make(self) -> str | None: - if self.device_info.make is not None: + if self.device_info is not None and self.device_info.make is not None: return str(self.device_info.make) + return "" - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def model(self) -> str | None: - if self.device_info.model is not None: + if self.device_info is not None and self.device_info.model is not None: return str(self.device_info.model) + return "" - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def firmware(self) -> str | None: - if self.device_info.firmware is not None: + if self.device_info is not None and self.device_info.firmware is not None: return str(self.device_info.firmware) + return "" - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def algo(self) -> str | None: - if self.device_info.algo is not None: + if self.device_info is not None and self.device_info.algo is not None: return str(self.device_info.algo) + return "" def keys(self) -> list: return list(self.model_fields.keys()) @@ -417,7 +436,8 @@ class MinerData(BaseModel): for dt in serialization_map_instance: if item_serialized is None: if isinstance(list_field_val, dt): - item_serialized = serialization_map_instance[dt]( + func = serialization_map_instance[dt] + item_serialized = func( f"{key}{level_delimiter}{idx}", list_field_val ) if item_serialized is not None: @@ -461,11 +481,11 @@ class MinerData(BaseModel): "pools", ] - serialization_map_instance = { + serialization_map_instance: dict[type, Callable[[str, Any], str | None]] = { AlgoHashRateType: serialize_algo_hash_rate, BaseMinerError: serialize_miner_error, } - serialization_map = { + serialization_map: dict[type, Callable[[str, Any], str | None]] = { int: serialize_int, float: serialize_float, str: serialize_str, @@ -499,9 +519,8 @@ class MinerData(BaseModel): for datatype in serialization_map_instance: if serialized is None: if isinstance(field_val, datatype): - serialized = serialization_map_instance[datatype]( - field, field_val - ) + func = serialization_map_instance[datatype] + serialized = func(field, field_val) if serialized is not None: field_data.append(serialized) diff --git a/pyasic/data/boards.py b/pyasic/data/boards.py index 61de3a9f..a261a14c 100644 --- a/pyasic/data/boards.py +++ b/pyasic/data/boards.py @@ -15,6 +15,7 @@ # ------------------------------------------------------------------------------ from __future__ import annotations +from collections.abc import Callable from typing import Any from pydantic import BaseModel @@ -89,7 +90,7 @@ class HashBoard(BaseModel): def serialize_algo_hash_rate(key: str, value: AlgoHashRateType) -> str: return f"{key}={round(float(value), 2)}" - def serialize_bool(key: str, value: bool): + def serialize_bool(key: str, value: bool) -> str: return f"{key}={str(value).lower()}" serialization_map_instance = { @@ -116,8 +117,11 @@ class HashBoard(BaseModel): field_data = [] for field in include: field_val = getattr(self, field) - serialization_func = serialization_map.get( - type(field_val), lambda _k, _v: None + serialization_func: Callable[[str, Any], str | None] = ( + serialization_map.get( + type(field_val), + lambda _k, _v: None, # type: ignore + ) ) serialized = serialization_func( f"{key_root}{level_delimiter}{field}", field_val diff --git a/pyasic/data/error_codes/base.py b/pyasic/data/error_codes/base.py index e7ec5c97..4f293b9b 100644 --- a/pyasic/data/error_codes/base.py +++ b/pyasic/data/error_codes/base.py @@ -2,6 +2,8 @@ from pydantic import BaseModel class BaseMinerError(BaseModel): + error_code: int | None = None + @classmethod def fields(cls): return list(cls.model_fields.keys()) @@ -24,9 +26,13 @@ class BaseMinerError(BaseModel): field_data.append( f"{root_key}{level_delimiter}error_code={self.error_code}" ) - if self.error_message is not None: - field_data.append( - f'{root_key}{level_delimiter}error_message="{self.error_message}"' - ) + + # Check if error_message exists as an attribute (either regular or computed field) + if hasattr(self, "error_message"): + error_message = getattr(self, "error_message") + if error_message is not None: + field_data.append( + f'{root_key}{level_delimiter}error_message="{error_message}"' + ) return ",".join(field_data) diff --git a/pyasic/data/error_codes/innosilicon.py b/pyasic/data/error_codes/innosilicon.py index 3f969350..a6f97520 100644 --- a/pyasic/data/error_codes/innosilicon.py +++ b/pyasic/data/error_codes/innosilicon.py @@ -30,7 +30,7 @@ class InnosiliconError(BaseMinerError): error_code: int - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def error_message(self) -> str: # noqa - Skip PyCharm inspection if self.error_code in ERROR_CODES: diff --git a/pyasic/data/error_codes/whatsminer.py b/pyasic/data/error_codes/whatsminer.py index 5ae16974..858f3769 100644 --- a/pyasic/data/error_codes/whatsminer.py +++ b/pyasic/data/error_codes/whatsminer.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ + from pydantic import computed_field from pyasic.data.error_codes.base import BaseMinerError @@ -28,50 +29,69 @@ class WhatsminerError(BaseMinerError): error_code: int - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def error_message(self) -> str: # noqa - Skip PyCharm inspection - if len(str(self.error_code)) == 6 and not str(self.error_code)[:1] == "1": - err_type = int(str(self.error_code)[:2]) - err_subtype = int(str(self.error_code)[2:3]) - err_value = int(str(self.error_code)[3:]) + error_str = str(self.error_code) + + # Handle edge cases for short error codes + if len(error_str) < 3: + return "Unknown error type." + + if len(error_str) == 6 and not error_str[:1] == "1": + err_type = int(error_str[:2]) + err_subtype = int(error_str[2:3]) + err_value = int(error_str[3:]) else: - err_type = int(str(self.error_code)[:-2]) - err_subtype = int(str(self.error_code)[-2:-1]) - err_value = int(str(self.error_code)[-1:]) + err_type = int(error_str[:-2]) + err_subtype = int(error_str[-2:-1]) + err_value = int(error_str[-1:]) try: - select_err_type = ERROR_CODES[err_type] + select_err_type = ERROR_CODES.get(err_type) + if select_err_type is None: + return "Unknown error type." + if err_subtype in select_err_type: select_err_subtype = select_err_type[err_subtype] - if err_value in select_err_subtype: - return select_err_subtype[err_value] - elif "n" in select_err_subtype: - return select_err_subtype[ - "n" # noqa: picks up `select_err_subtype["n"]` as not being numeric? - ].replace("{n}", str(err_value)) + if isinstance(select_err_subtype, dict): + if err_value in select_err_subtype: + result = select_err_subtype[err_value] + return str(result) if not isinstance(result, str) else result + elif "n" in select_err_subtype: + template = select_err_subtype["n"] + if isinstance(template, str): + return template.replace("{n}", str(err_value)) + else: + return "Unknown error type." + else: + return "Unknown error type." else: return "Unknown error type." elif "n" in select_err_type: - select_err_subtype = select_err_type[ - "n" # noqa: picks up `select_err_subtype["n"]` as not being numeric? - ] - if err_value in select_err_subtype: - return select_err_subtype[err_value] - elif "c" in select_err_subtype: - return ( - select_err_subtype["c"] - .replace( # noqa: picks up `select_err_subtype["n"]` as not being numeric? - "{n}", str(err_subtype) - ) - .replace("{c}", str(err_value)) - ) + select_err_subtype = select_err_type["n"] + if isinstance(select_err_subtype, dict): + if err_value in select_err_subtype: + result = select_err_subtype[err_value] + return str(result) if not isinstance(result, str) else result + elif "c" in select_err_subtype: + template = select_err_subtype["c"] + if isinstance(template, str): + return template.replace("{n}", str(err_subtype)).replace( + "{c}", str(err_value) + ) + else: + return "Unknown error type." + else: + return "Unknown error type." + else: + return "Unknown error type." else: return "Unknown error type." - except KeyError: + except (KeyError, TypeError): return "Unknown error type." -ERROR_CODES = { +ERROR_CODES: dict[int, dict[int | str, str | dict[int | str, str]]] = { 1: { # Fan error 0: { 0: "Fan unknown.", diff --git a/pyasic/data/fans.py b/pyasic/data/fans.py index 73f6e77a..ace0631b 100644 --- a/pyasic/data/fans.py +++ b/pyasic/data/fans.py @@ -26,7 +26,7 @@ class Fan(BaseModel): speed: The speed of the fan. """ - speed: int = None + speed: int | None = None def get(self, __key: str, default: Any = None): try: diff --git a/pyasic/data/pools.py b/pyasic/data/pools.py index 1d77379b..dbc1c6a8 100644 --- a/pyasic/data/pools.py +++ b/pyasic/data/pools.py @@ -1,5 +1,6 @@ +from collections.abc import Callable from enum import Enum -from typing import Optional +from typing import Any from urllib.parse import urlparse from pydantic import BaseModel, computed_field, model_serializer @@ -16,7 +17,7 @@ class PoolUrl(BaseModel): scheme: Scheme host: str port: int - pubkey: Optional[str] = None + pubkey: str | None = None @model_serializer def serialize(self): @@ -39,6 +40,8 @@ class PoolUrl(BaseModel): scheme = Scheme.STRATUM_V1 host = parsed_url.hostname port = parsed_url.port + if port is None: + return None pubkey = parsed_url.path.lstrip("/") if scheme == Scheme.STRATUM_V2 else None return cls(scheme=scheme, host=host, port=port, pubkey=pubkey) @@ -70,16 +73,20 @@ class PoolMetrics(BaseModel): index: int | None = None user: str | None = None - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def pool_rejected_percent(self) -> float: # noqa - Skip PyCharm inspection """Calculate and return the percentage of rejected shares""" + if self.rejected is None or self.accepted is None: + return 0.0 return self._calculate_percentage(self.rejected, self.accepted + self.rejected) - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def pool_stale_percent(self) -> float: # noqa - Skip PyCharm inspection """Calculate and return the percentage of stale shares.""" + if self.get_failures is None or self.accepted is None or self.rejected is None: + return 0.0 return self._calculate_percentage( self.get_failures, self.accepted + self.rejected ) @@ -87,10 +94,8 @@ class PoolMetrics(BaseModel): @staticmethod def _calculate_percentage(value: int, total: int) -> float: """Calculate the percentage.""" - if value is None or total is None: - return 0 if total == 0: - return 0 + return 0.0 return (value / total) * 100 def as_influxdb(self, key_root: str, level_delimiter: str = ".") -> str: @@ -103,13 +108,13 @@ class PoolMetrics(BaseModel): def serialize_str(key: str, value: str) -> str: return f'{key}="{value}"' - def serialize_pool_url(key: str, value: str) -> str: + def serialize_pool_url(key: str, value: PoolUrl) -> str: return f'{key}="{str(value)}"' - def serialize_bool(key: str, value: bool): + def serialize_bool(key: str, value: bool) -> str: return f"{key}={str(value).lower()}" - serialization_map = { + serialization_map: dict[type, Callable[[str, Any], str]] = { int: serialize_int, float: serialize_float, str: serialize_str, @@ -129,13 +134,14 @@ class PoolMetrics(BaseModel): field_data = [] for field in include: field_val = getattr(self, field) - serialization_func = serialization_map.get( - type(field_val), lambda _k, _v: None - ) - serialized = serialization_func( - f"{key_root}{level_delimiter}{field}", field_val - ) - if serialized is not None: - field_data.append(serialized) + if field_val is None: + continue + serialization_func = serialization_map.get(type(field_val)) + if serialization_func is not None: + serialized = serialization_func( + f"{key_root}{level_delimiter}{field}", field_val + ) + if serialized is not None: + field_data.append(serialized) return ",".join(field_data) diff --git a/pyasic/device/algorithm/hashrate/base.py b/pyasic/device/algorithm/hashrate/base.py index bb81ba34..55a72bb5 100644 --- a/pyasic/device/algorithm/hashrate/base.py +++ b/pyasic/device/algorithm/hashrate/base.py @@ -1,23 +1,26 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import Generic, TypeVar from pydantic import BaseModel, field_serializer from typing_extensions import Self from .unit.base import AlgoHashRateUnitType, GenericUnit +UnitType = TypeVar("UnitType", bound=AlgoHashRateUnitType) -class AlgoHashRateType(BaseModel, ABC): - unit: AlgoHashRateUnitType + +class AlgoHashRateType(BaseModel, ABC, Generic[UnitType]): + unit: UnitType rate: float @field_serializer("unit") - def serialize_unit(self, unit: AlgoHashRateUnitType): + def serialize_unit(self, unit: UnitType): return unit.model_dump() @abstractmethod - def into(self, other: "AlgoHashRateUnitType"): + def into(self, other: UnitType) -> Self: pass def auto_unit(self): @@ -46,7 +49,7 @@ class AlgoHashRateType(BaseModel, ABC): def __repr__(self): return f"{self.rate} {str(self.unit)}" - def __round__(self, n: int = None): + def __round__(self, n: int | None = None): return round(self.rate, n) def __add__(self, other: Self | int | float) -> Self: @@ -85,11 +88,11 @@ class AlgoHashRateType(BaseModel, ABC): return self.__class__(rate=self.rate * other, unit=self.unit) -class GenericHashrate(AlgoHashRateType): +class GenericHashrate(AlgoHashRateType[GenericUnit]): rate: float = 0 unit: GenericUnit = GenericUnit.H - def into(self, other: GenericUnit): + def into(self, other: GenericUnit) -> Self: return self.__class__( rate=self.rate / (other.value / self.unit.value), unit=other ) diff --git a/pyasic/device/algorithm/hashrate/blake256.py b/pyasic/device/algorithm/hashrate/blake256.py index 75324c6a..f9296455 100644 --- a/pyasic/device/algorithm/hashrate/blake256.py +++ b/pyasic/device/algorithm/hashrate/blake256.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.blake256 import Blake256Unit from .unit import HashUnit -class Blake256HashRate(AlgoHashRateType): +class Blake256HashRate(AlgoHashRateType[Blake256Unit]): rate: float unit: Blake256Unit = HashUnit.BLAKE256.default diff --git a/pyasic/device/algorithm/hashrate/blockflow.py b/pyasic/device/algorithm/hashrate/blockflow.py index 89c13d46..ad02ea91 100644 --- a/pyasic/device/algorithm/hashrate/blockflow.py +++ b/pyasic/device/algorithm/hashrate/blockflow.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.blockflow import BlockFlowUnit from .unit import HashUnit -class BlockFlowHashRate(AlgoHashRateType): +class BlockFlowHashRate(AlgoHashRateType[BlockFlowUnit]): rate: float unit: BlockFlowUnit = HashUnit.BLOCKFLOW.default diff --git a/pyasic/device/algorithm/hashrate/eaglesong.py b/pyasic/device/algorithm/hashrate/eaglesong.py index 0d3048e6..bdf6d0db 100644 --- a/pyasic/device/algorithm/hashrate/eaglesong.py +++ b/pyasic/device/algorithm/hashrate/eaglesong.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.eaglesong import EaglesongUnit from .unit import HashUnit -class EaglesongHashRate(AlgoHashRateType): +class EaglesongHashRate(AlgoHashRateType[EaglesongUnit]): rate: float unit: EaglesongUnit = HashUnit.EAGLESONG.default diff --git a/pyasic/device/algorithm/hashrate/equihash.py b/pyasic/device/algorithm/hashrate/equihash.py index b57f2bf3..08503a93 100644 --- a/pyasic/device/algorithm/hashrate/equihash.py +++ b/pyasic/device/algorithm/hashrate/equihash.py @@ -8,9 +8,9 @@ from pyasic.device.algorithm.hashrate.unit.equihash import EquihashUnit from .unit import HashUnit -class EquihashHashRate(AlgoHashRateType): +class EquihashHashRate(AlgoHashRateType[EquihashUnit]): rate: float - unit: EquihashUnit = HashUnit.ETHASH.default + unit: EquihashUnit = HashUnit.EQUIHASH.default def into(self, other: EquihashUnit) -> Self: return self.__class__( diff --git a/pyasic/device/algorithm/hashrate/ethash.py b/pyasic/device/algorithm/hashrate/ethash.py index 9b1ab435..ccff11dd 100644 --- a/pyasic/device/algorithm/hashrate/ethash.py +++ b/pyasic/device/algorithm/hashrate/ethash.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.ethash import EtHashUnit from .unit import HashUnit -class EtHashHashRate(AlgoHashRateType): +class EtHashHashRate(AlgoHashRateType[EtHashUnit]): rate: float unit: EtHashUnit = HashUnit.ETHASH.default diff --git a/pyasic/device/algorithm/hashrate/handshake.py b/pyasic/device/algorithm/hashrate/handshake.py index e7ae2e5b..6cef1485 100644 --- a/pyasic/device/algorithm/hashrate/handshake.py +++ b/pyasic/device/algorithm/hashrate/handshake.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.handshake import HandshakeUnit from .unit import HashUnit -class HandshakeHashRate(AlgoHashRateType): +class HandshakeHashRate(AlgoHashRateType[HandshakeUnit]): rate: float unit: HandshakeUnit = HashUnit.HANDSHAKE.default diff --git a/pyasic/device/algorithm/hashrate/kadena.py b/pyasic/device/algorithm/hashrate/kadena.py index 8d99be83..f37fc8e4 100644 --- a/pyasic/device/algorithm/hashrate/kadena.py +++ b/pyasic/device/algorithm/hashrate/kadena.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.kadena import KadenaUnit from .unit import HashUnit -class KadenaHashRate(AlgoHashRateType): +class KadenaHashRate(AlgoHashRateType[KadenaUnit]): rate: float unit: KadenaUnit = HashUnit.KADENA.default diff --git a/pyasic/device/algorithm/hashrate/kheavyhash.py b/pyasic/device/algorithm/hashrate/kheavyhash.py index 75953e0e..d07ed9de 100644 --- a/pyasic/device/algorithm/hashrate/kheavyhash.py +++ b/pyasic/device/algorithm/hashrate/kheavyhash.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.kheavyhash import KHeavyHashUnit from .unit import HashUnit -class KHeavyHashHashRate(AlgoHashRateType): +class KHeavyHashHashRate(AlgoHashRateType[KHeavyHashUnit]): rate: float unit: KHeavyHashUnit = HashUnit.KHEAVYHASH.default diff --git a/pyasic/device/algorithm/hashrate/scrypt.py b/pyasic/device/algorithm/hashrate/scrypt.py index 77f0bbfc..a9230675 100644 --- a/pyasic/device/algorithm/hashrate/scrypt.py +++ b/pyasic/device/algorithm/hashrate/scrypt.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.scrypt import ScryptUnit from .unit import HashUnit -class ScryptHashRate(AlgoHashRateType): +class ScryptHashRate(AlgoHashRateType[ScryptUnit]): rate: float unit: ScryptUnit = HashUnit.SCRYPT.default diff --git a/pyasic/device/algorithm/hashrate/sha256.py b/pyasic/device/algorithm/hashrate/sha256.py index 7b28343d..ba1f83fa 100644 --- a/pyasic/device/algorithm/hashrate/sha256.py +++ b/pyasic/device/algorithm/hashrate/sha256.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.sha256 import SHA256Unit from .unit import HashUnit -class SHA256HashRate(AlgoHashRateType): +class SHA256HashRate(AlgoHashRateType[SHA256Unit]): rate: float unit: SHA256Unit = HashUnit.SHA256.default diff --git a/pyasic/device/algorithm/hashrate/unit/base.py b/pyasic/device/algorithm/hashrate/unit/base.py index 2a358d02..a144397b 100644 --- a/pyasic/device/algorithm/hashrate/unit/base.py +++ b/pyasic/device/algorithm/hashrate/unit/base.py @@ -2,54 +2,46 @@ from enum import IntEnum class AlgoHashRateUnitType(IntEnum): - H: int - KH: int - MH: int - GH: int - TH: int - PH: int - EH: int - ZH: int - - default: int - def __str__(self): - if self.value == self.H: + if hasattr(self.__class__, "H") and self.value == self.__class__.H: return "H/s" - if self.value == self.KH: + if hasattr(self.__class__, "KH") and self.value == self.__class__.KH: return "KH/s" - if self.value == self.MH: + if hasattr(self.__class__, "MH") and self.value == self.__class__.MH: return "MH/s" - if self.value == self.GH: + if hasattr(self.__class__, "GH") and self.value == self.__class__.GH: return "GH/s" - if self.value == self.TH: + if hasattr(self.__class__, "TH") and self.value == self.__class__.TH: return "TH/s" - if self.value == self.PH: + if hasattr(self.__class__, "PH") and self.value == self.__class__.PH: return "PH/s" - if self.value == self.EH: + if hasattr(self.__class__, "EH") and self.value == self.__class__.EH: return "EH/s" - if self.value == self.ZH: + if hasattr(self.__class__, "ZH") and self.value == self.__class__.ZH: return "ZH/s" + return "" @classmethod def from_str(cls, value: str): - if value == "H": + if value == "H" and hasattr(cls, "H"): return cls.H - elif value == "KH": + elif value == "KH" and hasattr(cls, "KH"): return cls.KH - elif value == "MH": + elif value == "MH" and hasattr(cls, "MH"): return cls.MH - elif value == "GH": + elif value == "GH" and hasattr(cls, "GH"): return cls.GH - elif value == "TH": + elif value == "TH" and hasattr(cls, "TH"): return cls.TH - elif value == "PH": + elif value == "PH" and hasattr(cls, "PH"): return cls.PH - elif value == "EH": + elif value == "EH" and hasattr(cls, "EH"): return cls.EH - elif value == "ZH": + elif value == "ZH" and hasattr(cls, "ZH"): return cls.ZH - return cls.default + if hasattr(cls, "default"): + return cls.default + return None def __repr__(self): return str(self) diff --git a/pyasic/device/algorithm/hashrate/x11.py b/pyasic/device/algorithm/hashrate/x11.py index 7c1e6fb6..50ed80c7 100644 --- a/pyasic/device/algorithm/hashrate/x11.py +++ b/pyasic/device/algorithm/hashrate/x11.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.x11 import X11Unit from .unit import HashUnit -class X11HashRate(AlgoHashRateType): +class X11HashRate(AlgoHashRateType[X11Unit]): rate: float unit: X11Unit = HashUnit.X11.default diff --git a/pyasic/device/algorithm/hashrate/zksnark.py b/pyasic/device/algorithm/hashrate/zksnark.py index 32d30b82..b08ff2ce 100644 --- a/pyasic/device/algorithm/hashrate/zksnark.py +++ b/pyasic/device/algorithm/hashrate/zksnark.py @@ -8,7 +8,7 @@ from pyasic.device.algorithm.hashrate.unit.zksnark import ZkSnarkUnit from .unit import HashUnit -class ZkSnarkHashRate(AlgoHashRateType): +class ZkSnarkHashRate(AlgoHashRateType[ZkSnarkUnit]): rate: float unit: ZkSnarkUnit = HashUnit.ZKSNARK.default diff --git a/pyasic/device/models.py b/pyasic/device/models.py index 6a12538b..0ddf39e7 100644 --- a/pyasic/device/models.py +++ b/pyasic/device/models.py @@ -224,6 +224,7 @@ class WhatsminerModels(MinerModelType): M31V20 = "M31 V20" M32V10 = "M32 V10" M32V20 = "M32 V20" + M32S = "M32S" M33SPlusPlusVG40 = "M33S++ VG40" M33SPlusPlusVH20 = "M33S++ VH20" M33SPlusPlusVH30 = "M33S++ VH30" @@ -562,12 +563,18 @@ class BraiinsModels(MinerModelType): BMM100 = "BMM100" BMM101 = "BMM101" + def __str__(self): + return self.value + class ElphapexModels(MinerModelType): DG1 = "DG1" DG1Plus = "DG1+" DG1Home = "DG1Home" + def __str__(self): + return self.value + class MinerModel: ANTMINER = AntminerModels diff --git a/pyasic/load/__init__.py b/pyasic/load/__init__.py deleted file mode 100644 index 2c8b3967..00000000 --- a/pyasic/load/__init__.py +++ /dev/null @@ -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 diff --git a/pyasic/miners/antminer/epic/X19/S19.py b/pyasic/miners/antminer/epic/X19/S19.py index 0a7177f5..a64d8085 100644 --- a/pyasic/miners/antminer/epic/X19/S19.py +++ b/pyasic/miners/antminer/epic/X19/S19.py @@ -14,7 +14,7 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from pyasic.device.models import MinerModel +from pyasic.device.models import MinerModel, MinerModelType from pyasic.miners.backends import ePIC from pyasic.miners.device.models import ( S19, @@ -56,12 +56,12 @@ class ePICS19XP(ePIC, S19XP): class ePICS19jProDual(ePIC, S19jPro): - raw_model = MinerModel.EPIC.S19jProDual + raw_model: MinerModelType = MinerModel.EPIC.S19jProDual expected_fans = S19jPro.expected_fans * 2 expected_hashboards = S19jPro.expected_hashboards * 2 class ePICS19kProDual(ePIC, S19kPro): - raw_model = MinerModel.EPIC.S19kProDual + raw_model: MinerModelType = MinerModel.EPIC.S19kProDual expected_fans = S19kPro.expected_fans * 2 expected_hashboards = S19kPro.expected_hashboards * 2 diff --git a/pyasic/miners/antminer/hiveon/X9/T9.py b/pyasic/miners/antminer/hiveon/X9/T9.py index 1035bf30..7bedc249 100644 --- a/pyasic/miners/antminer/hiveon/X9/T9.py +++ b/pyasic/miners/antminer/hiveon/X9/T9.py @@ -14,7 +14,6 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional from pyasic.data import HashBoard from pyasic.device.algorithm import AlgoHashRate, HashUnit @@ -76,7 +75,7 @@ class HiveonT9(HiveonOld, T9): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]: hashboards = [ HashBoard(slot=board, expected_chips=self.expected_chips) for board in range(self.expected_hashboards) @@ -84,7 +83,7 @@ class HiveonT9(HiveonOld, T9): if rpc_stats is None: try: - rpc_stats = self.rpc.stats() + rpc_stats = await self.rpc.stats() except APIError: return [] @@ -98,7 +97,7 @@ class HiveonT9(HiveonOld, T9): hashrate = 0 chips = 0 for chipset in board_map[board]: - if hashboards[board].chip_temp is None: + if hashboards[board].chip_temp is None and rpc_stats is not None: try: hashboards[board].temp = rpc_stats["STATS"][1][f"temp{chipset}"] hashboards[board].chip_temp = rpc_stats["STATS"][1][ @@ -108,11 +107,12 @@ class HiveonT9(HiveonOld, T9): pass else: hashboards[board].missing = False - try: - hashrate += rpc_stats["STATS"][1][f"chain_rate{chipset}"] - chips += rpc_stats["STATS"][1][f"chain_acn{chipset}"] - except (KeyError, IndexError): - pass + if rpc_stats is not None: + try: + hashrate += rpc_stats["STATS"][1][f"chain_rate{chipset}"] + chips += rpc_stats["STATS"][1][f"chain_acn{chipset}"] + except (KeyError, IndexError): + pass hashboards[board].hashrate = AlgoHashRate.SHA256( rate=float(hashrate), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) @@ -120,8 +120,8 @@ class HiveonT9(HiveonOld, T9): return hashboards - async def _get_env_temp(self, rpc_stats: dict = None) -> Optional[float]: - env_temp_list = [] + async def _get_env_temp(self, rpc_stats: dict | None = None) -> float | None: + env_temp_list: list[int] = [] board_map = { 0: [2, 9, 10], 1: [3, 11, 12], @@ -144,3 +144,4 @@ class HiveonT9(HiveonOld, T9): if not env_temp_list == []: return round(sum(env_temp_list) / len(env_temp_list)) + return None diff --git a/pyasic/miners/antminer/luxos/X21/T21.py b/pyasic/miners/antminer/luxos/X21/T21.py index bce5f517..c50edfa1 100644 --- a/pyasic/miners/antminer/luxos/X21/T21.py +++ b/pyasic/miners/antminer/luxos/X21/T21.py @@ -15,7 +15,7 @@ # ------------------------------------------------------------------------------ from pyasic.miners.backends import LUXMiner -from pyasic.miners.device.models import S21, T21 +from pyasic.miners.device.models import T21 class LUXMinerT21(LUXMiner, T21): diff --git a/pyasic/miners/antminer/vnish/X19/S19.py b/pyasic/miners/antminer/vnish/X19/S19.py index 5a4b236c..d79797e3 100644 --- a/pyasic/miners/antminer/vnish/X19/S19.py +++ b/pyasic/miners/antminer/vnish/X19/S19.py @@ -87,7 +87,3 @@ class VNishS19ProHydro(VNish, S19ProHydro): class VNishS19kPro(VNish, S19kPro): pass - - -class VNishS19ProA(VNish, S19ProA): - pass diff --git a/pyasic/miners/avalonminer/cgminer/nano/nano3.py b/pyasic/miners/avalonminer/cgminer/nano/nano3.py index 17080dfd..74fe0b95 100644 --- a/pyasic/miners/avalonminer/cgminer/nano/nano3.py +++ b/pyasic/miners/avalonminer/cgminer/nano/nano3.py @@ -13,11 +13,11 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional +from typing import Any from pyasic import APIError from pyasic.data.boards import HashBoard -from pyasic.device.algorithm.hashrate import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.miners.backends import AvalonMiner from pyasic.miners.data import ( DataFunction, @@ -150,12 +150,12 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3): data_locations = AVALON_NANO_DATA_LOC - async def _get_mac(self, web_minerinfo: dict) -> Optional[dict]: + async def _get_mac(self, web_minerinfo: dict[Any, Any] | None = None) -> str | None: if web_minerinfo is None: try: web_minerinfo = await self.web.minerinfo() except APIError: - pass + return None if web_minerinfo is not None: try: @@ -164,17 +164,18 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3): return mac.upper() except (KeyError, ValueError): pass + return None class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s): data_locations = AVALON_NANO3S_DATA_LOC - async def _get_wattage(self, rpc_estats: dict = None) -> Optional[int]: + async def _get_wattage(self, rpc_estats: dict | None = None) -> int | None: if rpc_estats is None: try: rpc_estats = await self.rpc.estats() except APIError: - pass + return None if rpc_estats is not None: try: @@ -182,13 +183,16 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s): return int(parsed_estats["PS"][6]) except (IndexError, KeyError, ValueError, TypeError): pass + return None - async def _get_hashrate(self, rpc_estats: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_estats: dict | None = None + ) -> AlgoHashRateType | None: if rpc_estats is None: try: rpc_estats = await self.rpc.estats() except APIError: - pass + return None if rpc_estats is not None: try: @@ -198,15 +202,16 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s): ).into(self.algo.unit.default) except (IndexError, KeyError, ValueError, TypeError): pass + return None - async def _get_hashboards(self, rpc_estats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_estats: dict | None = None) -> list[HashBoard]: hashboards = await AvalonMiner._get_hashboards(self, rpc_estats) if rpc_estats is None: try: rpc_estats = await self.rpc.estats() except APIError: - pass + return hashboards if rpc_estats is not None: try: diff --git a/pyasic/miners/backends/antminer.py b/pyasic/miners/backends/antminer.py index b2c212b3..bdce3ddd 100644 --- a/pyasic/miners/backends/antminer.py +++ b/pyasic/miners/backends/antminer.py @@ -16,13 +16,12 @@ import logging from pathlib import Path -from typing import List, Optional from pyasic.config import MinerConfig, MiningModeConfig from pyasic.data import Fan, HashBoard -from pyasic.data.error_codes import MinerErrorData, X19Error +from pyasic.data.error_codes import X19Error from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.backends.bmminer import BMMiner from pyasic.miners.backends.cgminer import CGMiner @@ -120,9 +119,11 @@ class AntminerModern(BMMiner): data = await self.web.get_miner_conf() if data: self.config = MinerConfig.from_am_modern(data) - return self.config + return self.config or MinerConfig() - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config await self.web.set_miner_conf(config.as_am_modern(user_suffix=user_suffix)) # if data: @@ -135,54 +136,77 @@ class AntminerModern(BMMiner): # break # await asyncio.sleep(1) - async def upgrade_firmware(self, file: Path, keep_settings: bool = True) -> str: + async def upgrade_firmware( + self, + *, + file: str | None = None, + url: str | None = None, + version: str | None = None, + keep_settings: bool = True, + ) -> bool: """ Upgrade the firmware of the AntMiner device. Args: - file (Path): Path to the firmware file. - keep_settings (bool): Whether to keep the current settings after the update. + file: Path to the firmware file as a string. + url: URL to download firmware from (not implemented). + version: Version to upgrade to (not implemented). + keep_settings: Whether to keep the current settings after the update. Returns: - str: Result of the upgrade process. + bool: True if upgrade was successful, False otherwise. """ if not file: - raise ValueError("File location must be provided for firmware upgrade.") + logging.error("File location must be provided for firmware upgrade.") + return False + + if url or version: + logging.warning( + "URL and version parameters are not implemented for Antminer." + ) try: + file_path = Path(file) + + if not hasattr(self.web, "update_firmware"): + logging.error( + "Firmware upgrade not supported via web API for this Antminer model." + ) + return False + result = await self.web.update_firmware( - file=file, keep_settings=keep_settings + file=file_path, keep_settings=keep_settings ) if result.get("success"): logging.info( "Firmware upgrade process completed successfully for AntMiner." ) - return "Firmware upgrade completed successfully." + return True else: error_message = result.get("message", "Unknown error") logging.error(f"Firmware upgrade failed. Response: {error_message}") - return f"Firmware upgrade failed. Response: {error_message}" + return False except Exception as e: logging.error( f"An error occurred during the firmware upgrade process: {e}", exc_info=True, ) - raise + return False async def fault_light_on(self) -> bool: data = await self.web.blink(blink=True) if data: if data.get("code") == "B000": self.light = True - return self.light + return self.light or False async def fault_light_off(self) -> bool: data = await self.web.blink(blink=False) if data: if data.get("code") == "B100": self.light = False - return self.light + return self.light or False async def reboot(self) -> bool: data = await self.web.reboot() @@ -202,7 +226,9 @@ class AntminerModern(BMMiner): await self.send_config(cfg) return True - async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_hostname( + self, web_get_system_info: dict | None = None + ) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -214,8 +240,9 @@ class AntminerModern(BMMiner): return web_get_system_info["hostname"] except KeyError: pass + return None - async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -234,8 +261,11 @@ class AntminerModern(BMMiner): return data["macaddr"] except KeyError: pass + return None - async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]: + async def _get_errors( # type: ignore[override] + self, web_summary: dict | None = None + ) -> list[X19Error]: if web_summary is None: try: web_summary = await self.web.summary() @@ -255,7 +285,7 @@ class AntminerModern(BMMiner): pass return errors - async def _get_hashboards(self) -> List[HashBoard]: + async def _get_hashboards(self) -> list[HashBoard]: # type: ignore[override] if self.expected_hashboards is None: return [] @@ -273,8 +303,11 @@ class AntminerModern(BMMiner): try: for board in rpc_stats["STATS"][0]["chain"]: hashboards[board["index"]].hashrate = self.algo.hashrate( - rate=board["rate_real"], unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=board["rate_real"], + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hashboards[board["index"]].chips = board["asic_num"] if "S21+ Hyd" in self.model: @@ -324,8 +357,8 @@ class AntminerModern(BMMiner): return hashboards async def _get_fault_light( - self, web_get_blink_status: dict = None - ) -> Optional[bool]: + self, web_get_blink_status: dict | None = None + ) -> bool | None: if self.light: return self.light @@ -343,8 +376,8 @@ class AntminerModern(BMMiner): return self.light async def _get_expected_hashrate( - self, rpc_stats: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_stats: dict | None = None + ) -> AlgoHashRateType | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -360,13 +393,14 @@ class AntminerModern(BMMiner): rate_unit = "GH" return self.algo.hashrate( rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) - ).into(self.algo.unit.default) + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass + return None async def _get_serial_number( - self, web_get_system_info: dict = None - ) -> Optional[str]: + self, web_get_system_info: dict | None = None + ) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -378,6 +412,7 @@ class AntminerModern(BMMiner): return web_get_system_info["serinum"] except LookupError: pass + return None async def set_static_ip( self, @@ -385,10 +420,10 @@ class AntminerModern(BMMiner): dns: str, gateway: str, subnet_mask: str = "255.255.255.0", - hostname: str = None, + hostname: str | None = None, ): if not hostname: - hostname = await self.get_hostname() + hostname = await self.get_hostname() or "" await self.web.set_network_conf( ip=ip, dns=dns, @@ -398,9 +433,9 @@ class AntminerModern(BMMiner): protocol=2, ) - async def set_dhcp(self, hostname: str = None): + async def set_dhcp(self, hostname: str | None = None): if not hostname: - hostname = await self.get_hostname() + hostname = await self.get_hostname() or "" await self.web.set_network_conf( ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1 ) @@ -421,7 +456,7 @@ class AntminerModern(BMMiner): protocol=protocol, ) - async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]: + async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None: if web_get_conf is None: try: web_get_conf = await self.web.get_miner_conf() @@ -437,8 +472,9 @@ class AntminerModern(BMMiner): return False except LookupError: pass + return None - async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: + async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -450,8 +486,9 @@ class AntminerModern(BMMiner): return int(rpc_stats["STATS"][1]["Elapsed"]) except LookupError: pass + return None - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() @@ -540,19 +577,22 @@ class AntminerOld(CGMiner): data = await self.web.get_miner_conf() if data: self.config = MinerConfig.from_am_old(data) - return self.config + return self.config or MinerConfig() - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config await self.web.set_miner_conf(config.as_am_old(user_suffix=user_suffix)) - async def _get_mac(self) -> Optional[str]: + async def _get_mac(self) -> str | None: try: data = await self.web.get_system_info() if data: return data["macaddr"] except KeyError: pass + return None async def fault_light_on(self) -> bool: # this should time out, after it does do a check @@ -564,7 +604,7 @@ class AntminerOld(CGMiner): self.light = True except KeyError: pass - return self.light + return self.light or False async def fault_light_off(self) -> bool: await self.web.blink(blink=False) @@ -575,7 +615,7 @@ class AntminerOld(CGMiner): self.light = False except KeyError: pass - return self.light + return self.light or False async def reboot(self) -> bool: data = await self.web.reboot() @@ -584,8 +624,8 @@ class AntminerOld(CGMiner): return False async def _get_fault_light( - self, web_get_blink_status: dict = None - ) -> Optional[bool]: + self, web_get_blink_status: dict | None = None + ) -> bool | None: if self.light: return self.light @@ -602,7 +642,9 @@ class AntminerOld(CGMiner): pass return self.light - async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_hostname( + self, web_get_system_info: dict | None = None + ) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -614,8 +656,9 @@ class AntminerOld(CGMiner): return web_get_system_info["hostname"] except KeyError: pass + return None - async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -640,16 +683,16 @@ class AntminerOld(CGMiner): for fan in range(self.expected_fans): fans_data[fan].speed = rpc_stats["STATS"][1].get( - f"fan{fan_offset+fan}", 0 + f"fan{fan_offset + fan}", 0 ) except LookupError: pass return fans_data - async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]: if self.expected_hashboards is None: return [] - hashboards = [] + hashboards: list[HashBoard] = [] if rpc_stats is None: try: @@ -689,8 +732,11 @@ class AntminerOld(CGMiner): hashrate = boards[1].get(f"chain_rate{i}") if hashrate: hashboard.hashrate = self.algo.hashrate( - rate=float(hashrate), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(hashrate), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) chips = boards[1].get(f"chain_acn{i}") if chips: @@ -707,7 +753,7 @@ class AntminerOld(CGMiner): return hashboards - async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]: + async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None: if web_get_conf is None: try: web_get_conf = await self.web.get_miner_conf() @@ -732,7 +778,9 @@ class AntminerOld(CGMiner): else: return False - async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: + return None + + async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -744,3 +792,4 @@ class AntminerOld(CGMiner): return int(rpc_stats["STATS"][1]["Elapsed"]) except LookupError: pass + return None diff --git a/pyasic/miners/backends/auradine.py b/pyasic/miners/backends/auradine.py index d3d50e37..22e883a6 100644 --- a/pyasic/miners/backends/auradine.py +++ b/pyasic/miners/backends/auradine.py @@ -15,11 +15,10 @@ # ------------------------------------------------------------------------------ import logging from enum import Enum -from typing import List, Optional from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import ( DataFunction, @@ -187,7 +186,9 @@ class Auradine(StockFirmware): pass return MinerConfig() - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config conf = config.as_auradine(user_suffix=user_suffix) @@ -197,8 +198,8 @@ class Auradine(StockFirmware): async def upgrade_firmware( self, *, - url: str = None, - version: str = "latest", + url: str | None = None, + version: str | None = "latest", keep_settings: bool = False, **kwargs, ) -> bool: @@ -223,8 +224,12 @@ class Auradine(StockFirmware): if url: result = await self.web.firmware_upgrade(url=url) - else: + elif version: result = await self.web.firmware_upgrade(version=version) + else: + raise ValueError( + "Either URL or version must be provided for firmware upgrade." + ) if result.get("STATUS", [{}])[0].get("STATUS") == "S": logging.info("Firmware upgrade process completed successfully.") @@ -245,7 +250,7 @@ class Auradine(StockFirmware): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_mac(self, web_ipreport: dict = None) -> Optional[str]: + async def _get_mac(self, web_ipreport: dict | None = None) -> str | None: if web_ipreport is None: try: web_ipreport = await self.web.ipreport() @@ -257,8 +262,9 @@ class Auradine(StockFirmware): return web_ipreport["IPReport"][0]["mac"].upper() except (LookupError, AttributeError): pass + return None - async def _get_fw_ver(self, web_ipreport: dict = None) -> Optional[str]: + async def _get_fw_ver(self, web_ipreport: dict | None = None) -> str | None: if web_ipreport is None: try: web_ipreport = await self.web.ipreport() @@ -270,8 +276,9 @@ class Auradine(StockFirmware): return web_ipreport["IPReport"][0]["version"] except LookupError: pass + return None - async def _get_hostname(self, web_ipreport: dict = None) -> Optional[str]: + async def _get_hostname(self, web_ipreport: dict | None = None) -> str | None: if web_ipreport is None: try: web_ipreport = await self.web.ipreport() @@ -283,8 +290,11 @@ class Auradine(StockFirmware): return web_ipreport["IPReport"][0]["hostname"] except LookupError: pass + return None - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -295,14 +305,15 @@ class Auradine(StockFirmware): try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (LookupError, ValueError, TypeError): pass + return None async def _get_hashboards( - self, rpc_devs: dict = None, web_ipreport: dict = None - ) -> List[HashBoard]: + self, rpc_devs: dict | None = None, web_ipreport: dict | None = None + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -327,8 +338,11 @@ class Auradine(StockFirmware): for board in rpc_devs["DEVS"]: b_id = board["ID"] - 1 hashboards[b_id].hashrate = self.algo.hashrate( - rate=float(board["MHS 5s"]), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=float(board["MHS 5s"]), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hashboards[b_id].temp = round(float(board["Temperature"])) hashboards[b_id].missing = False except LookupError: @@ -344,7 +358,7 @@ class Auradine(StockFirmware): return hashboards - async def _get_wattage(self, web_psu: dict = None) -> Optional[int]: + async def _get_wattage(self, web_psu: dict | None = None) -> int | None: if web_psu is None: try: web_psu = await self.web.get_psu() @@ -356,10 +370,11 @@ class Auradine(StockFirmware): return int(float(web_psu["PSU"][0]["PowerIn"].replace("W", ""))) except (LookupError, TypeError, ValueError): pass + return None async def _get_wattage_limit( - self, web_mode: dict = None, web_psu: dict = None - ) -> Optional[int]: + self, web_mode: dict | None = None, web_psu: dict | None = None + ) -> int | None: if web_mode is None: try: web_mode = await self.web.get_mode() @@ -383,8 +398,9 @@ class Auradine(StockFirmware): return int(float(web_psu["PSU"][0]["PoutMax"].replace("W", ""))) except (LookupError, TypeError, ValueError): pass + return None - async def _get_fans(self, web_fan: dict = None) -> List[Fan]: + async def _get_fans(self, web_fan: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -403,7 +419,7 @@ class Auradine(StockFirmware): pass return fans - async def _get_fault_light(self, web_led: dict = None) -> Optional[bool]: + async def _get_fault_light(self, web_led: dict | None = None) -> bool | None: if web_led is None: try: web_led = await self.web.get_led() @@ -415,8 +431,9 @@ class Auradine(StockFirmware): return web_led["LED"][0]["Code"] == int(AuradineLEDCodes.LOCATE_MINER) except LookupError: pass + return None - async def _is_mining(self, web_mode: dict = None) -> Optional[bool]: + async def _is_mining(self, web_mode: dict | None = None) -> bool | None: if web_mode is None: try: web_mode = await self.web.get_mode() @@ -428,8 +445,9 @@ class Auradine(StockFirmware): return web_mode["Mode"][0]["Sleep"] == "off" except (LookupError, TypeError, ValueError): pass + return None - async def _get_uptime(self, rpc_summary: dict = None) -> Optional[int]: + async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -441,3 +459,4 @@ class Auradine(StockFirmware): return rpc_summary["SUMMARY"][0]["Elapsed"] except LookupError: pass + return None diff --git a/pyasic/miners/backends/avalonminer.py b/pyasic/miners/backends/avalonminer.py index 3d099231..1574dd80 100644 --- a/pyasic/miners/backends/avalonminer.py +++ b/pyasic/miners/backends/avalonminer.py @@ -16,10 +16,9 @@ import copy import re import time -from typing import List, Optional from pyasic.data import Fan, HashBoard -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.backends.cgminer import CGMiner from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand @@ -119,7 +118,7 @@ class AvalonMiner(CGMiner): limit = 1 else: limit = 0 - data = await self.rpc.ascset(0, "worklevel,set", 1) + data = await self.rpc.ascset(0, "worklevel,set", limit) except APIError: return False if data["STATUS"][0]["Msg"] == "ASC 0 set OK": @@ -143,7 +142,7 @@ class AvalonMiner(CGMiner): try: # Shut off 5 seconds from now timestamp = int(time.time()) + 5 - data = await self.rpc.ascset(0, f"softoff", f"1:{timestamp}") + data = await self.rpc.ascset(0, "softoff", f"1:{timestamp}") except APIError: return False if "success" in data["STATUS"][0]["Msg"]: @@ -154,7 +153,7 @@ class AvalonMiner(CGMiner): try: # Shut off 5 seconds from now timestamp = int(time.time()) + 5 - data = await self.rpc.ascset(0, f"softon", f"1:{timestamp}") + data = await self.rpc.ascset(0, "softon", f"1:{timestamp}") except APIError: return False if "success" in data["STATUS"][0]["Msg"]: @@ -232,7 +231,7 @@ class AvalonMiner(CGMiner): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_mac(self, rpc_version: dict = None) -> Optional[str]: + async def _get_mac(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -249,23 +248,28 @@ class AvalonMiner(CGMiner): return mac except (KeyError, ValueError): pass + return None - async def _get_hashrate(self, rpc_devs: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_devs: dict | None = None + ) -> AlgoHashRateType | None: if rpc_devs is None: try: rpc_devs = await self.rpc.devs() except APIError: - pass + return None if rpc_devs is not None: try: return self.algo.hashrate( - rate=float(rpc_devs["DEVS"][0]["MHS 1m"]), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=float(rpc_devs["DEVS"][0]["MHS 1m"]), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (KeyError, IndexError, ValueError, TypeError): pass + return None - async def _get_hashboards(self, rpc_estats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_estats: dict | None = None) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -291,12 +295,18 @@ class AvalonMiner(CGMiner): board_hr = parsed_estats["STATS"][0]["MM ID0"]["MGHS"] if isinstance(board_hr, list): hashboards[board].hashrate = self.algo.hashrate( - rate=float(board_hr[board]), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(board_hr[board]), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) else: hashboards[board].hashrate = self.algo.hashrate( - rate=float(board_hr), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(board_hr), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except LookupError: pass @@ -376,24 +386,26 @@ class AvalonMiner(CGMiner): return hashboards async def _get_expected_hashrate( - self, rpc_estats: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_estats: dict | None = None + ) -> AlgoHashRateType | None: if rpc_estats is None: try: rpc_estats = await self.rpc.estats() except APIError: - pass + return None if rpc_estats is not None: try: parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"] return self.algo.hashrate( - rate=float(parsed_estats["GHSmm"]), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(parsed_estats["GHSmm"]), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (IndexError, KeyError, ValueError, TypeError): pass + return None - async def _get_env_temp(self, rpc_estats: dict = None) -> Optional[float]: + async def _get_env_temp(self, rpc_estats: dict | None = None) -> float | None: if rpc_estats is None: try: rpc_estats = await self.rpc.estats() @@ -406,13 +418,14 @@ class AvalonMiner(CGMiner): return float(parsed_estats["Temp"]) except (IndexError, KeyError, ValueError, TypeError): pass + return None - async def _get_wattage_limit(self, rpc_estats: dict = None) -> Optional[int]: + async def _get_wattage_limit(self, rpc_estats: dict | None = None) -> int | None: if rpc_estats is None: try: rpc_estats = await self.rpc.estats() except APIError: - pass + return None if rpc_estats is not None: try: @@ -420,8 +433,9 @@ class AvalonMiner(CGMiner): return int(parsed_estats["MPO"]) except (IndexError, KeyError, ValueError, TypeError): pass + return None - async def _get_wattage(self, rpc_estats: dict = None) -> Optional[int]: + async def _get_wattage(self, rpc_estats: dict | None = None) -> int | None: if rpc_estats is None: try: rpc_estats = await self.rpc.estats() @@ -434,8 +448,9 @@ class AvalonMiner(CGMiner): return int(parsed_estats["WALLPOWER"]) except (IndexError, KeyError, ValueError, TypeError): pass + return None - async def _get_fans(self, rpc_estats: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_estats: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -459,7 +474,7 @@ class AvalonMiner(CGMiner): pass return fans_data - async def _get_fault_light(self, rpc_estats: dict = None) -> Optional[bool]: + async def _get_fault_light(self, rpc_estats: dict | None = None) -> bool | None: if self.light: return self.light if rpc_estats is None: diff --git a/pyasic/miners/backends/bfgminer.py b/pyasic/miners/backends/bfgminer.py index 484c04f5..e93a68e3 100644 --- a/pyasic/miners/backends/bfgminer.py +++ b/pyasic/miners/backends/bfgminer.py @@ -14,12 +14,10 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional - from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.device.firmware import StockFirmware @@ -72,7 +70,8 @@ class BFGMiner(StockFirmware): try: pools = await self.rpc.pools() except APIError: - return self.config + if self.config is not None: + return self.config self.config = MinerConfig.from_api(pools) return self.config @@ -81,7 +80,7 @@ class BFGMiner(StockFirmware): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -96,7 +95,7 @@ class BFGMiner(StockFirmware): return self.api_ver - async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -111,7 +110,9 @@ class BFGMiner(StockFirmware): return self.fw_ver - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: # get hr from API if rpc_summary is None: try: @@ -123,12 +124,15 @@ class BFGMiner(StockFirmware): try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["MHS 20s"]), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except (LookupError, ValueError, TypeError): pass + return None - async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -172,8 +176,11 @@ class BFGMiner(StockFirmware): hashrate = boards[1].get(f"chain_rate{i}") if hashrate: hashboard.hashrate = self.algo.hashrate( - rate=float(hashrate), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(hashrate), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) chips = boards[1].get(f"chain_acn{i}") if chips: @@ -187,7 +194,7 @@ class BFGMiner(StockFirmware): return hashboards - async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -212,7 +219,7 @@ class BFGMiner(StockFirmware): for fan in range(self.expected_fans): fans_data[fan] = rpc_stats["STATS"][1].get( - f"fan{fan_offset+fan}", 0 + f"fan{fan_offset + fan}", 0 ) except LookupError: pass @@ -220,7 +227,7 @@ class BFGMiner(StockFirmware): return fans - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() @@ -251,8 +258,8 @@ class BFGMiner(StockFirmware): return pools_data async def _get_expected_hashrate( - self, rpc_stats: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_stats: dict | None = None + ) -> AlgoHashRateType | None: # X19 method, not sure compatibility if rpc_stats is None: try: @@ -269,6 +276,7 @@ class BFGMiner(StockFirmware): rate_unit = "GH" return self.algo.hashrate( rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) - ).into(self.algo.unit.default) + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass + return None diff --git a/pyasic/miners/backends/bmminer.py b/pyasic/miners/backends/bmminer.py index 32767e59..5190957c 100644 --- a/pyasic/miners/backends/bmminer.py +++ b/pyasic/miners/backends/bmminer.py @@ -14,12 +14,11 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.device.firmware import StockFirmware @@ -76,7 +75,8 @@ class BMMiner(StockFirmware): try: pools = await self.rpc.pools() except APIError: - return self.config + if self.config is not None: + return self.config self.config = MinerConfig.from_api(pools) return self.config @@ -85,7 +85,7 @@ class BMMiner(StockFirmware): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -100,7 +100,7 @@ class BMMiner(StockFirmware): return self.api_ver - async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -115,7 +115,9 @@ class BMMiner(StockFirmware): return self.fw_ver - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: # get hr from API if rpc_summary is None: try: @@ -127,12 +129,15 @@ class BMMiner(StockFirmware): try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default) + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except (LookupError, ValueError, TypeError): pass + return None - async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -189,8 +194,11 @@ class BMMiner(StockFirmware): hashrate = boards[1].get(f"chain_rate{i}") if hashrate: hashboard.hashrate = self.algo.hashrate( - rate=float(hashrate), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(hashrate), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) chips = boards[1].get(f"chain_acn{i}") if chips: @@ -204,7 +212,7 @@ class BMMiner(StockFirmware): return hashboards - async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -229,7 +237,7 @@ class BMMiner(StockFirmware): for fan in range(self.expected_fans): fans[fan].speed = rpc_stats["STATS"][1].get( - f"fan{fan_offset+fan}", 0 + f"fan{fan_offset + fan}", 0 ) except LookupError: pass @@ -237,8 +245,8 @@ class BMMiner(StockFirmware): return fans async def _get_expected_hashrate( - self, rpc_stats: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_stats: dict | None = None + ) -> AlgoHashRateType | None: # X19 method, not sure compatibility if rpc_stats is None: try: @@ -255,11 +263,14 @@ class BMMiner(StockFirmware): rate_unit = "GH" return self.algo.hashrate( rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) - ).into(self.algo.unit.default) + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except LookupError: pass + return None - async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: + async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -271,8 +282,9 @@ class BMMiner(StockFirmware): return int(rpc_stats["STATS"][1]["Elapsed"]) except LookupError: pass + return None - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() diff --git a/pyasic/miners/backends/braiins_os.py b/pyasic/miners/backends/braiins_os.py index cbb7af0a..7c3a6b53 100644 --- a/pyasic/miners/backends/braiins_os.py +++ b/pyasic/miners/backends/braiins_os.py @@ -16,8 +16,6 @@ import base64 import logging import time -from pathlib import Path -from typing import List, Optional, Union import aiofiles import tomli_w @@ -25,14 +23,14 @@ import tomli_w try: import tomllib except ImportError: - import tomli as tomllib + import tomli as tomllib # type: ignore from pyasic.config import MinerConfig from pyasic.config.mining import MiningModePowerTune from pyasic.data import Fan, HashBoard from pyasic.data.error_codes import BraiinsOSError, MinerErrorData from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate, AlgoHashRateType +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import ( DataFunction, @@ -193,7 +191,9 @@ class BOSMiner(BraiinsOSFirmware): return self.config - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config parsed_cfg = config.as_bosminer(user_suffix=user_suffix) @@ -202,21 +202,18 @@ class BOSMiner(BraiinsOSFirmware): "format": { "version": "2.0", "generator": "pyasic", - "model": f"{self.make.replace('Miner', 'miner')} {self.raw_model.replace('j', 'J')}", + "model": f"{self.make.replace('Miner', 'miner') if self.make else ''} {self.raw_model.replace('j', 'J') if self.raw_model else ''}", "timestamp": int(time.time()), }, **parsed_cfg, } ) try: - conn = await self.ssh._get_connection() - except ConnectionError as e: - raise APIError("SSH connection failed when sending config.") from e - - async with conn: - await conn.run("/etc/init.d/bosminer stop") - await conn.run("echo '" + toml_conf + "' > /etc/bosminer.toml") - await conn.run("/etc/init.d/bosminer start") + await self.ssh.send_command("/etc/init.d/bosminer stop") + await self.ssh.send_command("echo '" + toml_conf + "' > /etc/bosminer.toml") + await self.ssh.send_command("/etc/init.d/bosminer start") + except Exception as e: + raise APIError("SSH command failed when sending config.") from e async def set_power_limit(self, wattage: int) -> bool: try: @@ -285,12 +282,12 @@ class BOSMiner(BraiinsOSFirmware): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_mac(self, web_net_conf: Union[dict, list] = None) -> Optional[str]: + async def _get_mac(self, web_net_conf: dict | list | None = None) -> str | None: if web_net_conf is None: try: web_net_conf = await self.web.get_net_conf() except APIError: - pass + return None if isinstance(web_net_conf, dict): if "admin/network/iface_status/lan" in web_net_conf.keys(): @@ -301,17 +298,18 @@ class BOSMiner(BraiinsOSFirmware): return web_net_conf[0]["macaddr"] except LookupError: pass + return None # could use ssh, but its slow and buggy # result = await self.send_ssh_command("cat /sys/class/net/eth0/address") # if result: # return result.upper().strip() - async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() except APIError: - pass + return None # Now get the API version if rpc_version is not None: @@ -320,17 +318,20 @@ class BOSMiner(BraiinsOSFirmware): except LookupError: rpc_ver = None self.api_ver = rpc_ver - self.rpc.rpc_ver = self.api_ver + self.rpc.rpc_ver = self.api_ver # type: ignore return self.api_ver - async def _get_fw_ver(self, web_bos_info: dict = None) -> Optional[str]: + async def _get_fw_ver(self, web_bos_info: dict | None = None) -> str | None: if web_bos_info is None: try: web_bos_info = await self.web.get_bos_info() except APIError: return None + if web_bos_info is None: + return None + if isinstance(web_bos_info, dict): if "bos/info" in web_bos_info.keys(): web_bos_info = web_bos_info["bos/info"] @@ -344,7 +345,7 @@ class BOSMiner(BraiinsOSFirmware): return self.fw_ver - async def _get_hostname(self) -> Union[str, None]: + async def _get_hostname(self) -> str | None: try: hostname = (await self.ssh.get_hostname()).strip() except AttributeError: @@ -354,28 +355,31 @@ class BOSMiner(BraiinsOSFirmware): return None return hostname - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() except APIError: - pass + return None if rpc_summary is not None: try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (KeyError, IndexError, ValueError, TypeError): pass + return None async def _get_hashboards( self, - rpc_temps: dict = None, - rpc_devdetails: dict = None, - rpc_devs: dict = None, - ) -> List[HashBoard]: + rpc_temps: dict | None = None, + rpc_devdetails: dict | None = None, + rpc_devs: dict | None = None, + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -440,19 +444,22 @@ class BOSMiner(BraiinsOSFirmware): for board in rpc_devs["DEVS"]: _id = board["ID"] - offset hashboards[_id].hashrate = self.algo.hashrate( - rate=float(board["MHS 1m"]), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=float(board["MHS 1m"]), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except (IndexError, KeyError): pass return hashboards - async def _get_wattage(self, rpc_tunerstatus: dict = None) -> Optional[int]: + async def _get_wattage(self, rpc_tunerstatus: dict | None = None) -> int | None: if rpc_tunerstatus is None: try: rpc_tunerstatus = await self.rpc.tunerstatus() except APIError: - pass + return None if rpc_tunerstatus is not None: try: @@ -461,21 +468,25 @@ class BOSMiner(BraiinsOSFirmware): ] except LookupError: pass + return None - async def _get_wattage_limit(self, rpc_tunerstatus: dict = None) -> Optional[int]: + async def _get_wattage_limit( + self, rpc_tunerstatus: dict | None = None + ) -> int | None: if rpc_tunerstatus is None: try: rpc_tunerstatus = await self.rpc.tunerstatus() except APIError: - pass + return None if rpc_tunerstatus is not None: try: return rpc_tunerstatus["TUNERSTATUS"][0]["PowerLimit"] except LookupError: pass + return None - async def _get_fans(self, rpc_fans: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_fans: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -483,7 +494,7 @@ class BOSMiner(BraiinsOSFirmware): try: rpc_fans = await self.rpc.fans() except APIError: - pass + return [Fan() for _ in range(self.expected_fans)] if rpc_fans is not None: fans = [] @@ -495,12 +506,14 @@ class BOSMiner(BraiinsOSFirmware): return fans return [Fan() for _ in range(self.expected_fans)] - async def _get_errors(self, rpc_tunerstatus: dict = None) -> List[MinerErrorData]: + async def _get_errors( + self, rpc_tunerstatus: dict | None = None + ) -> list[MinerErrorData]: if rpc_tunerstatus is None: try: rpc_tunerstatus = await self.rpc.tunerstatus() except APIError: - pass + return [] if rpc_tunerstatus is not None: errors = [] @@ -523,9 +536,10 @@ class BOSMiner(BraiinsOSFirmware): errors.append( BraiinsOSError(error_message=f"Slot {_id} {_error}") ) - return errors + return errors # type: ignore except (KeyError, IndexError): pass + return [] async def _get_fault_light(self) -> bool: if self.light: @@ -537,16 +551,16 @@ class BOSMiner(BraiinsOSFirmware): self.light = True return self.light except (TypeError, AttributeError): - return self.light + return self.light or False async def _get_expected_hashrate( - self, rpc_devs: dict = None - ) -> Optional[AlgoHashRateType]: + self, rpc_devs: dict | None = None + ) -> AlgoHashRateType | None: if rpc_devs is None: try: rpc_devs = await self.rpc.devs() except APIError: - pass + return None if rpc_devs is not None: try: @@ -559,52 +573,57 @@ class BOSMiner(BraiinsOSFirmware): if len(hr_list) == 0: return self.algo.hashrate( - rate=float(0), unit=self.algo.unit.default + rate=float(0), + unit=self.algo.unit.default, # type: ignore ) else: return self.algo.hashrate( rate=float( - (sum(hr_list) / len(hr_list)) * self.expected_hashboards + (sum(hr_list) / len(hr_list)) + * (self.expected_hashboards or 1) ), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (IndexError, KeyError): pass + return None - async def _is_mining(self, rpc_devdetails: dict = None) -> Optional[bool]: + async def _is_mining(self, rpc_devdetails: dict | None = None) -> bool | None: if rpc_devdetails is None: try: rpc_devdetails = await self.rpc.send_command( "devdetails", ignore_errors=True, allow_warning=False ) except APIError: - pass + return None if rpc_devdetails is not None: try: return not rpc_devdetails["STATUS"][0]["Msg"] == "Unavailable" except LookupError: pass + return None - async def _get_uptime(self, rpc_summary: dict = None) -> Optional[int]: + async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() except APIError: - pass + return None if rpc_summary is not None: try: return int(rpc_summary["SUMMARY"][0]["Elapsed"]) except LookupError: pass + return None - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() except APIError: - pass + return [] pools_data = [] if rpc_pools is not None: @@ -629,15 +648,25 @@ class BOSMiner(BraiinsOSFirmware): pass return pools_data - async def upgrade_firmware(self, file: Path) -> str: + async def upgrade_firmware( + self, + *, + file: str | None = None, + url: str | None = None, + version: str | None = None, + keep_settings: bool = True, + ) -> bool: """ Upgrade the firmware of the BOSMiner device. Args: - file (Path): The local file path of the firmware to be uploaded. + file: The local file path of the firmware to be uploaded. + url: URL of firmware to download (not used in this implementation). + version: Specific version to upgrade to (not used in this implementation). + keep_settings: Whether to keep current settings (not used in this implementation). Returns: - Confirmation message after upgrading the firmware. + True if upgrade was successful, False otherwise. """ try: logging.info("Starting firmware upgrade process.") @@ -659,24 +688,24 @@ class BOSMiner(BraiinsOSFirmware): ) logging.info("Firmware upgrade process completed successfully.") - return "Firmware upgrade completed successfully." + return True except FileNotFoundError as e: logging.error(f"File not found during the firmware upgrade process: {e}") - raise + return False except ValueError as e: logging.error( f"Validation error occurred during the firmware upgrade process: {e}" ) - raise + return False except OSError as e: logging.error(f"OS error occurred during the firmware upgrade process: {e}") - raise + return False except Exception as e: logging.error( f"An unexpected error occurred during the firmware upgrade process: {e}", exc_info=True, ) - raise + return False BOSER_DATA_LOC = DataLocations( @@ -805,7 +834,9 @@ class BOSer(BraiinsOSFirmware): return MinerConfig.from_boser(grpc_conf) - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: boser_cfg = config.as_boser(user_suffix=user_suffix) for key in boser_cfg: await self.web.send_command(key, message=boser_cfg[key]) @@ -813,7 +844,8 @@ class BOSer(BraiinsOSFirmware): async def set_power_limit(self, wattage: int) -> bool: try: result = await self.web.set_power_target( - wattage, save_action=SaveAction.SAVE_AND_FORCE_APPLY + wattage, + save_action=SaveAction(SaveAction.SAVE_AND_FORCE_APPLY), ) except APIError: return False @@ -829,25 +861,26 @@ class BOSer(BraiinsOSFirmware): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_mac(self, grpc_miner_details: dict = None) -> Optional[str]: + async def _get_mac(self, grpc_miner_details: dict | None = None) -> str | None: if grpc_miner_details is None: try: grpc_miner_details = await self.web.get_miner_details() except APIError: - pass + return None if grpc_miner_details is not None: try: return grpc_miner_details["macAddress"].upper() except (LookupError, TypeError): pass + return None - async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() except APIError: - pass + return None if rpc_version is not None: try: @@ -855,16 +888,16 @@ class BOSer(BraiinsOSFirmware): except LookupError: rpc_ver = None self.api_ver = rpc_ver - self.rpc.rpc_ver = self.api_ver + self.rpc.rpc_ver = self.api_ver # type: ignore return self.api_ver - async def _get_fw_ver(self, grpc_miner_details: dict = None) -> Optional[str]: + async def _get_fw_ver(self, grpc_miner_details: dict | None = None) -> str | None: if grpc_miner_details is None: try: grpc_miner_details = await self.web.get_miner_details() except APIError: - pass + return None fw_ver = None @@ -882,43 +915,47 @@ class BOSer(BraiinsOSFirmware): return self.fw_ver - async def _get_hostname(self, grpc_miner_details: dict = None) -> Optional[str]: + async def _get_hostname(self, grpc_miner_details: dict | None = None) -> str | None: if grpc_miner_details is None: try: grpc_miner_details = await self.web.get_miner_details() except APIError: - pass + return None if grpc_miner_details is not None: try: return grpc_miner_details["hostname"] except LookupError: pass + return None - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() except APIError: - pass + return None if rpc_summary is not None: try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (KeyError, IndexError, ValueError, TypeError): pass + return None async def _get_expected_hashrate( - self, grpc_miner_details: dict = None - ) -> Optional[AlgoHashRate]: + self, grpc_miner_details: dict | None = None + ) -> AlgoHashRateType | None: if grpc_miner_details is None: try: grpc_miner_details = await self.web.get_miner_details() except APIError: - pass + return None if grpc_miner_details is not None: try: @@ -926,12 +963,15 @@ class BOSer(BraiinsOSFirmware): rate=float( grpc_miner_details["stickerHashrate"]["gigahashPerSecond"] ), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default) + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass + return None - async def _get_hashboards(self, grpc_hashboards: dict = None) -> List[HashBoard]: + async def _get_hashboards( + self, grpc_hashboards: dict | None = None + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -944,7 +984,7 @@ class BOSer(BraiinsOSFirmware): try: grpc_hashboards = await self.web.get_hashboards() except APIError: - pass + return hashboards if grpc_hashboards is not None: grpc_boards = sorted( @@ -967,35 +1007,38 @@ class BOSer(BraiinsOSFirmware): "gigahashPerSecond" ] ), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default) + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hashboards[idx].missing = False return hashboards - async def _get_wattage(self, grpc_miner_stats: dict = None) -> Optional[int]: + async def _get_wattage(self, grpc_miner_stats: dict | None = None) -> int | None: if grpc_miner_stats is None: try: grpc_miner_stats = await self.web.get_miner_stats() except APIError: - pass + return None if grpc_miner_stats is not None: try: return grpc_miner_stats["powerStats"]["approximatedConsumption"]["watt"] except KeyError: pass + return None async def _get_wattage_limit( - self, grpc_active_performance_mode: dict = None - ) -> Optional[int]: + self, grpc_active_performance_mode: dict | None = None + ) -> int | None: if grpc_active_performance_mode is None: try: grpc_active_performance_mode = ( await self.web.get_active_performance_mode() ) except APIError: - pass + return None if grpc_active_performance_mode is not None: try: @@ -1004,8 +1047,9 @@ class BOSer(BraiinsOSFirmware): ]["watt"] except KeyError: pass + return None - async def _get_fans(self, grpc_cooling_state: dict = None) -> List[Fan]: + async def _get_fans(self, grpc_cooling_state: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -1013,7 +1057,7 @@ class BOSer(BraiinsOSFirmware): try: grpc_cooling_state = await self.web.get_cooling_state() except APIError: - pass + return [Fan() for _ in range(self.expected_fans)] if grpc_cooling_state is not None: fans = [] @@ -1025,12 +1069,14 @@ class BOSer(BraiinsOSFirmware): return fans return [Fan() for _ in range(self.expected_fans)] - async def _get_errors(self, rpc_tunerstatus: dict = None) -> List[MinerErrorData]: + async def _get_errors( + self, rpc_tunerstatus: dict | None = None + ) -> list[MinerErrorData]: if rpc_tunerstatus is None: try: rpc_tunerstatus = await self.rpc.tunerstatus() except APIError: - pass + return [] if rpc_tunerstatus is not None: errors = [] @@ -1053,11 +1099,14 @@ class BOSer(BraiinsOSFirmware): errors.append( BraiinsOSError(error_message=f"Slot {_id} {_error}") ) - return errors + return errors # type: ignore except LookupError: pass + return [] - async def _get_fault_light(self, grpc_locate_device_status: dict = None) -> bool: + async def _get_fault_light( + self, grpc_locate_device_status: dict | None = None + ) -> bool: if self.light is not None: return self.light @@ -1065,7 +1114,7 @@ class BOSer(BraiinsOSFirmware): try: grpc_locate_device_status = await self.web.get_locate_device_status() except APIError: - pass + return False if grpc_locate_device_status is not None: if grpc_locate_device_status == {}: @@ -1074,36 +1123,41 @@ class BOSer(BraiinsOSFirmware): return grpc_locate_device_status["enabled"] except LookupError: pass + return False - async def _is_mining(self, rpc_devdetails: dict = None) -> Optional[bool]: + async def _is_mining(self, rpc_devdetails: dict | None = None) -> bool | None: if rpc_devdetails is None: try: rpc_devdetails = await self.rpc.send_command( "devdetails", ignore_errors=True, allow_warning=False ) except APIError: - pass + return None if rpc_devdetails is not None: try: return not rpc_devdetails["STATUS"][0]["Msg"] == "Unavailable" except LookupError: pass + return None - async def _get_uptime(self, rpc_summary: dict = None) -> Optional[int]: + async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() except APIError: - pass + return None if rpc_summary is not None: try: return int(rpc_summary["SUMMARY"][0]["Elapsed"]) except LookupError: pass + return None - async def _get_pools(self, grpc_pool_groups: dict = None) -> List[PoolMetrics]: + async def _get_pools( + self, grpc_pool_groups: dict | None = None + ) -> list[PoolMetrics]: if grpc_pool_groups is None: try: grpc_pool_groups = await self.web.get_pool_groups() diff --git a/pyasic/miners/backends/btminer.py b/pyasic/miners/backends/btminer.py index f1bf1993..b3fcd5f0 100644 --- a/pyasic/miners/backends/btminer.py +++ b/pyasic/miners/backends/btminer.py @@ -15,16 +15,15 @@ # ------------------------------------------------------------------------------ import asyncio import logging -from pathlib import Path import aiofiles import semver -from pyasic.config import MinerConfig, MiningModeConfig, PoolConfig +from pyasic.config import MinerConfig, MiningModeConfig from pyasic.data import Fan, HashBoard from pyasic.data.error_codes import MinerErrorData, WhatsminerError from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.device.firmware import StockFirmware @@ -237,7 +236,9 @@ class BTMinerV2(StockFirmware): return True return False - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config conf = config.as_wm(user_suffix=user_suffix) @@ -308,6 +309,9 @@ class BTMinerV2(StockFirmware): self.config = cfg return self.config + cfg.mining_mode = MiningModeConfig.normal() + return cfg + async def set_power_limit(self, wattage: int) -> bool: try: await self.rpc.adjust_power_limit(wattage) @@ -316,13 +320,14 @@ class BTMinerV2(StockFirmware): return False else: return True + return False ################################################## ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## async def _get_mac( - self, rpc_summary: dict = None, rpc_get_miner_info: dict = None + self, rpc_summary: dict | None = None, rpc_get_miner_info: dict | None = None ) -> str | None: if rpc_get_miner_info is None: try: @@ -350,7 +355,9 @@ class BTMinerV2(StockFirmware): except LookupError: pass - async def _get_api_ver(self, rpc_get_version: dict = None) -> str | None: + return None + + async def _get_api_ver(self, rpc_get_version: dict | None = None) -> str | None: if rpc_get_version is None: try: rpc_get_version = await self.rpc.get_version() @@ -368,13 +375,12 @@ class BTMinerV2(StockFirmware): except (KeyError, TypeError): pass else: - self.rpc.rpc_ver = self.api_ver return self.api_ver return self.api_ver async def _get_fw_ver( - self, rpc_get_version: dict = None, rpc_summary: dict = None + self, rpc_get_version: dict | None = None, rpc_summary: dict | None = None ) -> str | None: if rpc_get_version is None: try: @@ -408,7 +414,7 @@ class BTMinerV2(StockFirmware): return self.fw_ver - async def _get_hostname(self, rpc_get_miner_info: dict = None) -> str | None: + async def _get_hostname(self, rpc_get_miner_info: dict | None = None) -> str | None: hostname = None if rpc_get_miner_info is None: try: @@ -424,7 +430,9 @@ class BTMinerV2(StockFirmware): return hostname - async def _get_hashrate(self, rpc_summary: dict = None) -> AlgoHashRate | None: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -435,13 +443,13 @@ class BTMinerV2(StockFirmware): try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass return None - async def _get_hashboards(self, rpc_devs: dict = None) -> list[HashBoard]: + async def _get_hashboards(self, rpc_devs: dict | None = None) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -470,8 +478,11 @@ class BTMinerV2(StockFirmware): hashboards[asc].chip_temp = round(board["Chip Temp Avg"]) hashboards[asc].temp = round(board["Temperature"]) hashboards[asc].hashrate = self.algo.hashrate( - rate=float(board["MHS 1m"]), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=float(board["MHS 1m"]), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hashboards[asc].chips = board["Effective Chips"] hashboards[asc].serial_number = board["PCB SN"] hashboards[asc].missing = False @@ -480,7 +491,7 @@ class BTMinerV2(StockFirmware): return hashboards - async def _get_env_temp(self, rpc_summary: dict = None) -> float | None: + async def _get_env_temp(self, rpc_summary: dict | None = None) -> float | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -494,7 +505,7 @@ class BTMinerV2(StockFirmware): pass return None - async def _get_wattage(self, rpc_summary: dict = None) -> int | None: + async def _get_wattage(self, rpc_summary: dict | None = None) -> int | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -509,7 +520,7 @@ class BTMinerV2(StockFirmware): pass return None - async def _get_wattage_limit(self, rpc_summary: dict = None) -> int | None: + async def _get_wattage_limit(self, rpc_summary: dict | None = None) -> int | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -524,7 +535,7 @@ class BTMinerV2(StockFirmware): return None async def _get_fans( - self, rpc_summary: dict = None, rpc_get_psu: dict = None + self, rpc_summary: dict | None = None, rpc_get_psu: dict | None = None ) -> list[Fan]: if self.expected_fans is None: return [] @@ -549,7 +560,7 @@ class BTMinerV2(StockFirmware): return fans async def _get_fan_psu( - self, rpc_summary: dict = None, rpc_get_psu: dict = None + self, rpc_summary: dict | None = None, rpc_get_psu: dict | None = None ) -> int | None: if rpc_summary is None: try: @@ -577,7 +588,7 @@ class BTMinerV2(StockFirmware): return None async def _get_errors( - self, rpc_summary: dict = None, rpc_get_error_code: dict = None + self, rpc_summary: dict | None = None, rpc_get_error_code: dict | None = None ) -> list[MinerErrorData]: errors = [] if rpc_get_error_code is None and rpc_summary is None: @@ -611,11 +622,11 @@ class BTMinerV2(StockFirmware): errors.append(WhatsminerError(error_code=err)) except (LookupError, ValueError, TypeError): pass - return errors + return errors # type: ignore[return-value] async def _get_expected_hashrate( - self, rpc_summary: dict = None - ) -> AlgoHashRate | None: + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -627,14 +638,17 @@ class BTMinerV2(StockFirmware): expected_hashrate = rpc_summary["SUMMARY"][0]["Factory GHS"] if expected_hashrate: return self.algo.hashrate( - rate=float(expected_hashrate), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(expected_hashrate), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass return None - async def _get_fault_light(self, rpc_get_miner_info: dict = None) -> bool | None: + async def _get_fault_light( + self, rpc_get_miner_info: dict | None = None + ) -> bool | None: if rpc_get_miner_info is None: try: rpc_get_miner_info = await self.rpc.get_miner_info() @@ -656,15 +670,17 @@ class BTMinerV2(StockFirmware): dns: str, gateway: str, subnet_mask: str = "255.255.255.0", - hostname: str = None, + hostname: str | None = None, ): if not hostname: hostname = await self.get_hostname() + if hostname is None: + hostname = str(self.ip) await self.rpc.net_config( ip=ip, mask=subnet_mask, dns=dns, gate=gateway, host=hostname, dhcp=False ) - async def set_dhcp(self, hostname: str = None): + async def set_dhcp(self, hostname: str | None = None): if hostname: await self.set_hostname(hostname) await self.rpc.net_config() @@ -672,7 +688,7 @@ class BTMinerV2(StockFirmware): async def set_hostname(self, hostname: str): await self.rpc.set_hostname(hostname) - async def _is_mining(self, rpc_status: dict = None) -> bool | None: + async def _is_mining(self, rpc_status: dict | None = None) -> bool | None: if rpc_status is None: try: rpc_status = await self.rpc.status() @@ -692,7 +708,7 @@ class BTMinerV2(StockFirmware): pass return False - async def _get_uptime(self, rpc_summary: dict = None) -> int | None: + async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -704,8 +720,9 @@ class BTMinerV2(StockFirmware): return int(rpc_summary["SUMMARY"][0]["Elapsed"]) except LookupError: pass + return None - async def _get_pools(self, rpc_pools: dict = None) -> list[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() @@ -735,15 +752,25 @@ class BTMinerV2(StockFirmware): pass return pools_data - async def upgrade_firmware(self, file: Path) -> str: + async def upgrade_firmware( + self, + *, + file: str | None = None, + url: str | None = None, + version: str | None = None, + keep_settings: bool = True, + ) -> bool: """ Upgrade the firmware of the Whatsminer device. Args: - file (Path): The local file path of the firmware to be uploaded. + file: The local file path of the firmware to be uploaded. + url: URL to download firmware from (not supported). + version: Specific version to upgrade to (not supported). + keep_settings: Whether to keep settings after upgrade. Returns: - str: Confirmation message after upgrading the firmware. + bool: True if firmware upgrade was successful. """ try: logging.info("Starting firmware upgrade process for Whatsminer.") @@ -755,12 +782,12 @@ class BTMinerV2(StockFirmware): async with aiofiles.open(file, "rb") as f: upgrade_contents = await f.read() - result = await self.rpc.update_firmware(upgrade_contents) + await self.rpc.update_firmware(upgrade_contents) logging.info( "Firmware upgrade process completed successfully for Whatsminer." ) - return result + return True except FileNotFoundError as e: logging.error(f"File not found during the firmware upgrade process: {e}") raise @@ -872,13 +899,18 @@ class BTMinerV3(StockFirmware): except LookupError: pass - self.config = MinerConfig.from_btminer_v3( - rpc_pools=pools, rpc_settings=settings, rpc_device_info=device_info - ) + if pools is not None and settings is not None and device_info is not None: + self.config = MinerConfig.from_btminer_v3( + rpc_pools=pools, rpc_settings=settings, rpc_device_info=device_info + ) + else: + self.config = MinerConfig() return self.config - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config conf = config.as_btminer_v3(user_suffix=user_suffix) @@ -902,11 +934,7 @@ class BTMinerV3(StockFirmware): async def fault_light_on(self) -> bool: try: data = await self.rpc.set_system_led( - leds=[ - { - {"color": "red", "period": 60, "duration": 20, "start": 0}, - } - ], + leds=[{"color": "red", "period": 60, "duration": 20, "start": 0}] ) except APIError: return False @@ -922,7 +950,7 @@ class BTMinerV3(StockFirmware): data = await self.rpc.set_system_reboot() except APIError: return False - if data.get("msg"): + if data and data.get("msg"): if data["msg"] == "ok": return True return False @@ -932,7 +960,7 @@ class BTMinerV3(StockFirmware): data = await self.rpc.set_miner_service("restart") except APIError: return False - if data.get("msg"): + if data and data.get("msg"): if data["msg"] == "ok": return True return False @@ -942,7 +970,7 @@ class BTMinerV3(StockFirmware): data = await self.rpc.set_miner_service("stop") except APIError: return False - if data.get("msg"): + if data and data.get("msg"): if data["msg"] == "ok": return True return False @@ -952,7 +980,7 @@ class BTMinerV3(StockFirmware): data = await self.rpc.set_miner_service("start") except APIError: return False - if data.get("msg"): + if data and data.get("msg"): if data["msg"] == "ok": return True return False @@ -966,74 +994,94 @@ class BTMinerV3(StockFirmware): else: return True - async def _get_mac(self, rpc_get_device_info: dict = None) -> str | None: + async def _get_mac(self, rpc_get_device_info: dict | None = None) -> str | None: if rpc_get_device_info is None: try: rpc_get_device_info = await self.rpc.get_device_info() except APIError: return None + if rpc_get_device_info is None: + return None return rpc_get_device_info.get("msg", {}).get("network", {}).get("mac") - async def _get_api_version(self, rpc_get_device_info: dict = None) -> str | None: - if rpc_get_device_info is None: - try: - rpc_get_device_info = await self.rpc.get_device_info() - except APIError: - return None - return rpc_get_device_info.get("msg", {}).get("system", {}).get("api") - - async def _get_firmware_version( - self, rpc_get_device_info: dict = None + async def _get_api_version( + self, rpc_get_device_info: dict | None = None ) -> str | None: if rpc_get_device_info is None: try: rpc_get_device_info = await self.rpc.get_device_info() except APIError: return None - return rpc_get_device_info.get("msg", {}).get("system", {}).get("fwversion") + if rpc_get_device_info is None: + return None + return rpc_get_device_info.get("msg", {}).get("system", {}).get("api") - async def _get_hostname(self, rpc_get_device_info: dict = None) -> str | None: + async def _get_firmware_version( + self, rpc_get_device_info: dict | None = None + ) -> str | None: if rpc_get_device_info is None: try: rpc_get_device_info = await self.rpc.get_device_info() except APIError: return None + if rpc_get_device_info is None: + return None + return rpc_get_device_info.get("msg", {}).get("system", {}).get("fwversion") + + async def _get_hostname( + self, rpc_get_device_info: dict | None = None + ) -> str | None: + if rpc_get_device_info is None: + try: + rpc_get_device_info = await self.rpc.get_device_info() + except APIError: + return None + if rpc_get_device_info is None: + return None return rpc_get_device_info.get("msg", {}).get("network", {}).get("hostname") async def _get_light_flashing( - self, rpc_get_device_info: dict = None + self, rpc_get_device_info: dict | None = None ) -> bool | None: if rpc_get_device_info is None: try: rpc_get_device_info = await self.rpc.get_device_info() except APIError: return None + if rpc_get_device_info is None: + return None val = rpc_get_device_info.get("msg", {}).get("system", {}).get("ledstatus") if isinstance(val, str): return val != "auto" return None async def _get_wattage_limit( - self, rpc_get_device_info: dict = None - ) -> float | None: + self, rpc_get_device_info: dict | None = None + ) -> int | None: if rpc_get_device_info is None: try: rpc_get_device_info = await self.rpc.get_device_info() except APIError: return None + if rpc_get_device_info is None: + return None val = rpc_get_device_info.get("msg", {}).get("miner", {}).get("power-limit-set") try: - return float(val) + return int(float(val)) except (ValueError, TypeError): return None - async def _get_fans(self, rpc_get_miner_status_summary: dict = None) -> list[Fan]: + async def _get_fans( + self, rpc_get_miner_status_summary: dict | None = None + ) -> list[Fan]: if rpc_get_miner_status_summary is None: try: rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() except APIError: return [] fans = [] + if rpc_get_miner_status_summary is None: + return [] summary = rpc_get_miner_status_summary.get("msg", {}).get("summary", {}) for idx, direction in enumerate(["in", "out"]): rpm = summary.get(f"fan-speed-{direction}") @@ -1041,19 +1089,21 @@ class BTMinerV3(StockFirmware): fans.append(Fan(speed=rpm)) return fans - async def _get_psu_fans(self, rpc_get_device_info: dict = None) -> list[Fan]: + async def _get_psu_fans(self, rpc_get_device_info: dict | None = None) -> list[Fan]: if rpc_get_device_info is None: try: rpc_get_device_info = await self.rpc.get_device_info() except APIError: return [] + if rpc_get_device_info is None: + return [] rpm = rpc_get_device_info.get("msg", {}).get("power", {}).get("fanspeed") return [Fan(speed=rpm)] if rpm is not None else [] async def _get_hashboards( self, - rpc_get_device_info: dict = None, - rpc_get_miner_status_edevs: dict = None, + rpc_get_device_info: dict | None = None, + rpc_get_miner_status_edevs: dict | None = None, ) -> list[HashBoard]: if rpc_get_device_info is None: try: @@ -1067,6 +1117,8 @@ class BTMinerV3(StockFirmware): return [] boards = [] + if rpc_get_device_info is None or rpc_get_miner_status_edevs is None: + return [] board_count = ( rpc_get_device_info.get("msg", {}).get("hardware", {}).get("boards", 3) ) @@ -1077,8 +1129,11 @@ class BTMinerV3(StockFirmware): HashBoard( slot=idx, hashrate=self.algo.hashrate( - rate=board_data.get("hash-average", 0), unit=self.algo.unit.TH - ).into(self.algo.unit.default), + rate=board_data.get("hash-average", 0), + unit=self.algo.unit.TH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ), temp=board_data.get("chip-temp-min"), inlet_temp=board_data.get("chip-temp-min"), outlet_temp=board_data.get("chip-temp-max"), @@ -1095,7 +1150,7 @@ class BTMinerV3(StockFirmware): return boards async def _get_pools( - self, rpc_get_miner_status_summary: dict = None + self, rpc_get_miner_status_summary: dict | None = None ) -> list[PoolMetrics]: if rpc_get_miner_status_summary is None: try: @@ -1103,6 +1158,8 @@ class BTMinerV3(StockFirmware): except APIError: return [] pools = [] + if rpc_get_miner_status_summary is None: + return [] msg_pools = rpc_get_miner_status_summary.get("msg", {}).get("pools", []) for idx, pool in enumerate(msg_pools): pools.append( @@ -1117,13 +1174,15 @@ class BTMinerV3(StockFirmware): return pools async def _get_uptime( - self, rpc_get_miner_status_summary: dict = None + self, rpc_get_miner_status_summary: dict | None = None ) -> int | None: if rpc_get_miner_status_summary is None: try: rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() except APIError: return None + if rpc_get_miner_status_summary is None: + return None return ( rpc_get_miner_status_summary.get("msg", {}) .get("summary", {}) @@ -1131,27 +1190,37 @@ class BTMinerV3(StockFirmware): ) async def _get_wattage( - self, rpc_get_miner_status_summary: dict = None - ) -> float | None: + self, rpc_get_miner_status_summary: dict | None = None + ) -> int | None: if rpc_get_miner_status_summary is None: try: rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() except APIError: return None - return ( + if rpc_get_miner_status_summary is None: + return None + power_val = ( rpc_get_miner_status_summary.get("msg", {}) .get("summary", {}) .get("power-realtime") ) + try: + return int(float(power_val)) if power_val is not None else None + except (ValueError, TypeError): + return None async def _get_hashrate( - self, rpc_get_miner_status_summary: dict = None - ) -> float | None: + self, rpc_get_miner_status_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_get_miner_status_summary is None: try: rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() except APIError: return None + + if rpc_get_miner_status_summary is None: + return None + return ( rpc_get_miner_status_summary.get("msg", {}) .get("summary", {}) @@ -1159,31 +1228,37 @@ class BTMinerV3(StockFirmware): ) async def _get_expected_hashrate( - self, rpc_get_miner_status_summary: dict = None - ) -> float | None: + self, rpc_get_miner_status_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_get_miner_status_summary is None: try: rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() except APIError: return None + if rpc_get_miner_status_summary is None: + return None res = ( rpc_get_miner_status_summary.get("msg", {}) .get("summary", {}) .get("factory-hash") ) - if res == (-0.001 * self.expected_hashboards): + if self.expected_hashboards is not None and res == ( + -0.001 * self.expected_hashboards + ): return None return res async def _get_env_temp( - self, rpc_get_miner_status_summary: dict = None + self, rpc_get_miner_status_summary: dict | None = None ) -> float | None: if rpc_get_miner_status_summary is None: try: rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() except APIError: return None + if rpc_get_miner_status_summary is None: + return None return ( rpc_get_miner_status_summary.get("msg", {}) .get("summary", {}) diff --git a/pyasic/miners/backends/cgminer.py b/pyasic/miners/backends/cgminer.py index 9558af25..483d07b7 100644 --- a/pyasic/miners/backends/cgminer.py +++ b/pyasic/miners/backends/cgminer.py @@ -14,11 +14,10 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional from pyasic.config import MinerConfig from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.device.firmware import StockFirmware @@ -75,7 +74,7 @@ class CGMiner(StockFirmware): try: pools = await self.rpc.pools() except APIError: - return self.config + return self.config or MinerConfig() self.config = MinerConfig.from_api(pools) return self.config @@ -84,7 +83,7 @@ class CGMiner(StockFirmware): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -99,7 +98,7 @@ class CGMiner(StockFirmware): return self.api_ver - async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -114,7 +113,9 @@ class CGMiner(StockFirmware): return self.fw_ver - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() @@ -125,12 +126,15 @@ class CGMiner(StockFirmware): try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default) + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except (LookupError, ValueError, TypeError): pass + return None - async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: + async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -142,8 +146,9 @@ class CGMiner(StockFirmware): return int(rpc_stats["STATS"][1]["Elapsed"]) except LookupError: pass + return None - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() diff --git a/pyasic/miners/backends/elphapex.py b/pyasic/miners/backends/elphapex.py index 671281bb..8318d23d 100644 --- a/pyasic/miners/backends/elphapex.py +++ b/pyasic/miners/backends/elphapex.py @@ -13,13 +13,11 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional from pyasic import APIError, MinerConfig from pyasic.data import Fan, HashBoard, X19Error -from pyasic.data.error_codes import MinerErrorData from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.miners.data import ( DataFunction, DataLocations, @@ -95,9 +93,13 @@ class ElphapexMiner(StockFirmware): data = await self.web.get_miner_conf() if data: self.config = MinerConfig.from_elphapex(data) + if self.config is None: + self.config = MinerConfig() return self.config - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config await self.web.set_miner_conf(config.as_elphapex(user_suffix=user_suffix)) @@ -106,14 +108,14 @@ class ElphapexMiner(StockFirmware): if data: if data.get("code") == "B000": self.light = True - return self.light + return self.light if self.light is not None else False async def fault_light_off(self) -> bool: data = await self.web.blink(blink=False) if data: if data.get("code") == "B100": self.light = False - return self.light + return self.light if self.light is not None else False async def reboot(self) -> bool: data = await self.web.reboot() @@ -121,7 +123,7 @@ class ElphapexMiner(StockFirmware): return True return False - async def _get_api_ver(self, web_summary: dict = None) -> Optional[str]: + async def _get_api_ver(self, web_summary: dict | None = None) -> str | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -136,7 +138,7 @@ class ElphapexMiner(StockFirmware): return self.api_ver - async def _get_fw_ver(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_fw_ver(self, web_get_system_info: dict | None = None) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -155,7 +157,9 @@ class ElphapexMiner(StockFirmware): return self.fw_ver - async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_hostname( + self, web_get_system_info: dict | None = None + ) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -167,8 +171,9 @@ class ElphapexMiner(StockFirmware): return web_get_system_info["hostname"] except KeyError: pass + return None - async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -187,8 +192,11 @@ class ElphapexMiner(StockFirmware): return data["macaddr"] except KeyError: pass + return None - async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]: + async def _get_errors( # type: ignore[override] + self, web_summary: dict | None = None + ) -> list[X19Error]: if web_summary is None: try: web_summary = await self.web.summary() @@ -208,7 +216,7 @@ class ElphapexMiner(StockFirmware): pass return errors - async def _get_hashboards(self, web_stats: dict | None = None) -> List[HashBoard]: + async def _get_hashboards(self, web_stats: dict | None = None) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -227,8 +235,11 @@ class ElphapexMiner(StockFirmware): try: for board in web_stats["STATS"][0]["chain"]: hashboards[board["index"]].hashrate = self.algo.hashrate( - rate=board["rate_real"], unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=board["rate_real"], + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hashboards[board["index"]].chips = board["asic_num"] board_temp_data = list( filter(lambda x: not x == 0, board["temp_pcb"]) @@ -250,8 +261,8 @@ class ElphapexMiner(StockFirmware): return hashboards async def _get_fault_light( - self, web_get_blink_status: dict = None - ) -> Optional[bool]: + self, web_get_blink_status: dict | None = None + ) -> bool | None: if self.light: return self.light @@ -269,8 +280,8 @@ class ElphapexMiner(StockFirmware): return self.light async def _get_expected_hashrate( - self, web_stats: dict = None - ) -> Optional[AlgoHashRate]: + self, web_stats: dict | None = None + ) -> AlgoHashRateType | None: if web_stats is None: try: web_stats = await self.web.stats() @@ -286,11 +297,12 @@ class ElphapexMiner(StockFirmware): rate_unit = "MH" return self.algo.hashrate( rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) - ).into(self.algo.unit.default) + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass + return None - async def _is_mining(self, web_get_miner_conf: dict = None) -> Optional[bool]: + async def _is_mining(self, web_get_miner_conf: dict | None = None) -> bool | None: if web_get_miner_conf is None: try: web_get_miner_conf = await self.web.get_miner_conf() @@ -306,8 +318,9 @@ class ElphapexMiner(StockFirmware): return False except LookupError: pass + return None - async def _get_uptime(self, web_summary: dict = None) -> Optional[int]: + async def _get_uptime(self, web_summary: dict | None = None) -> int | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -319,8 +332,9 @@ class ElphapexMiner(StockFirmware): return int(web_summary["SUMMARY"][1]["elapsed"]) except LookupError: pass + return None - async def _get_fans(self, web_stats: dict = None) -> List[Fan]: + async def _get_fans(self, web_stats: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -340,13 +354,16 @@ class ElphapexMiner(StockFirmware): return fans - async def _get_pools(self, web_pools: list = None) -> List[PoolMetrics]: + async def _get_pools(self, web_pools: dict | None = None) -> list[PoolMetrics]: if web_pools is None: try: web_pools = await self.web.pools() except APIError: return [] + if web_pools is None: + return [] + active_pool_index = None highest_priority = float("inf") @@ -359,23 +376,22 @@ class ElphapexMiner(StockFirmware): active_pool_index = pool_info["index"] pools_data = [] - if web_pools is not None: - try: - for pool_info in web_pools["POOLS"]: - url = pool_info.get("url") - pool_url = PoolUrl.from_str(url) if url else None - pool_data = PoolMetrics( - accepted=pool_info.get("accepted"), - rejected=pool_info.get("rejected"), - get_failures=pool_info.get("stale"), - remote_failures=pool_info.get("discarded"), - active=pool_info.get("index") == active_pool_index, - alive=pool_info.get("status") == "Alive", - url=pool_url, - user=pool_info.get("user"), - index=pool_info.get("index"), - ) - pools_data.append(pool_data) - except LookupError: - pass + try: + for pool_info in web_pools["POOLS"]: + url = pool_info.get("url") + pool_url = PoolUrl.from_str(url) if url else None + pool_data = PoolMetrics( + accepted=pool_info.get("accepted"), + rejected=pool_info.get("rejected"), + get_failures=pool_info.get("stale"), + remote_failures=pool_info.get("discarded"), + active=pool_info.get("index") == active_pool_index, + alive=pool_info.get("status") == "Alive", + url=pool_url, + user=pool_info.get("user"), + index=pool_info.get("index"), + ) + pools_data.append(pool_data) + except LookupError: + pass return pools_data diff --git a/pyasic/miners/backends/epic.py b/pyasic/miners/backends/epic.py index 01959f00..c4b6a18e 100644 --- a/pyasic/miners/backends/epic.py +++ b/pyasic/miners/backends/epic.py @@ -14,14 +14,12 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from pathlib import Path -from typing import List, Optional from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard from pyasic.data.error_codes import MinerErrorData, X19Error from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate, ScryptAlgo +from pyasic.device.algorithm import AlgoHashRateType, ScryptAlgo from pyasic.errors import APIError from pyasic.logger import logger from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand @@ -116,7 +114,9 @@ class ePIC(ePICFirmware): self.config = cfg return self.config - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config conf = self.config.as_epic(user_suffix=user_suffix) @@ -180,7 +180,7 @@ class ePIC(ePICFirmware): pass return False - async def _get_mac(self, web_network: dict = None) -> Optional[str]: + async def _get_mac(self, web_network: dict | None = None) -> str | None: if web_network is None: try: web_network = await self.web.network() @@ -194,8 +194,9 @@ class ePIC(ePICFirmware): return mac except KeyError: pass + return None - async def _get_hostname(self, web_summary: dict = None) -> Optional[str]: + async def _get_hostname(self, web_summary: dict | None = None) -> str | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -208,8 +209,9 @@ class ePIC(ePICFirmware): return hostname except KeyError: pass + return None - async def _get_wattage(self, web_summary: dict = None) -> Optional[int]: + async def _get_wattage(self, web_summary: dict | None = None) -> int | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -223,8 +225,11 @@ class ePIC(ePICFirmware): return wattage except KeyError: pass + return None - async def _get_hashrate(self, web_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, web_summary: dict | None = None + ) -> AlgoHashRateType | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -238,14 +243,16 @@ class ePIC(ePICFirmware): for hb in web_summary["HBs"]: hashrate += hb["Hashrate"][0] return self.algo.hashrate( - rate=float(hashrate), unit=self.algo.unit.MH - ).into(self.algo.unit.TH) + rate=float(hashrate), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.TH) # type: ignore[attr-defined] except (LookupError, ValueError, TypeError): pass + return None async def _get_expected_hashrate( - self, web_summary: dict = None - ) -> Optional[AlgoHashRate]: + self, web_summary: dict | None = None + ) -> AlgoHashRateType | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -264,12 +271,14 @@ class ePIC(ePICFirmware): hashrate += hb["Hashrate"][0] / ideal return self.algo.hashrate( - rate=float(hashrate), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=float(hashrate), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (LookupError, ValueError, TypeError): pass + return None - async def _get_fw_ver(self, web_summary: dict = None) -> Optional[str]: + async def _get_fw_ver(self, web_summary: dict | None = None) -> str | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -283,8 +292,9 @@ class ePIC(ePICFirmware): return fw_ver except KeyError: pass + return None - async def _get_fans(self, web_summary: dict = None) -> List[Fan]: + async def _get_fans(self, web_summary: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -305,8 +315,8 @@ class ePIC(ePICFirmware): return fans async def _get_hashboards( - self, web_summary: dict = None, web_capabilities: dict = None - ) -> List[HashBoard]: + self, web_summary: dict | None = None, web_capabilities: dict | None = None + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -362,16 +372,18 @@ class ePIC(ePICFirmware): # Update the Hashboard object hb_list[hb["Index"]].missing = False hb_list[hb["Index"]].hashrate = self.algo.hashrate( - rate=float(hashrate), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=float(hashrate), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] hb_list[hb["Index"]].chips = num_of_chips hb_list[hb["Index"]].temp = int(hb["Temperature"]) hb_list[hb["Index"]].tuned = tuned hb_list[hb["Index"]].active = active hb_list[hb["Index"]].voltage = hb["Input Voltage"] return hb_list + return hb_list - async def _is_mining(self, web_summary, *args, **kwargs) -> Optional[bool]: + async def _is_mining(self, web_summary: dict | None = None) -> bool | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -383,8 +395,9 @@ class ePIC(ePICFirmware): return not op_state == "Idling" except KeyError: pass + return None - async def _get_uptime(self, web_summary: dict = None) -> Optional[int]: + async def _get_uptime(self, web_summary: dict | None = None) -> int | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -399,7 +412,7 @@ class ePIC(ePICFirmware): pass return None - async def _get_fault_light(self, web_summary: dict = None) -> Optional[bool]: + async def _get_fault_light(self, web_summary: dict | None = None) -> bool | None: if web_summary is None: try: web_summary = await self.web.summary() @@ -414,7 +427,9 @@ class ePIC(ePICFirmware): pass return False - async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]: + async def _get_errors( + self, web_summary: dict | None = None + ) -> list[MinerErrorData]: if not web_summary: try: web_summary = await self.web.summary() @@ -427,12 +442,11 @@ class ePIC(ePICFirmware): error = web_summary["Status"]["Last Error"] if error is not None: errors.append(X19Error(error_message=str(error))) - return errors except KeyError: pass - return errors + return errors # type: ignore[return-value] - async def _get_pools(self, web_summary: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, web_summary: dict | None = None) -> list[PoolMetrics]: if web_summary is None: try: web_summary = await self.web.summary() @@ -466,18 +480,29 @@ class ePIC(ePICFirmware): return pool_data except LookupError: pass + return [] async def upgrade_firmware( - self, file: Path | str, keep_settings: bool = True + self, + *, + file: str | None = None, + url: str | None = None, + version: str | None = None, + keep_settings: bool = True, ) -> bool: """ Upgrade the firmware of the ePIC miner device. Args: - file (Path | str): The local file path of the firmware to be uploaded. - keep_settings (bool): Whether to keep the current settings after the update. + file: The local file path of the firmware to be uploaded. + url: The URL to download the firmware from. Must be a valid URL if provided. + version: The version of the firmware to upgrade to. If None, the version will be inferred from the file or URL. + keep_settings: Whether to keep the current settings after the update. Returns: bool: Whether the firmware update succeeded. """ - return await self.web.system_update(file=file, keep_settings=keep_settings) + if file is not None: + await self.web.system_update(file=file, keep_settings=keep_settings) + return True + return False diff --git a/pyasic/miners/backends/espminer.py b/pyasic/miners/backends/espminer.py index 0b54b9f7..be922ef7 100644 --- a/pyasic/miners/backends/espminer.py +++ b/pyasic/miners/backends/espminer.py @@ -1,8 +1,6 @@ -from typing import List, Optional - from pyasic import APIError, MinerConfig from pyasic.data import Fan, HashBoard -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.device.firmware import MinerFirmware from pyasic.miners.base import BaseMiner from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand @@ -72,24 +70,28 @@ class ESPMiner(BaseMiner): web_system_info = await self.web.system_info() return MinerConfig.from_espminer(web_system_info) - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: await self.web.update_settings(**config.as_espminer()) - async def _get_wattage(self, web_system_info: dict = None) -> Optional[int]: + async def _get_wattage(self, web_system_info: dict | None = None) -> int | None: if web_system_info is None: try: web_system_info = await self.web.system_info() except APIError: pass + if web_system_info is not None: try: return round(web_system_info["power"]) except KeyError: pass + return None async def _get_hashrate( - self, web_system_info: dict = None - ) -> Optional[AlgoHashRate]: + self, web_system_info: dict | None = None + ) -> AlgoHashRateType | None: if web_system_info is None: try: web_system_info = await self.web.system_info() @@ -99,14 +101,18 @@ class ESPMiner(BaseMiner): if web_system_info is not None: try: return self.algo.hashrate( - rate=float(web_system_info["hashRate"]), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(web_system_info["hashRate"]), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except KeyError: pass + return None async def _get_expected_hashrate( - self, web_system_info: dict = None - ) -> Optional[AlgoHashRate]: + self, web_system_info: dict | None = None + ) -> AlgoHashRateType | None: if web_system_info is None: try: web_system_info = await self.web.system_info() @@ -126,15 +132,23 @@ class ESPMiner(BaseMiner): except APIError: pass - expected_hashrate = small_core_count * asic_count * frequency - - return self.algo.hashrate( - rate=float(expected_hashrate), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + if ( + small_core_count is not None + and asic_count is not None + and frequency is not None + ): + expected_hashrate = small_core_count * asic_count * frequency + return self.algo.hashrate( + rate=float(expected_hashrate), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except KeyError: pass + return None - async def _get_uptime(self, web_system_info: dict = None) -> Optional[int]: + async def _get_uptime(self, web_system_info: dict | None = None) -> int | None: if web_system_info is None: try: web_system_info = await self.web.system_info() @@ -146,8 +160,11 @@ class ESPMiner(BaseMiner): return web_system_info["uptimeSeconds"] except KeyError: pass + return None - async def _get_hashboards(self, web_system_info: dict = None) -> List[HashBoard]: + async def _get_hashboards( + self, web_system_info: dict | None = None + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -163,8 +180,10 @@ class ESPMiner(BaseMiner): HashBoard( hashrate=self.algo.hashrate( rate=float(web_system_info["hashRate"]), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ), chip_temp=web_system_info.get("temp"), temp=web_system_info.get("vrTemp"), chips=web_system_info.get("asicCount", 1), @@ -178,7 +197,7 @@ class ESPMiner(BaseMiner): pass return [] - async def _get_fans(self, web_system_info: dict = None) -> List[Fan]: + async def _get_fans(self, web_system_info: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -195,7 +214,7 @@ class ESPMiner(BaseMiner): pass return [] - async def _get_hostname(self, web_system_info: dict = None) -> Optional[str]: + async def _get_hostname(self, web_system_info: dict | None = None) -> str | None: if web_system_info is None: try: web_system_info = await self.web.system_info() @@ -207,8 +226,9 @@ class ESPMiner(BaseMiner): return web_system_info["hostname"] except KeyError: pass + return None - async def _get_api_ver(self, web_system_info: dict = None) -> Optional[str]: + async def _get_api_ver(self, web_system_info: dict | None = None) -> str | None: if web_system_info is None: try: web_system_info = await self.web.system_info() @@ -220,8 +240,9 @@ class ESPMiner(BaseMiner): return web_system_info["version"] except KeyError: pass + return None - async def _get_fw_ver(self, web_system_info: dict = None) -> Optional[str]: + async def _get_fw_ver(self, web_system_info: dict | None = None) -> str | None: if web_system_info is None: try: web_system_info = await self.web.system_info() @@ -233,8 +254,9 @@ class ESPMiner(BaseMiner): return web_system_info["version"] except KeyError: pass + return None - async def _get_mac(self, web_system_info: dict = None) -> Optional[str]: + async def _get_mac(self, web_system_info: dict | None = None) -> str | None: if web_system_info is None: try: web_system_info = await self.web.system_info() @@ -246,3 +268,4 @@ class ESPMiner(BaseMiner): return web_system_info["macAddr"].upper() except KeyError: pass + return None diff --git a/pyasic/miners/backends/goldshell.py b/pyasic/miners/backends/goldshell.py index 69962d8e..ee0eadaf 100644 --- a/pyasic/miners/backends/goldshell.py +++ b/pyasic/miners/backends/goldshell.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List from pyasic.config import MinerConfig, MiningModeConfig from pyasic.data import HashBoard @@ -86,12 +85,15 @@ class GoldshellMiner(BFGMiner): try: pools = await self.web.pools() except APIError: - return self.config + if self.config is not None: + return self.config self.config = MinerConfig.from_goldshell(pools) return self.config - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: pools_data = await self.web.pools() # have to delete all the pools one at a time first for pool in pools_data: @@ -116,35 +118,37 @@ class GoldshellMiner(BFGMiner): settings["select"] = idx await self.web.set_setting(settings) - async def _get_mac(self, web_setting: dict = None) -> str: + async def _get_mac(self, web_setting: dict | None = None) -> str | None: if web_setting is None: try: web_setting = await self.web.setting() except APIError: - pass + return None if web_setting is not None: try: return web_setting["name"] except KeyError: pass + return None - async def _get_fw_ver(self, web_status: dict = None) -> str: + async def _get_fw_ver(self, web_status: dict | None = None) -> str | None: if web_status is None: try: web_status = await self.web.setting() except APIError: - pass + return None if web_status is not None: try: return web_status["firmware"] except KeyError: pass + return None async def _get_hashboards( - self, rpc_devs: dict = None, rpc_devdetails: dict = None - ) -> List[HashBoard]: + self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -166,8 +170,11 @@ class GoldshellMiner(BFGMiner): try: b_id = board["ID"] hashboards[b_id].hashrate = self.algo.hashrate( - rate=float(board["MHS 20s"]), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=float(board["MHS 20s"]), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hashboards[b_id].temp = board["tstemp-2"] hashboards[b_id].missing = False except KeyError: diff --git a/pyasic/miners/backends/hammer.py b/pyasic/miners/backends/hammer.py index a90b79b1..96fd53c0 100644 --- a/pyasic/miners/backends/hammer.py +++ b/pyasic/miners/backends/hammer.py @@ -14,13 +14,13 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional +from typing import cast from pyasic import MinerConfig from pyasic.data import Fan, HashBoard from pyasic.data.error_codes import MinerErrorData, X19Error from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import ( DataFunction, @@ -106,9 +106,11 @@ class BlackMiner(StockFirmware): data = await self.web.get_miner_conf() if data: self.config = MinerConfig.from_hammer(data) - return self.config + return self.config or MinerConfig() - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config await self.web.set_miner_conf(config.as_hammer(user_suffix=user_suffix)) @@ -117,14 +119,14 @@ class BlackMiner(StockFirmware): if data: if data.get("code") == "B000": self.light = True - return self.light + return self.light or False async def fault_light_off(self) -> bool: data = await self.web.blink(blink=False) if data: if data.get("code") == "B100": self.light = False - return self.light + return self.light or False async def reboot(self) -> bool: data = await self.web.reboot() @@ -132,7 +134,7 @@ class BlackMiner(StockFirmware): return True return False - async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -147,7 +149,7 @@ class BlackMiner(StockFirmware): return self.api_ver - async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -162,7 +164,9 @@ class BlackMiner(StockFirmware): return self.fw_ver - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: # get hr from API if rpc_summary is None: try: @@ -174,12 +178,13 @@ class BlackMiner(StockFirmware): try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (LookupError, ValueError, TypeError): pass + return None - async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -232,8 +237,11 @@ class BlackMiner(StockFirmware): hashrate = boards[1].get(f"chain_rate{i}") if hashrate: hashboard.hashrate = self.algo.hashrate( - rate=float(hashrate), unit=self.algo.unit.MH - ).into(self.algo.unit.default) + rate=float(hashrate), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) chips = boards[1].get(f"chain_acn{i}") if chips: @@ -247,7 +255,7 @@ class BlackMiner(StockFirmware): return hashboards - async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -272,14 +280,16 @@ class BlackMiner(StockFirmware): for fan in range(self.expected_fans): fans[fan].speed = rpc_stats["STATS"][1].get( - f"fan{fan_offset+fan}", 0 + f"fan{fan_offset + fan}", 0 ) except LookupError: pass return fans - async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_hostname( + self, web_get_system_info: dict | None = None + ) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -291,8 +301,9 @@ class BlackMiner(StockFirmware): return web_get_system_info["hostname"] except KeyError: pass + return None - async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -311,8 +322,11 @@ class BlackMiner(StockFirmware): return data["macaddr"] except KeyError: pass + return None - async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]: + async def _get_errors( + self, web_summary: dict | None = None + ) -> list[MinerErrorData]: if web_summary is None: try: web_summary = await self.web.summary() @@ -330,11 +344,11 @@ class BlackMiner(StockFirmware): continue except LookupError: pass - return errors + return cast(list[MinerErrorData], errors) async def _get_fault_light( - self, web_get_blink_status: dict = None - ) -> Optional[bool]: + self, web_get_blink_status: dict | None = None + ) -> bool | None: if self.light: return self.light @@ -352,8 +366,8 @@ class BlackMiner(StockFirmware): return self.light async def _get_expected_hashrate( - self, rpc_stats: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_stats: dict | None = None + ) -> AlgoHashRateType | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -364,16 +378,22 @@ class BlackMiner(StockFirmware): try: expected_rate = rpc_stats["STATS"][1].get("total_rateideal") if expected_rate is None: - return self.sticker_hashrate.into(self.algo.unit.default) + if ( + hasattr(self, "sticker_hashrate") + and self.sticker_hashrate is not None + ): + return self.sticker_hashrate.into(self.algo.unit.default) # type: ignore[attr-defined] + return None try: rate_unit = rpc_stats["STATS"][1]["rate_unit"] except KeyError: rate_unit = "MH" return self.algo.hashrate( rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) - ).into(self.algo.unit.default) + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass + return None async def set_static_ip( self, @@ -381,10 +401,10 @@ class BlackMiner(StockFirmware): dns: str, gateway: str, subnet_mask: str = "255.255.255.0", - hostname: str = None, + hostname: str | None = None, ): if not hostname: - hostname = await self.get_hostname() + hostname = await self.get_hostname() or "" await self.web.set_network_conf( ip=ip, dns=dns, @@ -394,9 +414,9 @@ class BlackMiner(StockFirmware): protocol=2, ) - async def set_dhcp(self, hostname: str = None): + async def set_dhcp(self, hostname: str | None = None): if not hostname: - hostname = await self.get_hostname() + hostname = await self.get_hostname() or "" await self.web.set_network_conf( ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1 ) @@ -417,7 +437,7 @@ class BlackMiner(StockFirmware): protocol=protocol, ) - async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]: + async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None: if web_get_conf is None: try: web_get_conf = await self.web.get_miner_conf() @@ -433,8 +453,9 @@ class BlackMiner(StockFirmware): return False except LookupError: pass + return None - async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: + async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -446,8 +467,9 @@ class BlackMiner(StockFirmware): return int(rpc_stats["STATS"][1]["Elapsed"]) except LookupError: pass + return None - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() diff --git a/pyasic/miners/backends/hiveon.py b/pyasic/miners/backends/hiveon.py index e7edf1b8..7b1bbe1f 100644 --- a/pyasic/miners/backends/hiveon.py +++ b/pyasic/miners/backends/hiveon.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import Optional from pyasic import APIError from pyasic.config import MinerConfig, MiningModeConfig @@ -92,7 +91,7 @@ class HiveonModern(HiveonFirmware, BMMiner): web: HiveonWebAPI _web_cls = HiveonWebAPI - async def get_config(self) -> MinerConfig: + async def get_config(self) -> MinerConfig | None: # type: ignore[override] data = await self.web.get_miner_conf() if data: self.config = MinerConfig.from_hiveon_modern(data) @@ -103,14 +102,16 @@ class HiveonModern(HiveonFirmware, BMMiner): if data: if data.get("code") == "B000": self.light = True - return self.light + return True + return False async def fault_light_off(self) -> bool: data = await self.web.blink(blink=False) if data: if data.get("code") == "B100": self.light = False - return self.light + return True + return False async def reboot(self) -> bool: data = await self.web.reboot() @@ -120,17 +121,21 @@ class HiveonModern(HiveonFirmware, BMMiner): async def stop_mining(self) -> bool: cfg = await self.get_config() - cfg.mining_mode = MiningModeConfig.sleep() - await self.send_config(cfg) - return True + if cfg is not None: + cfg.mining_mode = MiningModeConfig.sleep() + await self.send_config(cfg) + return True + return False async def resume_mining(self) -> bool: cfg = await self.get_config() - cfg.mining_mode = MiningModeConfig.normal() - await self.send_config(cfg) - return True + if cfg is not None: + cfg.mining_mode = MiningModeConfig.normal() + await self.send_config(cfg) + return True + return False - async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]: + async def _get_wattage(self, rpc_stats: dict | None = None) -> int | None: if not rpc_stats: try: rpc_stats = await self.rpc.stats() @@ -139,15 +144,19 @@ class HiveonModern(HiveonFirmware, BMMiner): if rpc_stats: boards = rpc_stats.get("STATS") - try: - wattage_raw = boards[1]["chain_power"] - except (KeyError, IndexError): - pass - else: - # parse wattage position out of raw data - return round(float(wattage_raw.split(" ")[0])) + if boards: + try: + wattage_raw = boards[1]["chain_power"] + except (KeyError, IndexError): + pass + else: + # parse wattage position out of raw data + return round(float(wattage_raw.split(" ")[0])) + return None - async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_hostname( + self, web_get_system_info: dict | None = None + ) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -159,8 +168,9 @@ class HiveonModern(HiveonFirmware, BMMiner): return web_get_system_info["hostname"] except KeyError: pass + return None - async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]: + async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None: if web_get_system_info is None: try: web_get_system_info = await self.web.get_system_info() @@ -179,10 +189,11 @@ class HiveonModern(HiveonFirmware, BMMiner): return data["macaddr"] except KeyError: pass + return None async def _get_fault_light( - self, web_get_blink_status: dict = None - ) -> Optional[bool]: + self, web_get_blink_status: dict | None = None + ) -> bool | None: if self.light: return self.light @@ -199,7 +210,7 @@ class HiveonModern(HiveonFirmware, BMMiner): pass return self.light - async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]: + async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None: if web_get_conf is None: try: web_get_conf = await self.web.get_miner_conf() @@ -215,6 +226,7 @@ class HiveonModern(HiveonFirmware, BMMiner): return False except LookupError: pass + return None HIVEON_OLD_DATA_LOC = DataLocations( @@ -262,7 +274,7 @@ HIVEON_OLD_DATA_LOC = DataLocations( class HiveonOld(HiveonFirmware, BMMiner): data_locations = HIVEON_OLD_DATA_LOC - async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]: + async def _get_wattage(self, rpc_stats: dict | None = None) -> int | None: if not rpc_stats: try: rpc_stats = await self.rpc.stats() @@ -271,10 +283,12 @@ class HiveonOld(HiveonFirmware, BMMiner): if rpc_stats: boards = rpc_stats.get("STATS") - try: - wattage_raw = boards[1]["chain_power"] - except (KeyError, IndexError): - pass - else: - # parse wattage position out of raw data - return round(float(wattage_raw.split(" ")[0])) + if boards: + try: + wattage_raw = boards[1]["chain_power"] + except (KeyError, IndexError): + pass + else: + # parse wattage position out of raw data + return round(float(wattage_raw.split(" ")[0])) + return None diff --git a/pyasic/miners/backends/iceriver.py b/pyasic/miners/backends/iceriver.py index e17ae492..f3a49ef1 100644 --- a/pyasic/miners/backends/iceriver.py +++ b/pyasic/miners/backends/iceriver.py @@ -1,9 +1,7 @@ -from typing import List, Optional - from pyasic import MinerConfig from pyasic.data import Fan, HashBoard from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate, MinerAlgo +from pyasic.device.algorithm import AlgoHashRateType, MinerAlgo from pyasic.errors import APIError from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand from pyasic.miners.device.firmware import StockFirmware @@ -78,7 +76,7 @@ class IceRiver(StockFirmware): return MinerConfig.from_iceriver(web_userpanel) - async def _get_fans(self, web_userpanel: dict = None) -> List[Fan]: + async def _get_fans(self, web_userpanel: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -86,7 +84,7 @@ class IceRiver(StockFirmware): try: web_userpanel = await self.web.userpanel() except APIError: - pass + return [] if web_userpanel is not None: try: @@ -95,13 +93,14 @@ class IceRiver(StockFirmware): ] except (LookupError, ValueError, TypeError): pass + return [] - async def _get_mac(self, web_userpanel: dict = None) -> Optional[str]: + async def _get_mac(self, web_userpanel: dict | None = None) -> str | None: if web_userpanel is None: try: web_userpanel = await self.web.userpanel() except APIError: - pass + return None if web_userpanel is not None: try: @@ -110,26 +109,30 @@ class IceRiver(StockFirmware): ) except (LookupError, ValueError, TypeError): pass + return None - async def _get_hostname(self, web_userpanel: dict = None) -> Optional[str]: + async def _get_hostname(self, web_userpanel: dict | None = None) -> str | None: if web_userpanel is None: try: web_userpanel = await self.web.userpanel() except APIError: - pass + return None if web_userpanel is not None: try: return web_userpanel["userpanel"]["data"]["host"] except (LookupError, ValueError, TypeError): pass + return None - async def _get_hashrate(self, web_userpanel: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, web_userpanel: dict | None = None + ) -> AlgoHashRateType | None: if web_userpanel is None: try: web_userpanel = await self.web.userpanel() except APIError: - pass + return None if web_userpanel is not None: try: @@ -144,8 +147,9 @@ class IceRiver(StockFirmware): ).into(MinerAlgo.SHA256.unit.default) except (LookupError, ValueError, TypeError): pass + return None - async def _get_fault_light(self, web_userpanel: dict = None) -> bool: + async def _get_fault_light(self, web_userpanel: dict | None = None) -> bool: if web_userpanel is None: try: web_userpanel = await self.web.userpanel() @@ -159,20 +163,23 @@ class IceRiver(StockFirmware): pass return False - async def _is_mining(self, web_userpanel: dict = None) -> Optional[bool]: + async def _is_mining(self, web_userpanel: dict | None = None) -> bool | None: if web_userpanel is None: try: web_userpanel = await self.web.userpanel() except APIError: - pass + return False if web_userpanel is not None: try: return web_userpanel["userpanel"]["data"]["powstate"] except (LookupError, ValueError, TypeError): pass + return False - async def _get_hashboards(self, web_userpanel: dict = None) -> List[HashBoard]: + async def _get_hashboards( + self, web_userpanel: dict | None = None + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -195,15 +202,17 @@ class IceRiver(StockFirmware): hb_list[idx].temp = round(board["intmp"]) hb_list[idx].hashrate = self.algo.hashrate( rate=float(board["rtpow"].replace("G", "")), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default) + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hb_list[idx].chips = int(board["chipnum"]) hb_list[idx].missing = False except LookupError: pass return hb_list - async def _get_uptime(self, web_userpanel: dict = None) -> Optional[int]: + async def _get_uptime(self, web_userpanel: dict | None = None) -> int | None: if web_userpanel is None: try: web_userpanel = await self.web.userpanel() @@ -222,8 +231,9 @@ class IceRiver(StockFirmware): ) except (LookupError, ValueError, TypeError): pass + return None - async def _get_pools(self, web_userpanel: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, web_userpanel: dict | None = None) -> list[PoolMetrics]: if web_userpanel is None: try: web_userpanel = await self.web.userpanel() diff --git a/pyasic/miners/backends/innosilicon.py b/pyasic/miners/backends/innosilicon.py index 8f3941c2..bd10d000 100644 --- a/pyasic/miners/backends/innosilicon.py +++ b/pyasic/miners/backends/innosilicon.py @@ -13,14 +13,12 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard -from pyasic.data.error_codes import MinerErrorData from pyasic.data.error_codes.innosilicon import InnosiliconError from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.backends import CGMiner from pyasic.miners.data import ( @@ -113,17 +111,17 @@ class Innosilicon(CGMiner): # get pool data try: pools = await self.web.pools() + if pools and "pools" in pools: + self.config = MinerConfig.from_inno(pools["pools"]) except APIError: - return self.config - - self.config = MinerConfig.from_inno(pools["pools"]) - return self.config + pass + return self.config or MinerConfig() async def reboot(self) -> bool: try: data = await self.web.reboot() except APIError: - pass + return False else: return data["success"] @@ -131,14 +129,16 @@ class Innosilicon(CGMiner): try: data = await self.web.restart_cgminer() except APIError: - pass + return False else: return data["success"] async def restart_backend(self) -> bool: return await self.restart_cgminer() - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: self.config = config await self.web.update_pools(config.as_inno(user_suffix=user_suffix)) @@ -147,8 +147,8 @@ class Innosilicon(CGMiner): ################################################## async def _get_mac( - self, web_get_all: dict = None, web_overview: dict = None - ) -> Optional[str]: + self, web_get_all: dict | None = None, web_overview: dict | None = None + ) -> str | None: if web_get_all: web_get_all = web_get_all["all"] @@ -171,10 +171,11 @@ class Innosilicon(CGMiner): return mac.upper() except KeyError: pass + return None async def _get_hashrate( - self, rpc_summary: dict = None, web_get_all: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_summary: dict | None = None, web_get_all: dict | None = None + ) -> AlgoHashRateType | None: if web_get_all: web_get_all = web_get_all["all"] @@ -189,13 +190,17 @@ class Innosilicon(CGMiner): if "Hash Rate H" in web_get_all["total_hash"].keys(): return self.algo.hashrate( rate=float(web_get_all["total_hash"]["Hash Rate H"]), - unit=self.algo.unit.H, - ).into(self.algo.unit.default) + unit=self.algo.unit.H, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) elif "Hash Rate" in web_get_all["total_hash"].keys(): return self.algo.hashrate( rate=float(web_get_all["total_hash"]["Hash Rate"]), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except KeyError: pass @@ -203,14 +208,17 @@ class Innosilicon(CGMiner): try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), - unit=self.algo.unit.MH, - ).into(self.algo.unit.default) + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except (KeyError, IndexError): pass + return None async def _get_hashboards( - self, rpc_stats: dict = None, web_get_all: dict = None - ) -> List[HashBoard]: + self, rpc_stats: dict | None = None, web_get_all: dict | None = None + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -260,8 +268,11 @@ class Innosilicon(CGMiner): hashrate = board.get("Hash Rate H") if hashrate: hashboards[idx].hashrate = self.algo.hashrate( - rate=float(hashrate), unit=self.algo.unit.H - ).into(self.algo.unit.default) + rate=float(hashrate), + unit=self.algo.unit.H, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) chip_temp = board.get("Temp max") if chip_temp: @@ -270,8 +281,8 @@ class Innosilicon(CGMiner): return hashboards async def _get_wattage( - self, web_get_all: dict = None, rpc_stats: dict = None - ) -> Optional[int]: + self, web_get_all: dict | None = None, rpc_stats: dict | None = None + ) -> int | None: if web_get_all: web_get_all = web_get_all["all"] @@ -305,8 +316,9 @@ class Innosilicon(CGMiner): else: wattage = int(wattage) return wattage + return None - async def _get_fans(self, web_get_all: dict = None) -> List[Fan]: + async def _get_fans(self, web_get_all: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -328,15 +340,15 @@ class Innosilicon(CGMiner): except KeyError: pass else: - round((int(spd) * 6000) / 100) + spd_converted = round((int(spd) * 6000) / 100) for i in range(self.expected_fans): - fans[i].speed = spd + fans[i].speed = spd_converted return fans - async def _get_errors( - self, web_get_error_detail: dict = None - ) -> List[MinerErrorData]: + async def _get_errors( # type: ignore[override] + self, web_get_error_detail: dict | None = None + ) -> list[InnosiliconError]: errors = [] if web_get_error_detail is None: try: @@ -357,7 +369,7 @@ class Innosilicon(CGMiner): errors.append(InnosiliconError(error_code=err)) return errors - async def _get_wattage_limit(self, web_get_all: dict = None) -> Optional[int]: + async def _get_wattage_limit(self, web_get_all: dict | None = None) -> int | None: if web_get_all: web_get_all = web_get_all["all"] @@ -379,8 +391,9 @@ class Innosilicon(CGMiner): level = int(level) limit = 1250 + (250 * level) return limit + return None - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() diff --git a/pyasic/miners/backends/luxminer.py b/pyasic/miners/backends/luxminer.py index ac0994fb..555c1736 100644 --- a/pyasic/miners/backends/luxminer.py +++ b/pyasic/miners/backends/luxminer.py @@ -14,13 +14,12 @@ # limitations under the License. - # ------------------------------------------------------------------------------ import logging -from typing import List, Optional from pyasic.config import MinerConfig from pyasic.config.mining import MiningModePreset from pyasic.data import Fan, HashBoard from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.device.firmware import LuxOSFirmware @@ -131,6 +130,7 @@ class LUXMiner(LuxOSFirmware): return True except (APIError, LookupError): pass + return False async def reboot(self) -> bool: try: @@ -169,24 +169,40 @@ class LUXMiner(LuxOSFirmware): return False - async def atm_enabled(self) -> Optional[bool]: + async def atm_enabled(self) -> bool | None: try: result = await self.rpc.atm() return result["ATM"][0]["Enabled"] except (APIError, LookupError): pass + return None async def set_power_limit(self, wattage: int) -> bool: config = await self.get_config() + + # Check if we have preset mode with available presets + if not hasattr(config.mining_mode, "available_presets"): + logging.warning(f"{self} - Mining mode does not support presets") + return False + + available_presets = getattr(config.mining_mode, "available_presets", []) + if not available_presets: + logging.warning(f"{self} - No available presets found") + return False + valid_presets = { preset.name: preset.power - for preset in config.mining_mode.available_presets - if preset.power <= wattage + for preset in available_presets + if preset.power is not None and preset.power <= wattage } + if not valid_presets: + logging.warning(f"{self} - No valid presets found for wattage {wattage}") + return False + # Set power to highest preset <= wattage # If ATM enabled, must disable it before setting power limit - new_preset = max(valid_presets, key=valid_presets.get) + new_preset = max(valid_presets, key=lambda x: valid_presets[x]) re_enable_atm = False try: @@ -211,12 +227,13 @@ class LUXMiner(LuxOSFirmware): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_mac(self, rpc_config: dict = None) -> Optional[str]: + async def _get_mac(self, rpc_config: dict | None = None) -> str | None: if rpc_config is None: try: rpc_config = await self.rpc.config() except APIError: pass + return None if rpc_config is not None: try: @@ -224,12 +241,15 @@ class LUXMiner(LuxOSFirmware): except KeyError: pass - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: if rpc_summary is None: try: rpc_summary = await self.rpc.summary() except APIError: pass + return None if rpc_summary is not None: try: @@ -240,7 +260,7 @@ class LUXMiner(LuxOSFirmware): except (LookupError, ValueError, TypeError): pass - async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: + async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -262,8 +282,10 @@ class LUXMiner(LuxOSFirmware): board_n = idx + 1 hashboards[idx].hashrate = self.algo.hashrate( rate=float(board_stats[f"chain_rate{board_n}"]), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default) + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hashboards[idx].chips = int(board_stats[f"chain_acn{board_n}"]) chip_temp_data = list( filter( @@ -288,22 +310,26 @@ class LUXMiner(LuxOSFirmware): pass return hashboards - async def _get_wattage(self, rpc_power: dict = None) -> Optional[int]: + async def _get_wattage(self, rpc_power: dict | None = None) -> int | None: if rpc_power is None: try: rpc_power = await self.rpc.power() except APIError: pass + return None if rpc_power is not None: try: return rpc_power["POWER"][0]["Watts"] except (LookupError, ValueError, TypeError): pass + return None async def _get_wattage_limit( - self, rpc_config: dict = None, rpc_profiles: list[dict] = None - ) -> Optional[int]: + self, rpc_config: dict | None = None, rpc_profiles: dict | None = None + ) -> int | None: + if rpc_config is None or rpc_profiles is None: + return None try: active_preset = MiningModePreset.get_active_preset_from_luxos( rpc_config, rpc_profiles @@ -311,8 +337,9 @@ class LUXMiner(LuxOSFirmware): return active_preset.power except (LookupError, ValueError, TypeError): pass + return None - async def _get_fans(self, rpc_fans: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_fans: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -333,8 +360,8 @@ class LUXMiner(LuxOSFirmware): return fans async def _get_expected_hashrate( - self, rpc_stats: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_stats: dict | None = None + ) -> AlgoHashRateType | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -350,11 +377,12 @@ class LUXMiner(LuxOSFirmware): rate_unit = "GH" return self.algo.hashrate( rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) - ).into(self.algo.unit.default) + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass + return None - async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: + async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -366,8 +394,9 @@ class LUXMiner(LuxOSFirmware): return int(rpc_stats["STATS"][1]["Elapsed"]) except LookupError: pass + return None - async def _get_fw_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_fw_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -379,8 +408,9 @@ class LUXMiner(LuxOSFirmware): return rpc_version["VERSION"][0]["Miner"] except LookupError: pass + return None - async def _get_api_ver(self, rpc_version: dict = None) -> Optional[str]: + async def _get_api_ver(self, rpc_version: dict | None = None) -> str | None: if rpc_version is None: try: rpc_version = await self.rpc.version() @@ -392,8 +422,9 @@ class LUXMiner(LuxOSFirmware): return rpc_version["VERSION"][0]["API"] except LookupError: pass + return None - async def _get_fault_light(self, rpc_config: dict = None) -> Optional[bool]: + async def _get_fault_light(self, rpc_config: dict | None = None) -> bool | None: if rpc_config is None: try: rpc_config = await self.rpc.config() @@ -405,8 +436,9 @@ class LUXMiner(LuxOSFirmware): return not rpc_config["CONFIG"][0]["RedLed"] == "off" except LookupError: pass + return None - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() diff --git a/pyasic/miners/backends/marathon.py b/pyasic/miners/backends/marathon.py index 42406144..5ffbda07 100644 --- a/pyasic/miners/backends/marathon.py +++ b/pyasic/miners/backends/marathon.py @@ -1,10 +1,8 @@ -from typing import List, Optional - from pyasic import MinerConfig from pyasic.config import MiningModeConfig from pyasic.data import Fan, HashBoard from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand from pyasic.miners.device.firmware import MaraFirmware @@ -90,9 +88,12 @@ class MaraMiner(MaraFirmware): data = await self.web.get_miner_config() if 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() cfg_data = config.as_mara(user_suffix=user_suffix) merged_cfg = merge_dicts(data, cfg_data) @@ -124,12 +125,13 @@ class MaraMiner(MaraFirmware): await self.web.reload() return True - async def _get_wattage(self, web_brief: dict = None) -> Optional[int]: + async def _get_wattage(self, web_brief: dict | None = None) -> int | None: if web_brief is None: try: web_brief = await self.web.brief() except APIError: pass + return None if web_brief is not None: try: @@ -137,12 +139,13 @@ class MaraMiner(MaraFirmware): except LookupError: pass - async def _is_mining(self, web_brief: dict = None) -> Optional[bool]: + async def _is_mining(self, web_brief: dict | None = None) -> bool | None: if web_brief is None: try: web_brief = await self.web.brief() except APIError: pass + return None if web_brief is not None: try: @@ -150,12 +153,13 @@ class MaraMiner(MaraFirmware): except LookupError: pass - async def _get_uptime(self, web_brief: dict = None) -> Optional[int]: + async def _get_uptime(self, web_brief: dict | None = None) -> int | None: if web_brief is None: try: web_brief = await self.web.brief() except APIError: pass + return None if web_brief is not None: try: @@ -163,7 +167,9 @@ class MaraMiner(MaraFirmware): except LookupError: pass - async def _get_hashboards(self, web_hashboards: dict = None) -> List[HashBoard]: + async def _get_hashboards( + self, web_hashboards: dict | None = None + ) -> list[HashBoard]: if self.expected_hashboards is None: return [] @@ -183,8 +189,11 @@ class MaraMiner(MaraFirmware): for hb in web_hashboards["hashboards"]: idx = hb["index"] hashboards[idx].hashrate = self.algo.hashrate( - rate=float(hb["hashrate_average"]), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(hb["hashrate_average"]), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) hashboards[idx].temp = round( sum(hb["temperature_pcb"]) / len(hb["temperature_pcb"]) ) @@ -198,7 +207,7 @@ class MaraMiner(MaraFirmware): pass return hashboards - async def _get_mac(self, web_overview: dict = None) -> Optional[str]: + async def _get_mac(self, web_overview: dict | None = None) -> str | None: if web_overview is None: try: web_overview = await self.web.overview() @@ -210,8 +219,9 @@ class MaraMiner(MaraFirmware): return web_overview["mac"].upper() except LookupError: pass + return None - async def _get_fw_ver(self, web_overview: dict = None) -> Optional[str]: + async def _get_fw_ver(self, web_overview: dict | None = None) -> str | None: if web_overview is None: try: web_overview = await self.web.overview() @@ -223,8 +233,9 @@ class MaraMiner(MaraFirmware): return web_overview["version_firmware"] except LookupError: pass + return None - async def _get_hostname(self, web_network_config: dict = None) -> Optional[str]: + async def _get_hostname(self, web_network_config: dict | None = None) -> str | None: if web_network_config is None: try: web_network_config = await self.web.get_network_config() @@ -236,8 +247,11 @@ class MaraMiner(MaraFirmware): return web_network_config["hostname"] except LookupError: pass + return None - async def _get_hashrate(self, web_brief: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, web_brief: dict | None = None + ) -> AlgoHashRateType | None: if web_brief is None: try: web_brief = await self.web.brief() @@ -247,12 +261,14 @@ class MaraMiner(MaraFirmware): if web_brief is not None: try: return self.algo.hashrate( - rate=float(web_brief["hashrate_realtime"]), unit=self.algo.unit.TH - ).into(self.algo.unit.default) + rate=float(web_brief["hashrate_realtime"]), + unit=self.algo.unit.TH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass + return None - async def _get_fans(self, web_fans: dict = None) -> List[Fan]: + async def _get_fans(self, web_fans: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -272,7 +288,7 @@ class MaraMiner(MaraFirmware): return fans return [Fan() for _ in range(self.expected_fans)] - async def _get_fault_light(self, web_locate_miner: dict = None) -> bool: + async def _get_fault_light(self, web_locate_miner: dict | None = None) -> bool: if web_locate_miner is None: try: web_locate_miner = await self.web.get_locate_miner() @@ -287,8 +303,8 @@ class MaraMiner(MaraFirmware): return False async def _get_expected_hashrate( - self, web_brief: dict = None - ) -> Optional[AlgoHashRate]: + self, web_brief: dict | None = None + ) -> AlgoHashRateType | None: if web_brief is None: try: web_brief = await self.web.brief() @@ -298,14 +314,16 @@ class MaraMiner(MaraFirmware): if web_brief is not None: try: return self.algo.hashrate( - rate=float(web_brief["hashrate_ideal"]), unit=self.algo.unit.GH - ).into(self.algo.unit.default) + rate=float(web_brief["hashrate_ideal"]), + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except LookupError: pass + return None async def _get_wattage_limit( - self, web_miner_config: dict = None - ) -> Optional[AlgoHashRate]: + self, web_miner_config: dict | None = None + ) -> int | None: if web_miner_config is None: try: web_miner_config = await self.web.get_miner_config() @@ -317,8 +335,9 @@ class MaraMiner(MaraFirmware): return web_miner_config["mode"]["concorde"]["power-target"] except LookupError: pass + return None - async def _get_pools(self, web_pools: list = None) -> List[PoolMetrics]: + async def _get_pools(self, web_pools: list | None = None) -> list[PoolMetrics]: if web_pools is None: try: web_pools = await self.web.pools() diff --git a/pyasic/miners/backends/mskminer.py b/pyasic/miners/backends/mskminer.py index b2597285..ec31312b 100644 --- a/pyasic/miners/backends/mskminer.py +++ b/pyasic/miners/backends/mskminer.py @@ -1,7 +1,5 @@ -from typing import Optional - from pyasic import APIError -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm import AlgoHashRateType from pyasic.miners.backends import BMMiner from pyasic.miners.data import ( DataFunction, @@ -66,7 +64,9 @@ class MSKMiner(BMMiner): web: MSKMinerWebAPI _web_cls = MSKMinerWebAPI - async def _get_hashrate(self, rpc_stats: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_stats: dict | None = None + ) -> AlgoHashRateType | None: # get hr from API if rpc_stats is None: try: @@ -78,12 +78,15 @@ class MSKMiner(BMMiner): try: return self.algo.hashrate( rate=float(rpc_stats["STATS"][0]["total_rate"]), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default) + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) except (LookupError, ValueError, TypeError): pass + return None - async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]: + async def _get_wattage(self, rpc_stats: dict | None = None) -> int | None: if rpc_stats is None: try: rpc_stats = await self.rpc.stats() @@ -95,8 +98,9 @@ class MSKMiner(BMMiner): return rpc_stats["STATS"][0]["total_power"] except (LookupError, ValueError, TypeError): pass + return None - async def _get_mac(self, web_info_v1: dict = None) -> Optional[str]: + async def _get_mac(self, web_info_v1: dict | None = None) -> str | None: if web_info_v1 is None: try: web_info_v1 = await self.web.info_v1() @@ -108,3 +112,4 @@ class MSKMiner(BMMiner): return web_info_v1["network_info"]["result"]["macaddr"].upper() except (LookupError, ValueError, TypeError): pass + return None diff --git a/pyasic/miners/backends/unknown.py b/pyasic/miners/backends/unknown.py index f3764750..1bcec0e5 100644 --- a/pyasic/miners/backends/unknown.py +++ b/pyasic/miners/backends/unknown.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Optional, Tuple from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard from pyasic.data.error_codes import MinerErrorData -from pyasic.device.algorithm import AlgoHashRate +from pyasic.data.pools import PoolMetrics +from pyasic.device.algorithm import AlgoHashRateType from pyasic.miners.base import BaseMiner from pyasic.rpc.unknown import UnknownRPCAPI @@ -47,8 +47,8 @@ class UnknownMiner(BaseMiner): async def fault_light_on(self) -> bool: return False - async def get_config(self) -> None: - return None + async def get_config(self) -> MinerConfig: + return MinerConfig() async def reboot(self) -> bool: return False @@ -62,7 +62,9 @@ class UnknownMiner(BaseMiner): async def resume_mining(self) -> bool: return False - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: return None async def set_power_limit(self, wattage: int) -> bool: @@ -72,53 +74,62 @@ class UnknownMiner(BaseMiner): ### DATA GATHERING FUNCTIONS (get_{some_data}) ### ################################################## - async def _get_mac(self) -> Optional[str]: + async def _get_mac(self) -> str | None: return None - async def _get_version(self) -> Tuple[Optional[str], Optional[str]]: + async def _get_serial_number(self) -> str | None: + return None + + async def _get_version(self) -> tuple[str | None, str | None]: return None, None - async def _get_hostname(self) -> Optional[str]: + async def _get_hostname(self) -> str | None: return None - async def _get_hashrate(self) -> Optional[AlgoHashRate]: + async def _get_hashrate(self) -> AlgoHashRateType | None: return None - async def _get_hashboards(self) -> List[HashBoard]: + async def _get_hashboards(self) -> list[HashBoard]: return [] - async def _get_env_temp(self) -> Optional[float]: + async def _get_env_temp(self) -> float | None: return None - async def _get_wattage(self) -> Optional[int]: + async def _get_wattage(self) -> int | None: return None - async def _get_wattage_limit(self) -> Optional[int]: + async def _get_wattage_limit(self) -> int | None: return None - async def _get_fans(self) -> List[Fan]: + async def _get_fans(self) -> list[Fan]: return [] - async def _get_fan_psu(self) -> Optional[int]: + async def _get_fan_psu(self) -> int | None: return None - async def _get_api_ver(self) -> Optional[str]: + async def _get_api_ver(self) -> str | None: return None - async def _get_fw_ver(self) -> Optional[str]: + async def _get_fw_ver(self) -> str | None: return None - async def _get_errors(self) -> List[MinerErrorData]: + async def _get_errors(self) -> list[MinerErrorData]: return [] async def _get_fault_light(self) -> bool: return False - async def _get_expected_hashrate(self) -> Optional[AlgoHashRate]: + async def _get_expected_hashrate(self) -> AlgoHashRateType | None: return None - async def _is_mining(self, *args, **kwargs) -> Optional[bool]: + async def _is_mining(self, *args, **kwargs) -> bool | None: return None - async def _get_uptime(self, *args, **kwargs) -> Optional[int]: + async def _get_uptime(self, *args, **kwargs) -> int | None: + return None + + async def _get_pools(self) -> list[PoolMetrics]: + return [] + + async def _get_voltage(self) -> float | None: return None diff --git a/pyasic/miners/backends/vnish.py b/pyasic/miners/backends/vnish.py index be7ef6de..148f43ab 100644 --- a/pyasic/miners/backends/vnish.py +++ b/pyasic/miners/backends/vnish.py @@ -15,11 +15,11 @@ # ------------------------------------------------------------------------------ import logging -from typing import List, Optional from pyasic import MinerConfig -from pyasic.data.error_codes import MinerErrorData, VnishError -from pyasic.device.algorithm import AlgoHashRate +from pyasic.config.mining import MiningModePreset +from pyasic.data.error_codes import VnishError +from pyasic.device.algorithm import AlgoHashRateType from pyasic.errors import APIError from pyasic.miners.backends.bmminer import BMMiner from pyasic.miners.data import ( @@ -106,7 +106,9 @@ class VNish(VNishFirmware, BMMiner): data_locations = VNISH_DATA_LOC - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: await self.web.post_settings( miner_settings=config.as_vnish(user_suffix=user_suffix) ) @@ -147,7 +149,7 @@ class VNish(VNishFirmware, BMMiner): pass return False - async def _get_mac(self, web_summary: dict = None) -> str: + async def _get_mac(self, web_summary: dict | None = None) -> str | None: if web_summary is not None: try: mac = web_summary["system"]["network_status"]["mac"] @@ -164,6 +166,8 @@ class VNish(VNishFirmware, BMMiner): except KeyError: pass + return None + async def fault_light_off(self) -> bool: result = await self.web.find_miner() if result is not None: @@ -171,6 +175,7 @@ class VNish(VNishFirmware, BMMiner): return True else: await self.web.find_miner() + return False async def fault_light_on(self) -> bool: result = await self.web.find_miner() @@ -179,26 +184,27 @@ class VNish(VNishFirmware, BMMiner): return True else: await self.web.find_miner() + return False - async def _get_hostname(self, web_summary: dict = None) -> str: + async def _get_hostname(self, web_summary: dict | None = None) -> str | None: if web_summary is None: web_info = await self.web.info() - if web_info is not None: try: hostname = web_info["system"]["network_status"]["hostname"] return hostname except KeyError: pass - - if web_summary is not None: + else: try: hostname = web_summary["system"]["network_status"]["hostname"] return hostname except KeyError: pass - async def _get_wattage(self, web_summary: dict = None) -> Optional[int]: + return None + + async def _get_wattage(self, web_summary: dict | None = None) -> int | None: if web_summary is None: web_summary = await self.web.summary() @@ -209,25 +215,30 @@ class VNish(VNishFirmware, BMMiner): return wattage except KeyError: pass + return None - async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_summary: dict | None = None + ) -> AlgoHashRateType | None: # get hr from API if rpc_summary is None: try: rpc_summary = await self.rpc.summary() except APIError: - pass + return None if rpc_summary is not None: try: return self.algo.hashrate( rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), - unit=self.algo.unit.GH, - ).into(self.algo.unit.default) + unit=self.algo.unit.GH, # type: ignore[attr-defined] + ).into(self.algo.unit.default) # type: ignore[attr-defined] except (LookupError, ValueError, TypeError): pass - async def _get_wattage_limit(self, web_settings: dict = None) -> Optional[int]: + return None + + async def _get_wattage_limit(self, web_settings: dict | None = None) -> int | None: if web_settings is None: web_settings = await self.web.summary() @@ -240,7 +251,9 @@ class VNish(VNishFirmware, BMMiner): except (KeyError, TypeError): pass - async def _get_fw_ver(self, web_summary: dict = None) -> Optional[str]: + return None + + async def _get_fw_ver(self, web_summary: dict | None = None) -> str | None: if web_summary is None: web_summary = await self.web.summary() @@ -253,16 +266,16 @@ class VNish(VNishFirmware, BMMiner): except LookupError: return fw_ver - async def _is_mining(self, web_summary: dict = None) -> Optional[bool]: + async def _is_mining(self, web_summary: dict | None = None) -> bool | None: if web_summary is None: try: web_summary = await self.web.summary() except APIError: - pass + return None if web_summary is not None: try: - is_mining = not web_summary["miner"]["miner_status"]["miner_state"] in [ + is_mining = web_summary["miner"]["miner_status"]["miner_state"] not in [ "stopped", "shutting-down", "failure", @@ -271,8 +284,13 @@ class VNish(VNishFirmware, BMMiner): except LookupError: pass - async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]: - errors = [] + return None + + async def _get_errors( # type: ignore[override] + self, web_summary: dict | None = None + ) -> list[VnishError]: + errors: list[VnishError] = [] + if web_summary is None: try: web_summary = await self.web.summary() @@ -292,10 +310,13 @@ class VNish(VNishFirmware, BMMiner): async def get_config(self) -> MinerConfig: try: web_settings = await self.web.settings() - web_presets = await self.web.autotune_presets() + web_presets_dict = await self.web.autotune_presets() + web_presets = ( + web_presets_dict.get("presets", []) if web_presets_dict else [] + ) web_perf_summary = (await self.web.perf_summary()) or {} except APIError: - return self.config + return self.config or MinerConfig() self.config = MinerConfig.from_vnish( web_settings, web_presets, web_perf_summary ) @@ -303,11 +324,20 @@ class VNish(VNishFirmware, BMMiner): async def set_power_limit(self, wattage: int) -> bool: config = await self.get_config() + + # Check if mining mode is preset mode and has available presets + if not isinstance(config.mining_mode, MiningModePreset): + return False + valid_presets = [ preset.power for preset in config.mining_mode.available_presets - if preset.tuned and preset.power <= wattage + if (preset.tuned and preset.power is not None and preset.power <= wattage) ] + + if not valid_presets: + return False + new_wattage = max(valid_presets) # Set power to highest preset <= wattage diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index ff51fa32..eae6fb7c 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -16,54 +16,53 @@ import asyncio import ipaddress import warnings -from typing import List, Optional, Protocol, Tuple, Type, TypeVar, Union +from typing import Any, Protocol, TypeVar from pyasic.config import MinerConfig from pyasic.data import Fan, HashBoard, MinerData from pyasic.data.device import DeviceInfo from pyasic.data.error_codes import MinerErrorData from pyasic.data.pools import PoolMetrics -from pyasic.device.algorithm import MinerAlgoType +from pyasic.device.algorithm import AlgoHashRateType, MinerAlgoType from pyasic.device.algorithm.base import GenericAlgo -from pyasic.device.algorithm.hashrate import AlgoHashRate from pyasic.device.firmware import MinerFirmware from pyasic.device.makes import MinerMake from pyasic.device.models import MinerModelType from pyasic.errors import APIError from pyasic.logger import logger -from pyasic.miners.data import DataLocations, DataOptions, RPCAPICommand, WebAPICommand +from pyasic.miners.data import DataOptions, RPCAPICommand, WebAPICommand class MinerProtocol(Protocol): - _rpc_cls: Type = None - _web_cls: Type = None - _ssh_cls: Type = None + _rpc_cls: type[Any] | None = None + _web_cls: type[Any] | None = None + _ssh_cls: type[Any] | None = None - ip: str = None - rpc: _rpc_cls = None - web: _web_cls = None - ssh: _ssh_cls = None + ip: str | None = None + rpc: Any | None = None + web: Any | None = None + ssh: Any | None = None - make: MinerMake = None - raw_model: MinerModelType = None - firmware: MinerFirmware = None + make: MinerMake | None = None + raw_model: MinerModelType | None = None + firmware: MinerFirmware | None = None algo: type[MinerAlgoType] = GenericAlgo - expected_hashboards: int = None - expected_chips: int = None - expected_fans: int = None + expected_hashboards: int | None = None + expected_chips: int | None = None + expected_fans: int | None = None - data_locations: DataLocations = None + data_locations: Any | None = None supports_shutdown: bool = False supports_power_modes: bool = False supports_presets: bool = False supports_autotuning: bool = False - api_ver: str = None - fw_ver: str = None - light: bool = None - config: MinerConfig = None + api_ver: str | None = None + fw_ver: str | None = None + light: bool | None = None + config: MinerConfig | None = None def __repr__(self): return f"{self.model}: {str(self.ip)}" @@ -80,9 +79,9 @@ class MinerProtocol(Protocol): @property def model(self) -> str: if self.raw_model is not None: - model_data = [self.raw_model] + model_data = [str(self.raw_model)] elif self.make is not None: - model_data = [self.make] + model_data = [str(self.make)] else: model_data = ["Unknown"] if self.firmware is not None: @@ -148,7 +147,9 @@ class MinerProtocol(Protocol): """ return False - async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: + async def send_config( + self, config: MinerConfig, user_suffix: str | None = None + ) -> None: """Set the mining configuration of the miner. Parameters: @@ -187,9 +188,9 @@ class MinerProtocol(Protocol): async def upgrade_firmware( self, *, - file: str = None, - url: str = None, - version: str = None, + file: str | None = None, + url: str | None = None, + version: str | None = None, keep_settings: bool = True, ) -> bool: """Upgrade the firmware of the miner. @@ -209,7 +210,7 @@ class MinerProtocol(Protocol): ### 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. Returns: @@ -217,7 +218,7 @@ class MinerProtocol(Protocol): """ 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. Returns: @@ -225,7 +226,7 @@ class MinerProtocol(Protocol): """ return await self._get_mac() - async def get_model(self) -> Optional[str]: + async def get_model(self) -> str | None: """Get the model of the miner and return it as a string. Returns: @@ -233,7 +234,7 @@ class MinerProtocol(Protocol): """ return self.model - async def get_device_info(self) -> Optional[DeviceInfo]: + async def get_device_info(self) -> DeviceInfo | None: """Get device information, including model, make, and firmware. Returns: @@ -241,7 +242,7 @@ class MinerProtocol(Protocol): """ return self.device_info - async def get_api_ver(self) -> Optional[str]: + async def get_api_ver(self) -> str | None: """Get the API version of the miner and is as a string. Returns: @@ -249,7 +250,7 @@ class MinerProtocol(Protocol): """ return await self._get_api_ver() - async def get_fw_ver(self) -> Optional[str]: + async def get_fw_ver(self) -> str | None: """Get the firmware version of the miner and is as a string. Returns: @@ -257,7 +258,7 @@ class MinerProtocol(Protocol): """ return await self._get_fw_ver() - async def get_version(self) -> Tuple[Optional[str], Optional[str]]: + async def get_version(self) -> tuple[str | None, str | None]: """Get the API version and firmware version of the miner and return them as strings. Returns: @@ -267,7 +268,7 @@ class MinerProtocol(Protocol): fw_ver = await self.get_fw_ver() return api_ver, fw_ver - async def get_hostname(self) -> Optional[str]: + async def get_hostname(self) -> str | None: """Get the hostname of the miner and return it as a string. Returns: @@ -275,7 +276,7 @@ class MinerProtocol(Protocol): """ return await self._get_hostname() - async def get_hashrate(self) -> Optional[AlgoHashRate]: + async def get_hashrate(self) -> AlgoHashRateType | None: """Get the hashrate of the miner and return it as a float in TH/s. Returns: @@ -283,7 +284,7 @@ class MinerProtocol(Protocol): """ return await self._get_hashrate() - async def get_hashboards(self) -> List[HashBoard]: + async def get_hashboards(self) -> list[HashBoard]: """Get hashboard data from the miner in the form of [`HashBoard`][pyasic.data.HashBoard]. Returns: @@ -291,7 +292,7 @@ class MinerProtocol(Protocol): """ return await self._get_hashboards() - async def get_env_temp(self) -> Optional[float]: + async def get_env_temp(self) -> float | None: """Get environment temp from the miner as a float. Returns: @@ -299,7 +300,7 @@ class MinerProtocol(Protocol): """ return await self._get_env_temp() - async def get_wattage(self) -> Optional[int]: + async def get_wattage(self) -> int | None: """Get wattage from the miner as an int. Returns: @@ -307,7 +308,7 @@ class MinerProtocol(Protocol): """ return await self._get_wattage() - async def get_voltage(self) -> Optional[float]: + async def get_voltage(self) -> float | None: """Get output voltage of the PSU as a float. Returns: @@ -315,7 +316,7 @@ class MinerProtocol(Protocol): """ return await self._get_voltage() - async def get_wattage_limit(self) -> Optional[int]: + async def get_wattage_limit(self) -> int | None: """Get wattage limit from the miner as an int. Returns: @@ -323,7 +324,7 @@ class MinerProtocol(Protocol): """ return await self._get_wattage_limit() - async def get_fans(self) -> List[Fan]: + async def get_fans(self) -> list[Fan]: """Get fan data from the miner in the form [fan_1, fan_2, fan_3, fan_4]. Returns: @@ -331,7 +332,7 @@ class MinerProtocol(Protocol): """ return await self._get_fans() - async def get_fan_psu(self) -> Optional[int]: + async def get_fan_psu(self) -> int | None: """Get PSU fan speed from the miner. Returns: @@ -339,7 +340,7 @@ class MinerProtocol(Protocol): """ return await self._get_fan_psu() - async def get_errors(self) -> List[MinerErrorData]: + async def get_errors(self) -> list[MinerErrorData]: """Get a list of the errors the miner is experiencing. Returns: @@ -353,9 +354,10 @@ class MinerProtocol(Protocol): Returns: A boolean value where `True` represents on and `False` represents off. """ - return await self._get_fault_light() + result = await self._get_fault_light() + return result if result is not None else False - async def get_expected_hashrate(self) -> Optional[AlgoHashRate]: + async def get_expected_hashrate(self) -> AlgoHashRateType | None: """Get the nominal hashrate from factory if available. Returns: @@ -363,7 +365,7 @@ class MinerProtocol(Protocol): """ return await self._get_expected_hashrate() - async def is_mining(self) -> Optional[bool]: + async def is_mining(self) -> bool | None: """Check whether the miner is mining. Returns: @@ -371,7 +373,7 @@ class MinerProtocol(Protocol): """ return await self._is_mining() - async def get_uptime(self) -> Optional[int]: + async def get_uptime(self) -> int | None: """Get the uptime of the miner in seconds. Returns: @@ -379,7 +381,7 @@ class MinerProtocol(Protocol): """ return await self._get_uptime() - async def get_pools(self) -> List[PoolMetrics]: + async def get_pools(self) -> list[PoolMetrics]: """Get the pools information from Miner. Returns: @@ -387,68 +389,68 @@ class MinerProtocol(Protocol): """ return await self._get_pools() - async def _get_serial_number(self) -> Optional[str]: + async def _get_serial_number(self) -> str | None: pass - async def _get_mac(self) -> Optional[str]: - pass + async def _get_mac(self) -> str | None: + return None - async def _get_api_ver(self) -> Optional[str]: - pass + async def _get_api_ver(self) -> str | None: + return None - async def _get_fw_ver(self) -> Optional[str]: - pass + async def _get_fw_ver(self) -> str | None: + return None - async def _get_hostname(self) -> Optional[str]: - pass + async def _get_hostname(self) -> str | None: + return None - async def _get_hashrate(self) -> Optional[AlgoHashRate]: - pass + async def _get_hashrate(self) -> AlgoHashRateType | None: + return None - async def _get_hashboards(self) -> List[HashBoard]: + async def _get_hashboards(self) -> list[HashBoard]: return [] - async def _get_env_temp(self) -> Optional[float]: - pass + async def _get_env_temp(self) -> float | None: + return None - async def _get_wattage(self) -> Optional[int]: - pass + async def _get_wattage(self) -> int | None: + return None - async def _get_voltage(self) -> Optional[float]: - pass + async def _get_voltage(self) -> float | None: + return None - async def _get_wattage_limit(self) -> Optional[int]: - pass + async def _get_wattage_limit(self) -> int | None: + return None - async def _get_fans(self) -> List[Fan]: + async def _get_fans(self) -> list[Fan]: return [] - async def _get_fan_psu(self) -> Optional[int]: - pass + async def _get_fan_psu(self) -> int | None: + return None - async def _get_errors(self) -> List[MinerErrorData]: + async def _get_errors(self) -> list[MinerErrorData]: return [] - async def _get_fault_light(self) -> Optional[bool]: - pass + async def _get_fault_light(self) -> bool | None: + return None - async def _get_expected_hashrate(self) -> Optional[AlgoHashRate]: - pass + async def _get_expected_hashrate(self) -> AlgoHashRateType | None: + return None - async def _is_mining(self) -> Optional[bool]: - pass + async def _is_mining(self) -> bool | None: + return None - async def _get_uptime(self) -> Optional[int]: - pass + async def _get_uptime(self) -> int | None: + return None - async def _get_pools(self) -> List[PoolMetrics]: - pass + async def _get_pools(self) -> list[PoolMetrics]: + return [] async def _get_data( self, allow_warning: bool, - include: List[Union[str, DataOptions]] = None, - exclude: List[Union[str, DataOptions]] = None, + include: list[str | DataOptions] | None = None, + exclude: list[str | DataOptions] | None = None, ) -> dict: # handle include if include is not None: @@ -470,7 +472,7 @@ class MinerProtocol(Protocol): for data_name in include: try: # get kwargs needed for the _get_xyz function - fn_args = getattr(self.data_locations, data_name).kwargs + fn_args = getattr(self.data_locations, str(data_name)).kwargs # keep track of which RPC/Web commands need to be sent for arg in fn_args: @@ -483,13 +485,21 @@ class MinerProtocol(Protocol): continue # create tasks for all commands that need to be sent, or no-op with sleep(0) -> None - if len(rpc_multicommand) > 0: + if ( + len(rpc_multicommand) > 0 + and self.rpc is not None + and hasattr(self.rpc, "multicommand") + ): rpc_command_task = asyncio.create_task( self.rpc.multicommand(*rpc_multicommand, allow_warning=allow_warning) ) else: rpc_command_task = asyncio.create_task(asyncio.sleep(0)) - if len(web_multicommand) > 0: + if ( + len(web_multicommand) > 0 + and self.web is not None + and hasattr(self.web, "multicommand") + ): web_command_task = asyncio.create_task( self.web.multicommand(*web_multicommand, allow_warning=allow_warning) ) @@ -511,7 +521,7 @@ class MinerProtocol(Protocol): for data_name in include: try: - fn_args = getattr(self.data_locations, data_name).kwargs + fn_args = getattr(self.data_locations, str(data_name)).kwargs args_to_send = {k.name: None for k in fn_args} for arg in fn_args: try: @@ -532,7 +542,9 @@ class MinerProtocol(Protocol): except LookupError: continue try: - function = getattr(self, getattr(self.data_locations, data_name).cmd) + function = getattr( + self, getattr(self.data_locations, str(data_name)).cmd + ) miner_data[data_name] = await function(**args_to_send) except Exception as e: raise APIError( @@ -543,8 +555,8 @@ class MinerProtocol(Protocol): async def get_data( self, allow_warning: bool = False, - include: List[Union[str, DataOptions]] = None, - exclude: List[Union[str, DataOptions]] = None, + include: list[str | DataOptions] | None = None, + exclude: list[str | DataOptions] | None = None, ) -> MinerData: """Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData]. @@ -562,6 +574,7 @@ class MinerProtocol(Protocol): expected_chips=( self.expected_chips * self.expected_hashboards if self.expected_chips is not None + and self.expected_hashboards is not None else 0 ), expected_hashboards=self.expected_hashboards, diff --git a/pyasic/miners/data.py b/pyasic/miners/data.py index 29ea23f2..818824db 100644 --- a/pyasic/miners/data.py +++ b/pyasic/miners/data.py @@ -16,7 +16,6 @@ from dataclasses import dataclass, field, make_dataclass from enum import Enum -from typing import List, Union class DataOptions(Enum): @@ -67,7 +66,7 @@ class WebAPICommand: @dataclass class DataFunction: cmd: str - kwargs: List[Union[RPCAPICommand, WebAPICommand]] = field(default_factory=list) + kwargs: list[RPCAPICommand | WebAPICommand] = field(default_factory=list) def __call__(self, *args, **kwargs): return self diff --git a/pyasic/miners/device/models/antminer/X19/S19.py b/pyasic/miners/device/models/antminer/X19/S19.py index 989b6894..551cd2e9 100644 --- a/pyasic/miners/device/models/antminer/X19/S19.py +++ b/pyasic/miners/device/models/antminer/X19/S19.py @@ -14,7 +14,7 @@ # limitations under the License. - # ------------------------------------------------------------------------------ from pyasic.device.algorithm import MinerAlgo -from pyasic.device.models import MinerModel +from pyasic.device.models import MinerModel, MinerModelType from pyasic.miners.device.makes import AntMinerMake @@ -118,7 +118,7 @@ class S19jNoPIC(AntMinerMake): class S19jPro(AntMinerMake): - raw_model = MinerModel.ANTMINER.S19jPro + raw_model: MinerModelType = MinerModel.ANTMINER.S19jPro expected_chips = 126 expected_fans = 4 @@ -163,7 +163,7 @@ class S19jProPlusNoPIC(AntMinerMake): class S19kPro(AntMinerMake): - raw_model = MinerModel.ANTMINER.S19kPro + raw_model: MinerModelType = MinerModel.ANTMINER.S19kPro expected_chips = 77 expected_fans = 4 diff --git a/pyasic/miners/device/models/antminer/X19/__init__.py b/pyasic/miners/device/models/antminer/X19/__init__.py index 43550efb..3ec0e61a 100644 --- a/pyasic/miners/device/models/antminer/X19/__init__.py +++ b/pyasic/miners/device/models/antminer/X19/__init__.py @@ -30,8 +30,8 @@ from .S19 import ( S19jProPlus, S19jProPlusNoPIC, S19jXP, - S19kPro, S19KPro, + S19kPro, S19kProNoPIC, S19NoPIC, S19Plus, diff --git a/pyasic/miners/device/models/iceriver/__init__.py b/pyasic/miners/device/models/iceriver/__init__.py index 7587b37e..1714b496 100644 --- a/pyasic/miners/device/models/iceriver/__init__.py +++ b/pyasic/miners/device/models/iceriver/__init__.py @@ -1,2 +1,18 @@ from .ALX import * from .KSX import * + +# Define what gets exported with wildcard to exclude KS3 and KS5 +# which conflict with antminer models +__all__ = [ + # From ALX + "AL3", + # From KSX + "KS0", + "KS1", + "KS2", + "KS3L", + "KS3M", + "KS5L", + "KS5M", + # Note: KS3 and KS5 are excluded to avoid conflicts with antminer +] diff --git a/pyasic/miners/device/models/whatsminer/M3X/__init__.py b/pyasic/miners/device/models/whatsminer/M3X/__init__.py index 106938b8..97d84cd4 100644 --- a/pyasic/miners/device/models/whatsminer/M3X/__init__.py +++ b/pyasic/miners/device/models/whatsminer/M3X/__init__.py @@ -141,6 +141,7 @@ from .M31S_Plus import ( ) from .M31SE import M31SEV10, M31SEV20, M31SEV30 from .M32 import M32V10, M32V20 +from .M32S import M32S from .M33 import M33V10, M33V20, M33V30 from .M33S import M33SVG30 from .M33S_Plus import M33SPlusVG20, M33SPlusVG30, M33SPlusVH20, M33SPlusVH30 diff --git a/pyasic/miners/factory.py b/pyasic/miners/factory.py index 80c62d8a..5c40916a 100644 --- a/pyasic/miners/factory.py +++ b/pyasic/miners/factory.py @@ -21,7 +21,8 @@ import ipaddress import json import re import warnings -from typing import Any, AsyncGenerator, Callable +from collections.abc import AsyncGenerator, Callable +from typing import Any, cast import anyio import httpx @@ -70,7 +71,7 @@ class MinerTypes(enum.Enum): MSKMINER = 18 -MINER_CLASSES = { +MINER_CLASSES: dict[MinerTypes, dict[str | None, Any]] = { MinerTypes.ANTMINER: { None: type("AntminerUnknown", (BMMiner, AntMinerMake), {}), "ANTMINER D3": CGMinerD3, @@ -731,8 +732,9 @@ class MinerFactory: async def get_multiple_miners( self, ips: list[str], limit: int = 200 ) -> list[AnyMiner]: - results = [] + results: list[AnyMiner] = [] + miner: AnyMiner async for miner in self.get_miner_generator(ips, limit): results.append(miner) @@ -740,7 +742,7 @@ class MinerFactory: async def get_miner_generator( self, ips: list, limit: int = 200 - ) -> AsyncGenerator[AnyMiner]: + ) -> AsyncGenerator[AnyMiner, None]: tasks = [] semaphore = asyncio.Semaphore(limit) @@ -749,11 +751,13 @@ class MinerFactory: for task in tasks: async with semaphore: - result = await task + result = await task # type: ignore[func-returns-value] if result is not None: yield result - async def get_miner(self, ip: str | ipaddress.ip_address) -> AnyMiner | None: + async def get_miner( + self, ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address + ) -> AnyMiner | None: ip = str(ip) miner_type = None @@ -771,7 +775,7 @@ class MinerFactory: break if miner_type is not None: - miner_model = None + miner_model: str | None = None miner_model_fns = { MinerTypes.ANTMINER: self.get_miner_model_antminer, MinerTypes.WHATSMINER: self.get_miner_model_whatsminer, @@ -792,7 +796,7 @@ class MinerFactory: MinerTypes.VOLCMINER: self.get_miner_model_volcminer, MinerTypes.ELPHAPEX: self.get_miner_model_elphapex, } - version = None + version: str | None = None miner_version_fns = { MinerTypes.WHATSMINER: self.get_miner_version_whatsminer, } @@ -801,18 +805,18 @@ class MinerFactory: if model_fn is not None: # noinspection PyArgumentList - task = asyncio.create_task(model_fn(ip)) + model_task = asyncio.create_task(model_fn(ip)) try: miner_model = await asyncio.wait_for( - task, timeout=settings.get("factory_get_timeout", 3) + model_task, timeout=settings.get("factory_get_timeout", 3) ) except asyncio.TimeoutError: pass if version_fn is not None: - task = asyncio.create_task(version_fn(ip)) + version_task = asyncio.create_task(version_fn(ip)) try: version = await asyncio.wait_for( - task, timeout=settings.get("factory_get_timeout", 3) + version_task, timeout=settings.get("factory_get_timeout", 3) ) except asyncio.TimeoutError: pass @@ -820,6 +824,7 @@ class MinerFactory: ip, miner_type=miner_type, miner_model=miner_model, version=version ) return miner + return None async def _get_miner_type(self, ip: str) -> MinerTypes | None: tasks = [ @@ -850,14 +855,15 @@ class MinerFactory: if res is not None: mtype = MinerTypes.MARATHON if mtype == MinerTypes.HAMMER: - res = await self.get_miner_model_hammer(ip) - if res is None: + hammer_model = await self.get_miner_model_hammer(ip) + if hammer_model is None: return MinerTypes.HAMMER - if "HAMMER" in res.upper(): + if "HAMMER" in hammer_model.upper(): mtype = MinerTypes.HAMMER else: mtype = MinerTypes.VOLCMINER return mtype + return None @staticmethod async def _web_ping( @@ -919,6 +925,7 @@ class MinerFactory: return MinerTypes.INNOSILICON if "Miner UI" in web_text: return MinerTypes.AURADINE + return None async def _get_miner_socket(self, ip: str) -> MinerTypes | None: commands = ["version", "devdetails"] @@ -931,6 +938,7 @@ class MinerFactory: if data is not None: d = self._parse_socket_type(data) return d + return None @staticmethod async def _socket_ping(ip: str, cmd: str) -> str | None: @@ -941,13 +949,13 @@ class MinerFactory: timeout=settings.get("factory_get_timeout", 3), ) except (ConnectionError, OSError, asyncio.TimeoutError): - return + return None - cmd = {"command": cmd} + command_dict = {"command": cmd} try: # send the command - writer.write(json.dumps(cmd).encode("utf-8")) + writer.write(json.dumps(command_dict).encode("utf-8")) await writer.drain() # loop to receive all the data @@ -964,11 +972,11 @@ class MinerFactory: logger.warning(f"{ip}: Socket ping timeout.") break except ConnectionResetError: - return + return None except asyncio.CancelledError: raise except (ConnectionError, OSError): - return + return None finally: # Handle cancellation explicitly if writer.transport.is_closing(): @@ -978,9 +986,10 @@ class MinerFactory: try: await writer.wait_closed() except (ConnectionError, OSError): - return + return None if data: return data.decode("utf-8") + return None @staticmethod def _parse_socket_type(data: str) -> MinerTypes | None: @@ -1013,12 +1022,13 @@ class MinerFactory: return MinerTypes.AURADINE if "VNISH" in upper_data: return MinerTypes.VNISH + return None async def send_web_command( self, ip: str, location: str, - auth: httpx.DigestAuth = None, + auth: httpx.DigestAuth | None = None, ) -> dict | None: async with httpx.AsyncClient(transport=settings.transport()) as session: try: @@ -1029,16 +1039,16 @@ class MinerFactory: ) except (httpx.HTTPError, asyncio.TimeoutError): logger.info(f"{ip}: Web command timeout.") - return + return None if data is None: - return + return None try: json_data = data.json() except (json.JSONDecodeError, asyncio.TimeoutError): try: return json.loads(data.text) except (json.JSONDecodeError, httpx.HTTPError): - return + return None else: return json_data @@ -1047,7 +1057,7 @@ class MinerFactory: try: reader, writer = await asyncio.open_connection(ip, 4028) except (ConnectionError, OSError): - return + return None cmd = {"command": command} try: @@ -1067,26 +1077,26 @@ class MinerFactory: except asyncio.CancelledError: writer.close() await writer.wait_closed() - return + return None except (ConnectionError, OSError): - return + return None if data == b"Socket connect failed: Connection refused\n": - return + return None - data = await self._fix_api_data(data) + data_str = await self._fix_api_data(data) try: - data = json.loads(data) + data_dict = json.loads(data_str) except json.JSONDecodeError: return {} - return data + return data_dict async def send_btminer_v3_api_command(self, ip, command): try: reader, writer = await asyncio.open_connection(ip, 4433) except (ConnectionError, OSError): - return + return None cmd = {"cmd": command} try: @@ -1108,11 +1118,11 @@ class MinerFactory: except asyncio.CancelledError: writer.close() await writer.wait_closed() - return + return None except (ConnectionError, OSError): - return + return None if data == b"Socket connect failed: Connection refused\n": - return + return None try: data = json.loads(data) @@ -1156,7 +1166,7 @@ class MinerFactory: @staticmethod def _select_miner_from_classes( - ip: ipaddress.ip_address, + ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address, miner_model: str | None, miner_type: MinerTypes | None, version: str | None = None, @@ -1165,6 +1175,10 @@ class MinerFactory: if "HIVEON" in str(miner_model).upper(): miner_model = str(miner_model).upper().replace(" HIVEON", "") miner_type = MinerTypes.HIVEON + + if miner_type is None: + return cast(AnyMiner, UnknownMiner(str(ip), version)) + try: return MINER_CLASSES[miner_type][str(miner_model).upper()](ip, version) except LookupError: @@ -1175,7 +1189,7 @@ class MinerFactory: f"and this model on GitHub (https://github.com/UpstreamData/pyasic/issues)." ) return MINER_CLASSES[miner_type][None](ip, version) - return UnknownMiner(str(ip), version) + return cast(AnyMiner, UnknownMiner(str(ip), version)) async def get_miner_model_antminer(self, ip: str) -> str | None: tasks = [ @@ -1194,25 +1208,28 @@ class MinerFactory: ip, "/cgi-bin/get_system_info.cgi", auth=auth ) - try: - miner_model = web_json_data["minertype"] - - return miner_model - except (TypeError, LookupError): - pass + if web_json_data is not None: + try: + miner_model = web_json_data["minertype"] + return miner_model + except (TypeError, LookupError): + pass + return None async def _get_model_antminer_sock(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "version") - try: - miner_model = sock_json_data["VERSION"][0]["Type"] + if sock_json_data is not None: + try: + miner_model = sock_json_data["VERSION"][0]["Type"] - if " (" in miner_model: - split_miner_model = miner_model.split(" (") - miner_model = split_miner_model[0] + if " (" in miner_model: + split_miner_model = miner_model.split(" (") + miner_model = split_miner_model[0] - return miner_model - except (TypeError, LookupError): - pass + return miner_model + except (TypeError, LookupError): + pass + return None sock_json_data = await self.send_api_command(ip, "stats") try: @@ -1225,24 +1242,29 @@ class MinerFactory: return miner_model except (TypeError, LookupError): pass + return None async def get_miner_model_goldshell(self, ip: str) -> str | None: json_data = await self.send_web_command(ip, "/mcb/status") - try: - miner_model = json_data["model"].replace("-", " ") - - return miner_model - except (TypeError, LookupError): - pass + if json_data is not None: + try: + miner_model = json_data["model"].replace("-", " ") + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_whatsminer(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "devdetails") - try: - miner_model = sock_json_data["DEVDETAILS"][0]["Model"].replace("_", "") - miner_model = miner_model[:-1] + "0" - return miner_model - except (TypeError, LookupError): + if sock_json_data is not None: + try: + miner_model = sock_json_data["DEVDETAILS"][0]["Model"].replace("_", "") + miner_model = miner_model[:-1] + "0" + return miner_model + except (TypeError, LookupError): + pass + else: sock_json_data_v3 = await self.send_btminer_v3_api_command( ip, "get.device.info" ) @@ -1253,27 +1275,32 @@ class MinerFactory: return miner_model except (TypeError, LookupError): pass + return None async def get_miner_version_whatsminer(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "get_version") - try: - version = sock_json_data["Msg"]["fw_ver"] - return version - except LookupError: - pass + if sock_json_data is not None: + try: + version = sock_json_data["Msg"]["fw_ver"] + return version + except LookupError: + pass + return None async def get_miner_model_avalonminer(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "version") - try: - miner_model = sock_json_data["VERSION"][0]["PROD"].upper() - if "-" in miner_model: - miner_model = miner_model.split("-")[0] - if miner_model in ["AVALONNANO", "AVALON0O", "AVALONMINER 15"]: - subtype = sock_json_data["VERSION"][0]["MODEL"].upper() - miner_model = f"AVALONMINER {subtype}" - return miner_model - except (TypeError, LookupError): - pass + if sock_json_data is not None: + try: + miner_model = sock_json_data["VERSION"][0]["PROD"].upper() + if "-" in miner_model: + miner_model = miner_model.split("-")[0] + if miner_model in ["AVALONNANO", "AVALON0O", "AVALONMINER 15"]: + subtype = sock_json_data["VERSION"][0]["MODEL"].upper() + miner_model = f"AVALONMINER {subtype}" + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_innosilicon(self, ip: str) -> str | None: try: @@ -1289,7 +1316,7 @@ class MinerFactory: ) auth = auth_req.json()["jwt"] except (httpx.HTTPError, LookupError): - return + return None try: async with httpx.AsyncClient(transport=settings.transport()) as session: @@ -1303,6 +1330,7 @@ class MinerFactory: return web_data["type"] except (httpx.HTTPError, LookupError): pass + return None try: async with httpx.AsyncClient(transport=settings.transport()) as session: web_data = ( @@ -1315,18 +1343,21 @@ class MinerFactory: return web_data["type"] except (httpx.HTTPError, LookupError): pass + return None async def get_miner_model_braiins_os(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "devdetails") - try: - miner_model = ( - sock_json_data["DEVDETAILS"][0]["Model"] - .replace("Bitmain ", "") - .replace("S19XP", "S19 XP") - ) - return miner_model - except (TypeError, LookupError): - pass + if sock_json_data is not None: + try: + miner_model = ( + sock_json_data["DEVDETAILS"][0]["Model"] + .replace("Bitmain ", "") + .replace("S19XP", "S19 XP") + ) + return miner_model + except (TypeError, LookupError): + pass + return None try: async with httpx.AsyncClient(transport=settings.transport()) as session: @@ -1342,64 +1373,75 @@ class MinerFactory: return miner_model except (httpx.HTTPError, LookupError): pass + return None async def get_miner_model_vnish(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "stats") - try: - miner_model = sock_json_data["STATS"][0]["Type"] - if " (" in miner_model: - split_miner_model = miner_model.split(" (") - miner_model = split_miner_model[0] + if sock_json_data is not None: + try: + miner_model = sock_json_data["STATS"][0]["Type"] + if " (" in miner_model: + split_miner_model = miner_model.split(" (") + miner_model = split_miner_model[0] - if "(88)" in miner_model: - miner_model = miner_model.replace("(88)", "NOPIC") + if "(88)" in miner_model: + miner_model = miner_model.replace("(88)", "NOPIC") - if " AML" in miner_model: - miner_model = miner_model.replace(" AML", "") + if " AML" in miner_model: + miner_model = miner_model.replace(" AML", "") - return miner_model - except (TypeError, LookupError): - pass + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_epic(self, ip: str) -> str | None: for retry_cnt in range(settings.get("get_data_retries", 1)): sock_json_data = await self.send_web_command(ip, ":4028/capabilities") - try: - miner_model = sock_json_data["Model"] - return miner_model - except (TypeError, LookupError): + if sock_json_data is not None: + try: + miner_model = sock_json_data["Model"] + return miner_model + except (TypeError, LookupError): + pass + else: if retry_cnt < settings.get("get_data_retries", 1) - 1: continue else: pass + return None async def get_miner_model_hiveon(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "version") - try: - miner_type = sock_json_data["VERSION"][0]["Type"] - - return miner_type.replace(" HIVEON", "") - except (TypeError, LookupError): - pass + if sock_json_data is not None: + try: + miner_type = sock_json_data["VERSION"][0]["Type"] + return miner_type.replace(" HIVEON", "") + except (TypeError, LookupError): + pass + return None async def get_miner_model_luxos(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "version") - try: - miner_model = sock_json_data["VERSION"][0]["Type"] - - if " (" in miner_model: - split_miner_model = miner_model.split(" (") - miner_model = split_miner_model[0] - return miner_model - except (TypeError, LookupError): - pass + if sock_json_data is not None: + try: + miner_model = sock_json_data["VERSION"][0]["Type"] + if " (" in miner_model: + split_miner_model = miner_model.split(" (") + miner_model = split_miner_model[0] + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_auradine(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "devdetails") - try: - return sock_json_data["DEVDETAILS"][0]["Model"] - except LookupError: - pass + if sock_json_data is not None: + try: + return sock_json_data["DEVDETAILS"][0]["Model"] + except LookupError: + pass + return None async def get_miner_model_marathon(self, ip: str) -> str | None: auth = httpx.DigestAuth("root", "root") @@ -1407,38 +1449,41 @@ class MinerFactory: ip, "/kaonsu/v1/overview", auth=auth ) - try: - miner_model = web_json_data["model"] - if miner_model == "": - return None - - return miner_model - except (TypeError, LookupError): - pass + if web_json_data is not None: + try: + miner_model = web_json_data["model"] + if miner_model == "": + return None + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_bitaxe(self, ip: str) -> str | None: web_json_data = await self.send_web_command(ip, "/api/system/info") - try: - miner_model = web_json_data["ASICModel"] - if miner_model == "": - return None - - return miner_model - except (TypeError, LookupError): - pass + if web_json_data is not None: + try: + miner_model = web_json_data["ASICModel"] + if miner_model == "": + return None + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_luckyminer(self, ip: str) -> str | None: web_json_data = await self.send_web_command(ip, "/api/system/info") - try: - miner_model = web_json_data["minerModel"] - if miner_model == "": - return None - - return miner_model - except (TypeError, LookupError): - pass + if web_json_data is not None: + try: + miner_model = web_json_data["minerModel"] + if miner_model == "": + return None + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_iceriver(self, ip: str) -> str | None: async with httpx.AsyncClient(transport=settings.transport()) as client: @@ -1461,7 +1506,7 @@ class MinerFactory: f"http://{ip}:/user/userpanel", params={"post": "4"} ) if not resp.status_code == 200: - return + return None result = resp.json() software_ver = result["data"]["softver1"] split_ver = software_ver.split("_") @@ -1472,6 +1517,7 @@ class MinerFactory: return miner_ver.upper() except httpx.HTTPError: pass + return None async def get_miner_model_hammer(self, ip: str) -> str | None: auth = httpx.DigestAuth( @@ -1481,12 +1527,13 @@ class MinerFactory: ip, "/cgi-bin/get_system_info.cgi", auth=auth ) - try: - miner_model = web_json_data["minertype"] - - return miner_model - except (TypeError, LookupError): - pass + if web_json_data is not None: + try: + miner_model = web_json_data["minertype"] + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_volcminer(self, ip: str) -> str | None: auth = httpx.DigestAuth( @@ -1496,12 +1543,13 @@ class MinerFactory: ip, "/cgi-bin/get_system_info.cgi", auth=auth ) - try: - miner_model = web_json_data["minertype"] - - return miner_model - except (TypeError, LookupError): - pass + if web_json_data is not None: + try: + miner_model = web_json_data["minertype"] + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_elphapex(self, ip: str) -> str | None: auth = httpx.DigestAuth( @@ -1511,24 +1559,29 @@ class MinerFactory: ip, "/cgi-bin/get_system_info.cgi", auth=auth ) - try: - miner_model = web_json_data["minertype"] - - return miner_model - except (TypeError, LookupError): - pass + if web_json_data is not None: + try: + miner_model = web_json_data["minertype"] + return miner_model + except (TypeError, LookupError): + pass + return None async def get_miner_model_mskminer(self, ip: str) -> str | None: sock_json_data = await self.send_api_command(ip, "version") - try: - return sock_json_data["VERSION"][0]["Type"].split(" ")[0] - except LookupError: - pass + if sock_json_data is not None: + try: + return sock_json_data["VERSION"][0]["Type"].split(" ")[0] + except LookupError: + pass + return None miner_factory = MinerFactory() # abstracted version of get miner that is easier to access -async def get_miner(ip: ipaddress.ip_address | str) -> AnyMiner: - return await miner_factory.get_miner(ip) +async def get_miner( + ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address, +) -> AnyMiner | None: + return await miner_factory.get_miner(ip) # type: ignore[func-returns-value] diff --git a/pyasic/miners/goldshell/bfgminer/byte/byte.py b/pyasic/miners/goldshell/bfgminer/byte/byte.py index 14ac9992..e02b1a71 100644 --- a/pyasic/miners/goldshell/bfgminer/byte/byte.py +++ b/pyasic/miners/goldshell/bfgminer/byte/byte.py @@ -13,13 +13,12 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional, Union from pyasic.config import MinerConfig from pyasic.data import Fan, MinerData from pyasic.data.boards import HashBoard from pyasic.data.pools import PoolMetrics, PoolUrl -from pyasic.device.algorithm import AlgoHashRate, MinerAlgo +from pyasic.device.algorithm import AlgoHashRateType, MinerAlgo from pyasic.errors import APIError from pyasic.miners.backends import GoldshellMiner from pyasic.miners.data import ( @@ -94,8 +93,8 @@ class GoldshellByte(GoldshellMiner, Byte): async def get_data( self, allow_warning: bool = False, - include: List[Union[str, DataOptions]] = None, - exclude: List[Union[str, DataOptions]] = None, + include: list[str | DataOptions] | None = None, + exclude: list[str | DataOptions] | None = None, ) -> MinerData: if self.web_devs is None: try: @@ -106,7 +105,7 @@ class GoldshellByte(GoldshellMiner, Byte): scrypt_board_count = 0 zksnark_board_count = 0 - for minfo in self.web_devs.get("minfos", []): + for minfo in (self.web_devs or {}).get("minfos", []): algo_name = minfo.get("name") for _ in minfo.get("infos", []): @@ -123,9 +122,9 @@ class GoldshellByte(GoldshellMiner, Byte): ) if scrypt_board_count > 0 and zksnark_board_count == 0: - self.algo = MinerAlgo.SCRYPT + self.algo = MinerAlgo.SCRYPT # type: ignore[assignment] elif zksnark_board_count > 0 and scrypt_board_count == 0: - self.algo = MinerAlgo.ZKSNARK + self.algo = MinerAlgo.ZKSNARK # type: ignore[assignment] data = await super().get_data(allow_warning, include, exclude) data.expected_chips = self.expected_chips @@ -136,12 +135,12 @@ class GoldshellByte(GoldshellMiner, Byte): try: pools = await self.web.pools() except APIError: - return self.config + return self.config or MinerConfig() - self.config = MinerConfig.from_goldshell_byte(pools) + self.config = MinerConfig.from_goldshell_byte(pools.get("groups", [])) return self.config - async def _get_api_ver(self, web_setting: dict = None) -> Optional[str]: + async def _get_api_ver(self, web_setting: dict | None = None) -> str | None: if web_setting is None: try: web_setting = await self.web.setting() @@ -160,15 +159,15 @@ class GoldshellByte(GoldshellMiner, Byte): return self.api_ver async def _get_expected_hashrate( - self, rpc_devs: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_devs: dict | None = None + ) -> AlgoHashRateType | None: if rpc_devs is None: try: rpc_devs = await self.rpc.devs() except APIError: pass - total_hash_rate_mh = 0 + total_hash_rate_mh = 0.0 if rpc_devs is not None: for board in rpc_devs.get("DEVS", []): @@ -192,14 +191,16 @@ class GoldshellByte(GoldshellMiner, Byte): return hash_rate - async def _get_hashrate(self, rpc_devs: dict = None) -> Optional[AlgoHashRate]: + async def _get_hashrate( + self, rpc_devs: dict | None = None + ) -> AlgoHashRateType | None: if rpc_devs is None: try: rpc_devs = await self.rpc.devs() except APIError: pass - total_hash_rate_mh = 0 + total_hash_rate_mh = 0.0 if rpc_devs is not None: for board in rpc_devs.get("DEVS", []): @@ -211,7 +212,7 @@ class GoldshellByte(GoldshellMiner, Byte): return hash_rate - async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: + async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]: if rpc_pools is None: try: rpc_pools = await self.rpc.pools() @@ -240,8 +241,8 @@ class GoldshellByte(GoldshellMiner, Byte): return pools_data async def _get_hashboards( - self, rpc_devs: dict = None, rpc_devdetails: dict = None - ) -> List[HashBoard]: + self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None + ) -> list[HashBoard]: if rpc_devs is None: try: rpc_devs = await self.rpc.devs() @@ -285,7 +286,7 @@ class GoldshellByte(GoldshellMiner, Byte): return hashboards - async def _get_fans(self, rpc_devs: dict = None) -> List[Fan]: + async def _get_fans(self, rpc_devs: dict | None = None) -> list[Fan]: if self.expected_fans is None: return [] @@ -312,7 +313,7 @@ class GoldshellByte(GoldshellMiner, Byte): return fans - async def _get_uptime(self, web_devs: dict = None) -> Optional[int]: + async def _get_uptime(self, web_devs: dict | None = None) -> int | None: if web_devs is None: try: web_devs = await self.web.devs() @@ -321,7 +322,7 @@ class GoldshellByte(GoldshellMiner, Byte): if web_devs is not None: try: - for minfo in self.web_devs.get("minfos", []): + for minfo in (self.web_devs or {}).get("minfos", []): for info in minfo.get("infos", []): uptime = int(float(info["time"])) return uptime @@ -330,7 +331,7 @@ class GoldshellByte(GoldshellMiner, Byte): return None - async def _get_wattage(self, web_devs: dict = None) -> Optional[int]: + async def _get_wattage(self, web_devs: dict | None = None) -> int | None: if web_devs is None: try: web_devs = await self.web.devs() @@ -339,7 +340,7 @@ class GoldshellByte(GoldshellMiner, Byte): if web_devs is not None: try: - for minfo in self.web_devs.get("minfos", []): + for minfo in (self.web_devs or {}).get("minfos", []): for info in minfo.get("infos", []): wattage = int(float(info["power"])) return wattage diff --git a/pyasic/miners/goldshell/bfgminer/mini_doge/mini_doge.py b/pyasic/miners/goldshell/bfgminer/mini_doge/mini_doge.py index 86905fc0..575c2d69 100644 --- a/pyasic/miners/goldshell/bfgminer/mini_doge/mini_doge.py +++ b/pyasic/miners/goldshell/bfgminer/mini_doge/mini_doge.py @@ -13,11 +13,10 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import List, Optional from pyasic.config import MinerConfig from pyasic.data.boards import HashBoard -from pyasic.device.algorithm import AlgoHashRate +from pyasic.device.algorithm.hashrate.base import AlgoHashRateType from pyasic.errors import APIError from pyasic.logger import logger from pyasic.miners.backends import GoldshellMiner @@ -80,18 +79,19 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge): supports_shutdown = False supports_power_modes = False - async def get_config(self) -> MinerConfig: + async def get_config(self) -> MinerConfig | None: # type: ignore[override] try: pools = await self.web.pools() except APIError: - 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 async def _get_expected_hashrate( - self, rpc_devs: dict = None - ) -> Optional[AlgoHashRate]: + self, rpc_devs: dict | None = None + ) -> AlgoHashRateType | None: if rpc_devs is None: try: rpc_devs = await self.rpc.devs() @@ -110,8 +110,8 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge): return None async def _get_hashboards( - self, rpc_devs: dict = None, rpc_devdetails: dict = None - ) -> List[HashBoard]: + self, rpc_devs: dict | None = None, rpc_devdetails: dict | None = None + ) -> list[HashBoard]: if rpc_devs is None: try: rpc_devs = await self.rpc.devs() @@ -162,7 +162,7 @@ class GoldshellMiniDoge(GoldshellMiner, MiniDoge): return hashboards - async def _get_uptime(self, web_devs: dict = None) -> Optional[int]: + async def _get_uptime(self, web_devs: dict | None = None) -> int | None: if web_devs is None: try: web_devs = await self.web.devs() diff --git a/pyasic/miners/listener.py b/pyasic/miners/listener.py index ad147d09..56f8561d 100644 --- a/pyasic/miners/listener.py +++ b/pyasic/miners/listener.py @@ -55,7 +55,7 @@ class MinerListenerProtocol(asyncio.Protocol): class MinerListener: def __init__(self, bind_addr: str = "0.0.0.0"): - self.found_miners = [] + self.found_miners: list[dict[str, str]] = [] self.stop = asyncio.Event() self.bind_addr = bind_addr diff --git a/pyasic/network/__init__.py b/pyasic/network/__init__.py index e198972f..f832d44a 100644 --- a/pyasic/network/__init__.py +++ b/pyasic/network/__init__.py @@ -17,7 +17,8 @@ import asyncio import ipaddress import logging -from typing import AsyncIterator, List, Union +from collections.abc import AsyncIterator +from typing import cast from pyasic import settings from pyasic.miners.factory import AnyMiner, miner_factory @@ -30,24 +31,24 @@ class MinerNetwork: hosts: A list of `ipaddress.IPv4Address` to be used when scanning. """ - def __init__(self, hosts: List[ipaddress.IPv4Address]): + def __init__(self, hosts: list[ipaddress.IPv4Address]): self.hosts = hosts semaphore_limit = settings.get("network_scan_semaphore", 255) if semaphore_limit is None: semaphore_limit = 255 self.semaphore = asyncio.Semaphore(semaphore_limit) - def __len__(self): + def __len__(self) -> int: return len(self.hosts) @classmethod - def from_list(cls, addresses: list) -> "MinerNetwork": + def from_list(cls, addresses: list[str]) -> "MinerNetwork": """Parse a list of address constructors into a MinerNetwork. Parameters: addresses: A list of address constructors, such as `["10.1-2.1.1-50", "10.4.1-2.1-50"]`. """ - hosts = [] + hosts: list[ipaddress.IPv4Address] = [] for address in addresses: hosts = [*hosts, *cls.from_address(address).hosts] return cls(sorted(list(set(hosts)))) @@ -79,7 +80,7 @@ class MinerNetwork: oct_4: An octet constructor, such as `"1-50"`. """ - hosts = [] + hosts: list[ipaddress.IPv4Address] = [] oct_1_start, oct_1_end = compute_oct_range(oct_1) for oct_1_idx in range((abs(oct_1_end - oct_1_start)) + 1): @@ -97,11 +98,11 @@ class MinerNetwork: for oct_4_idx in range((abs(oct_4_end - oct_4_start)) + 1): oct_4_val = str(oct_4_idx + oct_4_start) - hosts.append( - ipaddress.ip_address( - ".".join([oct_1_val, oct_2_val, oct_3_val, oct_4_val]) - ) + ip_addr = ipaddress.ip_address( + ".".join([oct_1_val, oct_2_val, oct_3_val, oct_4_val]) ) + if isinstance(ip_addr, ipaddress.IPv4Address): + hosts.append(ip_addr) return cls(sorted(hosts)) @classmethod @@ -111,9 +112,13 @@ class MinerNetwork: Parameters: subnet: A subnet string, such as `"10.0.0.1/24"`. """ - return cls(list(ipaddress.ip_network(subnet, strict=False).hosts())) + network = ipaddress.ip_network(subnet, strict=False) + hosts = [ + host for host in network.hosts() if isinstance(host, ipaddress.IPv4Address) + ] + return cls(hosts) - async def scan(self) -> List[AnyMiner]: + async def scan(self) -> list[AnyMiner]: """Scan the network for miners. Returns: @@ -121,15 +126,17 @@ class MinerNetwork: """ return await self.scan_network_for_miners() - async def scan_network_for_miners(self) -> List[AnyMiner]: + async def scan_network_for_miners(self) -> list[AnyMiner]: logging.debug(f"{self} - (Scan Network For Miners) - Scanning") - miners = await asyncio.gather( + raw_miners: list[AnyMiner | None] = await asyncio.gather( *[self.ping_and_get_miner(host) for host in self.hosts] ) # remove all None from the miner list - miners = list(filter(None, miners)) + miners: list[AnyMiner] = cast( + list[AnyMiner], [miner for miner in raw_miners if miner is not None] + ) logging.debug( f"{self} - (Scan Network For Miners) - Found {len(miners)} miners" ) @@ -137,7 +144,7 @@ class MinerNetwork: # return the miner objects return miners - async def scan_network_generator(self) -> AsyncIterator[AnyMiner]: + async def scan_network_generator(self) -> AsyncIterator[AnyMiner | None]: """ Scan the network for miners using an async generator. @@ -145,39 +152,47 @@ class MinerNetwork: An asynchronous generator containing found miners. """ # create a list of scan tasks - miners = asyncio.as_completed( - [asyncio.create_task(self.ping_and_get_miner(host)) for host in self.hosts] - ) - for miner in miners: + tasks: list[asyncio.Task[AnyMiner | None]] = [ + asyncio.create_task(self.ping_and_get_miner(host)) for host in self.hosts + ] + for miner in asyncio.as_completed(tasks): try: - yield await miner + result = await miner + yield result except TimeoutError: yield None + return async def ping_and_get_miner( - self, ip: ipaddress.ip_address - ) -> Union[None, AnyMiner]: + self, ip: ipaddress.IPv4Address | ipaddress.IPv6Address + ) -> AnyMiner | None: if settings.get("network_scan_semaphore") is None: - return await self._ping_and_get_miner(ip) + return await self._ping_and_get_miner(ip) # type: ignore[func-returns-value] async with self.semaphore: - return await self._ping_and_get_miner(ip) + return await self._ping_and_get_miner(ip) # type: ignore[func-returns-value] @staticmethod - async def _ping_and_get_miner(ip: ipaddress.ip_address) -> Union[None, AnyMiner]: + async def _ping_and_get_miner( + ip: ipaddress.IPv4Address | ipaddress.IPv6Address, + ) -> AnyMiner | None: try: - return await ping_and_get_miner(ip) + return await ping_and_get_miner(ip) # type: ignore[func-returns-value] except ConnectionRefusedError: - tasks = [ping_and_get_miner(ip, port=port) for port in [4028, 4029, 8889]] + tasks: list[asyncio.Task[AnyMiner | None]] = [ + asyncio.create_task(ping_and_get_miner(ip, port=port)) + for port in [4028, 4029, 8889] + ] for miner in asyncio.as_completed(tasks): try: return await miner except ConnectionRefusedError: pass + return None async def ping_and_get_miner( - ip: ipaddress.ip_address, port=80 -) -> Union[None, AnyMiner]: + ip: ipaddress.IPv4Address | ipaddress.IPv6Address, port: int = 80 +) -> AnyMiner | None: for _ in range(settings.get("network_ping_retries", 1)): try: connection_fut = asyncio.open_connection(str(ip), port) @@ -190,7 +205,7 @@ async def ping_and_get_miner( # make sure the writer is closed await writer.wait_closed() # ping was successful - return await miner_factory.get_miner(ip) + return await miner_factory.get_miner(ip) # type: ignore[func-returns-value] except asyncio.exceptions.TimeoutError: # ping failed if we time out continue @@ -198,11 +213,11 @@ async def ping_and_get_miner( raise ConnectionRefusedError from e except Exception as e: logging.warning(f"{str(ip)}: Unhandled ping exception: {e}") - return - return + return None + return None -def compute_oct_range(octet: str) -> tuple: +def compute_oct_range(octet: str) -> tuple[int, int]: octet_split = octet.split("-") octet_start = int(octet_split[0]) octet_end = None diff --git a/pyasic/rpc/base.py b/pyasic/rpc/base.py index 060e2061..eff331b2 100644 --- a/pyasic/rpc/base.py +++ b/pyasic/rpc/base.py @@ -20,7 +20,6 @@ import json import logging import re import warnings -from typing import Union from pyasic.errors import APIError, APIWarning from pyasic.misc import validate_command_output @@ -35,7 +34,7 @@ class BaseMinerRPCAPI: # api version if known self.api_ver = api_ver - self.pwd = None + self.pwd: str | None = None def __new__(cls, *args, **kwargs): if cls is BaseMinerRPCAPI: @@ -47,8 +46,8 @@ class BaseMinerRPCAPI: async def send_command( self, - command: Union[str, bytes], - parameters: Union[str, int, bool] = None, + command: str, + parameters: str | int | bool | None = None, ignore_errors: bool = False, allow_warning: bool = True, **kwargs, @@ -86,10 +85,10 @@ class BaseMinerRPCAPI: raise APIError(data.decode("utf-8")) return {} - data = self._load_api_data(data) + api_data = self._load_api_data(data) # check for if the user wants to allow errors to return - validation = validate_command_output(data) + validation = validate_command_output(api_data) if not validation[0]: if not ignore_errors: # validate the command succeeded @@ -100,7 +99,7 @@ class BaseMinerRPCAPI: ) logging.debug(f"{self} - (Send Command) - Received data.") - return data + return api_data # Privileged command handler, only used by whatsminers, defined here for consistency. async def send_privileged_command(self, *args, **kwargs) -> dict: @@ -115,10 +114,10 @@ class BaseMinerRPCAPI: """ # make sure we can actually run each command, otherwise they will fail - commands = self._check_commands(*commands) + valid_commands = self._check_commands(*commands) # standard multicommand format is "command1+command2" # doesn't work for S19 which uses the backup _send_split_multicommand - command = "+".join(commands) + command = "+".join(valid_commands) try: data = await self.send_command(command, allow_warning=allow_warning) except APIError: @@ -164,10 +163,13 @@ class BaseMinerRPCAPI: for func in # each function in self dir(self) - if not func in ["commands", "open_api"] - if callable(getattr(self, func)) and + if func not in ["commands", "open_api"] + if callable(getattr(self, func)) + and # no __ or _ methods - not func.startswith("__") and not func.startswith("_") and + not func.startswith("__") + and not func.startswith("_") + and # remove all functions that are in this base class func not in [ @@ -196,7 +198,7 @@ If you are sure you want to use this command please use API.send_command("{comma self, data: bytes, *, - port: int = None, + port: int | None = None, timeout: int = 100, ) -> bytes: if port is None: diff --git a/pyasic/rpc/bfgminer.py b/pyasic/rpc/bfgminer.py index 21f4eac1..366b822f 100644 --- a/pyasic/rpc/bfgminer.py +++ b/pyasic/rpc/bfgminer.py @@ -151,7 +151,7 @@ class BFGMinerRPCAPI(CGMinerRPCAPI): """ return await self.send_command("procidentify", parameters=n) - async def procset(self, n: int, opt: str, val: int = None) -> dict: + async def procset(self, n: int, opt: str, val: int | None = None) -> dict: """Set processor option opt to val on processor n.
Expand diff --git a/pyasic/rpc/btminer.py b/pyasic/rpc/btminer.py index 4cd55ef6..1dd9365d 100644 --- a/pyasic/rpc/btminer.py +++ b/pyasic/rpc/btminer.py @@ -23,13 +23,15 @@ import json import logging import re import struct +import typing import warnings -from asyncio import Future, StreamReader, StreamWriter -from typing import Any, AsyncGenerator, Callable, Literal, Union +from collections.abc import AsyncGenerator +from typing import Any, Literal import httpx from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from passlib.handlers.md5_crypt import md5_crypt +from pydantic import BaseModel, Field from pyasic import settings from pyasic.errors import APIError, APIWarning @@ -44,11 +46,54 @@ from pyasic.rpc.base import BaseMinerRPCAPI # you change the password, you can pass that to this class as pwd, # or add it as the Whatsminer_pwd in the settings.toml file. -PrePowerOnMessage = Union[ - Literal["wait for adjust temp"], - Literal["adjust complete"], - Literal["adjust continue"], -] + +class TokenResponse(BaseModel): + salt: str + time: str + newsalt: str + + class Config: + extra = "allow" + + +class TokenData(BaseModel): + host_sign: str + host_passwd_md5: str + timestamp: datetime.datetime = Field(default_factory=datetime.datetime.now) + + +class BTMinerPrivilegedCommand(BaseModel): + cmd: str + token: str + + class Config: + extra = "allow" + + +class BTMinerV3Command(BaseModel): + cmd: str + param: Any | None = None + + class Config: + extra = "forbid" + + +class BTMinerV3PrivilegedCommand(BaseModel): + cmd: str + param: Any | None = None + ts: int + account: str + token: str + + class Config: + extra = "forbid" + + +PrePowerOnMessage = ( + Literal["wait for adjust temp"] + | Literal["adjust complete"] + | Literal["adjust continue"] +) def _crypt(word: str, salt: str) -> str: @@ -93,7 +138,7 @@ def _add_to_16(string: str) -> bytes: return str.encode(string) # return bytes -def parse_btminer_priviledge_data(token_data: dict, data: dict) -> dict: +def parse_btminer_priviledge_data(token_data: TokenData, data: dict) -> dict: """Parses data returned from the BTMiner privileged API. Parses data from the BTMiner privileged API using the token @@ -109,9 +154,9 @@ def parse_btminer_priviledge_data(token_data: dict, data: dict) -> dict: # get the encoded data from the dict enc_data = data["enc"] # get the aes key from the token data - aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest() + aeskey_hex = hashlib.sha256(token_data.host_passwd_md5.encode()).hexdigest() # unhexlify the aes key - aeskey = binascii.unhexlify(aeskey.encode()) + aeskey = binascii.unhexlify(aeskey_hex.encode()) # create the required decryptor aes = Cipher(algorithms.AES(aeskey), modes.ECB()) decryptor = aes.decryptor() @@ -124,7 +169,7 @@ def parse_btminer_priviledge_data(token_data: dict, data: dict) -> dict: return ret_msg -def create_privileged_cmd(token_data: dict, command: dict) -> bytes: +def create_privileged_cmd(token_data: TokenData, command: dict) -> bytes: """Create a privileged command to send to the BTMiner API. Creates a privileged command using the token from the API and the @@ -138,13 +183,13 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes: Returns: The encrypted privileged command to be sent to the miner. """ - logging.debug(f"(Create Prilileged Command) - Creating Privileged Command") + logging.debug("(Create Prilileged Command) - Creating Privileged Command") # add token to command - command["token"] = token_data["host_sign"] + command["token"] = token_data.host_sign # encode host_passwd data and get hexdigest - aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest() + aeskey_hex = hashlib.sha256(token_data.host_passwd_md5.encode()).hexdigest() # unhexlify the encoded host_passwd - aeskey = binascii.unhexlify(aeskey.encode()) + aeskey = binascii.unhexlify(aeskey_hex.encode()) # create a new AES key aes = Cipher(algorithms.AES(aeskey), modes.ECB()) encryptor = aes.encryptor() @@ -188,8 +233,8 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): def __init__(self, ip: str, port: int = 4028, api_ver: str = "0.0.0") -> None: super().__init__(ip, port, api_ver) - self.pwd = settings.get("default_whatsminer_rpc_password", "admin") - self.token = None + self.pwd: str = settings.get("default_whatsminer_rpc_password", "admin") + self.token: TokenData | None = None async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict: """Creates and sends multiple commands as one command to the miner. @@ -199,19 +244,19 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): allow_warning: A boolean to supress APIWarnings. """ # make sure we can actually run each command, otherwise they will fail - commands = self._check_commands(*commands) + commands_list = self._check_commands(*commands) # standard multicommand format is "command1+command2" # commands starting with "get_" and the "status" command aren't supported, but we can fake that split_commands = [] - for command in list(commands): + for command in commands_list: if command.startswith("get_") or command == "status": - commands.remove(command) + commands_list.remove(command) # send seperately and append later split_commands.append(command) - command = "+".join(commands) + command = "+".join(commands_list) tasks = [] if len(split_commands) > 0: @@ -240,7 +285,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): async def send_privileged_command( self, - command: Union[str, bytes], + command: str, ignore_errors: bool = False, timeout: int = 10, **kwargs, @@ -252,6 +297,8 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): except APIError as e: if not e.message == "can't access write cmd": raise + # If we get here, we caught the specific error but didn't handle it + raise # try: # await self.open_api() # except Exception as e: @@ -262,7 +309,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): async def _send_privileged_command( self, - command: Union[str, bytes], + command: str, ignore_errors: bool = False, timeout: int = 10, **kwargs, @@ -272,10 +319,10 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): if len(kwargs) > 0 else "" ) - command = {"cmd": command, **kwargs} + cmd = {"cmd": command, **kwargs} token_data = await self.get_token() - enc_command = create_privileged_cmd(token_data, command) + enc_command = create_privileged_cmd(token_data, cmd) logging.debug(f"{self} - (Send Privileged Command) - Sending") try: @@ -289,24 +336,23 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): if ignore_errors: return {} raise APIError("No data was returned from the API.") - data = self._load_api_data(data) + data_dict: dict[Any, Any] = self._load_api_data(data) try: - data = parse_btminer_priviledge_data(self.token, data) - print(data) + data_dict = parse_btminer_priviledge_data(token_data, data_dict) except Exception as e: logging.info(f"{str(self.ip)}: {e}") if not ignore_errors: # if it fails to validate, it is likely an error - validation = validate_command_output(data) + validation = validate_command_output(data_dict) if not validation[0]: raise APIError(validation[1]) # return the parsed json as a dict - return data + return data_dict - async def get_token(self) -> dict: + async def get_token(self) -> TokenData: """Gets token information from the API.
Expand @@ -317,7 +363,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): """ logging.debug(f"{self} - (Get Token) - Getting token") if self.token: - if self.token["timestamp"] > datetime.datetime.now() - datetime.timedelta( + if self.token.timestamp > datetime.datetime.now() - datetime.timedelta( minutes=30 ): return self.token @@ -325,26 +371,30 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): # get the token data = await self.send_command("get_token") + token_response = TokenResponse.model_validate(data["Msg"]) + # encrypt the admin password with the salt - pwd = _crypt(self.pwd, "$1$" + data["Msg"]["salt"] + "$") - pwd = pwd.split("$") + pwd_str = _crypt(self.pwd, "$1$" + token_response.salt + "$") + pwd_parts = pwd_str.split("$") # take the 4th item from the pwd split - host_passwd_md5 = pwd[3] + host_passwd_md5 = pwd_parts[3] # encrypt the pwd with the time and new salt - tmp = _crypt(pwd[3] + data["Msg"]["time"], "$1$" + data["Msg"]["newsalt"] + "$") - tmp = tmp.split("$") + tmp_str = _crypt( + pwd_parts[3] + token_response.time, "$1$" + token_response.newsalt + "$" + ) + tmp_parts = tmp_str.split("$") # take the 4th item from the encrypted pwd split - host_sign = tmp[3] + host_sign = tmp_parts[3] # set the current token - self.token = { - "host_sign": host_sign, - "host_passwd_md5": host_passwd_md5, - "timestamp": datetime.datetime.now(), - } + self.token = TokenData( + host_sign=host_sign, + host_passwd_md5=host_passwd_md5, + timestamp=datetime.datetime.now(), + ) logging.debug(f"{self} - (Get Token) - Gathered token data: {self.token}") return self.token @@ -388,12 +438,12 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): pool_1: str, worker_1: str, passwd_1: str, - pool_2: str = None, - worker_2: str = None, - passwd_2: str = None, - pool_3: str = None, - worker_3: str = None, - passwd_3: str = None, + pool_2: str | None = None, + worker_2: str | None = None, + passwd_2: str | None = None, + pool_3: str | None = None, + worker_3: str | None = None, + passwd_3: str | None = None, ) -> dict: """Update the pools of the miner using the API.
@@ -650,11 +700,11 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): async def net_config( self, - ip: str = None, - mask: str = None, - gate: str = None, - dns: str = None, - host: str = None, + ip: str | None = None, + mask: str | None = None, + gate: str | None = None, + dns: str | None = None, + host: str | None = None, dhcp: bool = True, ): if dhcp: @@ -683,9 +733,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): """ if not -100 < percent < 100: raise APIError( - f"Frequency % is outside of the allowed " - f"range. Please set a % between -100 and " - f"100" + "Frequency % is outside of the allowed " + "range. Please set a % between -100 and " + "100" ) return await self.send_privileged_command( "set_target_freq", percent=str(percent) @@ -786,9 +836,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): if not 0 < percent < 100: raise APIError( - f"Power PCT % is outside of the allowed " - f"range. Please set a % between 0 and " - f"100" + "Power PCT % is outside of the allowed " + "range. Please set a % between 0 and " + "100" ) return await self.send_privileged_command("set_power_pct", percent=str(percent)) @@ -846,9 +896,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): if not 0 < percent < 100: raise APIError( - f"Power PCT % is outside of the allowed " - f"range. Please set a % between 0 and " - f"100" + "Power PCT % is outside of the allowed " + "range. Please set a % between 0 and " + "100" ) return await self.send_privileged_command( "set_power_pct_v2", percent=str(percent) @@ -873,9 +923,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): """ if not -30 < temp_offset < 0: raise APIError( - f"Temp offset is outside of the allowed " - f"range. Please set a number between -30 and " - f"0." + "Temp offset is outside of the allowed " + "range. Please set a number between -30 and " + "0." ) return await self.send_privileged_command( @@ -924,9 +974,9 @@ class BTMinerRPCAPI(BaseMinerRPCAPI): """ if not 0 < upfreq_speed < 9: raise APIError( - f"Upfreq speed is outside of the allowed " - f"range. Please set a number between 0 (Normal) and " - f"9 (Fastest)." + "Upfreq speed is outside of the allowed " + "range. Please set a number between 0 (Normal) and " + "9 (Fastest)." ) return await self.send_privileged_command( "adjust_upfreq_speed", upfreq_speed=upfreq_speed @@ -1109,8 +1159,8 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI): def __init__(self, ip: str, port: int = 4433, api_ver: str = "0.0.0"): super().__init__(ip, port, api_ver=api_ver) - self.salt = None - self.pwd = "super" + self.salt: str | None = None + self.pwd: str = "super" async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict: """Creates and sends multiple commands as one command to the miner. @@ -1120,47 +1170,52 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI): allow_warning: A boolean to supress APIWarnings. """ - commands = self._check_commands(*commands) - data = await self._send_split_multicommand(*commands) + checked_commands = self._check_commands(*commands) + data = await self._send_split_multicommand(*checked_commands) data["multicommand"] = True return data async def send_command( - self, command: str, parameters: Any = None, **kwargs + self, + command: str, + parameters: Any = None, + ignore_errors: bool = False, + allow_warning: bool = True, + **kwargs, ) -> dict: if ":" in command: parameters = command.split(":")[1] command = command.split(":")[0] - cmd = {"cmd": command} - if parameters is not None: - cmd["param"] = parameters + + cmd: BTMinerV3Command | BTMinerV3PrivilegedCommand if command.startswith("set."): salt = await self.get_salt() ts = int(datetime.datetime.now().timestamp()) - cmd["ts"] = ts - token_str = cmd["cmd"] + self.pwd + salt + str(ts) + token_str = command + self.pwd + salt + str(ts) token_hashed = bytearray( base64.b64encode(hashlib.sha256(token_str.encode("utf-8")).digest()) ) b_arr = bytearray(token_hashed) b_arr[8] = 0 str_token = b_arr.split(b"\x00")[0].decode("utf-8") - cmd["account"] = "super" - cmd["token"] = str_token - # send the command - ser = json.dumps(cmd).encode("utf-8") + cmd = BTMinerV3PrivilegedCommand( + cmd=command, param=parameters, ts=ts, account="super", token=str_token + ) + else: + cmd = BTMinerV3Command(cmd=command, param=parameters) + + cmd_dict = cmd.model_dump() + ser = json.dumps(cmd_dict).encode("utf-8") header = struct.pack(" bytes: 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"] return self.salt + @typing.no_type_check async def get_miner_report(self) -> AsyncGenerator[dict, None]: if self.writer is None: await self.connect() diff --git a/pyasic/rpc/cgminer.py b/pyasic/rpc/cgminer.py index 28cddd30..d88a9557 100644 --- a/pyasic/rpc/cgminer.py +++ b/pyasic/rpc/cgminer.py @@ -243,7 +243,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI): """ return await self.send_command("removepool", parameters=n) - async def save(self, filename: str = None) -> dict: + async def save(self, filename: str | None = None) -> dict: """Save the config.
Expand @@ -484,7 +484,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI): """ return await self.send_command("usbstats") - async def pgaset(self, n: int, opt: str, val: int = None) -> dict: + async def pgaset(self, n: int, opt: str, val: int | None = None) -> dict: """Set PGA option opt to val on PGA n.
Expand @@ -611,7 +611,7 @@ class CGMinerRPCAPI(BaseMinerRPCAPI): """ return await self.send_command("asccount") - async def ascset(self, n: int, opt: str, val: int = None) -> dict: + async def ascset(self, n: int, opt: str, val: int | str | None = None) -> dict: """Set ASC n option opt to value val.
Expand diff --git a/pyasic/rpc/luxminer.py b/pyasic/rpc/luxminer.py index 85e32d44..0a1014c8 100644 --- a/pyasic/rpc/luxminer.py +++ b/pyasic/rpc/luxminer.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from typing import Literal, Optional, Union +from typing import Literal from pyasic import APIError from pyasic.rpc.base import BaseMinerRPCAPI @@ -37,9 +37,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): super().__init__(*args, **kwargs) self.session_token = None - async def send_privileged_command( - self, command: Union[str, bytes], *args, **kwargs - ) -> dict: + async def send_privileged_command(self, command: str, *args, **kwargs) -> dict: if self.session_token is None: await self.auth() return await self.send_command( @@ -51,7 +49,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): async def send_command( self, - command: Union[str, bytes], + command: str, *args, **kwargs, ) -> dict: @@ -59,7 +57,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): return await super().send_command(command, **kwargs) return await super().send_command(command, parameters=",".join(args), **kwargs) - async def auth(self) -> Optional[str]: + async def auth(self) -> str | None: try: data = await self.session() if not data["SESSION"][0]["SessionID"] == "": @@ -74,6 +72,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): return self.session_token except (LookupError, APIError): pass + return None async def addgroup(self, name: str, quota: int) -> dict: """Add a pool group. @@ -91,7 +90,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): return await self.send_command("addgroup", name, quota) async def addpool( - self, url: str, user: str, pwd: str = "", group_id: str = None + self, url: str, user: str, pwd: str = "", group_id: str | None = None ) -> dict: """Add a pool.
@@ -163,13 +162,13 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): async def atmset( self, - enabled: bool = None, - startup_minutes: int = None, - post_ramp_minutes: int = None, - temp_window: int = None, - min_profile: str = None, - max_profile: str = None, - prevent_oc: bool = None, + enabled: bool | None = None, + startup_minutes: int | None = None, + post_ramp_minutes: int | None = None, + temp_window: int | None = None, + min_profile: str | None = None, + max_profile: str | None = None, + prevent_oc: bool | None = None, ) -> dict: """Sets the ATM configuration.
@@ -357,7 +356,10 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): return await self.send_command("fans") async def fanset( - self, speed: int = None, min_fans: int = None, power_off_speed: int = None + self, + speed: int | None = None, + min_fans: int | None = None, + power_off_speed: int | None = None, ) -> dict: """Set fan control.
@@ -380,7 +382,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): fanset_data.append(f"power_off_speed={power_off_speed}") return await self.send_privileged_command("fanset", *fanset_data) - async def frequencyget(self, board_n: int, chip_n: int = None) -> dict: + async def frequencyget(self, board_n: int, chip_n: int | None = None) -> dict: """Get frequency data for a board and chips.
Expand @@ -453,7 +455,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): """ return await self.send_command("groups") - async def healthchipget(self, board_n: int, chip_n: int = None) -> dict: + async def healthchipget(self, board_n: int, chip_n: int | None = None) -> dict: """Get chip health.
Expand @@ -471,7 +473,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): healthchipget_data.append(str(chip_n)) return await self.send_command("healthchipget", *healthchipget_data) - async def healthchipset(self, board_n: int, chip_n: int = None) -> dict: + async def healthchipset(self, board_n: int, chip_n: int | None = None) -> dict: """Select the next chip to have its health checked.
Expand @@ -641,7 +643,7 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): """ return await self.send_privileged_command("profileset", profile) - async def reboot(self, board_n: int, delay_s: int = None) -> dict: + async def reboot(self, board_n: int, delay_s: int | None = None) -> dict: """Reboot a board.
Expand @@ -721,7 +723,10 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI): return await self.send_command("session") async def tempctrlset( - self, target: int = None, hot: int = None, dangerous: int = None + self, + target: int | None = None, + hot: int | None = None, + dangerous: int | None = None, ) -> dict: """Set temp control values.
diff --git a/pyasic/settings/__init__.py b/pyasic/settings/__init__.py index aef0339d..cdbb3897 100644 --- a/pyasic/settings/__init__.py +++ b/pyasic/settings/__init__.py @@ -14,37 +14,45 @@ # limitations under the License. - # ------------------------------------------------------------------------------ from ssl import SSLContext -from typing import Any, Union +from typing import Any import httpx from httpx import AsyncHTTPTransport +from pydantic import BaseModel, Field -_settings = { # defaults - "network_ping_retries": 1, - "network_ping_timeout": 3, - "network_scan_semaphore": None, - "factory_get_retries": 1, - "factory_get_timeout": 3, - "get_data_retries": 1, - "api_function_timeout": 5, - "antminer_mining_mode_as_str": False, - "default_whatsminer_rpc_password": "admin", - "default_innosilicon_web_password": "admin", - "default_antminer_web_password": "root", - "default_hammer_web_password": "root", - "default_volcminer_web_password": "ltc@dog", - "default_bosminer_web_password": "root", - "default_vnish_web_password": "admin", - "default_goldshell_web_password": "123456789", - "default_auradine_web_password": "admin", - "default_epic_web_password": "letmein", - "default_hive_web_password": "root", - "default_iceriver_web_password": "12345678", - "default_elphapex_web_password": "root", - "default_mskminer_web_password": "root", - "default_antminer_ssh_password": "miner", - "default_bosminer_ssh_password": "root", -} + +class Settings(BaseModel): + network_ping_retries: int = Field(default=1) + network_ping_timeout: int = Field(default=3) + network_scan_semaphore: int | None = Field(default=None) + factory_get_retries: int = Field(default=1) + factory_get_timeout: int = Field(default=3) + get_data_retries: int = Field(default=1) + api_function_timeout: int = Field(default=5) + antminer_mining_mode_as_str: bool = Field(default=False) + default_whatsminer_rpc_password: str = Field(default="admin") + default_innosilicon_web_password: str = Field(default="admin") + default_antminer_web_password: str = Field(default="root") + default_hammer_web_password: str = Field(default="root") + default_volcminer_web_password: str = Field(default="ltc@dog") + default_bosminer_web_password: str = Field(default="root") + default_vnish_web_password: str = Field(default="admin") + default_goldshell_web_password: str = Field(default="123456789") + default_auradine_web_password: str = Field(default="admin") + default_epic_web_password: str = Field(default="letmein") + default_hive_web_password: str = Field(default="root") + default_iceriver_web_password: str = Field(default="12345678") + default_elphapex_web_password: str = Field(default="root") + default_mskminer_web_password: str = Field(default="root") + default_antminer_ssh_password: str = Field(default="miner") + default_bosminer_ssh_password: str = Field(default="root") + + class Config: + validate_assignment = True + extra = "allow" + + +_settings = Settings() ssl_cxt = httpx.create_ssl_context() @@ -52,13 +60,21 @@ ssl_cxt = httpx.create_ssl_context() # this function returns an AsyncHTTPTransport instance to perform asynchronous HTTP requests # using those options. -def transport(verify: Union[str, bool, SSLContext] = ssl_cxt): +def transport(verify: str | bool | SSLContext = ssl_cxt): return AsyncHTTPTransport(verify=verify) def get(key: str, other: Any = None) -> Any: - return _settings.get(key, other) + try: + return getattr(_settings, key) + except AttributeError: + if hasattr(_settings, "__dict__") and key in _settings.__dict__: + return _settings.__dict__[key] + return other -def update(key: str, val: Any) -> Any: - _settings[key] = val +def update(key: str, val: Any) -> None: + if hasattr(_settings, key): + setattr(_settings, key, val) + else: + _settings.__dict__[key] = val diff --git a/pyasic/ssh/base.py b/pyasic/ssh/base.py index be75220c..25a95ff7 100644 --- a/pyasic/ssh/base.py +++ b/pyasic/ssh/base.py @@ -1,6 +1,5 @@ import asyncio import logging -from typing import Optional import asyncssh @@ -8,9 +7,9 @@ import asyncssh class BaseSSH: def __init__(self, ip: str) -> None: self.ip = ip - self.pwd = None - self.username = "root" - self.port = 22 + self.pwd: str | None = None + self.username: str = "root" + self.port: int = 22 async def _get_connection(self) -> asyncssh.connect: """Create a new asyncssh connection""" @@ -29,7 +28,7 @@ class BaseSSH: except Exception as e: raise ConnectionError from e - async def send_command(self, cmd: str) -> Optional[str]: + async def send_command(self, cmd: str) -> str | None: """Send an ssh command to the miner""" try: conn = await asyncio.wait_for(self._get_connection(), timeout=10) diff --git a/pyasic/web/antminer.py b/pyasic/web/antminer.py index b5118914..23d21a2f 100644 --- a/pyasic/web/antminer.py +++ b/pyasic/web/antminer.py @@ -24,6 +24,7 @@ import aiofiles import httpx from pyasic import settings +from pyasic.errors import APIError from pyasic.web.base import BaseWebAPI @@ -35,12 +36,12 @@ class AntminerModernWebAPI(BaseWebAPI): ip (str): IP address of the Antminer device. """ super().__init__(ip) - self.username = "root" - self.pwd = settings.get("default_antminer_web_password", "root") + self.username: str = "root" + self.pwd: str = settings.get("default_antminer_web_password", "root") async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -49,7 +50,7 @@ class AntminerModernWebAPI(BaseWebAPI): """Send a command to the Antminer device using HTTP digest authentication. Args: - command (str | bytes): The CGI command to send. + command (str): The CGI command to send. ignore_errors (bool): If True, ignore any HTTP errors. allow_warning (bool): If True, proceed with warnings. privileged (bool): If set to True, requires elevated privileges. @@ -249,12 +250,12 @@ class AntminerOldWebAPI(BaseWebAPI): ip (str): IP address of the Antminer device. """ super().__init__(ip) - self.username = "root" - self.pwd = settings.get("default_antminer_web_password", "root") + self.username: str = "root" + self.pwd: str = settings.get("default_antminer_web_password", "root") async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -263,7 +264,7 @@ class AntminerOldWebAPI(BaseWebAPI): """Send a command to the Antminer device using HTTP digest authentication. Args: - command (str | bytes): The CGI command to send. + command (str): The CGI command to send. ignore_errors (bool): If True, ignore any HTTP errors. allow_warning (bool): If True, proceed with warnings. privileged (bool): If set to True, requires elevated privileges. @@ -293,6 +294,7 @@ class AntminerOldWebAPI(BaseWebAPI): return data.json() except json.decoder.JSONDecodeError: pass + raise APIError(f"Failed to send command to miner: {self}") async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True @@ -411,10 +413,9 @@ class AntminerOldWebAPI(BaseWebAPI): async with aiofiles.open(file, "rb") as firmware: file_content = await firmware.read() - parameters = { - "file": (file.name, file_content, "application/octet-stream"), - "filename": file.name, - "keep_settings": keep_settings, - } - - return await self.send_command(command="upgrade", **parameters) + return await self.send_command( + command="upgrade", + file=(file.name, file_content, "application/octet-stream"), + filename=file.name, + keep_settings=keep_settings, + ) diff --git a/pyasic/web/auradine.py b/pyasic/web/auradine.py index faa73cf7..75375443 100644 --- a/pyasic/web/auradine.py +++ b/pyasic/web/auradine.py @@ -69,7 +69,7 @@ class AuradineWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -78,7 +78,7 @@ class AuradineWebAPI(BaseWebAPI): """Send a command to the Auradine miner, handling authentication and retries. Args: - command (str | bytes): The specific command to execute. + command (str): The specific command to execute. ignore_errors (bool): Whether to ignore HTTP errors. allow_warning (bool): Whether to proceed with warnings. privileged (bool): Whether the command requires privileged access. @@ -95,6 +95,10 @@ class AuradineWebAPI(BaseWebAPI): await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: for i in range(settings.get("get_data_retries", 1)): + if self.token is None: + raise APIError( + f"Could not authenticate web token with miner: {self}" + ) try: if post: response = await client.post( @@ -120,6 +124,7 @@ class AuradineWebAPI(BaseWebAPI): return json_data except (httpx.HTTPError, json.JSONDecodeError): pass + raise APIError(f"Failed to send command to miner: {self}") async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True @@ -145,11 +150,9 @@ class AuradineWebAPI(BaseWebAPI): *[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): - if not isinstance(result, (APIError, Exception)): - if result is None or result == {}: - result = {} + if isinstance(result, dict): data[cmd] = result return data @@ -182,7 +185,9 @@ class AuradineWebAPI(BaseWebAPI): """ return await self.send_command("fan", index=fan, percentage=speed_pct) - async def firmware_upgrade(self, url: str = None, version: str = "latest") -> dict: + async def firmware_upgrade( + self, url: str | None = None, version: str = "latest" + ) -> dict: """Upgrade the firmware of the Auradine miner. Args: diff --git a/pyasic/web/avalonminer.py b/pyasic/web/avalonminer.py index 502b6db3..6d0c46b4 100644 --- a/pyasic/web/avalonminer.py +++ b/pyasic/web/avalonminer.py @@ -34,20 +34,21 @@ class AvalonMinerWebAPI(BaseWebAPI): ip (str): IP address of the Avalonminer device. """ super().__init__(ip) - self.username = "root" - self.pwd = settings.get("default_avalonminer_web_password", "root") + self.username: str = "root" + self.pwd: str = settings.get("default_avalonminer_web_password", "root") async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, + privileged: bool = False, **parameters: Any, ) -> dict: """Send a command to the Avalonminer device using HTTP digest authentication. Args: - command (str | bytes): The CGI command to send. + command (str): The CGI command to send. ignore_errors (bool): If True, ignore any HTTP errors. allow_warning (bool): If True, proceed with warnings. **parameters: Arbitrary keyword arguments to be sent as parameters in the request. diff --git a/pyasic/web/base.py b/pyasic/web/base.py index 6bdd3538..fabb9380 100644 --- a/pyasic/web/base.py +++ b/pyasic/web/base.py @@ -26,24 +26,24 @@ class BaseWebAPI(ABC): def __init__(self, ip: str) -> None: # ip address of the miner self.ip = ip - self.username = None - self.pwd = None - self.port = 80 + self.username: str | None = None + self.pwd: str | None = None + self.port: int = 80 - self.token = None + self.token: str | None = None - def __new__(cls, *args, **kwargs): + def __new__(cls, *args: Any, **kwargs: Any) -> BaseWebAPI: if cls is BaseWebAPI: raise TypeError(f"Only children of '{cls.__name__}' may be instantiated") return object.__new__(cls) - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}: {str(self.ip)}" @abstractmethod async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -57,7 +57,7 @@ class BaseWebAPI(ABC): ) -> dict: pass - def _check_commands(self, *commands): + def _check_commands(self, *commands: str) -> list[str]: allowed_commands = self.get_commands() return_commands = [] for command in [*commands]: @@ -72,10 +72,10 @@ If you are sure you want to use this command please use WebAPI.send_command("{co return return_commands @property - def commands(self) -> list: + def commands(self) -> list[str]: return self.get_commands() - def get_commands(self) -> list: + def get_commands(self) -> list[str]: """Get a list of command accessible to a specific type of web API on the miner. Returns: @@ -87,9 +87,12 @@ If you are sure you want to use this command please use WebAPI.send_command("{co # each function in self dir(self) if not func == "commands" - if callable(getattr(self, func)) and + if callable(getattr(self, func)) + and # no __ or _ methods - not func.startswith("__") and not func.startswith("_") and + not func.startswith("__") + and not func.startswith("_") + and # remove all functions that are in this base class func not in [ diff --git a/pyasic/web/braiins_os/better_monkey.py b/pyasic/web/braiins_os/better_monkey.py index e1f63050..45a2ec7c 100644 --- a/pyasic/web/braiins_os/better_monkey.py +++ b/pyasic/web/braiins_os/better_monkey.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import Any, Dict +from typing import Any from betterproto import DATETIME_ZERO, TYPE_MAP, TYPE_MESSAGE, Casing, Message @@ -7,7 +7,7 @@ from betterproto import DATETIME_ZERO, TYPE_MAP, TYPE_MESSAGE, Casing, Message # https://github.com/danielgtaylor/python-betterproto/pull/609 def to_pydict( self, casing: Casing = Casing.CAMEL, include_default_values: bool = False -) -> Dict[str, Any]: +) -> dict[str, Any]: """ Returns a python dict representation of this object. @@ -23,10 +23,10 @@ def to_pydict( Returns -------- - Dict[:class:`str`, Any] + dict[:class:`str`, Any] The python dict representation of this object. """ - output: Dict[str, Any] = {} + output: dict[str, Any] = {} defaults = self._betterproto.default_gen for field_name, meta in self._betterproto.meta_by_field_name.items(): field_is_repeated = defaults[field_name] is list diff --git a/pyasic/web/braiins_os/boser.py b/pyasic/web/braiins_os/boser.py index ef8cab5f..e538733d 100644 --- a/pyasic/web/braiins_os/boser.py +++ b/pyasic/web/braiins_os/boser.py @@ -51,10 +51,10 @@ class BOSMinerGRPCStub( class BOSerWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) - self.username = "root" - self.pwd = settings.get("default_bosminer_password", "root") + self.username: str = "root" + self.pwd: str = settings.get("default_bosminer_password", "root") self.port = 50051 - self._auth_time = None + self._auth_time: datetime | None = None @property def commands(self) -> list: @@ -68,15 +68,17 @@ class BOSerWebAPI(BaseWebAPI): dir(self) if func not in ["send_command", "multicommand", "auth", "commands", "get_commands"] - if callable(getattr(self, func)) and + if callable(getattr(self, func)) + and # no __ or _ methods - not func.startswith("__") and not func.startswith("_") + not func.startswith("__") + and not func.startswith("_") ] async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True ) -> dict: - result = {"multicommand": True} + result: dict[str, Any] = {"multicommand": True} tasks = {} for command in commands: try: @@ -89,14 +91,14 @@ class BOSerWebAPI(BaseWebAPI): ) 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 return result async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -125,14 +127,16 @@ class BOSerWebAPI(BaseWebAPI): raise APIError(f"gRPC command failed - {endpoint}") from e async def auth(self) -> str | None: - if self.token is not None and self._auth_time - datetime.now() < timedelta( - seconds=3540 + if ( + self.token is not None + and self._auth_time is not None + and datetime.now() - self._auth_time < timedelta(seconds=3540) ): return self.token await self._get_auth() return self.token - async def _get_auth(self) -> str: + async def _get_auth(self) -> str | None: async with Channel(self.ip, self.port) as c: req = LoginRequest(username=self.username, password=self.pwd) async with c.request( @@ -143,11 +147,13 @@ class BOSerWebAPI(BaseWebAPI): ) as stream: await stream.send_message(req, end=True) await stream.recv_initial_metadata() - auth = stream.initial_metadata.get("authorization") - if auth is not None: - self.token = auth - self._auth_time = datetime.now() - return self.token + if stream.initial_metadata is not None: + auth = stream.initial_metadata.get("authorization") + if auth is not None and isinstance(auth, str): + self.token = auth + self._auth_time = datetime.now() + return self.token + return None async def get_api_version(self) -> dict: return await self.send_command( @@ -194,7 +200,7 @@ class BOSerWebAPI(BaseWebAPI): privileged=True, ) - async def set_password(self, password: str = None) -> dict: + async def set_password(self, password: str | None = None) -> dict: return await self.send_command( "set_password", message=SetPasswordRequest(password=password), @@ -209,7 +215,7 @@ class BOSerWebAPI(BaseWebAPI): async def set_immersion_mode( self, enable: bool, - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "set_immersion_mode", @@ -230,7 +236,7 @@ class BOSerWebAPI(BaseWebAPI): ) async def set_default_power_target( - self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY + self, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY) ) -> dict: return await self.send_command( "set_default_power_target", @@ -241,7 +247,7 @@ class BOSerWebAPI(BaseWebAPI): async def set_power_target( self, power_target: int, - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "set_power_target", @@ -254,7 +260,7 @@ class BOSerWebAPI(BaseWebAPI): async def increment_power_target( self, power_target_increment: int, - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "increment_power_target", @@ -268,7 +274,7 @@ class BOSerWebAPI(BaseWebAPI): async def decrement_power_target( self, power_target_decrement: int, - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "decrement_power_target", @@ -280,7 +286,7 @@ class BOSerWebAPI(BaseWebAPI): ) async def set_default_hashrate_target( - self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY + self, save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY) ) -> dict: return await self.send_command( "set_default_hashrate_target", @@ -291,7 +297,7 @@ class BOSerWebAPI(BaseWebAPI): async def set_hashrate_target( self, hashrate_target: float, - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "set_hashrate_target", @@ -305,7 +311,7 @@ class BOSerWebAPI(BaseWebAPI): async def increment_hashrate_target( self, hashrate_target_increment: int, - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "increment_hashrate_target", @@ -321,7 +327,7 @@ class BOSerWebAPI(BaseWebAPI): async def decrement_hashrate_target( self, hashrate_target_decrement: int, - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "decrement_hashrate_target", @@ -339,15 +345,19 @@ class BOSerWebAPI(BaseWebAPI): enable: bool, power_step: int, min_power_target: int, - enable_shutdown: bool = None, - shutdown_duration: int = None, + enable_shutdown: bool | None = None, + shutdown_duration: int | None = None, ) -> dict: return await self.send_command( "set_dps", message=SetDpsRequest( enable=enable, enable_shutdown=enable_shutdown, - shutdown_duration=shutdown_duration, + shutdown_duration=( + Hours(hours=shutdown_duration) + if shutdown_duration is not None + else None + ), target=DpsTarget( power_target=DpsPowerTarget( power_step=Power(power_step), @@ -360,50 +370,40 @@ class BOSerWebAPI(BaseWebAPI): async def set_performance_mode( self, - wattage_target: int = None, - hashrate_target: int = None, - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + wattage_target: int | None = None, + hashrate_target: int | None = None, + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: if wattage_target is not None and hashrate_target is not None: logging.error( "Cannot use both wattage_target and hashrate_target, using wattage_target." ) - elif wattage_target is None and hashrate_target is None: + hashrate_target = None + + tuner_mode: TunerPerformanceMode + if wattage_target is not None: + tuner_mode = TunerPerformanceMode( + power_target=PowerTargetMode(power_target=Power(watt=wattage_target)) + ) + elif hashrate_target is not None: + tuner_mode = TunerPerformanceMode( + hashrate_target=HashrateTargetMode( + hashrate_target=TeraHashrate(terahash_per_second=hashrate_target) + ) + ) + else: raise APIError( "No target supplied, please supply either wattage_target or hashrate_target." ) - if wattage_target is not None: - return await self.send_command( - "set_performance_mode", - message=SetPerformanceModeRequest( - save_action=save_action, - mode=PerformanceMode( - tuner_mode=TunerPerformanceMode( - power_target=PowerTargetMode( - power_target=Power(watt=wattage_target) - ) - ) - ), - ), - privileged=True, - ) - if hashrate_target is not None: - return await self.send_command( - "set_performance_mode", - message=SetPerformanceModeRequest( - save_action=save_action, - mode=PerformanceMode( - tuner_mode=TunerPerformanceMode( - hashrate_target=HashrateTargetMode( - hashrate_target=TeraHashrate( - terahash_per_second=hashrate_target - ) - ) - ) - ), - ), - privileged=True, - ) + + return await self.send_command( + "set_performance_mode", + message=SetPerformanceModeRequest( + save_action=save_action, + mode=PerformanceMode(tuner_mode=tuner_mode), + ), + privileged=True, + ) async def get_active_performance_mode(self) -> dict: return await self.send_command( @@ -461,8 +461,8 @@ class BOSerWebAPI(BaseWebAPI): async def enable_hashboards( self, - hashboard_ids: List[str], - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + hashboard_ids: list[str], + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "enable_hashboards", @@ -474,8 +474,8 @@ class BOSerWebAPI(BaseWebAPI): async def disable_hashboards( self, - hashboard_ids: List[str], - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + hashboard_ids: list[str], + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "disable_hashboards", @@ -487,8 +487,8 @@ class BOSerWebAPI(BaseWebAPI): async def set_pool_groups( self, - pool_groups: List[PoolGroupConfiguration], - save_action: SaveAction = SaveAction.SAVE_AND_APPLY, + pool_groups: list[PoolGroupConfiguration], + save_action: SaveAction = SaveAction(SaveAction.SAVE_AND_APPLY), ) -> dict: return await self.send_command( "set_pool_groups", diff --git a/pyasic/web/braiins_os/bosminer.py b/pyasic/web/braiins_os/bosminer.py index 9b64f6f6..36d17c8b 100644 --- a/pyasic/web/braiins_os/bosminer.py +++ b/pyasic/web/braiins_os/bosminer.py @@ -34,7 +34,7 @@ class BOSMinerWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, diff --git a/pyasic/web/elphapex.py b/pyasic/web/elphapex.py index 2f456c00..b288e741 100644 --- a/pyasic/web/elphapex.py +++ b/pyasic/web/elphapex.py @@ -33,12 +33,12 @@ class ElphapexWebAPI(BaseWebAPI): ip (str): IP address of the Elphapex device. """ super().__init__(ip) - self.username = "root" - self.pwd = settings.get("default_elphapex_web_password", "root") + self.username: str = "root" + self.pwd: str = settings.get("default_elphapex_web_password", "root") async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -47,7 +47,7 @@ class ElphapexWebAPI(BaseWebAPI): """Send a command to the Elphapex device using HTTP digest authentication. Args: - command (str | bytes): The CGI command to send. + command (str): The CGI command to send. ignore_errors (bool): If True, ignore any HTTP errors. allow_warning (bool): If True, proceed with warnings. privileged (bool): If set to True, requires elevated privileges. diff --git a/pyasic/web/epic.py b/pyasic/web/epic.py index 4a72ec93..267a8a9b 100644 --- a/pyasic/web/epic.py +++ b/pyasic/web/epic.py @@ -38,7 +38,7 @@ class ePICWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -49,15 +49,17 @@ class ePICWebAPI(BaseWebAPI): async with httpx.AsyncClient(transport=settings.transport()) as client: for retry_cnt in range(settings.get("get_data_retries", 1)): try: - if parameters.get("form") is not None: - form_data = parameters["form"] - form_data.add_field("password", self.pwd) + if parameters.get("files") is not None: + files = parameters["files"] + data_fields = parameters.get("data", {}) + data_fields["password"] = self.pwd response = await client.post( f"http://{self.ip}:{self.port}/{command}", timeout=5, - data=form_data, + files=files, + data=data_fields, ) - if post: + elif post: response = await client.post( f"http://{self.ip}:{self.port}/{command}", timeout=5, @@ -89,11 +91,12 @@ class ePICWebAPI(BaseWebAPI): return {"success": True} except (httpx.HTTPError, json.JSONDecodeError, AttributeError): pass + raise APIError(f"Failed to send command to miner: {self}") async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True ) -> dict: - data = {k: None for k in commands} + data: dict[str, Any] = {k: None for k in commands} data["multicommand"] = True for command in commands: data[command] = await self.send_command(command) @@ -147,7 +150,7 @@ class ePICWebAPI(BaseWebAPI): async def capabilities(self) -> dict: return await self.send_command("capabilities") - async def system_update(self, file: Path | str, keep_settings: bool = True): + async def system_update(self, file: Path | str, keep_settings: bool = True) -> None: """Perform a system update by uploading a firmware file and sending a command to initiate the update.""" @@ -159,9 +162,7 @@ class ePICWebAPI(BaseWebAPI): checksum = sha256_hash.hexdigest() # prepare the multipart/form-data request - form_data = aiohttp.FormData() - form_data.add_field("checksum", checksum) - form_data.add_field("keepsettings", str(keep_settings).lower()) - form_data.add_field("update.zip", open(file, "rb"), filename="update.zip") - - await self.send_command("systemupdate", form=form_data) + with open(file, "rb") as f: + files = {"update.zip": ("update.zip", f, "application/zip")} + data = {"checksum": checksum, "keepsettings": str(keep_settings).lower()} + await self.send_command("systemupdate", files=files, data=data) diff --git a/pyasic/web/espminer.py b/pyasic/web/espminer.py index bb20d3a3..0bf933bf 100644 --- a/pyasic/web/espminer.py +++ b/pyasic/web/espminer.py @@ -13,7 +13,7 @@ from pyasic.web.base import BaseWebAPI class ESPMinerWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -21,7 +21,8 @@ class ESPMinerWebAPI(BaseWebAPI): ) -> dict: url = f"http://{self.ip}:{self.port}/api/{command}" async with httpx.AsyncClient(transport=settings.transport()) as client: - for _ in range(settings.get("get_data_retries", 1)): + retries = settings.get("get_data_retries", 1) + for attempt in range(retries): try: if parameters.get("post", False): parameters.pop("post") @@ -42,14 +43,22 @@ class ESPMinerWebAPI(BaseWebAPI): url, timeout=settings.get("api_function_timeout", 5), ) - except httpx.HTTPError: - pass + except httpx.HTTPError as e: + if attempt == retries - 1: + raise APIError( + f"HTTP error sending '{command}' to {self.ip}: {e}" + ) else: if data.status_code == 200: try: return data.json() - except json.decoder.JSONDecodeError: - pass + except json.decoder.JSONDecodeError as e: + response_text = data.text if data.text else "empty response" + if attempt == retries - 1: + raise APIError( + f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}" + ) + raise APIError(f"Failed to send command to miner API: {url}") async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True @@ -75,26 +84,27 @@ class ESPMinerWebAPI(BaseWebAPI): *[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): if not isinstance(result, (APIError, Exception)): if result is None or result == {}: - result = {} - data[cmd] = result + data[cmd] = {} + else: + data[cmd] = result return data - async def system_info(self): + async def system_info(self) -> dict: return await self.send_command("system/info") - async def swarm_info(self): + async def swarm_info(self) -> dict: return await self.send_command("swarm/info") - async def restart(self): + async def restart(self) -> dict: return await self.send_command("system/restart", post=True) - async def update_settings(self, **config): + async def update_settings(self, **config: Any) -> dict: return await self.send_command("system", patch=True, **config) - async def asic_info(self): + async def asic_info(self) -> dict: return await self.send_command("system/asic") diff --git a/pyasic/web/goldshell.py b/pyasic/web/goldshell.py index 7de6da11..abd1f9e0 100644 --- a/pyasic/web/goldshell.py +++ b/pyasic/web/goldshell.py @@ -17,20 +17,23 @@ from __future__ import annotations import json import warnings -from typing import Any +from typing import Any, TypedDict import httpx from pyasic import settings +from pyasic.errors import APIError from pyasic.web.base import BaseWebAPI +PoolPass = TypedDict("PoolPass", {"pass": str}) + class GoldshellWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) - self.username = "admin" - self.pwd = settings.get("default_goldshell_web_password", "123456789") - self.token = None + self.username: str = "admin" + self.pwd: str = settings.get("default_goldshell_web_password", "123456789") + self.token: str | None = None async def auth(self) -> str | None: async with httpx.AsyncClient(transport=settings.transport()) as client: @@ -63,7 +66,7 @@ class GoldshellWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -72,7 +75,12 @@ class GoldshellWebAPI(BaseWebAPI): if self.token is None: await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: - for _ in range(settings.get("get_data_retries", 1)): + retries = settings.get("get_data_retries", 1) + for attempt in range(retries): + if self.token is None: + raise APIError( + f"Could not authenticate web token with miner: {self}" + ) try: if not parameters == {}: response = await client.put( @@ -91,17 +99,33 @@ class GoldshellWebAPI(BaseWebAPI): return json_data except TypeError: await self.auth() - except (httpx.HTTPError, json.JSONDecodeError): - pass + except httpx.HTTPError as e: + if attempt == retries - 1: + raise APIError( + f"HTTP error sending '{command}' to {self.ip}: {e}" + ) + except json.JSONDecodeError as e: + if attempt == retries - 1: + response_text = ( + response.text if response.text else "empty response" + ) + raise APIError( + f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}" + ) + raise APIError(f"Failed to send command to miner: {self}") async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True ) -> dict: - data = {k: None for k in commands} + data: dict[str, Any] = {k: None for k in commands} data["multicommand"] = True await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: for command in commands: + if self.token is None: + raise APIError( + f"Could not authenticate web token with miner: {self}" + ) try: uri_commnand = command if command == "devs": @@ -127,22 +151,22 @@ class GoldshellWebAPI(BaseWebAPI): async def newpool(self, url: str, user: str, password: str) -> dict: # looks dumb, but cant pass `pass` since it is a built in type - return await self.send_command( - "newpool", **{"url": url, "user": user, "pass": password} - ) + poolpass: PoolPass = {"pass": password} + return await self.send_command("newpool", url=url, user=user, **poolpass) async def delpool( self, url: str, user: str, password: str, dragid: int = 0 ) -> dict: # looks dumb, but cant pass `pass` since it is a built in type + poolpass: PoolPass = {"pass": password} return await self.send_command( - "delpool", **{"url": url, "user": user, "pass": password, "dragid": dragid} + "delpool", url=url, user=user, dragid=dragid, **poolpass ) async def setting(self) -> dict: return await self.send_command("setting") - async def set_setting(self, values: dict): + async def set_setting(self, values: dict) -> None: await self.send_command("setting", **values) async def status(self) -> dict: diff --git a/pyasic/web/hammer.py b/pyasic/web/hammer.py index 90440e96..e4bc0972 100644 --- a/pyasic/web/hammer.py +++ b/pyasic/web/hammer.py @@ -33,12 +33,12 @@ class HammerWebAPI(BaseWebAPI): ip (str): IP address of the Hammer device. """ super().__init__(ip) - self.username = "root" - self.pwd = settings.get("default_hammer_web_password", "root") + self.username: str = "root" + self.pwd: str = settings.get("default_hammer_web_password", "root") async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -47,7 +47,7 @@ class HammerWebAPI(BaseWebAPI): """Send a command to the Hammer device using HTTP digest authentication. Args: - command (str | bytes): The CGI command to send. + command (str): The CGI command to send. ignore_errors (bool): If True, ignore any HTTP errors. allow_warning (bool): If True, proceed with warnings. privileged (bool): If set to True, requires elevated privileges. diff --git a/pyasic/web/hiveon.py b/pyasic/web/hiveon.py index 20ba223e..6b470cde 100644 --- a/pyasic/web/hiveon.py +++ b/pyasic/web/hiveon.py @@ -22,7 +22,7 @@ from typing import Any import aiofiles import httpx -from pyasic import settings +from pyasic import APIError, settings from pyasic.web.base import BaseWebAPI @@ -34,12 +34,12 @@ class HiveonWebAPI(BaseWebAPI): ip (str): IP address of the Antminer device. """ super().__init__(ip) - self.username = "root" - self.pwd = settings.get("default_hive_web_password", "root") + self.username: str = "root" + self.pwd: str = settings.get("default_hive_web_password", "root") async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -48,7 +48,7 @@ class HiveonWebAPI(BaseWebAPI): """Send a command to the Antminer device using HTTP digest authentication. Args: - command (str | bytes): The CGI command to send. + command (str): The CGI command to send. ignore_errors (bool): If True, ignore any HTTP errors. allow_warning (bool): If True, proceed with warnings. privileged (bool): If set to True, requires elevated privileges. @@ -62,22 +62,26 @@ class HiveonWebAPI(BaseWebAPI): try: async with httpx.AsyncClient(transport=settings.transport()) as client: if parameters: - data = await client.post( + response = await client.post( url, data=parameters, auth=auth, timeout=settings.get("api_function_timeout", 3), ) else: - data = await client.get(url, auth=auth) - except httpx.HTTPError: - pass + response = await client.get(url, auth=auth) + except httpx.HTTPError as e: + raise APIError(f"HTTP error sending '{command}' to {self.ip}: {e}") else: - if data.status_code == 200: + if response.status_code == 200: try: - return data.json() - except json.decoder.JSONDecodeError: - pass + return response.json() + except json.decoder.JSONDecodeError as e: + response_text = response.text if response.text else "empty response" + raise APIError( + f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}" + ) + raise APIError(f"Failed to send command to miner API: {url}") async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True @@ -204,10 +208,9 @@ class HiveonWebAPI(BaseWebAPI): async with aiofiles.open(file, "rb") as firmware: file_content = await firmware.read() - parameters = { - "file": (file.name, file_content, "application/octet-stream"), - "filename": file.name, - "keep_settings": keep_settings, - } - - return await self.send_command(command="upgrade", **parameters) + return await self.send_command( + "upgrade", + file=(file.name, file_content, "application/octet-stream"), + filename=file.name, + keep_settings=keep_settings, + ) diff --git a/pyasic/web/iceriver.py b/pyasic/web/iceriver.py index 1ba1e555..92766c69 100644 --- a/pyasic/web/iceriver.py +++ b/pyasic/web/iceriver.py @@ -41,7 +41,7 @@ class IceRiverWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, diff --git a/pyasic/web/innosilicon.py b/pyasic/web/innosilicon.py index 046b481b..895b3540 100644 --- a/pyasic/web/innosilicon.py +++ b/pyasic/web/innosilicon.py @@ -29,9 +29,9 @@ from pyasic.web.base import BaseWebAPI class InnosiliconWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) - self.username = "admin" - self.pwd = settings.get("default_innosilicon_web_password", "admin") - self.token = None + self.username: str = "admin" + self.pwd: str = settings.get("default_innosilicon_web_password", "admin") + self.token: str | None = None async def auth(self) -> str | None: async with httpx.AsyncClient(transport=settings.transport()) as client: @@ -49,7 +49,7 @@ class InnosiliconWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -58,7 +58,12 @@ class InnosiliconWebAPI(BaseWebAPI): if self.token is None: await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: - for _ in range(settings.get("get_data_retries", 1)): + retries = settings.get("get_data_retries", 1) + for attempt in range(retries): + if self.token is None: + raise APIError( + f"Could not authenticate web token with miner: {self}" + ) try: response = await client.post( f"http://{self.ip}:{self.port}/api/{command}", @@ -82,17 +87,33 @@ class InnosiliconWebAPI(BaseWebAPI): raise APIError(json_data["message"]) raise APIError("Innosilicon web api command failed.") return json_data - except (httpx.HTTPError, json.JSONDecodeError): - pass + except httpx.HTTPError as e: + if attempt == retries - 1: + raise APIError( + f"HTTP error sending '{command}' to {self.ip}: {e}" + ) + except json.JSONDecodeError as e: + if attempt == retries - 1: + response_text = ( + response.text if response.text else "empty response" + ) + raise APIError( + f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}" + ) + raise APIError(f"Failed to send command to miner: {self}") async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True ) -> dict: - data = {k: None for k in commands} + data: dict[str, Any] = {k: None for k in commands} data["multicommand"] = True await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: for command in commands: + if self.token is None: + raise APIError( + f"Could not authenticate web token with miner: {self}" + ) try: response = await client.post( f"http://{self.ip}:{self.port}/api/{command}", diff --git a/pyasic/web/marathon.py b/pyasic/web/marathon.py index f4ae90da..a5421e3a 100644 --- a/pyasic/web/marathon.py +++ b/pyasic/web/marathon.py @@ -6,13 +6,15 @@ from typing import Any import httpx -from pyasic import settings +from pyasic import APIError, settings from pyasic.web.base import BaseWebAPI class MaraWebAPI(BaseWebAPI): def __init__(self, ip: str) -> None: super().__init__(ip) + self.username: str = "root" + self.pwd: str = "root" async def multicommand( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True @@ -52,7 +54,7 @@ class MaraWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, @@ -65,22 +67,26 @@ class MaraWebAPI(BaseWebAPI): transport=settings.transport(), ) as client: if parameters: - data = await client.post( + response = await client.post( url, auth=auth, timeout=settings.get("api_function_timeout", 3), json=parameters, ) else: - data = await client.get(url, auth=auth) + response = await client.get(url, auth=auth) except httpx.HTTPError: pass else: - if data.status_code == 200: + if response.status_code == 200: try: - return data.json() - except json.decoder.JSONDecodeError: - pass + return response.json() + except json.decoder.JSONDecodeError as e: + response_text = response.text if response.text else "empty response" + raise APIError( + f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}" + ) + raise APIError(f"Failed to send command to miner API: {url}") async def brief(self): return await self.send_command("brief") diff --git a/pyasic/web/mskminer.py b/pyasic/web/mskminer.py index 26a08327..f930ba29 100644 --- a/pyasic/web/mskminer.py +++ b/pyasic/web/mskminer.py @@ -41,7 +41,7 @@ class MSKMinerWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, diff --git a/pyasic/web/vnish.py b/pyasic/web/vnish.py index 410aa89b..d123d980 100644 --- a/pyasic/web/vnish.py +++ b/pyasic/web/vnish.py @@ -22,6 +22,7 @@ from typing import Any import httpx from pyasic import settings +from pyasic.errors import APIError from pyasic.web.base import BaseWebAPI @@ -53,21 +54,26 @@ class VNishWebAPI(BaseWebAPI): async def send_command( self, - command: str | bytes, + command: str, ignore_errors: bool = False, allow_warning: bool = True, privileged: bool = False, **parameters: Any, - ) -> dict | None: + ) -> dict: post = privileged or not parameters == {} if self.token is None: await self.auth() async with httpx.AsyncClient(transport=settings.transport()) as client: - for _ in range(settings.get("get_data_retries", 1)): + retries = settings.get("get_data_retries", 1) + for attempt in range(retries): try: auth = self.token + if auth is None: + raise APIError( + f"Could not authenticate web token with miner: {self}" + ) if command.startswith("system"): - auth = "Bearer " + self.token + auth = "Bearer " + auth if post: response = await client.post( @@ -90,13 +96,30 @@ class VNishWebAPI(BaseWebAPI): if json_data: return json_data return {"success": True} - except (httpx.HTTPError, json.JSONDecodeError, AttributeError): - pass + except httpx.HTTPError as e: + if attempt == retries - 1: + raise APIError( + f"HTTP error sending '{command}' to {self.ip}: {e}" + ) + except json.JSONDecodeError as e: + if attempt == retries - 1: + response_text = ( + response.text if response.text else "empty response" + ) + raise APIError( + f"JSON decode error for '{command}' from {self.ip}: {e} - Response: {response_text}" + ) + 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( self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True ) -> dict: - data = {k: None for k in commands} + data: dict[str, Any] = {k: None for k in commands} data["multicommand"] = True for command in commands: data[command] = await self.send_command(command) @@ -141,14 +164,14 @@ class VNishWebAPI(BaseWebAPI): async def settings(self) -> dict: 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 settings = await self.settings() 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 - 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: return await self.send_command("autotune/presets") diff --git a/pyproject.toml b/pyproject.toml index 9301e186..4c41294d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] -requires-python = ">3.9, <4.0" +requires-python = ">3.10, <4.0" dependencies = [ "httpx>=0.26.0", "asyncssh>=2.20.0", @@ -59,8 +59,10 @@ dependencies = [ optional = true [tool.poetry.group.dev.dependencies] -pre-commit = "^4.0.1" -isort = "^5.12.0" +pre-commit = "^4.3.0" +ruff = "^0.13.2" +types-passlib = "^1.7.7.20250602" +types-aiofiles = "^24.1.0.20250822" [tool.poetry.group.docs] optional = true @@ -75,5 +77,46 @@ mkdocs-material = "^9.5.39" requires = ["poetry-core>=2.0.0"] build-backend = "poetry.core.masonry.api" -[tool.isort] -profile = "black" +[tool.mypy] +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" diff --git a/tests/config_tests/fans.py b/tests/config_tests/fans.py index e1f02226..fa6ceaa9 100644 --- a/tests/config_tests/fans.py +++ b/tests/config_tests/fans.py @@ -7,7 +7,7 @@ class TestFanConfig(unittest.TestCase): def test_serialize_and_deserialize(self): for fan_mode in FanModeConfig: with self.subTest( - msg=f"Test serialization and deserialization of fan config", + msg="Test serialization and deserialization of fan config", fan_mode=fan_mode, ): conf = fan_mode() @@ -17,7 +17,7 @@ class TestFanConfig(unittest.TestCase): def test_bosminer_deserialize_and_serialize(self): for fan_mode in FanModeConfig: 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, ): conf = fan_mode() @@ -27,7 +27,7 @@ class TestFanConfig(unittest.TestCase): def test_am_modern_deserialize_and_serialize(self): for fan_mode in FanModeConfig: 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, ): conf = fan_mode() @@ -37,7 +37,7 @@ class TestFanConfig(unittest.TestCase): def test_epic_deserialize_and_serialize(self): for fan_mode in FanModeConfig: 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, ): conf = fan_mode() @@ -47,7 +47,7 @@ class TestFanConfig(unittest.TestCase): def test_vnish_deserialize_and_serialize(self): for fan_mode in FanModeConfig: 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, ): conf = fan_mode() @@ -57,7 +57,7 @@ class TestFanConfig(unittest.TestCase): def test_auradine_deserialize_and_serialize(self): for fan_mode in FanModeConfig: 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, ): conf = fan_mode() @@ -67,7 +67,7 @@ class TestFanConfig(unittest.TestCase): def test_boser_deserialize_and_serialize(self): for fan_mode in FanModeConfig: 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, ): conf = fan_mode() diff --git a/tests/miners_tests/__init__.py b/tests/miners_tests/__init__.py index d5ecedeb..a9fd29f7 100644 --- a/tests/miners_tests/__init__.py +++ b/tests/miners_tests/__init__.py @@ -29,7 +29,7 @@ class MinersTest(unittest.TestCase): for miner_type in MINER_CLASSES.keys(): for miner_model in MINER_CLASSES[miner_type].keys(): with self.subTest( - msg=f"Test creation of miner", + msg="Test creation of miner", miner_type=miner_type, miner_model=miner_model, ): @@ -42,7 +42,7 @@ class MinersTest(unittest.TestCase): if miner_model is None: continue with self.subTest( - msg=f"Test miner has defined hashboards", + msg="Test miner has defined hashboards", miner_type=miner_type, miner_model=miner_model, ): @@ -56,7 +56,7 @@ class MinersTest(unittest.TestCase): if miner_model is None: continue with self.subTest( - msg=f"Test miner has defined fans", + msg="Test miner has defined fans", miner_type=miner_type, miner_model=miner_model, ): @@ -70,7 +70,7 @@ class MinersTest(unittest.TestCase): if miner_model is None: continue with self.subTest( - msg=f"Test miner has defined algo", + msg="Test miner has defined algo", miner_type=miner_type, miner_model=miner_model, ): @@ -105,7 +105,7 @@ class MinersTest(unittest.TestCase): for miner_type in MINER_CLASSES.keys(): for miner_model in MINER_CLASSES[miner_type].keys(): with self.subTest( - msg=f"Data map key check", + msg="Data map key check", miner_type=miner_type, miner_model=miner_model, ): diff --git a/tests/rpc_tests/__init__.py b/tests/rpc_tests/__init__.py index d3266202..2cc23cf4 100644 --- a/tests/rpc_tests/__init__.py +++ b/tests/rpc_tests/__init__.py @@ -50,23 +50,39 @@ class TestAPIBase(unittest.IsolatedAsyncioTestCase): ).encode("utf-8") def get_success_value(self, command: str): - if self.api_str == "BTMiner" and command == "status": - return json.dumps( - { - "STATUS": "S", - "When": 1706287567, - "Code": 131, - "Msg": { - "mineroff": "false", - "mineroff_reason": "", - "mineroff_time": "", - "FirmwareVersion": "20230911.12.Rel", - "power_mode": "", - "hash_percent": "", - }, - "Description": "", - } - ).encode("utf-8") + if self.api_str == "BTMiner": + if command == "status": + return json.dumps( + { + "STATUS": "S", + "When": 1706287567, + "Code": 131, + "Msg": { + "mineroff": "false", + "mineroff_reason": "", + "mineroff_time": "", + "FirmwareVersion": "20230911.12.Rel", + "power_mode": "", + "hash_percent": "", + }, + "Description": "", + } + ).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( { "STATUS": [ @@ -119,7 +135,33 @@ class TestAPIBase(unittest.IsolatedAsyncioTestCase): command=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: await api_func() except APIError: