Compare commits

...

10 Commits

Author SHA1 Message Date
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
47 changed files with 872 additions and 295 deletions

View File

@@ -9,6 +9,8 @@
## Intro
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.
<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

@@ -3,9 +3,29 @@ repo_url: https://github.com/UpstreamData/pyasic
nav:
- Introduction: "index.md"
- Miners:
- Supported Miners: "miners/supported_types.md"
- Miner Factory: "miners/miner_factory.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:

View File

@@ -10,7 +10,7 @@ from passlib.handlers.md5_crypt import md5_crypt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from pyasic.API import BaseMinerAPI, APIError
from pyasic.settings import WHATSMINER_PWD
from pyasic.settings import PyasicSettings
### IMPORTANT ###
@@ -161,7 +161,12 @@ class BTMinerAPI(BaseMinerAPI):
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)
self.admin_pwd = pwd
self.current_token = None

View File

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

View File

@@ -1,5 +1,6 @@
import ipaddress
import logging
from typing import Union
from pyasic.API.bmminer import BMMinerAPI
@@ -7,10 +8,12 @@ from pyasic.miners import BaseMiner
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):
"""Base handler for BMMiner based miners."""
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ipaddress.ip_address(ip)
@@ -19,10 +22,11 @@ class BMMiner(BaseMiner):
self.uname = "root"
self.pwd = "admin"
async def get_model(self) -> str or None:
async def get_model(self) -> Union[str, None]:
"""Get miner model.
:return: Miner model or None.
Returns:
Miner model or None.
"""
# check if model is cached
if self.model:
@@ -46,7 +50,8 @@ class BMMiner(BaseMiner):
async def get_hostname(self) -> str:
"""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:
return self.hostname
@@ -72,12 +77,14 @@ class BMMiner(BaseMiner):
logging.warning(f"Failed to get hostname for miner: {self}")
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.
: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
@@ -101,10 +108,11 @@ class BMMiner(BaseMiner):
# return the result, either command output or None
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.
:return: Pool config data or None.
Returns:
Pool config data or None.
"""
# get pool data
pools = await self.api.pools()
@@ -120,6 +128,11 @@ class BMMiner(BaseMiner):
return pool_data
async def reboot(self) -> bool:
"""Reboot the miner.
Returns:
The result of rebooting the miner.
"""
logging.debug(f"{self}: Sending reboot command.")
_ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.")
@@ -128,6 +141,11 @@ class BMMiner(BaseMiner):
return False
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)
board_offset = -1
@@ -147,7 +165,7 @@ class BMMiner(BaseMiner):
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True
)

View File

