Compare commits

..

17 Commits

Author SHA1 Message Date
UpstreamData
26ae6ebfb2 bump version number 2022-07-19 11:19:32 -06:00
UpstreamData
e65cb0573d update miner factory to handle some types of stock fw S9s 2022-07-19 11:18:55 -06:00
UpstreamData
f8590b0c5f improve more typing 2022-07-18 14:46:17 -06:00
UpstreamData
43b4992cee improve logging and some documentation 2022-07-18 14:38:54 -06:00
UpstreamData
98e2cfae84 bump version number 2022-07-18 12:05:44 -06:00
UpstreamData
cb01c1a8ee update network to scan fast even if some miners are not responding properly 2022-07-18 12:05:22 -06:00
UpstreamData
36a273ec2b bump version number 2022-07-18 11:45:14 -06:00
UpstreamData
6a0dc03b9d update to a better way to handle settings 2022-07-18 11:44:22 -06:00
UpstreamData
ce7b006c8f bump version number 2022-07-18 11:23:15 -06:00
UpstreamData
88cc05bcea handle for BraiinsOS miners that dont have bosminer running for some reason 2022-07-18 11:21:40 -06:00
UpstreamData
ae749f4a90 add additional scan ports as backups in case 4028 doesn't respond 2022-07-18 10:03:39 -06:00
UpstreamData
36b30a2cdd added supported miners to the docs 2022-07-14 16:32:43 -06:00
UpstreamData
ae9f103578 bump version number 2022-07-14 11:49:34 -06:00
UpstreamData
13b583b739 fixed some bugs and added support for M20Sv10 and 20 2022-07-14 11:39:55 -06:00
UpstreamData
aaf0d7fa75 bump version number 2022-07-14 09:47:36 -06:00
UpstreamData
a8cbb6394e fix a bug with ints being passed to miner network 2022-07-14 09:45:21 -06:00
UpstreamData
ca6980b1ad update documentation and add docs from config 2022-07-13 16:17:08 -06:00
63 changed files with 1152 additions and 512 deletions

24
docs/API/api.md Normal file
View File

@@ -0,0 +1,24 @@
# pyasic
## Miner APIs
Each miner has a unique API that is used to communicate with it.
Each of these API types has commands that differ between them, and some commands have data that others do not.
Each miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
All API implementations inherit from [`BaseMinerAPI`][pyasic.API.BaseMinerAPI], which implements the basic communications protocols.
BaseMinerAPI should never be used unless inheriting to create a new miner API class for a new type of miner (which should be exceedingly rare).
Use these instead -
#### [BMMiner API][pyasic.API.bmminer.BMMinerAPI]
#### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI]
#### [BTMiner API][pyasic.API.btminer.BTMinerAPI]
#### [CGMiner API][pyasic.API.cgminer.CGMinerAPI]
#### [Unknown API][pyasic.API.unknown.UnknownAPI]
<br>
## BaseMinerAPI
::: pyasic.API.BaseMinerAPI
handler: python
options:
heading_level: 4

7
docs/API/bmminer.md Normal file
View File

@@ -0,0 +1,7 @@
# pyasic
## BMMinerAPI
::: pyasic.API.bmminer.BMMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4

7
docs/API/bosminer.md Normal file
View File

@@ -0,0 +1,7 @@
# pyasic
## BOSMinerAPI
::: pyasic.API.bosminer.BOSMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4

7
docs/API/btminer.md Normal file
View File

@@ -0,0 +1,7 @@
# pyasic
## BTMinerAPI
::: pyasic.API.btminer.BTMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4

7
docs/API/cgminer.md Normal file
View File

@@ -0,0 +1,7 @@
# pyasic
## CGMinerAPI
::: pyasic.API.cgminer.CGMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4

7
docs/API/unknown.md Normal file
View File

@@ -0,0 +1,7 @@
# pyasic
## UnknownAPI
::: pyasic.API.unknown.UnknownAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,42 +0,0 @@
# pyasic
## Miner APIs
Each miner has a unique API that is used to communicate with it.
Each of these API types has commands that differ between them, and some commands have data that others do not.
Each miner that is a subclass of `BaseMiner` should have an API linked to it as `Miner.api`.
All API implementations inherit from `BaseMinerAPI`, which implements the basic communications protocols.
## BMMinerAPI
::: pyasic.API.bmminer.BMMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4
## BOSMinerAPI
::: pyasic.API.bosminer.BOSMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4
## BTMinerAPI
::: pyasic.API.btminer.BTMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4
## CGMinerAPI
::: pyasic.API.cgminer.CGMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4
## UnknownAPI
::: pyasic.API.unknown.UnknownAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,24 @@
# pyasic
## Miner Config
::: pyasic.config.MinerConfig
handler: python
options:
show_root_heading: false
heading_level: 4
## Pool Groups
::: pyasic.config._PoolGroup
handler: python
options:
show_root_heading: false
heading_level: 4
## Pools
::: pyasic.config._Pool
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -9,6 +9,8 @@
## Intro ## Intro
Welcome to pyasic! Pyasic uses an asynchronous method of communicating with asic miners on your network, which makes it super fast. Welcome to pyasic! Pyasic uses an asynchronous method of communicating with asic miners on your network, which makes it super fast.
[Supported Miner Types](miners/supported_types.md)
Getting started with pyasic is easy. First, find your miner (or miners) on the network by scanning for them or getting the correct class automatically for them if you know the IP. Getting started with pyasic is easy. First, find your miner (or miners) on the network by scanning for them or getting the correct class automatically for them if you know the IP.
<br> <br>

View File

@@ -0,0 +1,59 @@
# pyasic
## X17 Models
## S17
::: pyasic.miners.antminer.bmminer.X17.S17.BMMinerS17
handler: python
options:
show_root_heading: false
heading_level: 4
## S17+
::: pyasic.miners.antminer.bmminer.X17.S17_Plus.BMMinerS17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## S17 Pro
::: pyasic.miners.antminer.bmminer.X17.S17_Pro.BMMinerS17Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S17e
::: pyasic.miners.antminer.bmminer.X17.S17e.BMMinerS17e
handler: python
options:
show_root_heading: false
heading_level: 4
## T17
::: pyasic.miners.antminer.bmminer.X17.T17.BMMinerT17
handler: python
options:
show_root_heading: false
heading_level: 4
## T17+
::: pyasic.miners.antminer.bmminer.X17.T17_Plus.BMMinerT17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## T17e
::: pyasic.miners.antminer.bmminer.X17.T17e.BMMinerT17e
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,52 @@
# pyasic
## X19 Models
## S19
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro
::: pyasic.miners.antminer.bmminer.X19.S19_Pro.BMMinerS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19a
::: pyasic.miners.antminer.bmminer.X19.S19a.BMMinerS19a
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j
::: pyasic.miners.antminer.bmminer.X19.S19j.BMMinerS19j
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro
::: pyasic.miners.antminer.bmminer.X19.S19j_Pro.BMMinerS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19
::: pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,26 @@
# pyasic
## X9 Models
## S9
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4
## S9i
::: pyasic.miners.antminer.bmminer.X9.S9i.BMMinerS9i
handler: python
options:
show_root_heading: false
heading_level: 4
## T9
::: pyasic.miners.antminer.bmminer.X9.T9.BMMinerT9
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,26 @@
# pyasic
## A10X Models
## A1026
::: pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026
handler: python
options:
show_root_heading: false
heading_level: 4
## A1047
::: pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047
handler: python
options:
show_root_heading: false
heading_level: 4
## A1066
::: pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,26 @@
# pyasic
## A7X Models
## A721
::: pyasic.miners.avalonminer.cgminer.A7X.A721.CGMinerAvalon721
handler: python
options:
show_root_heading: false
heading_level: 4
## A741
::: pyasic.miners.avalonminer.cgminer.A7X.A741.CGMinerAvalon741
handler: python
options:
show_root_heading: false
heading_level: 4
## A761
::: pyasic.miners.avalonminer.cgminer.A7X.A761.CGMinerAvalon761
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,26 @@
# pyasic
## A8X Models
## A821
::: pyasic.miners.avalonminer.cgminer.A8X.A821.CGMinerAvalon821
handler: python
options:
show_root_heading: false
heading_level: 4
## A841
::: pyasic.miners.avalonminer.cgminer.A8X.A841.CGMinerAvalon841
handler: python
options:
show_root_heading: false
heading_level: 4
## A851
::: pyasic.miners.avalonminer.cgminer.A8X.A851.CGMinerAvalon851
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,10 @@
# pyasic
## A9X Models
## A921
::: pyasic.miners.avalonminer.cgminer.A9X.A921.CGMinerAvalon921
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,8 @@
# pyasic
## BMMiner Backend
::: pyasic.miners._backends.bmminer.BMMiner
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,8 @@
# pyasic
## BOSMiner Backend
::: pyasic.miners._backends.bosminer.BOSMiner
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,8 @@
# pyasic
## BTMiner Backend
::: pyasic.miners._backends.btminer.BTMiner
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,8 @@
# pyasic
## CGMiner Backend
::: pyasic.miners._backends.cgminer.CGMiner
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,8 @@
# pyasic
## Hiveon Backend
::: pyasic.miners._backends.hiveon.Hiveon
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,73 @@
# pyasic
## Supported Miners
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.
## Miner List
##### pyasic currently supports the following miners and subtypes:
* Braiins OS+ Devices:
* All devices supported by BraiinsOS+ are supported here.
* 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]
* [M32S][pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S]
* M2X Series:
* [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]

View File

