From d66739e2c9ee107819c279e7bc8adca0f5381393 Mon Sep 17 00:00:00 2001 From: Brett Rowan <121075405+b-rowan@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:42:41 -0700 Subject: [PATCH] feature: swap from `@dataclass` to pydantic `BaseModel` (#238) * feature: switch almost everything to pydantic BaseModel * feature: swap more dataclasses over to pydantic models * feature: use more model serializers to make data handle better * bug: fix some serialization issues with `None` values * bug: fix some initialization problems with miner mode config * bug: fix new BOS+ pool parsing * bug: fix new BOS+ board temperature parsing serialization error * bug: more misc fixes with serialization, extra methods, and hashrate types * bug: add explicit type conversions to hashrate types * bug: fix epic pool URL parsing * bug: fix positional args in hashboards * bug: fix epic missing url scheme * convert board temp to int --------- Co-authored-by: Upstream Data Co-authored-by: John-Paul Compagnone --- poetry.lock | 173 ++++++++++++++++--- pyasic/config/__init__.py | 22 +-- pyasic/config/base.py | 8 +- pyasic/config/fans.py | 22 ++- pyasic/config/mining/__init__.py | 44 +++-- pyasic/config/mining/algo.py | 13 +- pyasic/config/mining/scaling.py | 13 +- pyasic/config/pools.py | 44 ++--- pyasic/config/temperature.py | 7 +- pyasic/data/__init__.py | 223 ++++++++++--------------- pyasic/data/boards.py | 31 ++-- pyasic/data/device.py | 35 +++- pyasic/data/error_codes/X19.py | 13 +- pyasic/data/error_codes/base.py | 18 ++ pyasic/data/error_codes/bos.py | 12 +- pyasic/data/error_codes/innosilicon.py | 23 +-- pyasic/data/error_codes/whatsminer.py | 21 +-- pyasic/data/fans.py | 6 +- pyasic/data/hashrate/__init__.py | 1 + pyasic/data/hashrate/base.py | 5 + pyasic/data/hashrate/sha256.py | 36 ++-- pyasic/data/pools.py | 25 ++- pyasic/device/algorithm/__init__.py | 1 + pyasic/device/algorithm/base.py | 7 + pyasic/device/algorithm/sha256.py | 4 +- pyasic/device/models.py | 24 +-- pyasic/miners/antminer/hiveon/X9/T9.py | 4 +- pyasic/miners/backends/antminer.py | 14 +- pyasic/miners/backends/auradine.py | 9 +- pyasic/miners/backends/avalonminer.py | 6 +- pyasic/miners/backends/bfgminer.py | 7 +- pyasic/miners/backends/bitaxe.py | 5 +- pyasic/miners/backends/bmminer.py | 7 +- pyasic/miners/backends/braiins_os.py | 57 ++++--- pyasic/miners/backends/btminer.py | 13 +- pyasic/miners/backends/cgminer.py | 3 +- pyasic/miners/backends/epic.py | 28 ++-- pyasic/miners/backends/goldshell.py | 2 +- pyasic/miners/backends/hammer.py | 45 +---- pyasic/miners/backends/iceriver.py | 9 +- pyasic/miners/backends/innosilicon.py | 11 +- pyasic/miners/backends/luxminer.py | 12 +- pyasic/miners/backends/marathon.py | 14 +- pyasic/miners/backends/vnish.py | 3 +- pyasic/miners/base.py | 1 + pyproject.toml | 1 + 46 files changed, 597 insertions(+), 485 deletions(-) create mode 100644 pyasic/data/error_codes/base.py create mode 100644 pyasic/data/hashrate/base.py create mode 100644 pyasic/device/algorithm/base.py diff --git a/poetry.lock b/poetry.lock index 41e7febb..33f36ff8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aiofiles" @@ -11,6 +11,17 @@ files = [ {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "4.6.2.post1" @@ -380,13 +391,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.6" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, - {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] @@ -437,13 +448,13 @@ files = [ [[package]] name = "identify" -version = "2.6.1" +version = "2.6.2" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, + {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, ] [package.extras] @@ -837,13 +848,13 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -936,6 +947,130 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pymdown-extensions" version = "10.12" @@ -1068,13 +1203,13 @@ files = [ [[package]] name = "tomli" -version = "2.0.2" +version = "2.1.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, - {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, ] [[package]] @@ -1163,13 +1298,13 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] @@ -1183,4 +1318,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "d611b5e8b0c5611d6ee916cedfb7f07f20dfc90a675ebaed04188e8b3c96aabe" +content-hash = "0ae7d5fe3858e93c8a8715b11876be854ce8bf39685316ca1bee759bf0896ce6" diff --git a/pyasic/config/__init__.py b/pyasic/config/__init__.py index 627d5420..66ed80bd 100644 --- a/pyasic/config/__init__.py +++ b/pyasic/config/__init__.py @@ -13,25 +13,25 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ -from dataclasses import asdict, dataclass, field -from pyasic.config.fans import FanModeConfig -from pyasic.config.mining import MiningModeConfig +from pydantic import BaseModel, Field + +from pyasic.config.fans import FanMode, FanModeConfig +from pyasic.config.mining import MiningMode, MiningModeConfig from pyasic.config.mining.scaling import ScalingConfig from pyasic.config.pools import PoolConfig from pyasic.config.temperature import TemperatureConfig from pyasic.misc import merge_dicts -@dataclass -class MinerConfig: +class MinerConfig(BaseModel): """Represents the configuration for a miner including pool configuration, fan mode, temperature settings, mining mode, and power scaling.""" - pools: PoolConfig = field(default_factory=PoolConfig.default) - fan_mode: FanModeConfig = field(default_factory=FanModeConfig.default) - temperature: TemperatureConfig = field(default_factory=TemperatureConfig.default) - mining_mode: MiningModeConfig = field(default_factory=MiningModeConfig.default) + pools: PoolConfig = Field(default_factory=PoolConfig.default) + fan_mode: FanMode = Field(default_factory=FanModeConfig.default) + temperature: TemperatureConfig = Field(default_factory=TemperatureConfig.default) + mining_mode: MiningMode = Field(default_factory=MiningModeConfig.default) def __getitem__(self, item): try: @@ -41,7 +41,7 @@ class MinerConfig: def as_dict(self) -> dict: """Converts the MinerConfig object to a dictionary.""" - return asdict(self) + return self.model_dump() def as_am_modern(self, user_suffix: str = None) -> dict: """Generates the configuration in the format suitable for modern Antminers.""" @@ -107,7 +107,7 @@ class MinerConfig: } def as_boser(self, user_suffix: str = None) -> dict: - """ "Generates the configuration in the format suitable for BOSer.""" + """Generates the configuration in the format suitable for BOSer.""" return { **self.fan_mode.as_boser(), **self.temperature.as_boser(), diff --git a/pyasic/config/base.py b/pyasic/config/base.py index 237a1fe5..924ea70d 100644 --- a/pyasic/config/base.py +++ b/pyasic/config/base.py @@ -15,9 +15,10 @@ # ------------------------------------------------------------------------------ from __future__ import annotations -from dataclasses import asdict, dataclass from enum import Enum +from pydantic import BaseModel + class MinerConfigOption(Enum): @classmethod @@ -80,14 +81,13 @@ class MinerConfigOption(Enum): raise KeyError -@dataclass -class MinerConfigValue: +class MinerConfigValue(BaseModel): @classmethod def from_dict(cls, dict_conf: dict | None): return cls() def as_dict(self) -> dict: - return asdict(self) + return self.model_dump() def as_am_modern(self) -> dict: return {} diff --git a/pyasic/config/fans.py b/pyasic/config/fans.py index 251bbe5e..f67ff4c6 100644 --- a/pyasic/config/fans.py +++ b/pyasic/config/fans.py @@ -15,14 +15,15 @@ # ------------------------------------------------------------------------------ from __future__ import annotations -from dataclasses import dataclass, field +from typing import TypeVar, Union + +from pydantic import Field from pyasic.config.base import MinerConfigOption, MinerConfigValue -@dataclass class FanModeNormal(MinerConfigValue): - mode: str = field(init=False, default="normal") + mode: str = Field(init=False, default="normal") minimum_fans: int = 1 minimum_speed: int = 0 @@ -87,9 +88,8 @@ class FanModeNormal(MinerConfigValue): return {"fanset": {"speed": -1, "min_fans": self.minimum_fans}} -@dataclass class FanModeManual(MinerConfigValue): - mode: str = field(init=False, default="manual") + mode: str = Field(init=False, default="manual") speed: int = 100 minimum_fans: int = 1 @@ -151,9 +151,8 @@ class FanModeManual(MinerConfigValue): return {"fanset": {"speed": self.speed, "min_fans": self.minimum_fans}} -@dataclass class FanModeImmersion(MinerConfigValue): - mode: str = field(init=False, default="immersion") + mode: str = Field(init=False, default="immersion") @classmethod def from_dict(cls, dict_conf: dict | None) -> "FanModeImmersion": @@ -273,7 +272,7 @@ class FanModeConfig(MinerConfigOption): keys = temperature_conf.keys() if "auto" in keys: if "minimumRequiredFans" in keys: - return cls.normal(temperature_conf["minimumRequiredFans"]) + return cls.normal(minimum_fans=temperature_conf["minimumRequiredFans"]) return cls.normal() if "manual" in keys: conf = {} @@ -300,7 +299,9 @@ class FanModeConfig(MinerConfigOption): mode = web_config["general-config"]["environment-profile"] if mode == "AirCooling": if web_config["advance-config"]["override-fan-control"]: - return cls.manual(web_config["advance-config"]["fan-fixed-percent"]) + return cls.manual( + speed=web_config["advance-config"]["fan-fixed-percent"] + ) return cls.normal() return cls.immersion() except LookupError: @@ -333,3 +334,6 @@ class FanModeConfig(MinerConfigOption): except LookupError: pass return cls.default() + + +FanMode = TypeVar("FanMode", bound=Union[*[v.value for v in FanModeConfig]]) diff --git a/pyasic/config/mining/__init__.py b/pyasic/config/mining/__init__.py index ac8e5fd1..cb29cbc8 100644 --- a/pyasic/config/mining/__init__.py +++ b/pyasic/config/mining/__init__.py @@ -15,7 +15,8 @@ # ------------------------------------------------------------------------------ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import field +from typing import TypeVar, Union from pyasic import settings from pyasic.config.base import MinerConfigOption, MinerConfigValue @@ -34,11 +35,10 @@ from pyasic.web.braiins_os.proto.braiins.bos.v1 import ( TunerPerformanceMode, ) -from .algo import TunerAlgo +from .algo import TunerAlgo, TunerAlgoType from .scaling import ScalingConfig -@dataclass class MiningModeNormal(MinerConfigValue): mode: str = field(init=False, default="normal") @@ -74,7 +74,6 @@ class MiningModeNormal(MinerConfigValue): return {"autotunerset": {"enabled": False}} -@dataclass class MiningModeSleep(MinerConfigValue): mode: str = field(init=False, default="sleep") @@ -107,7 +106,6 @@ class MiningModeSleep(MinerConfigValue): } -@dataclass class MiningModeLPM(MinerConfigValue): mode: str = field(init=False, default="low") @@ -130,7 +128,6 @@ class MiningModeLPM(MinerConfigValue): return {"settings": {"level": 1}} -@dataclass class MiningModeHPM(MinerConfigValue): mode: str = field(init=False, default="high") @@ -150,12 +147,11 @@ class MiningModeHPM(MinerConfigValue): return {"mode": {"mode": "turbo"}} -@dataclass class MiningModePowerTune(MinerConfigValue): mode: str = field(init=False, default="power_tuning") - power: int = None - algo: TunerAlgo = field(default_factory=TunerAlgo.default) - scaling: ScalingConfig = None + power: int | None = None + algo: TunerAlgoType = field(default_factory=TunerAlgo.default) + scaling: ScalingConfig | None = None @classmethod def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune": @@ -247,11 +243,10 @@ class MiningModePowerTune(MinerConfigValue): return {"autotunerset": {"enabled": True}} -@dataclass class MiningModeHashrateTune(MinerConfigValue): mode: str = field(init=False, default="hashrate_tuning") hashrate: int = None - algo: TunerAlgo = field(default_factory=TunerAlgo.default) + algo: TunerAlgoType = field(default_factory=TunerAlgo.default) scaling: ScalingConfig = None @classmethod @@ -343,7 +338,6 @@ class MiningModeHashrateTune(MinerConfigValue): return {"autotunerset": {"enabled": True}} -@dataclass class ManualBoardSettings(MinerConfigValue): freq: float volt: float @@ -358,7 +352,6 @@ class ManualBoardSettings(MinerConfigValue): return {"miner-mode": 0} -@dataclass class MiningModeManual(MinerConfigValue): mode: str = field(init=False, default="manual") @@ -521,7 +514,7 @@ class MiningModeConfig(MinerConfigOption): if autotuning_conf.get("psu_power_limit") is not None: # old autotuning conf return cls.power_tuning( - autotuning_conf["psu_power_limit"], + power=autotuning_conf["psu_power_limit"], scaling=ScalingConfig.from_bosminer(toml_conf, mode="power"), ) if autotuning_conf.get("mode") is not None: @@ -530,7 +523,7 @@ class MiningModeConfig(MinerConfigOption): if mode == "power_target": if autotuning_conf.get("power_target") is not None: return cls.power_tuning( - autotuning_conf["power_target"], + power=autotuning_conf["power_target"], scaling=ScalingConfig.from_bosminer(toml_conf, mode="power"), ) return cls.power_tuning( @@ -539,7 +532,7 @@ class MiningModeConfig(MinerConfigOption): if mode == "hashrate_target": if autotuning_conf.get("hashrate_target") is not None: return cls.hashrate_tuning( - autotuning_conf["hashrate_target"], + hashrate=autotuning_conf["hashrate_target"], scaling=ScalingConfig.from_bosminer(toml_conf, mode="hashrate"), ) return cls.hashrate_tuning( @@ -556,7 +549,7 @@ class MiningModeConfig(MinerConfigOption): if mode_settings["preset"] == "disabled": return MiningModeManual.from_vnish(mode_settings) else: - return cls.power_tuning(int(mode_settings["preset"])) + return cls.power_tuning(power=int(mode_settings["preset"])) @classmethod def from_boser(cls, grpc_miner_conf: dict): @@ -571,7 +564,7 @@ class MiningModeConfig(MinerConfigOption): if tuner_conf["tunerMode"] == 1: if tuner_conf.get("powerTarget") is not None: return cls.power_tuning( - tuner_conf["powerTarget"]["watt"], + power=tuner_conf["powerTarget"]["watt"], scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="power"), ) return cls.power_tuning( @@ -581,7 +574,7 @@ class MiningModeConfig(MinerConfigOption): if tuner_conf["tunerMode"] == 2: if tuner_conf.get("hashrateTarget") is not None: return cls.hashrate_tuning( - int(tuner_conf["hashrateTarget"]["terahashPerSecond"]), + hashrate=int(tuner_conf["hashrateTarget"]["terahashPerSecond"]), scaling=ScalingConfig.from_boser( grpc_miner_conf, mode="hashrate" ), @@ -592,13 +585,13 @@ class MiningModeConfig(MinerConfigOption): if tuner_conf.get("powerTarget") is not None: return cls.power_tuning( - tuner_conf["powerTarget"]["watt"], + power=tuner_conf["powerTarget"]["watt"], scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="power"), ) if tuner_conf.get("hashrateTarget") is not None: return cls.hashrate_tuning( - int(tuner_conf["hashrateTarget"]["terahashPerSecond"]), + hashrate=int(tuner_conf["hashrateTarget"]["terahashPerSecond"]), scaling=ScalingConfig.from_boser(grpc_miner_conf, mode="hashrate"), ) @@ -617,9 +610,9 @@ class MiningModeConfig(MinerConfigOption): if mode_data.get("Mode") == "turbo": return cls.high() if mode_data.get("Ths") is not None: - return cls.hashrate_tuning(mode_data["Ths"]) + return cls.hashrate_tuning(hashrate=mode_data["Ths"]) if mode_data.get("Power") is not None: - return cls.power_tuning(mode_data["Power"]) + return cls.power_tuning(power=mode_data["Power"]) except LookupError: return cls.default() @@ -647,3 +640,6 @@ class MiningModeConfig(MinerConfigOption): except LookupError: pass return cls.default() + + +MiningMode = TypeVar("MiningMode", bound=Union[*[v.value for v in MiningModeConfig]]) diff --git a/pyasic/config/mining/algo.py b/pyasic/config/mining/algo.py index 96021b8f..46c7c294 100644 --- a/pyasic/config/mining/algo.py +++ b/pyasic/config/mining/algo.py @@ -1,11 +1,11 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import TypeVar, Union from pyasic.config.base import MinerConfigOption, MinerConfigValue -@dataclass class StandardTuneAlgo(MinerConfigValue): mode: str = field(init=False, default="standard") @@ -13,7 +13,6 @@ class StandardTuneAlgo(MinerConfigValue): return VOptAlgo().as_epic() -@dataclass class VOptAlgo(MinerConfigValue): mode: str = field(init=False, default="voltage_optimizer") @@ -21,7 +20,6 @@ class VOptAlgo(MinerConfigValue): return "VoltageOptimizer" -@dataclass class BoardTuneAlgo(MinerConfigValue): mode: str = field(init=False, default="board_tune") @@ -29,7 +27,6 @@ class BoardTuneAlgo(MinerConfigValue): return "BoardTune" -@dataclass class ChipTuneAlgo(MinerConfigValue): mode: str = field(init=False, default="chip_tune") @@ -37,7 +34,6 @@ class ChipTuneAlgo(MinerConfigValue): return "ChipTune" -@dataclass class TunerAlgo(MinerConfigOption): standard = StandardTuneAlgo voltage_optimizer = VOptAlgo @@ -45,11 +41,11 @@ class TunerAlgo(MinerConfigOption): chip_tune = ChipTuneAlgo @classmethod - def default(cls): + def default(cls) -> TunerAlgoType: return cls.standard() @classmethod - def from_dict(cls, dict_conf: dict | None): + def from_dict(cls, dict_conf: dict | None) -> TunerAlgoType: mode = dict_conf.get("mode") if mode is None: return cls.default() @@ -57,3 +53,6 @@ class TunerAlgo(MinerConfigOption): cls_attr = getattr(cls, mode) if cls_attr is not None: return cls_attr().from_dict(dict_conf) + + +TunerAlgoType = TypeVar("TunerAlgoType", bound=Union[*[v.value for v in TunerAlgo]]) diff --git a/pyasic/config/mining/scaling.py b/pyasic/config/mining/scaling.py index e73b5ea0..58093b91 100644 --- a/pyasic/config/mining/scaling.py +++ b/pyasic/config/mining/scaling.py @@ -20,7 +20,6 @@ from dataclasses import dataclass from pyasic.config.base import MinerConfigValue -@dataclass class ScalingShutdown(MinerConfigValue): enabled: bool = False duration: int = None @@ -35,7 +34,9 @@ class ScalingShutdown(MinerConfigValue): def from_bosminer(cls, power_scaling_conf: dict): sd_enabled = power_scaling_conf.get("shutdown_enabled") if sd_enabled is not None: - return cls(sd_enabled, power_scaling_conf.get("shutdown_duration")) + return cls( + enabled=sd_enabled, duration=power_scaling_conf.get("shutdown_duration") + ) return None @classmethod @@ -43,9 +44,12 @@ class ScalingShutdown(MinerConfigValue): sd_enabled = power_scaling_conf.get("shutdownEnabled") if sd_enabled is not None: try: - return cls(sd_enabled, power_scaling_conf["shutdownDuration"]["hours"]) + return cls( + enabled=sd_enabled, + duration=power_scaling_conf["shutdownDuration"]["hours"], + ) except KeyError: - return cls(sd_enabled) + return cls(enabled=sd_enabled) return None def as_bosminer(self) -> dict: @@ -60,7 +64,6 @@ class ScalingShutdown(MinerConfigValue): return {"enable_shutdown": self.enabled, "shutdown_duration": self.duration} -@dataclass class ScalingConfig(MinerConfigValue): step: int = None minimum: int = None diff --git a/pyasic/config/pools.py b/pyasic/config/pools.py index 3675f17b..68d3c7fc 100644 --- a/pyasic/config/pools.py +++ b/pyasic/config/pools.py @@ -17,9 +17,10 @@ from __future__ import annotations import random import string -from dataclasses import dataclass, field from typing import List +from pydantic import Field + from pyasic.config.base import MinerConfigValue from pyasic.web.braiins_os.proto.braiins.bos.v1 import ( PoolConfiguration, @@ -30,7 +31,6 @@ from pyasic.web.braiins_os.proto.braiins.bos.v1 import ( ) -@dataclass class Pool(MinerConfigValue): url: str user: str @@ -235,11 +235,10 @@ class Pool(MinerConfigValue): ) -@dataclass class PoolGroup(MinerConfigValue): - pools: list[Pool] = field(default_factory=list) + pools: list[Pool] = Field(default_factory=list) quota: int = 1 - name: str = None + name: str | None = None def __post_init__(self): if self.name is None: @@ -254,7 +253,7 @@ class PoolGroup(MinerConfigValue): if len(self.pools) > idx: pools.append(self.pools[idx].as_am_modern(user_suffix=user_suffix)) else: - pools.append(Pool("", "", "").as_am_modern()) + pools.append(Pool(url="", user="", password="").as_am_modern()) idx += 1 return pools @@ -267,7 +266,7 @@ class PoolGroup(MinerConfigValue): **self.pools[idx].as_wm(idx=idx + 1, user_suffix=user_suffix) ) else: - pools.update(**Pool("", "", "").as_wm(idx=idx + 1)) + pools.update(**Pool(url="", user="", password="").as_wm(idx=idx + 1)) idx += 1 return pools @@ -280,7 +279,9 @@ class PoolGroup(MinerConfigValue): **self.pools[idx].as_am_old(idx=idx + 1, user_suffix=user_suffix) ) else: - pools.update(**Pool("", "", "").as_am_old(idx=idx + 1)) + pools.update( + **Pool(url="", user="", password="").as_am_old(idx=idx + 1) + ) idx += 1 return pools @@ -290,7 +291,7 @@ class PoolGroup(MinerConfigValue): def as_avalon(self, user_suffix: str = None) -> str: if len(self.pools) > 0: return self.pools[0].as_avalon(user_suffix=user_suffix) - return Pool("", "", "").as_avalon() + return Pool(url="", user="", password="").as_avalon() def as_inno(self, user_suffix: str = None) -> dict: pools = {} @@ -301,7 +302,7 @@ class PoolGroup(MinerConfigValue): **self.pools[idx].as_inno(idx=idx + 1, user_suffix=user_suffix) ) else: - pools.update(**Pool("", "", "").as_inno(idx=idx + 1)) + pools.update(**Pool(url="", user="", password="").as_inno(idx=idx + 1)) idx += 1 return pools @@ -371,11 +372,11 @@ class PoolGroup(MinerConfigValue): @classmethod def from_goldshell(cls, web_pools: list) -> "PoolGroup": - return cls([Pool.from_goldshell(p) for p in web_pools]) + return cls(pools=[Pool.from_goldshell(p) for p in web_pools]) @classmethod def from_inno(cls, web_pools: list) -> "PoolGroup": - return cls([Pool.from_inno(p) for p in web_pools]) + return cls(pools=[Pool.from_inno(p) for p in web_pools]) @classmethod def from_bosminer(cls, toml_group_conf: dict) -> "PoolGroup": @@ -389,7 +390,7 @@ class PoolGroup(MinerConfigValue): @classmethod def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup": - return cls([Pool.from_vnish(p) for p in web_settings_pools]) + return cls(pools=[Pool.from_vnish(p) for p in web_settings_pools]) @classmethod def from_boser(cls, grpc_pool_group: dict) -> "PoolGroup": @@ -424,9 +425,8 @@ class PoolGroup(MinerConfigValue): ) -@dataclass class PoolConfig(MinerConfigValue): - groups: List[PoolGroup] = field(default_factory=list) + groups: List[PoolGroup] = Field(default_factory=list) @classmethod def default(cls) -> "PoolConfig": @@ -538,38 +538,38 @@ class PoolConfig(MinerConfigValue): return PoolConfig.default() pool_data = sorted(pool_data, key=lambda x: int(x["POOL"])) - return cls([PoolGroup.from_api(pool_data)]) + return cls(groups=[PoolGroup.from_api(pool_data)]) @classmethod def from_epic(cls, web_conf: dict) -> "PoolConfig": pool_data = web_conf["StratumConfigs"] - return cls([PoolGroup.from_epic(pool_data)]) + return cls(groups=[PoolGroup.from_epic(pool_data)]) @classmethod def from_am_modern(cls, web_conf: dict) -> "PoolConfig": pool_data = web_conf["pools"] - return cls([PoolGroup.from_am_modern(pool_data)]) + return cls(groups=[PoolGroup.from_am_modern(pool_data)]) @classmethod def from_goldshell(cls, web_pools: list) -> "PoolConfig": - return cls([PoolGroup.from_goldshell(web_pools)]) + return cls(groups=[PoolGroup.from_goldshell(web_pools)]) @classmethod def from_inno(cls, web_pools: list) -> "PoolConfig": - return cls([PoolGroup.from_inno(web_pools)]) + return cls(groups=[PoolGroup.from_inno(web_pools)]) @classmethod def from_bosminer(cls, toml_conf: dict) -> "PoolConfig": if toml_conf.get("group") is None: return cls() - return cls([PoolGroup.from_bosminer(g) for g in toml_conf["group"]]) + return cls(groups=[PoolGroup.from_bosminer(g) for g in toml_conf["group"]]) @classmethod def from_vnish(cls, web_settings: dict) -> "PoolConfig": try: - return cls([PoolGroup.from_vnish(web_settings["miner"]["pools"])]) + return cls(groups=[PoolGroup.from_vnish(web_settings["miner"]["pools"])]) except LookupError: return cls() diff --git a/pyasic/config/temperature.py b/pyasic/config/temperature.py index 0f024e5d..94fec61e 100644 --- a/pyasic/config/temperature.py +++ b/pyasic/config/temperature.py @@ -20,11 +20,10 @@ from dataclasses import dataclass from pyasic.config.base import MinerConfigValue -@dataclass class TemperatureConfig(MinerConfigValue): - target: int = None - hot: int = None - danger: int = None + target: int | None = None + hot: int | None = None + danger: int | None = None @classmethod def default(cls): diff --git a/pyasic/data/__init__.py b/pyasic/data/__init__.py index 2cb5fa38..b66f2494 100644 --- a/pyasic/data/__init__.py +++ b/pyasic/data/__init__.py @@ -15,12 +15,12 @@ # ------------------------------------------------------------------------------ import copy -import json import time -from dataclasses import asdict, dataclass, field, fields from datetime import datetime, timezone from typing import Any, List, Union +from pydantic import BaseModel, Field, computed_field, field_serializer + from pyasic.config import MinerConfig from pyasic.config.mining import MiningModePowerTune from pyasic.data.pools import PoolMetrics, Scheme @@ -29,11 +29,10 @@ from .boards import HashBoard from .device import DeviceInfo from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error from .fans import Fan -from .hashrate import AlgoHashRate, HashUnit +from .hashrate import AlgoHashRate, AlgoHashRateType, HashUnit -@dataclass -class MinerData: +class MinerData(BaseModel): """A Dataclass to standardize data returned from miners (specifically `AnyMiner().get_data()`) Attributes: @@ -77,58 +76,44 @@ class MinerData: # general ip: str - _datetime: datetime = field(repr=False, default=None) - datetime: str = field(init=False) - timestamp: int = field(init=False) + raw_datetime: datetime = Field( + exclude=True, default_factory=datetime.now(timezone.utc).astimezone, repr=False + ) # about - device_info: DeviceInfo = None - make: str = field(init=False) - model: str = field(init=False) - firmware: str = field(init=False) - algo: str = field(init=False) - mac: str = None - api_ver: str = None - fw_ver: str = None - hostname: str = None + device_info: DeviceInfo | None = None + mac: str | None = None + api_ver: str | None = None + fw_ver: str | None = None + hostname: str | None = None # hashrate - hashrate: AlgoHashRate = field(init=False) - _hashrate: AlgoHashRate = field(repr=False, default=None) + raw_hashrate: AlgoHashRateType = Field(exclude=True, default=None, repr=False) # expected - expected_hashrate: float = None - expected_hashboards: int = None - expected_chips: int = None - expected_fans: int = None - - # % expected - percent_expected_chips: float = field(init=False) - percent_expected_hashrate: float = field(init=False) - percent_expected_wattage: float = field(init=False) + expected_hashrate: AlgoHashRateType | None = None + expected_hashboards: int | None = None + expected_chips: int | None = None + expected_fans: int | None = None # temperature - temperature_avg: int = field(init=False) - env_temp: float = None + env_temp: int | None = None # power - wattage: int = None - wattage_limit: int = field(init=False) - voltage: float = None - _wattage_limit: int = field(repr=False, default=None) + wattage: int | None = None + voltage: float | None = None + raw_wattage_limit: int | None = Field(exclude=True, default=None, repr=False) # fans - fans: List[Fan] = field(default_factory=list) - fan_psu: int = None + fans: List[Fan] = Field(default_factory=list) + fan_psu: int | None = None # boards - hashboards: List[HashBoard] = field(default_factory=list) - total_chips: int = field(init=False) - nominal: bool = field(init=False) + hashboards: List[HashBoard] = Field(default_factory=list) # config - config: MinerConfig = None - fault_light: Union[bool, None] = None + config: MinerConfig | None = None + fault_light: bool | None = None # errors errors: List[ @@ -138,30 +123,21 @@ class MinerData: X19Error, InnosiliconError, ] - ] = field(default_factory=list) + ] = Field(default_factory=list) # mining state is_mining: bool = True - uptime: int = None - efficiency: int = field(init=False) + uptime: int | None = None # pools - pools: list[PoolMetrics] = field(default_factory=list) + pools: list[PoolMetrics] = Field(default_factory=list) @classmethod def fields(cls): - return [f.name for f in fields(cls) if not f.name.startswith("_")] - - @staticmethod - def dict_factory(x): - return { - k: v.value if isinstance(v, Scheme) else v - for (k, v) in x - if not k.startswith("_") - } + return list(cls.model_fields.keys()) def __post_init__(self): - self._datetime = datetime.now(timezone.utc).astimezone() + self.raw_datetime = datetime.now(timezone.utc).astimezone() def get(self, __key: str, default: Any = None): try: @@ -189,19 +165,19 @@ class MinerData: def __floordiv__(self, other): cp = copy.deepcopy(self) - for key in self: + for key in self.fields(): item = getattr(self, key) if isinstance(item, int): setattr(cp, key, item // other) if isinstance(item, float): - setattr(cp, key, round(item / other, 2)) + setattr(cp, key, item / other) return cp def __add__(self, other): if not isinstance(other, MinerData): raise TypeError("Cannot add MinerData to non MinerData type.") cp = copy.deepcopy(self) - for key in self: + for key in self.fields(): item = getattr(self, key) other_item = getattr(other, key) if item is None: @@ -221,34 +197,49 @@ class MinerData: setattr(cp, key, item & other_item) return cp + @computed_field # type: ignore[misc] @property - def hashrate(self): # noqa - Skip PyCharm inspection + def hashrate(self) -> AlgoHashRateType: if len(self.hashboards) > 0: hr_data = [] for item in self.hashboards: if item.hashrate is not None: hr_data.append(item.hashrate) if len(hr_data) > 0: - return sum(hr_data, start=type(hr_data[0])(0)) - return self._hashrate + return sum(hr_data, start=type(hr_data[0])(rate=0)) + return self.raw_hashrate + + @field_serializer("hashrate") + def serialize_hashrate(self, hashrate: AlgoHashRateType | None) -> float: + if hashrate is not None: + return float(hashrate) + + @field_serializer("expected_hashrate") + def serialize_expected_hashrate( + self, expected_hashrate: AlgoHashRateType | None, _info + ) -> float: + if expected_hashrate is not None: + return float(expected_hashrate) @hashrate.setter def hashrate(self, val): - self._hashrate = val + self.raw_hashrate = val + @computed_field # type: ignore[misc] @property - def wattage_limit(self): # noqa - Skip PyCharm inspection + def wattage_limit(self) -> int: if self.config is not None: if isinstance(self.config.mining_mode, MiningModePowerTune): return self.config.mining_mode.power - return self._wattage_limit + return self.raw_wattage_limit @wattage_limit.setter def wattage_limit(self, val: int): - self._wattage_limit = val + self.raw_wattage_limit = val + @computed_field # type: ignore[misc] @property - def total_chips(self): # noqa - Skip PyCharm inspection + def total_chips(self) -> int | None: if len(self.hashboards) > 0: chip_data = [] for item in self.hashboards: @@ -258,34 +249,25 @@ class MinerData: return sum(chip_data) return None - @total_chips.setter - def total_chips(self, val): - pass - + @computed_field # type: ignore[misc] @property - def nominal(self): # noqa - Skip PyCharm inspection + 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 - @nominal.setter - def nominal(self, val): - pass - + @computed_field # type: ignore[misc] @property - def percent_expected_chips(self): # noqa - Skip PyCharm inspection + def percent_expected_chips(self) -> int | None: if self.total_chips is None or self.expected_chips is None: return None if self.total_chips == 0 or self.expected_chips == 0: return 0 return round((self.total_chips / self.expected_chips) * 100) - @percent_expected_chips.setter - def percent_expected_chips(self, val): - pass - + @computed_field # type: ignore[misc] @property - def percent_expected_hashrate(self): # noqa - Skip PyCharm inspection + def percent_expected_hashrate(self) -> int | None: if self.hashrate is None or self.expected_hashrate is None: return None try: @@ -293,12 +275,9 @@ class MinerData: except ZeroDivisionError: return 0 - @percent_expected_hashrate.setter - def percent_expected_hashrate(self, val): - pass - + @computed_field # type: ignore[misc] @property - def percent_expected_wattage(self): # noqa - Skip PyCharm inspection + def percent_expected_wattage(self) -> int | None: if self.wattage_limit is None or self.wattage is None: return None try: @@ -306,12 +285,9 @@ class MinerData: except ZeroDivisionError: return 0 - @percent_expected_wattage.setter - def percent_expected_wattage(self, val): - pass - + @computed_field # type: ignore[misc] @property - def temperature_avg(self): # noqa - Skip PyCharm inspection + def temperature_avg(self) -> int | None: total_temp = 0 temp_count = 0 for hb in self.hashboards: @@ -322,12 +298,9 @@ class MinerData: return None return round(total_temp / temp_count) - @temperature_avg.setter - def temperature_avg(self, val): - pass - + @computed_field # type: ignore[misc] @property - def efficiency(self): # noqa - Skip PyCharm inspection + def efficiency(self) -> int | None: if self.hashrate is None or self.wattage is None: return None try: @@ -335,67 +308,45 @@ class MinerData: except ZeroDivisionError: return 0 - @efficiency.setter - def efficiency(self, val): - pass - + @computed_field # type: ignore[misc] @property - def datetime(self): # noqa - Skip PyCharm inspection - return self._datetime.isoformat() - - @datetime.setter - def datetime(self, val): - pass + def datetime(self) -> str: + return self.raw_datetime.isoformat() + @computed_field # type: ignore[misc] @property - def timestamp(self): # noqa - Skip PyCharm inspection - return int(time.mktime(self._datetime.timetuple())) - - @timestamp.setter - def timestamp(self, val): - pass + def timestamp(self) -> int: + return int(time.mktime(self.raw_datetime.timetuple())) + @computed_field # type: ignore[misc] @property - def make(self): # noqa - Skip PyCharm inspection + def make(self) -> str: if self.device_info.make is not None: return str(self.device_info.make) - @make.setter - def make(self, val): - pass - + @computed_field # type: ignore[misc] @property - def model(self): # noqa - Skip PyCharm inspection + def model(self) -> str: if self.device_info.model is not None: return str(self.device_info.model) - @model.setter - def model(self, val): - pass - + @computed_field # type: ignore[misc] @property - def firmware(self): # noqa - Skip PyCharm inspection + def firmware(self) -> str: if self.device_info.firmware is not None: return str(self.device_info.firmware) - @firmware.setter - def firmware(self, val): - pass - + @computed_field # type: ignore[misc] @property - def algo(self): # noqa - Skip PyCharm inspection + def algo(self) -> str: if self.device_info.algo is not None: return str(self.device_info.algo) - @algo.setter - def algo(self, val): - pass - def keys(self) -> list: - return [f.name for f in fields(self)] + return list(self.model_fields.keys()) def asdict(self) -> dict: - return asdict(self, dict_factory=self.dict_factory) + return self.model_dump() def as_dict(self) -> dict: """Get this dataclass as a dictionary. @@ -411,7 +362,7 @@ class MinerData: Returns: A JSON version of this class. """ - return json.dumps(self.as_dict()) + return self.model_dump_json() def as_csv(self) -> str: """Get this dataclass as CSV. @@ -440,7 +391,7 @@ class MinerData: field_data = [] tags = ["ip", "mac", "model", "hostname"] - for attribute in self: + for attribute in self.fields(): if attribute in tags: escaped_data = self.get(attribute, "Unknown").replace(" ", "\\ ") tag_data.append(f"{attribute}={escaped_data}") diff --git a/pyasic/data/boards.py b/pyasic/data/boards.py index 4b22f21b..6c22504f 100644 --- a/pyasic/data/boards.py +++ b/pyasic/data/boards.py @@ -14,14 +14,14 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from dataclasses import dataclass from typing import Any -from .hashrate import AlgoHashRate +from pydantic import BaseModel, field_serializer + +from .hashrate import AlgoHashRateType -@dataclass -class HashBoard: +class HashBoard(BaseModel): """A Dataclass to standardize hashboard data. Attributes: @@ -39,16 +39,21 @@ class HashBoard: """ slot: int = 0 - hashrate: AlgoHashRate = None - temp: int = None - chip_temp: int = None - chips: int = None - expected_chips: int = None - serial_number: str = None + hashrate: AlgoHashRateType | None = None + temp: int | None = None + chip_temp: int | None = None + chips: int | None = None + expected_chips: int | None = None + serial_number: str | None = None missing: bool = True - tuned: bool = None - active: bool = None - voltage: float = None + tuned: bool | None = None + active: bool | None = None + voltage: float | None = None + + @field_serializer("hashrate") + def serialize_hashrate(self, hashrate: AlgoHashRateType | None) -> float: + if hashrate is not None: + return float(hashrate) def get(self, __key: str, default: Any = None): try: diff --git a/pyasic/data/device.py b/pyasic/data/device.py index 3a1edb90..fe586c7c 100644 --- a/pyasic/data/device.py +++ b/pyasic/data/device.py @@ -1,14 +1,31 @@ -from dataclasses import dataclass +from pydantic import BaseModel, ConfigDict, field_serializer -from pyasic.device.algorithm import MinerAlgo +from pyasic.device.algorithm import MinerAlgoType from pyasic.device.firmware import MinerFirmware from pyasic.device.makes import MinerMake -from pyasic.device.models import MinerModel +from pyasic.device.models import MinerModelType -@dataclass -class DeviceInfo: - make: MinerMake = None - model: MinerModel = None - firmware: MinerFirmware = None - algo: MinerAlgo = None +class DeviceInfo(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + make: MinerMake | None = None + model: MinerModelType | None = None + firmware: MinerFirmware | None = None + algo: MinerAlgoType | None = None + + @field_serializer("make") + def serialize_make(self, make: MinerMake, _info): + return str(make) + + @field_serializer("model") + def serialize_model(self, model: MinerModelType, _info): + return str(model) + + @field_serializer("firmware") + def serialize_firmware(self, firmware: MinerFirmware, _info): + return str(firmware) + + @field_serializer("algo") + def serialize_algo(self, algo: MinerAlgoType, _info): + return str(algo) diff --git a/pyasic/data/error_codes/X19.py b/pyasic/data/error_codes/X19.py index c4fda8f9..7fe4595c 100644 --- a/pyasic/data/error_codes/X19.py +++ b/pyasic/data/error_codes/X19.py @@ -13,12 +13,10 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ - -from dataclasses import asdict, dataclass, fields +from pyasic.data.error_codes.base import BaseMinerError -@dataclass -class X19Error: +class X19Error(BaseMinerError): """A Dataclass to handle error codes of X19 miners. Attributes: @@ -28,10 +26,3 @@ class X19Error: error_message: str error_code: int = 0 - - @classmethod - def fields(cls): - return fields(cls) - - def asdict(self): - return asdict(self) diff --git a/pyasic/data/error_codes/base.py b/pyasic/data/error_codes/base.py new file mode 100644 index 00000000..1ba7aacc --- /dev/null +++ b/pyasic/data/error_codes/base.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + + +class BaseMinerError(BaseModel): + @classmethod + def fields(cls): + return list(cls.model_fields.keys()) + + def asdict(self) -> dict: + return self.model_dump() + + def as_dict(self) -> dict: + """Get this dataclass as a dictionary. + + Returns: + A dictionary version of this class. + """ + return self.asdict() diff --git a/pyasic/data/error_codes/bos.py b/pyasic/data/error_codes/bos.py index bf069ae9..1d8c6030 100644 --- a/pyasic/data/error_codes/bos.py +++ b/pyasic/data/error_codes/bos.py @@ -14,11 +14,10 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from dataclasses import asdict, dataclass, fields +from pyasic.data.error_codes.base import BaseMinerError -@dataclass -class BraiinsOSError: +class BraiinsOSError(BaseMinerError): """A Dataclass to handle error codes of BraiinsOS+ miners. Attributes: @@ -28,10 +27,3 @@ class BraiinsOSError: error_message: str error_code: int = 0 - - @classmethod - def fields(cls): - return fields(cls) - - def asdict(self): - return asdict(self) diff --git a/pyasic/data/error_codes/innosilicon.py b/pyasic/data/error_codes/innosilicon.py index d2f002f6..3f969350 100644 --- a/pyasic/data/error_codes/innosilicon.py +++ b/pyasic/data/error_codes/innosilicon.py @@ -14,11 +14,13 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from dataclasses import asdict, dataclass, field, fields + +from pydantic import computed_field + +from pyasic.data.error_codes.base import BaseMinerError -@dataclass -class InnosiliconError: +class InnosiliconError(BaseMinerError): """A Dataclass to handle error codes of Innosilicon miners. Attributes: @@ -27,25 +29,14 @@ class InnosiliconError: """ error_code: int - error_message: str = field(init=False) - - @classmethod - def fields(cls): - return fields(cls) + @computed_field # type: ignore[misc] @property - def error_message(self): # noqa - Skip PyCharm inspection + def error_message(self) -> str: # noqa - Skip PyCharm inspection if self.error_code in ERROR_CODES: return ERROR_CODES[self.error_code] return "Unknown error type." - @error_message.setter - def error_message(self, val): - pass - - def asdict(self): - return asdict(self) - ERROR_CODES = { 21: "The PLUG signal of the hash board is not detected.", diff --git a/pyasic/data/error_codes/whatsminer.py b/pyasic/data/error_codes/whatsminer.py index 20da875f..5ae16974 100644 --- a/pyasic/data/error_codes/whatsminer.py +++ b/pyasic/data/error_codes/whatsminer.py @@ -13,12 +13,12 @@ # See the License for the specific language governing permissions and - # limitations under the License. - # ------------------------------------------------------------------------------ +from pydantic import computed_field -from dataclasses import asdict, dataclass, field, fields +from pyasic.data.error_codes.base import BaseMinerError -@dataclass -class WhatsminerError: +class WhatsminerError(BaseMinerError): """A Dataclass to handle error codes of Whatsminers. Attributes: @@ -27,14 +27,10 @@ class WhatsminerError: """ error_code: int - error_message: str = field(init=False) - - @classmethod - def fields(cls): - return fields(cls) + @computed_field # type: ignore[misc] @property - def error_message(self): # noqa - Skip PyCharm inspection + def error_message(self) -> str: # noqa - Skip PyCharm inspection if len(str(self.error_code)) == 6 and not str(self.error_code)[:1] == "1": err_type = int(str(self.error_code)[:2]) err_subtype = int(str(self.error_code)[2:3]) @@ -74,13 +70,6 @@ class WhatsminerError: except KeyError: return "Unknown error type." - @error_message.setter - def error_message(self, val): - pass - - def asdict(self): - return asdict(self) - ERROR_CODES = { 1: { # Fan error diff --git a/pyasic/data/fans.py b/pyasic/data/fans.py index cbe76783..73f6e77a 100644 --- a/pyasic/data/fans.py +++ b/pyasic/data/fans.py @@ -14,12 +14,12 @@ # limitations under the License. - # ------------------------------------------------------------------------------ -from dataclasses import dataclass from typing import Any +from pydantic import BaseModel -@dataclass -class Fan: + +class Fan(BaseModel): """A Dataclass to standardize fan data. Attributes: diff --git a/pyasic/data/hashrate/__init__.py b/pyasic/data/hashrate/__init__.py index 5a8f2e12..e6e00ad7 100644 --- a/pyasic/data/hashrate/__init__.py +++ b/pyasic/data/hashrate/__init__.py @@ -1,5 +1,6 @@ from enum import Enum +from pyasic.data.hashrate.base import AlgoHashRateType from pyasic.data.hashrate.sha256 import SHA256HashRate from pyasic.device.algorithm.sha256 import SHA256Unit diff --git a/pyasic/data/hashrate/base.py b/pyasic/data/hashrate/base.py new file mode 100644 index 00000000..c0af955a --- /dev/null +++ b/pyasic/data/hashrate/base.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class AlgoHashRateType(BaseModel): + pass diff --git a/pyasic/data/hashrate/sha256.py b/pyasic/data/hashrate/sha256.py index 722b222e..be997a72 100644 --- a/pyasic/data/hashrate/sha256.py +++ b/pyasic/data/hashrate/sha256.py @@ -1,13 +1,11 @@ from __future__ import annotations -from dataclasses import dataclass - +from pyasic.data.hashrate.base import AlgoHashRateType from pyasic.device.algorithm import MinerAlgo from pyasic.device.algorithm.sha256 import SHA256Unit -@dataclass -class SHA256HashRate: +class SHA256HashRate(AlgoHashRateType): rate: float unit: SHA256Unit = MinerAlgo.SHA256.unit.default @@ -25,28 +23,38 @@ class SHA256HashRate: def __add__(self, other: SHA256HashRate | int | float) -> SHA256HashRate: if isinstance(other, SHA256HashRate): - return SHA256HashRate(self.rate + other.into(self.unit).rate, self.unit) - return SHA256HashRate(self.rate + other, self.unit) + return SHA256HashRate( + rate=self.rate + other.into(self.unit).rate, unit=self.unit + ) + return SHA256HashRate(rate=self.rate + other, unit=self.unit) def __sub__(self, other: SHA256HashRate | int | float) -> SHA256HashRate: if isinstance(other, SHA256HashRate): - return SHA256HashRate(self.rate - other.into(self.unit).rate, self.unit) - return SHA256HashRate(self.rate - other, self.unit) + return SHA256HashRate( + rate=self.rate - other.into(self.unit).rate, unit=self.unit + ) + return SHA256HashRate(rate=self.rate - other, unit=self.unit) def __truediv__(self, other: SHA256HashRate | int | float): if isinstance(other, SHA256HashRate): - return SHA256HashRate(self.rate / other.into(self.unit).rate, self.unit) - return SHA256HashRate(self.rate / other, self.unit) + return SHA256HashRate( + rate=self.rate / other.into(self.unit).rate, unit=self.unit + ) + return SHA256HashRate(rate=self.rate / other, unit=self.unit) def __floordiv__(self, other: SHA256HashRate | int | float): if isinstance(other, SHA256HashRate): - return SHA256HashRate(self.rate // other.into(self.unit).rate, self.unit) - return SHA256HashRate(self.rate // other, self.unit) + return SHA256HashRate( + rate=self.rate // other.into(self.unit).rate, unit=self.unit + ) + return SHA256HashRate(rate=self.rate // other, unit=self.unit) def __mul__(self, other: SHA256HashRate | int | float): if isinstance(other, SHA256HashRate): - return SHA256HashRate(self.rate * other.into(self.unit).rate, self.unit) - return SHA256HashRate(self.rate * other, self.unit) + return SHA256HashRate( + rate=self.rate * other.into(self.unit).rate, unit=self.unit + ) + return SHA256HashRate(rate=self.rate * other, unit=self.unit) def into(self, other: SHA256Unit) -> SHA256HashRate: return SHA256HashRate( diff --git a/pyasic/data/pools.py b/pyasic/data/pools.py index 6e272260..ae61846f 100644 --- a/pyasic/data/pools.py +++ b/pyasic/data/pools.py @@ -1,8 +1,9 @@ -from dataclasses import dataclass, field from enum import Enum from typing import Optional from urllib.parse import urlparse +from pydantic import BaseModel, computed_field, model_serializer + class Scheme(Enum): STRATUM_V1 = "stratum+tcp" @@ -10,13 +11,16 @@ class Scheme(Enum): STRATUM_V1_SSL = "stratum+ssl" -@dataclass -class PoolUrl: +class PoolUrl(BaseModel): scheme: Scheme host: str port: int pubkey: Optional[str] = None + @model_serializer + def serialize(self): + return str(self) + def __str__(self) -> str: if self.scheme == Scheme.STRATUM_V2 and self.pubkey: return f"{self.scheme.value}://{self.host}:{self.port}/{self.pubkey}" @@ -36,8 +40,7 @@ class PoolUrl: return cls(scheme=scheme, host=host, port=port, pubkey=pubkey) -@dataclass -class PoolMetrics: +class PoolMetrics(BaseModel): """A dataclass to standardize pool metrics returned from miners. Attributes: @@ -63,18 +66,14 @@ class PoolMetrics: alive: bool = None index: int = None user: str = None - pool_rejected_percent: float = field(init=False) - pool_stale_percent: float = field(init=False) + @computed_field # type: ignore[misc] @property def pool_rejected_percent(self) -> float: # noqa - Skip PyCharm inspection """Calculate and return the percentage of rejected shares""" return self._calculate_percentage(self.rejected, self.accepted + self.rejected) - @pool_rejected_percent.setter - def pool_rejected_percent(self, val): - pass - + @computed_field # type: ignore[misc] @property def pool_stale_percent(self) -> float: # noqa - Skip PyCharm inspection """Calculate and return the percentage of stale shares.""" @@ -82,10 +81,6 @@ class PoolMetrics: self.get_failures, self.accepted + self.rejected ) - @pool_stale_percent.setter - def pool_stale_percent(self, val): - pass - @staticmethod def _calculate_percentage(value: int, total: int) -> float: """Calculate the percentage.""" diff --git a/pyasic/device/algorithm/__init__.py b/pyasic/device/algorithm/__init__.py index d9c85521..5be570a6 100644 --- a/pyasic/device/algorithm/__init__.py +++ b/pyasic/device/algorithm/__init__.py @@ -1,3 +1,4 @@ +from pyasic.device.algorithm.base import MinerAlgoType from pyasic.device.algorithm.sha256 import SHA256Algo diff --git a/pyasic/device/algorithm/base.py b/pyasic/device/algorithm/base.py new file mode 100644 index 00000000..b70ba459 --- /dev/null +++ b/pyasic/device/algorithm/base.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from enum import IntEnum + + +class MinerAlgoType(str): + pass diff --git a/pyasic/device/algorithm/sha256.py b/pyasic/device/algorithm/sha256.py index 5bb3614d..bd43f7bb 100644 --- a/pyasic/device/algorithm/sha256.py +++ b/pyasic/device/algorithm/sha256.py @@ -2,6 +2,8 @@ from __future__ import annotations from enum import IntEnum +from .base import MinerAlgoType + class SHA256Unit(IntEnum): H = 1 @@ -58,7 +60,7 @@ class SHA256Unit(IntEnum): # make this json serializable -class _SHA256Algo(str): +class _SHA256Algo(MinerAlgoType): unit = SHA256Unit def __repr__(self): diff --git a/pyasic/device/models.py b/pyasic/device/models.py index 573174c2..82a32b56 100644 --- a/pyasic/device/models.py +++ b/pyasic/device/models.py @@ -1,7 +1,11 @@ from enum import Enum -class AntminerModels(str, Enum): +class MinerModelType(str, Enum): + pass + + +class AntminerModels(MinerModelType): D3 = "D3" HS3 = "HS3" L3Plus = "L3+" @@ -58,7 +62,7 @@ class AntminerModels(str, Enum): return self.value -class WhatsminerModels(str, Enum): +class WhatsminerModels(MinerModelType): M20V10 = "M20 V10" M20SV10 = "M20S V10" M20SV20 = "M20S V20" @@ -279,7 +283,7 @@ class WhatsminerModels(str, Enum): return self.value -class AvalonminerModels(str, Enum): +class AvalonminerModels(MinerModelType): Avalon721 = "Avalon 721" Avalon741 = "Avalon 741" Avalon761 = "Avalon 761" @@ -298,7 +302,7 @@ class AvalonminerModels(str, Enum): return self.value -class InnosiliconModels(str, Enum): +class InnosiliconModels(MinerModelType): T3HPlus = "T3H+" A10X = "A10X" A11 = "A11" @@ -308,7 +312,7 @@ class InnosiliconModels(str, Enum): return self.value -class GoldshellModels(str, Enum): +class GoldshellModels(MinerModelType): CK5 = "CK5" HS5 = "HS5" KD5 = "KD5" @@ -320,7 +324,7 @@ class GoldshellModels(str, Enum): return self.value -class ePICModels(str, Enum): +class ePICModels(MinerModelType): BM520i = "BlockMiner 520i" BM720i = "BlockMiner 720i" @@ -328,7 +332,7 @@ class ePICModels(str, Enum): return self.value -class AuradineModels(str, Enum): +class AuradineModels(MinerModelType): AT1500 = "AT1500" AT2860 = "AT2860" AT2880 = "AT2880" @@ -341,7 +345,7 @@ class AuradineModels(str, Enum): return self.value -class BitAxeModels(str, Enum): +class BitAxeModels(MinerModelType): BM1366 = "Ultra" BM1368 = "Supra" BM1397 = "Max" @@ -351,7 +355,7 @@ class BitAxeModels(str, Enum): return self.value -class IceRiverModels(str, Enum): +class IceRiverModels(MinerModelType): KS0 = "KS0" KS1 = "KS1" KS2 = "KS2" @@ -366,7 +370,7 @@ class IceRiverModels(str, Enum): return self.value -class HammerModels(str, Enum): +class HammerModels(MinerModelType): D10 = "D10" def __str__(self): diff --git a/pyasic/miners/antminer/hiveon/X9/T9.py b/pyasic/miners/antminer/hiveon/X9/T9.py index f5b0ad03..0e1ab451 100644 --- a/pyasic/miners/antminer/hiveon/X9/T9.py +++ b/pyasic/miners/antminer/hiveon/X9/T9.py @@ -126,7 +126,7 @@ class HiveonT9(Hiveon, T9): except (KeyError, IndexError): pass hashboards[board].hashrate = AlgoHashRate.SHA256( - hashrate, HashUnit.SHA256.GH + rate=float(hashrate), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) hashboards[board].chips = chips @@ -172,4 +172,4 @@ class HiveonT9(Hiveon, T9): pass if not env_temp_list == []: - return round(float(sum(env_temp_list) / len(env_temp_list)), 2) + return round(sum(env_temp_list) / len(env_temp_list)) diff --git a/pyasic/miners/backends/antminer.py b/pyasic/miners/backends/antminer.py index 21e44efb..e44b769e 100644 --- a/pyasic/miners/backends/antminer.py +++ b/pyasic/miners/backends/antminer.py @@ -183,13 +183,13 @@ class AntminerModern(BMMiner): async def stop_mining(self) -> bool: cfg = await self.get_config() - cfg.miner_mode = MiningModeConfig.sleep + cfg.miner_mode = MiningModeConfig.sleep() await self.send_config(cfg) return True async def resume_mining(self) -> bool: cfg = await self.get_config() - cfg.miner_mode = MiningModeConfig.normal + cfg.miner_mode = MiningModeConfig.normal() await self.send_config(cfg) return True @@ -239,7 +239,7 @@ class AntminerModern(BMMiner): for item in web_summary["SUMMARY"][0]["status"]: try: if not item["status"] == "s": - errors.append(X19Error(item["msg"])) + errors.append(X19Error(error_message=item["msg"])) except KeyError: continue except LookupError: @@ -248,7 +248,7 @@ class AntminerModern(BMMiner): async def _get_hashboards(self) -> List[HashBoard]: hashboards = [ - HashBoard(idx, expected_chips=self.expected_chips) + HashBoard(slot=idx, expected_chips=self.expected_chips) for idx in range(self.expected_hashboards) ] @@ -261,7 +261,7 @@ class AntminerModern(BMMiner): try: for board in rpc_stats["STATS"][0]["chain"]: hashboards[board["index"]].hashrate = AlgoHashRate.SHA256( - board["rate_real"], HashUnit.SHA256.GH + rate=board["rate_real"], unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) hashboards[board["index"]].chips = board["asic_num"] board_temp_data = list( @@ -318,7 +318,7 @@ class AntminerModern(BMMiner): except KeyError: rate_unit = "GH" return AlgoHashRate.SHA256( - expected_rate, HashUnit.SHA256.from_str(rate_unit) + rate=float(expected_rate), unit=HashUnit.SHA256.from_str(rate_unit) ).into(self.algo.unit.default) except LookupError: pass @@ -625,7 +625,7 @@ class AntminerOld(CGMiner): hashrate = boards[1].get(f"chain_rate{i}") if hashrate: hashboard.hashrate = AlgoHashRate.SHA256( - float(hashrate), HashUnit.SHA256.GH + rate=float(hashrate), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) chips = boards[1].get(f"chain_acn{i}") diff --git a/pyasic/miners/backends/auradine.py b/pyasic/miners/backends/auradine.py index d0405323..59ea7a05 100644 --- a/pyasic/miners/backends/auradine.py +++ b/pyasic/miners/backends/auradine.py @@ -293,7 +293,8 @@ class Auradine(StockFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["MHS 5s"], HashUnit.SHA256.MH + rate=float(rpc_summary["SUMMARY"][0]["MHS 5s"]), + unit=HashUnit.SHA256.MH, ).into(self.algo.unit.default) except (LookupError, ValueError, TypeError): pass @@ -322,9 +323,9 @@ class Auradine(StockFirmware): for board in rpc_devs["DEVS"]: b_id = board["ID"] - 1 hashboards[b_id].hashrate = AlgoHashRate.SHA256( - board["MHS 5s"], HashUnit.SHA256.MH + rate=float(board["MHS 5s"]), unit=HashUnit.SHA256.MH ).into(self.algo.unit.default) - hashboards[b_id].temp = round(float(float(board["Temperature"])), 2) + hashboards[b_id].temp = round(float(board["Temperature"])) hashboards[b_id].missing = False except LookupError: pass @@ -390,7 +391,7 @@ class Auradine(StockFirmware): if web_fan is not None: try: for fan in web_fan["Fan"]: - fans.append(Fan(round(fan["Speed"]))) + fans.append(Fan(speed=round(fan["Speed"]))) except LookupError: pass return fans diff --git a/pyasic/miners/backends/avalonminer.py b/pyasic/miners/backends/avalonminer.py index 2d6d8bab..681ac2b5 100644 --- a/pyasic/miners/backends/avalonminer.py +++ b/pyasic/miners/backends/avalonminer.py @@ -184,7 +184,7 @@ class AvalonMiner(CGMiner): if rpc_devs is not None: try: return AlgoHashRate.SHA256( - rpc_devs["DEVS"][0]["MHS 1m"], HashUnit.SHA256.MH + rate=float(rpc_devs["DEVS"][0]["MHS 1m"]), unit=HashUnit.SHA256.MH ).into(self.algo.unit.default) except (KeyError, IndexError, ValueError, TypeError): pass @@ -217,7 +217,7 @@ class AvalonMiner(CGMiner): try: board_hr = parsed_stats["MGHS"][board] hashboards[board].hashrate = AlgoHashRate.SHA256( - float(board_hr), HashUnit.SHA256.GH + rate=float(board_hr), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) except LookupError: pass @@ -253,7 +253,7 @@ class AvalonMiner(CGMiner): unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] parsed_stats = self.parse_stats(unparsed_stats) return AlgoHashRate.SHA256( - float(parsed_stats["GHSmm"][0]), HashUnit.SHA256.GH + rate=float(parsed_stats["GHSmm"][0]), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) except (IndexError, KeyError, ValueError, TypeError): pass diff --git a/pyasic/miners/backends/bfgminer.py b/pyasic/miners/backends/bfgminer.py index fe4cacc6..6a74b9e9 100644 --- a/pyasic/miners/backends/bfgminer.py +++ b/pyasic/miners/backends/bfgminer.py @@ -121,7 +121,8 @@ class BFGMiner(StockFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["MHS 20s"], HashUnit.SHA256.MH + rate=float(rpc_summary["SUMMARY"][0]["MHS 20s"]), + unit=HashUnit.SHA256.MH, ).into(self.algo.unit.default) except (LookupError, ValueError, TypeError): pass @@ -167,7 +168,7 @@ class BFGMiner(StockFirmware): hashrate = boards[1].get(f"chain_rate{i}") if hashrate: hashboard.hashrate = AlgoHashRate.SHA256( - hashrate, HashUnit.SHA256.GH + rate=float(hashrate), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) chips = boards[1].get(f"chain_acn{i}") @@ -260,7 +261,7 @@ class BFGMiner(StockFirmware): except KeyError: rate_unit = "GH" return AlgoHashRate.SHA256( - expected_rate, HashUnit.SHA256.from_str(rate_unit) + rate=float(expected_rate), unit=HashUnit.SHA256.from_str(rate_unit) ).into(self.algo.unit.default) except LookupError: pass diff --git a/pyasic/miners/backends/bitaxe.py b/pyasic/miners/backends/bitaxe.py index 04c11a1b..000ee4d5 100644 --- a/pyasic/miners/backends/bitaxe.py +++ b/pyasic/miners/backends/bitaxe.py @@ -94,7 +94,7 @@ class BitAxe(BaseMiner): if web_system_info is not None: try: return AlgoHashRate.SHA256( - web_system_info["hashRate"], HashUnit.SHA256.GH + rate=float(web_system_info["hashRate"]), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) except KeyError: pass @@ -124,7 +124,8 @@ class BitAxe(BaseMiner): return [ HashBoard( hashrate=AlgoHashRate.SHA256( - web_system_info["hashRate"], HashUnit.SHA256.GH + rate=float(web_system_info["hashRate"]), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default), chip_temp=web_system_info.get("temp"), temp=web_system_info.get("vrTemp"), diff --git a/pyasic/miners/backends/bmminer.py b/pyasic/miners/backends/bmminer.py index d54cfc42..eaf35d0c 100644 --- a/pyasic/miners/backends/bmminer.py +++ b/pyasic/miners/backends/bmminer.py @@ -125,7 +125,8 @@ class BMMiner(StockFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["GHS 5s"], HashUnit.SHA256.GH + rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) except (LookupError, ValueError, TypeError): pass @@ -184,7 +185,7 @@ class BMMiner(StockFirmware): hashrate = boards[1].get(f"chain_rate{i}") if hashrate: hashboard.hashrate = AlgoHashRate.SHA256( - hashrate, HashUnit.SHA256.GH + rate=float(hashrate), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) chips = boards[1].get(f"chain_acn{i}") @@ -246,7 +247,7 @@ class BMMiner(StockFirmware): except KeyError: rate_unit = "GH" return AlgoHashRate.SHA256( - expected_rate, HashUnit.SHA256.from_str(rate_unit) + rate=float(expected_rate), unit=HashUnit.SHA256.from_str(rate_unit) ).into(self.algo.unit.default) except LookupError: pass diff --git a/pyasic/miners/backends/braiins_os.py b/pyasic/miners/backends/braiins_os.py index 039f9260..750ab5fe 100644 --- a/pyasic/miners/backends/braiins_os.py +++ b/pyasic/miners/backends/braiins_os.py @@ -363,7 +363,8 @@ class BOSMiner(BraiinsOSFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["MHS 1m"], HashUnit.SHA256.MH + rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), + unit=HashUnit.SHA256.MH, ).into(self.algo.unit.default) except (KeyError, IndexError, ValueError, TypeError): pass @@ -435,7 +436,7 @@ class BOSMiner(BraiinsOSFirmware): for board in rpc_devs["DEVS"]: _id = board["ID"] - offset hashboards[_id].hashrate = AlgoHashRate.SHA256( - board["MHS 1m"], HashUnit.SHA256.MH + rate=float(board["MHS 1m"]), unit=HashUnit.SHA256.MH ).into(self.algo.unit.default) except (IndexError, KeyError): pass @@ -481,7 +482,7 @@ class BOSMiner(BraiinsOSFirmware): fans = [] for n in range(self.expected_fans): try: - fans.append(Fan(rpc_fans["FANS"][n]["RPM"])) + fans.append(Fan(speed=rpc_fans["FANS"][n]["RPM"])) except (IndexError, KeyError): pass return fans @@ -512,7 +513,9 @@ class BOSMiner(BraiinsOSFirmware): ]: _error = board["Status"].split(" {")[0] _error = _error[0].lower() + _error[1:] - errors.append(BraiinsOSError(f"Slot {_id} {_error}")) + errors.append( + BraiinsOSError(error_message=f"Slot {_id} {_error}") + ) return errors except (KeyError, IndexError): pass @@ -543,16 +546,19 @@ class BOSMiner(BraiinsOSFirmware): hr_list = [] for board in rpc_devs["DEVS"]: - expected_hashrate = round(float(board["Nominal MHS"] / 1000000), 2) + expected_hashrate = float(board["Nominal MHS"] / 1000000) if expected_hashrate: hr_list.append(expected_hashrate) if len(hr_list) == 0: - return AlgoHashRate.SHA256(0) + return AlgoHashRate.SHA256(rate=float(0)) else: return AlgoHashRate.SHA256( - (sum(hr_list) / len(hr_list)) * self.expected_hashboards - ) + rate=float( + (sum(hr_list) / len(hr_list)) * self.expected_hashboards + ), + unit=HashUnit.SHA256.MH, + ).into(self.algo.unit.default) except (IndexError, KeyError): pass @@ -890,7 +896,8 @@ class BOSer(BraiinsOSFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["MHS 1m"], HashUnit.SHA256.MH + rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), + unit=HashUnit.SHA256.MH, ).into(self.algo.unit.default) except (KeyError, IndexError, ValueError, TypeError): pass @@ -907,8 +914,10 @@ class BOSer(BraiinsOSFirmware): if grpc_miner_details is not None: try: return AlgoHashRate.SHA256( - grpc_miner_details["stickerHashrate"]["gigahashPerSecond"], - HashUnit.SHA256.GH, + rate=float( + grpc_miner_details["stickerHashrate"]["gigahashPerSecond"] + ), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) except LookupError: pass @@ -933,18 +942,20 @@ class BOSer(BraiinsOSFirmware): if board.get("chipsCount") is not None: hashboards[idx].chips = board["chipsCount"] if board.get("boardTemp") is not None: - hashboards[idx].temp = board["boardTemp"]["degreeC"] + hashboards[idx].temp = int(board["boardTemp"]["degreeC"]) if board.get("highestChipTemp") is not None: - hashboards[idx].chip_temp = board["highestChipTemp"]["temperature"][ - "degreeC" - ] + hashboards[idx].chip_temp = int( + board["highestChipTemp"]["temperature"]["degreeC"] + ) if board.get("stats") is not None: if not board["stats"]["realHashrate"]["last5S"] == {}: hashboards[idx].hashrate = AlgoHashRate.SHA256( - board["stats"]["realHashrate"]["last5S"][ - "gigahashPerSecond" - ], - HashUnit.SHA256.GH, + rate=float( + board["stats"]["realHashrate"]["last5S"][ + "gigahashPerSecond" + ] + ), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) hashboards[idx].missing = False @@ -993,7 +1004,7 @@ class BOSer(BraiinsOSFirmware): fans = [] for n in range(self.expected_fans): try: - fans.append(Fan(grpc_cooling_state["fans"][n]["rpm"])) + fans.append(Fan(speed=grpc_cooling_state["fans"][n]["rpm"])) except (IndexError, KeyError): pass return fans @@ -1024,7 +1035,9 @@ class BOSer(BraiinsOSFirmware): ]: _error = board["Status"].split(" {")[0] _error = _error[0].lower() + _error[1:] - errors.append(BraiinsOSError(f"Slot {_id} {_error}")) + errors.append( + BraiinsOSError(error_message=f"Slot {_id} {_error}") + ) return errors except LookupError: pass @@ -1085,7 +1098,7 @@ class BOSer(BraiinsOSFirmware): for group in grpc_pool_groups["poolGroups"]: for idx, pool_info in enumerate(group["pools"]): pool_data = PoolMetrics( - url=pool_info["url"], + url=PoolUrl.from_str(pool_info["url"]), user=pool_info["user"], index=idx, accepted=pool_info["stats"].get("acceptedShares", 0), diff --git a/pyasic/miners/backends/btminer.py b/pyasic/miners/backends/btminer.py index 7714165a..e58a8c13 100644 --- a/pyasic/miners/backends/btminer.py +++ b/pyasic/miners/backends/btminer.py @@ -274,7 +274,7 @@ class BTMiner(StockFirmware): cfg.mining_mode = MiningModeConfig.normal() return cfg - cfg.mining_mode = MiningModeConfig.power_tuning(power_lim) + cfg.mining_mode = MiningModeConfig.power_tuning(power=power_lim) self.config = cfg return self.config @@ -404,7 +404,8 @@ class BTMiner(StockFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["MHS 1m"], HashUnit.SHA256.MH + rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), + unit=HashUnit.SHA256.MH, ).into(self.algo.unit.default) except LookupError: pass @@ -434,7 +435,7 @@ class BTMiner(StockFirmware): hashboards[board["ASC"]].chip_temp = round(board["Chip Temp Avg"]) hashboards[board["ASC"]].temp = round(board["Temperature"]) hashboards[board["ASC"]].hashrate = AlgoHashRate.SHA256( - board["MHS 1m"], HashUnit.SHA256.MH + rate=float(board["MHS 1m"]), unit=HashUnit.SHA256.MH ).into(self.algo.unit.default) hashboards[board["ASC"]].chips = board["Effective Chips"] hashboards[board["ASC"]].serial_number = board["PCB SN"] @@ -498,8 +499,8 @@ class BTMiner(StockFirmware): try: if self.expected_fans > 0: fans = [ - Fan(rpc_summary["SUMMARY"][0].get("Fan Speed In", 0)), - Fan(rpc_summary["SUMMARY"][0].get("Fan Speed Out", 0)), + Fan(speed=rpc_summary["SUMMARY"][0].get("Fan Speed In", 0)), + Fan(speed=rpc_summary["SUMMARY"][0].get("Fan Speed Out", 0)), ] except LookupError: pass @@ -584,7 +585,7 @@ class BTMiner(StockFirmware): expected_hashrate = rpc_summary["SUMMARY"][0]["Factory GHS"] if expected_hashrate: return AlgoHashRate.SHA256( - expected_hashrate, HashUnit.SHA256.GH + rate=float(expected_hashrate), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) except LookupError: diff --git a/pyasic/miners/backends/cgminer.py b/pyasic/miners/backends/cgminer.py index e73180b0..dbf95693 100644 --- a/pyasic/miners/backends/cgminer.py +++ b/pyasic/miners/backends/cgminer.py @@ -124,7 +124,8 @@ class CGMiner(StockFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["GHS 5s"], HashUnit.SHA256.GH + rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) except (LookupError, ValueError, TypeError): pass diff --git a/pyasic/miners/backends/epic.py b/pyasic/miners/backends/epic.py index 74a5bcdc..15ce4710 100644 --- a/pyasic/miners/backends/epic.py +++ b/pyasic/miners/backends/epic.py @@ -20,7 +20,7 @@ from typing import List, Optional from pyasic.config import MinerConfig from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit from pyasic.data.error_codes import MinerErrorData, X19Error -from pyasic.data.pools import PoolMetrics +from pyasic.data.pools import PoolMetrics, PoolUrl from pyasic.errors import APIError from pyasic.logger import logger from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand @@ -234,9 +234,9 @@ class ePIC(ePICFirmware): if web_summary["HBs"] is not None: for hb in web_summary["HBs"]: hashrate += hb["Hashrate"][0] - return AlgoHashRate.SHA256(hashrate, HashUnit.SHA256.MH).into( - HashUnit.SHA256.TH - ) + return AlgoHashRate.SHA256( + rate=float(hashrate), unit=HashUnit.SHA256.MH + ).into(HashUnit.SHA256.TH) except (LookupError, ValueError, TypeError): pass @@ -260,9 +260,9 @@ class ePIC(ePICFirmware): ideal = hb["Hashrate"][1] / 100 hashrate += hb["Hashrate"][0] / ideal - return AlgoHashRate.SHA256(hashrate, HashUnit.SHA256.MH).into( - self.algo.unit.default - ) + return AlgoHashRate.SHA256( + rate=float(hashrate), unit=HashUnit.SHA256.MH + ).into(self.algo.unit.default) except (LookupError, ValueError, TypeError): pass @@ -293,7 +293,7 @@ class ePIC(ePICFirmware): if web_summary is not None: for fan in web_summary["Fans Rpm"]: try: - fans.append(Fan(web_summary["Fans Rpm"][fan])) + fans.append(Fan(speed=web_summary["Fans Rpm"][fan])) except (LookupError, ValueError, TypeError): fans.append(Fan()) return fans @@ -353,10 +353,10 @@ class ePIC(ePICFirmware): # Update the Hashboard object hb_list[hb["Index"]].missing = False hb_list[hb["Index"]].hashrate = AlgoHashRate.SHA256( - hashrate, HashUnit.SHA256.MH + rate=float(hashrate), unit=HashUnit.SHA256.MH ).into(self.algo.unit.default) hb_list[hb["Index"]].chips = num_of_chips - hb_list[hb["Index"]].temp = hb["Temperature"] + 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"] @@ -417,7 +417,7 @@ class ePIC(ePICFirmware): try: error = web_summary["Status"]["Last Error"] if error is not None: - errors.append(X19Error(str(error))) + errors.append(X19Error(error_message=str(error))) return errors except KeyError: pass @@ -437,6 +437,10 @@ class ePIC(ePICFirmware): web_summary.get("Session") is not None and web_summary.get("Stratum") is not None ): + url = web_summary["Stratum"].get("Current Pool") + # TODO: when scheme gets put in, update this + if url is not None: + url = PoolUrl.from_str(f"stratum+tcp://{url}") pool_data.append( PoolMetrics( accepted=web_summary["Session"].get("Accepted"), @@ -445,7 +449,7 @@ class ePIC(ePICFirmware): remote_failures=0, active=web_summary["Stratum"].get("IsPoolConnected"), alive=web_summary["Stratum"].get("IsPoolConnected"), - url=web_summary["Stratum"].get("Current Pool"), + url=url, user=web_summary["Stratum"].get("Current User"), index=web_summary["Stratum"].get("Config Id"), ) diff --git a/pyasic/miners/backends/goldshell.py b/pyasic/miners/backends/goldshell.py index 081f1ec3..97b0834d 100644 --- a/pyasic/miners/backends/goldshell.py +++ b/pyasic/miners/backends/goldshell.py @@ -163,7 +163,7 @@ class GoldshellMiner(BFGMiner): try: b_id = board["ID"] hashboards[b_id].hashrate = AlgoHashRate.SHA256( - board["MHS 20s"], HashUnit.SHA256.MH + rate=float(board["MHS 20s"]), unit=HashUnit.SHA256.MH ).into(self.algo.unit.default) hashboards[b_id].temp = board["tstemp-2"] hashboards[b_id].missing = False diff --git a/pyasic/miners/backends/hammer.py b/pyasic/miners/backends/hammer.py index 5ecd1d3e..4c429358 100644 --- a/pyasic/miners/backends/hammer.py +++ b/pyasic/miners/backends/hammer.py @@ -172,7 +172,8 @@ class BlackMiner(StockFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["GHS 5s"], HashUnit.SHA256.GH + rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) except (LookupError, ValueError, TypeError): pass @@ -231,7 +232,7 @@ class BlackMiner(StockFirmware): hashrate = boards[1].get(f"chain_rate{i}") if hashrate: hashboard.hashrate = AlgoHashRate.SHA256( - float(hashrate), HashUnit.SHA256.GH + rate=float(hashrate), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) chips = boards[1].get(f"chain_acn{i}") @@ -275,42 +276,6 @@ class BlackMiner(StockFirmware): return fans - async def _get_expected_hashrate( - self, rpc_stats: dict = None - ) -> Optional[AlgoHashRate]: - # X19 method, not sure compatibility - if rpc_stats is None: - try: - rpc_stats = await self.rpc.stats() - except APIError: - pass - - if rpc_stats is not None: - try: - expected_rate = rpc_stats["STATS"][1]["total_rateideal"] - try: - rate_unit = rpc_stats["STATS"][1]["rate_unit"] - except KeyError: - rate_unit = "GH" - return AlgoHashRate.SHA256( - expected_rate, HashUnit.SHA256.from_str(rate_unit) - ).into(self.algo.unit.default) - except LookupError: - pass - - async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: - if rpc_stats is None: - try: - rpc_stats = await self.rpc.stats() - except APIError: - pass - - if rpc_stats is not None: - try: - return int(rpc_stats["STATS"][1]["Elapsed"]) - except LookupError: - pass - async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: if web_get_system_info is None: try: @@ -357,7 +322,7 @@ class BlackMiner(StockFirmware): for item in web_summary["SUMMARY"][0]["status"]: try: if not item["status"] == "s": - errors.append(X19Error(item["msg"])) + errors.append(X19Error(error_message=item["msg"])) except KeyError: continue except LookupError: @@ -400,7 +365,7 @@ class BlackMiner(StockFirmware): except KeyError: rate_unit = "GH" return AlgoHashRate.SHA256( - expected_rate, HashUnit.SHA256.from_str(rate_unit) + rate=float(expected_rate), unit=HashUnit.SHA256.from_str(rate_unit) ).into(self.algo.unit.default) except LookupError: pass diff --git a/pyasic/miners/backends/iceriver.py b/pyasic/miners/backends/iceriver.py index 29bf51ad..6cd1f838 100644 --- a/pyasic/miners/backends/iceriver.py +++ b/pyasic/miners/backends/iceriver.py @@ -87,7 +87,9 @@ class IceRiver(StockFirmware): if web_userpanel is not None: try: - return [Fan(spd) for spd in web_userpanel["userpanel"]["data"]["fans"]] + return [ + Fan(speed=spd) for spd in web_userpanel["userpanel"]["data"]["fans"] + ] except (LookupError, ValueError, TypeError): pass @@ -130,7 +132,7 @@ class IceRiver(StockFirmware): try: base_unit = web_userpanel["userpanel"]["data"]["unit"] return AlgoHashRate.SHA256( - float( + rate=float( web_userpanel["userpanel"]["data"]["rtpow"].replace( base_unit, "" ) @@ -186,7 +188,8 @@ class IceRiver(StockFirmware): hb_list[idx].chip_temp = round(board["outtmp"]) hb_list[idx].temp = round(board["intmp"]) hb_list[idx].hashrate = AlgoHashRate.SHA256( - float(board["rtpow"].replace("G", "")), HashUnit.SHA256.GH + rate=float(board["rtpow"].replace("G", "")), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) hb_list[idx].chips = int(board["chipnum"]) hb_list[idx].missing = False diff --git a/pyasic/miners/backends/innosilicon.py b/pyasic/miners/backends/innosilicon.py index 0f089bfb..05da2c4d 100644 --- a/pyasic/miners/backends/innosilicon.py +++ b/pyasic/miners/backends/innosilicon.py @@ -187,11 +187,13 @@ class Innosilicon(CGMiner): try: if "Hash Rate H" in web_get_all["total_hash"].keys(): return AlgoHashRate.SHA256( - web_get_all["total_hash"]["Hash Rate H"], HashUnit.SHA256.H + rate=float(web_get_all["total_hash"]["Hash Rate H"]), + unit=HashUnit.SHA256.H, ).into(self.algo.unit.default) elif "Hash Rate" in web_get_all["total_hash"].keys(): return AlgoHashRate.SHA256( - web_get_all["total_hash"]["Hash Rate"], HashUnit.SHA256.MH + rate=float(web_get_all["total_hash"]["Hash Rate"]), + unit=HashUnit.SHA256.MH, ).into(self.algo.unit.default) except KeyError: pass @@ -199,7 +201,8 @@ class Innosilicon(CGMiner): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["MHS 1m"], HashUnit.SHA256.MH + rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), + unit=HashUnit.SHA256.MH, ).into(self.algo.unit.default) except (KeyError, IndexError): pass @@ -253,7 +256,7 @@ class Innosilicon(CGMiner): hashrate = board.get("Hash Rate H") if hashrate: hashboards[idx].hashrate = AlgoHashRate.SHA256( - hashrate, HashUnit.SHA256.H + rate=float(hashrate), unit=HashUnit.SHA256.H ).into(self.algo.unit.default) chip_temp = board.get("Temp max") diff --git a/pyasic/miners/backends/luxminer.py b/pyasic/miners/backends/luxminer.py index 9196919e..a8973549 100644 --- a/pyasic/miners/backends/luxminer.py +++ b/pyasic/miners/backends/luxminer.py @@ -179,14 +179,15 @@ class LUXMiner(LuxOSFirmware): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["GHS 5s"], HashUnit.SHA256.GH + rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) except (LookupError, ValueError, TypeError): pass async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: hashboards = [ - HashBoard(idx, expected_chips=self.expected_chips) + HashBoard(slot=idx, expected_chips=self.expected_chips) for idx in range(self.expected_hashboards) ] @@ -202,7 +203,8 @@ class LUXMiner(LuxOSFirmware): for idx in range(3): board_n = idx + 1 hashboards[idx].hashrate = AlgoHashRate.SHA256( - float(board_stats[f"chain_rate{board_n}"]), HashUnit.SHA256.GH + rate=float(board_stats[f"chain_rate{board_n}"]), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) hashboards[idx].chips = int(board_stats[f"chain_acn{board_n}"]) chip_temp_data = list( @@ -253,7 +255,7 @@ class LUXMiner(LuxOSFirmware): if rpc_fans is not None: for fan in range(self.expected_fans): try: - fans.append(Fan(rpc_fans["FANS"][fan]["RPM"])) + fans.append(Fan(speed=rpc_fans["FANS"][fan]["RPM"])) except (LookupError, ValueError, TypeError): fans.append(Fan()) return fans @@ -275,7 +277,7 @@ class LUXMiner(LuxOSFirmware): except KeyError: rate_unit = "GH" return AlgoHashRate.SHA256( - expected_rate, HashUnit.SHA256.from_str(rate_unit) + rate=float(expected_rate), unit=HashUnit.SHA256.from_str(rate_unit) ).into(self.algo.unit.default) except LookupError: pass diff --git a/pyasic/miners/backends/marathon.py b/pyasic/miners/backends/marathon.py index bafeb8e2..60639c09 100644 --- a/pyasic/miners/backends/marathon.py +++ b/pyasic/miners/backends/marathon.py @@ -99,7 +99,7 @@ class MaraMiner(MaraFirmware): async def set_power_limit(self, wattage: int) -> bool: cfg = await self.get_config() - cfg.mining_mode = MiningModeConfig.power_tuning(wattage) + cfg.mining_mode = MiningModeConfig.power_tuning(power=wattage) await self.send_config(cfg) return True @@ -179,13 +179,13 @@ class MaraMiner(MaraFirmware): for hb in web_hashboards["hashboards"]: idx = hb["index"] hashboards[idx].hashrate = AlgoHashRate.SHA256( - hb["hashrate_average"], HashUnit.SHA256.GH + rate=float(hb["hashrate_average"]), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) hashboards[idx].temp = round( - sum(hb["temperature_pcb"]) / len(hb["temperature_pcb"]), 2 + sum(hb["temperature_pcb"]) / len(hb["temperature_pcb"]) ) hashboards[idx].chip_temp = round( - sum(hb["temperature_chip"]) / len(hb["temperature_chip"]), 2 + sum(hb["temperature_chip"]) / len(hb["temperature_chip"]) ) hashboards[idx].chips = hb["asic_num"] hashboards[idx].serial_number = hb["serial_number"] @@ -243,7 +243,7 @@ class MaraMiner(MaraFirmware): if web_brief is not None: try: return AlgoHashRate.SHA256( - web_brief["hashrate_realtime"], HashUnit.SHA256.TH + rate=float(web_brief["hashrate_realtime"]), unit=HashUnit.SHA256.TH ).into(self.algo.unit.default) except LookupError: pass @@ -259,7 +259,7 @@ class MaraMiner(MaraFirmware): fans = [] for n in range(self.expected_fans): try: - fans.append(Fan(web_fans["fans"][n]["current_speed"])) + fans.append(Fan(speed=web_fans["fans"][n]["current_speed"])) except (IndexError, KeyError): pass return fans @@ -291,7 +291,7 @@ class MaraMiner(MaraFirmware): if web_brief is not None: try: return AlgoHashRate.SHA256( - web_brief["hashrate_ideal"], HashUnit.SHA256.GH + rate=float(web_brief["hashrate_ideal"]), unit=HashUnit.SHA256.GH ).into(self.algo.unit.default) except LookupError: pass diff --git a/pyasic/miners/backends/vnish.py b/pyasic/miners/backends/vnish.py index 5f75fc36..a5d0f9da 100644 --- a/pyasic/miners/backends/vnish.py +++ b/pyasic/miners/backends/vnish.py @@ -208,7 +208,8 @@ class VNish(VNishFirmware, BMMiner): if rpc_summary is not None: try: return AlgoHashRate.SHA256( - rpc_summary["SUMMARY"][0]["GHS 5s"], HashUnit.SHA256.GH + rate=float(rpc_summary["SUMMARY"][0]["GHS 5s"]), + unit=HashUnit.SHA256.GH, ).into(self.algo.unit.default) except (LookupError, ValueError, TypeError): pass diff --git a/pyasic/miners/base.py b/pyasic/miners/base.py index 0ada927a..87b1e87a 100644 --- a/pyasic/miners/base.py +++ b/pyasic/miners/base.py @@ -495,6 +495,7 @@ class MinerProtocol(Protocol): function = getattr(self, getattr(self.data_locations, data_name).cmd) miner_data[data_name] = await function(**args_to_send) except Exception as e: + raise e raise APIError( f"Failed to call {data_name} on {self} while getting data." ) from e diff --git a/pyproject.toml b/pyproject.toml index f0109198..2d54bdb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ tomli = { version = ">=2.0.1", python = "<3.11" } tomli-w = "^1.0.0" aiofiles = ">=23.2.1" betterproto = "2.0.0b7" +pydantic = "^2.9.2" [tool.poetry.group.dev] optional = true