Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1587f65196 | ||
|
|
5ff10c0cdd | ||
|
|
aa0a028564 | ||
|
|
82f6d2f274 | ||
|
|
833de3ab43 | ||
|
|
08d273c7c1 | ||
|
|
aac7598187 | ||
|
|
5c0ac4e665 | ||
|
|
7bd5e49412 | ||
|
|
53ff3c5f79 | ||
|
|
36ae6e5272 | ||
|
|
fb3dffb216 | ||
|
|
ff6a6d2ec6 | ||
|
|
f4bbc2c3e5 | ||
|
|
4dbfdbe29c | ||
|
|
5e2a18f91e | ||
|
|
3363bdc592 | ||
|
|
08180a2d59 | ||
|
|
1a64ff4038 | ||
|
|
8ad90a6abb | ||
|
|
8cdd5ff015 | ||
|
|
a08f434e1f | ||
|
|
9acd6d2fea | ||
|
|
0a1cdea2e3 | ||
|
|
73b1a0493c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ pyvenv.cfg
|
||||
.env/
|
||||
bin/
|
||||
lib/
|
||||
.idea/
|
||||
|
||||
@@ -240,6 +240,20 @@ Pyasic implements a few dataclasses as helpers to make data return types consist
|
||||
|
||||
You can call [`MinerData.asdict()`][pyasic.data.MinerData.asdict] to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
|
||||
|
||||
[`MinerData`][pyasic.data.MinerData] instances can also be added to each other to combine their data and can be divided by a number to divide all their data, allowing you to get average data from many miners by doing -
|
||||
```python
|
||||
from pyasic import MinerData
|
||||
|
||||
# examples of miner data
|
||||
d1 = MinerData("192.168.1.1")
|
||||
d2 = MinerData("192.168.1.2")
|
||||
|
||||
list_of_miner_data = [d1, d2]
|
||||
|
||||
average_data = sum(list_of_miner_data, start=MinerData("0.0.0.0"))/len(list_of_miner_data)
|
||||
```
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
### [`MinerConfig`][pyasic.config.MinerConfig]
|
||||
|
||||
@@ -17,31 +17,31 @@ details {
|
||||
<details>
|
||||
<summary>X19 Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/antminer/X19#s19-bos">S19</a></li>
|
||||
<li><a href="/miners/antminer/X19#s19-pro-bos">S19 Pro</a></li>
|
||||
<li><a href="/miners/antminer/X19#s19j-bos">S19j</a></li>
|
||||
<li><a href="/miners/antminer/X19#s19j-pro-bos">S19j Pro</a></li>
|
||||
<li><a href="/miners/antminer/X19#t19-bos">T19</a></li>
|
||||
<li><a href="../antminer/X19#s19-bos">S19</a></li>
|
||||
<li><a href="../antminer/X19#s19-pro-bos">S19 Pro</a></li>
|
||||
<li><a href="../antminer/X19#s19j-bos">S19j</a></li>
|
||||
<li><a href="../antminer/X19#s19j-pro-bos">S19j Pro</a></li>
|
||||
<li><a href="../antminer/X19#t19-bos">T19</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>X17 Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/antminer/X17#s17-bos">S17</a></li>
|
||||
<li><a href="/miners/antminer/X17#s17-plus-bos">S17+</a></li>
|
||||
<li><a href="/miners/antminer/X17#s17-pro-bos">S17 Pro</a></li>
|
||||
<li><a href="/miners/antminer/X17#s17e-bos">S17e</a></li>
|
||||
<li><a href="/miners/antminer/X17#t17-bos">T17</a></li>
|
||||
<li><a href="/miners/antminer/X17#t17-plus-bos">T17+</a></li>
|
||||
<li><a href="/miners/antminer/X17#t17e-bos">T17e</a></li>
|
||||
<li><a href="../antminer/X17#s17-bos">S17</a></li>
|
||||
<li><a href="../antminer/X17#s17-plus-bos">S17+</a></li>
|
||||
<li><a href="../antminer/X17#s17-pro-bos">S17 Pro</a></li>
|
||||
<li><a href="../antminer/X17#s17e-bos">S17e</a></li>
|
||||
<li><a href="../antminer/X17#t17-bos">T17</a></li>
|
||||
<li><a href="../antminer/X17#t17-plus-bos">T17+</a></li>
|
||||
<li><a href="../antminer/X17#t17e-bos">T17e</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>X9 Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/antminer/X9#s9-bos">S9</a></li>
|
||||
<li><a href="/miners/antminer/X9#s9-bos">S9i</a></li>
|
||||
<li><a href="/miners/antminer/X9#s9-bos">S9j</a></li>
|
||||
<li><a href="../antminer/X9#s9-bos">S9</a></li>
|
||||
<li><a href="../antminer/X9#s9-bos">S9i</a></li>
|
||||
<li><a href="../antminer/X9#s9-bos">S9j</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
@@ -50,55 +50,64 @@ details {
|
||||
<summary>Stock Firmware Whatsminers:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary>M5X Series:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary><a href="../whatsminer/M5X/#m50">M50</a></summary>
|
||||
<ul>
|
||||
<li><a href="../whatsminer/M5X/#m50vh50">VH50</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
<summary>M3X Series:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m30s">M30S</a></summary>
|
||||
<summary><a href="../whatsminer/M3X/#m30s">M30S</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30sve10">VE10</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30svg20">VG20</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30sve20">VE20</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30sv50">V50</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30sve10">VE10</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30svg20">VG20</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30sve20">VE20</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30sv50">V50</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m30s_1">M30S+</a></summary>
|
||||
<summary><a href="../whatsminer/M3X/#m30s_1">M30S+</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30svf20">VF20</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30sve40">VE40</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30svg60">VG60</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30svf20">VF20</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30sve40">VE40</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30svg60">VG60</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m30s_2">M30S++</a></summary>
|
||||
<summary><a href="../whatsminer/M3X/#m30s_2">M30S++</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30svg30">VG30</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30svg40">VG40</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m30svh60">VH60</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30svg30">VG30</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30svg40">VG40</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m30svh60">VH60</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m31s">M31S</a></summary>
|
||||
<summary><a href="../whatsminer/M3X/#m31s">M31S</a></summary>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m31s_1">M31S+</a></summary>
|
||||
<summary><a href="../whatsminer/M3X/#m31s_1">M31S+</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M3X/#m31sve20">VE20</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m31sv30">V30</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m31sv40">V40</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m31sv60">V60</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m31sv80">V80</a></li>
|
||||
<li><a href="/miners/whatsminer/M3X/#m31sv90">V90</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m31sve20">VE20</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m31sv30">V30</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m31sv40">V40</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m31sv60">V60</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m31sv80">V80</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m31sv90">V90</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m32">M32</a></summary>
|
||||
<summary><a href="../whatsminer/M3X/#m32">M32</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M3X/#m32v20">V20</a></li>
|
||||
<li><a href="../whatsminer/M3X/#m32v20">V20</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m32s">M32S</a></summary>
|
||||
<summary><a href="../whatsminer/M3X/#m32s">M32S</a></summary>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
@@ -106,33 +115,33 @@ details {
|
||||
<summary>M2X Series:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m20">M20</a></summary>
|
||||
<summary><a href="../whatsminer/M2X/#m20">M20</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M2X/#m20v10">V10</a></li>
|
||||
<li><a href="../whatsminer/M2X/#m20v10">V10</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m20s">M20S</a></summary>
|
||||
<summary><a href="../whatsminer/M2X/#m20s">M20S</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M2X/#m20sv10">V10</a></li>
|
||||
<li><a href="/miners/whatsminer/M2X/#m20sv20">V20</a></li>
|
||||
<li><a href="../whatsminer/M2X/#m20sv10">V10</a></li>
|
||||
<li><a href="../whatsminer/M2X/#m20sv20">V20</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m20s_1">M20S+</a></summary>
|
||||
<summary><a href="../whatsminer/M2X/#m20s_1">M20S+</a></summary>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m21">M21</a></summary>
|
||||
<summary><a href="../whatsminer/M2X/#m21">M21</a></summary>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m21s">M21S</a></summary>
|
||||
<summary><a href="../whatsminer/M2X/#m21s">M21S</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M2X/#m21sv20">V20</a></li>
|
||||
<li><a href="/miners/whatsminer/M2X/#m21sv60">V60</a></li>
|
||||
<li><a href="../whatsminer/M2X/#m21sv20">V20</a></li>
|
||||
<li><a href="../whatsminer/M2X/#m21sv60">V60</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m21s_1">M21S+</a></summary>
|
||||
<summary><a href="../whatsminer/M2X/#m21s_1">M21S+</a></summary>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
@@ -144,33 +153,33 @@ details {
|
||||
<details>
|
||||
<summary>X19 Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/antminer/X19/#s19">S19</a></li>
|
||||
<li><a href="/miners/antminer/X19/#s19-pro">S19 Pro</a></li>
|
||||
<li><a href="/miners/antminer/X19/#s19a">S19a</a></li>
|
||||
<li><a href="/miners/antminer/X19/#s19j">S19j</a></li>
|
||||
<li><a href="/miners/antminer/X19/#s19j-pro">S19j Pro</a></li>
|
||||
<li><a href="/miners/antminer/X19/#s19-xp">S19 XP</a></li>
|
||||
<li><a href="/miners/antminer/X19/#t19">T19</a></li>
|
||||
<li><a href="../antminer/X19/#s19">S19</a></li>
|
||||
<li><a href="../antminer/X19/#s19-pro">S19 Pro</a></li>
|
||||
<li><a href="../antminer/X19/#s19a">S19a</a></li>
|
||||
<li><a href="../antminer/X19/#s19j">S19j</a></li>
|
||||
<li><a href="../antminer/X19/#s19j-pro">S19j Pro</a></li>
|
||||
<li><a href="../antminer/X19/#s19-xp">S19 XP</a></li>
|
||||
<li><a href="../antminer/X19/#t19">T19</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>X17 Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/antminer/X17/#s17">S17</a></li>
|
||||
<li><a href="/miners/antminer/X17/#s17_1">S17+</a></li>
|
||||
<li><a href="/miners/antminer/X17/#s17-pro">S17 Pro</a></li>
|
||||
<li><a href="/miners/antminer/X17/#s17e">S17e</a></li>
|
||||
<li><a href="/miners/antminer/X17/#t17">T17</a></li>
|
||||
<li><a href="/miners/antminer/X17/#t17_1">T17+</a></li>
|
||||
<li><a href="/miners/antminer/X17/#t17e">T17e</a></li>
|
||||
<li><a href="../antminer/X17/#s17">S17</a></li>
|
||||
<li><a href="../antminer/X17/#s17_1">S17+</a></li>
|
||||
<li><a href="../antminer/X17/#s17-pro">S17 Pro</a></li>
|
||||
<li><a href="../antminer/X17/#s17e">S17e</a></li>
|
||||
<li><a href="../antminer/X17/#t17">T17</a></li>
|
||||
<li><a href="../antminer/X17/#t17_1">T17+</a></li>
|
||||
<li><a href="../antminer/X17/#t17e">T17e</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>X9 Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/antminer/X9/#s9">S9</a></li>
|
||||
<li><a href="/miners/antminer/X9/#s9i">S9i</a></li>
|
||||
<li><a href="/miners/antminer/X9/#t9">T9</a></li>
|
||||
<li><a href="../antminer/X9/#s9">S9</a></li>
|
||||
<li><a href="../antminer/X9/#s9i">S9i</a></li>
|
||||
<li><a href="../antminer/X9/#t9">T9</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
@@ -181,31 +190,31 @@ details {
|
||||
<details>
|
||||
<summary>A7X Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/avalonminer/A7X/#a721">A721</a></li>
|
||||
<li><a href="/miners/avalonminer/A7X/#a741">A741</a></li>
|
||||
<li><a href="/miners/avalonminer/A7X/#a761">A761</a></li>
|
||||
<li><a href="../avalonminer/A7X/#a721">A721</a></li>
|
||||
<li><a href="../avalonminer/A7X/#a741">A741</a></li>
|
||||
<li><a href="../avalonminer/A7X/#a761">A761</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>A8X Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/avalonminer/A8X/#a821">A821</a></li>
|
||||
<li><a href="/miners/avalonminer/A8X/#a841">A841</a></li>
|
||||
<li><a href="/miners/avalonminer/A8X/#a851">A851</a></li>
|
||||
<li><a href="../avalonminer/A8X/#a821">A821</a></li>
|
||||
<li><a href="../avalonminer/A8X/#a841">A841</a></li>
|
||||
<li><a href="../avalonminer/A8X/#a851">A851</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>A9X Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/avalonminer/A9X/#a921">A921</a></li>
|
||||
<li><a href="../avalonminer/A9X/#a921">A921</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>A10X Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/avalonminer/A10X/#a1026">A1026</a></li>
|
||||
<li><a href="/miners/avalonminer/A10X/#a1047">A1047</a></li>
|
||||
<li><a href="/miners/avalonminer/A10X/#a1066">A1066</a></li>
|
||||
<li><a href="../avalonminer/A10X/#a1026">A1026</a></li>
|
||||
<li><a href="../avalonminer/A10X/#a1047">A1047</a></li>
|
||||
<li><a href="../avalonminer/A10X/#a1066">A1066</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
@@ -216,7 +225,7 @@ details {
|
||||
<details>
|
||||
<summary>T3X Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/innosilicon/T3X/#t3h">T3H+</a></li>
|
||||
<li><a href="../innosilicon/T3X/#t3h">T3H+</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
|
||||
18
docs/miners/whatsminer/M5X.md
Normal file
18
docs/miners/whatsminer/M5X.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# pyasic
|
||||
## M5X Models
|
||||
|
||||
## M50
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M50VH50
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M5X.M50.BTMinerM50VH50
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
@@ -21,6 +21,7 @@ nav:
|
||||
- Avalon 10X: "miners/avalonminer/A10X.md"
|
||||
- Whatsminer M2X: "miners/whatsminer/M2X.md"
|
||||
- Whatsminer M3X: "miners/whatsminer/M3X.md"
|
||||
- Whatsminer M5X: "miners/whatsminer/M5X.md"
|
||||
- Innosilicon T3X: "miners/innosilicon/T3X.md"
|
||||
- Network:
|
||||
- Miner Network: "network/miner_network.md"
|
||||
|
||||
14
poetry.lock
generated
14
poetry.lock
generated
@@ -425,7 +425,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "9.5"
|
||||
version = "9.6"
|
||||
description = "Extension pack for Python Markdown."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -491,14 +491,14 @@ idna2008 = ["idna"]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "65.4.0"
|
||||
version = "65.4.1"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
@@ -837,8 +837,8 @@ pycparser = [
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
]
|
||||
pymdown-extensions = [
|
||||
{file = "pymdown_extensions-9.5-py3-none-any.whl", hash = "sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4"},
|
||||
{file = "pymdown_extensions-9.5.tar.gz", hash = "sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0"},
|
||||
{file = "pymdown_extensions-9.6-py3-none-any.whl", hash = "sha256:1e36490adc7bfcef1fdb21bb0306e93af99cff8ec2db199bd17e3bf009768c11"},
|
||||
{file = "pymdown_extensions-9.6.tar.gz", hash = "sha256:b956b806439bbff10f726103a941266beb03fbe99f897c7d5e774d7170339ad9"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
@@ -899,8 +899,8 @@ rfc3986 = [
|
||||
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
|
||||
]
|
||||
setuptools = [
|
||||
{file = "setuptools-65.4.0-py3-none-any.whl", hash = "sha256:c2d2709550f15aab6c9110196ea312f468f41cd546bceb24127a1be6fdcaeeb1"},
|
||||
{file = "setuptools-65.4.0.tar.gz", hash = "sha256:a8f6e213b4b0661f590ccf40de95d28a177cd747d098624ad3f69c40287297e9"},
|
||||
{file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"},
|
||||
{file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
|
||||
@@ -18,6 +18,7 @@ import ipaddress
|
||||
import warnings
|
||||
import logging
|
||||
from typing import Union
|
||||
import re
|
||||
|
||||
from pyasic.errors import APIError, APIWarning
|
||||
|
||||
@@ -46,8 +47,8 @@ class BaseMinerAPI:
|
||||
# each function in self
|
||||
dir(self)
|
||||
if callable(getattr(self, func)) and
|
||||
# no __ methods
|
||||
not func.startswith("__") and
|
||||
# no __ or _ methods
|
||||
not func.startswith("__") and not func.startswith("_") and
|
||||
# remove all functions that are in this base class
|
||||
func
|
||||
not in [
|
||||
@@ -71,14 +72,11 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
)
|
||||
return return_commands
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_x19_error: bool = False
|
||||
) -> dict:
|
||||
async def multicommand(self, *commands: str) -> dict:
|
||||
"""Creates and sends multiple commands as one command to the miner.
|
||||
|
||||
Parameters:
|
||||
*commands: The commands to send as a multicommand to the miner.
|
||||
ignore_x19_error: Whether or not to ignore errors raised by x19 miners when using the "+" delimited style.
|
||||
"""
|
||||
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
|
||||
# make sure we can actually run each command, otherwise they will fail
|
||||
@@ -87,26 +85,44 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
# doesnt work for S19 which uses the backup _x19_multicommand
|
||||
command = "+".join(commands)
|
||||
try:
|
||||
data = await self.send_command(command, x19_command=ignore_x19_error)
|
||||
data = await self.send_command(command)
|
||||
except APIError:
|
||||
logging.debug(f"{self.ip}: Handling X19 multicommand.")
|
||||
data = await self._x19_multicommand(*command.split("+"))
|
||||
return {}
|
||||
logging.debug(f"{self.ip}: Received multicommand data.")
|
||||
return data
|
||||
|
||||
async def _x19_multicommand(self, *commands):
|
||||
data = None
|
||||
async def _send_bytes(self, data: bytes) -> bytes:
|
||||
try:
|
||||
data = {}
|
||||
# send all commands individually
|
||||
for cmd in commands:
|
||||
data[cmd] = []
|
||||
data[cmd].append(await self.send_command(cmd, x19_command=True))
|
||||
except APIError as e:
|
||||
raise APIError(e)
|
||||
# get reader and writer streams
|
||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
||||
# handle OSError 121
|
||||
except OSError as e:
|
||||
if e.winerror == "121":
|
||||
logging.warning("Semaphore Timeout has Expired.")
|
||||
return b"{}"
|
||||
|
||||
# send the command
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
|
||||
# instantiate data
|
||||
ret_data = b""
|
||||
|
||||
# loop to receive all the data
|
||||
try:
|
||||
while True:
|
||||
d = await reader.read(4096)
|
||||
if not d:
|
||||
break
|
||||
ret_data += d
|
||||
except Exception as e:
|
||||
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
|
||||
return data
|
||||
logging.warning(f"{self.ip}: API Command Error: - {e}")
|
||||
|
||||
# close the connection
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
return ret_data
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
@@ -126,54 +142,32 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
Returns:
|
||||
The return data from the API command parsed from JSON into a dict.
|
||||
"""
|
||||
try:
|
||||
# get reader and writer streams
|
||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
||||
# handle OSError 121
|
||||
except OSError as e:
|
||||
if e.winerror == "121":
|
||||
logging.warning("Semaphore Timeout has Expired.")
|
||||
return {}
|
||||
|
||||
# create the command
|
||||
cmd = {"command": command}
|
||||
if parameters:
|
||||
cmd["parameter"] = parameters
|
||||
|
||||
# send the command
|
||||
writer.write(json.dumps(cmd).encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
# instantiate data
|
||||
data = b""
|
||||
|
||||
# loop to receive all the data
|
||||
try:
|
||||
while True:
|
||||
d = await reader.read(4096)
|
||||
if not d:
|
||||
break
|
||||
data += d
|
||||
except Exception as e:
|
||||
logging.warning(f"{self.ip}: API Command Error: - {e}")
|
||||
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
|
||||
|
||||
data = self._load_api_data(data)
|
||||
|
||||
# close the connection
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
# check for if the user wants to allow errors to return
|
||||
if not ignore_errors:
|
||||
# validate the command succeeded
|
||||
validation = self._validate_command_output(data)
|
||||
if not validation[0]:
|
||||
if not x19_command:
|
||||
logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
|
||||
logging.warning(
|
||||
f"{self.ip}: API Command Error: {command}: {validation[1]}"
|
||||
)
|
||||
raise APIError(validation[1])
|
||||
|
||||
return data
|
||||
|
||||
async def send_privileged_command(self, *args, **kwargs) -> dict:
|
||||
return await self.send_command(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _validate_command_output(data: dict) -> tuple:
|
||||
# check if the data returned is correct or an error
|
||||
@@ -204,31 +198,39 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
@staticmethod
|
||||
def _load_api_data(data: bytes) -> dict:
|
||||
str_data = None
|
||||
# some json from the API returns with a null byte (\x00) on the end
|
||||
if data.endswith(b"\x00"):
|
||||
# handle the null byte
|
||||
str_data = data.decode("utf-8")[:-1]
|
||||
else:
|
||||
# no null byte
|
||||
str_data = data.decode("utf-8")
|
||||
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
||||
str_data = str_data.replace(",}", "}")
|
||||
# fix an error with a btminer return having a newline that breaks json.loads()
|
||||
str_data = str_data.replace("\n", "")
|
||||
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
||||
str_data = str_data.replace("}{", "},{")
|
||||
# fix an error with a bmminer return having a specific comma that breaks json.loads()
|
||||
str_data = str_data.replace("[,{", "[{")
|
||||
# fix an error with Avalonminers returning inf and nan
|
||||
str_data = str_data.replace("inf", "0")
|
||||
str_data = str_data.replace("nan", "0")
|
||||
# fix whatever this garbage from avalonminers is `,"id":1}`
|
||||
if str_data.startswith(","):
|
||||
str_data = f"{{{str_data[1:]}"
|
||||
# try to fix an error with overflowing the receive buffer
|
||||
# this can happen in cases such as bugged btminers returning arbitrary length error info with 100s of errors.
|
||||
if not str_data.endswith("}"):
|
||||
str_data = ",".join(str_data.split(",")[:-1]) + "}"
|
||||
|
||||
# fix a really nasty bug with whatsminer API v2.0.4 where they return a list structured like a dict
|
||||
if re.search(r"\"error_code\":\[\".+\"\]", str_data):
|
||||
str_data = str_data.replace("[", "{").replace("]", "}")
|
||||
|
||||
# parse the json
|
||||
try:
|
||||
# some json from the API returns with a null byte (\x00) on the end
|
||||
if data.endswith(b"\x00"):
|
||||
# handle the null byte
|
||||
str_data = data.decode("utf-8")[:-1]
|
||||
else:
|
||||
# no null byte
|
||||
str_data = data.decode("utf-8")
|
||||
# fix an error with a btminer return having an extra comma that breaks json.loads()
|
||||
str_data = str_data.replace(",}", "}")
|
||||
# fix an error with a btminer return having a newline that breaks json.loads()
|
||||
str_data = str_data.replace("\n", "")
|
||||
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
|
||||
str_data = str_data.replace("}{", "},{")
|
||||
# fix an error with a bmminer return having a specific comma that breaks json.loads()
|
||||
str_data = str_data.replace("[,{", "[{")
|
||||
# fix an error with Avalonminers returning inf and nan
|
||||
str_data = str_data.replace("inf", "0")
|
||||
str_data = str_data.replace("nan", "0")
|
||||
# fix whatever this garbage from avalonminers is `,"id":1}`
|
||||
if str_data.startswith(","):
|
||||
str_data = f"{{{str_data[1:]}"
|
||||
# parse the json
|
||||
parsed_data = json.loads(str_data)
|
||||
# handle bad json
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
raise APIError(f"Decode Error {e}: {str_data}")
|
||||
return parsed_data
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
# 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 logging
|
||||
|
||||
from pyasic.API import BaseMinerAPI
|
||||
from pyasic.API import APIError
|
||||
|
||||
|
||||
class BMMinerAPI(BaseMinerAPI):
|
||||
@@ -36,6 +38,37 @@ class BMMinerAPI(BaseMinerAPI):
|
||||
def __init__(self, ip: str, port: int = 4028) -> None:
|
||||
super().__init__(ip, port)
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_x19_error: bool = False
|
||||
) -> dict:
|
||||
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
|
||||
# make sure we can actually run each command, otherwise they will fail
|
||||
commands = self._check_commands(*commands)
|
||||
# standard multicommand format is "command1+command2"
|
||||
# doesnt work for S19 which uses the backup _x19_multicommand
|
||||
command = "+".join(commands)
|
||||
try:
|
||||
data = await self.send_command(command, x19_command=ignore_x19_error)
|
||||
except APIError:
|
||||
logging.debug(f"{self.ip}: Handling X19 multicommand.")
|
||||
data = await self._x19_multicommand(*command.split("+"))
|
||||
logging.debug(f"{self.ip}: Received multicommand data.")
|
||||
return data
|
||||
|
||||
async def _x19_multicommand(self, *commands):
|
||||
data = None
|
||||
try:
|
||||
data = {}
|
||||
# send all commands individually
|
||||
for cmd in commands:
|
||||
data[cmd] = []
|
||||
data[cmd].append(await self.send_command(cmd, x19_command=True))
|
||||
except APIError as e:
|
||||
raise APIError(e)
|
||||
except Exception as e:
|
||||
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
|
||||
return data
|
||||
|
||||
async def version(self) -> dict:
|
||||
"""Get miner version info.
|
||||
<details>
|
||||
|
||||
@@ -187,57 +187,24 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
self.pwd = pwd
|
||||
self.current_token = None
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: Union[str, bytes],
|
||||
parameters: Union[str, int, bool] = None,
|
||||
ignore_errors: bool = False,
|
||||
**kwargs,
|
||||
async def send_privileged_command(
|
||||
self, command: Union[str, bytes], ignore_errors: bool = False, **kwargs
|
||||
) -> dict:
|
||||
# check if command is a string
|
||||
# if its bytes its encoded and needs to be sent raw
|
||||
if isinstance(command, str):
|
||||
# if it is a string, put it into the standard command format
|
||||
command = json.dumps({"command": command}).encode("utf-8")
|
||||
try:
|
||||
# get reader and writer streams
|
||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
||||
# handle OSError 121
|
||||
except OSError as e:
|
||||
if e.winerror == "121":
|
||||
print("Semaphore Timeout has Expired.")
|
||||
return {}
|
||||
command = {"cmd": command}
|
||||
for kwarg in kwargs:
|
||||
if kwargs[kwarg]:
|
||||
command[kwarg] = kwargs[kwarg]
|
||||
|
||||
# send the command
|
||||
writer.write(command)
|
||||
await writer.drain()
|
||||
|
||||
# instantiate data
|
||||
data = b""
|
||||
|
||||
# loop to receive all the data
|
||||
try:
|
||||
while True:
|
||||
d = await reader.read(4096)
|
||||
if not d:
|
||||
break
|
||||
data += d
|
||||
except Exception as e:
|
||||
logging.info(f"{str(self.ip)}: {e}")
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
|
||||
data = await self._send_bytes(enc_command)
|
||||
data = self._load_api_data(data)
|
||||
|
||||
# close the connection
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
# check if the returned data is encoded
|
||||
if "enc" in data.keys():
|
||||
# try to parse the encoded data
|
||||
try:
|
||||
data = parse_btminer_priviledge_data(self.current_token, data)
|
||||
except Exception as e:
|
||||
logging.info(f"{str(self.ip)}: {e}")
|
||||
try:
|
||||
data = parse_btminer_priviledge_data(self.current_token, data)
|
||||
except Exception as e:
|
||||
logging.info(f"{str(self.ip)}: {e}")
|
||||
|
||||
if not ignore_errors:
|
||||
# if it fails to validate, it is likely an error
|
||||
@@ -320,46 +287,18 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A dict from the API to confirm the pools were updated.
|
||||
</details>
|
||||
"""
|
||||
# get the token and password from the miner
|
||||
token_data = await self.get_token()
|
||||
|
||||
# parse pool data
|
||||
if not pool_1:
|
||||
raise APIError("No pools set.")
|
||||
elif pool_2 and pool_3:
|
||||
command = {
|
||||
"cmd": "update_pools",
|
||||
"pool1": pool_1,
|
||||
"worker1": worker_1,
|
||||
"passwd1": passwd_1,
|
||||
"pool2": pool_2,
|
||||
"worker2": worker_2,
|
||||
"passwd2": passwd_2,
|
||||
"pool3": pool_3,
|
||||
"worker3": worker_3,
|
||||
"passwd3": passwd_3,
|
||||
}
|
||||
elif pool_2:
|
||||
command = {
|
||||
"cmd": "update_pools",
|
||||
"pool1": pool_1,
|
||||
"worker1": worker_1,
|
||||
"passwd1": passwd_1,
|
||||
"pool2": pool_2,
|
||||
"worker2": worker_2,
|
||||
"passwd2": passwd_2,
|
||||
}
|
||||
else:
|
||||
command = {
|
||||
"cmd": "update_pools",
|
||||
"pool1": pool_1,
|
||||
"worker1": worker_1,
|
||||
"passwd1": passwd_1,
|
||||
}
|
||||
# encode the command with the token data
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
# send the command
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command(
|
||||
"update_pools",
|
||||
pool1=pool_1,
|
||||
worker1=worker_1,
|
||||
passwd1=passwd_1,
|
||||
pool2=pool_2,
|
||||
worker2=worker_2,
|
||||
passwd2=passwd_2,
|
||||
pool3=pool_3,
|
||||
worker3=worker_3,
|
||||
passwd3=passwd_3,
|
||||
)
|
||||
|
||||
async def restart(self) -> dict:
|
||||
"""Restart BTMiner using the API.
|
||||
@@ -373,10 +312,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the restart.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "restart_btminer"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("restart_btminer")
|
||||
|
||||
async def power_off(self, respbefore: bool = True) -> dict:
|
||||
"""Power off the miner using the API.
|
||||
@@ -393,12 +329,8 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
</details>
|
||||
"""
|
||||
if respbefore:
|
||||
command = {"cmd": "power_off", "respbefore": "true"}
|
||||
else:
|
||||
command = {"cmd": "power_off", "respbefore": "false"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("power_off", respbefore="true")
|
||||
return await self.send_privileged_command("power_off", respbefore="false")
|
||||
|
||||
async def power_on(self) -> dict:
|
||||
"""Power on the miner using the API.
|
||||
@@ -413,10 +345,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of powering on.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "power_on"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("power_on")
|
||||
|
||||
async def reset_led(self) -> dict:
|
||||
"""Reset the LED on the miner using the API.
|
||||
@@ -431,10 +360,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of resetting the LED.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "set_led", "param": "auto"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.set_led(auto=True)
|
||||
|
||||
async def set_led(
|
||||
self,
|
||||
@@ -462,19 +388,11 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of setting the LED.
|
||||
</details>
|
||||
"""
|
||||
if not auto:
|
||||
command = {
|
||||
"cmd": "set_led",
|
||||
"color": color,
|
||||
"period": period,
|
||||
"duration": duration,
|
||||
"start": start,
|
||||
}
|
||||
else:
|
||||
command = {"cmd": "set_led", "param": "auto"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command, ignore_errors=True)
|
||||
if auto:
|
||||
return await self.send_privileged_command("set_led", param=auto)
|
||||
return await self.send_privileged_command(
|
||||
"set_led", color=color, period=period, duration=duration, start=start
|
||||
)
|
||||
|
||||
async def set_low_power(self) -> dict:
|
||||
"""Set low power mode on the miner using the API.
|
||||
@@ -489,10 +407,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of setting low power mode.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "set_low_power"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("set_low_power")
|
||||
|
||||
async def update_firmware(self): # noqa - static
|
||||
"""Not implemented."""
|
||||
@@ -510,10 +425,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of the reboot.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "reboot"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("reboot")
|
||||
|
||||
async def factory_reset(self) -> dict:
|
||||
"""Reset the miner to factory defaults.
|
||||
@@ -525,10 +437,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of the reset.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "factory_reset"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("factory_reset")
|
||||
|
||||
async def update_pwd(self, old_pwd: str, new_pwd: str) -> dict:
|
||||
"""Update the admin user's password.
|
||||
@@ -555,11 +464,10 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
f"New password too long, the max length is 8. "
|
||||
f"Password size: {len(new_pwd.encode('utf-8'))}"
|
||||
)
|
||||
command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
try:
|
||||
data = await self.send_command(enc_command)
|
||||
data = await self.send_privileged_command(
|
||||
"update_pwd", old=old_pwd, new=new_pwd
|
||||
)
|
||||
except APIError as e:
|
||||
raise e
|
||||
self.pwd = new_pwd
|
||||
@@ -588,10 +496,9 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
f"range. Please set a % between -10 and "
|
||||
f"100"
|
||||
)
|
||||
command = {"cmd": "set_target_freq", "percent": str(percent)}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command(
|
||||
"set_target_freq", percent=str(percent)
|
||||
)
|
||||
|
||||
async def enable_fast_boot(self) -> dict:
|
||||
"""Turn on fast boot.
|
||||
@@ -607,10 +514,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of enabling fast boot.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "enable_btminer_fast_boot"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("enable_btminer_fast_boot")
|
||||
|
||||
async def disable_fast_boot(self) -> dict:
|
||||
"""Turn off fast boot.
|
||||
@@ -626,10 +530,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of disabling fast boot.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "disable_btminer_fast_boot"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("disable_btminer_fast_boot")
|
||||
|
||||
async def enable_web_pools(self) -> dict:
|
||||
"""Turn on web pool updates.
|
||||
@@ -645,10 +546,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of enabling web pools.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "enable_web_pools"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("enable_web_pools")
|
||||
|
||||
async def disable_web_pools(self) -> dict:
|
||||
"""Turn off web pool updates.
|
||||
@@ -664,10 +562,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of disabling web pools.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "disable_web_pools"}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("disable_web_pools")
|
||||
|
||||
async def set_hostname(self, hostname: str) -> dict:
|
||||
"""Set the hostname of the miner.
|
||||
@@ -685,10 +580,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
A reply informing of the status of setting the hostname.
|
||||
</details>
|
||||
"""
|
||||
command = {"cmd": "set_hostname", "hostname": hostname}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("set_hostname", hostname=hostname)
|
||||
|
||||
async def set_power_pct(self, percent: int) -> dict:
|
||||
"""Set the power percentage of the miner.
|
||||
@@ -713,10 +605,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
f"range. Please set a % between 0 and "
|
||||
f"100"
|
||||
)
|
||||
command = {"cmd": "set_power_pct", "percent": str(percent)}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command("set_power_pct", percent=str(percent))
|
||||
|
||||
async def pre_power_on(self, complete: bool, msg: str) -> dict:
|
||||
"""Configure or check status of pre power on.
|
||||
@@ -747,13 +636,12 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
'"adjust continue"]'
|
||||
)
|
||||
if complete:
|
||||
complete = "true"
|
||||
else:
|
||||
complete = "false"
|
||||
command = {"cmd": "pre_power_on", "complete": complete, "msg": msg}
|
||||
token_data = await self.get_token()
|
||||
enc_command = create_privileged_cmd(token_data, command)
|
||||
return await self.send_command(enc_command)
|
||||
return await self.send_privileged_command(
|
||||
"pre_power_on", complete="true", msg=msg
|
||||
)
|
||||
return await self.send_privileged_command(
|
||||
"pre_power_on", complete="false", msg=msg
|
||||
)
|
||||
|
||||
#### END privileged COMMANDS ####
|
||||
|
||||
@@ -880,3 +768,17 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("get_miner_info")
|
||||
|
||||
async def get_error_code(self) -> dict:
|
||||
"""Get a list of error codes from the miner.
|
||||
|
||||
<details>
|
||||
<summary>Expand</summary>
|
||||
Get a list of error codes from the miner. Replaced `summary` as the location of error codes with API version 2.0.4.
|
||||
|
||||
Returns:
|
||||
|
||||
A list of error codes on the miner.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("get_error_code")
|
||||
|
||||
@@ -17,6 +17,7 @@ from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime, timezone
|
||||
import time
|
||||
import json
|
||||
import copy
|
||||
|
||||
from .error_codes import X19Error, WhatsminerError, BraiinsOSError, InnosiliconError
|
||||
|
||||
@@ -48,6 +49,7 @@ class MinerData:
|
||||
fan_2: The speed of the second fan as an int.
|
||||
fan_3: The speed of the third fan as an int.
|
||||
fan_4: The speed of the fourth fan as an int.
|
||||
fan_psu: The speed of the PSU on the fan if the miner collects it.
|
||||
left_chips: The number of chips online in the left board as an int.
|
||||
center_chips: The number of chips online in the left board as an int.
|
||||
right_chips: The number of chips online in the left board as an int.
|
||||
@@ -75,19 +77,20 @@ class MinerData:
|
||||
center_board_hashrate: float = 0.0
|
||||
right_board_hashrate: float = 0.0
|
||||
temperature_avg: int = field(init=False)
|
||||
env_temp: float = 0.0
|
||||
left_board_temp: int = 0
|
||||
left_board_chip_temp: int = 0
|
||||
center_board_temp: int = 0
|
||||
center_board_chip_temp: int = 0
|
||||
right_board_temp: int = 0
|
||||
right_board_chip_temp: int = 0
|
||||
wattage: int = 0
|
||||
wattage_limit: int = 0
|
||||
env_temp: float = -1.0
|
||||
left_board_temp: int = -1
|
||||
left_board_chip_temp: int = -1
|
||||
center_board_temp: int = -1
|
||||
center_board_chip_temp: int = -1
|
||||
right_board_temp: int = -1
|
||||
right_board_chip_temp: int = -1
|
||||
wattage: int = -1
|
||||
wattage_limit: int = -1
|
||||
fan_1: int = -1
|
||||
fan_2: int = -1
|
||||
fan_3: int = -1
|
||||
fan_4: int = -1
|
||||
fan_psu: int = -1
|
||||
left_chips: int = 0
|
||||
center_chips: int = 0
|
||||
right_chips: int = 0
|
||||
@@ -121,6 +124,43 @@ class MinerData:
|
||||
def __iter__(self):
|
||||
return iter([item for item in self.asdict()])
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self // other
|
||||
|
||||
def __floordiv__(self, other):
|
||||
cp = copy.deepcopy(self)
|
||||
for key in self:
|
||||
item = getattr(self, key)
|
||||
if isinstance(item, int):
|
||||
setattr(cp, key, item // other)
|
||||
if isinstance(item, float):
|
||||
setattr(cp, key, round(item / other, 2))
|
||||
return cp
|
||||
|
||||
def __add__(self, other):
|
||||
if not isinstance(other, MinerData):
|
||||
raise TypeError("Cannot add MinerData to non MinerData type.")
|
||||
cp = copy.deepcopy(self)
|
||||
for key in self:
|
||||
item = getattr(self, key)
|
||||
other_item = getattr(other, key)
|
||||
if item is None:
|
||||
item = 0
|
||||
if other_item is None:
|
||||
other_item = 0
|
||||
|
||||
if isinstance(item, int):
|
||||
setattr(cp, key, item + other_item)
|
||||
if isinstance(item, float):
|
||||
setattr(cp, key, item + other_item)
|
||||
if isinstance(item, str):
|
||||
setattr(cp, key, "")
|
||||
if isinstance(item, list):
|
||||
setattr(cp, key, item + other_item)
|
||||
if isinstance(item, bool):
|
||||
setattr(cp, key, item & other_item)
|
||||
return cp
|
||||
|
||||
@property
|
||||
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||
return self.right_chips + self.center_chips + self.left_chips
|
||||
@@ -154,7 +194,7 @@ class MinerData:
|
||||
self.center_board_chip_temp,
|
||||
self.right_board_chip_temp,
|
||||
]:
|
||||
if temp and not temp == 0:
|
||||
if temp and not temp == -1:
|
||||
total_temp += temp
|
||||
temp_count += 1
|
||||
if not temp_count > 0:
|
||||
@@ -193,6 +233,21 @@ class MinerData:
|
||||
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
||||
return json.dumps(data)
|
||||
|
||||
def as_csv(self) -> str:
|
||||
"""Get this dataclass as CSV.
|
||||
|
||||
Returns:
|
||||
A CSV version of this class with no headers.
|
||||
"""
|
||||
data = self.asdict()
|
||||
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
||||
errs = []
|
||||
for error in data["errors"]:
|
||||
errs.append(error["error_message"])
|
||||
data["errors"] = "; ".join(errs)
|
||||
data_list = [str(data[item]) for item in data]
|
||||
return ",".join(data_list)
|
||||
|
||||
def as_influxdb(self, measurement_name: str = "miner_data") -> str:
|
||||
"""Get this dataclass as [influxdb line protocol](https://docs.influxdata.com/influxdb/v2.4/reference/syntax/line-protocol/).
|
||||
|
||||
|
||||
@@ -158,14 +158,37 @@ class BTMiner(BaseMiner):
|
||||
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
data = []
|
||||
try:
|
||||
err_data = await self.api.get_error_code()
|
||||
if err_data:
|
||||
if err_data.get("Msg"):
|
||||
if err_data["Msg"].get("error_code"):
|
||||
for err in err_data["Msg"]["error_code"]:
|
||||
if isinstance(err, dict):
|
||||
for code in err:
|
||||
data.append(
|
||||
WhatsminerError(
|
||||
error_code=int(code)
|
||||
)
|
||||
)
|
||||
else:
|
||||
data.append(
|
||||
WhatsminerError(
|
||||
error_code=int(err)
|
||||
)
|
||||
)
|
||||
except APIError:
|
||||
summary_data = await self.api.summary()
|
||||
if summary_data[0].get("Error Code Count"):
|
||||
for i in range(summary_data[0]["Error Code Count"]):
|
||||
if summary_data[0].get(f"Error Code {i}"):
|
||||
if not summary_data[0][f"Error Code {i}"] == "":
|
||||
data.append(
|
||||
WhatsminerError(
|
||||
error_code=summary_data[0][f"Error Code {i}"]
|
||||
)
|
||||
)
|
||||
|
||||
summary_data = await self.api.summary()
|
||||
if summary_data[0].get("Error Code Count"):
|
||||
for i in range(summary_data[0]["Error Code Count"]):
|
||||
if summary_data[0].get(f"Error Code {i}"):
|
||||
data.append(
|
||||
WhatsminerError(error_code=summary_data[0][f"Error Code {i}"])
|
||||
)
|
||||
return data
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
@@ -265,13 +288,20 @@ class BTMiner(BaseMiner):
|
||||
break
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if not miner_data:
|
||||
return data
|
||||
|
||||
summary = miner_data.get("summary")[0]
|
||||
devs = miner_data.get("devs")[0]
|
||||
pools = miner_data.get("pools")[0]
|
||||
try:
|
||||
psu_data = await self.api.get_psu()
|
||||
except APIError:
|
||||
psu_data = None
|
||||
try:
|
||||
err_data = await self.api.get_error_code()
|
||||
except APIError:
|
||||
err_data = None
|
||||
|
||||
if summary:
|
||||
summary_data = summary.get("SUMMARY")
|
||||
@@ -287,6 +317,9 @@ class BTMiner(BaseMiner):
|
||||
if summary_data[0].get("Power Limit"):
|
||||
wattage_limit = summary_data[0]["Power Limit"]
|
||||
|
||||
if summary_data[0].get("Power Fanspeed"):
|
||||
data.fan_psu = summary_data[0]["Power Fanspeed"]
|
||||
|
||||
data.fan_1 = summary_data[0]["Fan Speed In"]
|
||||
data.fan_2 = summary_data[0]["Fan Speed Out"]
|
||||
|
||||
@@ -306,11 +339,38 @@ class BTMiner(BaseMiner):
|
||||
if summary_data[0].get("Error Code Count"):
|
||||
for i in range(summary_data[0]["Error Code Count"]):
|
||||
if summary_data[0].get(f"Error Code {i}"):
|
||||
if not summary_data[0][f"Error Code {i}"] == "":
|
||||
data.errors.append(
|
||||
WhatsminerError(
|
||||
error_code=summary_data[0][
|
||||
f"Error Code {i}"
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if psu_data:
|
||||
psu = psu_data.get("Msg")
|
||||
if psu:
|
||||
if psu.get("fan_speed"):
|
||||
data.fan_psu = psu["fan_speed"]
|
||||
|
||||
if err_data:
|
||||
if err_data.get("Msg"):
|
||||
if err_data["Msg"].get("error_code"):
|
||||
for err in err_data["Msg"]["error_code"]:
|
||||
if isinstance(err, dict):
|
||||
for code in err:
|
||||
data.errors.append(
|
||||
WhatsminerError(
|
||||
error_code=summary_data[0][f"Error Code {i}"]
|
||||
error_code=int(code)
|
||||
)
|
||||
)
|
||||
else:
|
||||
data.errors.append(
|
||||
WhatsminerError(
|
||||
error_code=int(err)
|
||||
)
|
||||
)
|
||||
|
||||
if devs:
|
||||
temp_data = devs.get("DEVS")
|
||||
|
||||
33
pyasic/miners/_types/whatsminer/M5X/M50.py
Normal file
33
pyasic/miners/_types/whatsminer/M5X/M50.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# 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.base import BaseMiner
|
||||
|
||||
|
||||
class M50(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M50"
|
||||
self.nominal_chips = 105
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M50VH50(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M50 VH50"
|
||||
self.nominal_chips = 105
|
||||
self.fan_count = 2
|
||||
15
pyasic/miners/_types/whatsminer/M5X/__init__.py
Normal file
15
pyasic/miners/_types/whatsminer/M5X/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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 .M50 import M50, M50VH50
|
||||
@@ -14,3 +14,4 @@
|
||||
|
||||
from .M2X import *
|
||||
from .M3X import *
|
||||
from .M5X import *
|
||||
@@ -216,6 +216,11 @@ MINER_CLASSES = {
|
||||
"BTMiner": BTMinerM32,
|
||||
"20": BTMinerM32V20,
|
||||
},
|
||||
"M50": {
|
||||
"Default": BTMinerM50,
|
||||
"BTMiner": BTMinerM50,
|
||||
"H50": BTMinerM50VH50,
|
||||
},
|
||||
"AVALONMINER 721": {
|
||||
"Default": CGMinerAvalon721,
|
||||
"CGMiner": CGMinerAvalon721,
|
||||
|
||||
@@ -27,7 +27,13 @@ class _MinerListener:
|
||||
|
||||
def datagram_received(self, data, _addr):
|
||||
m = data.decode()
|
||||
ip, mac = m.split(",")
|
||||
if "," in m:
|
||||
ip, mac = m.split(",")
|
||||
else:
|
||||
d = m[:-1].split("MAC")
|
||||
ip = d[0][3:]
|
||||
mac = d[1][1:]
|
||||
|
||||
new_miner = {"IP": ip, "MAC": mac.upper()}
|
||||
MinerListener().new_miner = new_miner
|
||||
|
||||
@@ -46,9 +52,12 @@ class MinerListener(metaclass=Singleton):
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
transport, protocol = await loop.create_datagram_endpoint(
|
||||
transport_14235, protocol_14235 = await loop.create_datagram_endpoint(
|
||||
lambda: _MinerListener(), local_addr=("0.0.0.0", 14235) # noqa
|
||||
)
|
||||
transport_8888, protocol_8888 = await loop.create_datagram_endpoint(
|
||||
lambda: _MinerListener(), local_addr=("0.0.0.0", 8888) # noqa
|
||||
)
|
||||
|
||||
while True:
|
||||
if self.new_miner:
|
||||
@@ -56,7 +65,8 @@ class MinerListener(metaclass=Singleton):
|
||||
self.found_miners.append(self.new_miner)
|
||||
self.new_miner = None
|
||||
if self.stop:
|
||||
transport.close()
|
||||
transport_14235.close()
|
||||
transport_8888.close()
|
||||
break
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
28
pyasic/miners/whatsminer/btminer/M5X/M50.py
Normal file
28
pyasic/miners/whatsminer/btminer/M5X/M50.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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 BTMiner # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import M50, M50VH50 # noqa - Ignore access to _module
|
||||
|
||||
|
||||
class BTMinerM50(BTMiner, M50):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM50VH50(BTMiner, M50VH50):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
18
pyasic/miners/whatsminer/btminer/M5X/__init__.py
Normal file
18
pyasic/miners/whatsminer/btminer/M5X/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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 .M50 import (
|
||||
BTMinerM50,
|
||||
BTMinerM50VH50,
|
||||
)
|
||||
@@ -14,3 +14,4 @@
|
||||
|
||||
from .M2X import *
|
||||
from .M3X import *
|
||||
from .M5X import *
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.18.0"
|
||||
version = "0.19.1"
|
||||
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
||||
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
||||
repository = "https://github.com/UpstreamData/pyasic"
|
||||
|
||||
Reference in New Issue
Block a user