Compare commits

...

74 Commits

Author SHA1 Message Date
James Hilliard
1f4054bf38 feature: add mypy type checking for better type consistency 2025-09-26 11:24:38 -06:00
Brett Rowan
cd52d3aeaf version: bump version number 2025-09-23 12:40:02 -06:00
Brett Rowan
66c9b3663e feature: add serial number for antminers 2025-09-23 12:39:16 -06:00
James Hilliard
5f0e1da938 Update dependencies 2025-09-22 18:34:03 -06:00
Brett Rowan
2bd031b33d version: bump version number 2025-09-19 15:49:29 -06:00
James Hilliard
e2f07818cc Fix race conditions in RPC and web API multicommand methods
Multiple multicommand methods were double-awaiting tasks - first via
asyncio.gather() with return_exceptions=True, then calling .result() on
the same tasks. This caused ConnectionResetError and other exceptions
when connections were lost.

Changed to properly use the results from gather() instead of calling
.result() on completed tasks, preventing exceptions from being raised
after they were already caught.

Fixed in:
- pyasic/rpc/base.py:144 - RPC _send_split_multicommand
- pyasic/web/espminer.py:79 - ESPMiner multicommand
- pyasic/web/auradine.py:149 - Auradine multicommand
2025-09-19 14:31:33 -06:00
Brett Rowan
75056cfff5 version: bump version number 2025-09-19 14:18:36 -06:00
James Hilliard
7fbcb0dbd2 Fix race condition in BOSer multicommand causing CancelledError
The multicommand method was double-awaiting tasks - first via
asyncio.gather() with return_exceptions=True, then trying to await
the same tasks again. This caused CancelledError when gRPC connections
were lost.

Changed to properly use the results from gather() instead of
re-awaiting completed tasks, preventing the race condition and
properly handling exceptions.