@@ -0,0 +1,75 @@
# pyasic
## M2X Models
## M20S
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20S
handler: python
options:
show_root_heading: false
heading_level: 4
## M20SV10
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M20SV20
::: pyasic.miners.whatsminer.btminer.M2X.M20S.BTMinerM20SV20
handler: python
options:
show_root_heading: false
heading_level: 4
## M20S+
::: pyasic.miners.whatsminer.btminer.M2X.M20S_Plus.BTMinerM20SPlus
handler: python
options:
show_root_heading: false
heading_level: 4
## M21
::: pyasic.miners.whatsminer.btminer.M2X.M21.BTMinerM21
handler: python
options:
show_root_heading: false
heading_level: 4
## M21S
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21S
handler: python
options:
show_root_heading: false
heading_level: 4
## M21SV20
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV20
handler: python
options:
show_root_heading: false
heading_level: 4
## M21SV60
::: pyasic.miners.whatsminer.btminer.M2X.M21S.BTMinerM21SV60
handler: python
options:
show_root_heading: false
heading_level: 4
## M21S+
::: pyasic.miners.whatsminer.btminer.M2X.M21S_Plus.BTMinerM21SPlus
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,131 @@
# pyasic
## M3X Models
## M30S
::: pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30S
handler: python
options:
show_root_heading: false
heading_level: 4
## M30SVE10
::: pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVE10
handler: python
options:
show_root_heading: false
heading_level: 4
## M30SVG20
::: pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVG20
handler: python
options:
show_root_heading: false
heading_level: 4
## M30SVE20
::: pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SVE20
handler: python
options:
show_root_heading: false
heading_level: 4
## M30SV50
::: pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30SV50
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S+
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlus
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S+VF20
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVF20
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S+VE40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVE40
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S+VG60
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus.BTMinerM30SPlusVG60
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S++
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S++VG30
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30
handler: python
options:
show_root_heading: false
heading_level: 4
## M30S+VG40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S+VE20
::: pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20
handler: python
options:
show_root_heading: false
heading_level: 4
## M32S
::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,12 @@
# pyasic
## Miner Network Range
`MinerNetworkRange` is a class used by [`MinerNetwork`][pyasic.network.MinerNetwork] to handle any constructor stings.
The goal is to emulate what is produced by `ipaddress.ip_network` by allowing [`MinerNetwork`][pyasic.network.MinerNetwork] to get a list of hosts.
This allows this class to be the [`MinerNetwork.network`][pyasic.network.MinerNetwork] and hence be used for scanning.
::: pyasic.network.net_range.MinerNetworkRange
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -2,12 +2,42 @@ site_name: pyasic
repo_url: https://github.com/UpstreamData/pyasic repo_url: https://github.com/UpstreamData/pyasic
nav: nav:
- Introduction: "index.md" - Introduction: "index.md"
- Usage: - Miners:
- Miner Factory: "miner_factory.md" - Supported Miners: "miners/supported_types.md"
- Miner Network: "miner_network.md" - Miner Factory: "miners/miner_factory.md"
- Miner Data: "miner_data.md" - Backends:
- BMMiner: "miners/backends/bmminer.md"
- BOSMiner: "miners/backends/bosminer.md"
- 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"
- Antminer X19: "miners/antminer/X19.md"
- Avalon 7X: "miners/avalonminer/A7X.md"
- Avalon 8X: "miners/avalonminer/A8X.md"
- Avalon 9X: "miners/avalonminer/A9X.md"
- Avalon 10X: "miners/avalonminer/A10X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md"
- Network:
- Miner Network: "network/miner_network.md"
- Miner Network Range: "network/miner_network_range.md"
- Data:
- Miner Data: "data/miner_data.md"
- Config:
- Miner Config: "config/miner_config.md"
- Advanced: - Advanced:
- API: "api.md" - Miner APIs:
- Base: "API/api.md"
- BMMiner: "API/bmminer.md"
- BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md"
- Unknown: "API/unknown.md"
plugins: plugins:
- mkdocstrings - mkdocstrings

View File

@@ -3,6 +3,7 @@ import json
import ipaddress import ipaddress
import warnings import warnings
import logging import logging
from typing import Union
class APIError(Exception): class APIError(Exception):
@@ -41,7 +42,11 @@ class BaseMinerAPI:
self.ip = ipaddress.ip_address(ip) self.ip = ipaddress.ip_address(ip)
def get_commands(self) -> list: def get_commands(self) -> list:
"""Get a list of command accessible to a specific type of API on the miner.""" """Get a list of command accessible to a specific type of API on the miner.
Returns:
A list of all API commands that the miner supports.
"""
return [ return [
func func
for func in for func in
@@ -59,51 +64,75 @@ class BaseMinerAPI:
] ]
] ]
def _check_commands(self, *commands):
allowed_commands = self.get_commands()
return_commands = []
for command in [*commands]:
if command in allowed_commands:
return_commands.append(command)
else:
warnings.warn(
f"""Removing incorrect command: {command}
If you are sure you want to use this command please use API.send_command("{command}", ignore_errors=True) instead.""",
APIWarning,
)
return return_commands
async def multicommand( async def multicommand(
self, *commands: str, ignore_x19_error: bool = False self, *commands: str, ignore_x19_error: bool = False
) -> dict: ) -> dict:
"""Creates and sends multiple commands as one command to the miner.""" """Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
ignore_x19_error: Whether or not to ignore errors raised by x19 miners when using the "+" delimited style.
"""
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}") logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
# split the commands into a proper list # make sure we can actually run each command, otherwise they will fail
user_commands = [*commands] commands = self._check_commands(*commands)
allowed_commands = self.get_commands()
# make sure we can actually run the command, otherwise it will fail
commands = [command for command in user_commands if command in allowed_commands]
for item in list(set(user_commands) - set(commands)):
warnings.warn(
f"""Removing incorrect command: {item}
If you are sure you want to use this command please use API.send_command("{item}", ignore_errors=True) instead.""",
APIWarning,
)
# standard multicommand format is "command1+command2" # standard multicommand format is "command1+command2"
# doesnt work for S19 which is dealt with in the send command function # doesnt work for S19 which uses the backup _x19_multicommand
command = "+".join(commands) command = "+".join(commands)
data = None
try: try:
data = await self.send_command(command, x19_command=ignore_x19_error) data = await self.send_command(command, x19_command=ignore_x19_error)
except APIError: except APIError:
try: logging.debug(f"{self.ip}: Handling X19 multicommand.")
data = {} data = await self._x19_multicommand(command.split("+"))
# S19 handler, try again logging.debug(f"{self.ip}: Received multicommand data.")
for cmd in command.split("+"): return data
data[cmd] = []
data[cmd].append(await self.send_command(cmd)) async def _x19_multicommand(self, *commands):
except APIError as e: data = None
raise APIError(e) try:
except Exception as e: data = {}
logging.warning(f"{self.ip}: API Multicommand Error: {e}") # send all commands individually
if data: for cmd in commands:
logging.debug(f"{self.ip}: Received multicommand data.") data[cmd] = []
return data data[cmd].append(await self.send_command(cmd, x19_command=True))
except APIError as e:
raise APIError(e)
except Exception as e:
logging.warning(f"{self.ip}: API Multicommand Error: {e.__name__} - {e}")
return data
async def send_command( async def send_command(
self, self,
command: str or bytes, command: Union[str, bytes],
parameters: str or int or bool = None, parameters: Union[str, int, bool] = None,
ignore_errors: bool = False, ignore_errors: bool = False,
x19_command: bool = False, x19_command: bool = False,
) -> dict: ) -> dict:
"""Send an API command to the miner and return the result.""" """Send an API command to the miner and return the result.
Parameters:
command: The command to sent to the miner.
parameters: Any additional parameters to be sent with the command.
ignore_errors: Whether or not to raise APIError when the command returns an error.
x19_command: Whether this is a command for an x19 that may be an issue (such as a "+" delimited multicommand)
Returns:
The return data from the API command parsed from JSON into a dict.
"""
try: try:
# get reader and writer streams # get reader and writer streams
reader, writer = await asyncio.open_connection(str(self.ip), self.port) reader, writer = await asyncio.open_connection(str(self.ip), self.port)
@@ -115,7 +144,7 @@ If you are sure you want to use this command please use API.send_command("{item}
# create the command # create the command
cmd = {"command": command} cmd = {"command": command}
if parameters is not None: if parameters:
cmd["parameter"] = parameters cmd["parameter"] = parameters
# send the command # send the command
@@ -133,9 +162,9 @@ If you are sure you want to use this command please use API.send_command("{item}
break break
data += d data += d
except Exception as e: except Exception as e:
logging.warning(f"{self.ip}: API Command Error: {e}") logging.warning(f"{self.ip}: API Command Error: {e.__name__} - {e}")
data = self.load_api_data(data) data = self._load_api_data(data)
# close the connection # close the connection
writer.close() writer.close()
@@ -144,7 +173,7 @@ If you are sure you want to use this command please use API.send_command("{item}
# check for if the user wants to allow errors to return # check for if the user wants to allow errors to return
if not ignore_errors: if not ignore_errors:
# validate the command succeeded # validate the command succeeded
validation = self.validate_command_output(data) validation = self._validate_command_output(data)
if not validation[0]: if not validation[0]:
if not x19_command: if not x19_command:
logging.warning(f"{self.ip}: API Command Error: {validation[1]}") logging.warning(f"{self.ip}: API Command Error: {validation[1]}")
@@ -153,8 +182,7 @@ If you are sure you want to use this command please use API.send_command("{item}
return data return data
@staticmethod @staticmethod
def validate_command_output(data: dict) -> tuple: def _validate_command_output(data: dict) -> tuple:
"""Check if the returned command output is correctly formatted."""
# check if the data returned is correct or an error # check if the data returned is correct or an error
# if status isn't a key, it is a multicommand # if status isn't a key, it is a multicommand
if "STATUS" not in data.keys(): if "STATUS" not in data.keys():
@@ -181,8 +209,7 @@ If you are sure you want to use this command please use API.send_command("{item}
return True, None return True, None
@staticmethod @staticmethod
def load_api_data(data: bytes) -> dict: def _load_api_data(data: bytes) -> dict:
"""Convert API data from JSON to dict"""
str_data = None str_data = None
try: try:
# some json from the API returns with a null byte (\x00) on the end # some json from the API returns with a null byte (\x00) on the end

View File

@@ -10,7 +10,7 @@ from passlib.handlers.md5_crypt import md5_crypt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from pyasic.API import BaseMinerAPI, APIError from pyasic.API import BaseMinerAPI, APIError
from pyasic.settings import WHATSMINER_PWD from pyasic.settings import PyasicSettings
### IMPORTANT ### ### IMPORTANT ###
@@ -161,7 +161,12 @@ class BTMinerAPI(BaseMinerAPI):
pwd: The admin password of the miner. Default is admin. pwd: The admin password of the miner. Default is admin.
""" """
def __init__(self, ip: str, port: int = 4028, pwd: str = WHATSMINER_PWD): def __init__(
self,
ip: str,
port: int = 4028,
pwd: str = PyasicSettings().global_whatsminer_password,
):
super().__init__(ip, port) super().__init__(ip, port)
self.admin_pwd = pwd self.admin_pwd = pwd
self.current_token = None self.current_token = None
@@ -204,7 +209,7 @@ class BTMinerAPI(BaseMinerAPI):
except Exception as e: except Exception as e:
logging.info(f"{str(self.ip)}: {e}") logging.info(f"{str(self.ip)}: {e}")
data = self.load_api_data(data) data = self._load_api_data(data)
# close the connection # close the connection
writer.close() writer.close()
@@ -220,7 +225,7 @@ class BTMinerAPI(BaseMinerAPI):
if not ignore_errors: if not ignore_errors:
# if it fails to validate, it is likely an error # if it fails to validate, it is likely an error
validation = self.validate_command_output(data) validation = self._validate_command_output(data)
if not validation[0]: if not validation[0]:
raise APIError(validation[1]) raise APIError(validation[1])

View File

@@ -1,5 +1,5 @@
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from typing import List, Literal from typing import Literal, List
import random import random
import string import string
@@ -13,9 +13,10 @@ import time
class _Pool: class _Pool:
"""A dataclass for pool information. """A dataclass for pool information.
:param url: URL of the pool. Attributes:
:param username: Username on the pool. url: URL of the pool.
:param password: Worker password on the pool. username: Username on the pool.
password: Worker password on the pool.
""" """
url: str = "" url: str = ""
@@ -25,7 +26,8 @@ class _Pool:
def from_dict(self, data: dict): def from_dict(self, data: dict):
"""Convert raw pool data as a dict to usable data and save it to this class. """Convert raw pool data as a dict to usable data and save it to this class.
:param data: The raw config data to convert. Parameters:
data: The raw config data to convert.
""" """
for key in data.keys(): for key in data.keys():
if key == "url": if key == "url":
@@ -36,10 +38,11 @@ class _Pool:
self.password = data[key] self.password = data[key]
return self return self
def as_x19(self, user_suffix: str = None): def as_x19(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an X19 device. """Convert the data in this class to a dict usable by an X19 device.
:param user_suffix: The suffix to append to username. Parameters:
user_suffix: The suffix to append to username.
""" """
username = self.username username = self.username
if user_suffix: if user_suffix:
@@ -48,7 +51,12 @@ class _Pool:
pool = {"url": self.url, "user": username, "pass": self.password} pool = {"url": self.url, "user": username, "pass": self.password}
return pool return pool
def as_avalon(self, user_suffix: str = None): def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a string usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username username = self.username
if user_suffix: if user_suffix:
username = f"{username}{user_suffix}" username = f"{username}{user_suffix}"
@@ -56,10 +64,11 @@ class _Pool:
pool = ",".join([self.url, username, self.password]) pool = ",".join([self.url, username, self.password])
return pool return pool
def as_bos(self, user_suffix: str = None): def as_bos(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an BOSMiner device. """Convert the data in this class to a dict usable by an BOSMiner device.
:param user_suffix: The suffix to append to username. Parameters:
user_suffix: The suffix to append to username.
""" """
username = self.username username = self.username
if user_suffix: if user_suffix:
@@ -73,9 +82,10 @@ class _Pool:
class _PoolGroup: class _PoolGroup:
"""A dataclass for pool group information. """A dataclass for pool group information.
:param quota: The group quota. Attributes:
:param group_name: The name of the pool group. quota: The group quota.
:param pools: A list of pools in this group. group_name: The name of the pool group.
pools: A list of pools in this group.
""" """
quota: int = 1 quota: int = 1
@@ -91,7 +101,8 @@ class _PoolGroup:
def from_dict(self, data: dict): def from_dict(self, data: dict):
"""Convert raw pool group data as a dict to usable data and save it to this class. """Convert raw pool group data as a dict to usable data and save it to this class.
:param data: The raw config data to convert. Parameters:
data: The raw config data to convert.
""" """
pools = [] pools = []
for key in data.keys(): for key in data.keys():
@@ -105,24 +116,31 @@ class _PoolGroup:
self.pools = pools self.pools = pools
return self return self
def as_x19(self, user_suffix: str = None): def as_x19(self, user_suffix: str = None) -> List[dict]:
"""Convert the data in this class to a dict usable by an X19 device. """Convert the data in this class to a list usable by an X19 device.
:param user_suffix: The suffix to append to username. Parameters:
user_suffix: The suffix to append to username.
""" """
pools = [] pools = []
for pool in self.pools[:3]: for pool in self.pools[:3]:
pools.append(pool.as_x19(user_suffix=user_suffix)) pools.append(pool.as_x19(user_suffix=user_suffix))
return pools return pools
def as_avalon(self, user_suffix: str = None): def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a dict usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
pool = self.pools[0].as_avalon(user_suffix=user_suffix) pool = self.pools[0].as_avalon(user_suffix=user_suffix)
return pool return pool
def as_bos(self, user_suffix: str = None): def as_bos(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an BOSMiner device. """Convert the data in this class to a dict usable by an BOSMiner device.
:param user_suffix: The suffix to append to username. Parameters:
user_suffix: The suffix to append to username.
""" """
group = { group = {
"name": self.group_name, "name": self.group_name,
@@ -136,21 +154,22 @@ class _PoolGroup:
class MinerConfig: class MinerConfig:
"""A dataclass for miner configuration information. """A dataclass for miner configuration information.
:param pool_groups: A list of pool groups in this config. Attributes:
:param temp_mode: The temperature control mode. pool_groups: A list of pool groups in this config.
:param temp_target: The target temp. temp_mode: The temperature control mode.
:param temp_hot: The hot temp (100% fans). temp_target: The target temp.
:param temp_dangerous: The dangerous temp (shutdown). temp_hot: The hot temp (100% fans).
:param minimum_fans: The minimum numbers of fans to run the miner. temp_dangerous: The dangerous temp (shutdown).
:param fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual"). minimum_fans: The minimum numbers of fans to run the miner.
:param asicboost: Whether or not to enable asicboost. fan_speed: Manual fan speed to run the fan at (only if temp_mode == "manual").
:param autotuning_enabled: Whether or not to enable autotuning. asicboost: Whether or not to enable asicboost.
:param autotuning_wattage: The wattage to use when autotuning. autotuning_enabled: Whether or not to enable autotuning.
:param dps_enabled: Whether or not to enable dynamic power scaling. autotuning_wattage: The wattage to use when autotuning.
:param dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp. dps_enabled: Whether or not to enable dynamic power scaling.
:param dps_min_power: The minimum power to reduce autotuning to. dps_power_step: The amount of power to reduce autotuning by when the miner reaches dangerous temp.
:param dps_shutdown_enabled: Whether or not to shutdown the miner when `dps_min_power` is reached. dps_min_power: The minimum power to reduce autotuning to.
:param dps_shutdown_duration: The amount of time to shutdown for (in hours). dps_shutdown_enabled: Whether or not to shutdown the miner when `dps_min_power` is reached.
dps_shutdown_duration: The amount of time to shutdown for (in hours).
""" """
pool_groups: List[_PoolGroup] = None pool_groups: List[_PoolGroup] = None
@@ -174,27 +193,28 @@ class MinerConfig:
dps_shutdown_enabled: bool = None dps_shutdown_enabled: bool = None
dps_shutdown_duration: float = None dps_shutdown_duration: float = None
def as_dict(self): def as_dict(self) -> dict:
"""Convert the data in this class to a dict.""" """Convert the data in this class to a dict."""
data_dict = asdict(self) data_dict = asdict(self)
for key in asdict(self).keys(): for key in asdict(self).keys():
if data_dict[key] is None: if data_dict[key] is None:
del data_dict[key] del data_dict[key]
return data_dict return data_dict
def as_toml(self): def as_toml(self) -> str:
"""Convert the data in this class to toml.""" """Convert the data in this class to toml."""
return toml.dumps(self.as_dict()) return toml.dumps(self.as_dict())
def as_yaml(self): def as_yaml(self) -> str:
"""Convert the data in this class to yaml.""" """Convert the data in this class to yaml."""
return yaml.dump(self.as_dict(), sort_keys=False) return yaml.dump(self.as_dict(), sort_keys=False)
def from_raw(self, data: dict): def from_raw(self, data: dict):
"""Convert raw config data as a dict to usable data and save it to this class. """Convert raw config data as a dict to usable data and save it to this class.
This should be able to handle any raw config file from any miner supported by pyasic.
:param data: The raw config data to convert. Parameters:
data: The raw config data to convert.
""" """
pool_groups = [] pool_groups = []
for key in data.keys(): for key in data.keys():
@@ -256,7 +276,8 @@ class MinerConfig:
def from_dict(self, data: dict): def from_dict(self, data: dict):
"""Convert an output dict of this class back into usable data and save it to this class. """Convert an output dict of this class back into usable data and save it to this class.
:param data: The raw config data to convert. Parameters:
data: The dict config data to convert.
""" """
pool_groups = [] pool_groups = []
for group in data["pool_groups"]: for group in data["pool_groups"]:
@@ -270,21 +291,24 @@ class MinerConfig:
def from_toml(self, data: str): def from_toml(self, data: str):
"""Convert output toml of this class back into usable data and save it to this class. """Convert output toml of this class back into usable data and save it to this class.
:param data: The raw config data to convert. Parameters:
data: The toml config data to convert.
""" """
return self.from_dict(toml.loads(data)) return self.from_dict(toml.loads(data))
def from_yaml(self, data: str): def from_yaml(self, data: str):
"""Convert output yaml of this class back into usable data and save it to this class. """Convert output yaml of this class back into usable data and save it to this class.
:param data: The raw config data to convert. Parameters:
data: The yaml config data to convert.
""" """
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader)) return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
def as_x19(self, user_suffix: str = None) -> str: def as_x19(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an X19 device. """Convert the data in this class to a config usable by an X19 device.
:param user_suffix: The suffix to append to username. Parameters:
user_suffix: The suffix to append to username.
""" """
cfg = { cfg = {
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix), "pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
@@ -301,14 +325,20 @@ class MinerConfig:
return json.dumps(cfg) return json.dumps(cfg)
def as_avalon(self, user_suffix: str = None) -> str: def as_avalon(self, user_suffix: str = None) -> str:
cfg = self.pool_groups[0].as_avalon() """Convert the data in this class to a config usable by an Avalonminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
return cfg return cfg
def as_bos(self, model: str = "S9", user_suffix: str = None) -> str: def as_bos(self, model: str = "S9", user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an BOSMiner device. """Convert the data in this class to a config usable by an BOSMiner device.
:param model: The model of the miner to be used in the format portion of the config. Parameters:
:param user_suffix: The suffix to append to username. model: The model of the miner to be used in the format portion of the config.
user_suffix: The suffix to append to username.
""" """
cfg = { cfg = {
"format": { "format": {

View File

@@ -1,9 +1,9 @@
import logging import logging
from pyasic.settings import DEBUG, LOGFILE from pyasic.settings import PyasicSettings
def init_logger(): def init_logger():
if LOGFILE: if PyasicSettings().logfile:
logging.basicConfig( logging.basicConfig(
filename="logfile.txt", filename="logfile.txt",
filemode="a", filemode="a",
@@ -18,7 +18,7 @@ def init_logger():
_logger = logging.getLogger() _logger = logging.getLogger()
if DEBUG: if PyasicSettings().debug:
_logger.setLevel(logging.DEBUG) _logger.setLevel(logging.DEBUG)
logging.getLogger("asyncssh").setLevel(logging.DEBUG) logging.getLogger("asyncssh").setLevel(logging.DEBUG)
else: else:

View File

@@ -54,13 +54,11 @@ class BaseMiner:
) )
return conn return conn
except Exception as e: except Exception as e:
# logging.warning(f"{self} raised an exception: {e}")
raise e raise e
except OSError as e: except OSError as e:
logging.warning(f"Connection refused: {self}") logging.warning(f"Connection refused: {self}")
raise e raise e
except Exception as e: except Exception as e:
# logging.warning(f"{self} raised an exception: {e}")
raise e raise e
async def fault_light_on(self) -> bool: async def fault_light_on(self) -> bool:

View File

@@ -1,5 +1,6 @@
import ipaddress import ipaddress
import logging import logging
from typing import Union
from pyasic.API.bmminer import BMMinerAPI from pyasic.API.bmminer import BMMinerAPI
@@ -7,10 +8,12 @@ from pyasic.miners import BaseMiner
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
class BMMiner(BaseMiner): class BMMiner(BaseMiner):
"""Base handler for BMMiner based miners."""
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.ip = ipaddress.ip_address(ip) self.ip = ipaddress.ip_address(ip)
@@ -19,10 +22,11 @@ class BMMiner(BaseMiner):
self.uname = "root" self.uname = "root"
self.pwd = "admin" self.pwd = "admin"
async def get_model(self) -> str or None: async def get_model(self) -> Union[str, None]:
"""Get miner model. """Get miner model.
:return: Miner model or None. Returns:
Miner model or None.
""" """
# check if model is cached # check if model is cached
if self.model: if self.model:
@@ -46,7 +50,8 @@ class BMMiner(BaseMiner):
async def get_hostname(self) -> str: async def get_hostname(self) -> str:
"""Get miner hostname. """Get miner hostname.
:return: The hostname of the miner as a string or "?" Returns:
The hostname of the miner as a string or "?"
""" """
if self.hostname: if self.hostname:
return self.hostname return self.hostname
@@ -72,12 +77,14 @@ class BMMiner(BaseMiner):
logging.warning(f"Failed to get hostname for miner: {self}") logging.warning(f"Failed to get hostname for miner: {self}")
return "?" return "?"
async def send_ssh_command(self, cmd: str) -> str or None: async def send_ssh_command(self, cmd: str) -> Union[str, None]:
"""Send a command to the miner over ssh. """Send a command to the miner over ssh.
:param cmd: The command to run. Parameters:
cmd: The command to run.
:return: Result of the command or None. Returns:
Result of the command or None.
""" """
result = None result = None
@@ -101,10 +108,11 @@ class BMMiner(BaseMiner):
# return the result, either command output or None # return the result, either command output or None
return result return result
async def get_config(self) -> list or None: async def get_config(self) -> Union[list, None]:
"""Get the pool configuration of the miner. """Get the pool configuration of the miner.
:return: Pool config data or None. Returns:
Pool config data or None.
""" """
# get pool data # get pool data
pools = await self.api.pools() pools = await self.api.pools()
@@ -120,6 +128,11 @@ class BMMiner(BaseMiner):
return pool_data return pool_data
async def reboot(self) -> bool: async def reboot(self) -> bool:
"""Reboot the miner.
Returns:
The result of rebooting the miner.
"""
logging.debug(f"{self}: Sending reboot command.") logging.debug(f"{self}: Sending reboot command.")
_ret = await self.send_ssh_command("reboot") _ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.") logging.debug(f"{self}: Reboot command completed.")
@@ -128,6 +141,11 @@ class BMMiner(BaseMiner):
return False return False
async def get_data(self) -> MinerData: async def get_data(self) -> MinerData:
"""Get data from the miner.
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
"""
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3) data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
board_offset = -1 board_offset = -1
@@ -147,7 +165,7 @@ class BMMiner(BaseMiner):
data.mac = mac data.mac = mac
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True "summary", "pools", "stats", ignore_x19_error=True
) )

View File

@@ -1,6 +1,7 @@
import ipaddress import ipaddress
import logging import logging
import json import json
from typing import Union
import toml import toml
@@ -14,7 +15,7 @@ from pyasic.data import MinerData
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
class BOSMiner(BaseMiner): class BOSMiner(BaseMiner):
@@ -27,10 +28,11 @@ class BOSMiner(BaseMiner):
self.pwd = "admin" self.pwd = "admin"
self.config = None self.config = None
async def send_ssh_command(self, cmd: str) -> str or None: async def send_ssh_command(self, cmd: str) -> Union[str, None]:
"""Send a command to the miner over ssh. """Send a command to the miner over ssh.
:return: Result of the command or None. Returns:
Result of the command or None.
""" """
result = None result = None
@@ -74,6 +76,7 @@ class BOSMiner(BaseMiner):
return False return False
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
"""Restart bosminer hashing process. Wraps [`restart_bosminer`][pyasic.miners._backends.bosminer.BOSMiner.restart_bosminer] to standardize."""
return await self.restart_bosminer() return await self.restart_bosminer()
async def restart_bosminer(self) -> bool: async def restart_bosminer(self) -> bool:
@@ -94,7 +97,12 @@ class BOSMiner(BaseMiner):
return True return True
return False return False
async def get_config(self) -> None: async def get_config(self) -> MinerConfig:
"""Gets the config for the miner and sets it as `self.config`.
Returns:
The config from `self.config`.
"""
logging.debug(f"{self}: Getting config.") logging.debug(f"{self}: Getting config.")
async with (await self._get_ssh_connection()) as conn: async with (await self._get_ssh_connection()) as conn:
logging.debug(f"{self}: Opening SFTP connection.") logging.debug(f"{self}: Opening SFTP connection.")
@@ -110,7 +118,8 @@ class BOSMiner(BaseMiner):
async def get_hostname(self) -> str: async def get_hostname(self) -> str:
"""Get miner hostname. """Get miner hostname.
:return: The hostname of the miner as a string or "?" Returns:
The hostname of the miner as a string or "?"
""" """
if self.hostname: if self.hostname:
return self.hostname return self.hostname
@@ -129,10 +138,11 @@ class BOSMiner(BaseMiner):
logging.warning(f"Failed to get hostname for miner: {self}") logging.warning(f"Failed to get hostname for miner: {self}")
return "?" return "?"
async def get_model(self) -> str or None: async def get_model(self) -> Union[str, None]:
"""Get miner model. """Get miner model.
:return: Miner model or None. Returns:
Miner model or None.
""" """
# check if model is cached # check if model is cached
if self.model: if self.model:
@@ -166,10 +176,11 @@ class BOSMiner(BaseMiner):
logging.warning(f"Failed to get model for miner: {self}") logging.warning(f"Failed to get model for miner: {self}")
return None return None
async def get_version(self): async def get_version(self) -> Union[str, None]:
"""Get miner firmware version. """Get miner firmware version.
:return: Miner firmware version or None. Returns:
Miner firmware version or None.
""" """
# check if version is cached # check if version is cached
if self.version: if self.version:
@@ -206,65 +217,21 @@ class BOSMiner(BaseMiner):
.as_bos(model=self.model.replace(" (BOS)", "")) .as_bos(model=self.model.replace(" (BOS)", ""))
) )
async with (await self._get_ssh_connection()) as conn: async with (await self._get_ssh_connection()) as conn:
await conn.run("/etc/init.d/bosminer stop")
logging.debug(f"{self}: Opening SFTP connection.") logging.debug(f"{self}: Opening SFTP connection.")
async with conn.start_sftp_client() as sftp: async with conn.start_sftp_client() as sftp:
logging.debug(f"{self}: Opening config file.") logging.debug(f"{self}: Opening config file.")
async with sftp.open("/etc/bosminer.toml", "w+") as file: async with sftp.open("/etc/bosminer.toml", "w+") as file:
await file.write(toml_conf) await file.write(toml_conf)
logging.debug(f"{self}: Restarting BOSMiner") logging.debug(f"{self}: Restarting BOSMiner")
await conn.run("/etc/init.d/bosminer restart") await conn.run("/etc/init.d/bosminer start")
async def get_board_info(self) -> dict:
"""Gets data on each board and chain in the miner."""
logging.debug(f"{self}: Getting board info.")
devdetails = await self.api.devdetails()
if not devdetails.get("DEVDETAILS"):
print("devdetails error", devdetails)
return {0: [], 1: [], 2: []}
devs = devdetails["DEVDETAILS"]
boards = {}
offset = devs[0]["ID"]
for board in devs:
boards[board["ID"] - offset] = []
if not board["Chips"] == self.nominal_chips:
nominal = False
else:
nominal = True
boards[board["ID"] - offset].append(
{
"chain": board["ID"] - offset,
"chip_count": board["Chips"],
"chip_status": "o" * board["Chips"],
"nominal": nominal,
}
)
logging.debug(f"Found board data for {self}: {boards}")
return boards
async def get_bad_boards(self) -> dict:
"""Checks for and provides list of non working boards."""
boards = await self.get_board_info()
bad_boards = {}
for board in boards.keys():
for chain in boards[board]:
if not chain["chip_count"] == 63:
if board not in bad_boards.keys():
bad_boards[board] = []
bad_boards[board].append(chain)
return bad_boards
async def check_good_boards(self) -> str:
"""Checks for and provides list for working boards."""
devs = await self.api.devdetails()
bad = 0
chains = devs["DEVDETAILS"]
for chain in chains:
if chain["Chips"] == 0:
bad += 1
if not bad > 0:
return str(self.ip)
async def get_data(self) -> MinerData: async def get_data(self) -> MinerData:
"""Get data from the miner.
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
"""
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3) data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
board_offset = -1 board_offset = -1
@@ -284,7 +251,7 @@ class BOSMiner(BaseMiner):
data.mac = mac data.mac = mac
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
try: try:
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"summary", "summary",

View File

@@ -1,5 +1,6 @@
import ipaddress import ipaddress
import logging import logging
from typing import Union
from pyasic.API.btminer import BTMinerAPI from pyasic.API.btminer import BTMinerAPI
@@ -9,7 +10,7 @@ from pyasic.API import APIError
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.data.error_codes import WhatsminerError from pyasic.data.error_codes import WhatsminerError
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
class BTMiner(BaseMiner): class BTMiner(BaseMiner):
@@ -19,7 +20,12 @@ class BTMiner(BaseMiner):
self.api = BTMinerAPI(ip) self.api = BTMinerAPI(ip)
self.api_type = "BTMiner" self.api_type = "BTMiner"
async def get_model(self): async def get_model(self) -> Union[str, None]:
"""Get miner model.
Returns:
Miner model or None.
"""
if self.model: if self.model:
logging.debug(f"Found model for {self.ip}: {self.model}") logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model return self.model
@@ -31,7 +37,12 @@ class BTMiner(BaseMiner):
logging.warning(f"Failed to get model for miner: {self}") logging.warning(f"Failed to get model for miner: {self}")
return None return None
async def get_hostname(self) -> str or None: async def get_hostname(self) -> Union[str, None]:
"""Get miner hostname.
Returns:
The hostname of the miner as a string or None.
"""
if self.hostname: if self.hostname:
return self.hostname return self.hostname
try: try:
@@ -48,38 +59,12 @@ class BTMiner(BaseMiner):
logging.warning(f"Failed to get hostname for miner: {self}") logging.warning(f"Failed to get hostname for miner: {self}")
return None return None
async def get_board_info(self) -> dict: async def get_mac(self) -> str:
"""Gets data on each board and chain in the miner.""" """Get the mac address of the miner.
logging.debug(f"{self}: Getting board info.")
devs = await self.api.devs()
if not devs.get("DEVS"):
print("devs error", devs)
return {0: [], 1: [], 2: []}
devs = devs["DEVS"]
boards = {}
offset = devs[0]["ID"]
for board in devs:
boards[board["ID"] - offset] = []
if "Effective Chips" in board.keys():
if not board["Effective Chips"] in self.nominal_chips:
nominal = False
else:
nominal = True
boards[board["ID"] - offset].append(
{
"chain": board["ID"] - offset,
"chip_count": board["Effective Chips"],
"chip_status": "o" * board["Effective Chips"],
"nominal": nominal,
}
)
else:
logging.warning(f"Incorrect board data from {self}: {board}")
print(board)
logging.debug(f"Found board data for {self}: {boards}")
return boards
async def get_mac(self): Returns:
The mac address of the miner as a string.
"""
mac = "" mac = ""
data = await self.api.summary() data = await self.api.summary()
if data: if data:
@@ -100,7 +85,12 @@ class BTMiner(BaseMiner):
return str(mac).upper() return str(mac).upper()
async def get_data(self): async def get_data(self) -> MinerData:
"""Get data from the miner.
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
"""
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3) data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
mac = None mac = None
@@ -126,7 +116,7 @@ class BTMiner(BaseMiner):
data.hostname = hostname data.hostname = hostname
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
try: try:
miner_data = await self.api.multicommand("summary", "devs", "pools") miner_data = await self.api.multicommand("summary", "devs", "pools")
if miner_data: if miner_data:

View File

@@ -1,5 +1,6 @@
import ipaddress import ipaddress
import logging import logging
from typing import Union
from pyasic.API.cgminer import CGMinerAPI from pyasic.API.cgminer import CGMinerAPI
@@ -8,7 +9,7 @@ from pyasic.API import APIError
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
class CGMiner(BaseMiner): class CGMiner(BaseMiner):
@@ -21,7 +22,12 @@ class CGMiner(BaseMiner):
self.pwd = "admin" self.pwd = "admin"
self.config = None self.config = None
async def get_model(self): async def get_model(self) -> Union[str, None]:
"""Get miner model.
Returns:
Miner model or None.
"""
if self.model: if self.model:
return self.model return self.model
try: try:
@@ -33,7 +39,12 @@ class CGMiner(BaseMiner):
return self.model return self.model
return None return None
async def get_hostname(self) -> str or None: async def get_hostname(self) -> Union[str, None]:
"""Get miner hostname.
Returns:
The hostname of the miner as a string or "?"
"""
if self.hostname: if self.hostname:
return self.hostname return self.hostname
try: try:
@@ -48,7 +59,15 @@ class CGMiner(BaseMiner):
except Exception: except Exception:
return None return None
async def send_ssh_command(self, cmd): async def send_ssh_command(self, cmd: str) -> Union[str, None]:
"""Send a command to the miner over ssh.
Parameters:
cmd: The command to run.
Returns:
Result of the command or None.
"""
result = None result = None
async with (await self._get_ssh_connection()) as conn: async with (await self._get_ssh_connection()) as conn:
for i in range(3): for i in range(3):
@@ -63,9 +82,11 @@ class CGMiner(BaseMiner):
return result return result
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
"""Restart cgminer hashing process. Wraps [`restart_cgminer`][pyasic.miners._backends.cgminer.CGMiner.restart_cgminer] to standardize."""
return await self.restart_cgminer() return await self.restart_cgminer()
async def restart_cgminer(self) -> bool: async def restart_cgminer(self) -> bool:
"""Restart cgminer hashing process."""
commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"] commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"]
commands = ";".join(commands) commands = ";".join(commands)
_ret = await self.send_ssh_command(commands) _ret = await self.send_ssh_command(commands)
@@ -74,6 +95,7 @@ class CGMiner(BaseMiner):
return False return False
async def reboot(self) -> bool: async def reboot(self) -> bool:
"""Reboots power to the physical miner."""
logging.debug(f"{self}: Sending reboot command.") logging.debug(f"{self}: Sending reboot command.")
_ret = await self.send_ssh_command("reboot") _ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.") logging.debug(f"{self}: Reboot command completed.")
@@ -82,6 +104,7 @@ class CGMiner(BaseMiner):
return False return False
async def start_cgminer(self) -> None: async def start_cgminer(self) -> None:
"""Start cgminer hashing process."""
commands = [ commands = [
"mkdir -p /etc/tmp/", "mkdir -p /etc/tmp/",
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root', 'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
@@ -92,6 +115,7 @@ class CGMiner(BaseMiner):
await self.send_ssh_command(commands) await self.send_ssh_command(commands)
async def stop_cgminer(self) -> None: async def stop_cgminer(self) -> None:
"""Restart cgminer hashing process."""
commands = [ commands = [
"mkdir -p /etc/tmp/", "mkdir -p /etc/tmp/",
'echo "" > /etc/tmp/root', 'echo "" > /etc/tmp/root',
@@ -101,14 +125,24 @@ class CGMiner(BaseMiner):
commands = ";".join(commands) commands = ";".join(commands)
await self.send_ssh_command(commands) await self.send_ssh_command(commands)
async def get_config(self) -> None: async def get_config(self) -> str:
"""Gets the config for the miner and sets it as `self.config`.
Returns:
The config from `self.config`.
"""
async with (await self._get_ssh_connection()) as conn: async with (await self._get_ssh_connection()) as conn:
command = "cat /etc/config/cgminer" command = "cat /etc/config/cgminer"
result = await conn.run(command, check=True) result = await conn.run(command, check=True)
self.config = result.stdout self.config = result.stdout
print(str(self.config)) return self.config
async def get_data(self): async def get_data(self) -> MinerData:
"""Get data from the miner.
Returns:
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
"""
data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3) data = MinerData(ip=str(self.ip), ideal_chips=self.nominal_chips * 3)
board_offset = -1 board_offset = -1
@@ -128,7 +162,7 @@ class CGMiner(BaseMiner):
data.mac = mac data.mac = mac
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True "summary", "pools", "stats", ignore_x19_error=True
) )

View File

@@ -8,3 +8,21 @@ class M20S(BaseMiner):
self.model = "M20S" self.model = "M20S"
self.nominal_chips = 66 self.nominal_chips = 66
self.fan_count = 2 self.fan_count = 2
class M20SV10(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S"
self.nominal_chips = 105
self.fan_count = 2
class M20SV20(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M20S"
self.nominal_chips = 111
self.fan_count = 2

View File

@@ -1,4 +1,4 @@
from .M20S import M20S from .M20S import M20S, M20SV10, M20SV20
from .M20S_Plus import M20SPlus from .M20S_Plus import M20SPlus
from .M21 import M21 from .M21 import M21

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon1026 # noqa - Ignore access to _module from pyasic.miners._types import Avalon1026 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon1026(CGMiner, Avalon1026):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon1047 # noqa - Ignore access to _module from pyasic.miners._types import Avalon1047 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon1047(CGMiner, Avalon1047):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon1066 # noqa - Ignore access to _module from pyasic.miners._types import Avalon1066 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon1066(CGMiner, Avalon1066):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon721 # noqa - Ignore access to _module from pyasic.miners._types import Avalon721 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon721(CGMiner, Avalon721):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon741 # noqa - Ignore access to _module from pyasic.miners._types import Avalon741 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon741(CGMiner, Avalon741):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon761 # noqa - Ignore access to _module from pyasic.miners._types import Avalon761 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon761(CGMiner, Avalon761):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon821 # noqa - Ignore access to _module from pyasic.miners._types import Avalon821 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon821(CGMiner, Avalon821):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon841 # noqa - Ignore access to _module from pyasic.miners._types import Avalon841 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon841(CGMiner, Avalon841):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon851 # noqa - Ignore access to _module from pyasic.miners._types import Avalon851 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon851(CGMiner, Avalon851):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -2,7 +2,7 @@ from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import Avalon921 # noqa - Ignore access to _module from pyasic.miners._types import Avalon921 # noqa - Ignore access to _module
from pyasic.data import MinerData from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES from pyasic.settings import PyasicSettings
import re import re
from pyasic.config import MinerConfig from pyasic.config import MinerConfig
import logging import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon921(CGMiner, Avalon921):
data.model = model data.model = model
miner_data = None miner_data = None
for i in range(DATA_RETRIES): for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand( miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats" "version", "summary", "pools", "stats"
) )

View File

@@ -1,6 +1,7 @@
from typing import TypeVar, Tuple, List, Union from typing import TypeVar, Tuple, List, Union
from collections.abc import AsyncIterable from collections.abc import AsyncIterable
from pyasic.miners import BaseMiner from pyasic.miners import BaseMiner
import httpx
from pyasic.miners.antminer import * from pyasic.miners.antminer import *
from pyasic.miners.avalonminer import * from pyasic.miners.avalonminer import *
@@ -23,10 +24,7 @@ import ipaddress
import json import json
import logging import logging
from pyasic.settings import ( from pyasic.settings import PyasicSettings
MINER_FACTORY_GET_VERSION_RETRIES as GET_VERSION_RETRIES,
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
)
import asyncssh import asyncssh
@@ -123,6 +121,8 @@ MINER_CLASSES = {
"M20S": { "M20S": {
"Default": BTMinerM20S, "Default": BTMinerM20S,
"BTMiner": BTMinerM20S, "BTMiner": BTMinerM20S,
"10": BTMinerM20SV10,
"20": BTMinerM20SV20,
}, },
"M20S+": { "M20S+": {
"Default": BTMinerM20SPlus, "Default": BTMinerM20SPlus,
@@ -282,13 +282,12 @@ class MinerFactory(metaclass=Singleton):
ver = None ver = None
# try to get the API multiple times based on retries # try to get the API multiple times based on retries
for i in range(GET_VERSION_RETRIES): for i in range(PyasicSettings().miner_factory_get_version_retries):
try: try:
# get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None # get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None
new_model, new_api, new_ver = await asyncio.wait_for( new_model, new_api, new_ver = await asyncio.wait_for(
self._get_miner_type(ip), timeout=PING_TIMEOUT self._get_miner_type(ip), timeout=10
) )
# keep track of the API and model we found first # keep track of the API and model we found first
if new_api and not api: if new_api and not api:
api = new_api api = new_api
@@ -296,13 +295,11 @@ class MinerFactory(metaclass=Singleton):
model = new_model model = new_model
if new_ver and not ver: if new_ver and not ver:
ver = new_ver ver = new_ver
# if we find the API and model, don't need to loop anymore # if we find the API and model, don't need to loop anymore
if api and model: if api and model:
break break
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass logging.warning(f"{ip}: Get Miner Timed Out")
# make sure we have model information # make sure we have model information
if model: if model:
if not api: if not api:
@@ -310,12 +307,10 @@ class MinerFactory(metaclass=Singleton):
if model not in MINER_CLASSES.keys(): if model not in MINER_CLASSES.keys():
if "avalon" in model: if "avalon" in model:
print(model)
if model == "avalon10": if model == "avalon10":
miner = CGMinerAvalon1066(str(ip)) miner = CGMinerAvalon1066(str(ip))
else: else:
miner = CGMinerAvalon821(str(ip)) miner = CGMinerAvalon821(str(ip))
miner = UnknownMiner(str(ip))
return miner return miner
if api not in MINER_CLASSES[model].keys(): if api not in MINER_CLASSES[model].keys():
api = "Default" api = "Default"
@@ -355,32 +350,26 @@ class MinerFactory(metaclass=Singleton):
async def _get_miner_type( async def _get_miner_type(
self, ip: ipaddress.ip_address or str self, ip: ipaddress.ip_address or str
) -> Tuple[str or None, str or None, str or None]: ) -> Tuple[str or None, str or None, str or None]:
data = None
model = None model = None
api = None api = None
ver = None ver = None
devdetails = None devdetails = None
version = None version = None
try: try:
# get device details and version data # get device details and version data
data = await self._send_api_command(str(ip), "devdetails+version") data = await self._send_api_command(str(ip), "devdetails+version")
# validate success # validate success
validation = await self._validate_command(data) validation = await self._validate_command(data)
if not validation[0]: if not validation[0]:
raise APIError(validation[1]) raise APIError(validation[1])
# copy each part of the main command to devdetails and version # copy each part of the main command to devdetails and version
devdetails = data["devdetails"][0] devdetails = data["devdetails"][0]
version = data["version"][0] version = data["version"][0]
except APIError: except APIError:
# if getting data fails we need to check again
data = None
# if data is None then get it a slightly different way
if not data:
try: try:
# try devdetails and version separately (X19s mainly require this) # try devdetails and version separately (X19s mainly require this)
# get devdetails and validate # get devdetails and validate
@@ -405,6 +394,44 @@ class MinerFactory(metaclass=Singleton):
# catch APIError and let the factory know we cant get data # catch APIError and let the factory know we cant get data
logging.warning(f"{ip}: API Command Error: {e}") logging.warning(f"{ip}: API Command Error: {e}")
return None, None, None return None, None, None
except OSError as e:
# miner refused connection on API port, we wont be able to get data this way
# try ssh
try:
async with asyncssh.connect(
str(ip),
known_hosts=None,
username="root",
password="admin",
server_host_key_algs=["ssh-rsa"],
) as conn:
board_name = None
cmd = await conn.run("cat /tmp/sysinfo/board_name")
if cmd:
board_name = cmd.stdout.strip()
if board_name:
if board_name == "am1-s9":
model = "Antminer S9"
if board_name == "am2-s17":
model = "Antminer S17"
api = "BOSMiner+"
return model, api, None
except asyncssh.misc.PermissionDenied:
try:
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if "minertype" in data.keys():
model = data["minertype"]
if "bmminer" in "\t".join(data.keys()):
api = "BMMiner"
except:
return None, None, None
# if we have devdetails, we can get model data from there # if we have devdetails, we can get model data from there
if devdetails: if devdetails:
@@ -452,24 +479,24 @@ class MinerFactory(metaclass=Singleton):
if "BOSminer+" in version["VERSION"][0].keys(): if "BOSminer+" in version["VERSION"][0].keys():
api = "BOSMiner+" api = "BOSMiner+"
# check for avalonminers
if version["VERSION"][0].get("PROD"):
_data = version["VERSION"][0]["PROD"].split("-")
model = _data[0]
if len(data) > 1:
ver = _data[1]
elif version["VERSION"][0].get("MODEL"):
_data = version["VERSION"][0]["MODEL"].split("-")
model = f"AvalonMiner {_data[0]}"
if len(data) > 1:
ver = _data[1]
# if all that fails, check the Description to see if it is a whatsminer # if all that fails, check the Description to see if it is a whatsminer
if version.get("Description") and ( if version.get("Description") and (
"whatsminer" in version.get("Description") "whatsminer" in version.get("Description")
): ):
api = "BTMiner" api = "BTMiner"
# check for avalonminers
if version["VERSION"][0].get("PROD"):
_data = version["VERSION"][0]["PROD"].split("-")
model = _data[0]
if len(data) > 1:
ver = _data[1]
elif version["VERSION"][0].get("MODEL"):
_data = version["VERSION"][0]["MODEL"].split("-")
model = f"AvalonMiner {_data[0]}"
if len(data) > 1:
ver = _data[1]
# if we have no model from devdetails but have version, try to get it from there # if we have no model from devdetails but have version, try to get it from there
if version and not model: if version and not model:
# make sure version isn't blank # make sure version isn't blank
@@ -486,23 +513,6 @@ class MinerFactory(metaclass=Singleton):
elif "am2-s17" in version["STATUS"][0]["Description"]: elif "am2-s17" in version["STATUS"][0]["Description"]:
model = "Antminer S17" model = "Antminer S17"
# final try on a braiins OS bug with devdetails not returning
else:
try:
async with asyncssh.connect(
str(ip),
known_hosts=None,
username="root",
password="admin",
server_host_key_algs=["ssh-rsa"],
) as conn:
cfg = await conn.run("bosminer config --data")
if cfg:
cfg = json.loads(cfg.stdout)
model = cfg.get("data").get("format").get("model")
except asyncssh.misc.PermissionDenied:
pass
if model: if model:
# whatsminer have a V in their version string (M20SV41), remove everything after it # whatsminer have a V in their version string (M20SV41), remove everything after it
if "V" in model: if "V" in model:
@@ -519,7 +529,7 @@ class MinerFactory(metaclass=Singleton):
async def _validate_command(data: dict) -> Tuple[bool, str or None]: async def _validate_command(data: dict) -> Tuple[bool, str or None]:
"""Check if the returned command output is correctly formatted.""" """Check if the returned command output is correctly formatted."""
# check if the data returned is correct or an error # check if the data returned is correct or an error
if not data: if not data or data == {}:
return False, "No API data." return False, "No API data."
# if status isn't a key, it is a multicommand # if status isn't a key, it is a multicommand
if "STATUS" not in data.keys(): if "STATUS" not in data.keys():
@@ -548,9 +558,10 @@ class MinerFactory(metaclass=Singleton):
# get reader and writer streams # get reader and writer streams
reader, writer = await asyncio.open_connection(str(ip), 4028) reader, writer = await asyncio.open_connection(str(ip), 4028)
except OSError as e: except OSError as e:
if e.errno in [10061, 22] or e.winerror == 1225:
raise e
logging.warning(f"{str(ip)} - Command {command}: {e}") logging.warning(f"{str(ip)} - Command {command}: {e}")
return {} return {}
# create the command # create the command
cmd = {"command": command} cmd = {"command": command}

View File

@@ -1,8 +1,24 @@
from pyasic.miners._backends import BTMiner # noqa - Ignore access to _module from pyasic.miners._backends import BTMiner # noqa - Ignore access to _module
from pyasic.miners._types import M20S # noqa - Ignore access to _module from pyasic.miners._types import ( # noqa - Ignore access to _module
M20S,
M20SV10,
M20SV20,
)
class BTMinerM20S(BTMiner, M20S): class BTMinerM20S(BTMiner, M20S):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.ip = ip self.ip = ip
class BTMinerM20SV10(BTMiner, M20SV10):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM20SV20(BTMiner, M20SV20):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -1,4 +1,4 @@
from .M20S import BTMinerM20S from .M20S import BTMinerM20S, BTMinerM20SV10, BTMinerM20SV20
from .M20S_Plus import BTMinerM20SPlus from .M20S_Plus import BTMinerM20SPlus
from .M21 import BTMinerM21 from .M21 import BTMinerM21

View File

@@ -14,7 +14,7 @@ class BTMinerM30S(BTMiner, M30S):
self.ip = ip self.ip = ip
class BTMinerM30SV50(BTMiner, M30SV50): class BTMinerM30SVE10(BTMiner, M30SVE10):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.ip = ip self.ip = ip
@@ -32,7 +32,7 @@ class BTMinerM30SVE20(BTMiner, M30SVE20):
self.ip = ip self.ip = ip
class BTMinerM30SVE10(BTMiner, M30SVE10): class BTMinerM30SV50(BTMiner, M30SV50):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.ip = ip self.ip = ip

View File

@@ -13,13 +13,13 @@ class BTMinerM30SPlus(BTMiner, M30SPlus):
self.ip = ip self.ip = ip
class BTMinerM30SPlusVE40(BTMiner, M30SPlusVE40): class BTMinerM30SPlusVF20(BTMiner, M30SPlusVF20):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.ip = ip self.ip = ip
class BTMinerM30SPlusVF20(BTMiner, M30SPlusVF20): class BTMinerM30SPlusVE40(BTMiner, M30SPlusVE40):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.ip = ip self.ip = ip

View File

@@ -12,13 +12,13 @@ class BTMinerM30SPlusPlus(BTMiner, M30SPlusPlus):
self.ip = ip self.ip = ip
class BTMinerM30SPlusPlusVG40(BTMiner, M30SPlusPlusVG40):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM30SPlusPlusVG30(BTMiner, M30SPlusPlusVG30): class BTMinerM30SPlusPlusVG30(BTMiner, M30SPlusPlusVG30):
def __init__(self, ip: str) -> None: def __init__(self, ip: str) -> None:
super().__init__(ip) super().__init__(ip)
self.ip = ip self.ip = ip
class BTMinerM30SPlusPlusVG40(BTMiner, M30SPlusPlusVG40):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -1,70 +0,0 @@
import asyncio
from pyasic.network import MinerNetwork
from pyasic.miners._backends.bosminer import BOSMiner # noqa - Ignore access to _module
async def get_bos_bad_tuners(ip: str = "192.168.1.0", mask: int = 24):
# create a miner network
miner_network = MinerNetwork(ip, mask=mask)
# scan for miners
miners = await miner_network.scan_network_for_miners()
# create an empty list of tasks
tuner_tasks = []
# loop checks if the miner is a BOSMiner
for miner in miners:
# can only do this if its a subclass of BOSMiner
if BOSMiner in type(miner).__bases__:
tuner_tasks.append(_get_tuner_status(miner))
# run all the tuner status commands
tuner_status = await asyncio.gather(*tuner_tasks)
# create a list of all miners with bad board tuner status
bad_tuner_miners = []
for item in tuner_status:
# loop through and get each miners' bad board count
bad_boards = []
for board in item["tuner_status"]:
# if its not stable or still testing, its bad
if board["status"] not in [
"Stable",
"Testing performance profile",
"Tuning individual chips",
]:
# remove the part about the board refusing to start
bad_boards.append(
{
"board": board["board"],
"error": board["status"].replace(
"Hashchain refused to start: ", ""
),
}
)
# if this miner has bad boards, add it to the list of bad board miners
if len(bad_boards) > 0:
bad_tuner_miners.append({"ip": item["ip"], "boards": bad_boards})
# return the list of bad board miners
return bad_tuner_miners
async def _get_tuner_status(miner):
# run the tunerstatus command, since the miner will always be BOSMiner
tuner_status = await miner.api.tunerstatus()
# create a list to add the tuner data to
tuner_data = []
# if we have data, loop through to get the hashchain status
if tuner_status:
for board in tuner_status["TUNERSTATUS"][0]["TunerChainStatus"]:
tuner_data.append(
{"board": board["HashchainIndex"], "status": board["Status"]}
)
# return the data along with the IP or later tracking
return {"ip": str(miner.ip), "tuner_status": tuner_data}

View File

@@ -1,15 +1,11 @@
import ipaddress import ipaddress
import asyncio import asyncio
import logging import logging
from typing import Union from typing import Union, List, AsyncIterator
from pyasic.network.net_range import MinerNetworkRange from pyasic.network.net_range import MinerNetworkRange
from pyasic.miners.miner_factory import MinerFactory from pyasic.miners.miner_factory import MinerFactory, AnyMiner
from pyasic.settings import ( from pyasic.settings import PyasicSettings
NETWORK_PING_RETRIES as PING_RETRIES,
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
NETWORK_SCAN_THREADS as SCAN_THREADS,
)
class MinerNetwork: class MinerNetwork:
@@ -32,8 +28,9 @@ class MinerNetwork:
self.network = None self.network = None
self.ip_addr = ip_addr self.ip_addr = ip_addr
self.connected_miners = {} self.connected_miners = {}
if mask.startswith("/"): if isinstance(mask, str):
mask = mask.replace("/", "") if mask.startswith("/"):
mask = mask.replace("/", "")
self.mask = mask self.mask = mask
def __len__(self): def __len__(self):
@@ -43,7 +40,11 @@ class MinerNetwork:
return str(self.network) return str(self.network)
def get_network(self) -> ipaddress.ip_network: def get_network(self) -> ipaddress.ip_network:
"""Get the network using the information passed to the MinerNetwork or from cache.""" """Get the network using the information passed to the MinerNetwork or from cache.
Returns:
The proper network to be able to scan.
"""
# if we have a network cached already, use that # if we have a network cached already, use that
if self.network: if self.network:
return self.network return self.network
@@ -71,13 +72,19 @@ class MinerNetwork:
self.network = ipaddress.ip_network( self.network = ipaddress.ip_network(
f"{default_gateway}/{subnet_mask}", strict=False f"{default_gateway}/{subnet_mask}", strict=False
) )
logging.debug(f"Setting MinerNetwork: {self.network}")
return self.network return self.network
async def scan_network_for_miners(self) -> None or list: async def scan_network_for_miners(self) -> List[AnyMiner]:
"""Scan the network for miners, and return found miners as a list.""" """Scan the network for miners, and return found miners as a list.
Returns:
A list of found miners.
"""
# get the network # get the network
local_network = self.get_network() local_network = self.get_network()
print(f"Scanning {local_network} for miners...") logging.debug(f"Scanning {local_network} for miners")
# clear cached miners # clear cached miners
MinerFactory().clear_cached_miners() MinerFactory().clear_cached_miners()
@@ -90,7 +97,7 @@ class MinerNetwork:
for host in local_network.hosts(): for host in local_network.hosts():
# make sure we don't exceed the allowed async tasks # make sure we don't exceed the allowed async tasks
if len(scan_tasks) < SCAN_THREADS: if len(scan_tasks) < round(PyasicSettings().network_scan_threads / 3):
# add the task to the list # add the task to the list
scan_tasks.append(self.ping_and_get_miner(host)) scan_tasks.append(self.ping_and_get_miner(host))
else: else:
@@ -106,16 +113,17 @@ class MinerNetwork:
# remove all None from the miner list # remove all None from the miner list
miners = list(filter(None, miners)) miners = list(filter(None, miners))
print(f"Found {len(miners)} connected miners...") logging.debug(f"Found {len(miners)} connected miners")
# return the miner objects # return the miner objects
return miners return miners
async def scan_network_generator(self): async def scan_network_generator(self) -> AsyncIterator[AnyMiner]:
""" """
Scan the network for miners using an async generator. Scan the network for miners using an async generator.
Returns an asynchronous generator containing found miners. Returns:
An asynchronous generator containing found miners.
""" """
# get the current event loop # get the current event loop
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@@ -129,7 +137,7 @@ class MinerNetwork:
# for each ip on the network, loop through and scan it # for each ip on the network, loop through and scan it
for host in local_network.hosts(): for host in local_network.hosts():
# make sure we don't exceed the allowed async tasks # make sure we don't exceed the allowed async tasks
if len(scan_tasks) >= SCAN_THREADS: if len(scan_tasks) >= round(PyasicSettings().network_scan_threads / 3):
# scanned is a loopable list of awaitables # scanned is a loopable list of awaitables
scanned = asyncio.as_completed(scan_tasks) scanned = asyncio.as_completed(scan_tasks)
# when we scan, empty the scan tasks # when we scan, empty the scan tasks
@@ -148,25 +156,33 @@ class MinerNetwork:
yield await miner yield await miner
@staticmethod @staticmethod
async def ping_miner(ip: ipaddress.ip_address) -> None or ipaddress.ip_address: async def ping_miner(ip: ipaddress.ip_address) -> Union[None, ipaddress.ip_address]:
return await ping_miner(ip) tasks = [ping_miner(ip, port=port) for port in [4028, 4029, 8889]]
for miner in asyncio.as_completed(tasks):
miner = await miner
if miner:
return miner
@staticmethod @staticmethod
async def ping_and_get_miner( async def ping_and_get_miner(
ip: ipaddress.ip_address, ip: ipaddress.ip_address,
) -> None or ipaddress.ip_address: ) -> Union[None, AnyMiner]:
return await ping_and_get_miner(ip) tasks = [ping_and_get_miner(ip, port=port) for port in [4028, 4029, 8889]]
for miner in asyncio.as_completed(tasks):
miner = await miner
if miner:
return miner
async def ping_miner( async def ping_miner(
ip: ipaddress.ip_address, port=4028 ip: ipaddress.ip_address, port=4028
) -> None or ipaddress.ip_address: ) -> Union[None, ipaddress.ip_address]:
for i in range(PING_RETRIES): for i in range(PyasicSettings().network_ping_retries):
connection_fut = asyncio.open_connection(str(ip), port) connection_fut = asyncio.open_connection(str(ip), port)
try: try:
# get the read and write streams from the connection # get the read and write streams from the connection
reader, writer = await asyncio.wait_for( reader, writer = await asyncio.wait_for(
connection_fut, timeout=PING_TIMEOUT connection_fut, timeout=PyasicSettings().network_ping_timeout
) )
# immediately close connection, we know connection happened # immediately close connection, we know connection happened
writer.close() writer.close()
@@ -189,13 +205,13 @@ async def ping_miner(
async def ping_and_get_miner( async def ping_and_get_miner(
ip: ipaddress.ip_address, port=4028 ip: ipaddress.ip_address, port=4028
) -> None or ipaddress.ip_address: ) -> Union[None, AnyMiner]:
for i in range(PING_RETRIES): for i in range(PyasicSettings().network_ping_retries):
connection_fut = asyncio.open_connection(str(ip), port) connection_fut = asyncio.open_connection(str(ip), port)
try: try:
# get the read and write streams from the connection # get the read and write streams from the connection
reader, writer = await asyncio.wait_for( reader, writer = await asyncio.wait_for(
connection_fut, timeout=PING_TIMEOUT connection_fut, timeout=PyasicSettings().network_ping_timeout
) )
# immediately close connection, we know connection happened # immediately close connection, we know connection happened
writer.close() writer.close()

View File

@@ -8,9 +8,9 @@ class MinerNetworkRange:
Parameters: Parameters:
ip_range: ## A range of IP addresses to put in the network, or a list of IPs ip_range: ## A range of IP addresses to put in the network, or a list of IPs
* Takes a string formatted as: * Takes a string formatted as:
* {ip_range_1_start}-{ip_range_1_end}, {ip_address_1}, {ip_range_2_start}-{ip_range_2_end}, {ip_address_2}... ```f"{ip_range_1_start}-{ip_range_1_end}, {ip_address_1}, {ip_range_2_start}-{ip_range_2_end}, {ip_address_2}..."```
* Also takes a list of strings formatted as: * Also takes a list of strings or `ipaddress.ipaddress` formatted as:
* [{ip_address_1}, {ip_address_2}, {ip_address_3}, ...] ```[{ip_address_1}, {ip_address_2}, {ip_address_3}, ...]```
""" """
def __init__(self, ip_range: Union[str, list]): def __init__(self, ip_range: Union[str, list]):

View File

@@ -1,53 +1,26 @@
import toml from dataclasses import dataclass
import os
NETWORK_PING_RETRIES: int = 3
NETWORK_PING_TIMEOUT: int = 5
NETWORK_SCAN_THREADS: int = 300
CFG_UTIL_REBOOT_THREADS: int = 300
CFG_UTIL_CONFIG_THREADS: int = 300
MINER_FACTORY_GET_VERSION_RETRIES: int = 3
WHATSMINER_PWD = "admin"
DEBUG = False
LOGFILE = False
settings_keys = {}
try:
with open(
os.path.join(os.path.dirname(__file__), "settings.toml"), "r"
) as settings_file:
settings = toml.loads(settings_file.read())
settings_keys = settings.keys()
except:
pass
if "ping_retries" in settings_keys:
NETWORK_PING_RETRIES: int = settings["ping_retries"]
if "ping_timeout" in settings_keys:
NETWORK_PING_TIMEOUT: int = settings["ping_timeout"]
if "scan_threads" in settings_keys:
NETWORK_SCAN_THREADS: int = settings["scan_threads"]
if "reboot_threads" in settings_keys:
CFG_UTIL_REBOOT_THREADS: int = settings["reboot_threads"]
if "config_threads" in settings_keys:
CFG_UTIL_CONFIG_THREADS: int = settings["config_threads"]
if "get_version_retries" in settings_keys: class Singleton(type):
MINER_FACTORY_GET_VERSION_RETRIES: int = settings["get_version_retries"] _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]
if "whatsminer_pwd" in settings_keys: @dataclass
WHATSMINER_PWD: str = settings["whatsminer_pwd"] class PyasicSettings(metaclass=Singleton):
network_ping_retries: int = 1
network_ping_timeout: int = 3
network_scan_threads: int = 300
if "debug" in settings_keys: miner_factory_get_version_retries: int = 1
DEBUG: int = settings["debug"]
if "logfile" in settings_keys: miner_get_data_retries: int = 1
LOGFILE: bool = settings["logfile"]
global_whatsminer_password = "admin"
debug: bool = False
logfile: bool = False

View File

@@ -1,22 +0,0 @@
get_version_retries = 3
ping_retries = 3
ping_timeout = 3 # Seconds
scan_threads = 300
config_threads = 300
reboot_threads = 300
### IMPORTANT ###
# You need to change the password of the miners using the whatsminer
# tool or the privileged API will not work using admin as the password.
# If you change the password, you can pass that password here.
whatsminer_pwd = "admin"
logfile = true
### DEBUG MODE ###
# change this to debug = true
# to enable debug mode.
debug = false
# debug = true

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyasic" name = "pyasic"
version = "0.11.0" version = "0.12.4"
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH." 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>"] authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic" repository = "https://github.com/UpstreamData/pyasic"