Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dbab75cf4 | ||
|
|
a90ad3ba6e | ||
|
|
98a94ce4a6 | ||
|
|
f0a8b6e1c7 | ||
|
|
e07bd3bffb | ||
|
|
dcce944390 | ||
|
|
03ecd118a3 | ||
|
|
97c0331762 | ||
|
|
eda9804dea | ||
|
|
e94c81ce44 | ||
|
|
c95c58138e | ||
|
|
03c93b4de1 | ||
|
|
ff0d15c365 | ||
|
|
eadcb76d31 | ||
|
|
b7ce9288f8 | ||
|
|
e077a099d9 | ||
|
|
8542acfb01 | ||
|
|
0d80ce5a0e | ||
|
|
ddcafe0f2b | ||
|
|
ea195b34db | ||
|
|
7377cb0d26 | ||
|
|
24b66de971 | ||
|
|
62d664a14c | ||
|
|
03b9a90f68 | ||
|
|
fefe0324b9 | ||
|
|
62b14a78b7 | ||
|
|
0ff505bbb4 | ||
|
|
b6c8c930a2 | ||
|
|
903bb93c4e | ||
|
|
59667cf104 | ||
|
|
3fd1b41bec | ||
|
|
6569107f64 | ||
|
|
9d746a6dcb | ||
|
|
fce4c07c32 | ||
|
|
094857758a | ||
|
|
2a49b89849 | ||
|
|
4ecd135734 | ||
|
|
836defc216 | ||
|
|
f8f777b5b5 | ||
|
|
b15e0a7363 | ||
|
|
5c1d06f743 | ||
|
|
51de56feb3 | ||
|
|
256a4ac909 |
17
.coveragerc
Normal file
17
.coveragerc
Normal file
@@ -0,0 +1,17 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
# Skip @abstractmethod
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
def __repr__
|
||||
if self\.debug
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
@@ -23,3 +23,11 @@
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
<br>
|
||||
|
||||
## Innosilicon Error Codes
|
||||
::: pyasic.data.error_codes.InnosiliconError
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
148
docs/index.md
148
docs/index.md
@@ -101,3 +101,151 @@ async def gather_miner_data(): # define async scan function to allow awaiting
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(gather_miner_data())
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
## Controlling miners via pyasic
|
||||
Every miner class in pyasic must implement all the control functions defined in [`BaseMiner`][pyasic.miners.BaseMiner].
|
||||
|
||||
These functions are
|
||||
[`check_light`](#check-light),
|
||||
[`fault_light_off`](#fault-light-off),
|
||||
[`fault_light_on`](#fault-light-on),
|
||||
[`get_config`](#get-config),
|
||||
[`get_data`](#get-data),
|
||||
[`get_errors`](#get-errors),
|
||||
[`get_hostname`](#get-hostname),
|
||||
[`get_model`](#get-model),
|
||||
[`reboot`](#reboot),
|
||||
[`restart_backend`](#restart-backend), and
|
||||
[`stop_mining`](#stop-mining), and
|
||||
[`resume_mining`](#resume-mining), and
|
||||
[`send_config`](#send-config).
|
||||
|
||||
<br>
|
||||
|
||||
### Check Light
|
||||
::: pyasic.miners.BaseMiner.check_light
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Fault Light Off
|
||||
::: pyasic.miners.BaseMiner.fault_light_off
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Fault Light On
|
||||
::: pyasic.miners.BaseMiner.fault_light_on
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Get Config
|
||||
::: pyasic.miners.BaseMiner.get_config
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Get Data
|
||||
::: pyasic.miners.BaseMiner.get_data
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Get Errors
|
||||
::: pyasic.miners.BaseMiner.get_errors
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Get Hostname
|
||||
::: pyasic.miners.BaseMiner.get_hostname
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Get Model
|
||||
::: pyasic.miners.BaseMiner.get_model
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Reboot
|
||||
::: pyasic.miners.BaseMiner.reboot
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Restart Backend
|
||||
::: pyasic.miners.BaseMiner.restart_backend
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Stop Mining
|
||||
::: pyasic.miners.BaseMiner.stop_mining
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Resume Mining
|
||||
::: pyasic.miners.BaseMiner.resume_mining
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
### Send Config
|
||||
::: pyasic.miners.BaseMiner.send_config
|
||||
handler: python
|
||||
options:
|
||||
heading_level: 4
|
||||
|
||||
<br>
|
||||
|
||||
## [`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.
|
||||
|
||||
<br>
|
||||
|
||||
### [`MinerData`][pyasic.data.MinerData]
|
||||
|
||||
[`MinerData`][pyasic.data.MinerData] is a return from the [`get_data()`](#get-data) function, and is used to have a consistent dataset across all returns.
|
||||
|
||||
You can call [`MinerData.asdict()`][pyasic.data.MinerData.asdict] to get the dataclass as a dictionary, and there are many other helper functions contained in the class to convert to different data formats.
|
||||
|
||||
<br>
|
||||
|
||||
### [`MinerConfig`][pyasic.config.MinerConfig]
|
||||
|
||||
[`MinerConfig`][pyasic.config.MinerConfig] is pyasic's way to represent a configuration file from a miner.
|
||||
It is the return from [`get_config()`](#get-config).
|
||||
|
||||
Each miner has a unique way to convert the [`MinerConfig`][pyasic.config.MinerConfig] to their specific type, there are helper functions in the class.
|
||||
In most cases these helper functions should not be used, as [`send_config()`](#send-config) takes a [`MinerConfig`][pyasic.config.MinerConfig] and will do the conversion to the right type for you.
|
||||
|
||||
10
docs/miners/innosilicon/T3X.md
Normal file
10
docs/miners/innosilicon/T3X.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# pyasic
|
||||
## T3X Models
|
||||
|
||||
## T3H+
|
||||
|
||||
::: pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
@@ -4,89 +4,212 @@
|
||||
Supported miner types are here on this list. If your miner (or miner version) is not on this list, please feel free to [open an issue on GitHub](https://github.com/UpstreamData/pyasic/issues) to get it added.
|
||||
|
||||
##### pyasic currently supports the following miners and subtypes:
|
||||
* Braiins OS+ Devices:
|
||||
* X19 Series:
|
||||
* [S19][pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19]
|
||||
* [S19 Pro][pyasic.miners.antminer.bosminer.X19.S19_Pro.BOSMinerS19Pro]
|
||||
* [S19j][pyasic.miners.antminer.bosminer.X19.S19j.BOSMinerS19j]
|
||||
* [S19j Pro][pyasic.miners.antminer.bosminer.X19.S19j_Pro.BOSMinerS19jPro]
|
||||
* [T19][pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19]
|
||||
* X17 Series:
|
||||
* [S17][pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17]
|
||||
* [S17+][pyasic.miners.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus]
|
||||
* [S17 Pro][pyasic.miners.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro]
|
||||
* [S17e][pyasic.miners.antminer.bosminer.X17.S17e.BOSMinerS17e]
|
||||
* [T17][pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17]
|
||||
* [T17+][pyasic.miners.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus]
|
||||
* [T17e][pyasic.miners.antminer.bosminer.X17.T17e.BOSMinerT17e]
|
||||
* X9 Series:
|
||||
* [S9][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
|
||||
* [S9i][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
|
||||
* [S9j][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
|
||||
* Stock Firmware Whatsminers:
|
||||
* M3X Series:
|
||||
* [M30S][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30S]:
|
||||
* [VE10][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVE10]
|
||||
* [VG20][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVG20]
|
||||
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVE20]
|
||||
* [V50][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SV50]
|
||||
* [M30S+][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlus]:
|
||||
* [VF20][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVF20]
|
||||
* [VE40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVE40]
|
||||
* [VG60][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVG60]
|
||||
* [M30S++][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus]:
|
||||
* [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30]
|
||||
* [VG40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40]
|
||||
* [M31S][pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S]
|
||||
* [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]:
|
||||
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20]
|
||||
* [M32][pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32]
|
||||
* [V20][pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20]
|
||||
* [M32S][pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S]
|
||||
* M2X Series:
|
||||
* [M20][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20]:
|
||||
* [V10][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20V10]
|
||||
* [M20S][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S]:
|
||||
* [V10][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10]
|
||||
* [V20][pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV20]
|
||||
* [M20S+][pyasic.miners.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlus]
|
||||
* [M21][pyasic.miners.whatsminer.btminer.M2X.M21.BTMinerM21]
|
||||
* [M21S][pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21S]:
|
||||
* [V20][pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV20]
|
||||
* [V60][pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV60]
|
||||
* [M21S+][pyasic.miners.whatsminer.btminer.M2X.M21S_Plus.BTMinerM21SPlus]
|
||||
* Stock Firmware Antminers:
|
||||
* X19 Series:
|
||||
* [S19][pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19]
|
||||
* [S19 Pro][pyasic.miners.antminer.bmminer.X19.S19_Pro.BMMinerS19Pro]
|
||||
* [S19a][pyasic.miners.antminer.bmminer.X19.S19a.BMMinerS19a]
|
||||
* [S19j][pyasic.miners.antminer.bmminer.X19.S19j.BMMinerS19j]
|
||||
* [S19j Pro][pyasic.miners.antminer.bmminer.X19.S19j_Pro.BMMinerS19jPro]
|
||||
* [T19][pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19]
|
||||
* X17 Series:
|
||||
* [S17][pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17]
|
||||
* [S17+][pyasic.miners.antminer.bmminer.X17.S17_Plus.BMMinerS17Plus]
|
||||
* [S17 Pro][pyasic.miners.antminer.bmminer.X17.S17_Pro.BMMinerS17Pro]
|
||||
* [S17e][pyasic.miners.antminer.bmminer.X17.S17e.BMMinerS17e]
|
||||
* [T17][pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17]
|
||||
* [T17+][pyasic.miners.antminer.bmminer.X17.T17_Plus.BMMinerT17Plus]
|
||||
* [T17e][pyasic.miners.antminer.bmminer.X17.T17e.BMMinerT17e]
|
||||
* X9 Series:
|
||||
* [S9][pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9]
|
||||
* [S9i][pyasic.miners.antminer.bmminer.X9.S9i.BMMinerS9i]
|
||||
* [T9][pyasic.miners.antminer.bmminer.X9.T9.BMMinerT9]
|
||||
* Stock Firmware Avalonminers:
|
||||
* A7X Series:
|
||||
* [A721][pyasic.miners.avalonminer.cgminer.A7X.A721.CGMinerAvalon721]
|
||||
* [A741][pyasic.miners.avalonminer.cgminer.A7X.A741.CGMinerAvalon741]
|
||||
* [A761][pyasic.miners.avalonminer.cgminer.A7X.A761.CGMinerAvalon761]
|
||||
* A8X Series:
|
||||
* [A821][pyasic.miners.avalonminer.cgminer.A8X.A821.CGMinerAvalon821]
|
||||
* [A841][pyasic.miners.avalonminer.cgminer.A8X.A841.CGMinerAvalon841]
|
||||
* [A851][pyasic.miners.avalonminer.cgminer.A8X.A851.CGMinerAvalon851]
|
||||
* A9X Series:
|
||||
* [A921][pyasic.miners.avalonminer.cgminer.A9X.A921.CGMinerAvalon921]
|
||||
* A10X Series:
|
||||
* [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026]
|
||||
* [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047]
|
||||
* [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066]
|
||||
<details>
|
||||
<summary>Braiins OS+ Devices:</summary>
|
||||
<ul>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Stock Firmware Whatsminers:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary>M3X Series:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary><a href="/miners/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>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/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>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/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>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m31s">M31S</a></summary>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/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>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m32">M32</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M3X/#m32v20">V20</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M3X/#m32s">M32S</a></summary>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>M2X Series:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m20">M20</a></summary>
|
||||
<ul>
|
||||
<li><a href="/miners/whatsminer/M2X/#m20v10">V10</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/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>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m20s_1">M20S+</a></summary>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m21">M21</a></summary>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/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>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary><a href="/miners/whatsminer/M2X/#m21s_1">M21S+</a></summary>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Stock Firmware Antminers:</summary>
|
||||
<ul>
|
||||
<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/#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>
|
||||
</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>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Stock Firmware Avalonminers:</summary>
|
||||
<ul>
|
||||
<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>
|
||||
</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>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>A9X Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/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>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Stock Firmware Innosilicon Miners:</summary>
|
||||
<ul>
|
||||
<details>
|
||||
<summary>T3X Series:</summary>
|
||||
<ul>
|
||||
<li><a href="/miners/innosilicon/T3X/#t3h">T3H+</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M30S+VG40
|
||||
## M30S++VG40
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
|
||||
handler: python
|
||||
@@ -97,6 +97,14 @@
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M30S++VH60
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
|
||||
## M31S
|
||||
|
||||
@@ -122,6 +130,46 @@
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M31S+V30
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV30
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M31S+V40
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV40
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M31S+V60
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV60
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M31S+V80
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV80
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M31S+V90
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusV90
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M32
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32
|
||||
@@ -130,7 +178,7 @@
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## M32
|
||||
## M32V20
|
||||
|
||||
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20
|
||||
handler: python
|
||||
|
||||
@@ -11,7 +11,6 @@ nav:
|
||||
- BTMiner: "miners/backends/btminer.md"
|
||||
- CGMiner: "miners/backends/cgminer.md"
|
||||
- Hiveon: "miners/backends/hiveon.md"
|
||||
|
||||
- Classes:
|
||||
- Antminer X9: "miners/antminer/X9.md"
|
||||
- Antminer X17: "miners/antminer/X17.md"
|
||||
@@ -22,14 +21,13 @@ nav:
|
||||
- Avalon 10X: "miners/avalonminer/A10X.md"
|
||||
- Whatsminer M2X: "miners/whatsminer/M2X.md"
|
||||
- Whatsminer M3X: "miners/whatsminer/M3X.md"
|
||||
|
||||
- Innosilicon T3X: "miners/innosilicon/T3X.md"
|
||||
- Network:
|
||||
- Miner Network: "network/miner_network.md"
|
||||
- Miner Network Range: "network/miner_network_range.md"
|
||||
- Data:
|
||||
- Dataclasses:
|
||||
- Miner Data: "data/miner_data.md"
|
||||
- Error Codes: "data/error_codes.md"
|
||||
- Config:
|
||||
- Miner Config: "config/miner_config.md"
|
||||
- Advanced:
|
||||
- Miner APIs:
|
||||
|
||||
@@ -19,33 +19,7 @@ import warnings
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
def __init__(self, *args):
|
||||
if args:
|
||||
self.message = args[0]
|
||||
else:
|
||||
self.message = None
|
||||
|
||||
def __str__(self):
|
||||
if self.message:
|
||||
return f"{self.message}"
|
||||
else:
|
||||
return "Incorrect API parameters."
|
||||
|
||||
|
||||
class APIWarning(Warning):
|
||||
def __init__(self, *args):
|
||||
if args:
|
||||
self.message = args[0]
|
||||
else:
|
||||
self.message = None
|
||||
|
||||
def __str__(self):
|
||||
if self.message:
|
||||
return f"{self.message}"
|
||||
else:
|
||||
return "Incorrect API parameters."
|
||||
from pyasic.errors import APIError, APIWarning
|
||||
|
||||
|
||||
class BaseMinerAPI:
|
||||
|
||||
@@ -24,7 +24,8 @@ from typing import Union
|
||||
from passlib.handlers.md5_crypt import md5_crypt
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
from pyasic.API import BaseMinerAPI, APIError
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.API import BaseMinerAPI
|
||||
from pyasic.settings import PyasicSettings
|
||||
|
||||
|
||||
|
||||
@@ -11,3 +11,51 @@
|
||||
# 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.API.bmminer import BMMinerAPI
|
||||
from pyasic.API.bosminer import BOSMinerAPI
|
||||
from pyasic.API.btminer import BTMinerAPI
|
||||
from pyasic.API.cgminer import CGMinerAPI
|
||||
from pyasic.API.unknown import UnknownAPI
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
|
||||
from pyasic.data import (
|
||||
MinerData,
|
||||
BraiinsOSError,
|
||||
InnosiliconError,
|
||||
WhatsminerError,
|
||||
X19Error,
|
||||
)
|
||||
|
||||
from pyasic.errors import APIError, APIWarning
|
||||
|
||||
from pyasic.miners import get_miner
|
||||
from pyasic.miners.base import AnyMiner
|
||||
from pyasic.miners.miner_factory import MinerFactory
|
||||
from pyasic.miners.miner_listener import MinerListener
|
||||
|
||||
from pyasic.network import MinerNetwork
|
||||
|
||||
from pyasic.settings import PyasicSettings
|
||||
|
||||
__all__ = [
|
||||
"BMMinerAPI",
|
||||
"BOSMinerAPI",
|
||||
"BTMinerAPI",
|
||||
"CGMinerAPI",
|
||||
"UnknownAPI",
|
||||
"MinerConfig",
|
||||
"MinerData",
|
||||
"BraiinsOSError",
|
||||
"InnosiliconError",
|
||||
"WhatsminerError",
|
||||
"X19Error",
|
||||
"APIError",
|
||||
"APIWarning",
|
||||
"get_miner",
|
||||
"AnyMiner",
|
||||
"MinerFactory",
|
||||
"MinerListener",
|
||||
"MinerNetwork",
|
||||
"PyasicSettings",
|
||||
]
|
||||
|
||||
@@ -78,6 +78,23 @@ class _Pool:
|
||||
pool = {"url": self.url, "user": username, "pass": self.password}
|
||||
return pool
|
||||
|
||||
def as_inno(self, user_suffix: str = None) -> dict:
|
||||
"""Convert the data in this class to a dict usable by an Innosilicon device.
|
||||
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
username = self.username
|
||||
if user_suffix:
|
||||
username = f"{username}{user_suffix}"
|
||||
|
||||
pool = {
|
||||
f"Pool": self.url,
|
||||
f"UserName": username,
|
||||
f"Password": self.password,
|
||||
}
|
||||
return pool
|
||||
|
||||
def as_avalon(self, user_suffix: str = None) -> str:
|
||||
"""Convert the data in this class to a string usable by an Avalonminer device.
|
||||
|
||||
@@ -154,6 +171,19 @@ class _PoolGroup:
|
||||
pools.append(pool.as_x19(user_suffix=user_suffix))
|
||||
return pools
|
||||
|
||||
def as_inno(self, user_suffix: str = None) -> dict:
|
||||
"""Convert the data in this class to a list usable by an Innosilicon device.
|
||||
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
pools = {}
|
||||
for idx, pool in enumerate(self.pools[:3]):
|
||||
pool_data = pool.as_inno(user_suffix=user_suffix)
|
||||
for key in pool_data:
|
||||
pools[f"{key}{idx+1}"] = pool_data[key]
|
||||
return pools
|
||||
|
||||
def as_wm(self, user_suffix: str = None) -> List[dict]:
|
||||
"""Convert the data in this class to a list usable by an Whatsminer device.
|
||||
|
||||
@@ -217,7 +247,7 @@ class MinerConfig:
|
||||
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
|
||||
temp_target: float = 70.0
|
||||
temp_hot: float = 80.0
|
||||
temp_dangerous: float = 10.0
|
||||
temp_dangerous: float = 100.0
|
||||
|
||||
minimum_fans: int = None
|
||||
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
|
||||
@@ -269,6 +299,10 @@ class MinerConfig:
|
||||
self.temp_mode = "manual"
|
||||
if data.get("bitmain-fan-pwm"):
|
||||
self.fan_speed = int(data["bitmain-fan-pwm"])
|
||||
elif key == "bitmain-work-mode":
|
||||
if data[key]:
|
||||
if data[key] == 1:
|
||||
self.autotuning_wattage = 0
|
||||
elif key == "fan_control":
|
||||
for _key in data[key].keys():
|
||||
if _key == "min_fans":
|
||||
@@ -359,7 +393,15 @@ class MinerConfig:
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
return self.pool_groups[0].as_x19(user_suffix=user_suffix)
|
||||
return self.pool_groups[0].as_wm(user_suffix=user_suffix)
|
||||
|
||||
def as_inno(self, user_suffix: str = None) -> dict:
|
||||
"""Convert the data in this class to a config usable by an Innosilicon device.
|
||||
|
||||
Parameters:
|
||||
user_suffix: The suffix to append to username.
|
||||
"""
|
||||
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
|
||||
|
||||
def as_x19(self, user_suffix: str = None) -> str:
|
||||
"""Convert the data in this class to a config usable by an X19 device.
|
||||
@@ -371,7 +413,10 @@ class MinerConfig:
|
||||
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
||||
"bitmain-fan-ctrl": False,
|
||||
"bitmain-fan-pwn": 100,
|
||||
"miner-mode": 0, # Normal Mode
|
||||
}
|
||||
if self.autotuning_wattage == 0:
|
||||
cfg["miner-mode"] = 1 # Sleep Mode
|
||||
|
||||
if not self.temp_mode == "auto":
|
||||
cfg["bitmain-fan-ctrl"] = True
|
||||
|
||||
@@ -18,7 +18,7 @@ from datetime import datetime, timezone
|
||||
import time
|
||||
import json
|
||||
|
||||
from .error_codes import X19Error, WhatsminerError, BraiinsOSError
|
||||
from .error_codes import X19Error, WhatsminerError, BraiinsOSError, InnosiliconError
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -54,7 +54,7 @@ class MinerData:
|
||||
total_chips: The total number of chips on all boards. Calculated automatically.
|
||||
ideal_chips: The ideal number of chips in the miner as an int.
|
||||
percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
|
||||
nominal: The nominal amount of chips in the miner. Calculated automatically.
|
||||
nominal: Whether the number of chips in the miner is nominal. Calculated automatically.
|
||||
pool_split: The pool split as a str.
|
||||
pool_1_url: The first pool url on the miner as a str.
|
||||
pool_1_user: The first pool user on the miner as a str.
|
||||
@@ -71,11 +71,11 @@ class MinerData:
|
||||
model: str = "Unknown"
|
||||
hostname: str = "Unknown"
|
||||
hashrate: float = 0
|
||||
left_board_hashrate: float = 0
|
||||
center_board_hashrate: float = 0
|
||||
right_board_hashrate: float = 0
|
||||
left_board_hashrate: float = 0.0
|
||||
center_board_hashrate: float = 0.0
|
||||
right_board_hashrate: float = 0.0
|
||||
temperature_avg: int = field(init=False)
|
||||
env_temp: float = 0
|
||||
env_temp: float = 0.0
|
||||
left_board_temp: int = 0
|
||||
left_board_chip_temp: int = 0
|
||||
center_board_temp: int = 0
|
||||
@@ -100,9 +100,9 @@ class MinerData:
|
||||
pool_1_user: str = "Unknown"
|
||||
pool_2_url: str = ""
|
||||
pool_2_user: str = ""
|
||||
errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = field(
|
||||
default_factory=list
|
||||
)
|
||||
errors: List[
|
||||
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
|
||||
] = field(default_factory=list)
|
||||
fault_light: Union[bool, None] = None
|
||||
efficiency: int = field(init=False)
|
||||
|
||||
@@ -119,7 +119,7 @@ class MinerData:
|
||||
return setattr(self, key, value)
|
||||
|
||||
def __iter__(self):
|
||||
return iter([item for item in self.__dict__])
|
||||
return iter([item for item in self.asdict()])
|
||||
|
||||
@property
|
||||
def total_chips(self): # noqa - Skip PyCharm inspection
|
||||
@@ -175,22 +175,41 @@ class MinerData:
|
||||
def efficiency(self, val):
|
||||
pass
|
||||
|
||||
def asdict(self):
|
||||
def asdict(self) -> dict:
|
||||
"""Get this dataclass as a dictionary.
|
||||
|
||||
Returns:
|
||||
A dictionary version of this class.
|
||||
"""
|
||||
return asdict(self)
|
||||
|
||||
def as_json(self):
|
||||
def as_json(self) -> str:
|
||||
"""Get this dataclass as JSON.
|
||||
|
||||
Returns:
|
||||
A JSON version of this class.
|
||||
"""
|
||||
data = self.asdict()
|
||||
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
||||
return json.dumps(data)
|
||||
|
||||
def as_influxdb(self, measurement_name: str = "miner_data"):
|
||||
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/).
|
||||
|
||||
Parameters:
|
||||
measurement_name: The name of the measurement to insert into in influxdb.
|
||||
|
||||
Returns:
|
||||
A influxdb line protocol version of this class.
|
||||
"""
|
||||
tag_data = [measurement_name]
|
||||
field_data = []
|
||||
|
||||
tags = ["ip", "mac", "model", "hostname"]
|
||||
for attribute in self:
|
||||
if attribute in tags:
|
||||
tag_data.append(f"{attribute}={self[attribute]}")
|
||||
escaped_data = self[attribute].replace(" ", "\\ ")
|
||||
tag_data.append(f"{attribute}={escaped_data}")
|
||||
continue
|
||||
if isinstance(self[attribute], str):
|
||||
field_data.append(f'{attribute}="{self[attribute]}"')
|
||||
@@ -199,7 +218,7 @@ class MinerData:
|
||||
field_data.append(f"{attribute}={str(self[attribute]).lower()}")
|
||||
continue
|
||||
if isinstance(self[attribute], int):
|
||||
field_data.append(f"{attribute}={self[attribute]}i")
|
||||
field_data.append(f"{attribute}={self[attribute]}")
|
||||
continue
|
||||
if isinstance(self[attribute], float):
|
||||
field_data.append(f"{attribute}={self[attribute]}")
|
||||
@@ -207,6 +226,9 @@ class MinerData:
|
||||
if attribute == "fault_light" and not self[attribute]:
|
||||
field_data.append(f"{attribute}=false")
|
||||
continue
|
||||
if attribute == "errors":
|
||||
for idx, item in enumerate(self[attribute]):
|
||||
field_data.append(f'error_{idx+1}="{item.error_message}"')
|
||||
|
||||
tags_str = ",".join(tag_data)
|
||||
field_str = ",".join(field_data)
|
||||
|
||||
@@ -15,3 +15,10 @@
|
||||
from .whatsminer import WhatsminerError
|
||||
from .bos import BraiinsOSError
|
||||
from .X19 import X19Error
|
||||
from .innosilicon import InnosiliconError
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
MinerErrorData = TypeVar(
|
||||
"MinerErrorData", WhatsminerError, BraiinsOSError, X19Error, InnosiliconError
|
||||
)
|
||||
|
||||
65
pyasic/data/error_codes/innosilicon.py
Normal file
65
pyasic/data/error_codes/innosilicon.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# 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 dataclasses import dataclass, field, asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class InnosiliconError:
|
||||
"""A Dataclass to handle error codes of Innosilicon miners.
|
||||
|
||||
Attributes:
|
||||
error_code: The error code as an int.
|
||||
error_message: The error message as a string. Automatically found from the error code.
|
||||
"""
|
||||
|
||||
error_code: int
|
||||
error_message: str = field(init=False)
|
||||
|
||||
@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."
|
||||
|
||||
@error_message.setter
|
||||
def error_message(self, val):
|
||||
pass
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
|
||||
ERROR_CODES = {
|
||||
21: "The PLUG signal of the hash board is not detected.",
|
||||
22: "Power I2C communication is abnormal.",
|
||||
23: "The SPI of all hash boards is blocked.",
|
||||
24: "Some of the hash boards fail to connect to the SPI'.",
|
||||
25: "Hashboard failed to set frequency.",
|
||||
26: "Hashboard failed to set voltage.",
|
||||
27: "Chip BIST test failed.",
|
||||
28: "Hashboard SPI communication is abnormal.",
|
||||
29: "Power I2C communication is abnormal.",
|
||||
30: "Pool connection failed.",
|
||||
31: "Individual chips are damaged.",
|
||||
32: "Over temperature protection.",
|
||||
33: "Hashboard fault.",
|
||||
34: "The data cables are not connected in the correct order.",
|
||||
35: "No power output.",
|
||||
36: "Hashboard fault.",
|
||||
37: "Control board and/or hashboard do not match.",
|
||||
40: "Power output is abnormal.",
|
||||
41: "Power output is abnormal.",
|
||||
42: "Hashboard fault.",
|
||||
}
|
||||
@@ -152,6 +152,7 @@ ERROR_CODES = {
|
||||
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.",
|
||||
|
||||
41
pyasic/errors/__init__.py
Normal file
41
pyasic/errors/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright 2022 Upstream Data Inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
def __init__(self, *args):
|
||||
if args:
|
||||
self.message = args[0]
|
||||
else:
|
||||
self.message = None
|
||||
|
||||
def __str__(self):
|
||||
if self.message:
|
||||
return f"{self.message}"
|
||||
else:
|
||||
return "Incorrect API parameters."
|
||||
|
||||
|
||||
class APIWarning(Warning):
|
||||
def __init__(self, *args):
|
||||
if args:
|
||||
self.message = args[0]
|
||||
else:
|
||||
self.message = None
|
||||
|
||||
def __str__(self):
|
||||
if self.message:
|
||||
return f"{self.message}"
|
||||
else:
|
||||
return "Incorrect API parameters."
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Union, List
|
||||
|
||||
|
||||
from pyasic.API.bmminer import BMMinerAPI
|
||||
@@ -22,6 +22,7 @@ from pyasic.miners.base import BaseMiner
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
|
||||
from pyasic.settings import PyasicSettings
|
||||
|
||||
@@ -169,7 +170,7 @@ class BMMiner(BaseMiner):
|
||||
async def fault_light_on(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
return []
|
||||
|
||||
async def get_mac(self) -> str:
|
||||
@@ -178,6 +179,12 @@ class BMMiner(BaseMiner):
|
||||
async def restart_backend(self) -> bool:
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
"""Get data from the miner.
|
||||
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
import ipaddress
|
||||
import logging
|
||||
import json
|
||||
from typing import Union
|
||||
from typing import Union, List
|
||||
|
||||
import toml
|
||||
|
||||
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.API.bosminer import BOSMinerAPI
|
||||
from pyasic.API import APIError
|
||||
from pyasic.errors import APIError
|
||||
|
||||
from pyasic.data.error_codes import BraiinsOSError
|
||||
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
|
||||
from pyasic.data import MinerData
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
@@ -103,6 +103,20 @@ class BOSMiner(BaseMiner):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
data = await self.api.pause()
|
||||
if data.get("PAUSE"):
|
||||
if data["PAUSE"][0]:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
data = await self.api.resume()
|
||||
if data.get("RESUME"):
|
||||
if data["RESUME"][0]:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
"""Reboots power to the physical miner."""
|
||||
logging.debug(f"{self}: Sending reboot command.")
|
||||
@@ -232,11 +246,17 @@ class BOSMiner(BaseMiner):
|
||||
await conn.run("/etc/init.d/bosminer start")
|
||||
|
||||
async def check_light(self) -> bool:
|
||||
if not self.light:
|
||||
self.light = False
|
||||
if self.light:
|
||||
return self.light
|
||||
data = (
|
||||
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
|
||||
).strip()
|
||||
self.light = False
|
||||
if data == "50":
|
||||
self.light = True
|
||||
return self.light
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
tunerstatus = None
|
||||
errors = []
|
||||
|
||||
@@ -267,7 +287,7 @@ class BOSMiner(BaseMiner):
|
||||
"Stable",
|
||||
"Testing performance profile",
|
||||
]:
|
||||
_error = board["Status"]
|
||||
_error = board["Status"].split(" {")[0]
|
||||
_error = _error[0].lower() + _error[1:]
|
||||
errors.append(
|
||||
BraiinsOSError(f"{board_map[_id]} {_error}")
|
||||
@@ -431,7 +451,7 @@ class BOSMiner(BaseMiner):
|
||||
"Stable",
|
||||
"Testing performance profile",
|
||||
]:
|
||||
_error = board["Status"]
|
||||
_error = board["Status"].split(" {")[0]
|
||||
_error = _error[0].lower() + _error[1:]
|
||||
data.errors.append(
|
||||
BraiinsOSError(f"{board_map[_id]} {_error}")
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
import logging
|
||||
|
||||
import ipaddress
|
||||
from typing import Union
|
||||
from typing import Union, List
|
||||
|
||||
from pyasic.API.bosminer import BOSMinerAPI
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
|
||||
|
||||
class BOSMinerOld(BaseMiner):
|
||||
@@ -76,7 +78,7 @@ class BOSMinerOld(BaseMiner):
|
||||
async def get_config(self) -> None:
|
||||
return None
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
return []
|
||||
|
||||
async def get_hostname(self) -> str:
|
||||
@@ -94,5 +96,14 @@ class BOSMinerOld(BaseMiner):
|
||||
async def restart_backend(self) -> bool:
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
return None
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
return MinerData(ip=str(self.ip))
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Union, List
|
||||
|
||||
|
||||
from pyasic.API.btminer import BTMinerAPI
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.API import APIError
|
||||
from pyasic.errors import APIError
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data.error_codes import WhatsminerError
|
||||
from pyasic.data.error_codes import WhatsminerError, MinerErrorData
|
||||
from pyasic.config import MinerConfig
|
||||
|
||||
from pyasic.settings import PyasicSettings
|
||||
@@ -156,13 +156,44 @@ class BTMiner(BaseMiner):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
return []
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
data = []
|
||||
|
||||
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:
|
||||
data = await self.api.reboot()
|
||||
if data.get("Msg"):
|
||||
if data["Msg"] == "API command OK":
|
||||
return True
|
||||
return False
|
||||
|
||||
async def restart_backend(self) -> bool:
|
||||
data = await self.api.restart()
|
||||
if data.get("Msg"):
|
||||
if data["Msg"] == "API command OK":
|
||||
return True
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
data = await self.api.power_off(respbefore=True)
|
||||
if data.get("Msg"):
|
||||
if data["Msg"] == "API command OK":
|
||||
return True
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
data = await self.api.power_on()
|
||||
if data.get("Msg"):
|
||||
if data["Msg"] == "API command OK":
|
||||
return True
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
|
||||
@@ -14,15 +14,16 @@
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
from typing import Union
|
||||
from typing import Union, List
|
||||
|
||||
|
||||
from pyasic.API.cgminer import CGMinerAPI
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.API import APIError
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.config import MinerConfig
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
|
||||
from pyasic.settings import PyasicSettings
|
||||
|
||||
@@ -118,8 +119,7 @@ class CGMiner(BaseMiner):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def start_cgminer(self) -> None:
|
||||
"""Start cgminer hashing process."""
|
||||
async def resume_mining(self) -> bool:
|
||||
commands = [
|
||||
"mkdir -p /etc/tmp/",
|
||||
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
|
||||
@@ -128,9 +128,9 @@ class CGMiner(BaseMiner):
|
||||
]
|
||||
commands = ";".join(commands)
|
||||
await self.send_ssh_command(commands)
|
||||
return True
|
||||
|
||||
async def stop_cgminer(self) -> None:
|
||||
"""Restart cgminer hashing process."""
|
||||
async def stop_mining(self) -> bool:
|
||||
commands = [
|
||||
"mkdir -p /etc/tmp/",
|
||||
'echo "" > /etc/tmp/root',
|
||||
@@ -139,6 +139,7 @@ class CGMiner(BaseMiner):
|
||||
]
|
||||
commands = ";".join(commands)
|
||||
await self.send_ssh_command(commands)
|
||||
return True
|
||||
|
||||
async def get_config(self) -> str:
|
||||
"""Gets the config for the miner and sets it as `self.config`.
|
||||
@@ -163,7 +164,7 @@ class CGMiner(BaseMiner):
|
||||
async def fault_light_on(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
return []
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
from .antminer import *
|
||||
from .avalonminer import *
|
||||
from .whatsminer import *
|
||||
from .innosilicon import *
|
||||
|
||||
24
pyasic/miners/_types/innosilicon/T3X/T3H_Plus.py
Normal file
24
pyasic/miners/_types/innosilicon/T3X/T3H_Plus.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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 InnosiliconT3HPlus(BaseMiner):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "T3H+"
|
||||
self.nominal_chips = 114
|
||||
self.fan_count = 4
|
||||
@@ -12,9 +12,4 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import unittest
|
||||
from pyasic.tests.miners_tests import MinersTest
|
||||
from pyasic.tests.network_tests import NetworkTest
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
from .T3H_Plus import InnosiliconT3HPlus
|
||||
15
pyasic/miners/_types/innosilicon/__init__.py
Normal file
15
pyasic/miners/_types/innosilicon/__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 .T3X import *
|
||||
@@ -28,7 +28,7 @@ class M30SPlusPlusVG30(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S++ V30"
|
||||
self.model = "M30S++ VG30"
|
||||
self.nominal_chips = 111
|
||||
self.fan_count = 2
|
||||
|
||||
@@ -37,6 +37,15 @@ class M30SPlusPlusVG40(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S++ V40"
|
||||
self.model = "M30S++ VG40"
|
||||
self.nominal_chips = 117
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M30SPlusPlusVH60(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M30S++ VH60"
|
||||
self.nominal_chips = 78
|
||||
self.fan_count = 2
|
||||
|
||||
@@ -31,3 +31,48 @@ class M31SPlusVE20(BaseMiner):
|
||||
self.model = "M31S+ VE20"
|
||||
self.nominal_chips = 78
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M31SPlusV30(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M31S+ V30"
|
||||
self.nominal_chips = 117
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M31SPlusV40(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M31S+ V40"
|
||||
self.nominal_chips = 123
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M31SPlusV60(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M31S+ V60"
|
||||
self.nominal_chips = 156
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M31SPlusV80(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M31S+ V80"
|
||||
self.nominal_chips = 129
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
class M31SPlusV90(BaseMiner):
|
||||
def __init__(self, ip: str):
|
||||
super().__init__()
|
||||
self.ip = ip
|
||||
self.model = "M31S+ V90"
|
||||
self.nominal_chips = 117
|
||||
self.fan_count = 2
|
||||
|
||||
@@ -14,10 +14,23 @@
|
||||
|
||||
from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50
|
||||
from .M30S_Plus import M30SPlus, M30SPlusVG60, M30SPlusVE40, M30SPlusVF20
|
||||
from .M30S_Plus_Plus import M30SPlusPlus, M30SPlusPlusVG30, M30SPlusPlusVG40
|
||||
from .M30S_Plus_Plus import (
|
||||
M30SPlusPlus,
|
||||
M30SPlusPlusVG30,
|
||||
M30SPlusPlusVG40,
|
||||
M30SPlusPlusVH60,
|
||||
)
|
||||
|
||||
from .M31S import M31S
|
||||
from .M31S_Plus import M31SPlus, M31SPlusVE20
|
||||
from .M31S_Plus import (
|
||||
M31SPlus,
|
||||
M31SPlusVE20,
|
||||
M31SPlusV30,
|
||||
M31SPlusV40,
|
||||
M31SPlusV80,
|
||||
M31SPlusV60,
|
||||
M31SPlusV90,
|
||||
)
|
||||
|
||||
from .M32 import M32, M32V20
|
||||
from .M32S import M32S
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
|
||||
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data.error_codes import X19Error
|
||||
from pyasic.data.error_codes import X19Error, MinerErrorData
|
||||
from pyasic.settings import PyasicSettings
|
||||
|
||||
import httpx
|
||||
@@ -132,7 +132,7 @@ class BMMinerX19(BMMiner):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_errors(self) -> List[X19Error]:
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
errors = []
|
||||
url = f"http://{self.ip}/cgi-bin/summary.cgi"
|
||||
auth = httpx.DigestAuth(self.uname, self.pwd)
|
||||
@@ -146,3 +146,15 @@ class BMMinerX19(BMMiner):
|
||||
if not item["status"] == "s":
|
||||
errors.append(X19Error(item["msg"]))
|
||||
return errors
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
cfg = await self.get_config()
|
||||
cfg.autotuning_wattage = 0
|
||||
await self.send_config(cfg)
|
||||
return True
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
cfg = await self.get_config()
|
||||
cfg.autotuning_wattage = 1
|
||||
await self.send_config(cfg)
|
||||
return True
|
||||
|
||||
@@ -13,3 +13,4 @@
|
||||
# limitations under the License.
|
||||
|
||||
from .S9 import CGMinerS9
|
||||
from .T9 import CGMinerT9
|
||||
|
||||
@@ -51,6 +51,12 @@ class CGMinerA10X(CGMiner):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
"""Configures miner with yaml config."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -51,6 +51,12 @@ class CGMinerA7X(CGMiner):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
"""Configures miner with yaml config."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -51,6 +51,12 @@ class CGMinerA8X(CGMiner):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
"""Configures miner with yaml config."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -52,6 +52,12 @@ class CGMinerAvalon921(CGMiner, Avalon921):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
"""Configures miner with yaml config."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -16,10 +16,17 @@ import asyncssh
|
||||
import logging
|
||||
import ipaddress
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TypeVar
|
||||
from typing import TypeVar, List, Union
|
||||
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data.error_codes import (
|
||||
WhatsminerError,
|
||||
BraiinsOSError,
|
||||
InnosiliconError,
|
||||
X19Error,
|
||||
MinerErrorData,
|
||||
)
|
||||
|
||||
|
||||
class BaseMiner(ABC):
|
||||
@@ -85,58 +92,130 @@ class BaseMiner(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def fault_light_on(self) -> bool:
|
||||
"""Turn the fault light of the miner on and return success as a boolean.
|
||||
|
||||
Returns:
|
||||
A boolean value of the success of turning the light on.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def fault_light_off(self) -> bool:
|
||||
pass
|
||||
"""Turn the fault light of the miner off and return success as a boolean.
|
||||
|
||||
# async def send_file(self, src, dest):
|
||||
# async with (await self._get_ssh_connection()) as conn:
|
||||
# await asyncssh.scp(src, (conn, dest))
|
||||
Returns:
|
||||
A boolean value of the success of turning the light off.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def check_light(self) -> bool:
|
||||
pass
|
||||
"""Check the status and return on or off as a boolean.
|
||||
|
||||
# @abstractmethod
|
||||
async def get_board_info(self):
|
||||
return None
|
||||
Returns:
|
||||
A boolean value where `True` represents on and `False` represents off.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_config(self) -> MinerConfig:
|
||||
"""Get the mining configuration of the miner and return it as a [`MinerConfig`][pyasic.config.MinerConfig].
|
||||
|
||||
Returns:
|
||||
A [`MinerConfig`][pyasic.config.MinerConfig] containing the pool information and mining configuration.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_hostname(self) -> str:
|
||||
"""Get the hostname of the miner and return it as a string.
|
||||
|
||||
Returns:
|
||||
A string representing the hostname of the miner.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_model(self) -> str:
|
||||
"""Get the model of the miner and return it as a string.
|
||||
|
||||
Returns:
|
||||
A string representing the model of the miner.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def reboot(self) -> bool:
|
||||
"""Reboot the miner and return success as a boolean.
|
||||
|
||||
Returns:
|
||||
A boolean value of the success of rebooting the miner.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def restart_backend(self) -> bool:
|
||||
"""Restart the mining process of the miner (bosminer, bmminer, cgminer, etc) and return success as a boolean.
|
||||
|
||||
Returns:
|
||||
A boolean value of the success of restarting the mining process.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
"""Set the mining configuration of the miner.
|
||||
|
||||
Parameters:
|
||||
config: A [`MinerConfig`][pyasic.config.MinerConfig] containing the mining config you want to switch the miner to.
|
||||
user_suffix: A suffix to append to the username when sending to the miner.
|
||||
"""
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
async def get_mac(self) -> str:
|
||||
"""Get the MAC address of the miner and return it as a string.
|
||||
|
||||
Returns:
|
||||
A string representing the MAC address of the miner.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_errors(self) -> list:
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
"""Get a list of the errors the miner is experiencing.
|
||||
|
||||
Returns:
|
||||
A list of error classes representing different errors.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_data(self) -> MinerData:
|
||||
"""Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData].
|
||||
|
||||
Returns:
|
||||
A [`MinerData`][pyasic.data.MinerData] instance containing data from the miner.
|
||||
"""
|
||||
return MinerData(ip=str(self.ip))
|
||||
|
||||
@abstractmethod
|
||||
async def stop_mining(self) -> bool:
|
||||
"""Stop the mining process of the miner.
|
||||
|
||||
Returns:
|
||||
A boolean value of the success of stopping the mining process.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def resume_mining(self) -> bool:
|
||||
"""Stop the mining process of the miner.
|
||||
|
||||
Returns:
|
||||
A boolean value of the success of resuming the mining process.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
|
||||
|
||||
15
pyasic/miners/innosilicon/__init__.py
Normal file
15
pyasic/miners/innosilicon/__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 .cgminer import *
|
||||
322
pyasic/miners/innosilicon/cgminer/T3X/T3H_Plus.py
Normal file
322
pyasic/miners/innosilicon/cgminer/T3X/T3H_Plus.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# 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 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.error_codes import InnosiliconError, MinerErrorData
|
||||
from pyasic.settings import PyasicSettings
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.errors import APIError
|
||||
|
||||
import httpx
|
||||
import warnings
|
||||
from typing import Union, List
|
||||
import logging
|
||||
|
||||
|
||||
class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
self.uname = "admin"
|
||||
self.pwd = PyasicSettings().global_innosilicon_password
|
||||
self.jwt = None
|
||||
|
||||
async def auth(self):
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
auth = await client.post(
|
||||
f"http://{self.ip}/api/auth",
|
||||
data={"username": self.uname, "password": self.pwd},
|
||||
)
|
||||
except Exception:
|
||||
warnings.warn(f"Could not authenticate web token with miner: {self}")
|
||||
else:
|
||||
json_auth = auth.json()
|
||||
self.jwt = json_auth.get("jwt")
|
||||
return self.jwt
|
||||
|
||||
async def send_web_command(self, command: str, data: Union[dict, None] = None):
|
||||
if not self.jwt:
|
||||
await self.auth()
|
||||
if not data:
|
||||
data = {}
|
||||
async with httpx.AsyncClient() as client:
|
||||
for i in range(PyasicSettings().miner_get_data_retries):
|
||||
response = await client.post(
|
||||
f"http://{self.ip}/api/{command}",
|
||||
headers={"Authorization": "Bearer " + self.jwt},
|
||||
timeout=5,
|
||||
data=data,
|
||||
)
|
||||
json_data = response.json()
|
||||
if (
|
||||
not json_data.get("success")
|
||||
and "token" in json_data
|
||||
and json_data.get("token") == "expired"
|
||||
):
|
||||
# refresh the token, retry
|
||||
await self.auth()
|
||||
continue
|
||||
if not json_data.get("success"):
|
||||
if json_data.get("msg"):
|
||||
raise APIError(json_data["msg"])
|
||||
elif json_data.get("message"):
|
||||
raise APIError(json_data["message"])
|
||||
raise APIError("Innosilicon web api command failed.")
|
||||
return json_data
|
||||
|
||||
async def fault_light_on(self) -> bool:
|
||||
return False
|
||||
|
||||
async def fault_light_off(self) -> bool:
|
||||
return False
|
||||
|
||||
async def get_config(self) -> MinerConfig:
|
||||
pools = None
|
||||
cfg = MinerConfig()
|
||||
|
||||
try:
|
||||
pools = await self.api.pools()
|
||||
except APIError as e:
|
||||
logging.warning(e)
|
||||
|
||||
if pools:
|
||||
if "POOLS" in pools.keys():
|
||||
cfg = cfg.from_api(pools["POOLS"])
|
||||
return cfg
|
||||
|
||||
async def get_mac(self) -> Union[str, None]:
|
||||
try:
|
||||
data = await self.send_web_command("overview")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
if data.get("version"):
|
||||
return data["version"].get("ethaddr").upper()
|
||||
|
||||
async def get_hostname(self) -> Union[str, None]:
|
||||
return None
|
||||
|
||||
async def get_model(self) -> Union[str, None]:
|
||||
try:
|
||||
data = await self.send_web_command("type")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
return data["type"]
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
try:
|
||||
data = await self.send_web_command("reboot")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
return data["success"]
|
||||
|
||||
async def restart_cgminer(self) -> bool:
|
||||
try:
|
||||
data = await self.send_web_command("restartCgMiner")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
return data["success"]
|
||||
|
||||
async def restart_backend(self) -> bool:
|
||||
return await self.restart_cgminer()
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
await self.send_web_command(
|
||||
"updatePools", data=config.as_inno(user_suffix=user_suffix)
|
||||
)
|
||||
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
errors = []
|
||||
try:
|
||||
data = await self.send_web_command("getErrorDetail")
|
||||
except APIError:
|
||||
pass
|
||||
else:
|
||||
if "code" in data:
|
||||
err = data["code"]
|
||||
if isinstance(err, str):
|
||||
err = int(err)
|
||||
if not err == 0:
|
||||
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)
|
||||
|
||||
board_offset = -1
|
||||
fan_offset = -1
|
||||
|
||||
model = await self.get_model()
|
||||
hostname = await self.get_hostname()
|
||||
|
||||
if model:
|
||||
data.model = model
|
||||
|
||||
if hostname:
|
||||
data.hostname = hostname
|
||||
|
||||
data.errors = await self.get_errors()
|
||||
data.fault_light = await self.check_light()
|
||||
|
||||
miner_data = None
|
||||
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
|
||||
)
|
||||
|
||||
if miner_data:
|
||||
break
|
||||
|
||||
try:
|
||||
all_data = (await self.send_web_command("getAll"))["all"]
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if not (miner_data or all_data):
|
||||
return data
|
||||
|
||||
summary = miner_data.get("summary")
|
||||
pools = miner_data.get("pools")
|
||||
stats = miner_data.get("stats")
|
||||
|
||||
if summary:
|
||||
summary = summary[0]
|
||||
hr = summary.get("SUMMARY")
|
||||
if hr:
|
||||
if len(hr) > 0:
|
||||
hr = hr[0].get("MHS 1m")
|
||||
if hr:
|
||||
data.hashrate = round(hr / 1000000, 2)
|
||||
elif all_data:
|
||||
if all_data.get("total_hash"):
|
||||
print(all_data["total_hash"])
|
||||
hr = all_data["total_hash"].get("Hash Rate H")
|
||||
if hr:
|
||||
data.hashrate = round(hr / 1000000000000, 2)
|
||||
|
||||
if stats:
|
||||
stats = stats[0]
|
||||
if stats.get("STATS"):
|
||||
board_map = {0: "left", 1: "center", 2: "right"}
|
||||
for idx, board in enumerate(stats["STATS"]):
|
||||
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)
|
||||
|
||||
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")
|
||||
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),
|
||||
)
|
||||
if all_data.get("fansSpeed"):
|
||||
speed = round((all_data["fansSpeed"] * 6000) / 100)
|
||||
for fan in range(self.fan_count):
|
||||
setattr(data, f"fan_{fan+1}", speed)
|
||||
if all_data.get("mac"):
|
||||
data.mac = all_data["mac"].upper()
|
||||
else:
|
||||
mac = await self.get_mac()
|
||||
if mac:
|
||||
data.mac = mac
|
||||
if all_data.get("power"):
|
||||
data.wattage = all_data["power"]
|
||||
|
||||
if pools or all_data.get("pools_config"):
|
||||
pool_1 = None
|
||||
pool_2 = None
|
||||
pool_1_user = None
|
||||
pool_2_user = None
|
||||
pool_1_quota = 1
|
||||
pool_2_quota = 1
|
||||
quota = 0
|
||||
if pools:
|
||||
pools = pools[0]
|
||||
for pool in pools.get("POOLS"):
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("User")
|
||||
pool_1 = pool["URL"]
|
||||
if pool.get("Quota"):
|
||||
pool_2_quota = pool.get("Quota")
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
if pool.get("Quota"):
|
||||
pool_2_quota = pool.get("Quota")
|
||||
if not pool.get("User") == pool_1_user:
|
||||
if not pool_2_user == pool.get("User"):
|
||||
pool_2_user = pool.get("User")
|
||||
pool_2 = pool["URL"]
|
||||
if pool.get("Quota"):
|
||||
pool_2_quota = pool.get("Quota")
|
||||
elif all_data.get("pools_config"):
|
||||
print(all_data["pools_config"])
|
||||
for pool in all_data["pools_config"]:
|
||||
if not pool_1_user:
|
||||
pool_1_user = pool.get("user")
|
||||
pool_1 = pool["url"]
|
||||
elif not pool_2_user:
|
||||
pool_2_user = pool.get("user")
|
||||
pool_2 = pool["url"]
|
||||
if not pool.get("user") == pool_1_user:
|
||||
if not pool_2_user == pool.get("user"):
|
||||
pool_2_user = pool.get("user")
|
||||
pool_2 = pool["url"]
|
||||
|
||||
if pool_2_user and not pool_2_user == pool_1_user:
|
||||
quota = f"{pool_1_quota}/{pool_2_quota}"
|
||||
|
||||
if pool_1:
|
||||
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
|
||||
"stratum2+tcp://", ""
|
||||
)
|
||||
data.pool_1_url = pool_1
|
||||
|
||||
if pool_1_user:
|
||||
data.pool_1_user = pool_1_user
|
||||
|
||||
if pool_2:
|
||||
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
|
||||
"stratum2+tcp://", ""
|
||||
)
|
||||
data.pool_2_url = pool_2
|
||||
|
||||
if pool_2_user:
|
||||
data.pool_2_user = pool_2_user
|
||||
|
||||
if quota:
|
||||
data.pool_split = str(quota)
|
||||
|
||||
return data
|
||||
15
pyasic/miners/innosilicon/cgminer/T3X/__init__.py
Normal file
15
pyasic/miners/innosilicon/cgminer/T3X/__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 .T3H_Plus import CGMinerInnosiliconT3HPlus
|
||||
15
pyasic/miners/innosilicon/cgminer/__init__.py
Normal file
15
pyasic/miners/innosilicon/cgminer/__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 .T3X import *
|
||||
@@ -20,6 +20,7 @@ import httpx
|
||||
from pyasic.miners.antminer import *
|
||||
from pyasic.miners.avalonminer import *
|
||||
from pyasic.miners.whatsminer import *
|
||||
from pyasic.miners.innosilicon import *
|
||||
|
||||
from pyasic.miners._backends.cgminer import CGMiner # noqa - Ignore _module import
|
||||
from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import
|
||||
@@ -31,7 +32,9 @@ from pyasic.miners._backends.bosminer_old import ( # noqa - Ignore _module impo
|
||||
|
||||
from pyasic.miners.unknown import UnknownMiner
|
||||
|
||||
from pyasic.API import APIError
|
||||
from pyasic.errors import APIError
|
||||
|
||||
from pyasic.misc import Singleton
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
@@ -58,6 +61,7 @@ MINER_CLASSES = {
|
||||
"Default": BMMinerT9,
|
||||
"BMMiner": BMMinerT9,
|
||||
"Hiveon": HiveonT9,
|
||||
"CGMiner": CGMinerT9,
|
||||
},
|
||||
"ANTMINER S17": {
|
||||
"Default": BMMinerS17,
|
||||
@@ -193,6 +197,11 @@ MINER_CLASSES = {
|
||||
"Default": BTMinerM31SPlus,
|
||||
"BTMiner": BTMinerM31SPlus,
|
||||
"E20": BTMinerM31SPlusVE20,
|
||||
"30": BTMinerM31SPlusV30,
|
||||
"40": BTMinerM31SPlusV40,
|
||||
"60": BTMinerM31SPlusV60,
|
||||
"80": BTMinerM31SPlusV80,
|
||||
"90": BTMinerM31SPlusV90,
|
||||
},
|
||||
"M32S": {
|
||||
"Default": BTMinerM32S,
|
||||
@@ -243,19 +252,14 @@ MINER_CLASSES = {
|
||||
"Default": CGMinerAvalon1066,
|
||||
"CGMiner": CGMinerAvalon1066,
|
||||
},
|
||||
"T3H+": {
|
||||
"Default": CGMinerInnosiliconT3HPlus,
|
||||
"CGMiner": CGMinerInnosiliconT3HPlus,
|
||||
},
|
||||
"Unknown": {"Default": UnknownMiner},
|
||||
}
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class MinerFactory(metaclass=Singleton):
|
||||
"""A factory to handle identification and selection of the proper class of miner"""
|
||||
|
||||
@@ -407,9 +411,13 @@ class MinerFactory(metaclass=Singleton):
|
||||
except asyncssh.misc.PermissionDenied:
|
||||
try:
|
||||
data = await self.__get_system_info_from_web(ip)
|
||||
if "minertype" in data.keys():
|
||||
if not data.get("success"):
|
||||
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||
if _model:
|
||||
model = _model
|
||||
if "minertype" in data:
|
||||
model = data["minertype"].upper()
|
||||
if "bmminer" in "\t".join(data.keys()):
|
||||
if "bmminer" in "\t".join(data):
|
||||
api = "BMMiner"
|
||||
except Exception as e:
|
||||
logging.debug(f"Unable to get miner - {e}")
|
||||
@@ -488,8 +496,16 @@ class MinerFactory(metaclass=Singleton):
|
||||
if "PRO" in _model and " PRO" not in _model:
|
||||
_model = _model.replace("PRO", " PRO")
|
||||
model = _model
|
||||
else:
|
||||
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||
if _model:
|
||||
model = _model
|
||||
|
||||
if model:
|
||||
if "DRAGONMINT" in model:
|
||||
_model = await self.__get_dragonmint_version_from_web(ip)
|
||||
if _model:
|
||||
model = _model
|
||||
if " HIVEON" in model:
|
||||
# do hiveon check before whatsminer as HIVEON contains a V
|
||||
model = model.split(" HIVEON")[0]
|
||||
@@ -573,6 +589,31 @@ class MinerFactory(metaclass=Singleton):
|
||||
data = data.json()
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def __get_dragonmint_version_from_web(
|
||||
ip: ipaddress.ip_address,
|
||||
) -> Union[str, None]:
|
||||
response = None
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
auth = (
|
||||
await client.post(
|
||||
f"http://{ip}/api/auth",
|
||||
data={"username": "admin", "password": "admin"},
|
||||
)
|
||||
).json()["jwt"]
|
||||
response = (
|
||||
await client.post(
|
||||
f"http://{ip}/api/type",
|
||||
headers={"Authorization": "Bearer " + auth},
|
||||
data={},
|
||||
)
|
||||
).json()
|
||||
except Exception as e:
|
||||
logging.info(e)
|
||||
if response:
|
||||
return response["type"]
|
||||
|
||||
@staticmethod
|
||||
async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]:
|
||||
"""Check if the returned command output is correctly formatted."""
|
||||
@@ -595,9 +636,7 @@ class MinerFactory(metaclass=Singleton):
|
||||
else:
|
||||
# make sure the command succeeded
|
||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||
# this is an error
|
||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||
return False, data["STATUS"][0]["Msg"]
|
||||
return False, data["STATUS"][0]["Msg"]
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -14,14 +14,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
from pyasic.misc import Singleton
|
||||
|
||||
|
||||
class _MinerListener:
|
||||
|
||||
@@ -12,9 +12,13 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import List
|
||||
|
||||
from pyasic.API.unknown import UnknownAPI
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.config import MinerConfig
|
||||
from pyasic.data import MinerData
|
||||
from pyasic.data.error_codes import MinerErrorData
|
||||
|
||||
|
||||
class UnknownMiner(BaseMiner):
|
||||
@@ -47,7 +51,7 @@ class UnknownMiner(BaseMiner):
|
||||
async def get_config(self) -> None:
|
||||
return None
|
||||
|
||||
async def get_errors(self) -> list:
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
return []
|
||||
|
||||
async def get_mac(self) -> str:
|
||||
@@ -59,5 +63,14 @@ class UnknownMiner(BaseMiner):
|
||||
async def restart_backend(self) -> bool:
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
return False
|
||||
|
||||
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
|
||||
return None
|
||||
|
||||
async def get_data(self) -> MinerData:
|
||||
return MinerData(ip=str(self.ip))
|
||||
|
||||
@@ -17,6 +17,7 @@ from pyasic.miners._types import ( # noqa - Ignore access to _module
|
||||
M30SPlusPlus,
|
||||
M30SPlusPlusVG40,
|
||||
M30SPlusPlusVG30,
|
||||
M30SPlusPlusVH60,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,3 +37,9 @@ class BTMinerM30SPlusPlusVG40(BTMiner, M30SPlusPlusVG40):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM30SPlusPlusVH60(BTMiner, M30SPlusPlusVH60):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
@@ -16,6 +16,11 @@ from pyasic.miners._backends import BTMiner # noqa - Ignore access to _module
|
||||
from pyasic.miners._types import (
|
||||
M31SPlus,
|
||||
M31SPlusVE20,
|
||||
M31SPlusV30,
|
||||
M31SPlusV40,
|
||||
M31SPlusV60,
|
||||
M31SPlusV80,
|
||||
M31SPlusV90,
|
||||
) # noqa - Ignore access to _module
|
||||
|
||||
|
||||
@@ -29,3 +34,33 @@ class BTMinerM31SPlusVE20(BTMiner, M31SPlusVE20):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM31SPlusV30(BTMiner, M31SPlusV30):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM31SPlusV40(BTMiner, M31SPlusV40):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM31SPlusV60(BTMiner, M31SPlusV60):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM31SPlusV80(BTMiner, M31SPlusV80):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
|
||||
class BTMinerM31SPlusV90(BTMiner, M31SPlusV90):
|
||||
def __init__(self, ip: str) -> None:
|
||||
super().__init__(ip)
|
||||
self.ip = ip
|
||||
|
||||
@@ -29,10 +29,19 @@ from .M30S_Plus_Plus import (
|
||||
BTMinerM30SPlusPlus,
|
||||
BTMinerM30SPlusPlusVG40,
|
||||
BTMinerM30SPlusPlusVG30,
|
||||
BTMinerM30SPlusPlusVH60,
|
||||
)
|
||||
|
||||
from .M31S import BTMinerM31S
|
||||
from .M31S_Plus import BTMinerM31SPlus, BTMinerM31SPlusVE20
|
||||
from .M31S_Plus import (
|
||||
BTMinerM31SPlus,
|
||||
BTMinerM31SPlusVE20,
|
||||
BTMinerM31SPlusV30,
|
||||
BTMinerM31SPlusV40,
|
||||
BTMinerM31SPlusV60,
|
||||
BTMinerM31SPlusV80,
|
||||
BTMinerM31SPlusV90,
|
||||
)
|
||||
|
||||
from .M32 import BTMinerM32, BTMinerM32V20
|
||||
from .M32S import BTMinerM32S
|
||||
|
||||
22
pyasic/misc/__init__.py
Normal file
22
pyasic/misc/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Copyright 2022 Upstream Data Inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
@@ -37,7 +37,9 @@ class MinerNetwork:
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, ip_addr: Union[str, None] = None, mask: Union[str, int, None] = None
|
||||
self,
|
||||
ip_addr: Union[str, List[str], None] = None,
|
||||
mask: Union[str, int, None] = None,
|
||||
) -> None:
|
||||
self.network = None
|
||||
self.ip_addr = ip_addr
|
||||
@@ -46,6 +48,7 @@ class MinerNetwork:
|
||||
if mask.startswith("/"):
|
||||
mask = mask.replace("/", "")
|
||||
self.mask = mask
|
||||
self.network = self.get_network()
|
||||
|
||||
def __len__(self):
|
||||
return len([item for item in self.get_network().hosts()])
|
||||
@@ -53,6 +56,10 @@ class MinerNetwork:
|
||||
def __repr__(self):
|
||||
return str(self.network)
|
||||
|
||||
def hosts(self):
|
||||
for x in self.network.hosts():
|
||||
yield x
|
||||
|
||||
def get_network(self) -> ipaddress.ip_network:
|
||||
"""Get the network using the information passed to the MinerNetwork or from cache.
|
||||
|
||||
@@ -63,18 +70,14 @@ class MinerNetwork:
|
||||
if self.network:
|
||||
return self.network
|
||||
|
||||
# if there is no IP address passed, default to 192.168.1.0
|
||||
if not self.ip_addr:
|
||||
self.ip_addr = "192.168.1.0"
|
||||
if "-" in self.ip_addr:
|
||||
self.network = MinerNetworkRange(self.ip_addr)
|
||||
elif isinstance(self.ip_addr, list):
|
||||
self.network = MinerNetworkRange(self.ip_addr)
|
||||
else:
|
||||
# if there is no IP address passed, default to 192.168.1.0
|
||||
if not self.ip_addr:
|
||||
default_gateway = "192.168.1.0"
|
||||
# if we do have an IP address passed, use that
|
||||
else:
|
||||
default_gateway = self.ip_addr
|
||||
|
||||
# if there is no subnet mask passed, default to /24
|
||||
if not self.mask:
|
||||
subnet_mask = "24"
|
||||
@@ -84,7 +87,7 @@ class MinerNetwork:
|
||||
|
||||
# save the network and return it
|
||||
self.network = ipaddress.ip_network(
|
||||
f"{default_gateway}/{subnet_mask}", strict=False
|
||||
f"{self.ip_addr}/{subnet_mask}", strict=False
|
||||
)
|
||||
|
||||
logging.debug(f"Setting MinerNetwork: {self.network}")
|
||||
|
||||
@@ -14,14 +14,7 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
from pyasic.misc import Singleton
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -35,6 +28,7 @@ class PyasicSettings(metaclass=Singleton):
|
||||
miner_get_data_retries: int = 1
|
||||
|
||||
global_whatsminer_password = "admin"
|
||||
global_innosilicon_password = "admin"
|
||||
global_x19_password = "root"
|
||||
global_x17_password = "root"
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# Copyright 2022 Upstream Data Inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import unittest
|
||||
|
||||
from pyasic.miners.miner_factory import MINER_CLASSES
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
|
||||
class MinersTest(unittest.TestCase):
|
||||
def test_miner_model_creation(self):
|
||||
for miner_model in MINER_CLASSES.keys():
|
||||
for miner_api in MINER_CLASSES[miner_model].keys():
|
||||
with self.subTest(miner_model=miner_model, miner_api=miner_api):
|
||||
miner = MINER_CLASSES[miner_model][miner_api]("0.0.0.0")
|
||||
self.assertTrue(
|
||||
isinstance(miner, MINER_CLASSES[miner_model][miner_api])
|
||||
)
|
||||
|
||||
def test_miner_backend_backup_creation(self):
|
||||
backends = inspect.getmembers(
|
||||
sys.modules["pyasic.miners._backends"], inspect.isclass
|
||||
)
|
||||
for backend in backends:
|
||||
miner_class = backend[1]
|
||||
with self.subTest(miner_class=miner_class):
|
||||
miner = miner_class("0.0.0.0")
|
||||
self.assertTrue(isinstance(miner, miner_class))
|
||||
|
||||
def test_miner_type_creation_failure(self):
|
||||
backends = inspect.getmembers(
|
||||
sys.modules["pyasic.miners._types"], inspect.isclass
|
||||
)
|
||||
for backend in backends:
|
||||
miner_class = backend[1]
|
||||
with self.subTest(miner_class=miner_class):
|
||||
with self.assertRaises(TypeError):
|
||||
miner_class("0.0.0.0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.16.3"
|
||||
version = "0.17.10"
|
||||
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"
|
||||
|
||||
22
tests/__init__.py
Normal file
22
tests/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Copyright 2022 Upstream Data Inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import unittest
|
||||
from tests.miners_tests import MinersTest, MinerFactoryTest
|
||||
from tests.network_tests import NetworkTest
|
||||
from tests.config_tests import ConfigTest
|
||||
|
||||
if __name__ == "__main__":
|
||||
# `coverage run --source pyasic -m unittest discover` will give code coverage data
|
||||
unittest.main()
|
||||
111
tests/config_tests/__init__.py
Normal file
111
tests/config_tests/__init__.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# Copyright 2022 Upstream Data Inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import unittest
|
||||
|
||||
from pyasic.config import MinerConfig, _PoolGroup, _Pool # noqa
|
||||
from tests.test_data import (
|
||||
bosminer_api_pools,
|
||||
x19_api_pools,
|
||||
x19_web_pools,
|
||||
bosminer_config_pools,
|
||||
)
|
||||
|
||||
|
||||
class ConfigTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.test_config = MinerConfig(
|
||||
pool_groups=[
|
||||
_PoolGroup(
|
||||
quota=1,
|
||||
group_name="TEST",
|
||||
pools=[
|
||||
_Pool(
|
||||
url="stratum+tcp://pyasic.testpool_1.pool:3333",
|
||||
username="pyasic.test",
|
||||
password="123",
|
||||
),
|
||||
_Pool(
|
||||
url="stratum+tcp://pyasic.testpool_2.pool:3333",
|
||||
username="pyasic.test",
|
||||
password="123",
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
temp_mode="auto",
|
||||
temp_target=70.0,
|
||||
temp_hot=80.0,
|
||||
temp_dangerous=100.0,
|
||||
fan_speed=None,
|
||||
autotuning_enabled=True,
|
||||
autotuning_wattage=900,
|
||||
)
|
||||
|
||||
def test_config_from_raw(self):
|
||||
bos_config = MinerConfig().from_raw(bosminer_config_pools)
|
||||
bos_config.pool_groups[0].group_name = "TEST"
|
||||
|
||||
with self.subTest(
|
||||
msg="Testing BOSMiner config file config.", bos_config=bos_config
|
||||
):
|
||||
self.assertEqual(bos_config, self.test_config)
|
||||
|
||||
x19_cfg = MinerConfig().from_raw(x19_web_pools)
|
||||
x19_cfg.pool_groups[0].group_name = "TEST"
|
||||
|
||||
with self.subTest(msg="Testing X19 API config.", x19_cfg=x19_cfg):
|
||||
self.assertEqual(x19_cfg, self.test_config)
|
||||
|
||||
def test_config_from_api(self):
|
||||
bos_cfg = MinerConfig().from_api(bosminer_api_pools["POOLS"])
|
||||
bos_cfg.pool_groups[0].group_name = "TEST"
|
||||
|
||||
with self.subTest(msg="Testing BOSMiner API config.", bos_cfg=bos_cfg):
|
||||
self.assertEqual(bos_cfg, self.test_config)
|
||||
|
||||
x19_cfg = MinerConfig().from_api(x19_api_pools["POOLS"])
|
||||
x19_cfg.pool_groups[0].group_name = "TEST"
|
||||
|
||||
with self.subTest(msg="Testing X19 API config.", x19_cfg=x19_cfg):
|
||||
self.assertEqual(x19_cfg, self.test_config)
|
||||
|
||||
def test_config_as_types(self):
|
||||
cfg = MinerConfig().from_api(bosminer_api_pools["POOLS"])
|
||||
cfg.pool_groups[0].group_name = "TEST"
|
||||
|
||||
commands = [
|
||||
func
|
||||
for func in
|
||||
# each function in self
|
||||
dir(cfg)
|
||||
if callable(getattr(cfg, func)) and
|
||||
# no __ methods
|
||||
not func.startswith("__")
|
||||
]
|
||||
|
||||
for command in [cmd for cmd in commands if cmd.startswith("as_")]:
|
||||
with self.subTest():
|
||||
output = getattr(cfg, command)()
|
||||
self.assertEqual(output, getattr(self.test_config, command)())
|
||||
if f"from_{command.split('as_')[1]}" in commands:
|
||||
self.assertEqual(
|
||||
getattr(MinerConfig(), f"from_{command.split('as_')[1]}")(
|
||||
output
|
||||
),
|
||||
self.test_config,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
162
tests/miners_tests/__init__.py
Normal file
162
tests/miners_tests/__init__.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# Copyright 2022 Upstream Data Inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import unittest
|
||||
|
||||
from pyasic.miners.miner_factory import MINER_CLASSES
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.miners._backends import CGMiner
|
||||
from pyasic.miners.miner_factory import MinerFactory
|
||||
from pyasic.miners.miner_listener import MinerListener
|
||||
|
||||
import asyncio
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
|
||||
class MinersTest(unittest.TestCase):
|
||||
def test_miner_model_creation(self):
|
||||
for miner_model in MINER_CLASSES.keys():
|
||||
for miner_api in MINER_CLASSES[miner_model].keys():
|
||||
with self.subTest(
|
||||
msg=f"Creation of miner using model={miner_model}, api={miner_api}",
|
||||
miner_model=miner_model,
|
||||
miner_api=miner_api,
|
||||
):
|
||||
miner = MINER_CLASSES[miner_model][miner_api]("0.0.0.0")
|
||||
self.assertTrue(
|
||||
isinstance(miner, MINER_CLASSES[miner_model][miner_api])
|
||||
)
|
||||
|
||||
def test_miner_backend_backup_creation(self):
|
||||
backends = inspect.getmembers(
|
||||
sys.modules["pyasic.miners._backends"], inspect.isclass
|
||||
)
|
||||
for backend in backends:
|
||||
miner_class = backend[1]
|
||||
with self.subTest(miner_class=miner_class):
|
||||
miner = miner_class("0.0.0.0")
|
||||
self.assertTrue(isinstance(miner, miner_class))
|
||||
|
||||
def test_miner_type_creation_failure(self):
|
||||
backends = inspect.getmembers(
|
||||
sys.modules["pyasic.miners._types"], inspect.isclass
|
||||
)
|
||||
for backend in backends:
|
||||
miner_class = backend[1]
|
||||
with self.subTest(miner_class=miner_class):
|
||||
with self.assertRaises(TypeError):
|
||||
miner_class("0.0.0.0")
|
||||
with self.assertRaises(TypeError):
|
||||
BaseMiner("0.0.0.0")
|
||||
|
||||
def test_miner_comparisons(self):
|
||||
miner_1 = CGMiner("1.1.1.1")
|
||||
miner_2 = CGMiner("2.2.2.2")
|
||||
miner_3 = CGMiner("1.1.1.1")
|
||||
self.assertEqual(miner_1, miner_3)
|
||||
self.assertGreater(miner_2, miner_1)
|
||||
self.assertLess(miner_3, miner_2)
|
||||
|
||||
|
||||
class MinerFactoryTest(unittest.TestCase):
|
||||
def test_miner_factory_creation(self):
|
||||
self.assertDictEqual(MinerFactory().miners, {})
|
||||
miner_factory = MinerFactory()
|
||||
self.assertIs(MinerFactory(), miner_factory)
|
||||
|
||||
def test_get_miner_generator(self):
|
||||
async def _coro():
|
||||
gen = MinerFactory().get_miner_generator([])
|
||||
miners = []
|
||||
async for miner in gen:
|
||||
miners.append(miner)
|
||||
return miners
|
||||
|
||||
_miners = asyncio.run(_coro())
|
||||
self.assertListEqual(_miners, [])
|
||||
|
||||
def test_miner_selection(self):
|
||||
for miner_model in MINER_CLASSES.keys():
|
||||
with self.subTest():
|
||||
miner = MinerFactory()._select_miner_from_classes(
|
||||
"0.0.0.0", miner_model, None, None
|
||||
)
|
||||
self.assertIsInstance(miner, BaseMiner)
|
||||
for api in ["BOSMiner+", "BOSMiner", "CGMiner", "BTMiner", "BMMiner"]:
|
||||
with self.subTest():
|
||||
miner = MinerFactory()._select_miner_from_classes(
|
||||
"0.0.0.0", None, api, None
|
||||
)
|
||||
self.assertIsInstance(miner, BaseMiner)
|
||||
|
||||
with self.subTest():
|
||||
miner = MinerFactory()._select_miner_from_classes(
|
||||
"0.0.0.0", "ANTMINER S17+", "Fake API", None
|
||||
)
|
||||
self.assertIsInstance(miner, BaseMiner)
|
||||
|
||||
with self.subTest():
|
||||
miner = MinerFactory()._select_miner_from_classes(
|
||||
"0.0.0.0", "M30S", "BTMiner", "G20"
|
||||
)
|
||||
self.assertIsInstance(miner, BaseMiner)
|
||||
|
||||
def test_validate_command(self):
|
||||
bad_test_data_returns = [
|
||||
{},
|
||||
{
|
||||
"cmd": [
|
||||
{
|
||||
"STATUS": [
|
||||
{"STATUS": "E", "Msg": "Command failed for some reason."}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{"STATUS": "E", "Msg": "Command failed for some reason."},
|
||||
{
|
||||
"STATUS": [{"STATUS": "E", "Msg": "Command failed for some reason."}],
|
||||
"id": 1,
|
||||
},
|
||||
]
|
||||
for data in bad_test_data_returns:
|
||||
with self.subTest():
|
||||
|
||||
async def _coro(miner_ret):
|
||||
_data = await MinerFactory()._validate_command(miner_ret)
|
||||
return _data
|
||||
|
||||
ret = asyncio.run(_coro(data))
|
||||
self.assertFalse(ret[0])
|
||||
|
||||
good_test_data_returns = [
|
||||
{
|
||||
"STATUS": [{"STATUS": "S", "Msg": "Yay! Command succeeded."}],
|
||||
"id": 1,
|
||||
},
|
||||
]
|
||||
for data in good_test_data_returns:
|
||||
with self.subTest():
|
||||
|
||||
async def _coro(miner_ret):
|
||||
_data = await MinerFactory()._validate_command(miner_ret)
|
||||
return _data
|
||||
|
||||
ret = asyncio.run(_coro(data))
|
||||
self.assertTrue(ret[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -31,8 +31,8 @@ class NetworkTest(unittest.TestCase):
|
||||
"192.168.1.60",
|
||||
]
|
||||
|
||||
net_1 = list(MinerNetworkRange(net_range_str).hosts())
|
||||
net_2 = list(MinerNetworkRange(net_range_list).hosts())
|
||||
net_1 = list(MinerNetwork(net_range_str).get_network().hosts())
|
||||
net_2 = list(MinerNetwork(net_range_list).get_network().hosts())
|
||||
|
||||
correct_net = [
|
||||
ipaddress.IPv4Address("192.168.1.29"),
|
||||
@@ -48,7 +48,7 @@ class NetworkTest(unittest.TestCase):
|
||||
|
||||
def test_net(self):
|
||||
net_1_str = "192.168.1.0"
|
||||
net_1_mask = 29
|
||||
net_1_mask = "/29"
|
||||
|
||||
net_1 = list(MinerNetwork(net_1_str, mask=net_1_mask).get_network().hosts())
|
||||
|
||||
@@ -68,6 +68,20 @@ class NetworkTest(unittest.TestCase):
|
||||
self.assertTrue(net_1 == correct_net)
|
||||
self.assertTrue(net_2 == correct_net)
|
||||
|
||||
def test_net_len(self):
|
||||
net = MinerNetwork("192.168.1.0", mask=32)
|
||||
self.assertEqual(len(net), 1)
|
||||
|
||||
net2 = MinerNetwork("192.168.1.0", mask=31)
|
||||
self.assertEqual(len(net2), 2)
|
||||
|
||||
def test_net_defaults(self):
|
||||
net = MinerNetwork()
|
||||
net_obj = net.get_network()
|
||||
self.assertEqual(net_obj, MinerNetwork("192.168.1.0", mask=24).get_network())
|
||||
|
||||
self.assertEqual(net_obj, net.get_network())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
128
tests/test_data.py
Normal file
128
tests/test_data.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# 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.
|
||||
bosminer_api_pools = {
|
||||
"STATUS": [
|
||||
{
|
||||
"STATUS": "S",
|
||||
"Msg": "2 Pool(s)",
|
||||
"Description": "",
|
||||
}
|
||||
],
|
||||
"POOLS": [
|
||||
{
|
||||
"POOL": 0,
|
||||
"URL": "stratum+tcp://pyasic.testpool_1.pool:3333",
|
||||
"Status": "Alive",
|
||||
"Quota": 1,
|
||||
"User": "pyasic.test",
|
||||
"Stratum URL": "pyasic.testpool_1.pool:3333",
|
||||
"AsicBoost": True,
|
||||
},
|
||||
{
|
||||
"POOL": 1,
|
||||
"URL": "stratum+tcp://pyasic.testpool_2.pool:3333",
|
||||
"Status": "Alive",
|
||||
"Quota": 1,
|
||||
"User": "pyasic.test",
|
||||
"Stratum URL": "pyasic.testpool_2.pool:3333",
|
||||
"AsicBoost": True,
|
||||
},
|
||||
],
|
||||
"id": 1,
|
||||
}
|
||||
|
||||
x19_api_pools = {
|
||||
"STATUS": [
|
||||
{
|
||||
"STATUS": "S",
|
||||
"Msg": "2 Pool(s)",
|
||||
"Description": "",
|
||||
}
|
||||
],
|
||||
"POOLS": [
|
||||
{
|
||||
"POOL": 0,
|
||||
"URL": "stratum+tcp://pyasic.testpool_1.pool:3333",
|
||||
"Status": "Alive",
|
||||
"Quota": 1,
|
||||
"User": "pyasic.test",
|
||||
"Stratum URL": "pyasic.testpool_1.pool:3333",
|
||||
},
|
||||
{
|
||||
"POOL": 1,
|
||||
"URL": "stratum+tcp://pyasic.testpool_2.pool:3333",
|
||||
"Status": "Alive",
|
||||
"Quota": 1,
|
||||
"User": "pyasic.test",
|
||||
"Stratum URL": "pyasic.testpool_2.pool:3333",
|
||||
},
|
||||
],
|
||||
"id": 1,
|
||||
}
|
||||
|
||||
x19_web_pools = {
|
||||
"pools": [
|
||||
{
|
||||
"url": "stratum+tcp://pyasic.testpool_1.pool:3333",
|
||||
"user": "pyasic.test",
|
||||
"pass": "123",
|
||||
},
|
||||
{
|
||||
"url": "stratum+tcp://pyasic.testpool_2.pool:3333",
|
||||
"user": "pyasic.test",
|
||||
"pass": "123",
|
||||
},
|
||||
],
|
||||
"api-listen": True,
|
||||
"api-network": True,
|
||||
"api-groups": "A:stats:pools:devs:summary:version",
|
||||
"api-allow": "A:0/0,W:*",
|
||||
"bitmain-fan-ctrl": False,
|
||||
"bitmain-fan-pwm": "100",
|
||||
"bitmain-use-vil": True,
|
||||
"bitmain-freq": "675",
|
||||
"bitmain-voltage": "1400",
|
||||
"bitmain-ccdelay": "0",
|
||||
"bitmain-pwth": "0",
|
||||
"bitmain-work-mode": "0",
|
||||
"bitmain-freq-level": "100",
|
||||
}
|
||||
|
||||
bosminer_config_pools = {
|
||||
"format": {
|
||||
"version": "1.2+",
|
||||
"model": "Antminer S9",
|
||||
"generator": "pyasic",
|
||||
},
|
||||
"group": [
|
||||
{
|
||||
"name": "TEST",
|
||||
"quota": 1,
|
||||
"pool": [
|
||||
{
|
||||
"enabled": True,
|
||||
"url": "stratum+tcp://pyasic.testpool_1.pool:3333",
|
||||
"user": "pyasic.test",
|
||||
"password": "123",
|
||||
},
|
||||
{
|
||||
"enabled": True,
|
||||
"url": "stratum+tcp://pyasic.testpool_2.pool:3333",
|
||||
"user": "pyasic.test",
|
||||
"password": "123",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user