Fixes StreamTerminatedError occurring in pyasic/web/braiins_os/boser.py:91
2025-09-19 14:17:59 -06:00
Brett Rowan
7329aeace2 version: bump version number 2025-09-17 19:18:51 -06:00
Brett Rowan
e8c3953106 bug: fix btminer V3 password 2025-09-17 19:18:27 -06:00
James Hilliard
a1a7562bdb Handle invalid unicode in json response 2025-09-17 19:11:31 -06:00
Brett Rowan
b2726c77a0 version: bump version number 2025-09-09 08:13:09 -06:00
Ryan Heideman
68299fa54d Avalon Nano3s Fixes (#374)
* Avalon Nano3s Fixes

* Fix to handle both cases

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-30 22:04:58 -06:00
UpstreamData
1466039e08 version: bump version number 2025-08-28 21:43:00 -06:00
Ryan Heideman
0aa72c6c85 Fix Byte Folder Names (#376) 2025-08-26 21:18:57 -06:00
Ryan Heideman
24134a5991 Goldshell Mini Doge Support (#375)
* Docs for Mini Doge

* Mini Doge Recognition Support

* Loading fixes

* Fix for number of fans

* Fixed expected_hashrate

* Fixes for hashboards

* Implemented uptime

* Fixed uptime

* Doc fixes

* Fixes for byte

* Copyright update

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Typo fix

* File renames

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-26 21:06:10 -06:00
Brett Rowan
038208efa6 version: bump version number 2025-08-16 10:25:55 -06:00
Brett Rowan
7e319b79df feature: add misidentified L7_i 2025-08-16 10:25:40 -06:00
Brett Rowan
a0fdec3ce5 version: bump version number 2025-08-16 10:06:58 -06:00
Brett Rowan
1a8928de18 feature: add retries to elphapex 2025-08-16 10:06:46 -06:00
Brett Rowan
bce0058930 version: bump version number 2025-08-16 09:38:24 -06:00
Brett Rowan
850656fce4 bug: fix elphapex hashboard parsing 2025-08-16 09:38:06 -06:00
Brett Rowan
8bb35d6d7c version: bump version number 2025-08-16 09:24:17 -06:00
Brett Rowan
e85f06dbc2 feature: add misidentified L9_i 2025-08-16 09:23:12 -06:00
Brett Rowan
a566801316 feature: add chip count for IceRiver KS5M 2025-08-16 09:16:07 -06:00
Brett Rowan
e1a9cc5d19 bug: fix some issues with whatsminer BTMinerV3 implementation 2025-08-16 09:14:11 -06:00
Brett Rowan
27bb06de2b version: bump version number 2025-08-15 14:36:04 -06:00
Brett Rowan
debd4d2d4d feature: add BTMiner V3 configuration 2025-08-15 14:35:32 -06:00
Brett Rowan
56ad6cbc6f feature: add dynamic version selection for whatsminers 2025-08-15 14:35:32 -06:00
Brett Rowan
3fa54213bf bug: handle vnish S19 pro hydro alternate naming 2025-08-15 14:35:15 -06:00
Brett Rowan
076958ec0e version: bump version number 2025-08-15 12:39:04 -06:00
Brett Rowan
5319089ebe feature: fix S21+ hydro for cases where there is no . at the end 2025-08-15 12:38:17 -06:00
Brett Rowan
76a77b51e8 version: bump version number 2025-08-14 13:25:26 -06:00
Brett Rowan
b099ff45d2 bug: remove print statements 2025-08-14 13:25:05 -06:00
Brett Rowan
9bc3cc221a version: bump version number 2025-08-14 11:41:50 -06:00
Brett Rowan
6418c2e102 feature: add BTMinerV3 control support 2025-08-14 11:41:08 -06:00
Brett Rowan
aa9f3b2c45 feature: add BTMinerV3 data support 2025-08-14 11:41:08 -06:00
Ryan Heideman
bb1c98f061 Goldshell Byte Support (#366) 2025-08-12 21:53:28 -06:00
Brett Rowan
d984431fe5 version: bump version number 2025-08-08 11:46:46 -06:00
Tony Scelfo
f1e4feb91e add expected chips for BraiinsModels BM100 and BM101 2025-08-08 11:45:43 -06:00
Brett Rowan
90c8986900 version: bump version number 2025-07-23 11:56:21 -06:00
Brett Rowan
5457ae6cd5 feature: add support for avalon Q 2025-07-23 11:55:57 -06:00
Brett Rowan
aa3d105fcb version: bump version number 2025-07-21 15:41:35 -06:00
Brett Rowan
77f59f6db6 feature: add support for Elphapex DG1 Home
Fixes: #311
2025-07-21 10:08:11 -06:00
Brett Rowan
3fa0d96fbb feature: add support for Iceriver AL3
Fixes: #317
2025-07-21 10:02:06 -06:00
Brett Rowan
e55477a8b8 feature: add support for S19j+
Fixes: #330
2025-07-21 09:51:51 -06:00
Brett Rowan
7d5744ae28 feature: add support for vnish XP Hydro
Fixes: #342
2025-07-21 09:48:11 -06:00
Brett Rowan
d4500be10c feature: add support for a couple alternate models
- Antminer S19j Pro+, ANTMINER
 - Antminer S19 Pro A, VNISH
 - Antminer S19 XP, VNISH
 - Antminer L9, VNISH
 - DG1, ELPHAPEX

 Fixes: #354
2025-07-21 09:35:00 -06:00
Brett Rowan
7ef2540133 feature: add support for Vnish S19 Hydro
Fixes: #355
2025-07-21 09:18:20 -06:00
Brett Rowan
1ea4f4e124 version: bump version number 2025-07-15 08:11:34 -06:00
bbemoll
a8a0e4a5fe Added support for Antminer S19XH Hydro running on Braiins OS (#353)
* Add pyasic files

* Add pyasic files

* Update pyproject.toml

* Update pyproject.toml

* Added support for Antminer S19XP Hydro running on Braiins OS

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-15 08:10:59 -06:00
Brett Rowan
5f2f6e01da version: bump version number 2025-07-10 10:56:42 -06:00
Brody
41b4c23d45 Fix API Issue with Bitaxe Miners (#352)
* added checks to identify which field is None.

* better logging

* if asicCount is None, try to find it in /system/asic

* ran pre-commit
2025-07-10 10:56:22 -06:00
Brett Rowan
b4687f18fd version: bump version number 2025-05-01 15:15:42 -06:00
Brett Rowan
2437421005 feature: add support for a bunch of BOS and VNish S21 models. 2025-05-01 15:15:18 -06:00
Ryan Heideman
40ebc2773f Retry and Timeout Changes for espminer (#340)
* Retry and Timeout Fixes for espminer

* Build check fix
2025-05-01 15:07:58 -06:00
Art
b8ae238d23 Fix incorrect computation of active_preset for Antminer VNish. See #338. (#339)
* Fix incorrect computation of active_preset for Antminer VNish. See #338.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* #338 renamed perf_summary to web_perf_summary for clarity and consistency. New API call is done in backward compatibility manner. Data is also retrieved safely

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-01 15:07:11 -06:00
Brett Rowan
2920639b70 version: bump version number 2025-04-27 11:01:10 -06:00
Brett Rowan
bd9144b3de requirements: bump dependency versions 2025-04-27 11:01:10 -06:00
SKART1
8f7a67d4dc Fix efficiency_fract property to correctly return computed value. See #334. 2025-04-19 08:28:44 -06:00
Brett Rowan
a62bea33a7 docs: update docs 2025-04-18 16:15:31 -06:00
pre-commit-ci[bot]
406bcd179c [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-04-18 16:12:55 -06:00
Ryan Heideman
aa87ef7d71 nano3s fixes 2025-04-18 16:12:55 -06:00
SKART1
ec88fbf6aa Fix #334 efficiency calculation and add computed fields 2025-04-18 16:12:44 -06:00
pre-commit-ci[bot]
e23c86a944 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/python-poetry/poetry: 2.1.1 → 2.1.2](https://github.com/python-poetry/poetry/compare/2.1.1...2.1.2)
2025-04-16 08:41:30 -06:00
Brett Rowan
b81276cb19 version: bump version number 2025-04-16 08:40:36 -06:00
Adrian
8dcc72b1bb T21 working on LuxOS (#331) 2025-04-14 12:55:54 -06:00
Timur Taipov
540572356f feature: add support for S21+ hydro (#326) 2025-04-14 09:15:06 -06:00
Ryan Heideman
83035a869b feature: Add Support for Avalon Nano 3s (#329) 2025-04-12 15:34:45 -06:00
Brett Rowan
4c104a59ff version: bump version number 2025-04-01 09:24:20 -06:00
Brett Rowan
e708ae3728 bug: remove MSKminer web identification 2025-04-01 09:23:51 -06:00
Wilfred Allyn
a4352816ee bug: pass atmset param as bool 2025-04-01 07:48:42 -06:00
Will Jackson
336bd9c002 bug: update set_dps configuration to handle optional power and hashrate parameters (#319)
* bug: update set_dps configuration to handle optional power and hashrate parameters
2025-03-27 11:09:47 -06:00
Wilfred Allyn
e3c917efde feature: set supports_autotuning True for luxminer 2025-03-26 08:06:38 -06:00
181 changed files with 6798 additions and 3175 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ pyvenv.cfg
bin/ bin/
lib/ lib/
.idea/ .idea/
.vs/

View File

@@ -5,13 +5,13 @@ ci:
- generate-docs - generate-docs
repos: repos:
- repo: https://github.com/python-poetry/poetry - repo: https://github.com/python-poetry/poetry
rev: 2.1.1 rev: 2.2.1
hooks: hooks:
- id: poetry-check - id: poetry-check
- id: poetry-lock - id: poetry-lock
- id: poetry-install - id: poetry-install
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: v6.0.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: check-yaml - id: check-yaml
@@ -22,16 +22,24 @@ repos:
name: check-yaml for other YAML files name: check-yaml for other YAML files
exclude: ^mkdocs\.yml$ exclude: ^mkdocs\.yml$
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/psf/black - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 25.1.0 rev: v0.13.2
hooks: hooks:
- id: black - id: ruff-check
- repo: https://github.com/pycqa/isort args: [--fix]
rev: 6.0.1 - id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
hooks: hooks:
- id: isort - id: mypy
name: isort (python) additional_dependencies:
[
betterproto==2.0.0b7,
httpx==0.28.1,
types-aiofiles==24.1.0.20250822,
types-passlib==1.7.7.20250602,
pydantic==2.11.9,
]
- repo: local - repo: local
hooks: hooks:
- id: unittest - id: unittest

View File

@@ -23,7 +23,7 @@ Welcome to `pyasic`! `pyasic` uses an asynchronous method of communicating with
## Installation ## Installation
It is recommended to install `pyasic` in a [virtual environment](https://realpython.com/python-virtual-environments-a-primer/#what-other-popular-options-exist-aside-from-venv) to isolate it from the rest of your system. Options include: It is recommended to install `pyasic` in a [virtual environment](https://realpython.com/python-virtual-environments-a-primer/#what-other-popular-options-exist-aside-from-venv) to isolate it from the rest of your system. Options include:
- [pypoetry](https://python-poetry.org/): the reccommended way, since pyasic already uses it by default - [pypoetry](https://python-poetry.org/): the reccommended way, since pyasic already uses it by default. Use version 2.0+
``` ```
poetry install poetry install

View File

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

View File

@@ -157,6 +157,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S19j Pro+ (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19jProPlus
handler: python
options:
show_root_heading: false
heading_level: 0
## S19j XP (Stock) ## S19j XP (Stock)
- [x] Shutdowns - [x] Shutdowns
@@ -170,6 +183,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S19j+ (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19jPlus
handler: python
options:
show_root_heading: false
heading_level: 0
## S19j No PIC (Stock) ## S19j No PIC (Stock)
- [x] Shutdowns - [x] Shutdowns
@@ -287,6 +313,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S19 XP Hydro (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19XPHydro
handler: python
options:
show_root_heading: false
heading_level: 0
## S19+ (BOS+) ## S19+ (BOS+)
- [x] Shutdowns - [x] Shutdowns
@@ -443,6 +482,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S19 Hydro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19Hydro
handler: python
options:
show_root_heading: false
heading_level: 0
## S19 Pro (VNish) ## S19 Pro (VNish)
- [x] Shutdowns - [x] Shutdowns
@@ -456,6 +508,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S19 Pro A (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19ProA
handler: python
options:
show_root_heading: false
heading_level: 0
## S19 Pro Hydro (VNish) ## S19 Pro Hydro (VNish)
- [x] Shutdowns - [x] Shutdowns
@@ -469,6 +534,32 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S19 XP (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19XP
handler: python
options:
show_root_heading: false
heading_level: 0
## S19 XP Hydro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X19.S19.VNishS19XPHydro
handler: python
options:
show_root_heading: false
heading_level: 0
## S19a (VNish) ## S19a (VNish)
- [x] Shutdowns - [x] Shutdowns
@@ -759,7 +850,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19 ::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19
@@ -772,7 +863,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19Pro ::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19Pro
@@ -785,7 +876,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19XP ::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19XP
@@ -798,7 +889,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19jPro ::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19jPro
@@ -811,7 +902,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19jProPlus ::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19jProPlus
@@ -824,7 +915,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19kPro ::: pyasic.miners.antminer.luxos.X19.S19.LUXMinerS19kPro
@@ -837,7 +928,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X19.T19.LUXMinerT19 ::: pyasic.miners.antminer.luxos.X19.T19.LUXMinerT19

View File

@@ -53,6 +53,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S21+ Hydro (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21PlusHydro
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (Stock) ## T21 (Stock)
- [x] Shutdowns - [x] Shutdowns
@@ -79,6 +92,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S21 Hydro (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21Hydro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Pro (BOS+) ## S21 Pro (BOS+)
- [x] Shutdowns - [x] Shutdowns
@@ -92,6 +118,32 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S21+ (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21Plus
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ Hydro (BOS+)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [ ] Presets
::: pyasic.miners.antminer.bosminer.X21.S21.BOSMinerS21PlusHydro
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (BOS+) ## T21 (BOS+)
- [x] Shutdowns - [x] Shutdowns
@@ -118,6 +170,58 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## S21 Hydro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21Hydro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 Pro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21Pro
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21Plus
handler: python
options:
show_root_heading: false
heading_level: 0
## S21+ Hydro (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21PlusHydro
handler: python
options:
show_root_heading: false
heading_level: 0
## T21 (VNish) ## T21 (VNish)
- [x] Shutdowns - [x] Shutdowns
@@ -174,7 +278,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X21.S21.LUXMinerS21 ::: pyasic.miners.antminer.luxos.X21.S21.LUXMinerS21
@@ -183,6 +287,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## T21 (LuxOS)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.luxos.X21.T21.LUXMinerT21
handler: python
options:
show_root_heading: false
heading_level: 0
## S21 (MaraFW) ## S21 (MaraFW)
- [ ] Shutdowns - [ ] Shutdowns

View File

@@ -73,7 +73,7 @@
- [x] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.vnish.X3.L3.VnishL3Plus ::: pyasic.miners.antminer.vnish.X3.L3.VNishL3Plus
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false

View File

@@ -47,7 +47,7 @@
- [x] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.vnish.X7.L7.VnishL7 ::: pyasic.miners.antminer.vnish.X7.L7.VNishL7
handler: python handler: python
options: options:
show_root_heading: false show_root_heading: false

View File

@@ -105,6 +105,19 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## L9 (VNish)
- [x] Shutdowns
- [ ] Power Modes
- [x] Setpoints
- [x] Presets
::: pyasic.miners.antminer.vnish.X9.L9.VNishL9
handler: python
options:
show_root_heading: false
heading_level: 0
## T9 (Hive) ## T9 (Hive)
- [ ] Shutdowns - [ ] Shutdowns
@@ -122,7 +135,7 @@
- [x] Shutdowns - [x] Shutdowns
- [ ] Power Modes - [ ] Power Modes
- [ ] Setpoints - [x] Setpoints
- [x] Presets - [x] Presets
::: pyasic.miners.antminer.luxos.X9.S9.LUXMinerS9 ::: pyasic.miners.antminer.luxos.X9.S9.LUXMinerS9

View File

@@ -0,0 +1,16 @@
# pyasic
## Q Models
## Avalon Q Home (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.Q.Q.CGMinerAvalonQHome
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,6 +1,19 @@
# pyasic # pyasic
## nano Models ## nano Models
## Avalon Nano 3s (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.nano.nano3.CGMinerAvalonNano3s
handler: python
options:
show_root_heading: false
heading_level: 0
## Avalon Nano 3 (Stock) ## Avalon Nano 3 (Stock)
- [ ] Shutdowns - [ ] Shutdowns

View File

@@ -1,6 +1,19 @@
# pyasic # pyasic
## DGX Models ## DGX Models
## DG1 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.elphapex.daoge.DGX.DG1.ElphapexDG1
handler: python
options:
show_root_heading: false
heading_level: 0
## DG1+ (Stock) ## DG1+ (Stock)
- [ ] Shutdowns - [ ] Shutdowns
@@ -14,3 +27,16 @@
show_root_heading: false show_root_heading: false
heading_level: 0 heading_level: 0
## DG1Home (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.elphapex.daoge.DGX.DG1.ElphapexDG1Home
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -0,0 +1,16 @@
# pyasic
## byte Models
## Byte (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.byte.byte.GoldshellByte
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -0,0 +1,16 @@
# pyasic
## Mini Doge Models
## Mini Doge (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.MiniDoge.MiniDoge.GoldshellMiniDoge
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -0,0 +1,16 @@
# pyasic
## mini_doge Models
## Mini Doge (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.mini_doge.mini_doge.GoldshellMiniDoge
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -0,0 +1,16 @@
# pyasic
## ALX Models
## AL3 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.ALX.AL3.IceRiverAL3
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -38,6 +38,7 @@ details {
<details> <details>
<summary>X7 Series:</summary> <summary>X7 Series:</summary>
<ul> <ul>
<li><a href="../antminer/X7#l7-stock">L7 (Stock)</a></li>
<li><a href="../antminer/X7#l7-stock">L7 (Stock)</a></li> <li><a href="../antminer/X7#l7-stock">L7 (Stock)</a></li>
<li><a href="../antminer/X7#k7-stock">K7 (Stock)</a></li> <li><a href="../antminer/X7#k7-stock">K7 (Stock)</a></li>
<li><a href="../antminer/X7#d7-stock">D7 (Stock)</a></li> <li><a href="../antminer/X7#d7-stock">D7 (Stock)</a></li>
@@ -53,6 +54,7 @@ details {
<li><a href="../antminer/X9#s9j-stock">S9j (Stock)</a></li> <li><a href="../antminer/X9#s9j-stock">S9j (Stock)</a></li>
<li><a href="../antminer/X9#t9-stock">T9 (Stock)</a></li> <li><a href="../antminer/X9#t9-stock">T9 (Stock)</a></li>
<li><a href="../antminer/X9#l9-stock">L9 (Stock)</a></li> <li><a href="../antminer/X9#l9-stock">L9 (Stock)</a></li>
<li><a href="../antminer/X9#l9-stock">L9 (Stock)</a></li>
</ul> </ul>
</details> </details>
<details> <details>
@@ -86,6 +88,8 @@ details {
<li><a href="../antminer/X19#s19j-no-pic-stock">S19j No PIC (Stock)</a></li> <li><a href="../antminer/X19#s19j-no-pic-stock">S19j No PIC (Stock)</a></li>
<li><a href="../antminer/X19#s19-pro_1-stock">S19 Pro+ (Stock)</a></li> <li><a href="../antminer/X19#s19-pro_1-stock">S19 Pro+ (Stock)</a></li>
<li><a href="../antminer/X19#s19j-pro-stock">S19j Pro (Stock)</a></li> <li><a href="../antminer/X19#s19j-pro-stock">S19j Pro (Stock)</a></li>
<li><a href="../antminer/X19#s19j_1-stock">S19j+ (Stock)</a></li>
<li><a href="../antminer/X19#s19j-pro_1-stock">S19j Pro+ (Stock)</a></li>
<li><a href="../antminer/X19#s19-xp-stock">S19 XP (Stock)</a></li> <li><a href="../antminer/X19#s19-xp-stock">S19 XP (Stock)</a></li>
<li><a href="../antminer/X19#s19a-stock">S19a (Stock)</a></li> <li><a href="../antminer/X19#s19a-stock">S19a (Stock)</a></li>
<li><a href="../antminer/X19#s19a-pro-stock">S19a Pro (Stock)</a></li> <li><a href="../antminer/X19#s19a-pro-stock">S19a Pro (Stock)</a></li>
@@ -104,6 +108,8 @@ details {
<li><a href="../antminer/X21#s21-stock">S21 (Stock)</a></li> <li><a href="../antminer/X21#s21-stock">S21 (Stock)</a></li>
<li><a href="../antminer/X21#s21-stock">S21 (Stock)</a></li> <li><a href="../antminer/X21#s21-stock">S21 (Stock)</a></li>
<li><a href="../antminer/X21#s21_1-stock">S21+ (Stock)</a></li> <li><a href="../antminer/X21#s21_1-stock">S21+ (Stock)</a></li>
<li><a href="../antminer/X21#s21_1-hydro-stock">S21+ Hydro (Stock)</a></li>
<li><a href="../antminer/X21#s21_1-hydro-stock">S21+ Hydro (Stock)</a></li>
<li><a href="../antminer/X21#s21-pro-stock">S21 Pro (Stock)</a></li> <li><a href="../antminer/X21#s21-pro-stock">S21 Pro (Stock)</a></li>
<li><a href="../antminer/X21#t21-stock">T21 (Stock)</a></li> <li><a href="../antminer/X21#t21-stock">T21 (Stock)</a></li>
<li><a href="../antminer/X21#s21-hydro-stock">S21 Hydro (Stock)</a></li> <li><a href="../antminer/X21#s21-hydro-stock">S21 Hydro (Stock)</a></li>
@@ -553,6 +559,7 @@ details {
<summary>nano Series:</summary> <summary>nano Series:</summary>
<ul> <ul>
<li><a href="../avalonminer/nano#avalon-nano-3-stock">Avalon Nano 3 (Stock)</a></li> <li><a href="../avalonminer/nano#avalon-nano-3-stock">Avalon Nano 3 (Stock)</a></li>
<li><a href="../avalonminer/nano#avalon-nano-3s-stock">Avalon Nano 3s (Stock)</a></li>
</ul> </ul>
</details> </details>
<details> <details>
@@ -561,6 +568,12 @@ details {
<li><a href="../avalonminer/A15X#avalon-1566-stock">Avalon 1566 (Stock)</a></li> <li><a href="../avalonminer/A15X#avalon-1566-stock">Avalon 1566 (Stock)</a></li>
</ul> </ul>
</details> </details>
<details>
<summary>Q Series:</summary>
<ul>
<li><a href="../avalonminer/Q#avalon-q-home-stock">Avalon Q Home (Stock)</a></li>
</ul>
</details>
</ul> </ul>
</details> </details>
<details> <details>
@@ -611,6 +624,18 @@ details {
<li><a href="../goldshell/XBox#kd-box-pro-stock">KD Box Pro (Stock)</a></li> <li><a href="../goldshell/XBox#kd-box-pro-stock">KD Box Pro (Stock)</a></li>
</ul> </ul>
</details> </details>
<details>
<summary>byte Series:</summary>
<ul>
<li><a href="../goldshell/byte#byte-stock">Byte (Stock)</a></li>
</ul>
</details>
<details>
<summary>mini_doge Series:</summary>
<ul>
<li><a href="../goldshell/mini_doge#mini-doge-stock">Mini Doge (Stock)</a></li>
</ul>
</details>
</ul> </ul>
</details> </details>
<details> <details>
@@ -654,6 +679,7 @@ details {
<li><a href="../antminer/X19#s19-xp-bos_1">S19 XP (BOS+)</a></li> <li><a href="../antminer/X19#s19-xp-bos_1">S19 XP (BOS+)</a></li>
<li><a href="../antminer/X19#s19-pro_1-hydro-bos_1">S19 Pro+ Hydro (BOS+)</a></li> <li><a href="../antminer/X19#s19-pro_1-hydro-bos_1">S19 Pro+ Hydro (BOS+)</a></li>
<li><a href="../antminer/X19#t19-bos_1">T19 (BOS+)</a></li> <li><a href="../antminer/X19#t19-bos_1">T19 (BOS+)</a></li>
<li><a href="../antminer/X19#s19-xp-hydro-bos_1">S19 XP Hydro (BOS+)</a></li>
</ul> </ul>
</details> </details>
<details> <details>
@@ -661,6 +687,9 @@ details {
<ul> <ul>
<li><a href="../antminer/X21#s21-bos_1">S21 (BOS+)</a></li> <li><a href="../antminer/X21#s21-bos_1">S21 (BOS+)</a></li>
<li><a href="../antminer/X21#s21-pro-bos_1">S21 Pro (BOS+)</a></li> <li><a href="../antminer/X21#s21-pro-bos_1">S21 Pro (BOS+)</a></li>
<li><a href="../antminer/X21#s21_1-bos_1">S21+ (BOS+)</a></li>
<li><a href="../antminer/X21#s21_1-hydro-bos_1">S21+ Hydro (BOS+)</a></li>
<li><a href="../antminer/X21#s21-hydro-bos_1">S21 Hydro (BOS+)</a></li>
<li><a href="../antminer/X21#t21-bos_1">T21 (BOS+)</a></li> <li><a href="../antminer/X21#t21-bos_1">T21 (BOS+)</a></li>
</ul> </ul>
</details> </details>
@@ -689,6 +718,12 @@ details {
<li><a href="../antminer/X7#l7-vnish">L7 (VNish)</a></li> <li><a href="../antminer/X7#l7-vnish">L7 (VNish)</a></li>
</ul> </ul>
</details> </details>
<details>
<summary>X9 Series:</summary>
<ul>
<li><a href="../antminer/X9#l9-vnish">L9 (VNish)</a></li>
</ul>
</details>
<details> <details>
<summary>X17 Series:</summary> <summary>X17 Series:</summary>
<ul> <ul>
@@ -704,11 +739,16 @@ details {
<li><a href="../antminer/X19#s19-pro-vnish">S19 Pro (VNish)</a></li> <li><a href="../antminer/X19#s19-pro-vnish">S19 Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19j-vnish">S19j (VNish)</a></li> <li><a href="../antminer/X19#s19j-vnish">S19j (VNish)</a></li>
<li><a href="../antminer/X19#s19i-vnish">S19i (VNish)</a></li> <li><a href="../antminer/X19#s19i-vnish">S19i (VNish)</a></li>
<li><a href="../antminer/X19#s19-xp-vnish">S19 XP (VNish)</a></li>
<li><a href="../antminer/X19#s19-xp-hydro-vnish">S19 XP Hydro (VNish)</a></li>
<li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li> <li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li> <li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li> <li><a href="../antminer/X19#s19j-pro-vnish">S19j Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19a-vnish">S19a (VNish)</a></li> <li><a href="../antminer/X19#s19a-vnish">S19a (VNish)</a></li>
<li><a href="../antminer/X19#s19-hydro-vnish">S19 Hydro (VNish)</a></li>
<li><a href="../antminer/X19#s19a-pro-vnish">S19a Pro (VNish)</a></li> <li><a href="../antminer/X19#s19a-pro-vnish">S19a Pro (VNish)</a></li>
<li><a href="../antminer/X19#s19-pro-a-vnish">S19 Pro A (VNish)</a></li>
<li><a href="../antminer/X19#s19-pro-hydro-vnish">S19 Pro Hydro (VNish)</a></li>
<li><a href="../antminer/X19#s19-pro-hydro-vnish">S19 Pro Hydro (VNish)</a></li> <li><a href="../antminer/X19#s19-pro-hydro-vnish">S19 Pro Hydro (VNish)</a></li>
<li><a href="../antminer/X19#s19k-pro-vnish">S19k Pro (VNish)</a></li> <li><a href="../antminer/X19#s19k-pro-vnish">S19k Pro (VNish)</a></li>
<li><a href="../antminer/X19#t19-vnish">T19 (VNish)</a></li> <li><a href="../antminer/X19#t19-vnish">T19 (VNish)</a></li>
@@ -719,6 +759,10 @@ details {
<ul> <ul>
<li><a href="../antminer/X21#t21-vnish">T21 (VNish)</a></li> <li><a href="../antminer/X21#t21-vnish">T21 (VNish)</a></li>
<li><a href="../antminer/X21#s21-vnish">S21 (VNish)</a></li> <li><a href="../antminer/X21#s21-vnish">S21 (VNish)</a></li>
<li><a href="../antminer/X21#s21_1-vnish">S21+ (VNish)</a></li>
<li><a href="../antminer/X21#s21_1-hydro-vnish">S21+ Hydro (VNish)</a></li>
<li><a href="../antminer/X21#s21-pro-vnish">S21 Pro (VNish)</a></li>
<li><a href="../antminer/X21#s21-hydro-vnish">S21 Hydro (VNish)</a></li>
</ul> </ul>
</details> </details>
</ul> </ul>
@@ -814,6 +858,7 @@ details {
<summary>X21 Series:</summary> <summary>X21 Series:</summary>
<ul> <ul>
<li><a href="../antminer/X21#s21-luxos">S21 (LuxOS)</a></li> <li><a href="../antminer/X21#s21-luxos">S21 (LuxOS)</a></li>
<li><a href="../antminer/X21#t21-luxos">T21 (LuxOS)</a></li>
</ul> </ul>
</details> </details>
</ul> </ul>
@@ -912,6 +957,12 @@ details {
<li><a href="../iceriver/KSX#ks5m-stock">KS5M (Stock)</a></li> <li><a href="../iceriver/KSX#ks5m-stock">KS5M (Stock)</a></li>
</ul> </ul>
</details> </details>
<details>
<summary>ALX Series:</summary>
<ul>
<li><a href="../iceriver/ALX#al3-stock">AL3 (Stock)</a></li>
</ul>
</details>
</ul> </ul>
</details> </details>
<details> <details>
@@ -943,6 +994,8 @@ details {
<summary>DGX Series:</summary> <summary>DGX Series:</summary>
<ul> <ul>
<li><a href="../elphapex/DGX#dg1_1-stock">DG1+ (Stock)</a></li> <li><a href="../elphapex/DGX#dg1_1-stock">DG1+ (Stock)</a></li>
<li><a href="../elphapex/DGX#dg1-stock">DG1 (Stock)</a></li>
<li><a href="../elphapex/DGX#dg1home-stock">DG1Home (Stock)</a></li>
</ul> </ul>
</details> </details>
</ul> </ul>

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK10 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK10
@@ -18,7 +18,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK20 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK20
@@ -31,7 +31,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK30
@@ -44,7 +44,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK40 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK40
@@ -57,7 +57,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK50 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK50
@@ -70,7 +70,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK60 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVK60
@@ -83,7 +83,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL20 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL20
@@ -96,7 +96,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL30
@@ -109,7 +109,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL40 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL40
@@ -122,7 +122,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL50 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL50
@@ -135,7 +135,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL60 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus_Plus.BTMinerM50SPlusPlusVL60
@@ -148,7 +148,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH30
@@ -161,7 +161,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH40 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVH40
@@ -174,7 +174,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ30
@@ -187,7 +187,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ40 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ40
@@ -200,7 +200,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ60 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVJ60
@@ -213,7 +213,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVK10 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVK10
@@ -226,7 +226,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVK20 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVK20
@@ -239,7 +239,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVK30
@@ -252,7 +252,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVL10 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVL10
@@ -265,7 +265,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVL20 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVL20
@@ -278,7 +278,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVL30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S_Plus.BTMinerM50SPlusVL30
@@ -291,7 +291,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH10 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH10
@@ -304,7 +304,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH20 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH20
@@ -317,7 +317,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH30
@@ -330,7 +330,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH40 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH40
@@ -343,7 +343,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH50 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVH50
@@ -356,7 +356,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ10 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ10
@@ -369,7 +369,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ20 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ20
@@ -382,7 +382,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ30
@@ -395,7 +395,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ40 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ40
@@ -408,7 +408,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ50 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVJ50
@@ -421,7 +421,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK10 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK10
@@ -434,7 +434,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK20 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK20
@@ -447,7 +447,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK30
@@ -460,7 +460,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK50 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK50
@@ -473,7 +473,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK60 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK60
@@ -486,7 +486,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK70 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK70
@@ -499,7 +499,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK80 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVK80
@@ -512,7 +512,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVL20 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVL20
@@ -525,7 +525,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVL30 ::: pyasic.miners.whatsminer.btminer.M5X.M50S.BTMinerM50SVL30
@@ -538,7 +538,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VE30 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VE30
@@ -551,7 +551,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VG30 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VG30
@@ -564,7 +564,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH10 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH10
@@ -577,7 +577,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH20 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH20
@@ -590,7 +590,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH30 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH30
@@ -603,7 +603,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH40 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH40
@@ -616,7 +616,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH50 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH50
@@ -629,7 +629,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH60 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH60
@@ -642,7 +642,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH70 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH70
@@ -655,7 +655,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH80 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH80
@@ -668,7 +668,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH90 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH90
@@ -681,7 +681,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ10 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ10
@@ -694,7 +694,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ20 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ20
@@ -707,7 +707,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ30 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ30
@@ -720,7 +720,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ40 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ40
@@ -733,7 +733,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ60 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VJ60
@@ -746,7 +746,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VK40 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VK40
@@ -759,7 +759,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VK50 ::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VK50
@@ -772,7 +772,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M52S_Plus_Plus.BTMinerM52SPlusPlusVL10 ::: pyasic.miners.whatsminer.btminer.M5X.M52S_Plus_Plus.BTMinerM52SPlusPlusVL10
@@ -785,7 +785,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M52S.BTMinerM52SVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M52S.BTMinerM52SVK30
@@ -798,7 +798,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53H.BTMinerM53HVH10 ::: pyasic.miners.whatsminer.btminer.M5X.M53H.BTMinerM53HVH10
@@ -811,7 +811,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVK10 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVK10
@@ -824,7 +824,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVK20 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVK20
@@ -837,7 +837,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVK30
@@ -850,7 +850,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVK50 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVK50
@@ -863,7 +863,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVL10 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVL10
@@ -876,7 +876,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVL30 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus_Plus.BTMinerM53SPlusPlusVL30
@@ -889,7 +889,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ30 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ30
@@ -902,7 +902,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ40 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ40
@@ -915,7 +915,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ50 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVJ50
@@ -928,7 +928,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M53S_Plus.BTMinerM53SPlusVK30
@@ -941,7 +941,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVH20 ::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVH20
@@ -954,7 +954,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVH30 ::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVH30
@@ -967,7 +967,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVJ30 ::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVJ30
@@ -980,7 +980,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVJ40 ::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVJ40
@@ -993,7 +993,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M53S.BTMinerM53SVK30
@@ -1006,7 +1006,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VH30 ::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VH30
@@ -1019,7 +1019,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VH40 ::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VH40
@@ -1032,7 +1032,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VH50 ::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VH50
@@ -1045,7 +1045,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VK30 ::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VK30
@@ -1058,7 +1058,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VK60 ::: pyasic.miners.whatsminer.btminer.M5X.M53.BTMinerM53VK60
@@ -1071,7 +1071,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M54S_Plus_Plus.BTMinerM54SPlusPlusVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M54S_Plus_Plus.BTMinerM54SPlusPlusVK30
@@ -1084,7 +1084,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M54S_Plus_Plus.BTMinerM54SPlusPlusVL30 ::: pyasic.miners.whatsminer.btminer.M5X.M54S_Plus_Plus.BTMinerM54SPlusPlusVL30
@@ -1097,7 +1097,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M54S_Plus_Plus.BTMinerM54SPlusPlusVL40 ::: pyasic.miners.whatsminer.btminer.M5X.M54S_Plus_Plus.BTMinerM54SPlusPlusVL40
@@ -1110,7 +1110,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus_Plus.BTMinerM56SPlusPlusVK10 ::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus_Plus.BTMinerM56SPlusPlusVK10
@@ -1123,7 +1123,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus_Plus.BTMinerM56SPlusPlusVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus_Plus.BTMinerM56SPlusPlusVK30
@@ -1136,7 +1136,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus_Plus.BTMinerM56SPlusPlusVK40 ::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus_Plus.BTMinerM56SPlusPlusVK40
@@ -1149,7 +1149,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus_Plus.BTMinerM56SPlusPlusVK50 ::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus_Plus.BTMinerM56SPlusPlusVK50
@@ -1162,7 +1162,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVJ30 ::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVJ30
@@ -1175,7 +1175,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVK30 ::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVK30
@@ -1188,7 +1188,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVK40 ::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVK40
@@ -1201,7 +1201,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVK50 ::: pyasic.miners.whatsminer.btminer.M5X.M56S_Plus.BTMinerM56SPlusVK50
@@ -1214,7 +1214,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S.BTMinerM56SVH30 ::: pyasic.miners.whatsminer.btminer.M5X.M56S.BTMinerM56SVH30
@@ -1227,7 +1227,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S.BTMinerM56SVJ30 ::: pyasic.miners.whatsminer.btminer.M5X.M56S.BTMinerM56SVJ30
@@ -1240,7 +1240,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56S.BTMinerM56SVJ40 ::: pyasic.miners.whatsminer.btminer.M5X.M56S.BTMinerM56SVJ40
@@ -1253,7 +1253,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M56.BTMinerM56VH30 ::: pyasic.miners.whatsminer.btminer.M5X.M56.BTMinerM56VH30
@@ -1266,7 +1266,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M5X.M59.BTMinerM59VH30 ::: pyasic.miners.whatsminer.btminer.M5X.M59.BTMinerM59VH30

View File

@@ -5,7 +5,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus_Plus.BTMinerM60SPlusPlusVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus_Plus.BTMinerM60SPlusPlusVL30
@@ -18,7 +18,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus_Plus.BTMinerM60SPlusPlusVL40 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus_Plus.BTMinerM60SPlusPlusVL40
@@ -31,7 +31,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK30
@@ -44,7 +44,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK40 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK40
@@ -57,7 +57,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK50 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK50
@@ -70,7 +70,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK60 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK60
@@ -83,7 +83,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK70 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVK70
@@ -96,7 +96,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL10 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL10
@@ -109,7 +109,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL30
@@ -122,7 +122,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL40 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL40
@@ -135,7 +135,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL50 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL50
@@ -148,7 +148,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL60 ::: pyasic.miners.whatsminer.btminer.M6X.M60S_Plus.BTMinerM60SPlusVL60
@@ -161,7 +161,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVK10 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVK10
@@ -174,7 +174,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVK20 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVK20
@@ -187,7 +187,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVK30
@@ -200,7 +200,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVK40 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVK40
@@ -213,7 +213,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL10 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL10
@@ -226,7 +226,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL20 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL20
@@ -239,7 +239,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL30
@@ -252,7 +252,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL40 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL40
@@ -265,7 +265,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL50 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL50
@@ -278,7 +278,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL60 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL60
@@ -291,7 +291,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL70 ::: pyasic.miners.whatsminer.btminer.M6X.M60S.BTMinerM60SVL70
@@ -304,7 +304,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK10 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK10
@@ -317,7 +317,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK20 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK20
@@ -330,7 +330,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK30 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK30
@@ -343,7 +343,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK40 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK40
@@ -356,7 +356,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK6A ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VK6A
@@ -369,7 +369,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL10 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL10
@@ -382,7 +382,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL20 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL20
@@ -395,7 +395,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL30 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL30
@@ -408,7 +408,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL40 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL40
@@ -421,7 +421,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL50 ::: pyasic.miners.whatsminer.btminer.M6X.M60.BTMinerM60VL50
@@ -434,7 +434,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61S_Plus.BTMinerM61SPlusVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M61S_Plus.BTMinerM61SPlusVL30
@@ -447,7 +447,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61S.BTMinerM61SVL10 ::: pyasic.miners.whatsminer.btminer.M6X.M61S.BTMinerM61SVL10
@@ -460,7 +460,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61S.BTMinerM61SVL20 ::: pyasic.miners.whatsminer.btminer.M6X.M61S.BTMinerM61SVL20
@@ -473,7 +473,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61S.BTMinerM61SVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M61S.BTMinerM61SVL30
@@ -486,7 +486,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VK10 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VK10
@@ -499,7 +499,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VK20 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VK20
@@ -512,7 +512,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VK30 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VK30
@@ -525,7 +525,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VK40 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VK40
@@ -538,7 +538,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL10 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL10
@@ -551,7 +551,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL30 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL30
@@ -564,7 +564,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL40 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL40
@@ -577,7 +577,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL50 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL50
@@ -590,7 +590,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL60 ::: pyasic.miners.whatsminer.btminer.M6X.M61.BTMinerM61VL60
@@ -603,7 +603,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M62S_Plus.BTMinerM62SPlusVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M62S_Plus.BTMinerM62SPlusVK30
@@ -616,7 +616,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus_Plus.BTMinerM63SPlusPlusVL20 ::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus_Plus.BTMinerM63SPlusPlusVL20
@@ -629,7 +629,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVK30
@@ -642,7 +642,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVL10 ::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVL10
@@ -655,7 +655,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVL20 ::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVL20
@@ -668,7 +668,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVL30
@@ -681,7 +681,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVL50 ::: pyasic.miners.whatsminer.btminer.M6X.M63S_Plus.BTMinerM63SPlusVL50
@@ -694,7 +694,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVK10 ::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVK10
@@ -707,7 +707,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVK20 ::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVK20
@@ -720,7 +720,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVK30
@@ -733,7 +733,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVK60 ::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVK60
@@ -746,7 +746,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVL10 ::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVL10
@@ -759,7 +759,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVL50 ::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVL50
@@ -772,7 +772,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVL60 ::: pyasic.miners.whatsminer.btminer.M6X.M63S.BTMinerM63SVL60
@@ -785,7 +785,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VK10 ::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VK10
@@ -798,7 +798,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VK20 ::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VK20
@@ -811,7 +811,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VK30 ::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VK30
@@ -824,7 +824,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VL10 ::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VL10
@@ -837,7 +837,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VL30 ::: pyasic.miners.whatsminer.btminer.M6X.M63.BTMinerM63VL30
@@ -850,7 +850,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M64S.BTMinerM64SVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M64S.BTMinerM64SVL30
@@ -863,7 +863,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M64.BTMinerM64VL30 ::: pyasic.miners.whatsminer.btminer.M6X.M64.BTMinerM64VL30
@@ -876,7 +876,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M64.BTMinerM64VL40 ::: pyasic.miners.whatsminer.btminer.M6X.M64.BTMinerM64VL40
@@ -889,7 +889,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M65S_Plus.BTMinerM65SPlusVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M65S_Plus.BTMinerM65SPlusVK30
@@ -902,7 +902,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M65S.BTMinerM65SVK20 ::: pyasic.miners.whatsminer.btminer.M6X.M65S.BTMinerM65SVK20
@@ -915,7 +915,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M65S.BTMinerM65SVL60 ::: pyasic.miners.whatsminer.btminer.M6X.M65S.BTMinerM65SVL60
@@ -928,7 +928,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus_Plus.BTMinerM66SPlusPlusVL20 ::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus_Plus.BTMinerM66SPlusPlusVL20
@@ -941,7 +941,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVK30
@@ -954,7 +954,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL10 ::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL10
@@ -967,7 +967,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL20 ::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL20
@@ -980,7 +980,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL30
@@ -993,7 +993,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL40 ::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL40
@@ -1006,7 +1006,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL60 ::: pyasic.miners.whatsminer.btminer.M6X.M66S_Plus.BTMinerM66SPlusVL60
@@ -1019,7 +1019,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK20 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK20
@@ -1032,7 +1032,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK30
@@ -1045,7 +1045,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK40 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK40
@@ -1058,7 +1058,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK50 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK50
@@ -1071,7 +1071,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK60 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVK60
@@ -1084,7 +1084,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL10 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL10
@@ -1097,7 +1097,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL20 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL20
@@ -1110,7 +1110,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL30 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL30
@@ -1123,7 +1123,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL40 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL40
@@ -1136,7 +1136,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL50 ::: pyasic.miners.whatsminer.btminer.M6X.M66S.BTMinerM66SVL50
@@ -1149,7 +1149,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66.BTMinerM66VK20 ::: pyasic.miners.whatsminer.btminer.M6X.M66.BTMinerM66VK20
@@ -1162,7 +1162,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66.BTMinerM66VK30 ::: pyasic.miners.whatsminer.btminer.M6X.M66.BTMinerM66VK30
@@ -1175,7 +1175,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66.BTMinerM66VL20 ::: pyasic.miners.whatsminer.btminer.M6X.M66.BTMinerM66VL20
@@ -1188,7 +1188,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M66.BTMinerM66VL30 ::: pyasic.miners.whatsminer.btminer.M6X.M66.BTMinerM66VL30
@@ -1201,7 +1201,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M6X.M67S.BTMinerM67SVK30 ::: pyasic.miners.whatsminer.btminer.M6X.M67S.BTMinerM67SVK30

View File

@@ -5,7 +5,7 @@
- [x] Shutdowns - [x] Shutdowns
- [x] Power Modes - [x] Power Modes
- [x] Setpoints - [ ] Setpoints
- [ ] Presets - [ ] Presets
::: pyasic.miners.whatsminer.btminer.M7X.M70.BTMinerM70VM30 ::: pyasic.miners.whatsminer.btminer.M7X.M70.BTMinerM70VM30

View File

@@ -97,6 +97,8 @@ nav:
- Innosilicon T3X: "miners/innosilicon/T3X.md" - Innosilicon T3X: "miners/innosilicon/T3X.md"
- Innosilicon A10X: "miners/innosilicon/A10X.md" - Innosilicon A10X: "miners/innosilicon/A10X.md"
- Innosilicon A11X: "miners/innosilicon/A11X.md" - Innosilicon A11X: "miners/innosilicon/A11X.md"
- Goldshell Byte: "miners/goldshell/Byte.md"
- Goldshell Mini Doge: "miners/goldshell/MiniDoge.md"
- Goldshell X5: "miners/goldshell/X5.md" - Goldshell X5: "miners/goldshell/X5.md"
- Goldshell XMax: "miners/goldshell/XMax.md" - Goldshell XMax: "miners/goldshell/XMax.md"
- Goldshell XBox: "miners/goldshell/XBox.md" - Goldshell XBox: "miners/goldshell/XBox.md"

1148
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,10 +14,41 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pyasic.config.fans import FanMode, FanModeConfig, FanModeNormal from pyasic.config.fans import (
from pyasic.config.mining import MiningMode, MiningModeConfig FanModeConfig,
FanModeImmersion,
FanModeManual,
FanModeNormal,
)
from pyasic.config.mining import (
MiningModeConfig,
MiningModeHashrateTune,
MiningModeHPM,
MiningModeLPM,
MiningModeManual,
MiningModeNormal,
MiningModePowerTune,
MiningModePreset,
MiningModeSleep,
)
# Type aliases for config field types
FanModeType = FanModeNormal | FanModeManual | FanModeImmersion | FanModeConfig
MiningModeType = (
MiningModeNormal
| MiningModeHPM
| MiningModeLPM
| MiningModeSleep
| MiningModeManual
| MiningModePowerTune
| MiningModeHashrateTune
| MiningModePreset
| MiningModeConfig
)
from pyasic.config.mining.scaling import ScalingConfig from pyasic.config.mining.scaling import ScalingConfig
from pyasic.config.pools import PoolConfig from pyasic.config.pools import PoolConfig
from pyasic.config.temperature import TemperatureConfig from pyasic.config.temperature import TemperatureConfig
@@ -32,11 +63,11 @@ class MinerConfig(BaseModel):
arbitrary_types_allowed = True arbitrary_types_allowed = True
pools: PoolConfig = Field(default_factory=PoolConfig.default) pools: PoolConfig = Field(default_factory=PoolConfig.default)
fan_mode: FanMode = Field(default_factory=FanModeConfig.default) fan_mode: FanModeType = Field(default_factory=FanModeConfig.default)
temperature: TemperatureConfig = Field(default_factory=TemperatureConfig.default) temperature: TemperatureConfig = Field(default_factory=TemperatureConfig.default)
mining_mode: MiningMode = Field(default_factory=MiningModeConfig.default) mining_mode: MiningModeType = Field(default_factory=MiningModeConfig.default)
def __getitem__(self, item): def __getitem__(self, item: str) -> Any:
try: try:
return getattr(self, item) return getattr(self, item)
except AttributeError: except AttributeError:
@@ -85,6 +116,13 @@ class MinerConfig(BaseModel):
**self.temperature.as_wm(), **self.temperature.as_wm(),
} }
def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
"""Generates the configuration in the format suitable for Whatsminers running BTMiner V3."""
return {
"set.miner.pools": self.pools.as_btminer_v3(),
**self.mining_mode.as_btminer_v3(),
}
def as_am_old(self, user_suffix: str | None = None) -> dict: def as_am_old(self, user_suffix: str | None = None) -> dict:
"""Generates the configuration in the format suitable for old versions of Antminers.""" """Generates the configuration in the format suitable for old versions of Antminers."""
return { return {
@@ -247,6 +285,16 @@ class MinerConfig(BaseModel):
"""Constructs a MinerConfig object from web configuration for Goldshell miners.""" """Constructs a MinerConfig object from web configuration for Goldshell miners."""
return cls(pools=PoolConfig.from_am_modern(web_conf)) return cls(pools=PoolConfig.from_am_modern(web_conf))
@classmethod
def from_goldshell_list(cls, web_conf: list) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for Goldshell miners."""
return cls(pools=PoolConfig.from_goldshell(web_conf))
@classmethod
def from_goldshell_byte(cls, web_conf: list) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for Goldshell Byte miners."""
return cls(pools=PoolConfig.from_goldshell_byte(web_conf))
@classmethod @classmethod
def from_inno(cls, web_pools: list) -> "MinerConfig": def from_inno(cls, web_pools: list) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for Innosilicon miners.""" """Constructs a MinerConfig object from web configuration for Innosilicon miners."""
@@ -283,13 +331,17 @@ class MinerConfig(BaseModel):
) )
@classmethod @classmethod
def from_vnish(cls, web_settings: dict, web_presets: list[dict]) -> "MinerConfig": def from_vnish(
cls, web_settings: dict, web_presets: list[dict], web_perf_summary: dict
) -> "MinerConfig":
"""Constructs a MinerConfig object from web settings for VNish miners.""" """Constructs a MinerConfig object from web settings for VNish miners."""
return cls( return cls(
pools=PoolConfig.from_vnish(web_settings), pools=PoolConfig.from_vnish(web_settings),
fan_mode=FanModeConfig.from_vnish(web_settings), fan_mode=FanModeConfig.from_vnish(web_settings),
temperature=TemperatureConfig.from_vnish(web_settings), temperature=TemperatureConfig.from_vnish(web_settings),
mining_mode=MiningModeConfig.from_vnish(web_settings, web_presets), mining_mode=MiningModeConfig.from_vnish(
web_settings, web_presets, web_perf_summary
),
) )
@classmethod @classmethod
@@ -346,3 +398,14 @@ class MinerConfig(BaseModel):
@classmethod @classmethod
def from_hammer(cls, *args, **kwargs) -> "MinerConfig": def from_hammer(cls, *args, **kwargs) -> "MinerConfig":
return cls.from_am_modern(*args, **kwargs) return cls.from_am_modern(*args, **kwargs)
@classmethod
def from_btminer_v3(
cls, rpc_pools: dict, rpc_settings: dict, rpc_device_info: dict
) -> "MinerConfig":
return cls(
pools=PoolConfig.from_btminer_v3(rpc_pools=rpc_pools["msg"]),
mining_mode=MiningModeConfig.from_btminer_v3(
rpc_device_info=rpc_device_info, rpc_settings=rpc_settings
),
)

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import field from dataclasses import field
from typing import TypeVar, Union from typing import Any, TypeVar
from pyasic import settings from pyasic import settings
from pyasic.config.base import MinerConfigOption, MinerConfigValue from pyasic.config.base import MinerConfigOption, MinerConfigValue
@@ -35,7 +35,14 @@ from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
TunerPerformanceMode, TunerPerformanceMode,
) )
from .algo import TunerAlgo, TunerAlgoType from .algo import (
BoardTuneAlgo,
ChipTuneAlgo,
StandardTuneAlgo,
TunerAlgo,
TunerAlgoType,
VOptAlgo,
)
from .presets import MiningPreset from .presets import MiningPreset
from .scaling import ScalingConfig from .scaling import ScalingConfig
@@ -44,7 +51,7 @@ class MiningModeNormal(MinerConfigValue):
mode: str = field(init=False, default="normal") mode: str = field(init=False, default="normal")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeNormal": def from_dict(cls, dict_conf: dict | None) -> MiningModeNormal:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -63,6 +70,9 @@ class MiningModeNormal(MinerConfigValue):
def as_wm(self) -> dict: def as_wm(self) -> dict:
return {"mode": self.mode} return {"mode": self.mode}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
def as_auradine(self) -> dict: def as_auradine(self) -> dict:
return {"mode": {"mode": self.mode}} return {"mode": {"mode": self.mode}}
@@ -90,7 +100,7 @@ class MiningModeSleep(MinerConfigValue):
mode: str = field(init=False, default="sleep") mode: str = field(init=False, default="sleep")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeSleep": def from_dict(cls, dict_conf: dict | None) -> MiningModeSleep:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -109,6 +119,9 @@ class MiningModeSleep(MinerConfigValue):
def as_wm(self) -> dict: def as_wm(self) -> dict:
return {"mode": self.mode} return {"mode": self.mode}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "stop"}
def as_auradine(self) -> dict: def as_auradine(self) -> dict:
return {"mode": {"sleep": "on"}} return {"mode": {"sleep": "on"}}
@@ -130,7 +143,7 @@ class MiningModeLPM(MinerConfigValue):
mode: str = field(init=False, default="low") mode: str = field(init=False, default="low")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeLPM": def from_dict(cls, dict_conf: dict | None) -> MiningModeLPM:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -149,6 +162,9 @@ class MiningModeLPM(MinerConfigValue):
def as_wm(self) -> dict: def as_wm(self) -> dict:
return {"mode": self.mode} return {"mode": self.mode}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
def as_auradine(self) -> dict: def as_auradine(self) -> dict:
return {"mode": {"mode": "eco"}} return {"mode": {"mode": "eco"}}
@@ -160,7 +176,7 @@ class MiningModeHPM(MinerConfigValue):
mode: str = field(init=False, default="high") mode: str = field(init=False, default="high")
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHPM": def from_dict(cls, dict_conf: dict | None) -> MiningModeHPM:
return cls() return cls()
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -179,6 +195,9 @@ class MiningModeHPM(MinerConfigValue):
def as_wm(self) -> dict: def as_wm(self) -> dict:
return {"mode": self.mode} return {"mode": self.mode}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "start", "set.miner.power_mode": self.mode}
def as_auradine(self) -> dict: def as_auradine(self) -> dict:
return {"mode": {"mode": "turbo"}} return {"mode": {"mode": "turbo"}}
@@ -189,11 +208,15 @@ class MiningModePowerTune(MinerConfigValue):
mode: str = field(init=False, default="power_tuning") mode: str = field(init=False, default="power_tuning")
power: int | None = None power: int | None = None
algo: TunerAlgoType = field(default_factory=TunerAlgo.default) algo: StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo = field(
default_factory=TunerAlgo.default
)
scaling: ScalingConfig | None = None scaling: ScalingConfig | None = None
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModePowerTune": def from_dict(cls, dict_conf: dict | None) -> MiningModePowerTune:
if dict_conf is None:
return cls()
cls_conf = {} cls_conf = {}
if dict_conf.get("power"): if dict_conf.get("power"):
cls_conf["power"] = dict_conf["power"] cls_conf["power"] = dict_conf["power"]
@@ -222,6 +245,9 @@ class MiningModePowerTune(MinerConfigValue):
return {"mode": self.mode, self.mode: {"wattage": self.power}} return {"mode": self.mode, self.mode: {"wattage": self.power}}
return {} return {}
def as_btminer_v3(self) -> dict:
return {"set.miner.service": "start", "set.miner.power_limit": self.power}
def as_bosminer(self) -> dict: def as_bosminer(self) -> dict:
tuning_cfg = {"enabled": True, "mode": "power_target"} tuning_cfg = {"enabled": True, "mode": "power_target"}
if self.power is not None: if self.power is not None:
@@ -230,25 +256,27 @@ class MiningModePowerTune(MinerConfigValue):
cfg = {"autotuning": tuning_cfg} cfg = {"autotuning": tuning_cfg}
if self.scaling is not None: if self.scaling is not None:
scaling_cfg = {"enabled": True} scaling_cfg: dict[str, Any] = {"enabled": True}
if self.scaling.step is not None: if self.scaling.step is not None:
scaling_cfg["power_step"] = self.scaling.step scaling_cfg["power_step"] = self.scaling.step
if self.scaling.minimum is not None: if self.scaling.minimum is not None:
scaling_cfg["min_power_target"] = self.scaling.minimum scaling_cfg["min_power_target"] = self.scaling.minimum
if self.scaling.shutdown is not None: if self.scaling.shutdown is not None:
scaling_cfg = {**scaling_cfg, **self.scaling.shutdown.as_bosminer()} scaling_cfg.update(self.scaling.shutdown.as_bosminer())
cfg["performance_scaling"] = scaling_cfg cfg["performance_scaling"] = scaling_cfg
return cfg return cfg
def as_boser(self) -> dict: def as_boser(self) -> dict:
cfg = { cfg: dict[str, Any] = {
"set_performance_mode": SetPerformanceModeRequest( "set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_AND_APPLY, save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
mode=PerformanceMode( mode=PerformanceMode(
tuner_mode=TunerPerformanceMode( tuner_mode=TunerPerformanceMode(
power_target=PowerTargetMode( power_target=PowerTargetMode(
power_target=Power(watt=self.power) power_target=Power(watt=self.power)
if self.power is not None
else None # type: ignore[arg-type]
) )
) )
), ),
@@ -258,11 +286,15 @@ class MiningModePowerTune(MinerConfigValue):
sd_cfg = {} sd_cfg = {}
if self.scaling.shutdown is not None: if self.scaling.shutdown is not None:
sd_cfg = self.scaling.shutdown.as_boser() sd_cfg = self.scaling.shutdown.as_boser()
power_target_kwargs = {"power_step": Power(self.scaling.step)} power_target_kwargs: dict[str, Any] = {}
if self.scaling.step is not None:
power_target_kwargs["power_step"] = Power(watt=self.scaling.step)
if self.scaling.minimum is not None: if self.scaling.minimum is not None:
power_target_kwargs["min_power_target"] = Power(self.scaling.minimum) power_target_kwargs["min_power_target"] = Power(
watt=self.scaling.minimum
)
cfg["set_dps"] = SetDpsRequest( cfg["set_dps"] = SetDpsRequest(
save_action=SaveAction.SAVE_AND_APPLY, save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
enable=True, enable=True,
**sd_cfg, **sd_cfg,
target=DpsTarget(power_target=DpsPowerTarget(**power_target_kwargs)), target=DpsTarget(power_target=DpsPowerTarget(**power_target_kwargs)),
@@ -294,11 +326,15 @@ class MiningModeHashrateTune(MinerConfigValue):
mode: str = field(init=False, default="hashrate_tuning") mode: str = field(init=False, default="hashrate_tuning")
hashrate: int | None = None hashrate: int | None = None
algo: TunerAlgoType = field(default_factory=TunerAlgo.default) algo: StandardTuneAlgo | VOptAlgo | BoardTuneAlgo | ChipTuneAlgo = field(
default_factory=TunerAlgo.default
)
scaling: ScalingConfig | None = None scaling: ScalingConfig | None = None
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeHashrateTune": def from_dict(cls, dict_conf: dict | None) -> MiningModeHashrateTune:
if dict_conf is None:
return cls()
cls_conf = {} cls_conf = {}
if dict_conf.get("hashrate"): if dict_conf.get("hashrate"):
cls_conf["hashrate"] = dict_conf["hashrate"] cls_conf["hashrate"] = dict_conf["hashrate"]
@@ -328,16 +364,17 @@ class MiningModeHashrateTune(MinerConfigValue):
conf["hashrate_target"] = self.hashrate conf["hashrate_target"] = self.hashrate
return {"autotuning": conf} return {"autotuning": conf}
@property
def as_boser(self) -> dict: def as_boser(self) -> dict:
cfg = { cfg: dict[str, Any] = {
"set_performance_mode": SetPerformanceModeRequest( "set_performance_mode": SetPerformanceModeRequest(
save_action=SaveAction.SAVE_AND_APPLY, save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
mode=PerformanceMode( mode=PerformanceMode(
tuner_mode=TunerPerformanceMode( tuner_mode=TunerPerformanceMode(
hashrate_target=HashrateTargetMode( hashrate_target=HashrateTargetMode(
hashrate_target=TeraHashrate( hashrate_target=TeraHashrate(
terahash_per_second=self.hashrate terahash_per_second=float(self.hashrate)
if self.hashrate is not None
else None # type: ignore[arg-type]
) )
) )
) )
@@ -348,14 +385,21 @@ class MiningModeHashrateTune(MinerConfigValue):
sd_cfg = {} sd_cfg = {}
if self.scaling.shutdown is not None: if self.scaling.shutdown is not None:
sd_cfg = self.scaling.shutdown.as_boser() sd_cfg = self.scaling.shutdown.as_boser()
hashrate_target_kwargs: dict[str, Any] = {}
if self.scaling.step is not None:
hashrate_target_kwargs["hashrate_step"] = TeraHashrate(
terahash_per_second=float(self.scaling.step)
)
if self.scaling.minimum is not None:
hashrate_target_kwargs["min_hashrate_target"] = TeraHashrate(
terahash_per_second=float(self.scaling.minimum)
)
cfg["set_dps"] = SetDpsRequest( cfg["set_dps"] = SetDpsRequest(
save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
enable=True, enable=True,
**sd_cfg, **sd_cfg,
target=DpsTarget( target=DpsTarget(
hashrate_target=DpsHashrateTarget( hashrate_target=DpsHashrateTarget(**hashrate_target_kwargs)
hashrate_step=TeraHashrate(self.scaling.step),
min_hashrate_target=TeraHashrate(self.scaling.minimum),
)
), ),
) )
@@ -367,7 +411,11 @@ class MiningModeHashrateTune(MinerConfigValue):
def as_epic(self) -> dict: def as_epic(self) -> dict:
mode = { mode = {
"ptune": { "ptune": {
"algo": self.algo.as_epic(), "algo": (
self.algo.as_epic()
if hasattr(self.algo, "as_epic")
else TunerAlgo.default().as_epic()
),
"target": self.hashrate, "target": self.hashrate,
} }
} }
@@ -404,19 +452,25 @@ class MiningModePreset(MinerConfigValue):
@classmethod @classmethod
def from_vnish( def from_vnish(
cls, web_overclock_settings: dict, web_presets: list[dict] cls,
) -> "MiningModePreset": web_overclock_settings: dict,
active_preset = None web_presets: list[dict],
for preset in web_presets: web_perf_summary: dict,
if preset["name"] == web_overclock_settings["preset"]: ) -> MiningModePreset:
active_preset = preset active_preset = web_perf_summary.get("current_preset")
if active_preset is None:
for preset in web_presets:
if preset["name"] == web_overclock_settings["preset"]:
active_preset = preset
return cls( return cls(
active_preset=MiningPreset.from_vnish(active_preset), active_preset=MiningPreset.from_vnish(active_preset or {}),
available_presets=[MiningPreset.from_vnish(p) for p in web_presets], available_presets=[MiningPreset.from_vnish(p) for p in web_presets],
) )
@classmethod @classmethod
def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> "MiningModePreset": def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> MiningModePreset:
active_preset = cls.get_active_preset_from_luxos(rpc_config, rpc_profiles) active_preset = cls.get_active_preset_from_luxos(rpc_config, rpc_profiles)
return cls( return cls(
active_preset=active_preset, active_preset=active_preset,
@@ -434,7 +488,7 @@ class MiningModePreset(MinerConfigValue):
for profile in rpc_profiles["PROFILES"]: for profile in rpc_profiles["PROFILES"]:
if profile["Profile Name"] == active_profile: if profile["Profile Name"] == active_profile:
active_preset = profile active_preset = profile
return MiningPreset.from_luxos(active_preset) return MiningPreset.from_luxos(active_preset or {})
class ManualBoardSettings(MinerConfigValue): class ManualBoardSettings(MinerConfigValue):
@@ -442,7 +496,7 @@ class ManualBoardSettings(MinerConfigValue):
volt: float volt: float
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "ManualBoardSettings": def from_dict(cls, dict_conf: dict) -> ManualBoardSettings:
return cls(freq=dict_conf["freq"], volt=dict_conf["volt"]) return cls(freq=dict_conf["freq"], volt=dict_conf["volt"])
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -470,11 +524,15 @@ class MiningModeManual(MinerConfigValue):
boards: dict[int, ManualBoardSettings] = field(default_factory=dict) boards: dict[int, ManualBoardSettings] = field(default_factory=dict)
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "MiningModeManual": def from_dict(cls, dict_conf: dict) -> MiningModeManual:
return cls( return cls(
global_freq=dict_conf["global_freq"], global_freq=dict_conf["global_freq"],
global_volt=dict_conf["global_volt"], global_volt=dict_conf["global_volt"],
boards={i: ManualBoardSettings.from_dict(dict_conf[i]) for i in dict_conf}, boards={
i: ManualBoardSettings.from_dict(dict_conf[i])
for i in dict_conf
if isinstance(i, int)
},
) )
def as_am_modern(self) -> dict: def as_am_modern(self) -> dict:
@@ -498,7 +556,7 @@ class MiningModeManual(MinerConfigValue):
} }
@classmethod @classmethod
def from_vnish(cls, web_overclock_settings: dict) -> "MiningModeManual": def from_vnish(cls, web_overclock_settings: dict) -> MiningModeManual:
# will raise KeyError if it cant find the settings, values cannot be empty # will raise KeyError if it cant find the settings, values cannot be empty
voltage = web_overclock_settings["globals"]["volt"] voltage = web_overclock_settings["globals"]["volt"]
freq = web_overclock_settings["globals"]["freq"] freq = web_overclock_settings["globals"]["freq"]
@@ -512,7 +570,7 @@ class MiningModeManual(MinerConfigValue):
return cls(global_freq=freq, global_volt=voltage, boards=boards) return cls(global_freq=freq, global_volt=voltage, boards=boards)
@classmethod @classmethod
def from_epic(cls, epic_conf: dict) -> "MiningModeManual": def from_epic(cls, epic_conf: dict) -> MiningModeManual:
voltage = 0 voltage = 0
freq = 0 freq = 0
if epic_conf.get("HwConfig") is not None: if epic_conf.get("HwConfig") is not None:
@@ -552,11 +610,11 @@ class MiningModeConfig(MinerConfigOption):
manual = MiningModeManual manual = MiningModeManual
@classmethod @classmethod
def default(cls): def default(cls) -> MiningModeConfig:
return cls.normal() return cls.normal()
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None): def from_dict(cls, dict_conf: dict | None) -> MiningModeConfig:
if dict_conf is None: if dict_conf is None:
return cls.default() return cls.default()
@@ -564,12 +622,13 @@ class MiningModeConfig(MinerConfigOption):
if mode is None: if mode is None:
return cls.default() return cls.default()
cls_attr = getattr(cls, mode) cls_attr = getattr(cls, mode, None)
if cls_attr is not None: if cls_attr is not None:
return cls_attr().from_dict(dict_conf) return cls_attr().from_dict(dict_conf)
return cls.default()
@classmethod @classmethod
def from_am_modern(cls, web_conf: dict): def from_am_modern(cls, web_conf: dict) -> MiningModeConfig:
if web_conf.get("bitmain-work-mode") is not None: if web_conf.get("bitmain-work-mode") is not None:
work_mode = web_conf["bitmain-work-mode"] work_mode = web_conf["bitmain-work-mode"]
if work_mode == "": if work_mode == "":
@@ -583,7 +642,7 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @classmethod
def from_hiveon_modern(cls, web_conf: dict): def from_hiveon_modern(cls, web_conf: dict) -> MiningModeConfig:
if web_conf.get("bitmain-work-mode") is not None: if web_conf.get("bitmain-work-mode") is not None:
work_mode = web_conf["bitmain-work-mode"] work_mode = web_conf["bitmain-work-mode"]
if work_mode == "": if work_mode == "":
@@ -597,7 +656,7 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @classmethod
def from_elphapex(cls, web_conf: dict): def from_elphapex(cls, web_conf: dict) -> MiningModeConfig:
if web_conf.get("fc-work-mode") is not None: if web_conf.get("fc-work-mode") is not None:
work_mode = web_conf["fc-work-mode"] work_mode = web_conf["fc-work-mode"]
if work_mode == "": if work_mode == "":
@@ -611,7 +670,7 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @classmethod
def from_epic(cls, web_conf: dict): def from_epic(cls, web_conf: dict) -> MiningModeConfig:
try: try:
tuner_running = web_conf["PerpetualTune"]["Running"] tuner_running = web_conf["PerpetualTune"]["Running"]
if tuner_running: if tuner_running:
@@ -650,12 +709,12 @@ class MiningModeConfig(MinerConfigOption):
algo=TunerAlgo.chip_tune(), algo=TunerAlgo.chip_tune(),
) )
else: else:
return MiningModeManual.from_epic(web_conf) return cls.manual.from_epic(web_conf)
except KeyError: except KeyError:
return cls.default() return cls.default()
@classmethod @classmethod
def from_bosminer(cls, toml_conf: dict): def from_bosminer(cls, toml_conf: dict) -> MiningModeConfig:
if toml_conf.get("autotuning") is None: if toml_conf.get("autotuning") is None:
return cls.default() return cls.default()
autotuning_conf = toml_conf["autotuning"] autotuning_conf = toml_conf["autotuning"]
@@ -695,19 +754,21 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @classmethod
def from_vnish(cls, web_settings: dict, web_presets: list[dict]): def from_vnish(
cls, web_settings: dict, web_presets: list[dict], web_perf_summary: dict
) -> MiningModeConfig:
try: try:
mode_settings = web_settings["miner"]["overclock"] mode_settings = web_settings["miner"]["overclock"]
except KeyError: except KeyError:
return cls.default() return cls.default()
if mode_settings["preset"] == "disabled": if mode_settings["preset"] == "disabled":
return MiningModeManual.from_vnish(mode_settings) return cls.manual.from_vnish(mode_settings, web_presets, web_perf_summary)
else: else:
return MiningModePreset.from_vnish(mode_settings, web_presets) return cls.preset.from_vnish(mode_settings, web_presets, web_perf_summary)
@classmethod @classmethod
def from_boser(cls, grpc_miner_conf: dict): def from_boser(cls, grpc_miner_conf: dict) -> MiningModeConfig:
try: try:
tuner_conf = grpc_miner_conf["tuner"] tuner_conf = grpc_miner_conf["tuner"]
if not tuner_conf.get("enabled", False): if not tuner_conf.get("enabled", False):
@@ -753,7 +814,7 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @classmethod
def from_auradine(cls, web_mode: dict): def from_auradine(cls, web_mode: dict) -> MiningModeConfig:
try: try:
mode_data = web_mode["Mode"][0] mode_data = web_mode["Mode"][0]
if mode_data.get("Sleep") == "on": if mode_data.get("Sleep") == "on":
@@ -770,9 +831,33 @@ class MiningModeConfig(MinerConfigOption):
return cls.power_tuning(power=mode_data["Power"]) return cls.power_tuning(power=mode_data["Power"])
except LookupError: except LookupError:
return cls.default() return cls.default()
return cls.default()
@classmethod @classmethod
def from_mara(cls, web_config: dict): def from_btminer_v3(
cls, rpc_device_info: dict, rpc_settings: dict
) -> MiningModeConfig:
try:
is_mining = rpc_device_info["msg"]["miner"]["working"] == "true"
if not is_mining:
return cls.sleep()
power_limit = rpc_settings["msg"]["power-limit"]
if not power_limit == 0:
return cls.power_tuning(power=power_limit)
power_mode = rpc_settings["msg"]["power-mode"]
if power_mode == "normal":
return cls.normal()
if power_mode == "high":
return cls.high()
if power_mode == "low":
return cls.low()
except LookupError:
return cls.default()
return cls.default()
@classmethod
def from_mara(cls, web_config: dict) -> MiningModeConfig:
try: try:
mode = web_config["mode"]["work-mode-selector"] mode = web_config["mode"]["work-mode-selector"]
if mode == "Fixed": if mode == "Fixed":
@@ -797,24 +882,26 @@ class MiningModeConfig(MinerConfigOption):
return cls.default() return cls.default()
@classmethod @classmethod
def from_luxos(cls, rpc_config: dict, rpc_profiles: dict): def from_luxos(cls, rpc_config: dict, rpc_profiles: dict) -> MiningModeConfig:
preset_info = MiningModePreset.from_luxos(rpc_config, rpc_profiles) preset_info = MiningModePreset.from_luxos(rpc_config, rpc_profiles)
return cls.preset( return cls.preset(
active_preset=preset_info.active_preset, active_preset=preset_info.active_preset,
available_presets=preset_info.available_presets, available_presets=preset_info.available_presets,
) )
def as_btminer_v3(self) -> dict:
"""Delegate to the default instance for btminer v3 configuration."""
return self.default().as_btminer_v3()
MiningMode = TypeVar( MiningMode = TypeVar(
"MiningMode", "MiningMode",
bound=Union[ bound=MiningModeNormal
MiningModeNormal, | MiningModeHPM
MiningModeHPM, | MiningModeLPM
MiningModeLPM, | MiningModeSleep
MiningModeSleep, | MiningModeManual
MiningModeManual, | MiningModePowerTune
MiningModePowerTune, | MiningModeHashrateTune
MiningModeHashrateTune, | MiningModePreset,
MiningModePreset,
],
) )

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ from __future__ import annotations
import random import random
import string import string
from typing import List from typing import Any
from pydantic import Field from pydantic import Field
@@ -64,7 +64,17 @@ class Pool(MinerConfigValue):
f"passwd_{idx}": self.password, f"passwd_{idx}": self.password,
} }
def as_am_old(self, idx: int = 1, user_suffix: str | None = None) -> dict: def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
return {
"pool": self.url,
"worker": f"{self.user}{user_suffix or ''}",
"passwd": self.password,
}
def as_am_old(
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
) -> dict:
idx = args[0] if args else kwargs.get("idx", 1)
return { return {
f"_ant_pool{idx}url": self.url, f"_ant_pool{idx}url": self.url,
f"_ant_pool{idx}user": f"{self.user}{user_suffix or ''}", f"_ant_pool{idx}user": f"{self.user}{user_suffix or ''}",
@@ -81,7 +91,10 @@ class Pool(MinerConfigValue):
def as_avalon(self, user_suffix: str | None = None) -> str: def as_avalon(self, user_suffix: str | None = None) -> str:
return ",".join([self.url, f"{self.user}{user_suffix or ''}", self.password]) return ",".join([self.url, f"{self.user}{user_suffix or ''}", self.password])
def as_inno(self, idx: int = 1, user_suffix: str | None = None) -> dict: def as_inno(
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
) -> dict:
idx = args[0] if args else kwargs.get("idx", 1)
return { return {
f"Pool{idx}": self.url, f"Pool{idx}": self.url,
f"UserName{idx}": f"{self.user}{user_suffix or ''}", f"UserName{idx}": f"{self.user}{user_suffix or ''}",
@@ -102,7 +115,7 @@ class Pool(MinerConfigValue):
"pass": self.password, "pass": self.password,
} }
def as_epic(self, user_suffix: str | None = None): def as_epic(self, user_suffix: str | None = None) -> dict:
return { return {
"pool": self.url, "pool": self.url,
"login": f"{self.user}{user_suffix or ''}", "login": f"{self.user}{user_suffix or ''}",
@@ -139,54 +152,60 @@ class Pool(MinerConfigValue):
} }
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "Pool": def from_dict(cls, dict_conf: dict | None) -> Pool:
if dict_conf is None:
raise ValueError("dict_conf cannot be None")
return cls( return cls(
url=dict_conf["url"], user=dict_conf["user"], password=dict_conf["password"] url=dict_conf["url"], user=dict_conf["user"], password=dict_conf["password"]
) )
@classmethod @classmethod
def from_api(cls, api_pool: dict) -> "Pool": def from_api(cls, api_pool: dict) -> Pool:
return cls(url=api_pool["URL"], user=api_pool["User"], password="x") return cls(url=api_pool["URL"], user=api_pool["User"], password="x")
@classmethod @classmethod
def from_epic(cls, api_pool: dict) -> "Pool": def from_btminer_v3(cls, api_pool: dict) -> Pool:
return cls(url=api_pool["url"], user=api_pool["account"], password="x")
@classmethod
def from_epic(cls, api_pool: dict) -> Pool:
return cls( return cls(
url=api_pool["pool"], user=api_pool["login"], password=api_pool["password"] url=api_pool["pool"], user=api_pool["login"], password=api_pool["password"]
) )
@classmethod @classmethod
def from_am_modern(cls, web_pool: dict) -> "Pool": def from_am_modern(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
@classmethod @classmethod
def from_hiveon_modern(cls, web_pool: dict) -> "Pool": def from_hiveon_modern(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
@classmethod @classmethod
def from_elphapex(cls, web_pool: dict) -> "Pool": def from_elphapex(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
# TODO: check if this is accurate, user/username, pass/password # TODO: check if this is accurate, user/username, pass/password
@classmethod @classmethod
def from_goldshell(cls, web_pool: dict) -> "Pool": def from_goldshell(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
@classmethod @classmethod
def from_inno(cls, web_pool: dict) -> "Pool": def from_inno(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"] url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
) )
@classmethod @classmethod
def from_bosminer(cls, toml_pool_conf: dict) -> "Pool": def from_bosminer(cls, toml_pool_conf: dict) -> Pool:
return cls( return cls(
url=toml_pool_conf["url"], url=toml_pool_conf["url"],
user=toml_pool_conf["user"], user=toml_pool_conf["user"],
@@ -194,7 +213,7 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_vnish(cls, web_pool: dict) -> "Pool": def from_vnish(cls, web_pool: dict) -> Pool:
return cls( return cls(
url="stratum+tcp://" + web_pool["url"], url="stratum+tcp://" + web_pool["url"],
user=web_pool["user"], user=web_pool["user"],
@@ -202,7 +221,7 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_boser(cls, grpc_pool: dict) -> "Pool": def from_boser(cls, grpc_pool: dict) -> Pool:
return cls( return cls(
url=grpc_pool["url"], url=grpc_pool["url"],
user=grpc_pool["user"], user=grpc_pool["user"],
@@ -210,7 +229,7 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_mara(cls, web_pool: dict) -> "Pool": def from_mara(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["url"], url=web_pool["url"],
user=web_pool["user"], user=web_pool["user"],
@@ -218,7 +237,7 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_espminer(cls, web_system_info: dict) -> "Pool": def from_espminer(cls, web_system_info: dict) -> Pool:
url = f"stratum+tcp://{web_system_info['stratumURL']}:{web_system_info['stratumPort']}" url = f"stratum+tcp://{web_system_info['stratumURL']}:{web_system_info['stratumPort']}"
return cls( return cls(
url=url, url=url,
@@ -227,11 +246,11 @@ class Pool(MinerConfigValue):
) )
@classmethod @classmethod
def from_luxos(cls, rpc_pools: dict) -> "Pool": def from_luxos(cls, rpc_pools: dict) -> Pool:
return cls.from_api(rpc_pools) return cls.from_api(rpc_pools)
@classmethod @classmethod
def from_iceriver(cls, web_pool: dict) -> "Pool": def from_iceriver(cls, web_pool: dict) -> Pool:
return cls( return cls(
url=web_pool["addr"], url=web_pool["addr"],
user=web_pool["user"], user=web_pool["user"],
@@ -283,31 +302,32 @@ class PoolGroup(MinerConfigValue):
idx += 1 idx += 1
return pools return pools
def as_wm(self, user_suffix: str | None = None) -> dict: def as_wm(self, *args: Any, user_suffix: str | None = None, **kwargs: Any) -> dict:
pools = {} pools: dict[str, str] = {}
idx = 0 idx = 0
while idx < 3: while idx < 3:
if len(self.pools) > idx: if len(self.pools) > idx:
pools.update( pools.update(**self.pools[idx].as_wm(idx + 1, user_suffix=user_suffix))
**self.pools[idx].as_wm(idx=idx + 1, user_suffix=user_suffix)
)
else: else:
pools.update(**Pool(url="", user="", password="").as_wm(idx=idx + 1)) pools.update(**Pool(url="", user="", password="").as_wm(idx + 1))
idx += 1 idx += 1
return pools return pools
def as_am_old(self, user_suffix: str | None = None) -> dict: def as_btminer_v3(self, user_suffix: str | None = None) -> list:
pools = {} return [pool.as_btminer_v3(user_suffix) for pool in self.pools[:3]]
def as_am_old(
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
) -> dict:
pools: dict[str, str] = {}
idx = 0 idx = 0
while idx < 3: while idx < 3:
if len(self.pools) > idx: if len(self.pools) > idx:
pools.update( pools.update(
**self.pools[idx].as_am_old(idx=idx + 1, user_suffix=user_suffix) **self.pools[idx].as_am_old(idx + 1, user_suffix=user_suffix)
) )
else: else:
pools.update( pools.update(**Pool(url="", user="", password="").as_am_old(idx + 1))
**Pool(url="", user="", password="").as_am_old(idx=idx + 1)
)
idx += 1 idx += 1
return pools return pools
@@ -319,22 +339,24 @@ class PoolGroup(MinerConfigValue):
return self.pools[0].as_avalon(user_suffix=user_suffix) return self.pools[0].as_avalon(user_suffix=user_suffix)
return Pool(url="", user="", password="").as_avalon() return Pool(url="", user="", password="").as_avalon()
def as_inno(self, user_suffix: str | None = None) -> dict: def as_inno(
pools = {} self, *args: Any, user_suffix: str | None = None, **kwargs: Any
) -> dict:
pools: dict[str, str] = {}
idx = 0 idx = 0
while idx < 3: while idx < 3:
if len(self.pools) > idx: if len(self.pools) > idx:
pools.update( pools.update(
**self.pools[idx].as_inno(idx=idx + 1, user_suffix=user_suffix) **self.pools[idx].as_inno(idx + 1, user_suffix=user_suffix)
) )
else: else:
pools.update(**Pool(url="", user="", password="").as_inno(idx=idx + 1)) pools.update(**Pool(url="", user="", password="").as_inno(idx + 1))
idx += 1 idx += 1
return pools return pools
def as_bosminer(self, user_suffix: str | None = None) -> dict: def as_bosminer(self, user_suffix: str | None = None) -> dict:
if len(self.pools) > 0: if len(self.pools) > 0:
conf = { conf: dict[str, Any] = {
"name": self.name, "name": self.name,
"pool": [ "pool": [
pool.as_bosminer(user_suffix=user_suffix) for pool in self.pools pool.as_bosminer(user_suffix=user_suffix) for pool in self.pools
@@ -359,7 +381,7 @@ class PoolGroup(MinerConfigValue):
def as_boser(self, user_suffix: str | None = None) -> PoolGroupConfiguration: def as_boser(self, user_suffix: str | None = None) -> PoolGroupConfiguration:
return PoolGroupConfiguration( return PoolGroupConfiguration(
name=self.name, name=self.name or "",
quota=Quota(value=self.quota), quota=Quota(value=self.quota),
pools=[p.as_boser() for p in self.pools], pools=[p.as_boser() for p in self.pools],
) )
@@ -368,7 +390,10 @@ class PoolGroup(MinerConfigValue):
return {"pools": [p.as_vnish(user_suffix=user_suffix) for p in self.pools]} return {"pools": [p.as_vnish(user_suffix=user_suffix) for p in self.pools]}
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolGroup": def from_dict(cls, dict_conf: dict | None) -> PoolGroup:
if dict_conf is None:
return cls()
cls_conf = {} cls_conf = {}
if dict_conf.get("quota") is not None: if dict_conf.get("quota") is not None:
@@ -379,50 +404,57 @@ class PoolGroup(MinerConfigValue):
return cls(**cls_conf) return cls(**cls_conf)
@classmethod @classmethod
def from_api(cls, api_pool_list: list) -> "PoolGroup": def from_api(cls, api_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in api_pool_list: for pool in api_pool_list:
pools.append(Pool.from_api(pool)) pools.append(Pool.from_api(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_epic(cls, api_pool_list: list) -> "PoolGroup": def from_btminer_v3(cls, api_pool_list: list) -> PoolGroup:
pools = []
for pool in api_pool_list:
pools.append(Pool.from_btminer_v3(pool))
return cls(pools=pools)
@classmethod
def from_epic(cls, api_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in api_pool_list: for pool in api_pool_list:
pools.append(Pool.from_epic(pool)) pools.append(Pool.from_epic(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_am_modern(cls, web_pool_list: list) -> "PoolGroup": def from_am_modern(cls, web_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in web_pool_list: for pool in web_pool_list:
pools.append(Pool.from_am_modern(pool)) pools.append(Pool.from_am_modern(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_hiveon_modern(cls, web_pool_list: list) -> "PoolGroup": def from_hiveon_modern(cls, web_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in web_pool_list: for pool in web_pool_list:
pools.append(Pool.from_hiveon_modern(pool)) pools.append(Pool.from_hiveon_modern(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_elphapex(cls, web_pool_list: list) -> "PoolGroup": def from_elphapex(cls, web_pool_list: list) -> PoolGroup:
pools = [] pools = []
for pool in web_pool_list: for pool in web_pool_list:
pools.append(Pool.from_elphapex(pool)) pools.append(Pool.from_elphapex(pool))
return cls(pools=pools) return cls(pools=pools)
@classmethod @classmethod
def from_goldshell(cls, web_pools: list) -> "PoolGroup": def from_goldshell(cls, web_pools: list) -> PoolGroup:
return cls(pools=[Pool.from_goldshell(p) for p in web_pools]) return cls(pools=[Pool.from_goldshell(p) for p in web_pools])
@classmethod @classmethod
def from_inno(cls, web_pools: list) -> "PoolGroup": def from_inno(cls, web_pools: list) -> PoolGroup:
return cls(pools=[Pool.from_inno(p) for p in web_pools]) return cls(pools=[Pool.from_inno(p) for p in web_pools])
@classmethod @classmethod
def from_bosminer(cls, toml_group_conf: dict) -> "PoolGroup": def from_bosminer(cls, toml_group_conf: dict) -> PoolGroup:
if toml_group_conf.get("pool") is not None: if toml_group_conf.get("pool") is not None:
return cls( return cls(
name=toml_group_conf["name"], name=toml_group_conf["name"],
@@ -432,13 +464,13 @@ class PoolGroup(MinerConfigValue):
return cls() return cls()
@classmethod @classmethod
def from_vnish(cls, web_settings_pools: dict) -> "PoolGroup": def from_vnish(cls, web_settings_pools: dict) -> PoolGroup:
return cls( return cls(
pools=[Pool.from_vnish(p) for p in web_settings_pools if p["url"] != ""] pools=[Pool.from_vnish(p) for p in web_settings_pools if p["url"] != ""]
) )
@classmethod @classmethod
def from_boser(cls, grpc_pool_group: dict) -> "PoolGroup": def from_boser(cls, grpc_pool_group: dict) -> PoolGroup:
try: try:
return cls( return cls(
pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]], pools=[Pool.from_boser(p) for p in grpc_pool_group["pools"]],
@@ -453,15 +485,15 @@ class PoolGroup(MinerConfigValue):
return cls() return cls()
@classmethod @classmethod
def from_mara(cls, web_config_pools: dict) -> "PoolGroup": def from_mara(cls, web_config_pools: dict) -> PoolGroup:
return cls(pools=[Pool.from_mara(pool_conf) for pool_conf in web_config_pools]) return cls(pools=[Pool.from_mara(pool_conf) for pool_conf in web_config_pools])
@classmethod @classmethod
def from_espminer(cls, web_system_info: dict) -> "PoolGroup": def from_espminer(cls, web_system_info: dict) -> PoolGroup:
return cls(pools=[Pool.from_espminer(web_system_info)]) return cls(pools=[Pool.from_espminer(web_system_info)])
@classmethod @classmethod
def from_iceriver(cls, web_userpanel: dict) -> "PoolGroup": def from_iceriver(cls, web_userpanel: dict) -> PoolGroup:
return cls( return cls(
pools=[ pools=[
Pool.from_iceriver(web_pool) Pool.from_iceriver(web_pool)
@@ -471,21 +503,21 @@ class PoolGroup(MinerConfigValue):
class PoolConfig(MinerConfigValue): class PoolConfig(MinerConfigValue):
groups: List[PoolGroup] = Field(default_factory=list) groups: list[PoolGroup] = Field(default_factory=list)
@classmethod @classmethod
def default(cls) -> "PoolConfig": def default(cls) -> PoolConfig:
return cls() return cls()
@classmethod @classmethod
def from_dict(cls, dict_conf: dict | None) -> "PoolConfig": def from_dict(cls, dict_conf: dict | None) -> PoolConfig:
if dict_conf is None: if dict_conf is None:
return cls.default() return cls.default()
return cls(groups=[PoolGroup.from_dict(g) for g in dict_conf["groups"]]) return cls(groups=[PoolGroup.from_dict(g) for g in dict_conf["groups"]])
@classmethod @classmethod
def simple(cls, pools: list[Pool | dict[str, str]]) -> "PoolConfig": def simple(cls, pools: list[Pool | dict[str, str]]) -> PoolConfig:
group_pools = [] group_pools = []
for pool in pools: for pool in pools:
if isinstance(pool, dict): if isinstance(pool, dict):
@@ -508,12 +540,19 @@ class PoolConfig(MinerConfigValue):
return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)} return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_elphapex()} return {"pools": PoolGroup().as_elphapex()}
def as_wm(self, user_suffix: str | None = None) -> dict: def as_wm(self, *args: Any, user_suffix: str | None = None, **kwargs: Any) -> dict:
if len(self.groups) > 0: if len(self.groups) > 0:
return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)} return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_wm()} return {"pools": PoolGroup().as_wm()}
def as_am_old(self, user_suffix: str | None = None) -> dict: def as_btminer_v3(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_btminer_v3(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_btminer_v3()}
def as_am_old(
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
) -> dict:
if len(self.groups) > 0: if len(self.groups) > 0:
return self.groups[0].as_am_old(user_suffix=user_suffix) return self.groups[0].as_am_old(user_suffix=user_suffix)
return PoolGroup().as_am_old() return PoolGroup().as_am_old()
@@ -528,7 +567,9 @@ class PoolConfig(MinerConfigValue):
return {"pools": self.groups[0].as_avalon(user_suffix=user_suffix)} return {"pools": self.groups[0].as_avalon(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_avalon()} return {"pools": PoolGroup().as_avalon()}
def as_inno(self, user_suffix: str | None = None) -> dict: def as_inno(
self, *args: Any, user_suffix: str | None = None, **kwargs: Any
) -> dict:
if len(self.groups) > 0: if len(self.groups) > 0:
return self.groups[0].as_inno(user_suffix=user_suffix) return self.groups[0].as_inno(user_suffix=user_suffix)
return PoolGroup().as_inno() return PoolGroup().as_inno()
@@ -543,7 +584,7 @@ class PoolConfig(MinerConfigValue):
def as_boser(self, user_suffix: str | None = None) -> dict: def as_boser(self, user_suffix: str | None = None) -> dict:
return { return {
"set_pool_groups": SetPoolGroupsRequest( "set_pool_groups": SetPoolGroupsRequest(
save_action=SaveAction.SAVE_AND_APPLY, save_action=SaveAction(SaveAction.SAVE_AND_APPLY),
pool_groups=[g.as_boser(user_suffix=user_suffix) for g in self.groups], pool_groups=[g.as_boser(user_suffix=user_suffix) for g in self.groups],
) )
} }
@@ -589,7 +630,7 @@ class PoolConfig(MinerConfigValue):
return self.groups[0].as_vnish(user_suffix=user_suffix) return self.groups[0].as_vnish(user_suffix=user_suffix)
@classmethod @classmethod
def from_api(cls, api_pools: dict) -> "PoolConfig": def from_api(cls, api_pools: dict) -> PoolConfig:
try: try:
pool_data = api_pools["POOLS"] pool_data = api_pools["POOLS"]
except KeyError: except KeyError:
@@ -599,12 +640,22 @@ class PoolConfig(MinerConfigValue):
return cls(groups=[PoolGroup.from_api(pool_data)]) return cls(groups=[PoolGroup.from_api(pool_data)])
@classmethod @classmethod
def from_epic(cls, web_conf: dict) -> "PoolConfig": def from_btminer_v3(cls, rpc_pools: dict) -> PoolConfig:
try:
pool_data = rpc_pools["pools"]
except KeyError:
return PoolConfig.default()
pool_data = sorted(pool_data, key=lambda x: int(x["id"]))
return cls(groups=[PoolGroup.from_btminer_v3(pool_data)])
@classmethod
def from_epic(cls, web_conf: dict) -> PoolConfig:
pool_data = web_conf["StratumConfigs"] pool_data = web_conf["StratumConfigs"]
return cls(groups=[PoolGroup.from_epic(pool_data)]) return cls(groups=[PoolGroup.from_epic(pool_data)])
@classmethod @classmethod
def from_am_modern(cls, web_conf: dict) -> "PoolConfig": def from_am_modern(cls, web_conf: dict) -> PoolConfig:
try: try:
pool_data = web_conf["pools"] pool_data = web_conf["pools"]
except KeyError: except KeyError:
@@ -613,7 +664,7 @@ class PoolConfig(MinerConfigValue):
return cls(groups=[PoolGroup.from_am_modern(pool_data)]) return cls(groups=[PoolGroup.from_am_modern(pool_data)])
@classmethod @classmethod
def from_hiveon_modern(cls, web_conf: dict) -> "PoolConfig": def from_hiveon_modern(cls, web_conf: dict) -> PoolConfig:
try: try:
pool_data = web_conf["pools"] pool_data = web_conf["pools"]
except KeyError: except KeyError:
@@ -622,35 +673,45 @@ class PoolConfig(MinerConfigValue):
return cls(groups=[PoolGroup.from_hiveon_modern(pool_data)]) return cls(groups=[PoolGroup.from_hiveon_modern(pool_data)])
@classmethod @classmethod
def from_elphapex(cls, web_conf: dict) -> "PoolConfig": def from_elphapex(cls, web_conf: dict) -> PoolConfig:
pool_data = web_conf["pools"] pool_data = web_conf["pools"]
return cls(groups=[PoolGroup.from_elphapex(pool_data)]) return cls(groups=[PoolGroup.from_elphapex(pool_data)])
@classmethod @classmethod
def from_goldshell(cls, web_pools: list) -> "PoolConfig": def from_goldshell(cls, web_pools: list) -> PoolConfig:
return cls(groups=[PoolGroup.from_goldshell(web_pools)]) return cls(groups=[PoolGroup.from_goldshell(web_pools)])
@classmethod @classmethod
def from_inno(cls, web_pools: list) -> "PoolConfig": def from_goldshell_byte(cls, web_pools: list) -> PoolConfig:
return cls(
groups=[
PoolGroup.from_goldshell(g["pools"])
for g in web_pools
if len(g["pools"]) > 0
]
)
@classmethod
def from_inno(cls, web_pools: list) -> PoolConfig:
return cls(groups=[PoolGroup.from_inno(web_pools)]) return cls(groups=[PoolGroup.from_inno(web_pools)])
@classmethod @classmethod
def from_bosminer(cls, toml_conf: dict) -> "PoolConfig": def from_bosminer(cls, toml_conf: dict) -> PoolConfig:
if toml_conf.get("group") is None: if toml_conf.get("group") is None:
return cls() return cls()
return cls(groups=[PoolGroup.from_bosminer(g) for g in toml_conf["group"]]) return cls(groups=[PoolGroup.from_bosminer(g) for g in toml_conf["group"]])
@classmethod @classmethod
def from_vnish(cls, web_settings: dict) -> "PoolConfig": def from_vnish(cls, web_settings: dict) -> PoolConfig:
try: try:
return cls(groups=[PoolGroup.from_vnish(web_settings["miner"]["pools"])]) return cls(groups=[PoolGroup.from_vnish(web_settings["miner"]["pools"])])
except LookupError: except LookupError:
return cls() return cls()
@classmethod @classmethod
def from_boser(cls, grpc_miner_conf: dict) -> "PoolConfig": def from_boser(cls, grpc_miner_conf: dict) -> PoolConfig:
try: try:
return cls( return cls(
groups=[ groups=[
@@ -662,19 +723,19 @@ class PoolConfig(MinerConfigValue):
return cls() return cls()
@classmethod @classmethod
def from_mara(cls, web_config: dict) -> "PoolConfig": def from_mara(cls, web_config: dict) -> PoolConfig:
return cls(groups=[PoolGroup.from_mara(web_config["pools"])]) return cls(groups=[PoolGroup.from_mara(web_config["pools"])])
@classmethod @classmethod
def from_espminer(cls, web_system_info: dict) -> "PoolConfig": def from_espminer(cls, web_system_info: dict) -> PoolConfig:
return cls(groups=[PoolGroup.from_espminer(web_system_info)]) return cls(groups=[PoolGroup.from_espminer(web_system_info)])
@classmethod @classmethod
def from_iceriver(cls, web_userpanel: dict) -> "PoolConfig": def from_iceriver(cls, web_userpanel: dict) -> PoolConfig:
return cls(groups=[PoolGroup.from_iceriver(web_userpanel)]) return cls(groups=[PoolGroup.from_iceriver(web_userpanel)])
@classmethod @classmethod
def from_luxos(cls, rpc_groups: dict, rpc_pools: dict) -> "PoolConfig": def from_luxos(cls, rpc_groups: dict, rpc_pools: dict) -> PoolConfig:
return cls( return cls(
groups=[ groups=[
PoolGroup( PoolGroup(

View File

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

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import copy import copy
import time import time
from collections.abc import Callable
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any
@@ -24,6 +25,7 @@ from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune from pyasic.config.mining import MiningModePowerTune
from pyasic.data.pools import PoolMetrics, Scheme from pyasic.data.pools import PoolMetrics, Scheme
from pyasic.device.algorithm.hashrate import AlgoHashRateType from pyasic.device.algorithm.hashrate import AlgoHashRateType
from pyasic.device.algorithm.hashrate.base import GenericHashrate
from .boards import HashBoard from .boards import HashBoard
from .device import DeviceInfo from .device import DeviceInfo
@@ -70,6 +72,7 @@ class MinerData(BaseModel):
errors: A list of errors on the miner. errors: A list of errors on the miner.
fault_light: Whether the fault light is on as a boolean. fault_light: Whether the fault light is on as a boolean.
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically. efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
efficiency_fract: Same as efficiency, but is not rounded to integer. Calculated automatically.
is_mining: Whether the miner is mining. is_mining: Whether the miner is mining.
pools: A list of PoolMetrics instances, each representing metrics for a pool. pools: A list of PoolMetrics instances, each representing metrics for a pool.
""" """
@@ -82,13 +85,16 @@ class MinerData(BaseModel):
# about # about
device_info: DeviceInfo | None = None device_info: DeviceInfo | None = None
serial_number: str | None = None
mac: str | None = None mac: str | None = None
api_ver: str | None = None api_ver: str | None = None
fw_ver: str | None = None fw_ver: str | None = None
hostname: str | None = None hostname: str | None = None
# hashrate # hashrate
raw_hashrate: AlgoHashRateType = Field(exclude=True, default=None, repr=False) raw_hashrate: AlgoHashRateType | None = Field(
exclude=True, default=None, repr=False
)
# sticker # sticker
sticker_hashrate: AlgoHashRateType | None = None sticker_hashrate: AlgoHashRateType | None = None
@@ -192,7 +198,7 @@ class MinerData(BaseModel):
setattr(cp, key, item & other_item) setattr(cp, key, item & other_item)
return cp return cp
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def hashrate(self) -> AlgoHashRateType | None: def hashrate(self) -> AlgoHashRateType | None:
if len(self.hashboards) > 0: if len(self.hashboards) > 0:
@@ -201,14 +207,24 @@ class MinerData(BaseModel):
if item.hashrate is not None: if item.hashrate is not None:
hr_data.append(item.hashrate) hr_data.append(item.hashrate)
if len(hr_data) > 0: if len(hr_data) > 0:
return sum(hr_data, start=self.device_info.algo.hashrate(rate=0)) if self.device_info is not None and self.device_info.algo is not None:
from pyasic.device.algorithm.hashrate.unit.base import GenericUnit
return sum(
hr_data,
start=self.device_info.algo.hashrate(
rate=0, unit=GenericUnit.H
),
)
else:
return sum(hr_data, start=GenericHashrate(rate=0))
return self.raw_hashrate return self.raw_hashrate
@hashrate.setter @hashrate.setter
def hashrate(self, val): def hashrate(self, val):
self.raw_hashrate = val self.raw_hashrate = val
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def wattage_limit(self) -> int | None: def wattage_limit(self) -> int | None:
if self.config is not None: if self.config is not None:
@@ -220,7 +236,7 @@ class MinerData(BaseModel):
def wattage_limit(self, val: int): def wattage_limit(self, val: int):
self.raw_wattage_limit = val self.raw_wattage_limit = val
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def total_chips(self) -> int | None: def total_chips(self) -> int | None:
if len(self.hashboards) > 0: if len(self.hashboards) > 0:
@@ -231,15 +247,16 @@ class MinerData(BaseModel):
if len(chip_data) > 0: if len(chip_data) > 0:
return sum(chip_data) return sum(chip_data)
return None return None
return 0
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def nominal(self) -> bool | None: def nominal(self) -> bool | None:
if self.total_chips is None or self.expected_chips is None: if self.total_chips is None or self.expected_chips is None:
return None return None
return self.expected_chips == self.total_chips return self.expected_chips == self.total_chips
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def percent_expected_chips(self) -> int | None: def percent_expected_chips(self) -> int | None:
if self.total_chips is None or self.expected_chips is None: if self.total_chips is None or self.expected_chips is None:
@@ -248,7 +265,7 @@ class MinerData(BaseModel):
return 0 return 0
return round((self.total_chips / self.expected_chips) * 100) return round((self.total_chips / self.expected_chips) * 100)
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def percent_expected_hashrate(self) -> int | None: def percent_expected_hashrate(self) -> int | None:
if self.hashrate is None or self.expected_hashrate is None: if self.hashrate is None or self.expected_hashrate is None:
@@ -258,7 +275,7 @@ class MinerData(BaseModel):
except ZeroDivisionError: except ZeroDivisionError:
return 0 return 0
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def percent_expected_wattage(self) -> int | None: def percent_expected_wattage(self) -> int | None:
if self.wattage_limit is None or self.wattage is None: if self.wattage_limit is None or self.wattage is None:
@@ -268,10 +285,10 @@ class MinerData(BaseModel):
except ZeroDivisionError: except ZeroDivisionError:
return 0 return 0
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def temperature_avg(self) -> int | None: def temperature_avg(self) -> int | None:
total_temp = 0 total_temp: float = 0
temp_count = 0 temp_count = 0
for hb in self.hashboards: for hb in self.hashboards:
if hb.temp is not None: if hb.temp is not None:
@@ -281,49 +298,65 @@ class MinerData(BaseModel):
return None return None
return round(total_temp / temp_count) return round(total_temp / temp_count)
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def efficiency(self) -> int | None: def efficiency(self) -> int | None:
efficiency = self._efficiency(0)
if efficiency is None:
return None
else:
return int(efficiency)
@computed_field # type: ignore[prop-decorator]
@property
def efficiency_fract(self) -> float | None:
return self._efficiency(2)
def _efficiency(self, ndigits: int) -> float | None:
if self.hashrate is None or self.wattage is None: if self.hashrate is None or self.wattage is None:
return None return None
try: try:
return round(self.wattage / float(self.hashrate)) return round(self.wattage / float(self.hashrate), ndigits)
except ZeroDivisionError: except ZeroDivisionError:
return 0 return 0.0
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def datetime(self) -> str: def datetime(self) -> str:
return self.raw_datetime.isoformat() return self.raw_datetime.isoformat()
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def timestamp(self) -> int: def timestamp(self) -> int:
return int(time.mktime(self.raw_datetime.timetuple())) return int(time.mktime(self.raw_datetime.timetuple()))
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def make(self) -> str | None: def make(self) -> str | None:
if self.device_info.make is not None: if self.device_info is not None and self.device_info.make is not None:
return str(self.device_info.make) return str(self.device_info.make)
return ""
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def model(self) -> str | None: def model(self) -> str | None:
if self.device_info.model is not None: if self.device_info is not None and self.device_info.model is not None:
return str(self.device_info.model) return str(self.device_info.model)
return ""
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def firmware(self) -> str | None: def firmware(self) -> str | None:
if self.device_info.firmware is not None: if self.device_info is not None and self.device_info.firmware is not None:
return str(self.device_info.firmware) return str(self.device_info.firmware)
return ""
@computed_field # type: ignore[misc] @computed_field # type: ignore[prop-decorator]
@property @property
def algo(self) -> str | None: def algo(self) -> str | None:
if self.device_info.algo is not None: if self.device_info is not None and self.device_info.algo is not None:
return str(self.device_info.algo) return str(self.device_info.algo)
return ""
def keys(self) -> list: def keys(self) -> list:
return list(self.model_fields.keys()) return list(self.model_fields.keys())
@@ -403,7 +436,8 @@ class MinerData(BaseModel):
for dt in serialization_map_instance: for dt in serialization_map_instance:
if item_serialized is None: if item_serialized is None:
if isinstance(list_field_val, dt): if isinstance(list_field_val, dt):
item_serialized = serialization_map_instance[dt]( func = serialization_map_instance[dt]
item_serialized = func(
f"{key}{level_delimiter}{idx}", list_field_val f"{key}{level_delimiter}{idx}", list_field_val
) )
if item_serialized is not None: if item_serialized is not None:
@@ -447,11 +481,11 @@ class MinerData(BaseModel):
"pools", "pools",
] ]
serialization_map_instance = { serialization_map_instance: dict[type, Callable[[str, Any], str | None]] = {
AlgoHashRateType: serialize_algo_hash_rate, AlgoHashRateType: serialize_algo_hash_rate,
BaseMinerError: serialize_miner_error, BaseMinerError: serialize_miner_error,
} }
serialization_map = { serialization_map: dict[type, Callable[[str, Any], str | None]] = {
int: serialize_int, int: serialize_int,
float: serialize_float, float: serialize_float,
str: serialize_str, str: serialize_str,
@@ -485,9 +519,8 @@ class MinerData(BaseModel):
for datatype in serialization_map_instance: for datatype in serialization_map_instance:
if serialized is None: if serialized is None:
if isinstance(field_val, datatype): if isinstance(field_val, datatype):
serialized = serialization_map_instance[datatype]( func = serialization_map_instance[datatype]
field, field_val serialized = func(field, field_val)
)
if serialized is not None: if serialized is not None:
field_data.append(serialized) field_data.append(serialized)

View File

@@ -15,6 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from typing import Any from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
@@ -28,6 +29,8 @@ class HashBoard(BaseModel):
Attributes: Attributes:
slot: The slot of the board as an int. slot: The slot of the board as an int.
hashrate: The hashrate of the board in TH/s as a float. hashrate: The hashrate of the board in TH/s as a float.
inlet_temp: Inlet temperature for hydro asics as an int
outlet_temp: Outlet temperature for hydro asics as an int
temp: The temperature of the PCB as an int. temp: The temperature of the PCB as an int.
chip_temp: The temperature of the chips as an int. chip_temp: The temperature of the chips as an int.
chips: The chip count of the board as an int. chips: The chip count of the board as an int.
@@ -41,6 +44,8 @@ class HashBoard(BaseModel):
slot: int = 0 slot: int = 0
hashrate: AlgoHashRateType | None = None hashrate: AlgoHashRateType | None = None
inlet_temp: float | None = None
outlet_temp: float | None = None
temp: float | None = None temp: float | None = None
chip_temp: float | None = None chip_temp: float | None = None
chips: int | None = None chips: int | None = None
@@ -73,7 +78,6 @@ class HashBoard(BaseModel):
raise KeyError(f"{item}") raise KeyError(f"{item}")
def as_influxdb(self, key_root: str, level_delimiter: str = ".") -> str: def as_influxdb(self, key_root: str, level_delimiter: str = ".") -> str:
def serialize_int(key: str, value: int) -> str: def serialize_int(key: str, value: int) -> str:
return f"{key}={value}" return f"{key}={value}"
@@ -86,7 +90,7 @@ class HashBoard(BaseModel):
def serialize_algo_hash_rate(key: str, value: AlgoHashRateType) -> str: def serialize_algo_hash_rate(key: str, value: AlgoHashRateType) -> str:
return f"{key}={round(float(value), 2)}" return f"{key}={round(float(value), 2)}"
def serialize_bool(key: str, value: bool): def serialize_bool(key: str, value: bool) -> str:
return f"{key}={str(value).lower()}" return f"{key}={str(value).lower()}"
serialization_map_instance = { serialization_map_instance = {
@@ -113,8 +117,11 @@ class HashBoard(BaseModel):
field_data = [] field_data = []
for field in include: for field in include:
field_val = getattr(self, field) field_val = getattr(self, field)
serialization_func = serialization_map.get( serialization_func: Callable[[str, Any], str | None] = (
type(field_val), lambda _k, _v: None serialization_map.get(
type(field_val),
lambda _k, _v: None, # type: ignore
)
) )
serialized = serialization_func( serialized = serialization_func(
f"{key_root}{level_delimiter}{field}", field_val f"{key_root}{level_delimiter}{field}", field_val

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
from .base import MinerAlgoType from .base import MinerAlgoType
from .blake256 import Blake256Algo from .blake256 import Blake256Algo
from .blockflow import BlockFlowAlgo
from .eaglesong import EaglesongAlgo from .eaglesong import EaglesongAlgo
from .equihash import EquihashAlgo from .equihash import EquihashAlgo
from .ethash import EtHashAlgo from .ethash import EtHashAlgo
@@ -11,6 +12,7 @@ from .kheavyhash import KHeavyHashAlgo
from .scrypt import ScryptAlgo from .scrypt import ScryptAlgo
from .sha256 import SHA256Algo from .sha256 import SHA256Algo
from .x11 import X11Algo from .x11 import X11Algo
from .zksnark import ZkSnarkAlgo
class MinerAlgo: class MinerAlgo:
@@ -24,3 +26,5 @@ class MinerAlgo:
EAGLESONG = EaglesongAlgo EAGLESONG = EaglesongAlgo
ETHASH = EtHashAlgo ETHASH = EtHashAlgo
EQUIHASH = EquihashAlgo EQUIHASH = EquihashAlgo
BLOCKFLOW = BlockFlowAlgo
ZKSNARK = ZkSnarkAlgo

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from .base import MinerAlgoType
from .hashrate import BlockFlowHashRate
from .hashrate.unit import BlockFlowUnit
class BlockFlowAlgo(MinerAlgoType):
hashrate: type[BlockFlowHashRate] = BlockFlowHashRate
unit: type[BlockFlowUnit] = BlockFlowUnit
name = "BlockFlow"

View File

@@ -1,5 +1,6 @@
from .base import AlgoHashRateType from .base import AlgoHashRateType
from .blake256 import Blake256HashRate from .blake256 import Blake256HashRate
from .blockflow import BlockFlowHashRate
from .eaglesong import EaglesongHashRate from .eaglesong import EaglesongHashRate
from .equihash import EquihashHashRate from .equihash import EquihashHashRate
from .ethash import EtHashHashRate from .ethash import EtHashHashRate
@@ -9,6 +10,7 @@ from .kheavyhash import KHeavyHashHashRate
from .scrypt import ScryptHashRate from .scrypt import ScryptHashRate
from .sha256 import SHA256HashRate from .sha256 import SHA256HashRate
from .x11 import X11HashRate from .x11 import X11HashRate
from .zksnark import ZkSnarkHashRate
class AlgoHashRate: class AlgoHashRate:
@@ -22,3 +24,5 @@ class AlgoHashRate:
EAGLESONG = EaglesongHashRate EAGLESONG = EaglesongHashRate
ETHASH = EtHashHashRate ETHASH = EtHashHashRate
EQUIHASH = EquihashHashRate EQUIHASH = EquihashHashRate
BLOCKFLOW = BlockFlowHashRate
ZKSNARK = ZkSnarkHashRate

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
from __future__ import annotations
from typing_extensions import Self
from pyasic.device.algorithm.hashrate.base import AlgoHashRateType
from pyasic.device.algorithm.hashrate.unit.blockflow import BlockFlowUnit
from .unit import HashUnit
class BlockFlowHashRate(AlgoHashRateType[BlockFlowUnit]):
rate: float
unit: BlockFlowUnit = HashUnit.BLOCKFLOW.default
def into(self, other: BlockFlowUnit) -> Self:
return self.__class__(
rate=self.rate / (other.value / self.unit.value), unit=other
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
from .blake256 import Blake256Unit from .blake256 import Blake256Unit
from .blockflow import BlockFlowUnit
from .eaglesong import EaglesongUnit from .eaglesong import EaglesongUnit
from .equihash import EquihashUnit from .equihash import EquihashUnit
from .ethash import EtHashUnit from .ethash import EtHashUnit
@@ -8,6 +9,7 @@ from .kheavyhash import KHeavyHashUnit
from .scrypt import ScryptUnit from .scrypt import ScryptUnit
from .sha256 import SHA256Unit from .sha256 import SHA256Unit
from .x11 import X11Unit from .x11 import X11Unit
from .zksnark import ZkSnarkUnit
class HashUnit: class HashUnit:
@@ -21,3 +23,5 @@ class HashUnit:
EAGLESONG = EaglesongUnit EAGLESONG = EaglesongUnit
ETHASH = EtHashUnit ETHASH = EtHashUnit
EQUIHASH = EquihashUnit EQUIHASH = EquihashUnit
BLOCKFLOW = BlockFlowUnit
ZKSNARK = ZkSnarkUnit

View File

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

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from .base import AlgoHashRateUnitType
class BlockFlowUnit(AlgoHashRateUnitType):
H = 1
KH = int(H) * 1000
MH = int(KH) * 1000
GH = int(MH) * 1000
TH = int(GH) * 1000
PH = int(TH) * 1000
EH = int(PH) * 1000
ZH = int(EH) * 1000
default = MH

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from .base import AlgoHashRateUnitType
class ZkSnarkUnit(AlgoHashRateUnitType):
H = 1
KH = int(H) * 1000
MH = int(KH) * 1000
GH = int(MH) * 1000
TH = int(GH) * 1000
PH = int(TH) * 1000
EH = int(PH) * 1000
ZH = int(EH) * 1000
default = GH

View File

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

View File

@@ -0,0 +1,18 @@
from __future__ import annotations
from typing_extensions import Self
from pyasic.device.algorithm.hashrate.base import AlgoHashRateType
from pyasic.device.algorithm.hashrate.unit.zksnark import ZkSnarkUnit
from .unit import HashUnit
class ZkSnarkHashRate(AlgoHashRateType[ZkSnarkUnit]):
rate: float
unit: ZkSnarkUnit = HashUnit.ZKSNARK.default
def into(self, other: ZkSnarkUnit) -> Self:
return self.__class__(
rate=self.rate / (other.value / self.unit.value), unit=other
)

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from .base import MinerAlgoType
from .hashrate import ZkSnarkHashRate
from .hashrate.unit import ZkSnarkUnit
class ZkSnarkAlgo(MinerAlgoType):
hashrate: type[ZkSnarkHashRate] = ZkSnarkHashRate
unit: type[ZkSnarkUnit] = ZkSnarkUnit
name = "zkSNARK"

View File

@@ -43,6 +43,7 @@ class AntminerModels(MinerModelType):
S19jNoPIC = "S19j No PIC" S19jNoPIC = "S19j No PIC"
S19ProPlus = "S19 Pro+" S19ProPlus = "S19 Pro+"
S19jPro = "S19j Pro" S19jPro = "S19j Pro"
S19jPlus = "S19j+"
S19jProNoPIC = "S19j Pro No PIC" S19jProNoPIC = "S19j Pro No PIC"
S19jProPlus = "S19j Pro+" S19jProPlus = "S19j Pro+"
S19jProPlusNoPIC = "S19j Pro+ No PIC" S19jProPlusNoPIC = "S19j Pro+ No PIC"
@@ -54,14 +55,17 @@ class AntminerModels(MinerModelType):
S19ProPlusHydro = "S19 Pro+ Hydro" S19ProPlusHydro = "S19 Pro+ Hydro"
S19KPro = "S19K Pro" S19KPro = "S19K Pro"
S19kPro = "S19k Pro" S19kPro = "S19k Pro"
S19ProA = "S19 Pro A"
S19kProNoPIC = "S19k Pro No PIC" S19kProNoPIC = "S19k Pro No PIC"
S19jXP = "S19j XP" S19jXP = "S19j XP"
T19 = "T19" T19 = "T19"
S21 = "S21" S21 = "S21"
S21Plus = "S21+" S21Plus = "S21+"
S21PlusHydro = "S21+ Hydro"
S21Pro = "S21 Pro" S21Pro = "S21 Pro"
S21Hydro = "S21 Hydro" S21Hydro = "S21 Hydro"
T21 = "T21" T21 = "T21"
S19XPHydro = "S19 XP Hydro"
def __str__(self): def __str__(self):
return self.value return self.value
@@ -220,6 +224,7 @@ class WhatsminerModels(MinerModelType):
M31V20 = "M31 V20" M31V20 = "M31 V20"
M32V10 = "M32 V10" M32V10 = "M32 V10"
M32V20 = "M32 V20" M32V20 = "M32 V20"
M32S = "M32S"
M33SPlusPlusVG40 = "M33S++ VG40" M33SPlusPlusVG40 = "M33S++ VG40"
M33SPlusPlusVH20 = "M33S++ VH20" M33SPlusPlusVH20 = "M33S++ VH20"
M33SPlusPlusVH30 = "M33S++ VH30" M33SPlusPlusVH30 = "M33S++ VH30"
@@ -451,6 +456,8 @@ class AvalonminerModels(MinerModelType):
Avalon1246 = "Avalon 1246" Avalon1246 = "Avalon 1246"
Avalon1566 = "Avalon 1566" Avalon1566 = "Avalon 1566"
AvalonNano3 = "Avalon Nano 3" AvalonNano3 = "Avalon Nano 3"
AvalonNano3s = "Avalon Nano 3s"
AvalonQHome = "Avalon Q Home"
def __str__(self): def __str__(self):
return self.value return self.value
@@ -473,6 +480,8 @@ class GoldshellModels(MinerModelType):
KDMax = "KD Max" KDMax = "KD Max"
KDBoxII = "KD Box II" KDBoxII = "KD Box II"
KDBoxPro = "KD Box Pro" KDBoxPro = "KD Box Pro"
Byte = "Byte"
MiniDoge = "Mini Doge"
def __str__(self): def __str__(self):
return self.value return self.value
@@ -530,6 +539,7 @@ class IceRiverModels(MinerModelType):
KS5 = "KS5" KS5 = "KS5"
KS5L = "KS5L" KS5L = "KS5L"
KS5M = "KS5M" KS5M = "KS5M"
AL3 = "AL3"
def __str__(self): def __str__(self):
return self.value return self.value
@@ -553,9 +563,17 @@ class BraiinsModels(MinerModelType):
BMM100 = "BMM100" BMM100 = "BMM100"
BMM101 = "BMM101" BMM101 = "BMM101"
def __str__(self):
return self.value
class ElphapexModels(MinerModelType): class ElphapexModels(MinerModelType):
DG1 = "DG1"
DG1Plus = "DG1+" DG1Plus = "DG1+"
DG1Home = "DG1Home"
def __str__(self):
return self.value
class MinerModel: class MinerModel:

View File

@@ -1,352 +0,0 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
from typing import List, Union
from pyasic.errors import APIError
from pyasic.miners import AnyMiner
from pyasic.miners.backends import AntminerModern, BOSMiner, BTMiner
from pyasic.miners.device.models import (
S9,
S17,
T17,
S17e,
S17Plus,
S17Pro,
T17e,
T17Plus,
)
FAN_USAGE = 50 # 50 W per fan
class MinerLoadBalancer:
"""A load balancer for miners. Can be passed a list of `AnyMiner`, or a list of phases (lists of `AnyMiner`)."""
def __init__(
self,
phases: Union[List[List[AnyMiner]], None] = None,
):
self.phases = [_MinerPhaseBalancer(phase) for phase in phases]
async def balance(self, wattage: int) -> int:
phase_wattage = wattage // len(self.phases)
setpoints = await asyncio.gather(
*[phase.get_balance_setpoints(phase_wattage) for phase in self.phases]
)
tasks = []
total_wattage = 0
for setpoint in setpoints:
wattage_set = 0
for miner in setpoint:
if setpoint[miner]["set"] == "on":
wattage_set += setpoint[miner]["max"]
tasks.append(setpoint[miner]["miner"].resume_mining())
elif setpoint[miner]["set"] == "off":
wattage_set += setpoint[miner]["min"]
tasks.append(setpoint[miner]["miner"].stop_mining())
else:
wattage_set += setpoint[miner]["set"]
tasks.append(
setpoint[miner]["miner"].set_power_limit(setpoint[miner]["set"])
)
total_wattage += wattage_set
await asyncio.gather(*tasks)
return total_wattage
class _MinerPhaseBalancer:
def __init__(self, miners: List[AnyMiner]):
self.miners = {
str(miner.ip): {
"miner": miner,
"set": 0,
"min": miner.expected_fans * FAN_USAGE,
}
for miner in miners
}
for miner in miners:
if (
isinstance(miner, BTMiner)
and not (miner.raw_model.startswith("M2") if miner.raw_model else True)
) or isinstance(miner, BOSMiner):
if isinstance(miner, S9):
self.miners[str(miner.ip)]["tune"] = True
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 1400
elif True in [
isinstance(miner, x)
for x in [S17, S17Plus, S17Pro, S17e, T17, T17Plus, T17e]
]:
self.miners[str(miner.ip)]["tune"] = True
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 2400
else:
self.miners[str(miner.ip)]["tune"] = True
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 3600
elif isinstance(miner, AntminerModern):
self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 3600
elif isinstance(miner, BTMiner):
self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 3600
if miner.raw_model:
if miner.raw_model.startswith("M2"):
self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 2400
else:
self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = False
self.miners[str(miner.ip)]["max"] = 3600
self.miners[str(miner.ip)]["min"] = 3600
async def balance(self, wattage: int) -> int:
setpoint = await self.get_balance_setpoints(wattage)
wattage_set = 0
tasks = []
for miner in setpoint:
if setpoint[miner]["set"] == "on":
wattage_set += setpoint[miner]["max"]
tasks.append(setpoint[miner]["miner"].resume_mining())
elif setpoint[miner]["set"] == "off":
wattage_set += setpoint[miner]["min"]
tasks.append(setpoint[miner]["miner"].stop_mining())
else:
wattage_set += setpoint[miner]["set"]
tasks.append(
setpoint[miner]["miner"].set_power_limit(setpoint[miner]["set"])
)
await asyncio.gather(*tasks)
return wattage_set
async def get_balance_setpoints(self, wattage: int) -> dict:
# gather data needed to optimize shutdown only miners
dp = ["hashrate", "wattage", "wattage_limit", "hashboards"]
data = await asyncio.gather(
*[
self.miners[miner]["miner"].get_data(data_to_get=dp)
for miner in self.miners
]
)
pct_expected_list = [d.percent_ideal for d in data]
pct_ideal = 0
if len(pct_expected_list) > 0:
pct_ideal = sum(pct_expected_list) / len(pct_expected_list)
wattage = round(wattage * 1 / (pct_ideal / 100))
for data_point in data:
if (not self.miners[data_point.ip]["tune"]) and (
not self.miners[data_point.ip]["shutdown"]
):
# cant do anything with it so need to find a semi-accurate power limit
if data_point.wattage_limit is not None:
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
elif data_point.wattage is not None:
self.miners[data_point.ip]["max"] = int(data_point.wattage)
self.miners[data_point.ip]["min"] = int(data_point.wattage)
max_tune_wattage = sum(
[miner["max"] for miner in self.miners.values() if miner["tune"]]
)
max_shutdown_wattage = sum(
[
miner["max"]
for miner in self.miners.values()
if (not miner["tune"]) and (miner["shutdown"])
]
)
max_other_wattage = sum(
[
miner["max"]
for miner in self.miners.values()
if (not miner["tune"]) and (not miner["shutdown"])
]
)
min_tune_wattage = sum(
[miner["min"] for miner in self.miners.values() if miner["tune"]]
)
min_shutdown_wattage = sum(
[
miner["min"]
for miner in self.miners.values()
if (not miner["tune"]) and (miner["shutdown"])
]
)
# min_other_wattage = sum(
# [
# miner["min"]
# for miner in self.miners.values()
# if (not miner["tune"]) and (not miner["shutdown"])
# ]
# )
# make sure wattage isnt set too high
if wattage > (max_tune_wattage + max_shutdown_wattage + max_other_wattage):
raise APIError(
f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W"
)
# should now know wattage limits and which can be tuned/shutdown
# check if 1/2 max of the miners which can be tuned is low enough
if (max_tune_wattage / 2) + max_shutdown_wattage + max_other_wattage < wattage:
useable_wattage = wattage - (max_other_wattage + max_shutdown_wattage)
useable_miners = len(
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
)
if not useable_miners == 0:
watts_per_miner = useable_wattage // useable_miners
# loop through and set useable miners to wattage
for miner in self.miners:
if (self.miners[miner]["set"] == 0) and (
self.miners[miner]["tune"]
):
self.miners[miner]["set"] = watts_per_miner
elif self.miners[miner]["set"] == 0 and (
self.miners[miner]["shutdown"]
):
self.miners[miner]["set"] = "on"
# check if shutting down miners will help
elif (
max_tune_wattage / 2
) + min_shutdown_wattage + max_other_wattage < wattage:
# tuneable inclusive since could be S9 BOS+ and S19 Stock, would rather shut down the S9, tuneable should always support shutdown
useable_wattage = wattage - (
min_tune_wattage + max_other_wattage + min_shutdown_wattage
)
for miner in sorted(
[miner for miner in self.miners.values() if miner["shutdown"]],
key=lambda x: x["max"],
reverse=True,
):
if miner["tune"]:
miner_min_watt_use = miner["max"] / 2
useable_wattage -= miner_min_watt_use - miner["min"]
if useable_wattage < 0:
useable_wattage += miner_min_watt_use - miner["min"]
self.miners[str(miner["miner"].ip)]["set"] = "off"
else:
miner_min_watt_use = miner["max"]
useable_wattage -= miner_min_watt_use - miner["min"]
if useable_wattage < 0:
useable_wattage += miner_min_watt_use - miner["min"]
self.miners[str(miner["miner"].ip)]["set"] = "off"
new_shutdown_wattage = sum(
[
miner["max"] if miner["set"] == 0 else miner["min"]
for miner in self.miners.values()
if miner["shutdown"] and not miner["tune"]
]
)
new_tune_wattage = sum(
[
miner["min"]
for miner in self.miners.values()
if miner["tune"] and miner["set"] == "off"
]
)
useable_wattage = wattage - (
new_tune_wattage + max_other_wattage + new_shutdown_wattage
)
useable_miners = len(
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
)
if not useable_miners == 0:
watts_per_miner = useable_wattage // useable_miners
# loop through and set useable miners to wattage
for miner in self.miners:
if (self.miners[miner]["set"] == 0) and (
self.miners[miner]["tune"]
):
self.miners[miner]["set"] = watts_per_miner
elif self.miners[miner]["set"] == 0 and (
self.miners[miner]["shutdown"]
):
self.miners[miner]["set"] = "on"
# check if shutting down tuneable miners will do it
elif min_tune_wattage + min_shutdown_wattage + max_other_wattage < wattage:
# all miners that can be shutdown need to be
for miner in self.miners:
if (not self.miners[miner]["tune"]) and (
self.miners[miner]["shutdown"]
):
self.miners[miner]["set"] = "off"
# calculate wattage usable by tuneable miners
useable_wattage = wattage - (
min_tune_wattage + max_other_wattage + min_shutdown_wattage
)
# loop through miners to see how much is actually useable
# sort the largest first
for miner in sorted(
[
miner
for miner in self.miners.values()
if miner["tune"] and miner["shutdown"]
],
key=lambda x: x["max"],
reverse=True,
):
# add min to useable wattage since it was removed earlier, and remove 1/2 tuner max
useable_wattage -= (miner["max"] / 2) - miner["min"]
if useable_wattage < 0:
useable_wattage += (miner["max"] / 2) - miner["min"]
self.miners[str(miner["miner"].ip)]["set"] = "off"
new_tune_wattage = sum(
[
miner["min"]
for miner in self.miners.values()
if miner["tune"] and miner["set"] == "off"
]
)
useable_wattage = wattage - (
new_tune_wattage + max_other_wattage + min_shutdown_wattage
)
useable_miners = len(
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
)
if not useable_miners == 0:
watts_per_miner = useable_wattage // useable_miners
# loop through and set useable miners to wattage
for miner in self.miners:
if (self.miners[miner]["set"] == 0) and (
self.miners[miner]["tune"]
):
self.miners[miner]["set"] = watts_per_miner
elif self.miners[miner]["set"] == 0 and (
self.miners[miner]["shutdown"]
):
self.miners[miner]["set"] = "on"
else:
raise APIError(
f"Wattage setpoint is too low, setpoint: {wattage}W, min: {min_tune_wattage + min_shutdown_wattage + max_other_wattage}W"
) # PhaseBalancingError(f"Wattage setpoint is too low, setpoint: {wattage}W, min: {min_tune_wattage + min_shutdown_wattage + max_other_wattage}W")
return self.miners

View File

@@ -25,7 +25,9 @@ from pyasic.miners.device.models import (
S19i, S19i,
S19j, S19j,
S19jNoPIC, S19jNoPIC,
S19jPlus,
S19jPro, S19jPro,
S19jProPlus,
S19jXP, S19jXP,
S19KPro, S19KPro,
S19Plus, S19Plus,
@@ -80,6 +82,10 @@ class BMMinerS19jPro(AntminerModern, S19jPro):
pass pass
class BMMinerS19jPlus(AntminerModern, S19jPlus):
pass
class BMMinerS19L(AntminerModern, S19L): class BMMinerS19L(AntminerModern, S19L):
pass pass
@@ -102,3 +108,7 @@ class BMMinerS19KPro(AntminerModern, S19KPro):
class BMMinerS19jXP(AntminerModern, S19jXP): class BMMinerS19jXP(AntminerModern, S19jXP):
pass pass
class BMMinerS19jProPlus(AntminerModern, S19jProPlus):
pass

View File

@@ -22,7 +22,9 @@ from .S19 import (
BMMinerS19i, BMMinerS19i,
BMMinerS19j, BMMinerS19j,
BMMinerS19jNoPIC, BMMinerS19jNoPIC,
BMMinerS19jPlus,
BMMinerS19jPro, BMMinerS19jPro,
BMMinerS19jProPlus,
BMMinerS19jXP, BMMinerS19jXP,
BMMinerS19KPro, BMMinerS19KPro,
BMMinerS19L, BMMinerS19L,

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import AntminerModern from pyasic.miners.backends import AntminerModern
from pyasic.miners.device.models import S21, S21Hydro, S21Plus, S21Pro from pyasic.miners.device.models import S21, S21Hydro, S21Plus, S21PlusHydro, S21Pro
class BMMinerS21(AntminerModern, S21): class BMMinerS21(AntminerModern, S21):
@@ -26,6 +26,10 @@ class BMMinerS21Plus(AntminerModern, S21Plus):
pass pass
class BMMinerS21PlusHydro(AntminerModern, S21PlusHydro):
pass
class BMMinerS21Pro(AntminerModern, S21Pro): class BMMinerS21Pro(AntminerModern, S21Pro):
pass pass

View File

@@ -13,5 +13,11 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .S21 import BMMinerS21, BMMinerS21Hydro, BMMinerS21Plus, BMMinerS21Pro from .S21 import (
BMMinerS21,
BMMinerS21Hydro,
BMMinerS21Plus,
BMMinerS21PlusHydro,
BMMinerS21Pro,
)
from .T21 import BMMinerT21 from .T21 import BMMinerT21

View File

@@ -30,6 +30,7 @@ from pyasic.miners.device.models import (
S19Plus, S19Plus,
S19Pro, S19Pro,
S19ProPlusHydro, S19ProPlusHydro,
S19XPHydro,
) )
@@ -87,3 +88,7 @@ class BOSMinerS19XP(BOSer, S19XP):
class BOSMinerS19ProPlusHydro(BOSer, S19ProPlusHydro): class BOSMinerS19ProPlusHydro(BOSer, S19ProPlusHydro):
pass pass
class BOSMinerS19XPHydro(BOSer, S19XPHydro):
pass

View File

@@ -29,5 +29,6 @@ from .S19 import (
BOSMinerS19Pro, BOSMinerS19Pro,
BOSMinerS19ProPlusHydro, BOSMinerS19ProPlusHydro,
BOSMinerS19XP, BOSMinerS19XP,
BOSMinerS19XPHydro,
) )
from .T19 import BOSMinerT19 from .T19 import BOSMinerT19

View File

@@ -15,12 +15,40 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import BOSer from pyasic.miners.backends import BOSer
from pyasic.miners.device.models import S21, S21Pro from pyasic.miners.device.models import S21, S21Hydro, S21Plus, S21PlusHydro, S21Pro
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
class BOSMinerS21(BOSer, S21): class BOSMinerS21(BOSer, S21):
pass pass
class BOSMinerS21Plus(BOSer, S21Plus):
pass
class BOSMinerS21PlusHydro(BOSer, S21PlusHydro):
pass
class BOSMinerS21Pro(BOSer, S21Pro): class BOSMinerS21Pro(BOSer, S21Pro):
pass pass
class BOSMinerS21Hydro(BOSer, S21Hydro):
pass

View File

@@ -14,5 +14,11 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .S21 import BOSMinerS21, BOSMinerS21Pro from .S21 import (
BOSMinerS21,
BOSMinerS21Hydro,
BOSMinerS21Plus,
BOSMinerS21PlusHydro,
BOSMinerS21Pro,
)
from .T21 import BOSMinerT21 from .T21 import BOSMinerT21

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.backends import LUXMiner
from pyasic.miners.device.models import T21
class LUXMinerT21(LUXMiner, T21):
pass

View File

@@ -15,3 +15,4 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .S21 import LUXMinerS21 from .S21 import LUXMinerS21
from .T21 import LUXMinerT21

View File

@@ -20,13 +20,16 @@ from pyasic.miners.device.models import (
S19XP, S19XP,
S19a, S19a,
S19aPro, S19aPro,
S19Hydro,
S19i, S19i,
S19j, S19j,
S19jPro, S19jPro,
S19kPro, S19kPro,
S19NoPIC, S19NoPIC,
S19Pro, S19Pro,
S19ProA,
S19ProHydro, S19ProHydro,
S19XPHydro,
) )
@@ -42,10 +45,18 @@ class VNishS19Pro(VNish, S19Pro):
pass pass
class VNishS19Hydro(VNish, S19Hydro):
pass
class VNishS19XP(VNish, S19XP): class VNishS19XP(VNish, S19XP):
pass pass
class VNishS19XPHydro(VNish, S19XPHydro):
pass
class VNishS19a(VNish, S19a): class VNishS19a(VNish, S19a):
pass pass
@@ -54,6 +65,10 @@ class VNishS19aPro(VNish, S19aPro):
pass pass
class VNishS19ProA(VNish, S19ProA):
pass
class VNishS19i(VNish, S19i): class VNishS19i(VNish, S19i):
pass pass

View File

@@ -18,13 +18,16 @@ from .S19 import (
VNishS19, VNishS19,
VNishS19a, VNishS19a,
VNishS19aPro, VNishS19aPro,
VNishS19Hydro,
VNishS19i, VNishS19i,
VNishS19j, VNishS19j,
VNishS19jPro, VNishS19jPro,
VNishS19kPro, VNishS19kPro,
VNishS19NoPIC, VNishS19NoPIC,
VNishS19Pro, VNishS19Pro,
VNishS19ProA,
VNishS19ProHydro, VNishS19ProHydro,
VNishS19XP, VNishS19XP,
VNishS19XPHydro,
) )
from .T19 import VNishT19 from .T19 import VNishT19

View File

@@ -15,8 +15,24 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from pyasic.miners.backends import VNish from pyasic.miners.backends import VNish
from pyasic.miners.device.models import S21 from pyasic.miners.device.models import S21, S21Hydro, S21Plus, S21PlusHydro, S21Pro
class VNishS21(VNish, S21): class VNishS21(VNish, S21):
pass pass
class VNishS21Plus(VNish, S21Plus):
pass
class VNishS21PlusHydro(VNish, S21PlusHydro):
pass
class VNishS21Pro(VNish, S21Pro):
pass
class VNishS21Hydro(VNish, S21Hydro):
pass

View File

@@ -14,5 +14,11 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .S21 import VNishS21 from .S21 import (
VNishS21,
VNishS21Hydro,
VNishS21Plus,
VNishS21PlusHydro,
VNishS21Pro,
)
from .T21 import VNishT21 from .T21 import VNishT21

View File

@@ -18,5 +18,5 @@ from pyasic.miners.backends import VNish
from pyasic.miners.device.models import L3Plus from pyasic.miners.device.models import L3Plus
class VnishL3Plus(VNish, L3Plus): class VNishL3Plus(VNish, L3Plus):
pass pass

View File

@@ -14,4 +14,4 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .L3 import VnishL3Plus from .L3 import VNishL3Plus

View File

@@ -18,5 +18,5 @@ from pyasic.miners.backends import VNish
from pyasic.miners.device.models import L7 from pyasic.miners.device.models import L7
class VnishL7(VNish, L7): class VNishL7(VNish, L7):
pass pass

View File

@@ -14,4 +14,4 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .L7 import VnishL7 from .L7 import VNishL7

View File

@@ -0,0 +1,22 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.backends import VNish
from pyasic.miners.device.models import L9
class VNishL9(VNish, L9):
pass

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .L9 import VNishL9

View File

@@ -16,6 +16,7 @@
from .X3 import * from .X3 import *
from .X7 import * from .X7 import *
from .X9 import *
from .X17 import * from .X17 import *
from .X19 import * from .X19 import *
from .X21 import * from .X21 import *

View File

@@ -0,0 +1,22 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners.backends import AvalonMiner
from pyasic.miners.device.models import AvalonQHome
class CGMinerAvalonQHome(AvalonMiner, AvalonQHome):
pass

View File

@@ -0,0 +1 @@
from .Q import CGMinerAvalonQHome

View File

@@ -22,3 +22,4 @@ from .A11X import *
from .A12X import * from .A12X import *
from .A15X import * from .A15X import *
from .nano import * from .nano import *
from .Q import *

View File

@@ -14,4 +14,4 @@
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from .nano3 import CGMinerAvalonNano3 from .nano3 import CGMinerAvalonNano3, CGMinerAvalonNano3s

View File

@@ -13,9 +13,11 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from typing import Optional from typing import Any
from pyasic import APIError from pyasic import APIError
from pyasic.data.boards import HashBoard
from pyasic.device.algorithm import AlgoHashRateType
from pyasic.miners.backends import AvalonMiner from pyasic.miners.backends import AvalonMiner
from pyasic.miners.data import ( from pyasic.miners.data import (
DataFunction, DataFunction,
@@ -24,7 +26,7 @@ from pyasic.miners.data import (
RPCAPICommand, RPCAPICommand,
WebAPICommand, WebAPICommand,
) )
from pyasic.miners.device.models import AvalonNano3 from pyasic.miners.device.models import AvalonNano3, AvalonNano3s
from pyasic.web.avalonminer import AvalonMinerWebAPI from pyasic.web.avalonminer import AvalonMinerWebAPI
AVALON_NANO_DATA_LOC = DataLocations( AVALON_NANO_DATA_LOC = DataLocations(
@@ -47,32 +49,89 @@ AVALON_NANO_DATA_LOC = DataLocations(
), ),
str(DataOptions.EXPECTED_HASHRATE): DataFunction( str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate", "_get_expected_hashrate",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.HASHBOARDS): DataFunction( str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards", "_get_hashboards",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.ENVIRONMENT_TEMP): DataFunction( str(DataOptions.ENVIRONMENT_TEMP): DataFunction(
"_get_env_temp", "_get_env_temp",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.WATTAGE_LIMIT): DataFunction( str(DataOptions.WATTAGE_LIMIT): DataFunction(
"_get_wattage_limit", "_get_wattage_limit",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.WATTAGE): DataFunction( str(DataOptions.WATTAGE): DataFunction(
"_get_wattage", "_get_wattage",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.FANS): DataFunction( str(DataOptions.FANS): DataFunction(
"_get_fans", "_get_fans",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.FAULT_LIGHT): DataFunction( str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light", "_get_fault_light",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_stats", "stats")],
), ),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
}
)
AVALON_NANO3S_DATA_LOC = DataLocations(
**{
str(DataOptions.MAC): DataFunction(
"_get_mac",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.API_VERSION): DataFunction(
"_get_api_ver",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.ENVIRONMENT_TEMP): DataFunction(
"_get_env_temp",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.WATTAGE_LIMIT): DataFunction(
"_get_wattage_limit",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light",
[RPCAPICommand("rpc_estats", "estats")],
),
str(DataOptions.UPTIME): DataFunction( str(DataOptions.UPTIME): DataFunction(
"_get_uptime", "_get_uptime",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_stats", "stats")],
@@ -91,12 +150,12 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3):
data_locations = AVALON_NANO_DATA_LOC data_locations = AVALON_NANO_DATA_LOC
async def _get_mac(self, web_minerinfo: dict) -> Optional[dict]: async def _get_mac(self, web_minerinfo: dict[Any, Any] | None = None) -> str | None:
if web_minerinfo is None: if web_minerinfo is None:
try: try:
web_minerinfo = await self.web.minerinfo() web_minerinfo = await self.web.minerinfo()
except APIError: except APIError:
pass return None
if web_minerinfo is not None: if web_minerinfo is not None:
try: try:
@@ -105,3 +164,68 @@ class CGMinerAvalonNano3(AvalonMiner, AvalonNano3):
return mac.upper() return mac.upper()
except (KeyError, ValueError): except (KeyError, ValueError):
pass pass
return None
class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
data_locations = AVALON_NANO3S_DATA_LOC
async def _get_wattage(self, rpc_estats: dict | None = None) -> int | None:
if rpc_estats is None:
try:
rpc_estats = await self.rpc.estats()
except APIError:
return None
if rpc_estats is not None:
try:
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
return int(parsed_estats["PS"][6])
except (IndexError, KeyError, ValueError, TypeError):
pass
return None
async def _get_hashrate(
self, rpc_estats: dict | None = None
) -> AlgoHashRateType | None:
if rpc_estats is None:
try:
rpc_estats = await self.rpc.estats()
except APIError:
return None
if rpc_estats is not None:
try:
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
return self.algo.hashrate(
rate=float(parsed_estats["GHSspd"]), unit=self.algo.unit.GH
).into(self.algo.unit.default)
except (IndexError, KeyError, ValueError, TypeError):
pass
return None
async def _get_hashboards(self, rpc_estats: dict | None = None) -> list[HashBoard]:
hashboards = await AvalonMiner._get_hashboards(self, rpc_estats)
if rpc_estats is None:
try:
rpc_estats = await self.rpc.estats()
except APIError:
return hashboards
if rpc_estats is not None:
try:
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
except (IndexError, KeyError, ValueError, TypeError):
return hashboards
for board in range(len(hashboards)):
try:
board_hr = parsed_estats["GHSspd"]
hashboards[board].hashrate = self.algo.hashrate(
rate=float(board_hr), unit=self.algo.unit.GH
).into(self.algo.unit.default)
except LookupError:
pass
return hashboards

View File

@@ -20,7 +20,7 @@ from .bfgminer import BFGMiner
from .bitaxe import BitAxe from .bitaxe import BitAxe
from .bmminer import BMMiner from .bmminer import BMMiner
from .braiins_os import BOSer, BOSMiner from .braiins_os import BOSer, BOSMiner
from .btminer import BTMiner from .btminer import BTMiner, BTMinerV2, BTMinerV3
from .cgminer import CGMiner from .cgminer import CGMiner
from .elphapex import ElphapexMiner from .elphapex import ElphapexMiner
from .epic import ePIC from .epic import ePIC

View File

@@ -16,13 +16,12 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List, Optional
from pyasic.config import MinerConfig, MiningModeConfig from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, X19Error from pyasic.data.error_codes import X19Error
from pyasic.data.pools import PoolMetrics, PoolUrl from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.device.algorithm import AlgoHashRate from pyasic.device.algorithm import AlgoHashRateType
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.miners.backends.bmminer import BMMiner from pyasic.miners.backends.bmminer import BMMiner
from pyasic.miners.backends.cgminer import CGMiner from pyasic.miners.backends.cgminer import CGMiner
@@ -39,6 +38,10 @@ from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
ANTMINER_MODERN_DATA_LOC = DataLocations( ANTMINER_MODERN_DATA_LOC = DataLocations(
**{ **{
str(DataOptions.SERIAL_NUMBER): DataFunction(
"_get_serial_number",
[WebAPICommand("web_get_system_info", "get_system_info")],
),
str(DataOptions.MAC): DataFunction( str(DataOptions.MAC): DataFunction(
"_get_mac", "_get_mac",
[WebAPICommand("web_get_system_info", "get_system_info")], [WebAPICommand("web_get_system_info", "get_system_info")],
@@ -116,9 +119,11 @@ class AntminerModern(BMMiner):
data = await self.web.get_miner_conf() data = await self.web.get_miner_conf()
if data: if data:
self.config = MinerConfig.from_am_modern(data) self.config = MinerConfig.from_am_modern(data)
return self.config return self.config or MinerConfig()
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: async def send_config(
self, config: MinerConfig, user_suffix: str | None = None
) -> None:
self.config = config self.config = config
await self.web.set_miner_conf(config.as_am_modern(user_suffix=user_suffix)) await self.web.set_miner_conf(config.as_am_modern(user_suffix=user_suffix))
# if data: # if data:
@@ -131,54 +136,77 @@ class AntminerModern(BMMiner):
# break # break
# await asyncio.sleep(1) # await asyncio.sleep(1)
async def upgrade_firmware(self, file: Path, keep_settings: bool = True) -> str: async def upgrade_firmware(
self,
*,
file: str | None = None,
url: str | None = None,
version: str | None = None,
keep_settings: bool = True,
) -> bool:
""" """
Upgrade the firmware of the AntMiner device. Upgrade the firmware of the AntMiner device.
Args: Args:
file (Path): Path to the firmware file. file: Path to the firmware file as a string.
keep_settings (bool): Whether to keep the current settings after the update. url: URL to download firmware from (not implemented).
version: Version to upgrade to (not implemented).
keep_settings: Whether to keep the current settings after the update.
Returns: Returns:
str: Result of the upgrade process. bool: True if upgrade was successful, False otherwise.
""" """
if not file: if not file:
raise ValueError("File location must be provided for firmware upgrade.") logging.error("File location must be provided for firmware upgrade.")
return False
if url or version:
logging.warning(
"URL and version parameters are not implemented for Antminer."
)
try: try:
file_path = Path(file)
if not hasattr(self.web, "update_firmware"):
logging.error(
"Firmware upgrade not supported via web API for this Antminer model."
)
return False
result = await self.web.update_firmware( result = await self.web.update_firmware(
file=file, keep_settings=keep_settings file=file_path, keep_settings=keep_settings
) )
if result.get("success"): if result.get("success"):
logging.info( logging.info(
"Firmware upgrade process completed successfully for AntMiner." "Firmware upgrade process completed successfully for AntMiner."
) )
return "Firmware upgrade completed successfully." return True
else: else:
error_message = result.get("message", "Unknown error") error_message = result.get("message", "Unknown error")
logging.error(f"Firmware upgrade failed. Response: {error_message}") logging.error(f"Firmware upgrade failed. Response: {error_message}")
return f"Firmware upgrade failed. Response: {error_message}" return False
except Exception as e: except Exception as e:
logging.error( logging.error(
f"An error occurred during the firmware upgrade process: {e}", f"An error occurred during the firmware upgrade process: {e}",
exc_info=True, exc_info=True,
) )
raise return False
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
data = await self.web.blink(blink=True) data = await self.web.blink(blink=True)
if data: if data:
if data.get("code") == "B000": if data.get("code") == "B000":
self.light = True self.light = True
return self.light return self.light or False
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
data = await self.web.blink(blink=False) data = await self.web.blink(blink=False)
if data: if data:
if data.get("code") == "B100": if data.get("code") == "B100":
self.light = False self.light = False
return self.light return self.light or False
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.web.reboot() data = await self.web.reboot()
@@ -198,7 +226,9 @@ class AntminerModern(BMMiner):
await self.send_config(cfg) await self.send_config(cfg)
return True return True
async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: async def _get_hostname(
self, web_get_system_info: dict | None = None
) -> str | None:
if web_get_system_info is None: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -210,8 +240,9 @@ class AntminerModern(BMMiner):
return web_get_system_info["hostname"] return web_get_system_info["hostname"]
except KeyError: except KeyError:
pass pass
return None
async def _get_mac(self, web_get_system_info: dict = None) -> Optional[str]: async def _get_mac(self, web_get_system_info: dict | None = None) -> str | None:
if web_get_system_info is None: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -230,8 +261,11 @@ class AntminerModern(BMMiner):
return data["macaddr"] return data["macaddr"]
except KeyError: except KeyError:
pass pass
return None
async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]: async def _get_errors( # type: ignore[override]
self, web_summary: dict | None = None
) -> list[X19Error]:
if web_summary is None: if web_summary is None:
try: try:
web_summary = await self.web.summary() web_summary = await self.web.summary()
@@ -251,7 +285,7 @@ class AntminerModern(BMMiner):
pass pass
return errors return errors
async def _get_hashboards(self) -> List[HashBoard]: async def _get_hashboards(self) -> list[HashBoard]: # type: ignore[override]
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
@@ -269,21 +303,53 @@ class AntminerModern(BMMiner):
try: try:
for board in rpc_stats["STATS"][0]["chain"]: for board in rpc_stats["STATS"][0]["chain"]:
hashboards[board["index"]].hashrate = self.algo.hashrate( hashboards[board["index"]].hashrate = self.algo.hashrate(
rate=board["rate_real"], unit=self.algo.unit.GH rate=board["rate_real"],
).into(self.algo.unit.default) unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
hashboards[board["index"]].chips = board["asic_num"] hashboards[board["index"]].chips = board["asic_num"]
board_temp_data = list(
filter(lambda x: not x == 0, board["temp_pcb"]) if "S21+ Hyd" in self.model:
) hashboards[board["index"]].inlet_temp = board["temp_pcb"][0]
hashboards[board["index"]].temp = sum(board_temp_data) / len( hashboards[board["index"]].outlet_temp = board["temp_pcb"][2]
board_temp_data hashboards[board["index"]].chip_temp = board["temp_pic"][0]
) board_temp_data = list(
chip_temp_data = list( filter(
filter(lambda x: not x == 0, board["temp_chip"]) lambda x: not x == 0,
) [
hashboards[board["index"]].chip_temp = sum(chip_temp_data) / len( board["temp_pic"][1],
chip_temp_data board["temp_pic"][2],
) board["temp_pic"][3],
board["temp_pcb"][1],
board["temp_pcb"][3],
],
)
)
hashboards[board["index"]].temp = (
sum(board_temp_data) / len(board_temp_data)
if len(board_temp_data) > 0
else 0
)
else:
board_temp_data = list(
filter(lambda x: not x == 0, board["temp_pcb"])
)
hashboards[board["index"]].temp = (
sum(board_temp_data) / len(board_temp_data)
if len(board_temp_data) > 0
else 0
)
chip_temp_data = list(
filter(lambda x: not x == 0, board["temp_chip"])
)
hashboards[board["index"]].chip_temp = (
sum(chip_temp_data) / len(chip_temp_data)
if len(chip_temp_data) > 0
else 0
)
hashboards[board["index"]].serial_number = board["sn"] hashboards[board["index"]].serial_number = board["sn"]
hashboards[board["index"]].missing = False hashboards[board["index"]].missing = False
except LookupError: except LookupError:
@@ -291,8 +357,8 @@ class AntminerModern(BMMiner):
return hashboards return hashboards
async def _get_fault_light( async def _get_fault_light(
self, web_get_blink_status: dict = None self, web_get_blink_status: dict | None = None
) -> Optional[bool]: ) -> bool | None:
if self.light: if self.light:
return self.light return self.light
@@ -310,8 +376,8 @@ class AntminerModern(BMMiner):
return self.light return self.light
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_stats: dict = None self, rpc_stats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_stats is None: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -327,9 +393,26 @@ class AntminerModern(BMMiner):
rate_unit = "GH" rate_unit = "GH"
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit) rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit)
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except LookupError: except LookupError:
pass pass
return None
async def _get_serial_number(
self, web_get_system_info: dict | None = None
) -> str | None:
if web_get_system_info is None:
try:
web_get_system_info = await self.web.get_system_info()
except APIError:
pass
if web_get_system_info is not None:
try:
return web_get_system_info["serinum"]
except LookupError:
pass
return None
async def set_static_ip( async def set_static_ip(
self, self,
@@ -337,10 +420,10 @@ class AntminerModern(BMMiner):
dns: str, dns: str,
gateway: str, gateway: str,
subnet_mask: str = "255.255.255.0", subnet_mask: str = "255.255.255.0",
hostname: str = None, hostname: str | None = None,
): ):
if not hostname: if not hostname:
hostname = await self.get_hostname() hostname = await self.get_hostname() or ""
await self.web.set_network_conf( await self.web.set_network_conf(
ip=ip, ip=ip,
dns=dns, dns=dns,
@@ -350,9 +433,9 @@ class AntminerModern(BMMiner):
protocol=2, protocol=2,
) )
async def set_dhcp(self, hostname: str = None): async def set_dhcp(self, hostname: str | None = None):
if not hostname: if not hostname:
hostname = await self.get_hostname() hostname = await self.get_hostname() or ""
await self.web.set_network_conf( await self.web.set_network_conf(
ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1 ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1
) )
@@ -373,7 +456,7 @@ class AntminerModern(BMMiner):
protocol=protocol, protocol=protocol,
) )
async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]: async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None:
if web_get_conf is None: if web_get_conf is None:
try: try:
web_get_conf = await self.web.get_miner_conf() web_get_conf = await self.web.get_miner_conf()
@@ -389,8 +472,9 @@ class AntminerModern(BMMiner):
return False return False
except LookupError: except LookupError:
pass pass
return None
async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None:
if rpc_stats is None: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -402,8 +486,9 @@ class AntminerModern(BMMiner):
return int(rpc_stats["STATS"][1]["Elapsed"]) return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass pass
return None
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
if rpc_pools is None: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()
@@ -492,19 +577,22 @@ class AntminerOld(CGMiner):
data = await self.web.get_miner_conf() data = await self.web.get_miner_conf()
if data: if data:
self.config = MinerConfig.from_am_old(data) self.config = MinerConfig.from_am_old(data)
return self.config return self.config or MinerConfig()
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: async def send_config(
self, config: MinerConfig, user_suffix: str | None = None
) -> None:
self.config = config self.config = config
await self.web.set_miner_conf(config.as_am_old(user_suffix=user_suffix)) await self.web.set_miner_conf(config.as_am_old(user_suffix=user_suffix))
async def _get_mac(self) -> Optional[str]: async def _get_mac(self) -> str | None:
try: try:
data = await self.web.get_system_info() data = await self.web.get_system_info()
if data: if data:
return data["macaddr"] return data["macaddr"]
except KeyError: except KeyError:
pass pass
return None
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
# this should time out, after it does do a check # this should time out, after it does do a check
@@ -516,7 +604,7 @@ class AntminerOld(CGMiner):
self.light = True self.light = True
except KeyError: except KeyError:
pass pass
return self.light return self.light or False
async def fault_light_off(self) -> bool: async def fault_light_off(self) -> bool:
await self.web.blink(blink=False) await self.web.blink(blink=False)
@@ -527,7 +615,7 @@ class AntminerOld(CGMiner):
self.light = False self.light = False
except KeyError: except KeyError:
pass pass
return self.light return self.light or False
async def reboot(self) -> bool: async def reboot(self) -> bool:
data = await self.web.reboot() data = await self.web.reboot()
@@ -536,8 +624,8 @@ class AntminerOld(CGMiner):
return False return False
async def _get_fault_light( async def _get_fault_light(
self, web_get_blink_status: dict = None self, web_get_blink_status: dict | None = None
) -> Optional[bool]: ) -> bool | None:
if self.light: if self.light:
return self.light return self.light
@@ -554,7 +642,9 @@ class AntminerOld(CGMiner):
pass pass
return self.light return self.light
async def _get_hostname(self, web_get_system_info: dict = None) -> Optional[str]: async def _get_hostname(
self, web_get_system_info: dict | None = None
) -> str | None:
if web_get_system_info is None: if web_get_system_info is None:
try: try:
web_get_system_info = await self.web.get_system_info() web_get_system_info = await self.web.get_system_info()
@@ -566,8 +656,9 @@ class AntminerOld(CGMiner):
return web_get_system_info["hostname"] return web_get_system_info["hostname"]
except KeyError: except KeyError:
pass pass
return None
async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]: async def _get_fans(self, rpc_stats: dict | None = None) -> list[Fan]:
if self.expected_fans is None: if self.expected_fans is None:
return [] return []
@@ -592,16 +683,16 @@ class AntminerOld(CGMiner):
for fan in range(self.expected_fans): for fan in range(self.expected_fans):
fans_data[fan].speed = rpc_stats["STATS"][1].get( fans_data[fan].speed = rpc_stats["STATS"][1].get(
f"fan{fan_offset+fan}", 0 f"fan{fan_offset + fan}", 0
) )
except LookupError: except LookupError:
pass pass
return fans_data return fans_data
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: async def _get_hashboards(self, rpc_stats: dict | None = None) -> list[HashBoard]:
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
hashboards = [] hashboards: list[HashBoard] = []
if rpc_stats is None: if rpc_stats is None:
try: try:
@@ -641,8 +732,11 @@ class AntminerOld(CGMiner):
hashrate = boards[1].get(f"chain_rate{i}") hashrate = boards[1].get(f"chain_rate{i}")
if hashrate: if hashrate:
hashboard.hashrate = self.algo.hashrate( hashboard.hashrate = self.algo.hashrate(
rate=float(hashrate), unit=self.algo.unit.GH rate=float(hashrate),
).into(self.algo.unit.default) unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
chips = boards[1].get(f"chain_acn{i}") chips = boards[1].get(f"chain_acn{i}")
if chips: if chips:
@@ -659,7 +753,7 @@ class AntminerOld(CGMiner):
return hashboards return hashboards
async def _is_mining(self, web_get_conf: dict = None) -> Optional[bool]: async def _is_mining(self, web_get_conf: dict | None = None) -> bool | None:
if web_get_conf is None: if web_get_conf is None:
try: try:
web_get_conf = await self.web.get_miner_conf() web_get_conf = await self.web.get_miner_conf()
@@ -684,7 +778,9 @@ class AntminerOld(CGMiner):
else: else:
return False return False
async def _get_uptime(self, rpc_stats: dict = None) -> Optional[int]: return None
async def _get_uptime(self, rpc_stats: dict | None = None) -> int | None:
if rpc_stats is None: if rpc_stats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_stats = await self.rpc.stats()
@@ -696,3 +792,4 @@ class AntminerOld(CGMiner):
return int(rpc_stats["STATS"][1]["Elapsed"]) return int(rpc_stats["STATS"][1]["Elapsed"])
except LookupError: except LookupError:
pass pass
return None

View File

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

View File

@@ -13,15 +13,16 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import copy
import re import re
from typing import List, Optional import time
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.device.algorithm import AlgoHashRate from pyasic.device.algorithm import AlgoHashRateType
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.miners.backends.cgminer import CGMiner from pyasic.miners.backends.cgminer import CGMiner
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.rpc.avalonminer import AvalonMinerRPCAPI
AVALON_DATA_LOC = DataLocations( AVALON_DATA_LOC = DataLocations(
**{ **{
@@ -43,31 +44,31 @@ AVALON_DATA_LOC = DataLocations(
), ),
str(DataOptions.EXPECTED_HASHRATE): DataFunction( str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate", "_get_expected_hashrate",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.HASHBOARDS): DataFunction( str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards", "_get_hashboards",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.ENVIRONMENT_TEMP): DataFunction( str(DataOptions.ENVIRONMENT_TEMP): DataFunction(
"_get_env_temp", "_get_env_temp",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.WATTAGE_LIMIT): DataFunction( str(DataOptions.WATTAGE_LIMIT): DataFunction(
"_get_wattage_limit", "_get_wattage_limit",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.WATTAGE): DataFunction( str(DataOptions.WATTAGE): DataFunction(
"_get_wattage", "_get_wattage",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.FANS): DataFunction( str(DataOptions.FANS): DataFunction(
"_get_fans", "_get_fans",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.FAULT_LIGHT): DataFunction( str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light", "_get_fault_light",
[RPCAPICommand("rpc_stats", "stats")], [RPCAPICommand("rpc_estats", "estats")],
), ),
str(DataOptions.UPTIME): DataFunction( str(DataOptions.UPTIME): DataFunction(
"_get_uptime", "_get_uptime",
@@ -84,6 +85,9 @@ AVALON_DATA_LOC = DataLocations(
class AvalonMiner(CGMiner): class AvalonMiner(CGMiner):
"""Handler for Avalon Miners""" """Handler for Avalon Miners"""
_rpc_cls = AvalonMinerRPCAPI
rpc: AvalonMinerRPCAPI
data_locations = AVALON_DATA_LOC data_locations = AVALON_DATA_LOC
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:
@@ -114,7 +118,7 @@ class AvalonMiner(CGMiner):
limit = 1 limit = 1
else: else:
limit = 0 limit = 0
data = await self.rpc.ascset(0, "worklevel,set", 1) data = await self.rpc.ascset(0, "worklevel,set", limit)
except APIError: except APIError:
return False return False
if data["STATUS"][0]["Msg"] == "ASC 0 set OK": if data["STATUS"][0]["Msg"] == "ASC 0 set OK":
@@ -134,51 +138,100 @@ class AvalonMiner(CGMiner):
return False return False
return False return False
async def stop_mining(self) -> bool:
try:
# Shut off 5 seconds from now
timestamp = int(time.time()) + 5
data = await self.rpc.ascset(0, "softoff", f"1:{timestamp}")
except APIError:
return False
if "success" in data["STATUS"][0]["Msg"]:
return True
return False
async def resume_mining(self) -> bool:
try:
# Shut off 5 seconds from now
timestamp = int(time.time()) + 5
data = await self.rpc.ascset(0, "softon", f"1:{timestamp}")
except APIError:
return False
if "success" in data["STATUS"][0]["Msg"]:
return True
return False
@staticmethod @staticmethod
def parse_stats(stats): def parse_estats(data):
_stats_items = re.findall(".+?\\[*?]", stats) # Deep copy to preserve original structure
stats_items = [] new_data = copy.deepcopy(data)
stats_dict = {}
for item in _stats_items:
if ": " in item:
data = item.replace("]", "").split("[")
data_list = [i.split(": ") for i in data[1].strip().split(", ")]
data_dict = {}
try:
for key, val in [tuple(item) for item in data_list]:
data_dict[key] = val
except ValueError:
# --avalon args
for arg_item in data_list:
item_data = arg_item[0].split(" ")
for idx, val in enumerate(item_data):
if idx % 2 == 0 or idx == 0:
data_dict[val] = item_data[idx + 1]
raw_data = [data[0].strip(), data_dict] def convert_value(val, key):
val = val.strip()
if key == "SYSTEMSTATU":
return val
if " " in val:
parts = val.split()
result = []
for part in parts:
if part.isdigit():
result.append(int(part))
else:
try:
result.append(float(part))
except ValueError:
result.append(part)
return result
else: else:
raw_data = [ if val.isdigit():
value return int(val)
for value in item.replace("[", " ") try:
.replace("]", " ") return float(val)
.split(" ")[:-1] except ValueError:
if value != "" return val
]
if len(raw_data) == 1:
raw_data.append("")
if raw_data[0] == "":
raw_data = raw_data[1:]
stats_dict[raw_data[0]] = raw_data[1:] def parse_info_block(info_str):
stats_items.append(raw_data) pattern = re.compile(r"(\w+)\[([^\]]*)\]")
return {
key: convert_value(val, key) for key, val in pattern.findall(info_str)
}
return stats_dict for stat in new_data.get("STATS", []):
keys_to_replace = {}
for key, value in stat.items():
if "MM" in key:
# Normalize key by removing suffix after colon
norm_key = key.split(":")[0]
mm_data = value
if not isinstance(mm_data, str):
continue
if mm_data.startswith("'STATS':"):
mm_data = mm_data[len("'STATS':") :]
keys_to_replace[norm_key] = parse_info_block(mm_data)
elif key == "HBinfo":
match = re.search(r"'(\w+)':\{(.+)\}", value)
if match:
hb_key = match.group(1)
hb_data = match.group(2)
keys_to_replace[key] = {hb_key: parse_info_block(hb_data)}
# Remove old keys and insert parsed versions
for k in list(stat.keys()):
if "MM" in k or k == "HBinfo":
del stat[k]
stat.update(keys_to_replace)
return new_data
################################################## ##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
################################################## ##################################################
async def _get_mac(self, rpc_version: dict = None) -> Optional[str]: async def _get_mac(self, rpc_version: dict | None = None) -> str | None:
if rpc_version is None: if rpc_version is None:
try: try:
rpc_version = await self.rpc.version() rpc_version = await self.rpc.version()
@@ -195,23 +248,28 @@ class AvalonMiner(CGMiner):
return mac return mac
except (KeyError, ValueError): except (KeyError, ValueError):
pass pass
return None
async def _get_hashrate(self, rpc_devs: dict = None) -> Optional[AlgoHashRate]: async def _get_hashrate(
self, rpc_devs: dict | None = None
) -> AlgoHashRateType | None:
if rpc_devs is None: if rpc_devs is None:
try: try:
rpc_devs = await self.rpc.devs() rpc_devs = await self.rpc.devs()
except APIError: except APIError:
pass return None
if rpc_devs is not None: if rpc_devs is not None:
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_devs["DEVS"][0]["MHS 1m"]), unit=self.algo.unit.MH rate=float(rpc_devs["DEVS"][0]["MHS 1m"]),
).into(self.algo.unit.default) unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) # type: ignore[attr-defined]
except (KeyError, IndexError, ValueError, TypeError): except (KeyError, IndexError, ValueError, TypeError):
pass pass
return None
async def _get_hashboards(self, rpc_stats: dict = None) -> List[HashBoard]: async def _get_hashboards(self, rpc_estats: dict | None = None) -> list[HashBoard]:
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
@@ -220,164 +278,216 @@ class AvalonMiner(CGMiner):
for i in range(self.expected_hashboards) for i in range(self.expected_hashboards)
] ]
if rpc_stats is None: if rpc_estats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass pass
if rpc_stats is not None: if rpc_estats is not None:
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] parsed_estats = self.parse_estats(rpc_estats)
parsed_stats = self.parse_stats(unparsed_stats)
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
return hashboards return hashboards
for board in range(self.expected_hashboards): for board in range(self.expected_hashboards):
try: try:
hashboards[board].chip_temp = int(parsed_stats["MTmax"][board]) board_hr = parsed_estats["STATS"][0]["MM ID0"]["MGHS"]
if isinstance(board_hr, list):
hashboards[board].hashrate = self.algo.hashrate(
rate=float(board_hr[board]),
unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
else:
hashboards[board].hashrate = self.algo.hashrate(
rate=float(board_hr),
unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
except LookupError: except LookupError:
pass pass
try: try:
board_hr = parsed_stats["MGHS"][board] hashboards[board].chip_temp = int(
hashboards[board].hashrate = self.algo.hashrate( parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board]
rate=float(board_hr), unit=self.algo.unit.GH )
).into(self.algo.unit.default) except (LookupError, TypeError):
except LookupError: try:
pass hashboards[board].chip_temp = int(
parsed_estats["STATS"][0]["MM ID0"].get(
"Tmax", parsed_estats["STATS"][0]["MM ID0"]["TMax"]
)
)
except LookupError:
pass
try: try:
hashboards[board].temp = int(parsed_stats["MTavg"][board]) hashboards[board].temp = int(
except LookupError: parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board]
pass )
except (LookupError, TypeError):
try:
hashboards[board].temp = int(
parsed_estats["STATS"][0]["MM ID0"].get(
"Tavg", parsed_estats["STATS"][0]["MM ID0"]["TAvg"]
)
)
except LookupError:
pass
try: try:
chip_data = parsed_stats[f"PVT_T{board}"] hashboards[board].inlet_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["MTavg"][board]
)
except (LookupError, TypeError):
try:
hashboards[board].inlet_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["HBITemp"]
)
except LookupError:
pass
try:
hashboards[board].outlet_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board]
)
except (LookupError, TypeError):
try:
hashboards[board].outlet_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["HBOTemp"]
)
except LookupError:
pass
try:
chip_data = parsed_estats["STATS"][0]["MM ID0"][f"PVT_T{board}"]
hashboards[board].missing = False hashboards[board].missing = False
if chip_data: if chip_data:
hashboards[board].chips = len( hashboards[board].chips = len(
[item for item in chip_data if not item == "0"] [item for item in chip_data if not item == "0"]
) )
except LookupError: except (LookupError, TypeError):
pass try:
chip_data = parsed_estats["STATS"][0]["HBinfo"][f"HB{board}"][
f"PVT_T{board}"
]
hashboards[board].missing = False
if chip_data:
hashboards[board].chips = len(
[item for item in chip_data if not item == "0"]
)
except LookupError:
pass
return hashboards return hashboards
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_stats: dict = None self, rpc_estats: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_stats is None: if rpc_estats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass return None
if rpc_stats is not None: if rpc_estats is not None:
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
return self.algo.hashrate( return self.algo.hashrate(
rate=float(parsed_stats["GHSmm"][0]), unit=self.algo.unit.GH rate=float(parsed_estats["GHSmm"]),
).into(self.algo.unit.default) unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) # type: ignore[attr-defined]
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
return None
async def _get_env_temp(self, rpc_stats: dict = None) -> Optional[float]: async def _get_env_temp(self, rpc_estats: dict | None = None) -> float | None:
if rpc_stats is None: if rpc_estats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass pass
if rpc_stats is not None: if rpc_estats is not None:
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats) return float(parsed_estats["Temp"])
return float(parsed_stats["Temp"][0])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
return None
async def _get_wattage_limit(self, rpc_stats: dict = None) -> Optional[int]: async def _get_wattage_limit(self, rpc_estats: dict | None = None) -> int | None:
if rpc_stats is None: if rpc_estats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_estats = await self.rpc.estats()
except APIError:
return None
if rpc_estats is not None:
try:
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
return int(parsed_estats["MPO"])
except (IndexError, KeyError, ValueError, TypeError):
pass
return None
async def _get_wattage(self, rpc_estats: dict | None = None) -> int | None:
if rpc_estats is None:
try:
rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass pass
if rpc_stats is not None: if rpc_estats is not None:
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats) return int(parsed_estats["WALLPOWER"])
return int(parsed_stats["MPO"][0])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
return None
async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]: async def _get_fans(self, rpc_estats: dict | None = None) -> list[Fan]:
if rpc_stats is None:
try:
rpc_stats = await self.rpc.stats()
except APIError:
pass
if rpc_stats is not None:
try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
return int(parsed_stats["WALLPOWER"][0])
except (IndexError, KeyError, ValueError, TypeError):
pass
async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]:
if self.expected_fans is None: if self.expected_fans is None:
return [] return []
if rpc_stats is None: if rpc_estats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass pass
fans_data = [Fan() for _ in range(self.expected_fans)] fans_data = [Fan() for _ in range(self.expected_fans)]
if rpc_stats is not None: if rpc_estats is not None:
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
except LookupError: except LookupError:
return fans_data return fans_data
for fan in range(self.expected_fans): for fan in range(self.expected_fans):
try: try:
fans_data[fan].speed = int(parsed_stats[f"Fan{fan + 1}"][0]) fans_data[fan].speed = int(parsed_estats[f"Fan{fan + 1}"])
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
return fans_data return fans_data
async def _get_fault_light(self, rpc_stats: dict = None) -> Optional[bool]: async def _get_fault_light(self, rpc_estats: dict | None = None) -> bool | None:
if self.light: if self.light:
return self.light return self.light
if rpc_stats is None: if rpc_estats is None:
try: try:
rpc_stats = await self.rpc.stats() rpc_estats = await self.rpc.estats()
except APIError: except APIError:
pass pass
if rpc_stats is not None: if rpc_estats is not None:
try: try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"] parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats) led = int(parsed_estats["Led"])
led = int(parsed_stats["Led"][0])
return True if led == 1 else False return True if led == 1 else False
except (IndexError, KeyError, ValueError, TypeError): except (IndexError, KeyError, ValueError, TypeError):
pass pass
try:
data = await self.rpc.ascset(0, "led", "1-255")
except APIError:
return False
try:
if data["STATUS"][0]["Msg"] == "ASC 0 set info: LED[1]":
return True
except LookupError:
pass
return False return False

View File

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

View File

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

View File

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

View File

@@ -13,22 +13,50 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import logging import logging
from pathlib import Path
from typing import List, Optional
import aiofiles import aiofiles
import semver
from pyasic.config import MinerConfig, MiningModeConfig from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import Fan, HashBoard from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, WhatsminerError from pyasic.data.error_codes import MinerErrorData, WhatsminerError
from pyasic.data.pools import PoolMetrics, PoolUrl from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.device.algorithm import AlgoHashRate from pyasic.device.algorithm import AlgoHashRateType
from pyasic.errors import APIError from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware from pyasic.miners.device.firmware import StockFirmware
from pyasic.rpc.btminer import BTMinerRPCAPI from pyasic.rpc.btminer import BTMinerRPCAPI, BTMinerV3RPCAPI
class BTMiner(StockFirmware):
def __new__(cls, ip: str, version: str | None = None):
bases = cls.__bases__
bases = bases[1:]
def get_new(v: str | None):
if v is None:
return BTMinerV2
try:
semantic = semver.Version(
major=int(v[0:4]),
minor=int(v[4:6]),
patch=int(v[6:8]),
)
except ValueError:
return BTMinerV2
if semantic > semver.Version(major=2024, minor=11, patch=0):
return BTMinerV3
return BTMinerV2
inject = get_new(version)
bases = (inject,) + bases
cls = type(cls.__name__, bases, {})(ip=ip, version=version)
return cls
BTMINER_DATA_LOC = DataLocations( BTMINER_DATA_LOC = DataLocations(
**{ **{
@@ -119,7 +147,7 @@ BTMINER_DATA_LOC = DataLocations(
) )
class BTMiner(StockFirmware): class BTMinerV2(StockFirmware):
"""Base handler for BTMiner based miners.""" """Base handler for BTMiner based miners."""
_rpc_cls = BTMinerRPCAPI _rpc_cls = BTMinerRPCAPI
@@ -208,7 +236,9 @@ class BTMiner(StockFirmware):
return True return True
return False return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: async def send_config(
self, config: MinerConfig, user_suffix: str | None = None
) -> None:
self.config = config self.config = config
conf = config.as_wm(user_suffix=user_suffix) conf = config.as_wm(user_suffix=user_suffix)
@@ -279,6 +309,9 @@ class BTMiner(StockFirmware):
self.config = cfg self.config = cfg
return self.config return self.config
cfg.mining_mode = MiningModeConfig.normal()
return cfg
async def set_power_limit(self, wattage: int) -> bool: async def set_power_limit(self, wattage: int) -> bool:
try: try:
await self.rpc.adjust_power_limit(wattage) await self.rpc.adjust_power_limit(wattage)
@@ -287,14 +320,15 @@ class BTMiner(StockFirmware):
return False return False
else: else:
return True return True
return False
################################################## ##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ### ### DATA GATHERING FUNCTIONS (get_{some_data}) ###
################################################## ##################################################
async def _get_mac( async def _get_mac(
self, rpc_summary: dict = None, rpc_get_miner_info: dict = None self, rpc_summary: dict | None = None, rpc_get_miner_info: dict | None = None
) -> Optional[str]: ) -> str | None:
if rpc_get_miner_info is None: if rpc_get_miner_info is None:
try: try:
rpc_get_miner_info = await self.rpc.get_miner_info() rpc_get_miner_info = await self.rpc.get_miner_info()
@@ -321,7 +355,9 @@ class BTMiner(StockFirmware):
except LookupError: except LookupError:
pass pass
async def _get_api_ver(self, rpc_get_version: dict = None) -> Optional[str]: return None
async def _get_api_ver(self, rpc_get_version: dict | None = None) -> str | None:
if rpc_get_version is None: if rpc_get_version is None:
try: try:
rpc_get_version = await self.rpc.get_version() rpc_get_version = await self.rpc.get_version()
@@ -339,14 +375,13 @@ class BTMiner(StockFirmware):
except (KeyError, TypeError): except (KeyError, TypeError):
pass pass
else: else:
self.rpc.rpc_ver = self.api_ver
return self.api_ver return self.api_ver
return self.api_ver return self.api_ver
async def _get_fw_ver( async def _get_fw_ver(
self, rpc_get_version: dict = None, rpc_summary: dict = None self, rpc_get_version: dict | None = None, rpc_summary: dict | None = None
) -> Optional[str]: ) -> str | None:
if rpc_get_version is None: if rpc_get_version is None:
try: try:
rpc_get_version = await self.rpc.get_version() rpc_get_version = await self.rpc.get_version()
@@ -379,7 +414,7 @@ class BTMiner(StockFirmware):
return self.fw_ver return self.fw_ver
async def _get_hostname(self, rpc_get_miner_info: dict = None) -> Optional[str]: async def _get_hostname(self, rpc_get_miner_info: dict | None = None) -> str | None:
hostname = None hostname = None
if rpc_get_miner_info is None: if rpc_get_miner_info is None:
try: try:
@@ -395,7 +430,9 @@ class BTMiner(StockFirmware):
return hostname return hostname
async def _get_hashrate(self, rpc_summary: dict = None) -> Optional[AlgoHashRate]: async def _get_hashrate(
self, rpc_summary: dict | None = None
) -> AlgoHashRateType | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -406,12 +443,13 @@ class BTMiner(StockFirmware):
try: try:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]), rate=float(rpc_summary["SUMMARY"][0]["MHS 1m"]),
unit=self.algo.unit.MH, unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(self.algo.unit.default) ).into(self.algo.unit.default) # type: ignore[attr-defined]
except LookupError: except LookupError:
pass pass
return None
async def _get_hashboards(self, rpc_devs: dict = None) -> List[HashBoard]: async def _get_hashboards(self, rpc_devs: dict | None = None) -> list[HashBoard]:
if self.expected_hashboards is None: if self.expected_hashboards is None:
return [] return []
@@ -440,8 +478,11 @@ class BTMiner(StockFirmware):
hashboards[asc].chip_temp = round(board["Chip Temp Avg"]) hashboards[asc].chip_temp = round(board["Chip Temp Avg"])
hashboards[asc].temp = round(board["Temperature"]) hashboards[asc].temp = round(board["Temperature"])
hashboards[asc].hashrate = self.algo.hashrate( hashboards[asc].hashrate = self.algo.hashrate(
rate=float(board["MHS 1m"]), unit=self.algo.unit.MH rate=float(board["MHS 1m"]),
).into(self.algo.unit.default) unit=self.algo.unit.MH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
)
hashboards[asc].chips = board["Effective Chips"] hashboards[asc].chips = board["Effective Chips"]
hashboards[asc].serial_number = board["PCB SN"] hashboards[asc].serial_number = board["PCB SN"]
hashboards[asc].missing = False hashboards[asc].missing = False
@@ -450,7 +491,7 @@ class BTMiner(StockFirmware):
return hashboards return hashboards
async def _get_env_temp(self, rpc_summary: dict = None) -> Optional[float]: async def _get_env_temp(self, rpc_summary: dict | None = None) -> float | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -462,8 +503,9 @@ class BTMiner(StockFirmware):
return rpc_summary["SUMMARY"][0]["Env Temp"] return rpc_summary["SUMMARY"][0]["Env Temp"]
except LookupError: except LookupError:
pass pass
return None
async def _get_wattage(self, rpc_summary: dict = None) -> Optional[int]: async def _get_wattage(self, rpc_summary: dict | None = None) -> int | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -476,8 +518,9 @@ class BTMiner(StockFirmware):
return wattage if not wattage == -1 else None return wattage if not wattage == -1 else None
except LookupError: except LookupError:
pass pass
return None
async def _get_wattage_limit(self, rpc_summary: dict = None) -> Optional[int]: async def _get_wattage_limit(self, rpc_summary: dict | None = None) -> int | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -489,10 +532,11 @@ class BTMiner(StockFirmware):
return rpc_summary["SUMMARY"][0]["Power Limit"] return rpc_summary["SUMMARY"][0]["Power Limit"]
except LookupError: except LookupError:
pass pass
return None
async def _get_fans( async def _get_fans(
self, rpc_summary: dict = None, rpc_get_psu: dict = None self, rpc_summary: dict | None = None, rpc_get_psu: dict | None = None
) -> List[Fan]: ) -> list[Fan]:
if self.expected_fans is None: if self.expected_fans is None:
return [] return []
@@ -516,8 +560,8 @@ class BTMiner(StockFirmware):
return fans return fans
async def _get_fan_psu( async def _get_fan_psu(
self, rpc_summary: dict = None, rpc_get_psu: dict = None self, rpc_summary: dict | None = None, rpc_get_psu: dict | None = None
) -> Optional[int]: ) -> int | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -541,10 +585,11 @@ class BTMiner(StockFirmware):
return int(rpc_get_psu["Msg"]["fan_speed"]) return int(rpc_get_psu["Msg"]["fan_speed"])
except (KeyError, TypeError): except (KeyError, TypeError):
pass pass
return None
async def _get_errors( async def _get_errors(
self, rpc_summary: dict = None, rpc_get_error_code: dict = None self, rpc_summary: dict | None = None, rpc_get_error_code: dict | None = None
) -> List[MinerErrorData]: ) -> list[MinerErrorData]:
errors = [] errors = []
if rpc_get_error_code is None and rpc_summary is None: if rpc_get_error_code is None and rpc_summary is None:
try: try:
@@ -577,11 +622,11 @@ class BTMiner(StockFirmware):
errors.append(WhatsminerError(error_code=err)) errors.append(WhatsminerError(error_code=err))
except (LookupError, ValueError, TypeError): except (LookupError, ValueError, TypeError):
pass pass
return errors return errors # type: ignore[return-value]
async def _get_expected_hashrate( async def _get_expected_hashrate(
self, rpc_summary: dict = None self, rpc_summary: dict | None = None
) -> Optional[AlgoHashRate]: ) -> AlgoHashRateType | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -593,13 +638,17 @@ class BTMiner(StockFirmware):
expected_hashrate = rpc_summary["SUMMARY"][0]["Factory GHS"] expected_hashrate = rpc_summary["SUMMARY"][0]["Factory GHS"]
if expected_hashrate: if expected_hashrate:
return self.algo.hashrate( return self.algo.hashrate(
rate=float(expected_hashrate), unit=self.algo.unit.GH rate=float(expected_hashrate),
).into(self.algo.unit.default) unit=self.algo.unit.GH, # type: ignore[attr-defined]
).into(self.algo.unit.default) # type: ignore[attr-defined]
except LookupError: except LookupError:
pass pass
return None
async def _get_fault_light(self, rpc_get_miner_info: dict = None) -> Optional[bool]: async def _get_fault_light(
self, rpc_get_miner_info: dict | None = None
) -> bool | None:
if rpc_get_miner_info is None: if rpc_get_miner_info is None:
try: try:
rpc_get_miner_info = await self.rpc.get_miner_info() rpc_get_miner_info = await self.rpc.get_miner_info()
@@ -621,15 +670,17 @@ class BTMiner(StockFirmware):
dns: str, dns: str,
gateway: str, gateway: str,
subnet_mask: str = "255.255.255.0", subnet_mask: str = "255.255.255.0",
hostname: str = None, hostname: str | None = None,
): ):
if not hostname: if not hostname:
hostname = await self.get_hostname() hostname = await self.get_hostname()
if hostname is None:
hostname = str(self.ip)
await self.rpc.net_config( await self.rpc.net_config(
ip=ip, mask=subnet_mask, dns=dns, gate=gateway, host=hostname, dhcp=False ip=ip, mask=subnet_mask, dns=dns, gate=gateway, host=hostname, dhcp=False
) )
async def set_dhcp(self, hostname: str = None): async def set_dhcp(self, hostname: str | None = None):
if hostname: if hostname:
await self.set_hostname(hostname) await self.set_hostname(hostname)
await self.rpc.net_config() await self.rpc.net_config()
@@ -637,7 +688,7 @@ class BTMiner(StockFirmware):
async def set_hostname(self, hostname: str): async def set_hostname(self, hostname: str):
await self.rpc.set_hostname(hostname) await self.rpc.set_hostname(hostname)
async def _is_mining(self, rpc_status: dict = None) -> Optional[bool]: async def _is_mining(self, rpc_status: dict | None = None) -> bool | None:
if rpc_status is None: if rpc_status is None:
try: try:
rpc_status = await self.rpc.status() rpc_status = await self.rpc.status()
@@ -655,8 +706,9 @@ class BTMiner(StockFirmware):
return True if rpc_status["Msg"]["mineroff"] == "false" else False return True if rpc_status["Msg"]["mineroff"] == "false" else False
except LookupError: except LookupError:
pass pass
return False
async def _get_uptime(self, rpc_summary: dict = None) -> Optional[int]: async def _get_uptime(self, rpc_summary: dict | None = None) -> int | None:
if rpc_summary is None: if rpc_summary is None:
try: try:
rpc_summary = await self.rpc.summary() rpc_summary = await self.rpc.summary()
@@ -668,8 +720,9 @@ class BTMiner(StockFirmware):
return int(rpc_summary["SUMMARY"][0]["Elapsed"]) return int(rpc_summary["SUMMARY"][0]["Elapsed"])
except LookupError: except LookupError:
pass pass
return None
async def _get_pools(self, rpc_pools: dict = None) -> List[PoolMetrics]: async def _get_pools(self, rpc_pools: dict | None = None) -> list[PoolMetrics]:
if rpc_pools is None: if rpc_pools is None:
try: try:
rpc_pools = await self.rpc.pools() rpc_pools = await self.rpc.pools()
@@ -699,15 +752,25 @@ class BTMiner(StockFirmware):
pass pass
return pools_data return pools_data
async def upgrade_firmware(self, file: Path) -> str: async def upgrade_firmware(
self,
*,
file: str | None = None,
url: str | None = None,
version: str | None = None,
keep_settings: bool = True,
) -> bool:
""" """
Upgrade the firmware of the Whatsminer device. Upgrade the firmware of the Whatsminer device.
Args: Args:
file (Path): The local file path of the firmware to be uploaded. file: The local file path of the firmware to be uploaded.
url: URL to download firmware from (not supported).
version: Specific version to upgrade to (not supported).
keep_settings: Whether to keep settings after upgrade.
Returns: Returns:
str: Confirmation message after upgrading the firmware. bool: True if firmware upgrade was successful.
""" """
try: try:
logging.info("Starting firmware upgrade process for Whatsminer.") logging.info("Starting firmware upgrade process for Whatsminer.")
@@ -719,12 +782,12 @@ class BTMiner(StockFirmware):
async with aiofiles.open(file, "rb") as f: async with aiofiles.open(file, "rb") as f:
upgrade_contents = await f.read() upgrade_contents = await f.read()
result = await self.rpc.update_firmware(upgrade_contents) await self.rpc.update_firmware(upgrade_contents)
logging.info( logging.info(
"Firmware upgrade process completed successfully for Whatsminer." "Firmware upgrade process completed successfully for Whatsminer."
) )
return result return True
except FileNotFoundError as e: except FileNotFoundError as e:
logging.error(f"File not found during the firmware upgrade process: {e}") logging.error(f"File not found during the firmware upgrade process: {e}")
raise raise
@@ -742,3 +805,462 @@ class BTMiner(StockFirmware):
exc_info=True, exc_info=True,
) )
raise raise
BTMINERV3_DATA_LOC = DataLocations(
**{
str(DataOptions.MAC): DataFunction(
"_get_mac", [RPCAPICommand("rpc_get_device_info", "get.device.info")]
),
str(DataOptions.API_VERSION): DataFunction(
"_get_api_version",
[RPCAPICommand("rpc_get_device_info", "get.device.info")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_firmware_version",
[RPCAPICommand("rpc_get_device_info", "get.device.info")],
),
str(DataOptions.HOSTNAME): DataFunction(
"_get_hostname", [RPCAPICommand("rpc_get_device_info", "get.device.info")]
),
str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_light_flashing",
[RPCAPICommand("rpc_get_device_info", "get.device.info")],
),
str(DataOptions.WATTAGE_LIMIT): DataFunction(
"_get_wattage_limit",
[RPCAPICommand("rpc_get_device_info", "get.device.info")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
),
str(DataOptions.FAN_PSU): DataFunction(
"_get_psu_fans", [RPCAPICommand("rpc_get_device_info", "get.device.info")]
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[
RPCAPICommand("rpc_get_device_info", "get.device.info"),
RPCAPICommand(
"rpc_get_miner_status_edevs",
"get.miner.status:edevs",
),
],
),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
),
str(DataOptions.ENVIRONMENT_TEMP): DataFunction(
"_get_env_temp",
[RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")],
),
}
)
class BTMinerV3(StockFirmware):
_rpc_cls = BTMinerV3RPCAPI
rpc: BTMinerV3RPCAPI
data_locations = BTMINERV3_DATA_LOC
supports_shutdown = True
supports_autotuning = True
supports_power_modes = True
async def get_config(self) -> MinerConfig:
pools = None
settings = None
device_info = None
try:
pools = await self.rpc.get_miner_status_pools()
settings = await self.rpc.get_miner_setting()
device_info = await self.rpc.get_device_info()
except APIError as e:
logging.warning(e)
except LookupError:
pass
if pools is not None and settings is not None and device_info is not None:
self.config = MinerConfig.from_btminer_v3(
rpc_pools=pools, rpc_settings=settings, rpc_device_info=device_info
)
else:
self.config = MinerConfig()
return self.config
async def send_config(
self, config: MinerConfig, user_suffix: str | None = None
) -> None:
self.config = config
conf = config.as_btminer_v3(user_suffix=user_suffix)
await asyncio.gather(
*[self.rpc.send_command(k, parameters=v) for k, v in conf.values()]
)
async def fault_light_off(self) -> bool:
try:
data = await self.rpc.set_system_led()
except APIError:
return False
if data:
if "code" in data.keys():
if data["code"] == 0:
self.light = False
return True
return False
async def fault_light_on(self) -> bool:
try:
data = await self.rpc.set_system_led(
leds=[{"color": "red", "period": 60, "duration": 20, "start": 0}]
)
except APIError:
return False
if data:
if "code" in data.keys():
if data["code"] == 0:
self.light = True
return True
return False
async def reboot(self) -> bool:
try:
data = await self.rpc.set_system_reboot()
except APIError:
return False
if data and data.get("msg"):
if data["msg"] == "ok":
return True
return False
async def restart_backend(self) -> bool:
try:
data = await self.rpc.set_miner_service("restart")
except APIError:
return False
if data and data.get("msg"):
if data["msg"] == "ok":
return True
return False
async def stop_mining(self) -> bool:
try:
data = await self.rpc.set_miner_service("stop")
except APIError:
return False
if data and data.get("msg"):
if data["msg"] == "ok":
return True
return False
async def resume_mining(self) -> bool:
try:
data = await self.rpc.set_miner_service("start")
except APIError:
return False
if data and data.get("msg"):
if data["msg"] == "ok":
return True
return False
async def set_power_limit(self, wattage: int) -> bool:
try:
await self.rpc.set_miner_power_limit(wattage)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
return False
else:
return True
async def _get_mac(self, rpc_get_device_info: dict | None = None) -> str | None:
if rpc_get_device_info is None:
try:
rpc_get_device_info = await self.rpc.get_device_info()
except APIError:
return None
if rpc_get_device_info is None:
return None
return rpc_get_device_info.get("msg", {}).get("network", {}).get("mac")
async def _get_api_version(
self, rpc_get_device_info: dict | None = None
) -> str | None:
if rpc_get_device_info is None:
try:
rpc_get_device_info = await self.rpc.get_device_info()
except APIError:
return None
if rpc_get_device_info is None:
return None
return rpc_get_device_info.get("msg", {}).get("system", {}).get("api")
async def _get_firmware_version(
self, rpc_get_device_info: dict | None = None
) -> str | None:
if rpc_get_device_info is None:
try:
rpc_get_device_info = await self.rpc.get_device_info()
except APIError:
return None
if rpc_get_device_info is None:
return None
return rpc_get_device_info.get("msg", {}).get("system", {}).get("fwversion")
async def _get_hostname(
self, rpc_get_device_info: dict | None = None
) -> str | None:
if rpc_get_device_info is None:
try:
rpc_get_device_info = await self.rpc.get_device_info()
except APIError:
return None
if rpc_get_device_info is None:
return None
return rpc_get_device_info.get("msg", {}).get("network", {}).get("hostname")
async def _get_light_flashing(
self, rpc_get_device_info: dict | None = None
) -> bool | None:
if rpc_get_device_info is None:
try:
rpc_get_device_info = await self.rpc.get_device_info()
except APIError:
return None
if rpc_get_device_info is None:
return None
val = rpc_get_device_info.get("msg", {}).get("system", {}).get("ledstatus")
if isinstance(val, str):
return val != "auto"
return None
async def _get_wattage_limit(
self, rpc_get_device_info: dict | None = None
) -> int | None:
if rpc_get_device_info is None:
try:
rpc_get_device_info = await self.rpc.get_device_info()
except APIError:
return None
if rpc_get_device_info is None:
return None
val = rpc_get_device_info.get("msg", {}).get("miner", {}).get("power-limit-set")
try:
return int(float(val))
except (ValueError, TypeError):
return None
async def _get_fans(
self, rpc_get_miner_status_summary: dict | None = None
) -> list[Fan]:
if rpc_get_miner_status_summary is None:
try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError:
return []
fans = []
if rpc_get_miner_status_summary is None:
return []
summary = rpc_get_miner_status_summary.get("msg", {}).get("summary", {})
for idx, direction in enumerate(["in", "out"]):
rpm = summary.get(f"fan-speed-{direction}")
if rpm is not None:
fans.append(Fan(speed=rpm))
return fans
async def _get_psu_fans(self, rpc_get_device_info: dict | None = None) -> list[Fan]:
if rpc_get_device_info is None:
try:
rpc_get_device_info = await self.rpc.get_device_info()
except APIError:
return []
if rpc_get_device_info is None:
return []
rpm = rpc_get_device_info.get("msg", {}).get("power", {}).get("fanspeed")
return [Fan(speed=rpm)] if rpm is not None else []
async def _get_hashboards(
self,
rpc_get_device_info: dict | None = None,
rpc_get_miner_status_edevs: dict | None = None,
) -> list[HashBoard]:
if rpc_get_device_info is None:
try:
rpc_get_device_info = await self.rpc.get_device_info()
except APIError:
return []
if rpc_get_miner_status_edevs is None:
try:
rpc_get_miner_status_edevs = await self.rpc.get_miner_status_edevs()
except APIError:
return []
boards = []
if rpc_get_device_info is None or rpc_get_miner_status_edevs is None:
return []
board_count = (
rpc_get_device_info.get("msg", {}).get("hardware", {}).get("boards", 3)
)
edevs = rpc_get_miner_status_edevs.get("msg", {}).get("edevs", [])
for idx in range(board_count):
board_data = edevs[idx] if idx < len(edevs) else {}
boards.append(
HashBoard(
slot=idx,
hashrate=self.algo.hashrate(
rate=board_data.get("hash-average", 0),
unit=self.algo.unit.TH, # type: ignore[attr-defined]
).into(
self.algo.unit.default # type: ignore[attr-defined]
),
temp=board_data.get("chip-temp-min"),
inlet_temp=board_data.get("chip-temp-min"),
outlet_temp=board_data.get("chip-temp-max"),
serial_number=rpc_get_device_info.get("msg", {})
.get("miner", {})
.get(f"pcbsn{idx}"),
chips=board_data.get("effective-chips"),
expected_chips=self.expected_chips,
active=(board_data.get("hash-average") or 0) > 0,
missing=False,
tuned=True,
)
)
return boards
async def _get_pools(
self, rpc_get_miner_status_summary: dict | None = None
) -> list[PoolMetrics]:
if rpc_get_miner_status_summary is None:
try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError:
return []
pools = []
if rpc_get_miner_status_summary is None:
return []
msg_pools = rpc_get_miner_status_summary.get("msg", {}).get("pools", [])
for idx, pool in enumerate(msg_pools):
pools.append(
PoolMetrics(
index=idx,
user=pool.get("account"),
alive=pool.get("status") == "alive",
active=pool.get("stratum-active"),
url=pool.get("url"),
)
)
return pools
async def _get_uptime(
self, rpc_get_miner_status_summary: dict | None = None
) -> int | None:
if rpc_get_miner_status_summary is None:
try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError:
return None
if rpc_get_miner_status_summary is None:
return None
return (
rpc_get_miner_status_summary.get("msg", {})
.get("summary", {})
.get("elapsed")
)
async def _get_wattage(
self, rpc_get_miner_status_summary: dict | None = None
) -> int | None:
if rpc_get_miner_status_summary is None:
try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError:
return None
if rpc_get_miner_status_summary is None:
return None
power_val = (
rpc_get_miner_status_summary.get("msg", {})
.get("summary", {})
.get("power-realtime")
)
try:
return int(float(power_val)) if power_val is not None else None
except (ValueError, TypeError):
return None
async def _get_hashrate(
self, rpc_get_miner_status_summary: dict | None = None
) -> AlgoHashRateType | None:
if rpc_get_miner_status_summary is None:
try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError:
return None
if rpc_get_miner_status_summary is None:
return None
return (
rpc_get_miner_status_summary.get("msg", {})
.get("summary", {})
.get("hash-realtime")
)
async def _get_expected_hashrate(
self, rpc_get_miner_status_summary: dict | None = None
) -> AlgoHashRateType | None:
if rpc_get_miner_status_summary is None:
try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError:
return None
if rpc_get_miner_status_summary is None:
return None
res = (
rpc_get_miner_status_summary.get("msg", {})
.get("summary", {})
.get("factory-hash")
)
if self.expected_hashboards is not None and res == (
-0.001 * self.expected_hashboards
):
return None
return res
async def _get_env_temp(
self, rpc_get_miner_status_summary: dict | None = None
) -> float | None:
if rpc_get_miner_status_summary is None:
try:
rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary()
except APIError:
return None
if rpc_get_miner_status_summary is None:
return None
return (
rpc_get_miner_status_summary.get("msg", {})
.get("summary", {})
.get("environment-temperature")
)

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More