Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
228daecbbf | ||
|
|
81aa569d07 | ||
|
|
aba8d7b8b9 | ||
|
|
449403bc57 | ||
|
|
997a1bbe31 | ||
|
|
2c7e0e3efe | ||
|
|
6051a46654 | ||
|
|
c7847d2789 | ||
|
|
215af72a6b | ||
|
|
b33586f8eb | ||
|
|
8db81d9b3c | ||
|
|
cf3b2fedf4 | ||
|
|
81db6d8e28 | ||
|
|
d47e59d057 | ||
|
|
8388d2f4ac | ||
|
|
f5ec513ce0 | ||
|
|
8975842f0b | ||
|
|
c6ca1df112 | ||
|
|
92f58a9682 | ||
|
|
5dc19dbd78 | ||
|
|
b0fc11bcc1 | ||
|
|
23ebe460a4 | ||
|
|
92db8c8161 | ||
|
|
d9b522734a | ||
|
|
e02457f0b2 | ||
|
|
104b4c18c2 | ||
|
|
1970f8c924 | ||
|
|
e431a06ac5 | ||
|
|
6da9b58088 | ||
|
|
3108443041 | ||
|
|
37ebb9e6c3 | ||
|
|
6c2e0f59b1 | ||
|
|
6eb9cdce90 | ||
|
|
e96d9447d1 | ||
|
|
d3cca11322 | ||
|
|
86155db455 | ||
|
|
5b078b4b27 | ||
|
|
9672dd6873 | ||
|
|
1587f65196 | ||
|
|
5ff10c0cdd | ||
|
|
aa0a028564 | ||
|
|
82f6d2f274 | ||
|
|
833de3ab43 | ||
|
|
08d273c7c1 | ||
|
|
aac7598187 | ||
|
|
5c0ac4e665 | ||
|
|
7bd5e49412 | ||
|
|
53ff3c5f79 | ||
|
|
36ae6e5272 | ||
|
|
fb3dffb216 | ||
|
|
ff6a6d2ec6 | ||
|
|
f4bbc2c3e5 | ||
|
|
4dbfdbe29c | ||
|
|
5e2a18f91e | ||
|
|
3363bdc592 | ||
|
|
08180a2d59 | ||
|
|
1a64ff4038 | ||
|
|
8ad90a6abb | ||
|
|
8cdd5ff015 | ||
|
|
a08f434e1f | ||
|
|
9acd6d2fea |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ pyvenv.cfg
|
||||
.env/
|
||||
bin/
|
||||
lib/
|
||||
.idea/
|
||||
|
||||
@@ -230,7 +230,7 @@ These functions are
|
||||
|
||||
## [`MinerConfig`][pyasic.config.MinerConfig] and [`MinerData`][pyasic.data.MinerData]
|
||||
|
||||
Pyasic implements a few dataclasses as helpers to make data return types consistent across different miners and miner APIs.
|
||||
Pyasic implements a few dataclasses as helpers to make data return types consistent across different miners and miner APIs. The different fields of these dataclasses can all be viewed with the classmethod `cls.fields()`.
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -49,56 +49,73 @@ details {
|
||||
<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>
|
||||
</details>
|
||||
<details>
|
||||
<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>
|
||||
<details>
|
||||
<summary><a href="../whatsminer/M3X/#m34s">M34S+</a></summary>
|
||||
<ul>
|
||||
<li><a href="../whatsminer/M3X/#m34sve10">VE10</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
@@ -106,33 +123,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 +161,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 +198,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 +233,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>
|
||||
|
||||
@@ -193,3 +193,19 @@
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M34S+
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M34S_Plus.BTMinerM34SPlus
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M34S+VE10
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M34S_Plus.BTMinerM34SPlusVE10
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
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"
|
||||
|
||||
@@ -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,61 +72,27 @@ 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, allow_warning: bool = True) -> 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.
|
||||
allow_warning: A boolean to supress APIWarnings.
|
||||
"""
|
||||
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
|
||||
# doesn't 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, allow_warning=allow_warning)
|
||||
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
|
||||
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 send_command(
|
||||
self,
|
||||
command: Union[str, bytes],
|
||||
parameters: Union[str, int, bool] = None,
|
||||
ignore_errors: bool = False,
|
||||
x19_command: bool = False,
|
||||
) -> dict:
|
||||
"""Send an API command to the miner and return the result.
|
||||
|
||||
Parameters:
|
||||
command: The command to sent to the miner.
|
||||
parameters: Any additional parameters to be sent with the command.
|
||||
ignore_errors: Whether or not to raise APIError when the command returns an error.
|
||||
x19_command: Whether this is a command for an x19 that may be an issue (such as a "+" delimited multicommand)
|
||||
|
||||
Returns:
|
||||
The return data from the API command parsed from JSON into a dict.
|
||||
"""
|
||||
async def _send_bytes(self, data: bytes) -> bytes:
|
||||
try:
|
||||
# get reader and writer streams
|
||||
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
|
||||
@@ -133,19 +100,14 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
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
|
||||
return b"{}"
|
||||
|
||||
# send the command
|
||||
writer.write(json.dumps(cmd).encode("utf-8"))
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
|
||||
# instantiate data
|
||||
data = b""
|
||||
ret_data = b""
|
||||
|
||||
# loop to receive all the data
|
||||
try:
|
||||
@@ -153,27 +115,60 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
d = await reader.read(4096)
|
||||
if not d:
|
||||
break
|
||||
data += d
|
||||
ret_data += d
|
||||
except Exception as e:
|
||||
logging.warning(f"{self.ip}: API Command Error: - {e}")
|
||||
|
||||
data = self._load_api_data(data)
|
||||
|
||||
# close the connection
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
return ret_data
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: Union[str, bytes],
|
||||
parameters: Union[str, int, bool] = None,
|
||||
ignore_errors: bool = False,
|
||||
allow_warning: bool = True,
|
||||
) -> dict:
|
||||
"""Send an API command to the miner and return the result.
|
||||
|
||||
Parameters:
|
||||
command: The command to sent to the miner.
|
||||
parameters: Any additional parameters to be sent with the command.
|
||||
ignore_errors: Whether to raise APIError when the command returns an error.
|
||||
allow_warning: Whether to warn if the command fails.
|
||||
|
||||
Returns:
|
||||
The return data from the API command parsed from JSON into a dict.
|
||||
"""
|
||||
# create the command
|
||||
cmd = {"command": command}
|
||||
if parameters:
|
||||
cmd["parameter"] = parameters
|
||||
|
||||
# send the command
|
||||
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
|
||||
|
||||
data = self._load_api_data(data)
|
||||
|
||||
# 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]}")
|
||||
if allow_warning:
|
||||
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 +199,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, allow_warning: bool = True
|
||||
) -> 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"
|
||||
# doesn't work for S19 which uses the backup _x19_multicommand
|
||||
command = "+".join(commands)
|
||||
try:
|
||||
data = await self.send_command(command, allow_warning=allow_warning)
|
||||
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, allow_warning=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 ####
|
||||
|
||||
@@ -879,4 +767,18 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
General miner info.
|
||||
</details>
|
||||
"""
|
||||
return await self.send_command("get_miner_info")
|
||||
return await self.send_command("get_miner_info", allow_warning=False)
|
||||
|
||||
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", allow_warning=False)
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from pyasic.API import BaseMinerAPI
|
||||
from pyasic.API import BaseMinerAPI, APIError
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class CGMinerAPI(BaseMinerAPI):
|
||||
@@ -36,6 +38,37 @@ class CGMinerAPI(BaseMinerAPI):
|
||||
def __init__(self, ip: str, port: int = 4028):
|
||||
super().__init__(ip, port)
|
||||
|
||||
async def multicommand(
|
||||
self, *commands: str, allow_warning: bool = True
|
||||
) -> 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, allow_warning=allow_warning)
|
||||
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, allow_warning=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>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
from dataclasses import dataclass, asdict, fields
|
||||
from typing import Literal, List
|
||||
import random
|
||||
import string
|
||||
@@ -37,6 +37,10 @@ class _Pool:
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return fields(cls)
|
||||
|
||||
def from_dict(self, data: dict):
|
||||
"""Convert raw pool data as a dict to usable data and save it to this class.
|
||||
|
||||
@@ -136,6 +140,10 @@ class _PoolGroup:
|
||||
group_name: str = None
|
||||
pools: List[_Pool] = None
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return fields(cls)
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.group_name:
|
||||
self.group_name = "".join(
|
||||
@@ -263,6 +271,10 @@ class MinerConfig:
|
||||
dps_shutdown_enabled: bool = None
|
||||
dps_shutdown_duration: float = None
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return fields(cls)
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
"""Convert the data in this class to a dict."""
|
||||
data_dict = asdict(self)
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Union, List
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from dataclasses import dataclass, field, asdict, fields
|
||||
from datetime import datetime, timezone
|
||||
from functools import reduce
|
||||
import time
|
||||
import json
|
||||
import copy
|
||||
@@ -22,6 +23,17 @@ import copy
|
||||
from .error_codes import X19Error, WhatsminerError, BraiinsOSError, InnosiliconError
|
||||
|
||||
|
||||
@dataclass
|
||||
class HashBoard:
|
||||
slot: int = 0
|
||||
hashrate: float = 0.0
|
||||
temp: int = -1
|
||||
chip_temp: int = -1
|
||||
chips: int = 0
|
||||
expected_chips: int = 0
|
||||
missing: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinerData:
|
||||
"""A Dataclass to standardize data returned from miners (specifically `AnyMiner().get_data()`)
|
||||
@@ -49,6 +61,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.
|
||||
@@ -72,26 +85,29 @@ class MinerData:
|
||||
model: str = "Unknown"
|
||||
hostname: str = "Unknown"
|
||||
hashrate: float = 0
|
||||
left_board_hashrate: float = 0.0
|
||||
center_board_hashrate: float = 0.0
|
||||
right_board_hashrate: float = 0.0
|
||||
hashboards: List[HashBoard] = field(default_factory=list)
|
||||
ideal_hashboards: int = 1
|
||||
left_board_hashrate: float = field(init=False)
|
||||
center_board_hashrate: float = field(init=False)
|
||||
right_board_hashrate: float = field(init=False)
|
||||
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 = field(init=False)
|
||||
left_board_chip_temp: int = field(init=False)
|
||||
center_board_temp: int = field(init=False)
|
||||
center_board_chip_temp: int = field(init=False)
|
||||
right_board_temp: int = field(init=False)
|
||||
right_board_chip_temp: int = field(init=False)
|
||||
wattage: int = -1
|
||||
wattage_limit: int = -1
|
||||
fan_1: int = -1
|
||||
fan_2: int = -1
|
||||
fan_3: int = -1
|
||||
fan_4: int = -1
|
||||
left_chips: int = 0
|
||||
center_chips: int = 0
|
||||
right_chips: int = 0
|
||||
fan_psu: int = -1
|
||||
left_chips: int = field(init=False)
|
||||
center_chips: int = field(init=False)
|
||||
right_chips: int = field(init=False)
|
||||
total_chips: int = field(init=False)
|
||||
ideal_chips: int = 1
|
||||
percent_ideal: float = field(init=False)
|
||||
@@ -107,6 +123,11 @@ class MinerData:
|
||||
fault_light: Union[bool, None] = None
|
||||
efficiency: int = field(init=False)
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return [f.name for f in fields(cls)]
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
self.datetime = datetime.now(timezone.utc).astimezone()
|
||||
|
||||
@@ -161,12 +182,148 @@ class MinerData:
|
||||
|
||||
@property
|
||||
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||
return self.right_chips + self.center_chips + self.left_chips
|
||||
return sum([hb.chips for hb in self.hashboards])
|
||||
|
||||
@total_chips.setter
|
||||
def total_chips(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def left_chips(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) in [2, 3]:
|
||||
return self.hashboards[0].chips
|
||||
return 0
|
||||
|
||||
@left_chips.setter
|
||||
def left_chips(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def center_chips(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) == 1:
|
||||
return self.hashboards[0].chips
|
||||
if len(self.hashboards) == 3:
|
||||
return self.hashboards[1].chips
|
||||
return 0
|
||||
|
||||
@center_chips.setter
|
||||
def center_chips(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def right_chips(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) == 2:
|
||||
return self.hashboards[1].chips
|
||||
if len(self.hashboards) == 3:
|
||||
return self.hashboards[2].chips
|
||||
return 0
|
||||
|
||||
@right_chips.setter
|
||||
def right_chips(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def left_board_hashrate(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) in [2, 3]:
|
||||
return self.hashboards[0].hashrate
|
||||
return 0
|
||||
|
||||
@left_board_hashrate.setter
|
||||
def left_board_hashrate(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def center_board_hashrate(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) == 1:
|
||||
return self.hashboards[0].hashrate
|
||||
if len(self.hashboards) == 3:
|
||||
return self.hashboards[1].hashrate
|
||||
return 0
|
||||
|
||||
@center_board_hashrate.setter
|
||||
def center_board_hashrate(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def right_board_hashrate(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) == 2:
|
||||
return self.hashboards[1].hashrate
|
||||
if len(self.hashboards) == 3:
|
||||
return self.hashboards[2].hashrate
|
||||
return 0
|
||||
|
||||
@right_board_hashrate.setter
|
||||
def right_board_hashrate(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def left_board_temp(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) in [2, 3]:
|
||||
return self.hashboards[0].temp
|
||||
return 0
|
||||
|
||||
@left_board_temp.setter
|
||||
def left_board_temp(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def center_board_temp(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) == 1:
|
||||
return self.hashboards[0].temp
|
||||
if len(self.hashboards) == 3:
|
||||
return self.hashboards[1].temp
|
||||
return 0
|
||||
|
||||
@center_board_temp.setter
|
||||
def center_board_temp(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def right_board_temp(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) == 2:
|
||||
return self.hashboards[1].temp
|
||||
if len(self.hashboards) == 3:
|
||||
return self.hashboards[2].temp
|
||||
return 0
|
||||
|
||||
@right_board_temp.setter
|
||||
def right_board_temp(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def left_board_chip_temp(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) in [2, 3]:
|
||||
return self.hashboards[0].chip_temp
|
||||
return 0
|
||||
|
||||
@left_board_chip_temp.setter
|
||||
def left_board_chip_temp(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def center_board_chip_temp(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) == 1:
|
||||
return self.hashboards[0].chip_temp
|
||||
if len(self.hashboards) == 3:
|
||||
return self.hashboards[1].chip_temp
|
||||
return 0
|
||||
|
||||
@center_board_chip_temp.setter
|
||||
def center_board_chip_temp(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def right_board_chip_temp(self): # noqa - Skip PyCharm inspection
|
||||
if len(self.hashboards) == 2:
|
||||
return self.hashboards[1].chip_temp
|
||||
if len(self.hashboards) == 3:
|
||||
return self.hashboards[2].chip_temp
|
||||
return 0
|
||||
|
||||
@right_board_chip_temp.setter
|
||||
def right_board_chip_temp(self, val):
|
||||
pass
|
||||
|
||||
@property
|
||||
def nominal(self): # noqa - Skip PyCharm inspection
|
||||
return self.ideal_chips == self.total_chips
|
||||
@@ -187,13 +344,9 @@ class MinerData:
|
||||
def temperature_avg(self): # noqa - Skip PyCharm inspection
|
||||
total_temp = 0
|
||||
temp_count = 0
|
||||
for temp in [
|
||||
self.left_board_chip_temp,
|
||||
self.center_board_chip_temp,
|
||||
self.right_board_chip_temp,
|
||||
]:
|
||||
if temp and not temp == 0:
|
||||
total_temp += temp
|
||||
for hb in self.hashboards:
|
||||
if hb.temp and not hb.temp == -1:
|
||||
total_temp += hb.temp
|
||||
temp_count += 1
|
||||
if not temp_count > 0:
|
||||
return 0
|
||||
@@ -231,6 +384,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/).
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
from dataclasses import dataclass, asdict, fields
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -21,9 +21,15 @@ class X19Error:
|
||||
|
||||
Attributes:
|
||||
error_message: The error message as a string.
|
||||
error_code: The error code as an int. 0 if the message is not assigned a code.
|
||||
"""
|
||||
|
||||
error_message: str
|
||||
error_code: int = 0
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return fields(cls)
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
from dataclasses import dataclass, asdict, fields
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -21,9 +21,16 @@ class BraiinsOSError:
|
||||
|
||||
Attributes:
|
||||
error_message: The error message as a string.
|
||||
error_code: The error code as an int. 0 if the message is not assigned a code.
|
||||
"""
|
||||
|
||||
error_message: str
|
||||
error_code: int = 0
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return fields(cls)
|
||||
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from dataclasses import dataclass, field, asdict, fields
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -27,6 +27,11 @@ class InnosiliconError:
|
||||
error_code: int
|
||||
error_message: str = field(init=False)
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return fields(cls)
|
||||
|
||||
|
||||
@property
|
||||
def error_message(self): # noqa - Skip PyCharm inspection
|
||||
if self.error_code in ERROR_CODES:
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from dataclasses import dataclass, field, asdict, fields
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -27,11 +27,25 @@ class WhatsminerError:
|
||||
error_code: int
|
||||
error_message: str = field(init=False)
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return fields(cls)
|
||||
|
||||
@property
|
||||
def error_message(self): # noqa - Skip PyCharm inspection
|
||||
if self.error_code in ERROR_CODES:
|
||||
return ERROR_CODES[self.error_code]
|
||||
return "Unknown error type."
|
||||
err_type = int(str(self.error_code)[:-2])
|
||||
err_subtype = int(str(self.error_code)[-2:-1])
|
||||
err_value = int(str(self.error_code)[-1:])
|
||||
try:
|
||||
select_err_subtype = ERROR_CODES[err_type][err_subtype]
|
||||
if err_value in select_err_subtype:
|
||||
return select_err_subtype[err_value]
|
||||
elif "n" in select_err_subtype:
|
||||
return select_err_subtype["n"].replace("{n}", str(err_value)) # noqa: picks up `select_err_subtype["n"]` as not being numeric?
|
||||
else:
|
||||
return "Unknown error type."
|
||||
except KeyError:
|
||||
return "Unknown error type."
|
||||
|
||||
@error_message.setter
|
||||
def error_message(self, val):
|
||||
@@ -42,131 +56,158 @@ class WhatsminerError:
|
||||
|
||||
|
||||
ERROR_CODES = {
|
||||
110: "Intake fan speed error.",
|
||||
111: "Exhaust fan speed error.",
|
||||
120: "Intake fan speed error. Fan speed deviates by more than 2000.",
|
||||
121: "Exhaust fan speed error. Fan speed deviates by more than 2000.",
|
||||
130: "Intake fan speed error. Fan speed deviates by more than 3000.",
|
||||
131: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
|
||||
140: "Fan speed too high.",
|
||||
200: "Power probing error. No power found.",
|
||||
201: "Power supply and configuration file don't match.",
|
||||
202: "Power output voltage error.",
|
||||
203: "Power protecting due to high environment temperature.",
|
||||
204: "Power current protecting due to high environment temperature.",
|
||||
205: "Power current error.",
|
||||
206: "Power input low voltage error.",
|
||||
207: "Power input current protecting due to bad power input.",
|
||||
210: "Power error.",
|
||||
213: "Power input voltage and current do not match power output.",
|
||||
216: "Power remained unchanged for a long time.",
|
||||
217: "Power set enable error.",
|
||||
218: "Power input voltage is lower than 230V for high power mode.",
|
||||
233: "Power output high temperature protection error.",
|
||||
234: "Power output high temperature protection error.",
|
||||
235: "Power output high temperature protection error.",
|
||||
236: "Power output high current protection error.",
|
||||
237: "Power output high current protection error.",
|
||||
238: "Power output high current protection error.",
|
||||
239: "Power output high voltage protection error.",
|
||||
240: "Power output low voltage protection error.",
|
||||
241: "Power output current imbalance error.",
|
||||
243: "Power input high temperature protection error.",
|
||||
244: "Power input high temperature protection error.",
|
||||
245: "Power input high temperature protection error.",
|
||||
246: "Power input high voltage protection error.",
|
||||
247: "Power input high voltage protection error.",
|
||||
248: "Power input high current protection error.",
|
||||
249: "Power input high current protection error.",
|
||||
250: "Power input low voltage protection error.",
|
||||
251: "Power input low voltage protection error.",
|
||||
253: "Power supply fan error.",
|
||||
254: "Power supply fan error.",
|
||||
255: "Power output high power protection error.",
|
||||
256: "Power output high power protection error.",
|
||||
257: "Input over current protection of power supply on primary side.",
|
||||
263: "Power communication warning.",
|
||||
264: "Power communication error.",
|
||||
267: "Power watchdog protection.",
|
||||
268: "Power output high current protection.",
|
||||
269: "Power input high current protection.",
|
||||
270: "Power input high voltage protection.",
|
||||
271: "Power input low voltage protection.",
|
||||
272: "Excessive power supply output warning.",
|
||||
273: "Power input too high warning.",
|
||||
274: "Power fan warning.",
|
||||
275: "Power high temperature warning.",
|
||||
300: "Right board temperature sensor detection error.",
|
||||
301: "Center board temperature sensor detection error.",
|
||||
302: "Left board temperature sensor detection error.",
|
||||
320: "Right board temperature reading error.",
|
||||
321: "Center board temperature reading error.",
|
||||
322: "Left board temperature reading error.",
|
||||
329: "Control board temperature sensor communication error.",
|
||||
350: "Right board temperature protecting.",
|
||||
351: "Center board temperature protecting.",
|
||||
352: "Left board temperature protecting.",
|
||||
360: "Hashboard high temperature error.",
|
||||
410: "Right board eeprom detection error.",
|
||||
411: "Center board eeprom detection error.",
|
||||
412: "Left board eeprom detection error.",
|
||||
420: "Right board eeprom parsing error.",
|
||||
421: "Center board eeprom parsing error.",
|
||||
422: "Left board eeprom parsing error.",
|
||||
430: "Right board chip bin type error.",
|
||||
431: "Center board chip bin type error.",
|
||||
432: "Left board chip bin type error.",
|
||||
440: "Right board eeprom chip number X error.",
|
||||
441: "Center board eeprom chip number X error.",
|
||||
442: "Left board eeprom chip number X error.",
|
||||
450: "Right board eeprom xfer error.",
|
||||
451: "Center board eeprom xfer error.",
|
||||
452: "Left board eeprom xfer error.",
|
||||
510: "Right board miner type error.",
|
||||
511: "Center board miner type error.",
|
||||
512: "Left board miner type error.",
|
||||
520: "Right board bin type error.",
|
||||
521: "Center board bin type error.",
|
||||
522: "Left board bin type error.",
|
||||
530: "Right board not found.",
|
||||
531: "Center board not found.",
|
||||
532: "Left board not found.",
|
||||
540: "Right board error reading chip id.",
|
||||
541: "Center board error reading chip id.",
|
||||
542: "Left board error reading chip id.",
|
||||
550: "Right board has bad chips.",
|
||||
551: "Center board has bad chips.",
|
||||
552: "Left board has bad chips.",
|
||||
560: "Right board loss of balance error.",
|
||||
561: "Center board loss of balance error.",
|
||||
562: "Left board loss of balance error.",
|
||||
600: "Environment temperature is too high.",
|
||||
610: "Environment temperature is too high for high performance mode.",
|
||||
701: "Control board no support chip.",
|
||||
710: "Control board rebooted as an exception.",
|
||||
712: "Control board rebooted as an exception.",
|
||||
800: "CGMiner checksum error.",
|
||||
801: "System monitor checksum error.",
|
||||
802: "Remote daemon checksum error.",
|
||||
2010: "All pools are disabled.",
|
||||
2020: "Pool 0 connection failed.",
|
||||
2021: "Pool 1 connection failed.",
|
||||
2022: "Pool 2 connection failed.",
|
||||
2023: "Pool 3 connection failed.",
|
||||
2030: "High rejection rate on pool.",
|
||||
2040: "The pool does not support asicboost mode.",
|
||||
2310: "Hashrate is too low.",
|
||||
2320: "Hashrate is too low.",
|
||||
2340: "Hashrate loss is too high.",
|
||||
2350: "Hashrate loss is too high.",
|
||||
5070: "Right hashboard water velocity is abnormal.",
|
||||
5071: "Center hashboard water velocity is abnormal.",
|
||||
5072: "Left hashboard water velocity is abnormal.",
|
||||
5110: "Right hashboard frequency up timeout.",
|
||||
5111: "Center hashboard frequency up timeout.",
|
||||
5112: "Left hashboard frequency up timeout.",
|
||||
8410: "Software version error.",
|
||||
100001: "/antiv/signature illegal.",
|
||||
100002: "/antiv/dig/init.d illegal.",
|
||||
100003: "/antiv/dig/pf_partial.dig illegal.",
|
||||
1: { # Fan error
|
||||
1: { # Fan speed error of 1000+
|
||||
0: "Intake fan speed error.",
|
||||
1: "Exhaust fan speed error.",
|
||||
},
|
||||
2: { # Fan speed error of 2000+
|
||||
0: "Intake fan speed error. Fan speed deviates by more than 2000.",
|
||||
1: "Exhaust fan speed error. Fan speed deviates by more than 2000.",
|
||||
},
|
||||
3: { # Fan speed error of 3000+
|
||||
0: "Intake fan speed error. Fan speed deviates by more than 3000.",
|
||||
1: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
|
||||
},
|
||||
4: {0: "Fan speed too high."}, # High speed
|
||||
},
|
||||
2: { # Power error
|
||||
0: {
|
||||
0: "Power probing error. No power found.",
|
||||
1: "Power supply and configuration file don't match.",
|
||||
2: "Power output voltage error.",
|
||||
3: "Power protecting due to high environment temperature.",
|
||||
4: "Power current protecting due to high environment temperature.",
|
||||
5: "Power current error.",
|
||||
6: "Power input low voltage error.",
|
||||
7: "Power input current protecting due to bad power input.",
|
||||
},
|
||||
1: {
|
||||
0: "Power error.",
|
||||
3: "Power input voltage and current do not match power output.",
|
||||
6: "Power remained unchanged for a long time.",
|
||||
7: "Power set enable error.",
|
||||
8: "Power input voltage is lower than 230V for high power mode.",
|
||||
},
|
||||
3: {
|
||||
3: "Power output high temperature protection error.",
|
||||
4: "Power output high temperature protection error.",
|
||||
5: "Power output high temperature protection error.",
|
||||
6: "Power output high current protection error.",
|
||||
7: "Power output high current protection error.",
|
||||
8: "Power output high current protection error.",
|
||||
9: "Power output high voltage protection error.",
|
||||
},
|
||||
4: {
|
||||
0: "Power output low voltage protection error.",
|
||||
1: "Power output current imbalance error.",
|
||||
3: "Power input high temperature protection error.",
|
||||
4: "Power input high temperature protection error.",
|
||||
5: "Power input high temperature protection error.",
|
||||
6: "Power input high voltage protection error.",
|
||||
7: "Power input high voltage protection error.",
|
||||
8: "Power input high current protection error.",
|
||||
9: "Power input high current protection error.",
|
||||
},
|
||||
5: {
|
||||
0: "Power input low voltage protection error.",
|
||||
1: "Power input low voltage protection error.",
|
||||
3: "Power supply fan error.",
|
||||
4: "Power supply fan error.",
|
||||
5: "Power output high power protection error.",
|
||||
6: "Power output high power protection error.",
|
||||
7: "Input over current protection of power supply on primary side.",
|
||||
},
|
||||
6: {
|
||||
3: "Power communication warning.",
|
||||
4: "Power communication error.",
|
||||
7: "Power watchdog protection.",
|
||||
8: "Power output high current protection.",
|
||||
9: "Power input high current protection.",
|
||||
},
|
||||
7: {
|
||||
0: "Power input high voltage protection.",
|
||||
1: "Power input low voltage protection.",
|
||||
2: "Excessive power supply output warning.",
|
||||
3: "Power input too high warning.",
|
||||
4: "Power fan warning.",
|
||||
5: "Power high temperature warning.",
|
||||
},
|
||||
},
|
||||
3: { # temperature error
|
||||
0: { # sensor detection error
|
||||
"n": "Slot {n} temperature sensor detection error."
|
||||
},
|
||||
2: { # temperature reading error
|
||||
"n": "Slot {n} temperature reading error.",
|
||||
9: "Control board temperature sensor communication error.",
|
||||
},
|
||||
5: {"n": "Slot {n} temperature protecting."}, # temperature protection
|
||||
6: {0: "Hashboard high temperature error."}, # high temp
|
||||
},
|
||||
4: { # EEPROM error
|
||||
1: {"n": "Slot {n} eeprom detection error."}, # EEPROM detection error
|
||||
2: {"n": "Slot {n} eeprom parsing error."}, # EEPROM parsing error
|
||||
3: {"n": "Slot {n} chip bin type error."}, # chip bin error
|
||||
4: {"n": "Slot {n} eeprom chip number X error."}, # EEPROM chip number error
|
||||
5: {"n": "Slot {n} eeprom xfer error."}, # EEPROM xfer error
|
||||
},
|
||||
5: { # hashboard error
|
||||
1: {"n": "Slot {n} miner type error."}, # board miner type error
|
||||
2: {"n": "Slot {n} bin type error."}, # chip bin type error
|
||||
3: {"n": "Slot {n} not found."}, # board not found error
|
||||
4: {"n": "Slot {n} error reading chip id."}, # reading chip id error
|
||||
5: {"n": "Slot {n} has bad chips."}, # board has bad chips error
|
||||
6: {"n": "Slot {n} loss of balance error."}, # loss of balance error
|
||||
},
|
||||
6: { # env temp error
|
||||
0: {0: "Environment temperature is too high."}, # normal env temp error
|
||||
1: { # high power env temp error
|
||||
0: "Environment temperature is too high for high performance mode."
|
||||
},
|
||||
},
|
||||
7: { # control board error
|
||||
0: {1: "Control board no support chip."},
|
||||
1: {
|
||||
0: "Control board rebooted as an exception.",
|
||||
2: "Control board rebooted as an exception.",
|
||||
},
|
||||
},
|
||||
8: { # checksum error
|
||||
0: {
|
||||
0: "CGMiner checksum error.",
|
||||
1: "System monitor checksum error.",
|
||||
2: "Remote daemon checksum error.",
|
||||
}
|
||||
},
|
||||
20: { # pool error
|
||||
1: {0: "All pools are disabled."}, # all disabled error
|
||||
2: {"n": "Pool {n} connection failed."}, # pool connection failed error
|
||||
3: {0: "High rejection rate on pool."}, # rejection rate error
|
||||
4: { # asicboost not supported error
|
||||
0: "The pool does not support asicboost mode."
|
||||
},
|
||||
},
|
||||
23: { # hashrate error
|
||||
1: {0: "Hashrate is too low."},
|
||||
2: {0: "Hashrate is too low."},
|
||||
3: {0: "Hashrate loss is too high."},
|
||||
4: {0: "Hashrate loss is too high."},
|
||||
},
|
||||
50: { # water velocity error
|
||||
7: {"n": "Slot {n} water velocity is abnormal."}, # abnormal water velocity
|
||||
},
|
||||
51: { # frequency error
|
||||
7: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
|
||||
},
|
||||
84: {
|
||||
1: {0: "Software version error."},
|
||||
},
|
||||
1000: {
|
||||
0: {
|
||||
1: "/antiv/signature illegal.",
|
||||
2: "/antiv/dig/init.d illegal.",
|
||||
3: "/antiv/dig/pf_partial.dig illegal.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ from typing import Union, List
|
||||
from pyasic.API.bmminer import BMMinerAPI
|
||||
from pyasic.miners.base import BaseMiner
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
|
||||
@@ -185,13 +185,17 @@ class BMMiner(BaseMiner):
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
async def get_data(self, allow_warning: bool = False) -> MinerData:
|
||||
"""Get data from the miner.
|
||||
|
||||
Returns:
|
||||
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||
"""
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
)
|
||||
|
||||
board_offset = -1
|
||||
fan_offset = -1
|
||||
@@ -219,7 +223,7 @@ class BMMiner(BaseMiner):
|
||||
miner_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"summary", "pools", "stats", ignore_x19_error=True
|
||||
"summary", "pools", "stats", allow_warning=allow_warning
|
||||
)
|
||||
if miner_data:
|
||||
break
|
||||
@@ -242,7 +246,7 @@ class BMMiner(BaseMiner):
|
||||
if stats:
|
||||
boards = stats.get("STATS")
|
||||
if boards:
|
||||
if len(boards) > 0:
|
||||
if len(boards) > 1:
|
||||
for board_num in range(1, 16, 5):
|
||||
for _b_num in range(5):
|
||||
b = boards[1].get(f"chain_acn{board_num + _b_num}")
|
||||
@@ -252,30 +256,38 @@ class BMMiner(BaseMiner):
|
||||
if board_offset == -1:
|
||||
board_offset = 1
|
||||
|
||||
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
|
||||
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
|
||||
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
|
||||
env_temp_list = []
|
||||
for i in range(board_offset, board_offset + self.ideal_hashboards):
|
||||
hashboard = HashBoard(
|
||||
slot=i - board_offset, expected_chips=self.nominal_chips
|
||||
)
|
||||
|
||||
try:
|
||||
data.left_board_hashrate = round(
|
||||
float(boards[1].get(f"chain_rate{board_offset}")) / 1000, 2
|
||||
)
|
||||
except ValueError as e:
|
||||
data.left_board_hashrate = round(0.00, 2)
|
||||
try:
|
||||
data.center_board_hashrate = round(
|
||||
float(boards[1].get(f"chain_rate{board_offset+1}")) / 1000,
|
||||
2,
|
||||
)
|
||||
except ValueError as e:
|
||||
data.center_board_hashrate = round(0.00, 2)
|
||||
try:
|
||||
data.right_board_hashrate = round(
|
||||
float(boards[1].get(f"chain_rate{board_offset+2}")) / 1000,
|
||||
2,
|
||||
)
|
||||
except ValueError as e:
|
||||
data.right_board_hashrate = round(0.00, 2)
|
||||
chip_temp = boards[1].get(f"temp{i}")
|
||||
if chip_temp:
|
||||
hashboard.chip_temp = round(chip_temp)
|
||||
|
||||
temp = boards[1].get(f"temp2_{i}")
|
||||
if temp:
|
||||
hashboard.temp = round(temp)
|
||||
|
||||
hashrate = boards[1].get(f"chain_rate{i}")
|
||||
if hashrate:
|
||||
hashboard.hashrate = round(float(hashrate) / 1000, 2)
|
||||
|
||||
chips = boards[1].get(f"chain_acn{i}")
|
||||
if chips:
|
||||
hashboard.chips = chips
|
||||
hashboard.missing = False
|
||||
if (not chips) or (not chips > 0):
|
||||
hashboard.missing = True
|
||||
data.hashboards.append(hashboard)
|
||||
|
||||
if f"temp_pcb{i}" in boards[1].keys():
|
||||
env_temp = boards[1][f"temp_pcb{i}"].split("-")[0]
|
||||
if not env_temp == 0:
|
||||
env_temp_list.append(int(env_temp))
|
||||
if not env_temp_list == []:
|
||||
data.env_temp = round(sum(env_temp_list) / len(env_temp_list))
|
||||
|
||||
if stats:
|
||||
temp = stats.get("STATS")
|
||||
@@ -293,20 +305,6 @@ class BMMiner(BaseMiner):
|
||||
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
|
||||
)
|
||||
|
||||
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
|
||||
env_temp_list = []
|
||||
for item in range(3):
|
||||
board_temp = temp[1].get(f"temp{item + board_offset}")
|
||||
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
|
||||
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
|
||||
setattr(data, f"{board_map[item]}_temp", board_temp)
|
||||
if f"temp_pcb{item}" in temp[1].keys():
|
||||
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
|
||||
if not env_temp == 0:
|
||||
env_temp_list.append(int(env_temp))
|
||||
if not env_temp_list == []:
|
||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
|
||||
@@ -25,7 +25,7 @@ from pyasic.API.bosminer import BOSMinerAPI
|
||||
from pyasic.errors import APIError
|
||||
|
||||
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
|
||||
@@ -266,7 +266,10 @@ class BOSMiner(BaseMiner):
|
||||
logging.warning(e)
|
||||
|
||||
if tunerstatus:
|
||||
tuner = tunerstatus[0].get("TUNERSTATUS")
|
||||
try:
|
||||
tuner = tunerstatus[0].get("TUNERSTATUS")
|
||||
except KeyError:
|
||||
tuner = tunerstatus.get("TUNERSTATUS")
|
||||
if tuner:
|
||||
if len(tuner) > 0:
|
||||
chain_status = tuner[0].get("TunerChainStatus")
|
||||
@@ -294,13 +297,21 @@ class BOSMiner(BaseMiner):
|
||||
)
|
||||
return errors
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
async def get_data(self, allow_warning: bool = True) -> MinerData:
|
||||
"""Get data from the miner.
|
||||
|
||||
Returns:
|
||||
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||
"""
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
hashboards=[
|
||||
HashBoard(slot=i, expected_chips=self.nominal_chips)
|
||||
for i in range(self.ideal_hashboards)
|
||||
],
|
||||
)
|
||||
|
||||
board_offset = -1
|
||||
fan_offset = -1
|
||||
@@ -331,6 +342,7 @@ class BOSMiner(BaseMiner):
|
||||
"devdetails",
|
||||
"fans",
|
||||
"devs",
|
||||
allow_warning=allow_warning
|
||||
)
|
||||
except APIError as e:
|
||||
if str(e.message) == "Not ready":
|
||||
@@ -361,14 +373,13 @@ class BOSMiner(BaseMiner):
|
||||
temp = temps[0].get("TEMPS")
|
||||
if temp:
|
||||
if len(temp) > 0:
|
||||
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
|
||||
offset = 6 if temp[0]["ID"] in [6, 7, 8] else temp[0]["ID"]
|
||||
for board in temp:
|
||||
_id = board["ID"] - offset
|
||||
chip_temp = round(board["Chip"])
|
||||
board_temp = round(board["Board"])
|
||||
setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
|
||||
setattr(data, f"{board_map[_id]}_temp", board_temp)
|
||||
data.hashboards[_id].chip_temp = chip_temp
|
||||
data.hashboards[_id].temp = board_temp
|
||||
|
||||
if fans:
|
||||
fan_data = fans[0].get("FANS")
|
||||
@@ -461,27 +472,25 @@ class BOSMiner(BaseMiner):
|
||||
boards = devdetails[0].get("DEVDETAILS")
|
||||
if boards:
|
||||
if len(boards) > 0:
|
||||
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
|
||||
offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"]
|
||||
for board in boards:
|
||||
_id = board["ID"] - offset
|
||||
chips = board["Chips"]
|
||||
setattr(data, board_map[_id], chips)
|
||||
data.hashboards[_id].chips = chips
|
||||
if chips > 0:
|
||||
data.hashboards[_id].missing = False
|
||||
else:
|
||||
data.hashboards[_id].missing = True
|
||||
|
||||
if devs:
|
||||
boards = devs[0].get("DEVS")
|
||||
if boards:
|
||||
if len(boards) > 0:
|
||||
board_map = {
|
||||
0: "left_board_hashrate",
|
||||
1: "center_board_hashrate",
|
||||
2: "right_board_hashrate",
|
||||
}
|
||||
offset = 6 if boards[0]["ID"] in [6, 7, 8] else boards[0]["ID"]
|
||||
for board in boards:
|
||||
_id = board["ID"] - offset
|
||||
hashrate = round(board["MHS 1m"] / 1000000, 2)
|
||||
setattr(data, board_map[_id], hashrate)
|
||||
data.hashboards[_id].hashrate = hashrate
|
||||
return data
|
||||
|
||||
async def get_mac(self):
|
||||
|
||||
@@ -21,7 +21,7 @@ from pyasic.API.btminer import BTMinerAPI
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.errors import APIError
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.data.error_codes import WhatsminerError, MinerErrorData
|
||||
from pyasic.config import MinerConfig
|
||||
|
||||
@@ -109,7 +109,6 @@ class BTMiner(BaseMiner):
|
||||
if "Code" in data.keys():
|
||||
if data["Code"] == 131:
|
||||
return True
|
||||
print(data)
|
||||
return False
|
||||
|
||||
async def check_light(self) -> bool:
|
||||
@@ -158,14 +157,31 @@ 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.get("SUMMARY"):
|
||||
summary_data = summary_data["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:
|
||||
@@ -225,13 +241,17 @@ class BTMiner(BaseMiner):
|
||||
cfg = cfg.from_api(pools["POOLS"])
|
||||
return cfg
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
async def get_data(self, allow_warning: bool = True) -> MinerData:
|
||||
"""Get data from the miner.
|
||||
|
||||
Returns:
|
||||
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||
"""
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
)
|
||||
|
||||
mac = None
|
||||
|
||||
@@ -260,18 +280,25 @@ class BTMiner(BaseMiner):
|
||||
miner_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
try:
|
||||
miner_data = await self.api.multicommand("summary", "devs", "pools")
|
||||
miner_data = await self.api.multicommand("summary", "devs", "pools", allow_warning=allow_warning)
|
||||
if miner_data:
|
||||
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 +314,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,39 +336,47 @@ 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}"):
|
||||
data.errors.append(
|
||||
WhatsminerError(
|
||||
error_code=summary_data[0][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=int(code))
|
||||
)
|
||||
else:
|
||||
data.errors.append(WhatsminerError(error_code=int(err)))
|
||||
|
||||
if devs:
|
||||
temp_data = devs.get("DEVS")
|
||||
if temp_data:
|
||||
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
|
||||
for board in temp_data:
|
||||
_id = board["ASC"]
|
||||
chip_temp = round(board["Chip Temp Avg"])
|
||||
board_temp = round(board["Temperature"])
|
||||
hashrate = round(board["MHS 1m"] / 1000000, 2)
|
||||
setattr(data, f"{board_map[_id]}_chip_temp", chip_temp)
|
||||
setattr(data, f"{board_map[_id]}_temp", board_temp)
|
||||
setattr(data, f"{board_map[_id]}_hashrate", hashrate)
|
||||
|
||||
if devs:
|
||||
boards = devs.get("DEVS")
|
||||
if boards:
|
||||
if len(boards) > 0:
|
||||
board_map = {0: "left_chips", 1: "center_chips", 2: "right_chips"}
|
||||
if "ID" in boards[0].keys():
|
||||
id_key = "ID"
|
||||
else:
|
||||
id_key = "ASC"
|
||||
offset = boards[0][id_key]
|
||||
for board in boards:
|
||||
_id = board[id_key] - offset
|
||||
chips = board["Effective Chips"]
|
||||
setattr(data, board_map[_id], chips)
|
||||
dev_data = devs.get("DEVS")
|
||||
if dev_data:
|
||||
for board in dev_data:
|
||||
temp_board = HashBoard(
|
||||
slot=board["ASC"],
|
||||
chip_temp=round(board["Chip Temp Avg"]),
|
||||
temp=round(board["Temperature"]),
|
||||
hashrate=round(board["MHS 1m"] / 1000000, 2),
|
||||
chips=board["Effective Chips"],
|
||||
missing=False if board["Effective Chips"] > 0 else True,
|
||||
expected_chips=self.nominal_chips,
|
||||
)
|
||||
data.hashboards.append(temp_board)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
|
||||
@@ -22,7 +22,7 @@ from pyasic.miners.base import BaseMiner
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.config import MinerConfig
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
|
||||
from pyasic.settings import PyasicSettings
|
||||
@@ -173,13 +173,17 @@ class CGMiner(BaseMiner):
|
||||
async def get_mac(self) -> str:
|
||||
return "00:00:00:00:00:00"
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
async def get_data(self, allow_warning: bool = False) -> MinerData:
|
||||
"""Get data from the miner.
|
||||
|
||||
Returns:
|
||||
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||
"""
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
)
|
||||
|
||||
board_offset = -1
|
||||
fan_offset = -1
|
||||
@@ -202,7 +206,7 @@ class CGMiner(BaseMiner):
|
||||
miner_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"summary", "pools", "stats", ignore_x19_error=True
|
||||
"summary", "pools", "stats", allow_warning=allow_warning
|
||||
)
|
||||
if miner_data:
|
||||
break
|
||||
@@ -235,19 +239,38 @@ class CGMiner(BaseMiner):
|
||||
if board_offset == -1:
|
||||
board_offset = 1
|
||||
|
||||
data.left_chips = boards[1].get(f"chain_acn{board_offset}")
|
||||
data.center_chips = boards[1].get(f"chain_acn{board_offset+1}")
|
||||
data.right_chips = boards[1].get(f"chain_acn{board_offset+2}")
|
||||
env_temp_list = []
|
||||
for i in range(board_offset, board_offset + self.ideal_hashboards):
|
||||
hashboard = HashBoard(
|
||||
slot=i - board_offset, expected_chips=self.nominal_chips
|
||||
)
|
||||
|
||||
data.left_board_hashrate = round(
|
||||
float(boards[1].get(f"chain_rate{board_offset}")) / 1000, 2
|
||||
)
|
||||
data.center_board_hashrate = round(
|
||||
float(boards[1].get(f"chain_rate{board_offset+1}")) / 1000, 2
|
||||
)
|
||||
data.right_board_hashrate = round(
|
||||
float(boards[1].get(f"chain_rate{board_offset+2}")) / 1000, 2
|
||||
)
|
||||
chip_temp = boards[1].get(f"temp{i}")
|
||||
if chip_temp:
|
||||
hashboard.chip_temp = round(chip_temp)
|
||||
|
||||
temp = boards[1].get(f"temp2_{i}")
|
||||
if temp:
|
||||
hashboard.temp = round(temp)
|
||||
|
||||
hashrate = boards[1].get(f"chain_rate{i}")
|
||||
if hashrate:
|
||||
hashboard.hashrate = round(float(hashrate) / 1000, 2)
|
||||
|
||||
chips = boards[1].get(f"chain_acn{i}")
|
||||
if chips:
|
||||
hashboard.chips = chips
|
||||
hashboard.missing = False
|
||||
if (not chips) or (not chips > 0):
|
||||
hashboard.missing = True
|
||||
data.hashboards.append(hashboard)
|
||||
|
||||
if f"temp_pcb{i}" in boards[1].keys():
|
||||
env_temp = boards[1][f"temp_pcb{i}"].split("-")[0]
|
||||
if not env_temp == 0:
|
||||
env_temp_list.append(int(env_temp))
|
||||
if not env_temp_list == []:
|
||||
data.env_temp = round(sum(env_temp_list) / len(env_temp_list))
|
||||
|
||||
if stats:
|
||||
temp = stats.get("STATS")
|
||||
@@ -265,19 +288,6 @@ class CGMiner(BaseMiner):
|
||||
data, f"fan_{fan + 1}", temp[1].get(f"fan{fan_offset+fan}")
|
||||
)
|
||||
|
||||
board_map = {0: "left_board", 1: "center_board", 2: "right_board"}
|
||||
env_temp_list = []
|
||||
for item in range(3):
|
||||
board_temp = temp[1].get(f"temp{item + board_offset}")
|
||||
chip_temp = temp[1].get(f"temp2_{item + board_offset}")
|
||||
setattr(data, f"{board_map[item]}_chip_temp", chip_temp)
|
||||
setattr(data, f"{board_map[item]}_temp", board_temp)
|
||||
if f"temp_pcb{item}" in temp[1].keys():
|
||||
env_temp = temp[1][f"temp_pcb{item}"].split("-")[0]
|
||||
if not env_temp == 0:
|
||||
env_temp_list.append(int(env_temp))
|
||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
|
||||
35
pyasic/miners/_types/whatsminer/M3X/M34S_Plus.py
Normal file
35
pyasic/miners/_types/whatsminer/M3X/M34S_Plus.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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 M34SPlus(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M34S+"
|
||||
self.ideal_hashboards = 4
|
||||
self.nominal_chips = 116
|
||||
self.fan_count = 0
|
||||
|
||||
|
||||
class M34SPlusVE10(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M34S+ VE10"
|
||||
self.ideal_hashboards = 4
|
||||
self.nominal_chips = 116
|
||||
self.fan_count = 0
|
||||
@@ -34,3 +34,4 @@ from .M31S_Plus import (
|
||||
|
||||
from .M32 import M32, M32V20
|
||||
from .M32S import M32S
|
||||
from .M34S_Plus import M34SPlus, M34SPlusVE10
|
||||
|
||||
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 *
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from pyasic.miners._backends import Hiveon # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import T9 # noqa - Ignore access to _module
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.settings import PyasicSettings
|
||||
|
||||
|
||||
@@ -33,13 +33,17 @@ class HiveonT9(Hiveon, T9):
|
||||
)
|
||||
return mac
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
async def get_data(self, allow_warning: bool = False) -> MinerData:
|
||||
"""Get data from the miner.
|
||||
|
||||
Returns:
|
||||
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||
"""
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
)
|
||||
|
||||
board_offset = -1
|
||||
fan_offset = -1
|
||||
@@ -67,7 +71,7 @@ class HiveonT9(Hiveon, T9):
|
||||
miner_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"summary", "pools", "stats", ignore_x19_error=True
|
||||
"summary", "pools", "stats", allow_warning=allow_warning
|
||||
)
|
||||
if miner_data:
|
||||
break
|
||||
@@ -97,14 +101,17 @@ class HiveonT9(Hiveon, T9):
|
||||
)
|
||||
|
||||
board_map = {
|
||||
"left": [2, 9, 10],
|
||||
"center": [3, 11, 12],
|
||||
"right": [4, 13, 14],
|
||||
0: [2, 9, 10],
|
||||
1: [3, 11, 12],
|
||||
2: [4, 13, 14],
|
||||
}
|
||||
|
||||
env_temp_list = []
|
||||
|
||||
for board in board_map.keys():
|
||||
hashboard = HashBoard(
|
||||
slot=board, expected_chips=self.nominal_chips
|
||||
)
|
||||
chips = 0
|
||||
hashrate = 0
|
||||
chip_temp = 0
|
||||
@@ -121,10 +128,15 @@ class HiveonT9(Hiveon, T9):
|
||||
|
||||
hashrate += boards[1][f"chain_rate{chipset}"]
|
||||
chips += boards[1][f"chain_acn{chipset}"]
|
||||
setattr(data, f"{board}_chips", chips)
|
||||
setattr(data, f"{board}_board_hashrate", hashrate)
|
||||
setattr(data, f"{board}_board_temp", board_temp)
|
||||
setattr(data, f"{board}_board_chip_temp", chip_temp)
|
||||
hashboard.hashrate = hashrate
|
||||
hashboard.chips = chips
|
||||
hashboard.temp = board_temp
|
||||
hashboard.chip_temp = chip_temp
|
||||
hashboard.missing = True
|
||||
if chips and chips > 0:
|
||||
hashboard.missing = False
|
||||
data.hashboards.append(hashboard)
|
||||
|
||||
if not env_temp_list == []:
|
||||
data.env_temp = sum(env_temp_list) / len(env_temp_list)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.settings import PyasicSettings
|
||||
import re
|
||||
from pyasic.config import MinerConfig
|
||||
@@ -80,8 +80,16 @@ class CGMinerA10X(CGMiner):
|
||||
)
|
||||
return mac
|
||||
|
||||
async def get_data(self):
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
async def get_data(self, allow_warning: bool = True):
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
hashboards=[
|
||||
HashBoard(slot=i, expected_chips=self.nominal_chips)
|
||||
for i in range(self.ideal_hashboards)
|
||||
],
|
||||
)
|
||||
|
||||
model = await self.get_model()
|
||||
mac = None
|
||||
@@ -92,7 +100,7 @@ class CGMinerA10X(CGMiner):
|
||||
miner_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"version", "summary", "pools", "stats"
|
||||
"version", "summary", "pools", "stats", allow_warning=allow_warning
|
||||
)
|
||||
if miner_data:
|
||||
break
|
||||
@@ -145,27 +153,20 @@ class CGMinerA10X(CGMiner):
|
||||
f"fan_{fan+1}",
|
||||
int(raw_data[f"Fan{fan+1}"]),
|
||||
)
|
||||
if "MTmax" in raw_data.keys():
|
||||
data.left_board_chip_temp = int(raw_data["MTmax"][0])
|
||||
data.center_board_chip_temp = int(raw_data["MTmax"][1])
|
||||
data.right_board_chip_temp = int(raw_data["MTmax"][2])
|
||||
if "MTavg" in raw_data.keys():
|
||||
data.left_board_temp = int(raw_data["MTavg"][0])
|
||||
data.center_board_temp = int(raw_data["MTavg"][1])
|
||||
data.right_board_temp = int(raw_data["MTavg"][2])
|
||||
for board in range(self.ideal_hashboards):
|
||||
chip_temp = raw_data.get("MTmax")
|
||||
if chip_temp:
|
||||
data.hashboards[board].chip_temp = chip_temp[board]
|
||||
|
||||
if "PVT_T0" in raw_data:
|
||||
data.left_chips = len(
|
||||
[item for item in raw_data["PVT_T0"] if not item == "0"]
|
||||
)
|
||||
if "PVT_T1" in raw_data:
|
||||
data.center_chips = len(
|
||||
[item for item in raw_data["PVT_T1"] if not item == "0"]
|
||||
)
|
||||
if "PVT_T2" in raw_data:
|
||||
data.right_chips = len(
|
||||
[item for item in raw_data["PVT_T2"] if not item == "0"]
|
||||
)
|
||||
temp = raw_data.get("MTavg")
|
||||
if temp:
|
||||
data.hashboards[board].temp = temp[board]
|
||||
|
||||
chips = raw_data.get(f"PVT_T{board}")
|
||||
if chips:
|
||||
data.hashboards[board].chips = len(
|
||||
[item for item in chips if not item == "0"]
|
||||
)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.settings import PyasicSettings
|
||||
import re
|
||||
from pyasic.config import MinerConfig
|
||||
@@ -80,8 +80,16 @@ class CGMinerA7X(CGMiner):
|
||||
)
|
||||
return mac
|
||||
|
||||
async def get_data(self):
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
async def get_data(self, allow_warning: bool = True):
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
hashboards=[
|
||||
HashBoard(slot=i, expected_chips=self.nominal_chips)
|
||||
for i in range(self.ideal_hashboards)
|
||||
],
|
||||
)
|
||||
|
||||
model = await self.get_model()
|
||||
mac = None
|
||||
@@ -92,7 +100,7 @@ class CGMinerA7X(CGMiner):
|
||||
miner_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"version", "summary", "pools", "stats"
|
||||
"version", "summary", "pools", "stats", allow_warning=allow_warning
|
||||
)
|
||||
if miner_data:
|
||||
break
|
||||
@@ -145,27 +153,20 @@ class CGMinerA7X(CGMiner):
|
||||
f"fan_{fan+1}",
|
||||
int(raw_data[f"Fan{fan+1}"]),
|
||||
)
|
||||
if "MTmax" in raw_data.keys():
|
||||
data.left_board_chip_temp = int(raw_data["MTmax"][0])
|
||||
data.center_board_chip_temp = int(raw_data["MTmax"][1])
|
||||
data.right_board_chip_temp = int(raw_data["MTmax"][2])
|
||||
if "MTavg" in raw_data.keys():
|
||||
data.left_board_temp = int(raw_data["MTavg"][0])
|
||||
data.center_board_temp = int(raw_data["MTavg"][1])
|
||||
data.right_board_temp = int(raw_data["MTavg"][2])
|
||||
for board in range(self.ideal_hashboards):
|
||||
chip_temp = raw_data.get("MTmax")
|
||||
if chip_temp:
|
||||
data.hashboards[board].chip_temp = chip_temp[board]
|
||||
|
||||
if "PVT_T0" in raw_data:
|
||||
data.left_chips = len(
|
||||
[item for item in raw_data["PVT_T0"] if not item == "0"]
|
||||
)
|
||||
if "PVT_T1" in raw_data:
|
||||
data.center_chips = len(
|
||||
[item for item in raw_data["PVT_T1"] if not item == "0"]
|
||||
)
|
||||
if "PVT_T2" in raw_data:
|
||||
data.right_chips = len(
|
||||
[item for item in raw_data["PVT_T2"] if not item == "0"]
|
||||
)
|
||||
temp = raw_data.get("MTavg")
|
||||
if temp:
|
||||
data.hashboards[board].temp = temp[board]
|
||||
|
||||
chips = raw_data.get(f"PVT_T{board}")
|
||||
if chips:
|
||||
data.hashboards[board].chips = len(
|
||||
[item for item in chips if not item == "0"]
|
||||
)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.settings import PyasicSettings
|
||||
import re
|
||||
from pyasic.config import MinerConfig
|
||||
@@ -80,8 +80,16 @@ class CGMinerA8X(CGMiner):
|
||||
)
|
||||
return mac
|
||||
|
||||
async def get_data(self):
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
async def get_data(self, allow_warning: bool = True):
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
hashboards=[
|
||||
HashBoard(slot=i, expected_chips=self.nominal_chips)
|
||||
for i in range(self.ideal_hashboards)
|
||||
],
|
||||
)
|
||||
|
||||
model = await self.get_model()
|
||||
mac = None
|
||||
@@ -92,7 +100,7 @@ class CGMinerA8X(CGMiner):
|
||||
miner_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"version", "summary", "pools", "stats"
|
||||
"version", "summary", "pools", "stats", allow_warning=allow_warning
|
||||
)
|
||||
if miner_data:
|
||||
break
|
||||
@@ -145,27 +153,20 @@ class CGMinerA8X(CGMiner):
|
||||
f"fan_{fan+1}",
|
||||
int(raw_data[f"Fan{fan+1}"]),
|
||||
)
|
||||
if "MTmax" in raw_data.keys():
|
||||
data.left_board_chip_temp = int(raw_data["MTmax"][0])
|
||||
data.center_board_chip_temp = int(raw_data["MTmax"][1])
|
||||
data.right_board_chip_temp = int(raw_data["MTmax"][2])
|
||||
if "MTavg" in raw_data.keys():
|
||||
data.left_board_temp = int(raw_data["MTavg"][0])
|
||||
data.center_board_temp = int(raw_data["MTavg"][1])
|
||||
data.right_board_temp = int(raw_data["MTavg"][2])
|
||||
for board in range(self.ideal_hashboards):
|
||||
chip_temp = raw_data.get("MTmax")
|
||||
if chip_temp:
|
||||
data.hashboards[board].chip_temp = chip_temp[board]
|
||||
|
||||
if "PVT_T0" in raw_data:
|
||||
data.left_chips = len(
|
||||
[item for item in raw_data["PVT_T0"] if not item == "0"]
|
||||
)
|
||||
if "PVT_T1" in raw_data:
|
||||
data.center_chips = len(
|
||||
[item for item in raw_data["PVT_T1"] if not item == "0"]
|
||||
)
|
||||
if "PVT_T2" in raw_data:
|
||||
data.right_chips = len(
|
||||
[item for item in raw_data["PVT_T2"] if not item == "0"]
|
||||
)
|
||||
temp = raw_data.get("MTavg")
|
||||
if temp:
|
||||
data.hashboards[board].temp = temp[board]
|
||||
|
||||
chips = raw_data.get(f"PVT_T{board}")
|
||||
if chips:
|
||||
data.hashboards[board].chips = len(
|
||||
[item for item in chips if not item == "0"]
|
||||
)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import Avalon921 # noqa - Ignore access to _module
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.settings import PyasicSettings
|
||||
import re
|
||||
from pyasic.config import MinerConfig
|
||||
@@ -81,8 +81,16 @@ class CGMinerAvalon921(CGMiner, Avalon921):
|
||||
)
|
||||
return mac
|
||||
|
||||
async def get_data(self):
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
async def get_data(self, allow_warning: bool = True):
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
hashboards=[
|
||||
HashBoard(slot=i, expected_chips=self.nominal_chips)
|
||||
for i in range(self.ideal_hashboards)
|
||||
],
|
||||
)
|
||||
|
||||
model = await self.get_model()
|
||||
mac = None
|
||||
@@ -95,7 +103,7 @@ class CGMinerAvalon921(CGMiner, Avalon921):
|
||||
miner_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"version", "summary", "pools", "stats"
|
||||
"version", "summary", "pools", "stats", allow_warning=allow_warning
|
||||
)
|
||||
if miner_data:
|
||||
break
|
||||
@@ -146,27 +154,20 @@ class CGMinerAvalon921(CGMiner, Avalon921):
|
||||
f"fan_{fan+1}",
|
||||
int(raw_data[f"Fan{fan+1}"]),
|
||||
)
|
||||
if "MTmax" in raw_data.keys():
|
||||
data.left_board_chip_temp = int(raw_data["MTmax"][0])
|
||||
data.center_board_chip_temp = int(raw_data["MTmax"][1])
|
||||
data.right_board_chip_temp = int(raw_data["MTmax"][2])
|
||||
if "MTavg" in raw_data.keys():
|
||||
data.left_board_temp = int(raw_data["MTavg"][0])
|
||||
data.center_board_temp = int(raw_data["MTavg"][1])
|
||||
data.right_board_temp = int(raw_data["MTavg"][2])
|
||||
for board in range(self.ideal_hashboards):
|
||||
chip_temp = raw_data.get("MTmax")
|
||||
if chip_temp:
|
||||
data.hashboards[board].chip_temp = chip_temp[board]
|
||||
|
||||
if "PVT_T0" in raw_data:
|
||||
data.left_chips = len(
|
||||
[item for item in raw_data["PVT_T0"] if not item == "0"]
|
||||
)
|
||||
if "PVT_T1" in raw_data:
|
||||
data.center_chips = len(
|
||||
[item for item in raw_data["PVT_T1"] if not item == "0"]
|
||||
)
|
||||
if "PVT_T2" in raw_data:
|
||||
data.right_chips = len(
|
||||
[item for item in raw_data["PVT_T2"] if not item == "0"]
|
||||
)
|
||||
temp = raw_data.get("MTavg")
|
||||
if temp:
|
||||
data.hashboards[board].temp = temp[board]
|
||||
|
||||
chips = raw_data.get(f"PVT_T{board}")
|
||||
if chips:
|
||||
data.hashboards[board].chips = len(
|
||||
[item for item in chips if not item == "0"]
|
||||
)
|
||||
|
||||
if pools:
|
||||
pool_1 = None
|
||||
|
||||
@@ -43,6 +43,7 @@ class BaseMiner(ABC):
|
||||
self.version = None
|
||||
self.fan_count = 2
|
||||
self.config = None
|
||||
self.ideal_hashboards = 3
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is BaseMiner:
|
||||
@@ -191,7 +192,7 @@ class BaseMiner(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_data(self) -> MinerData:
|
||||
async def get_data(self, allow_warning: bool = True) -> MinerData:
|
||||
"""Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData].
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import InnosiliconT3HPlus # noqa - Ignore access to _module
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data import MinerData, HashBoard
|
||||
from pyasic.data.error_codes import InnosiliconError, MinerErrorData
|
||||
from pyasic.settings import PyasicSettings
|
||||
from pyasic.config import MinerConfig
|
||||
@@ -157,8 +157,16 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
|
||||
errors.append(InnosiliconError(error_code=err))
|
||||
return errors
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
|
||||
async def get_data(self, allow_warning: bool = False) -> MinerData:
|
||||
data = MinerData(
|
||||
ip=str(self.ip),
|
||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||
ideal_hashboards=self.ideal_hashboards,
|
||||
hashboards=[
|
||||
HashBoard(slot=i, expected_chips=self.nominal_chips)
|
||||
for i in range(self.ideal_hashboards)
|
||||
],
|
||||
)
|
||||
|
||||
board_offset = -1
|
||||
fan_offset = -1
|
||||
@@ -179,7 +187,7 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
|
||||
all_data = None
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
miner_data = await self.api.multicommand(
|
||||
"summary", "pools", "stats", ignore_x19_error=True
|
||||
"summary", "pools", "stats", allow_warning=allow_warning
|
||||
)
|
||||
|
||||
if miner_data:
|
||||
@@ -215,32 +223,31 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
|
||||
if stats:
|
||||
stats = stats[0]
|
||||
if stats.get("STATS"):
|
||||
board_map = {0: "left", 1: "center", 2: "right"}
|
||||
for idx, board in enumerate(stats["STATS"]):
|
||||
data.hashboards[idx].missing = True
|
||||
chips = board.get("Num active chips")
|
||||
if chips:
|
||||
setattr(data, f"{board_map[idx]}_chips", chips)
|
||||
temp = board.get("Temp")
|
||||
if temp:
|
||||
setattr(data, f"{board_map[idx]}_board_chip_temp", temp)
|
||||
data.hashboards[idx].chips = chips
|
||||
if chips > 0:
|
||||
data.hashboards[idx].missing = False
|
||||
|
||||
if all_data:
|
||||
if all_data.get("chain"):
|
||||
board_map = {0: "left", 1: "center", 2: "right"}
|
||||
for idx, board in enumerate(all_data["chain"]):
|
||||
temp = board.get("Temp max")
|
||||
temp = board.get("Temp min")
|
||||
if temp:
|
||||
setattr(data, f"{board_map[idx]}_board_chip_temp", temp)
|
||||
temp_board = board.get("Temp min")
|
||||
if temp_board:
|
||||
setattr(data, f"{board_map[idx]}_board_temp", temp_board)
|
||||
hr = board.get("Hash Rate H")
|
||||
if hr:
|
||||
setattr(
|
||||
data,
|
||||
f"{board_map[idx]}_board_hashrate",
|
||||
round(hr / 1000000000000, 2),
|
||||
data.hashboards[idx].temp = round(temp)
|
||||
|
||||
hashrate = board.get("Hash Rate H")
|
||||
if hashrate:
|
||||
data.hashboards[idx].hashrate = round(
|
||||
hashrate / 1000000000000, 2
|
||||
)
|
||||
|
||||
chip_temp = board.get("Temp max")
|
||||
if chip_temp:
|
||||
data.hashboards[idx].chip_temp = round(chip_temp)
|
||||
|
||||
if all_data.get("fansSpeed"):
|
||||
speed = round((all_data["fansSpeed"] * 6000) / 100)
|
||||
for fan in range(self.fan_count):
|
||||
|
||||
@@ -216,6 +216,16 @@ MINER_CLASSES = {
|
||||
"BTMiner": BTMinerM32,
|
||||
"20": BTMinerM32V20,
|
||||
},
|
||||
"M34S+": {
|
||||
"Default": BTMinerM34SPlus,
|
||||
"BTMiner": BTMinerM34SPlus,
|
||||
"E10": BTMinerM34SPlusVE10,
|
||||
},
|
||||
"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)
|
||||
|
||||
|
||||
@@ -72,5 +72,5 @@ class UnknownMiner(BaseMiner):
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
return None
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
async def get_data(self, allow_warning: bool = False) -> MinerData:
|
||||
return MinerData(ip=str(self.ip))
|
||||
|
||||
31
pyasic/miners/whatsminer/btminer/M3X/M34S_Plus.py
Normal file
31
pyasic/miners/whatsminer/btminer/M3X/M34S_Plus.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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 (
|
||||
M34SPlus,
|
||||
M34SPlusVE10,
|
||||
) # noqa - Ignore access to _module
|
||||
|
||||
|
||||
class BTMinerM34SPlus(BTMiner, M34SPlus):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM34SPlusVE10(BTMiner, M34SPlusVE10):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
@@ -45,3 +45,4 @@ from .M31S_Plus import (
|
||||
|
||||
from .M32 import BTMinerM32, BTMinerM32V20
|
||||
from .M32S import BTMinerM32S
|
||||
from .M34S_Plus import BTMinerM34SPlus, BTMinerM34SPlusVE10
|
||||
|
||||
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.1"
|
||||
version = "0.20.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