@@ -1,6 +1,7 @@
import ipaddress
import logging
import json
from typing import Union
import toml
@@ -14,7 +15,7 @@ from pyasic.data import MinerData
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):
@@ -27,10 +28,11 @@ class BOSMiner(BaseMiner):
self.pwd = "admin"
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.
:return: Result of the command or None.
Returns:
Result of the command or None.
"""
result = None
@@ -74,6 +76,7 @@ class BOSMiner(BaseMiner):
return False
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()
async def restart_bosminer(self) -> bool:
@@ -94,7 +97,12 @@ class BOSMiner(BaseMiner):
return True
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.")
async with (await self._get_ssh_connection()) as conn:
logging.debug(f"{self}: Opening SFTP connection.")
@@ -110,7 +118,8 @@ class BOSMiner(BaseMiner):
async def get_hostname(self) -> str:
"""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:
return self.hostname
@@ -129,10 +138,11 @@ class BOSMiner(BaseMiner):
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
async def get_model(self) -> str or None:
async def get_model(self) -> Union[str, None]:
"""Get miner model.
:return: Miner model or None.
Returns:
Miner model or None.
"""
# check if model is cached
if self.model:
@@ -166,10 +176,11 @@ class BOSMiner(BaseMiner):
logging.warning(f"Failed to get model for miner: {self}")
return None
async def get_version(self):
async def get_version(self) -> Union[str, None]:
"""Get miner firmware version.
:return: Miner firmware version or None.
Returns:
Miner firmware version or None.
"""
# check if version is cached
if self.version:
@@ -206,65 +217,21 @@ class BOSMiner(BaseMiner):
.as_bos(model=self.model.replace(" (BOS)", ""))
)
async with (await self._get_ssh_connection()) as conn:
await conn.run("/etc/init.d/bosminer stop")
logging.debug(f"{self}: Opening SFTP connection.")
async with conn.start_sftp_client() as sftp:
logging.debug(f"{self}: Opening config file.")
async with sftp.open("/etc/bosminer.toml", "w+") as file:
await file.write(toml_conf)
logging.debug(f"{self}: Restarting BOSMiner")
await conn.run("/etc/init.d/bosminer restart")
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)
await conn.run("/etc/init.d/bosminer start")
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)
board_offset = -1
@@ -284,7 +251,7 @@ class BOSMiner(BaseMiner):
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
try:
miner_data = await self.api.multicommand(
"summary",

View File

@@ -1,5 +1,6 @@
import ipaddress
import logging
from typing import Union
from pyasic.API.btminer import BTMinerAPI
@@ -9,7 +10,7 @@ from pyasic.API import APIError
from pyasic.data import MinerData
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):
@@ -19,7 +20,12 @@ class BTMiner(BaseMiner):
self.api = BTMinerAPI(ip)
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:
logging.debug(f"Found model for {self.ip}: {self.model}")
return self.model
@@ -31,7 +37,12 @@ class BTMiner(BaseMiner):
logging.warning(f"Failed to get model for miner: {self}")
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:
return self.hostname
try:
@@ -48,38 +59,12 @@ class BTMiner(BaseMiner):
logging.warning(f"Failed to get hostname for miner: {self}")
return None
async def get_board_info(self) -> dict:
"""Gets data on each board and chain in 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) -> str:
"""Get the mac address of the miner.
async def get_mac(self):
Returns:
The mac address of the miner as a string.
"""
mac = ""
data = await self.api.summary()
if data:
@@ -100,7 +85,12 @@ class BTMiner(BaseMiner):
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)
mac = None
@@ -126,7 +116,7 @@ class BTMiner(BaseMiner):
data.hostname = hostname
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
try:
miner_data = await self.api.multicommand("summary", "devs", "pools")
if miner_data:

View File

@@ -1,5 +1,6 @@
import ipaddress
import logging
from typing import Union
from pyasic.API.cgminer import CGMinerAPI
@@ -8,7 +9,7 @@ from pyasic.API import APIError
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):
@@ -21,7 +22,12 @@ class CGMiner(BaseMiner):
self.pwd = "admin"
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:
return self.model
try:
@@ -33,7 +39,12 @@ class CGMiner(BaseMiner):
return self.model
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:
return self.hostname
try:
@@ -48,7 +59,15 @@ class CGMiner(BaseMiner):
except Exception:
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
async with (await self._get_ssh_connection()) as conn:
for i in range(3):
@@ -63,9 +82,11 @@ class CGMiner(BaseMiner):
return result
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()
async def restart_cgminer(self) -> bool:
"""Restart cgminer hashing process."""
commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"]
commands = ";".join(commands)
_ret = await self.send_ssh_command(commands)
@@ -74,6 +95,7 @@ class CGMiner(BaseMiner):
return False
async def reboot(self) -> bool:
"""Reboots power to the physical miner."""
logging.debug(f"{self}: Sending reboot command.")
_ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.")
@@ -82,6 +104,7 @@ class CGMiner(BaseMiner):
return False
async def start_cgminer(self) -> None:
"""Start cgminer hashing process."""
commands = [
"mkdir -p /etc/tmp/",
'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root',
@@ -92,6 +115,7 @@ class CGMiner(BaseMiner):
await self.send_ssh_command(commands)
async def stop_cgminer(self) -> None:
"""Restart cgminer hashing process."""
commands = [
"mkdir -p /etc/tmp/",
'echo "" > /etc/tmp/root',
@@ -101,14 +125,24 @@ class CGMiner(BaseMiner):
commands = ";".join(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:
command = "cat /etc/config/cgminer"
result = await conn.run(command, check=True)
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)
board_offset = -1
@@ -128,7 +162,7 @@ class CGMiner(BaseMiner):
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True
)

View File

@@ -8,3 +8,21 @@ class M20S(BaseMiner):
self.model = "M20S"
self.nominal_chips = 66
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 .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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon1026(CGMiner, Avalon1026):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon1047(CGMiner, Avalon1047):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon1066(CGMiner, Avalon1066):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon721(CGMiner, Avalon721):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon741(CGMiner, Avalon741):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon761(CGMiner, Avalon761):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon821(CGMiner, Avalon821):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon841(CGMiner, Avalon841):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon851(CGMiner, Avalon851):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"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.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
import re
from pyasic.config import MinerConfig
import logging
@@ -67,7 +67,7 @@ class CGMinerAvalon921(CGMiner, Avalon921):
data.model = model
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"version", "summary", "pools", "stats"
)

View File

@@ -23,10 +23,7 @@ import ipaddress
import json
import logging
from pyasic.settings import (
MINER_FACTORY_GET_VERSION_RETRIES as GET_VERSION_RETRIES,
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
)
from pyasic.settings import PyasicSettings
import asyncssh
@@ -123,6 +120,8 @@ MINER_CLASSES = {
"M20S": {
"Default": BTMinerM20S,
"BTMiner": BTMinerM20S,
"10": BTMinerM20SV10,
"20": BTMinerM20SV20,
},
"M20S+": {
"Default": BTMinerM20SPlus,
@@ -282,13 +281,12 @@ class MinerFactory(metaclass=Singleton):
ver = None
# 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:
# get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None
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
if new_api and not api:
api = new_api
@@ -296,13 +294,11 @@ class MinerFactory(metaclass=Singleton):
model = new_model
if new_ver and not ver:
ver = new_ver
# if we find the API and model, don't need to loop anymore
if api and model:
break
except asyncio.TimeoutError:
pass
logging.warning(f"{ip}: Get Miner Timed Out")
# make sure we have model information
if model:
if not api:
@@ -310,12 +306,10 @@ class MinerFactory(metaclass=Singleton):
if model not in MINER_CLASSES.keys():
if "avalon" in model:
print(model)
if model == "avalon10":
miner = CGMinerAvalon1066(str(ip))
else:
miner = CGMinerAvalon821(str(ip))
miner = UnknownMiner(str(ip))
return miner
if api not in MINER_CLASSES[model].keys():
api = "Default"
@@ -355,32 +349,26 @@ class MinerFactory(metaclass=Singleton):
async def _get_miner_type(
self, ip: ipaddress.ip_address or str
) -> Tuple[str or None, str or None, str or None]:
data = None
model = None
api = None
ver = None
devdetails = None
version = None
try:
# get device details and version data
data = await self._send_api_command(str(ip), "devdetails+version")
# validate success
validation = await self._validate_command(data)
if not validation[0]:
raise APIError(validation[1])
# copy each part of the main command to devdetails and version
devdetails = data["devdetails"][0]
version = data["version"][0]
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 devdetails and version separately (X19s mainly require this)
# get devdetails and validate
@@ -405,6 +393,32 @@ class MinerFactory(metaclass=Singleton):
# catch APIError and let the factory know we cant get data
logging.warning(f"{ip}: API Command Error: {e}")
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:
return None, None, None
# if we have devdetails, we can get model data from there
if devdetails:
@@ -452,24 +466,24 @@ class MinerFactory(metaclass=Singleton):
if "BOSminer+" in version["VERSION"][0].keys():
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 version.get("Description") and (
"whatsminer" in version.get("Description")
):
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 version and not model:
# make sure version isn't blank
@@ -486,23 +500,6 @@ class MinerFactory(metaclass=Singleton):
elif "am2-s17" in version["STATUS"][0]["Description"]:
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:
# whatsminer have a V in their version string (M20SV41), remove everything after it
if "V" in model:
@@ -519,7 +516,7 @@ class MinerFactory(metaclass=Singleton):
async def _validate_command(data: dict) -> Tuple[bool, str or None]:
"""Check if the returned command output is correctly formatted."""
# check if the data returned is correct or an error
if not data:
if not data or data == {}:
return False, "No API data."
# if status isn't a key, it is a multicommand
if "STATUS" not in data.keys():
@@ -548,9 +545,10 @@ class MinerFactory(metaclass=Singleton):
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(ip), 4028)
except OSError as e:
if e.errno in [10061, 22] or e.winerror == 1225:
raise e
logging.warning(f"{str(ip)} - Command {command}: {e}")
return {}
# create the command
cmd = {"command": command}

View File

@@ -1,8 +1,24 @@
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):
def __init__(self, ip: str) -> None:
super().__init__(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 .M21 import BTMinerM21

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,8 @@ import logging
from typing import Union
from pyasic.network.net_range import MinerNetworkRange
from pyasic.miners.miner_factory import MinerFactory
from pyasic.settings import (
NETWORK_PING_RETRIES as PING_RETRIES,
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
NETWORK_SCAN_THREADS as SCAN_THREADS,
)
from pyasic.miners.miner_factory import MinerFactory, AnyMiner
from pyasic.settings import PyasicSettings
class MinerNetwork:
@@ -91,7 +87,7 @@ class MinerNetwork:
for host in local_network.hosts():
# 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
scan_tasks.append(self.ping_and_get_miner(host))
else:
@@ -130,7 +126,7 @@ class MinerNetwork:
# for each ip on the network, loop through and scan it
for host in local_network.hosts():
# 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 = asyncio.as_completed(scan_tasks)
# when we scan, empty the scan tasks
@@ -150,24 +146,32 @@ class MinerNetwork:
@staticmethod
async def ping_miner(ip: ipaddress.ip_address) -> None or 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
async def ping_and_get_miner(
ip: ipaddress.ip_address,
) -> None or ipaddress.ip_address:
return await ping_and_get_miner(ip)
) -> None or AnyMiner:
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(
ip: ipaddress.ip_address, port=4028
) -> None or 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)
try:
# get the read and write streams from the connection
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
writer.close()
@@ -188,15 +192,13 @@ async def ping_miner(
return
async def ping_and_get_miner(
ip: ipaddress.ip_address, port=4028
) -> None or ipaddress.ip_address:
for i in range(PING_RETRIES):
async def ping_and_get_miner(ip: ipaddress.ip_address, port=4028) -> None or AnyMiner:
for i in range(PyasicSettings().network_ping_retries):
connection_fut = asyncio.open_connection(str(ip), port)
try:
# get the read and write streams from the connection
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
writer.close()

View File

@@ -8,9 +8,9 @@ class MinerNetworkRange:
Parameters:
ip_range: ## A range of IP addresses to put in the network, or a list of IPs
* 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}...
* Also takes a list of strings formatted as:
* [{ip_address_1}, {ip_address_2}, {ip_address_3}, ...]
```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 or `ipaddress.ipaddress` formatted as:
```[{ip_address_1}, {ip_address_2}, {ip_address_3}, ...]```
"""
def __init__(self, ip_range: Union[str, list]):

View File

@@ -1,53 +1,26 @@
import toml
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"]
from dataclasses import dataclass
if "get_version_retries" in settings_keys:
MINER_FACTORY_GET_VERSION_RETRIES: int = settings["get_version_retries"]
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
if "whatsminer_pwd" in settings_keys:
WHATSMINER_PWD: str = settings["whatsminer_pwd"]
@dataclass
class PyasicSettings(metaclass=Singleton):
network_ping_retries: int = 1
network_ping_timeout: int = 3
network_scan_threads: int = 300
if "debug" in settings_keys:
DEBUG: int = settings["debug"]
miner_factory_get_version_retries: int = 1
if "logfile" in settings_keys:
LOGFILE: bool = settings["logfile"]
miner_get_data_retries: int = 1
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]
name = "pyasic"
version = "0.11.1"
version = "0.12.3"
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic"