Compare commits

...

46 Commits

Author SHA1 Message Date
UpstreamData
0d80ce5a0e bump version number 2022-09-12 15:28:22 -06:00
UpstreamData
ddcafe0f2b finish abstracting BaseMiner by implementing get_data() as abstract 2022-09-12 15:27:51 -06:00
UpstreamData
ea195b34db update tests and add code coverage with coverage, although coverage is not required 2022-09-12 15:18:00 -06:00
UpstreamData
7377cb0d26 refactor some classes into their own files and fill base __init__.py with imports 2022-09-12 15:15:13 -06:00
UpstreamData
24b66de971 bump version number 2022-09-06 11:18:34 -06:00
UpstreamData
62d664a14c strip file output when checking for fault light in bosminer 2022-09-06 11:18:17 -06:00
UpstreamData
03b9a90f68 bump version number 2022-09-06 11:02:05 -06:00
UpstreamData
fefe0324b9 fix a bug with checking miner fault lights in bosminer 2022-09-06 11:01:42 -06:00
UpstreamData
62b14a78b7 bump version number 2022-09-06 10:49:16 -06:00
UpstreamData
0ff505bbb4 add support for innosilicon T3H+ 2022-09-06 10:48:04 -06:00
UpstreamData
b6c8c930a2 bump version number 2022-08-30 16:01:48 -06:00
UpstreamData
903bb93c4e add check_light() for bosminers by checking if delay_on exists in the Red LED directory 2022-08-30 10:51:22 -06:00
UpstreamData
59667cf104 bump version number 2022-08-29 09:53:01 -06:00
UpstreamData
3fd1b41bec add support for whatsminer VH60 2022-08-29 09:52:35 -06:00
UpstreamData
6569107f64 bump version number 2022-08-29 09:07:02 -06:00
UpstreamData
9d746a6dcb add errors to MinerData().as_influxdb() 2022-08-29 09:06:20 -06:00
UpstreamData
fce4c07c32 bump version number 2022-08-25 15:34:28 -06:00
UpstreamData
094857758a update MinerData().as_influxdb() to include properties. 2022-08-25 15:34:04 -06:00
UpstreamData
2a49b89849 bump version number 2022-08-25 13:09:19 -06:00
UpstreamData
4ecd135734 fix a bug with incorrect types in miner data 2022-08-25 13:09:03 -06:00
UpstreamData
836defc216 bump version number 2022-08-25 13:04:06 -06:00
UpstreamData
f8f777b5b5 fix tag data to be escaped properly 2022-08-25 13:03:46 -06:00
UpstreamData
b15e0a7363 bump version number 2022-08-25 12:50:55 -06:00
UpstreamData
5c1d06f743 attempt to fix a bug with influx db miner data 2022-08-25 12:50:30 -06:00
UpstreamData
51de56feb3 bump version number 2022-08-25 12:43:05 -06:00
UpstreamData
256a4ac909 fix boolean bug in miner data 2022-08-25 12:39:57 -06:00
UpstreamData
09800c8ad2 bump version number 2022-08-24 15:18:04 -06:00
UpstreamData
83a7d8c60f add MinerData().as_json() 2022-08-24 15:17:36 -06:00
UpstreamData
ee2698be50 update poetry.lock 2022-08-24 13:56:05 -06:00
UpstreamData
dc43087b0d bump version number 2022-08-24 13:51:25 -06:00
UpstreamData
4fa3511725 add float support to MinerData().as_influx() 2022-08-24 13:50:55 -06:00
UpstreamData
4b9ae70424 bump version number 2022-08-24 13:35:48 -06:00
UpstreamData
74ebffb4fc add MinerData().as_influx() to write miner data as influx db line format data. 2022-08-24 13:34:57 -06:00
UpstreamData
54206da449 add getitem and setitem methods to MinerData 2022-08-24 10:02:51 -06:00
UpstreamData
dd54ff7ee4 bump version number 2022-08-22 15:19:08 -06:00
UpstreamData
abef0c3d59 add efficiency to MinerData 2022-08-22 14:50:49 -06:00
UpstreamData
957c9a3678 Refactor MinerFactory._get_miner_type(), move BaseMiner to its own file, and improve interface of miner.send_config() (#17) 2022-08-22 14:10:37 -06:00
Arceris
50ccfec1b3 Add a check in _parse_type_from_version (#16) 2022-08-18 10:12:19 -06:00
UpstreamData
8e7d6751e2 update docs to include all currently supported miners including BOS devices. 2022-08-12 12:30:29 -06:00
UpstreamData
b77c4173c6 update supported miners in docs and add link to it in README.md 2022-08-12 12:22:49 -06:00
UpstreamData
4941cffb70 fix a bad character in requirements.txt caused by pre-commit 2022-08-12 12:14:12 -06:00
UpstreamData
81d5d23189 Revert "Revert "attempt to improve the readability of miner_factory.py""
This reverts commit 9da5a836ce.
2022-08-11 15:22:00 -06:00
UpstreamData
9da5a836ce Revert "attempt to improve the readability of miner_factory.py"
This reverts commit c9a536fc60.
2022-08-11 15:16:42 -06:00
UpstreamData
c9a536fc60 attempt to improve the readability of miner_factory.py 2022-08-11 15:12:45 -06:00
UpstreamData
fa172b56b0 bump version number 2022-08-11 15:11:09 -06:00
UpstreamData
ee45f2342e improve how the fault light looks on whatsminers 2022-08-11 11:58:42 -06:00
97 changed files with 1871 additions and 575 deletions

17
.coveragerc Normal file
View File

@@ -0,0 +1,17 @@
[report]
exclude_lines =
# Skip @abstractmethod
@abstractmethod
@abc.abstractmethod
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
# Don't complain about missing debug-only code:
def __repr__
if self\.debug
# Don't complain if non-runnable code isn't run:
if 0:
if __name__ == .__main__.:

View File

@@ -3,7 +3,6 @@ repos:
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black

View File

@@ -7,6 +7,9 @@
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/)
[![GitHub](https://img.shields.io/github/license/UpstreamData/pyasic)](https://github.com/UpstreamData/pyasic/blob/master/LICENSE.txt)
[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/UpstreamData/pyasic)](https://www.codefactor.io/repository/github/upstreamdata/pyasic)
## Supported Miners
Supported miners are listed in the docs, [here](https://pyasic.readthedocs.io/en/latest/miners/supported_types/)
## Documentation
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/)
@@ -91,7 +94,7 @@ if __name__ == "__main__":
import asyncio
import sys
from pyasic.miners.miner_factory import MinerFactory
from pyasic.miners import get_miner
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -103,7 +106,7 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
async def get_miner_data(miner_ip: str):
# Use MinerFactory to get miner
# MinerFactory is a singleton, so we can just get the instance in place
miner = await MinerFactory().get_miner(miner_ip)
miner = await get_miner(miner_ip)
# Get data from the miner
data = await miner.get_data()
@@ -122,7 +125,7 @@ If needed, this library exposes a wrapper for the miner API that can be used for
import asyncio
import sys
from pyasic.miners.miner_factory import MinerFactory
from pyasic.miners import get_miner
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -132,7 +135,7 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
async def get_api_commands(miner_ip: str):
# Get the miner
miner = await MinerFactory().get_miner(miner_ip)
miner = await get_miner(miner_ip)
# List all available commands
print(miner.api.get_commands())
@@ -150,7 +153,7 @@ The miner API commands will raise an `APIError` if they fail with a bad status c
import asyncio
import sys
from pyasic.miners.miner_factory import MinerFactory
from pyasic.miners import get_miner
# Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -160,7 +163,7 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.starts
async def get_api_commands(miner_ip: str):
# Get the miner
miner = await MinerFactory().get_miner(miner_ip)
miner = await get_miner(miner_ip)
# Run the devdetails command
# This is equivalent to await miner.api.send_command("devdetails")

View File

@@ -57,3 +57,61 @@
options:
show_root_heading: false
heading_level: 4
## S17 (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17
handler: python
options:
show_root_heading: false
heading_level: 4
## S17+ (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## S17 Pro (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S17e (BOS)
::: pyasic.miners.antminer.bosminer.X17.S17e.BOSMinerS17e
handler: python
options:
show_root_heading: false
heading_level: 4
## T17 (BOS)
::: pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17
handler: python
options:
show_root_heading: false
heading_level: 4
## T17+ (BOS)
::: pyasic.miners.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus
handler: python
options:
show_root_heading: false
heading_level: 4
## T17e (BOS)
::: pyasic.miners.antminer.bosminer.X17.T17e.BOSMinerT17e
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -50,3 +50,45 @@
options:
show_root_heading: false
heading_level: 4
## S19 (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19
handler: python
options:
show_root_heading: false
heading_level: 4
## S19 Pro (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19_Pro.BOSMinerS19Pro
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19j.BOSMinerS19j
handler: python
options:
show_root_heading: false
heading_level: 4
## S19j Pro (BOS)
::: pyasic.miners.antminer.bosminer.X19.S19j_Pro.BOSMinerS19jPro
handler: python
options:
show_root_heading: false
heading_level: 4
## T19 (BOS)
::: pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,6 +1,16 @@
# pyasic
## X9 Models
## X9 (BOS)
::: pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4
## S9
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9

View File

@@ -0,0 +1,10 @@
# pyasic
## T3X Models
## T3H+
::: pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -5,7 +5,24 @@ Supported miner types are here on this list. If your miner (or miner version) i
##### pyasic currently supports the following miners and subtypes:
* Braiins OS+ Devices:
* All devices supported by BraiinsOS+ are supported here.
* X19 Series:
* [S19][pyasic.miners.antminer.bosminer.X19.S19.BOSMinerS19]
* [S19 Pro][pyasic.miners.antminer.bosminer.X19.S19_Pro.BOSMinerS19Pro]
* [S19j][pyasic.miners.antminer.bosminer.X19.S19j.BOSMinerS19j]
* [S19j Pro][pyasic.miners.antminer.bosminer.X19.S19j_Pro.BOSMinerS19jPro]
* [T19][pyasic.miners.antminer.bosminer.X19.T19.BOSMinerT19]
* X17 Series:
* [S17][pyasic.miners.antminer.bosminer.X17.S17.BOSMinerS17]
* [S17+][pyasic.miners.antminer.bosminer.X17.S17_Plus.BOSMinerS17Plus]
* [S17 Pro][pyasic.miners.antminer.bosminer.X17.S17_Pro.BOSMinerS17Pro]
* [S17e][pyasic.miners.antminer.bosminer.X17.S17e.BOSMinerS17e]
* [T17][pyasic.miners.antminer.bosminer.X17.T17.BOSMinerT17]
* [T17+][pyasic.miners.antminer.bosminer.X17.T17_Plus.BOSMinerT17Plus]
* [T17e][pyasic.miners.antminer.bosminer.X17.T17e.BOSMinerT17e]
* X9 Series:
* [S9][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
* [S9i][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
* [S9j][pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9]
* Stock Firmware Whatsminers:
* M3X Series:
* [M30S][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30S]:
@@ -20,9 +37,12 @@ Supported miner types are here on this list. If your miner (or miner version) i
* [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]
* [VH60][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60]
* [M31S][pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S]
* [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]:
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20]
* [M32][pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32]
* [V20][pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20]
* [M32S][pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S]
* M2X Series:
* [M20][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20]:
@@ -71,3 +91,6 @@ Supported miner types are here on this list. If your miner (or miner version) i
* [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026]
* [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047]
* [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066]
* Stock Firmware Innosilicon Miners:
* T3X Series:
* [T3H+][pyasic.miners.innosilicon.cgminer.T3X.T3H_Plus.CGMinerInnosiliconT3HPlus]

View File

@@ -89,7 +89,7 @@
show_root_heading: false
heading_level: 4
## M30S+VG40
## M30S++VG40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
handler: python
@@ -97,6 +97,14 @@
show_root_heading: false
heading_level: 4
## M30S++VH60
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVH60
handler: python
options:
show_root_heading: false
heading_level: 4
## M31S
@@ -122,6 +130,22 @@
show_root_heading: false
heading_level: 4
## M32
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32
handler: python
options:
show_root_heading: false
heading_level: 4
## M32V20
::: pyasic.miners.whatsminer.btminer.M3X.M32.BTMinerM32V20
handler: python
options:
show_root_heading: false
heading_level: 4
## M32S
::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S

View File

@@ -22,6 +22,7 @@ nav:
- Avalon 10X: "miners/avalonminer/A10X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md"
- Network:
- Miner Network: "network/miner_network.md"

6
poetry.lock generated
View File

@@ -17,7 +17,7 @@ trio = ["trio (>=0.16)"]
[[package]]
name = "asyncssh"
version = "2.11.0"
version = "2.12.0"
description = "AsyncSSH: Asynchronous SSHv2 client and server library"
category = "main"
optional = false
@@ -218,8 +218,8 @@ anyio = [
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
]
asyncssh = [
{file = "asyncssh-2.11.0-py3-none-any.whl", hash = "sha256:7302348cbd54c58d3259da17f13e77912de1b005e366b15c8b183d948c8a91a8"},
{file = "asyncssh-2.11.0.tar.gz", hash = "sha256:59c36ce77ba9dda8dd57ad875776e7105ddb1fa851bc039bb3aeadeac4f67b56"},
{file = "asyncssh-2.12.0-py3-none-any.whl", hash = "sha256:6841c4242c606fd51188c974ec2f4887efeec67ecdfa5b84140711dacd985ab3"},
{file = "asyncssh-2.12.0.tar.gz", hash = "sha256:274101322c4b941823aeed8e1ab6e7be5191686c6db2d2bd35afeba30505e780"},
]
certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},

View File

@@ -19,33 +19,7 @@ import warnings
import logging
from typing import Union
class APIError(Exception):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."
class APIWarning(Warning):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."
from pyasic.errors import APIError, APIWarning
class BaseMinerAPI:

View File

@@ -24,7 +24,8 @@ from typing import Union
from passlib.handlers.md5_crypt import md5_crypt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from pyasic.API import BaseMinerAPI, APIError
from pyasic.errors import APIError
from pyasic.API import BaseMinerAPI
from pyasic.settings import PyasicSettings
@@ -439,8 +440,8 @@ class BTMinerAPI(BaseMinerAPI):
self,
auto: bool = True,
color: str = "red",
period: int = 2000,
duration: int = 1000,
period: int = 60,
duration: int = 20,
start: int = 0,
) -> dict:
"""Set the LED on the miner using the API.

View File

@@ -11,3 +11,51 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.API.bmminer import BMMinerAPI
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API.btminer import BTMinerAPI
from pyasic.API.cgminer import CGMinerAPI
from pyasic.API.unknown import UnknownAPI
from pyasic.config import MinerConfig
from pyasic.data import (
MinerData,
BraiinsOSError,
InnosiliconError,
WhatsminerError,
X19Error,
)
from pyasic.errors import APIError, APIWarning
from pyasic.miners import get_miner
from pyasic.miners.base import AnyMiner
from pyasic.miners.miner_factory import MinerFactory
from pyasic.miners.miner_listener import MinerListener
from pyasic.network import MinerNetwork
from pyasic.settings import PyasicSettings
__all__ = [
"BMMinerAPI",
"BOSMinerAPI",
"BTMinerAPI",
"CGMinerAPI",
"UnknownAPI",
"MinerConfig",
"MinerData",
"BraiinsOSError",
"InnosiliconError",
"WhatsminerError",
"X19Error",
"APIError",
"APIWarning",
"get_miner",
"AnyMiner",
"MinerFactory",
"MinerListener",
"MinerNetwork",
"PyasicSettings",
]

View File

@@ -78,6 +78,23 @@ class _Pool:
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {
f"Pool": self.url,
f"UserName": username,
f"Password": self.password,
}
return pool
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a string usable by an Avalonminer device.
@@ -154,6 +171,19 @@ class _PoolGroup:
pools.append(pool.as_x19(user_suffix=user_suffix))
return pools
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {}
for idx, pool in enumerate(self.pools[:3]):
pool_data = pool.as_inno(user_suffix=user_suffix)
for key in pool_data:
pools[f"{key}{idx+1}"] = pool_data[key]
return pools
def as_wm(self, user_suffix: str = None) -> List[dict]:
"""Convert the data in this class to a list usable by an Whatsminer device.
@@ -217,7 +247,7 @@ class MinerConfig:
temp_mode: Literal["auto", "manual", "disabled"] = "auto"
temp_target: float = 70.0
temp_hot: float = 80.0
temp_dangerous: float = 10.0
temp_dangerous: float = 100.0
minimum_fans: int = None
fan_speed: Literal[tuple(range(101))] = None # noqa - Ignore weird Literal usage
@@ -359,7 +389,15 @@ class MinerConfig:
Parameters:
user_suffix: The suffix to append to username.
"""
return self.pool_groups[0].as_x19(user_suffix=user_suffix)
return self.pool_groups[0].as_wm(user_suffix=user_suffix)
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an Innosilicon device.
Parameters:
user_suffix: The suffix to append to username.
"""
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
def as_x19(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an X19 device.

View File

@@ -14,9 +14,11 @@
from typing import Union, List
from dataclasses import dataclass, field, asdict
from datetime import datetime
from datetime import datetime, timezone
import time
import json
from .error_codes import X19Error, WhatsminerError, BraiinsOSError
from .error_codes import X19Error, WhatsminerError, BraiinsOSError, InnosiliconError
@dataclass
@@ -52,7 +54,7 @@ class MinerData:
total_chips: The total number of chips on all boards. Calculated automatically.
ideal_chips: The ideal number of chips in the miner as an int.
percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
nominal: The nominal amount of chips in the miner. Calculated automatically.
nominal: Whether the number of chips in the miner is nominal. Calculated automatically.
pool_split: The pool split as a str.
pool_1_url: The first pool url on the miner as a str.
pool_1_user: The first pool user on the miner as a str.
@@ -60,6 +62,7 @@ class MinerData:
pool_2_user: The second pool user on the miner as a str.
errors: A list of errors on the miner.
fault_light: Whether or not the fault light is on as a boolean.
efficiency: Efficiency of the miner in J/TH (Watts per TH/s). Calculated automatically.
"""
ip: str
@@ -68,11 +71,11 @@ class MinerData:
model: str = "Unknown"
hostname: str = "Unknown"
hashrate: float = 0
left_board_hashrate: float = 0
center_board_hashrate: float = 0
right_board_hashrate: float = 0
left_board_hashrate: float = 0.0
center_board_hashrate: float = 0.0
right_board_hashrate: float = 0.0
temperature_avg: int = field(init=False)
env_temp: float = 0
env_temp: float = 0.0
left_board_temp: int = 0
left_board_chip_temp: int = 0
center_board_temp: int = 0
@@ -97,13 +100,26 @@ class MinerData:
pool_1_user: str = "Unknown"
pool_2_url: str = ""
pool_2_user: str = ""
errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = field(
default_factory=list
)
errors: List[
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError]
] = field(default_factory=list)
fault_light: Union[bool, None] = None
efficiency: int = field(init=False)
def __post_init__(self):
self.datetime = datetime.now()
self.datetime = datetime.now(timezone.utc).astimezone()
def __getitem__(self, item):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
def __setitem__(self, key, value):
return setattr(self, key, value)
def __iter__(self):
return iter([item for item in self.asdict()])
@property
def total_chips(self): # noqa - Skip PyCharm inspection
@@ -149,5 +165,55 @@ class MinerData:
def temperature_avg(self, val):
pass
@property
def efficiency(self): # noqa - Skip PyCharm inspection
if self.hashrate == 0:
return 0
return round(self.wattage / self.hashrate)
@efficiency.setter
def efficiency(self, val):
pass
def asdict(self):
return asdict(self)
def as_json(self):
data = self.asdict()
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
return json.dumps(data)
def as_influxdb(self, measurement_name: str = "miner_data"):
tag_data = [measurement_name]
field_data = []
tags = ["ip", "mac", "model", "hostname"]
for attribute in self:
if attribute in tags:
escaped_data = self[attribute].replace(" ", "\\ ")
tag_data.append(f"{attribute}={escaped_data}")
continue
if isinstance(self[attribute], str):
field_data.append(f'{attribute}="{self[attribute]}"')
continue
if isinstance(self[attribute], bool):
field_data.append(f"{attribute}={str(self[attribute]).lower()}")
continue
if isinstance(self[attribute], int):
field_data.append(f"{attribute}={self[attribute]}")
continue
if isinstance(self[attribute], float):
field_data.append(f"{attribute}={self[attribute]}")
continue
if attribute == "fault_light" and not self[attribute]:
field_data.append(f"{attribute}=false")
continue
if attribute == "errors":
for idx, item in enumerate(self[attribute]):
field_data.append(f'error_{idx+1}="{item.error_message}"')
tags_str = ",".join(tag_data)
field_str = ",".join(field_data)
timestamp = str(int(time.mktime(self.datetime.timetuple()) * 1e9))
return " ".join([tags_str, field_str, timestamp])

View File

@@ -15,3 +15,4 @@
from .whatsminer import WhatsminerError
from .bos import BraiinsOSError
from .X19 import X19Error
from .innosilicon import InnosiliconError

View File

@@ -0,0 +1,65 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass, field, asdict
@dataclass
class InnosiliconError:
"""A Dataclass to handle error codes of Innosilicon miners.
Attributes:
error_code: The error code as an int.
error_message: The error message as a string. Automatically found from the error code.
"""
error_code: int
error_message: str = field(init=False)
@property
def error_message(self): # noqa - Skip PyCharm inspection
if self.error_code in ERROR_CODES:
return ERROR_CODES[self.error_code]
return "Unknown error type."
@error_message.setter
def error_message(self, val):
pass
def asdict(self):
return asdict(self)
ERROR_CODES = {
21: "The PLUG signal of the hash board is not detected.",
22: "Power I2C communication is abnormal.",
23: "The SPI of all hash boards is blocked.",
24: "Some of the hash boards fail to connect to the SPI'.",
25: "Hashboard failed to set frequency.",
26: "Hashboard failed to set voltage.",
27: "Chip BIST test failed.",
28: "Hashboard SPI communication is abnormal.",
29: "Power I2C communication is abnormal.",
30: "Pool connection failed.",
31: "Individual chips are damaged.",
32: "Over temperature protection.",
33: "Hashboard fault.",
34: "The data cables are not connected in the correct order.",
35: "No power output.",
36: "Hashboard fault.",
37: "Control board and/or hashboard do not match.",
40: "Power output is abnormal.",
41: "Power output is abnormal.",
42: "Hashboard fault.",
}

View File

@@ -152,6 +152,7 @@ ERROR_CODES = {
2020: "Pool 0 connection failed.",
2021: "Pool 1 connection failed.",
2022: "Pool 2 connection failed.",
2023: "Pool 3 connection failed.",
2030: "High rejection rate on pool.",
2040: "The pool does not support asicboost mode.",
2310: "Hashrate is too low.",

41
pyasic/errors/__init__.py Normal file
View File

@@ -0,0 +1,41 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class APIError(Exception):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."
class APIWarning(Warning):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."

View File

@@ -12,126 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncssh
import logging
import ipaddress
from abc import ABC, abstractmethod
from typing import Union
from pyasic.data import MinerData
from pyasic.config import MinerConfig
from pyasic.miners.base import BaseMiner, AnyMiner
from pyasic.miners.miner_factory import MinerFactory
class BaseMiner(ABC):
def __init__(self, *args) -> None:
self.ip = None
self.uname = "root"
self.pwd = "admin"
self.api = None
self.api_type = None
self.model = None
self.light = None
self.hostname = None
self.nominal_chips = 1
self.version = None
self.fan_count = 2
self.config = None
def __new__(cls, *args, **kwargs):
if cls is BaseMiner:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls)
def __repr__(self):
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
def __lt__(self, other):
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
def __gt__(self, other):
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username=self.uname,
password=self.pwd,
server_host_key_algs=["ssh-rsa"],
)
return conn
except asyncssh.misc.PermissionDenied:
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username="root",
password="admin",
server_host_key_algs=["ssh-rsa"],
)
return conn
except Exception as e:
raise e
except OSError as e:
logging.warning(f"Connection refused: {self}")
raise e
except Exception as e:
raise e
@abstractmethod
async def fault_light_on(self) -> bool:
pass
@abstractmethod
async def fault_light_off(self) -> bool:
pass
# async def send_file(self, src, dest):
# async with (await self._get_ssh_connection()) as conn:
# await asyncssh.scp(src, (conn, dest))
@abstractmethod
async def check_light(self) -> bool:
pass
# @abstractmethod
async def get_board_info(self):
return None
@abstractmethod
async def get_config(self) -> MinerConfig:
pass
@abstractmethod
async def get_hostname(self) -> str:
pass
@abstractmethod
async def get_model(self) -> str:
pass
@abstractmethod
async def reboot(self) -> bool:
pass
@abstractmethod
async def restart_backend(self) -> bool:
pass
async def send_config(self, *args, **kwargs):
return None
@abstractmethod
async def get_mac(self) -> str:
pass
@abstractmethod
async def get_errors(self) -> list:
pass
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))
# abstracted version of get miner that is easier to access
async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner:
return await MinerFactory().get_miner(ip)

View File

@@ -18,9 +18,10 @@ from typing import Union
from pyasic.API.bmminer import BMMinerAPI
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
from pyasic.data import MinerData
from pyasic.config import MinerConfig
from pyasic.settings import PyasicSettings
@@ -154,6 +155,9 @@ class BMMiner(BaseMiner):
return True
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def check_light(self) -> bool:
if not self.light:
self.light = False

View File

@@ -20,9 +20,9 @@ from typing import Union
import toml
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API import APIError
from pyasic.errors import APIError
from pyasic.data.error_codes import BraiinsOSError
from pyasic.data import MinerData
@@ -215,22 +215,12 @@ class BOSMiner(BaseMiner):
logging.warning(f"Failed to get model for miner: {self}")
return None
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
logging.debug(f"{self}: Sending config.")
if ip_user:
suffix = str(self.ip).split(".")[-1]
toml_conf = (
MinerConfig()
.from_yaml(yaml_config)
.as_bos(model=self.model.replace(" (BOS)", ""), user_suffix=suffix)
)
else:
toml_conf = (
MinerConfig()
.from_yaml(yaml_config)
.as_bos(model=self.model.replace(" (BOS)", ""))
)
toml_conf = config.as_bos(
model=self.model.replace(" (BOS)", ""), user_suffix=user_suffix
)
async with (await self._get_ssh_connection()) as conn:
await conn.run("/etc/init.d/bosminer stop")
logging.debug(f"{self}: Opening SFTP connection.")
@@ -242,8 +232,14 @@ class BOSMiner(BaseMiner):
await conn.run("/etc/init.d/bosminer start")
async def check_light(self) -> bool:
if not self.light:
self.light = False
if self.light:
return self.light
data = (
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
).strip()
self.light = False
if data == "50":
self.light = True
return self.light
async def get_errors(self) -> list:

View File

@@ -18,7 +18,9 @@ import ipaddress
from typing import Union
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
from pyasic.config import MinerConfig
from pyasic.data import MinerData
class BOSMinerOld(BaseMiner):
@@ -92,3 +94,9 @@ class BOSMinerOld(BaseMiner):
async def restart_backend(self) -> bool:
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))

View File

@@ -18,8 +18,8 @@ from typing import Union
from pyasic.API.btminer import BTMinerAPI
from pyasic.miners import BaseMiner
from pyasic.API import APIError
from pyasic.miners.base import BaseMiner
from pyasic.errors import APIError
from pyasic.data import MinerData
from pyasic.data.error_codes import WhatsminerError
@@ -144,6 +144,9 @@ class BTMiner(BaseMiner):
async def fault_light_on(self) -> bool:
try:
data = await self.api.set_led(auto=False)
await self.api.set_led(
auto=False, color="green", start=0, period=1, duration=0
)
except APIError:
return False
if data:
@@ -162,12 +165,8 @@ class BTMiner(BaseMiner):
async def restart_backend(self) -> bool:
return False
async def send_config(self, yaml_config, ip_user: bool = False):
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_wm(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_wm()
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
conf = config.as_wm(user_suffix=user_suffix)
await self.api.update_pools(
conf[0]["url"],

View File

@@ -18,8 +18,9 @@ from typing import Union
from pyasic.API.cgminer import CGMinerAPI
from pyasic.miners import BaseMiner
from pyasic.API import APIError
from pyasic.miners.base import BaseMiner
from pyasic.errors import APIError
from pyasic.config import MinerConfig
from pyasic.data import MinerData
@@ -165,6 +166,9 @@ class CGMiner(BaseMiner):
async def get_errors(self) -> list:
return []
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def get_mac(self) -> str:
return "00:00:00:00:00:00"

View File

@@ -15,3 +15,4 @@
from .antminer import *
from .avalonminer import *
from .whatsminer import *
from .innosilicon import *

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S17(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S17Plus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S17Pro(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S17e(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class T17(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class T17Plus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class T17e(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S19(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S19Pro(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S19a(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S19j(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S19jPro(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class T19(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S9(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class S9i(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class T9(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon1026(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon1047(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon1066(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon721(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon741(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon761(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon821(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon841(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon851(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class Avalon921(BaseMiner):

View File

@@ -0,0 +1,24 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners.base import BaseMiner
class InnosiliconT3HPlus(BaseMiner):
def __init__(self, ip: str) -> None:
super().__init__()
self.ip = ip
self.model = "T3H+"
self.nominal_chips = 114
self.fan_count = 4

View File

@@ -12,9 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
from pyasic.tests.miners_tests import MinersTest
from pyasic.tests.network_tests import NetworkTest
if __name__ == "__main__":
unittest.main()
from .T3H_Plus import InnosiliconT3HPlus

View File

@@ -0,0 +1,15 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .T3X import *

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M20(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M20S(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M20SPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M21(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M21S(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M21SPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M30S(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M30SPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M30SPlusPlus(BaseMiner):
@@ -28,7 +28,7 @@ class M30SPlusPlusVG30(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ V30"
self.model = "M30S++ VG30"
self.nominal_chips = 111
self.fan_count = 2
@@ -37,6 +37,15 @@ class M30SPlusPlusVG40(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ V40"
self.model = "M30S++ VG40"
self.nominal_chips = 117
self.fan_count = 2
class M30SPlusPlusVH60(BaseMiner):
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M30S++ VH60"
self.nominal_chips = 78
self.fan_count = 2

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M31S(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M31SPlus(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M32(BaseMiner):

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
class M32S(BaseMiner):

View File

@@ -14,7 +14,12 @@
from .M30S import M30S, M30SVE10, M30SVE20, M30SVG20, M30SV50
from .M30S_Plus import M30SPlus, M30SPlusVG60, M30SPlusVE40, M30SPlusVF20
from .M30S_Plus_Plus import M30SPlusPlus, M30SPlusPlusVG30, M30SPlusPlusVG40
from .M30S_Plus_Plus import (
M30SPlusPlus,
M30SPlusPlusVG30,
M30SPlusPlusVG40,
M30SPlusPlusVH60,
)
from .M31S import M31S
from .M31S_Plus import M31SPlus, M31SPlusVE20

View File

@@ -55,14 +55,10 @@ class BMMinerX19(BMMiner):
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth(self.uname, self.pwd)
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_x19(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_x19()
conf = config.as_x19(user_suffix=user_suffix)
try:
async with httpx.AsyncClient() as client:

View File

@@ -13,3 +13,4 @@
# limitations under the License.
from .S9 import CGMinerS9
from .T9 import CGMinerT9

View File

@@ -51,15 +51,11 @@ class CGMinerA10X(CGMiner):
return True
return False
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
raise NotImplementedError
logging.debug(f"{self}: Sending config.")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
conf = config.as_avalon(user_suffix=user_suffix)
data = await self.api.ascset(
0, "setpool", f"root,root,{conf}"
) # this should work but doesn't

View File

@@ -51,15 +51,11 @@ class CGMinerA7X(CGMiner):
return True
return False
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
raise NotImplementedError
logging.debug(f"{self}: Sending config.")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
conf = config.as_avalon(user_suffix=user_suffix)
data = await self.api.ascset(
0, "setpool", f"root,root,{conf}"
) # this should work but doesn't

View File

@@ -51,15 +51,11 @@ class CGMinerA8X(CGMiner):
return True
return False
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
raise NotImplementedError
logging.debug(f"{self}: Sending config.")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
conf = config.as_avalon(user_suffix=user_suffix)
data = await self.api.ascset(
0, "setpool", f"root,root,{conf}"
) # this should work but doesn't

View File

@@ -52,15 +52,11 @@ class CGMinerAvalon921(CGMiner, Avalon921):
return True
return False
async def send_config(self, yaml_config, ip_user: bool = False) -> None:
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
raise NotImplementedError
logging.debug(f"{self}: Sending config.")
if ip_user:
suffix = str(self.ip).split(".")[-1]
conf = MinerConfig().from_yaml(yaml_config).as_avalon(user_suffix=suffix)
else:
conf = MinerConfig().from_yaml(yaml_config).as_avalon()
conf = config.as_avalon(user_suffix=user_suffix)
data = await self.api.ascset(
0, "setpool", f"root,root,{conf}"
) # this should work but doesn't

135
pyasic/miners/base.py Normal file
View File

@@ -0,0 +1,135 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncssh
import logging
import ipaddress
from abc import ABC, abstractmethod
from typing import TypeVar
from pyasic.data import MinerData
from pyasic.config import MinerConfig
class BaseMiner(ABC):
def __init__(self, *args) -> None:
self.ip = None
self.uname = "root"
self.pwd = "admin"
self.api = None
self.api_type = None
self.model = None
self.light = None
self.hostname = None
self.nominal_chips = 1
self.version = None
self.fan_count = 2
self.config = None
def __new__(cls, *args, **kwargs):
if cls is BaseMiner:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls)
def __repr__(self):
return f"{'' if not self.api_type else self.api_type} {'' if not self.model else self.model}: {str(self.ip)}"
def __lt__(self, other):
return ipaddress.ip_address(self.ip) < ipaddress.ip_address(other.ip)
def __gt__(self, other):
return ipaddress.ip_address(self.ip) > ipaddress.ip_address(other.ip)
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username=self.uname,
password=self.pwd,
server_host_key_algs=["ssh-rsa"],
)
return conn
except asyncssh.misc.PermissionDenied:
try:
conn = await asyncssh.connect(
str(self.ip),
known_hosts=None,
username="root",
password="admin",
server_host_key_algs=["ssh-rsa"],
)
return conn
except Exception as e:
raise e
except OSError as e:
logging.warning(f"Connection refused: {self}")
raise e
except Exception as e:
raise e
@abstractmethod
async def fault_light_on(self) -> bool:
pass
@abstractmethod
async def fault_light_off(self) -> bool:
pass
@abstractmethod
async def check_light(self) -> bool:
pass
@abstractmethod
async def get_config(self) -> MinerConfig:
pass
@abstractmethod
async def get_hostname(self) -> str:
pass
@abstractmethod
async def get_model(self) -> str:
pass
@abstractmethod
async def reboot(self) -> bool:
pass
@abstractmethod
async def restart_backend(self) -> bool:
pass
@abstractmethod
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
@abstractmethod
async def get_mac(self) -> str:
pass
@abstractmethod
async def get_errors(self) -> list:
pass
@abstractmethod
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)

View File

@@ -0,0 +1,15 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .cgminer import *

View File

@@ -0,0 +1,327 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import InnosiliconT3HPlus # noqa - Ignore access to _module
from pyasic.data import MinerData
from pyasic.data.error_codes import InnosiliconError
from pyasic.settings import PyasicSettings
from pyasic.config import MinerConfig
from pyasic.errors import APIError
import httpx
import warnings
from typing import Union
import logging
class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
self.uname = "admin"
self.pwd = "admin"
self.jwt = None
async def auth(self):
async with httpx.AsyncClient() as client:
try:
auth = await client.post(
f"http://{self.ip}/api/auth",
data={"username": self.uname, "password": self.pwd},
)
except Exception:
warnings.warn(f"Could not authenticate web token with miner: {self}")
else:
json_auth = auth.json()
self.jwt = json_auth.get("jwt")
return self.jwt
async def send_web_command(self, command: str, data: Union[dict, None] = None):
if not self.jwt:
await self.auth()
if not data:
data = {}
async with httpx.AsyncClient() as client:
for i in range(PyasicSettings().miner_get_data_retries):
response = await client.post(
f"http://{self.ip}/api/{command}",
headers={"Authorization": "Bearer " + self.jwt},
timeout=5,
data=data,
)
json_data = response.json()
if (
not json_data.get("success")
and "token" in json_data
and json_data.get("token") == "expired"
):
# refresh the token, retry
await self.auth()
continue
if not json_data.get("success"):
if json_data.get("msg"):
raise APIError(json_data["msg"])
elif json_data.get("message"):
raise APIError(json_data["message"])
raise APIError("Innosilicon web api command failed.")
return json_data
async def fault_light_on(self) -> bool:
return False
async def fault_light_off(self) -> bool:
return False
async def get_config(self) -> MinerConfig:
pools = None
cfg = MinerConfig()
try:
pools = await self.api.pools()
except APIError as e:
logging.warning(e)
if pools:
if "POOLS" in pools.keys():
cfg = cfg.from_api(pools["POOLS"])
return cfg
async def get_mac(self) -> Union[str, None]:
try:
data = await self.send_web_command("overview")
except APIError:
pass
else:
if data.get("version"):
return data["version"].get("ethaddr").upper()
async def get_hostname(self) -> Union[str, None]:
return None
async def get_model(self) -> Union[str, None]:
try:
data = await self.send_web_command("type")
except APIError:
pass
else:
return data["type"]
async def reboot(self) -> bool:
try:
data = await self.send_web_command("reboot")
except APIError:
pass
else:
return data["success"]
async def restart_cgminer(self) -> bool:
try:
data = await self.send_web_command("restartCgMiner")
except APIError:
pass
else:
return data["success"]
async def restart_backend(self) -> bool:
return await self.restart_cgminer()
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
await self.send_web_command(
"updatePools", data=config.as_inno(user_suffix=user_suffix)
)
async def get_errors(self) -> list:
errors = []
try:
data = await self.send_web_command("getErrorDetail")
except APIError:
pass
else:
if "code" in data:
err = data["code"]
if isinstance(err, str):
err = int(err)
if not err == 0:
errors.append(InnosiliconError(error_code=err))
return errors
async def get_data(self) -> MinerData:
"""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
fan_offset = -1
model = await self.get_model()
hostname = await self.get_hostname()
if model:
data.model = model
if hostname:
data.hostname = hostname
data.errors = await self.get_errors()
data.fault_light = await self.check_light()
miner_data = None
all_data = None
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True
)
if miner_data:
break
try:
all_data = (await self.send_web_command("getAll"))["all"]
except APIError:
pass
if not (miner_data or all_data):
return data
summary = miner_data.get("summary")
pools = miner_data.get("pools")
stats = miner_data.get("stats")
if summary:
summary = summary[0]
hr = summary.get("SUMMARY")
if hr:
if len(hr) > 0:
hr = hr[0].get("MHS 1m")
if hr:
data.hashrate = round(hr / 1000000, 2)
elif all_data:
if all_data.get("total_hash"):
print(all_data["total_hash"])
hr = all_data["total_hash"].get("Hash Rate H")
if hr:
data.hashrate = round(hr / 1000000000000, 2)
if stats:
stats = stats[0]
if stats.get("STATS"):
board_map = {0: "left", 1: "center", 2: "right"}
for idx, board in enumerate(stats["STATS"]):
chips = board.get("Num active chips")
if chips:
setattr(data, f"{board_map[idx]}_chips", chips)
temp = board.get("Temp")
if temp:
setattr(data, f"{board_map[idx]}_board_chip_temp", temp)
if all_data:
if all_data.get("chain"):
board_map = {0: "left", 1: "center", 2: "right"}
for idx, board in enumerate(all_data["chain"]):
temp = board.get("Temp max")
if temp:
setattr(data, f"{board_map[idx]}_board_chip_temp", temp)
temp_board = board.get("Temp min")
if temp_board:
setattr(data, f"{board_map[idx]}_board_temp", temp_board)
hr = board.get("Hash Rate H")
if hr:
setattr(
data,
f"{board_map[idx]}_board_hashrate",
round(hr / 1000000000000, 2),
)
if all_data.get("fansSpeed"):
speed = round((all_data["fansSpeed"] * 6000) / 100)
for fan in range(self.fan_count):
setattr(data, f"fan_{fan+1}", speed)
if all_data.get("mac"):
data.mac = all_data["mac"].upper()
else:
mac = await self.get_mac()
if mac:
data.mac = mac
if all_data.get("power"):
data.wattage = all_data["power"]
if pools or all_data.get("pools_config"):
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
if pools:
pools = pools[0]
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
if pool.get("Quota"):
pool_2_quota = pool.get("Quota")
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
if pool.get("Quota"):
pool_2_quota = pool.get("Quota")
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
if pool.get("Quota"):
pool_2_quota = pool.get("Quota")
elif all_data.get("pools_config"):
print(all_data["pools_config"])
for pool in all_data["pools_config"]:
if not pool_1_user:
pool_1_user = pool.get("user")
pool_1 = pool["url"]
elif not pool_2_user:
pool_2_user = pool.get("user")
pool_2 = pool["url"]
if not pool.get("user") == pool_1_user:
if not pool_2_user == pool.get("user"):
pool_2_user = pool.get("user")
pool_2 = pool["url"]
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
pool_1 = pool_1.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_1_url = pool_1
if pool_1_user:
data.pool_1_user = pool_1_user
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "").replace(
"stratum2+tcp://", ""
)
data.pool_2_url = pool_2
if pool_2_user:
data.pool_2_user = pool_2_user
if quota:
data.pool_split = str(quota)
return data

View File

@@ -0,0 +1,15 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .T3H_Plus import CGMinerInnosiliconT3HPlus

View File

@@ -0,0 +1,15 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .T3X import *

View File

@@ -12,14 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import TypeVar, Tuple, List, Union
from typing import Tuple, List, Union
from collections.abc import AsyncIterable
from pyasic.miners import BaseMiner
from pyasic.miners.base import AnyMiner
import httpx
from pyasic.miners.antminer import *
from pyasic.miners.avalonminer import *
from pyasic.miners.whatsminer import *
from pyasic.miners.innosilicon import *
from pyasic.miners._backends.cgminer import CGMiner # noqa - Ignore _module import
from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import
@@ -31,7 +32,9 @@ from pyasic.miners._backends.bosminer_old import ( # noqa - Ignore _module impo
from pyasic.miners.unknown import UnknownMiner
from pyasic.API import APIError
from pyasic.errors import APIError
from pyasic.misc import Singleton
import asyncio
import ipaddress
@@ -42,8 +45,6 @@ from pyasic.settings import PyasicSettings
import asyncssh
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
MINER_CLASSES = {
"ANTMINER S9": {
"Default": BOSMinerS9,
@@ -60,6 +61,7 @@ MINER_CLASSES = {
"Default": BMMinerT9,
"BMMiner": BMMinerT9,
"Hiveon": HiveonT9,
"CGMiner": CGMinerT9,
},
"ANTMINER S17": {
"Default": BMMinerS17,
@@ -205,59 +207,54 @@ MINER_CLASSES = {
"BTMiner": BTMinerM32,
"20": BTMinerM32V20,
},
"AvalonMiner 721": {
"AVALONMINER 721": {
"Default": CGMinerAvalon721,
"CGMiner": CGMinerAvalon721,
},
"AvalonMiner 741": {
"AVALONMINER 741": {
"Default": CGMinerAvalon741,
"CGMiner": CGMinerAvalon741,
},
"AvalonMiner 761": {
"AVALONMINER 761": {
"Default": CGMinerAvalon761,
"CGMiner": CGMinerAvalon761,
},
"AvalonMiner 821": {
"AVALONMINER 821": {
"Default": CGMinerAvalon821,
"CGMiner": CGMinerAvalon821,
},
"AvalonMiner 841": {
"AVALONMINER 841": {
"Default": CGMinerAvalon841,
"CGMiner": CGMinerAvalon841,
},
"AvalonMiner 851": {
"AVALONMINER 851": {
"Default": CGMinerAvalon851,
"CGMiner": CGMinerAvalon851,
},
"AvalonMiner 921": {
"AVALONMINER 921": {
"Default": CGMinerAvalon921,
"CGMiner": CGMinerAvalon921,
},
"AvalonMiner 1026": {
"AVALONMINER 1026": {
"Default": CGMinerAvalon1026,
"CGMiner": CGMinerAvalon1026,
},
"AvalonMiner 1047": {
"AVALONMINER 1047": {
"Default": CGMinerAvalon1047,
"CGMiner": CGMinerAvalon1047,
},
"AvalonMiner 1066": {
"AVALONMINER 1066": {
"Default": CGMinerAvalon1066,
"CGMiner": CGMinerAvalon1066,
},
"T3H+": {
"Default": CGMinerInnosiliconT3HPlus,
"CGMiner": CGMinerInnosiliconT3HPlus,
},
"Unknown": {"Default": UnknownMiner},
}
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class MinerFactory(metaclass=Singleton):
"""A factory to handle identification and selection of the proper class of miner"""
@@ -306,10 +303,7 @@ class MinerFactory(metaclass=Singleton):
if ip in self.miners:
return self.miners[ip]
# if everything fails, the miner is already set to unknown
miner = UnknownMiner(str(ip))
api = None
model = None
ver = None
model, api, ver = None, None, None
# try to get the API multiple times based on retries
for i in range(PyasicSettings().miner_factory_get_version_retries):
@@ -330,6 +324,24 @@ class MinerFactory(metaclass=Singleton):
break
except asyncio.TimeoutError:
logging.warning(f"{ip}: Get Miner Timed Out")
miner = self._select_miner_from_classes(ip, model, api, ver)
# save the miner to the cache at its IP if its not unknown
if not isinstance(miner, UnknownMiner):
self.miners[ip] = miner
# return the miner
return miner
@staticmethod
def _select_miner_from_classes(
ip: ipaddress.ip_address,
model: Union[str, None],
api: Union[str, None],
ver: Union[str, None],
) -> AnyMiner:
miner = UnknownMiner(str(ip))
# make sure we have model information
if model:
if not api:
@@ -365,11 +377,6 @@ class MinerFactory(metaclass=Singleton):
elif "BMMiner" in api:
miner = BMMiner(str(ip))
# save the miner to the cache at its IP if its not unknown
if not isinstance(miner, UnknownMiner):
self.miners[ip] = miner
# return the miner
return miner
def clear_cached_miners(self) -> None:
@@ -380,13 +387,139 @@ class MinerFactory(metaclass=Singleton):
async def _get_miner_type(
self, ip: Union[ipaddress.ip_address, str]
) -> Tuple[Union[str, None], Union[str, None], Union[str, None]]:
data = None
model, api, ver = None, None, None
model = None
api = None
ver = None
try:
devdetails, version = await self.__get_devdetails_and_version(ip)
except APIError as e:
# 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 or ConnectionRefusedError:
# miner refused connection on API port, we wont be able to get data this way
# try ssh
try:
_model = await self.__get_model_from_ssh(ip)
if _model:
model = _model
api = "BOSMiner+"
except asyncssh.misc.PermissionDenied:
try:
data = await self.__get_system_info_from_web(ip)
if not data.get("success"):
_model = await self.__get_dragonmint_version_from_web(ip)
if _model:
model = _model
if "minertype" in data:
model = data["minertype"].upper()
if "bmminer" in "\t".join(data):
api = "BMMiner"
except Exception as e:
logging.debug(f"Unable to get miner - {e}")
return model, api, ver
devdetails = None
# if we have devdetails, we can get model data from there
if devdetails:
for _devdetails_key in ["Model", "Driver"]:
try:
model = devdetails["DEVDETAILS"][0][_devdetails_key].upper()
if not model == "BITMICRO":
break
except KeyError:
continue
if not model:
# braiins OS bug check just in case
if "s9" in devdetails["STATUS"][0]["Description"]:
model = "ANTMINER S9"
if "s17" in version["STATUS"][0]["Description"]:
model = "ANTMINER S17"
# if we have version we can get API type from here
if version:
if "VERSION" in version:
api_types = ["BMMiner", "CGMiner", "BTMiner"]
# check basic API types, BOSMiner needs a special check
for api_type in api_types:
if any(api_type in string for string in version["VERSION"][0]):
api = api_type
# check if there are any BOSMiner strings in any of the dict keys
if any("BOSminer" in string for string in version["VERSION"][0]):
api = "BOSMiner"
if version["VERSION"][0].get("BOSminer"):
if "plus" in version["VERSION"][0]["BOSminer"]:
api = "BOSMiner+"
if "BOSminer+" in version["VERSION"][0]:
api = "BOSMiner+"
# check for avalonminers
for _version_key in ["PROD", "MODEL"]:
try:
_data = version["VERSION"][0][_version_key].split("-")
except KeyError:
continue
model = _data[0].upper()
if _version_key == "MODEL":
model = f"AVALONMINER {_data[0]}"
if len(_data) > 1:
ver = _data[1]
if version.get("Description") and (
"whatsminer" in version.get("Description")
):
api = "BTMiner"
# if we have no model from devdetails but have version, try to get it from there
if version and not model:
try:
model = version["VERSION"][0]["Type"].upper()
except KeyError:
pass
if not model:
stats = await self._send_api_command(str(ip), "stats")
if stats:
try:
_model = stats["STATS"][0]["Type"].upper()
except KeyError:
pass
else:
for split_point in [" BB", " XILINX", " (VNISH"]:
if split_point in _model:
_model = _model.split(split_point)[0]
if "PRO" in _model and " PRO" not in _model:
_model = _model.replace("PRO", " PRO")
model = _model
else:
_model = await self.__get_dragonmint_version_from_web(ip)
if _model:
model = _model
if model:
if "DRAGONMINT" in model:
_model = await self.__get_dragonmint_version_from_web(ip)
if _model:
model = _model
if " HIVEON" in model:
# do hiveon check before whatsminer as HIVEON contains a V
model = model.split(" HIVEON")[0]
api = "Hiveon"
# whatsminer have a V in their version string (M20SV41), everything after it is ver
if "V" in model:
_ver = model.split("V")
if len(_ver) > 1:
ver = model.split("V")[1]
model = model.split("V")[0]
# don't need "Bitmain", just "ANTMINER XX" as model
if "BITMAIN " in model:
model = model.replace("BITMAIN ", "")
return model, api, ver
async def __get_devdetails_and_version(
self, ip
) -> Tuple[Union[dict, None], Union[dict, None]]:
version = None
try:
# get device details and version data
@@ -398,179 +531,83 @@ class MinerFactory(metaclass=Singleton):
# copy each part of the main command to devdetails and version
devdetails = data["devdetails"][0]
version = data["version"][0]
return devdetails, version
except APIError:
try:
# try devdetails and version separately (X19s mainly require this)
# get devdetails and validate
devdetails = await self._send_api_command(str(ip), "devdetails")
validation = await self._validate_command(devdetails)
# try devdetails and version separately (X19s mainly require this)
# get devdetails and validate
devdetails = await self._send_api_command(str(ip), "devdetails")
validation = await self._validate_command(devdetails)
if not validation[0]:
# if devdetails fails try version instead
devdetails = None
# get version and validate
version = await self._send_api_command(str(ip), "version")
validation = await self._validate_command(version)
if not validation[0]:
# if devdetails fails try version instead
devdetails = None
# get version and validate
version = await self._send_api_command(str(ip), "version")
# finally try get_version (Whatsminers) and validate
version = await self._send_api_command(str(ip), "get_version")
validation = await self._validate_command(version)
# if this fails we raise an error to be caught below
if not validation[0]:
# finally try get_version (Whatsminers) and validate
version = await self._send_api_command(str(ip), "get_version")
validation = await self._validate_command(version)
raise APIError(validation[1])
return devdetails, version
# if this fails we raise an error to be caught below
if not validation[0]:
raise APIError(validation[1])
except APIError as e:
# 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 or ConnectionRefusedError:
# miner refused connection on API port, we wont be able to get data this way
# try ssh
@staticmethod
async def __get_model_from_ssh(ip: ipaddress.ip_address) -> Union[str, None]:
model = None
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 == "am1-s9":
model = "ANTMINER S9"
if board_name == "am2-s17":
model = "ANTMINER S17"
return model
@staticmethod
async def __get_system_info_from_web(ip) -> dict:
url = f"http://{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()
return data
@staticmethod
async def __get_dragonmint_version_from_web(
ip: ipaddress.ip_address,
) -> Union[str, None]:
response = None
async with httpx.AsyncClient() as client:
try:
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"].upper()
if "bmminer" in "\t".join(data.keys()):
api = "BMMiner"
except Exception as e:
logging.debug(f"Unable to get miner - {e}")
return None, None, None
# if we have devdetails, we can get model data from there
if devdetails:
if "DEVDETAILS" in devdetails.keys() and not devdetails["DEVDETAILS"] == []:
# check for model, for most miners
if not devdetails["DEVDETAILS"][0]["Model"] == "":
# model of most miners
model = devdetails["DEVDETAILS"][0]["Model"].upper()
# if model fails, try driver
else:
# some avalonminers have model in driver
model = devdetails["DEVDETAILS"][0]["Driver"].upper()
else:
if "s9" in devdetails["STATUS"][0]["Description"]:
model = "ANTMINER S9"
# if we have version we can get API type from here
if version:
if "VERSION" in version.keys():
# check if there are any BMMiner strings in any of the dict keys
if any("BMMiner" in string for string in version["VERSION"][0].keys()):
api = "BMMiner"
# check if there are any CGMiner strings in any of the dict keys
elif any(
"CGMiner" in string for string in version["VERSION"][0].keys()
):
api = "CGMiner"
elif any(
"BTMiner" in string for string in version["VERSION"][0].keys()
):
api = "BTMiner"
# check if there are any BOSMiner strings in any of the dict keys
elif any(
"BOSminer" in string for string in version["VERSION"][0].keys()
):
api = "BOSMiner"
if version["VERSION"][0].get("BOSminer"):
if "plus" in version["VERSION"][0]["BOSminer"]:
api = "BOSMiner+"
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].upper()
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"
# 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
if (
"VERSION" in version.keys()
and version.get("VERSION")
and not version.get("VERSION") == []
):
# try to get "Type" which is model
if version["VERSION"][0].get("Type"):
model = version["VERSION"][0]["Type"].upper()
# braiins OS bug check just in case
elif "am2-s17" in version["STATUS"][0]["Description"]:
model = "ANTMINER S17"
if not model:
stats = await self._send_api_command(str(ip), "stats")
if stats:
if "STATS" in stats.keys():
if stats["STATS"][0].get("Type"):
_model = stats["STATS"][0]["Type"].upper()
if " BB" in _model:
_model = _model.split(" BB")[0]
if " XILINX" in _model:
_model = _model.split(" XILINX")[0]
if "PRO" in _model and not " PRO" in _model:
model = _model.replace("PRO", " PRO")
if model:
if " HIVEON" in model:
model = model.split(" HIVEON")[0]
api = "Hiveon"
# whatsminer have a V in their version string (M20SV41), remove everything after it
if "V" in model:
_ver = model.split("V")
if len(_ver) > 1:
ver = model.split("V")[1]
model = model.split("V")[0]
# don't need "Bitmain", just "ANTMINER XX" as model
if "BITMAIN " in model:
model = model.replace("BITMAIN ", "")
return model, api, ver
auth = (
await client.post(
f"http://{ip}/api/auth",
data={"username": "admin", "password": "admin"},
)
).json()["jwt"]
response = (
await client.post(
f"http://{ip}/api/type",
headers={"Authorization": "Bearer " + auth},
data={},
)
).json()
except Exception as e:
logging.info(e)
if response:
return response["type"]
@staticmethod
async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]:
@@ -594,9 +631,7 @@ class MinerFactory(metaclass=Singleton):
else:
# make sure the command succeeded
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
# this is an error
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
return False, data["STATUS"][0]["Msg"]
return False, data["STATUS"][0]["Msg"]
return True, None
@staticmethod

View File

@@ -14,14 +14,7 @@
import asyncio
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
from pyasic.misc import Singleton
class _MinerListener:

View File

@@ -13,7 +13,9 @@
# limitations under the License.
from pyasic.API.unknown import UnknownAPI
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
from pyasic.config import MinerConfig
from pyasic.data import MinerData
class UnknownMiner(BaseMiner):
@@ -57,3 +59,9 @@ class UnknownMiner(BaseMiner):
async def restart_backend(self) -> bool:
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
return None
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))

View File

@@ -17,6 +17,7 @@ from pyasic.miners._types import ( # noqa - Ignore access to _module
M30SPlusPlus,
M30SPlusPlusVG40,
M30SPlusPlusVG30,
M30SPlusPlusVH60,
)
@@ -36,3 +37,9 @@ class BTMinerM30SPlusPlusVG40(BTMiner, M30SPlusPlusVG40):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM30SPlusPlusVH60(BTMiner, M30SPlusPlusVH60):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip

View File

@@ -29,6 +29,7 @@ from .M30S_Plus_Plus import (
BTMinerM30SPlusPlus,
BTMinerM30SPlusPlusVG40,
BTMinerM30SPlusPlusVG30,
BTMinerM30SPlusPlusVH60,
)
from .M31S import BTMinerM31S

22
pyasic/misc/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]

View File

@@ -37,7 +37,9 @@ class MinerNetwork:
"""
def __init__(
self, ip_addr: Union[str, None] = None, mask: Union[str, int, None] = None
self,
ip_addr: Union[str, List[str], None] = None,
mask: Union[str, int, None] = None,
) -> None:
self.network = None
self.ip_addr = ip_addr
@@ -63,18 +65,14 @@ class MinerNetwork:
if self.network:
return self.network
# if there is no IP address passed, default to 192.168.1.0
if not self.ip_addr:
self.ip_addr = "192.168.1.0"
if "-" in self.ip_addr:
self.network = MinerNetworkRange(self.ip_addr)
elif isinstance(self.ip_addr, list):
self.network = MinerNetworkRange(self.ip_addr)
else:
# if there is no IP address passed, default to 192.168.1.0
if not self.ip_addr:
default_gateway = "192.168.1.0"
# if we do have an IP address passed, use that
else:
default_gateway = self.ip_addr
# if there is no subnet mask passed, default to /24
if not self.mask:
subnet_mask = "24"
@@ -84,7 +82,7 @@ class MinerNetwork:
# save the network and return it
self.network = ipaddress.ip_network(
f"{default_gateway}/{subnet_mask}", strict=False
f"{self.ip_addr}/{subnet_mask}", strict=False
)
logging.debug(f"Setting MinerNetwork: {self.network}")

View File

@@ -14,14 +14,7 @@
from dataclasses import dataclass
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
from pyasic.misc import Singleton
@dataclass

View File

@@ -1,54 +0,0 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
from pyasic.miners.miner_factory import MINER_CLASSES
import inspect
import sys
class MinersTest(unittest.TestCase):
def test_miner_model_creation(self):
for miner_model in MINER_CLASSES.keys():
for miner_api in MINER_CLASSES[miner_model].keys():
with self.subTest(miner_model=miner_model, miner_api=miner_api):
miner = MINER_CLASSES[miner_model][miner_api]("0.0.0.0")
self.assertTrue(
isinstance(miner, MINER_CLASSES[miner_model][miner_api])
)
def test_miner_backend_backup_creation(self):
backends = inspect.getmembers(
sys.modules["pyasic.miners._backends"], inspect.isclass
)
for backend in backends:
miner_class = backend[1]
with self.subTest(miner_class=miner_class):
miner = miner_class("0.0.0.0")
self.assertTrue(isinstance(miner, miner_class))
def test_miner_type_creation_failure(self):
backends = inspect.getmembers(
sys.modules["pyasic.miners._types"], inspect.isclass
)
for backend in backends:
miner_class = backend[1]
with self.subTest(miner_class=miner_class):
with self.assertRaises(TypeError):
miner_class("0.0.0.0")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyasic"
version = "0.15.2"
version = "0.17.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"

Binary file not shown.

22
tests/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
from tests.miners_tests import MinersTest, MinerFactoryTest
from tests.network_tests import NetworkTest
from tests.config_tests import ConfigTest
if __name__ == "__main__":
# `coverage run --source pyasic -m unittest discover` will give code coverage data
unittest.main()

View File

@@ -0,0 +1,111 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
from pyasic.config import MinerConfig, _PoolGroup, _Pool # noqa
from tests.test_data import (
bosminer_api_pools,
x19_api_pools,
x19_web_pools,
bosminer_config_pools,
)
class ConfigTest(unittest.TestCase):
def setUp(self) -> None:
self.test_config = MinerConfig(
pool_groups=[
_PoolGroup(
quota=1,
group_name="TEST",
pools=[
_Pool(
url="stratum+tcp://pyasic.testpool_1.pool:3333",
username="pyasic.test",
password="123",
),
_Pool(
url="stratum+tcp://pyasic.testpool_2.pool:3333",
username="pyasic.test",
password="123",
),
],
)
],
temp_mode="auto",
temp_target=70.0,
temp_hot=80.0,
temp_dangerous=100.0,
fan_speed=None,
autotuning_enabled=True,
autotuning_wattage=900,
)
def test_config_from_raw(self):
bos_config = MinerConfig().from_raw(bosminer_config_pools)
bos_config.pool_groups[0].group_name = "TEST"
with self.subTest(
msg="Testing BOSMiner config file config.", bos_config=bos_config
):
self.assertEqual(bos_config, self.test_config)
x19_cfg = MinerConfig().from_raw(x19_web_pools)
x19_cfg.pool_groups[0].group_name = "TEST"
with self.subTest(msg="Testing X19 API config.", x19_cfg=x19_cfg):
self.assertEqual(x19_cfg, self.test_config)
def test_config_from_api(self):
bos_cfg = MinerConfig().from_api(bosminer_api_pools["POOLS"])
bos_cfg.pool_groups[0].group_name = "TEST"
with self.subTest(msg="Testing BOSMiner API config.", bos_cfg=bos_cfg):
self.assertEqual(bos_cfg, self.test_config)
x19_cfg = MinerConfig().from_api(x19_api_pools["POOLS"])
x19_cfg.pool_groups[0].group_name = "TEST"
with self.subTest(msg="Testing X19 API config.", x19_cfg=x19_cfg):
self.assertEqual(x19_cfg, self.test_config)
def test_config_as_types(self):
cfg = MinerConfig().from_api(bosminer_api_pools["POOLS"])
cfg.pool_groups[0].group_name = "TEST"
commands = [
func
for func in
# each function in self
dir(cfg)
if callable(getattr(cfg, func)) and
# no __ methods
not func.startswith("__")
]
for command in [cmd for cmd in commands if cmd.startswith("as_")]:
with self.subTest():
output = getattr(cfg, command)()
self.assertEqual(output, getattr(self.test_config, command)())
if f"from_{command.split('as_')[1]}" in commands:
self.assertEqual(
getattr(MinerConfig(), f"from_{command.split('as_')[1]}")(
output
),
self.test_config,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,162 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
from pyasic.miners.miner_factory import MINER_CLASSES
from pyasic.miners.base import BaseMiner
from pyasic.miners._backends import CGMiner
from pyasic.miners.miner_factory import MinerFactory
from pyasic.miners.miner_listener import MinerListener
import asyncio
import inspect
import sys
class MinersTest(unittest.TestCase):
def test_miner_model_creation(self):
for miner_model in MINER_CLASSES.keys():
for miner_api in MINER_CLASSES[miner_model].keys():
with self.subTest(
msg=f"Creation of miner using model={miner_model}, api={miner_api}",
miner_model=miner_model,
miner_api=miner_api,
):
miner = MINER_CLASSES[miner_model][miner_api]("0.0.0.0")
self.assertTrue(
isinstance(miner, MINER_CLASSES[miner_model][miner_api])
)
def test_miner_backend_backup_creation(self):
backends = inspect.getmembers(
sys.modules["pyasic.miners._backends"], inspect.isclass
)
for backend in backends:
miner_class = backend[1]
with self.subTest(miner_class=miner_class):
miner = miner_class("0.0.0.0")
self.assertTrue(isinstance(miner, miner_class))
def test_miner_type_creation_failure(self):
backends = inspect.getmembers(
sys.modules["pyasic.miners._types"], inspect.isclass
)
for backend in backends:
miner_class = backend[1]
with self.subTest(miner_class=miner_class):
with self.assertRaises(TypeError):
miner_class("0.0.0.0")
with self.assertRaises(TypeError):
BaseMiner("0.0.0.0")
def test_miner_comparisons(self):
miner_1 = CGMiner("1.1.1.1")
miner_2 = CGMiner("2.2.2.2")
miner_3 = CGMiner("1.1.1.1")
self.assertEqual(miner_1, miner_3)
self.assertGreater(miner_2, miner_1)
self.assertLess(miner_3, miner_2)
class MinerFactoryTest(unittest.TestCase):
def test_miner_factory_creation(self):
self.assertDictEqual(MinerFactory().miners, {})
miner_factory = MinerFactory()
self.assertIs(MinerFactory(), miner_factory)
def test_get_miner_generator(self):
async def _coro():
gen = MinerFactory().get_miner_generator([])
miners = []
async for miner in gen:
miners.append(miner)
return miners
_miners = asyncio.run(_coro())
self.assertListEqual(_miners, [])
def test_miner_selection(self):
for miner_model in MINER_CLASSES.keys():
with self.subTest():
miner = MinerFactory()._select_miner_from_classes(
"0.0.0.0", miner_model, None, None
)
self.assertIsInstance(miner, BaseMiner)
for api in ["BOSMiner+", "BOSMiner", "CGMiner", "BTMiner", "BMMiner"]:
with self.subTest():
miner = MinerFactory()._select_miner_from_classes(
"0.0.0.0", None, api, None
)
self.assertIsInstance(miner, BaseMiner)
with self.subTest():
miner = MinerFactory()._select_miner_from_classes(
"0.0.0.0", "ANTMINER S17+", "Fake API", None
)
self.assertIsInstance(miner, BaseMiner)
with self.subTest():
miner = MinerFactory()._select_miner_from_classes(
"0.0.0.0", "M30S", "BTMiner", "G20"
)
self.assertIsInstance(miner, BaseMiner)
def test_validate_command(self):
bad_test_data_returns = [
{},
{
"cmd": [
{
"STATUS": [
{"STATUS": "E", "Msg": "Command failed for some reason."}
]
}
]
},
{"STATUS": "E", "Msg": "Command failed for some reason."},
{
"STATUS": [{"STATUS": "E", "Msg": "Command failed for some reason."}],
"id": 1,
},
]
for data in bad_test_data_returns:
with self.subTest():
async def _coro(miner_ret):
_data = await MinerFactory()._validate_command(miner_ret)
return _data
ret = asyncio.run(_coro(data))
self.assertFalse(ret[0])
good_test_data_returns = [
{
"STATUS": [{"STATUS": "S", "Msg": "Yay! Command succeeded."}],
"id": 1,
},
]
for data in good_test_data_returns:
with self.subTest():
async def _coro(miner_ret):
_data = await MinerFactory()._validate_command(miner_ret)
return _data
ret = asyncio.run(_coro(data))
self.assertTrue(ret[0])
if __name__ == "__main__":
unittest.main()

View File

@@ -31,8 +31,8 @@ class NetworkTest(unittest.TestCase):
"192.168.1.60",
]
net_1 = list(MinerNetworkRange(net_range_str).hosts())
net_2 = list(MinerNetworkRange(net_range_list).hosts())
net_1 = list(MinerNetwork(net_range_str).get_network().hosts())
net_2 = list(MinerNetwork(net_range_list).get_network().hosts())
correct_net = [
ipaddress.IPv4Address("192.168.1.29"),
@@ -48,7 +48,7 @@ class NetworkTest(unittest.TestCase):
def test_net(self):
net_1_str = "192.168.1.0"
net_1_mask = 29
net_1_mask = "/29"
net_1 = list(MinerNetwork(net_1_str, mask=net_1_mask).get_network().hosts())
@@ -68,6 +68,20 @@ class NetworkTest(unittest.TestCase):
self.assertTrue(net_1 == correct_net)
self.assertTrue(net_2 == correct_net)
def test_net_len(self):
net = MinerNetwork("192.168.1.0", mask=32)
self.assertEqual(len(net), 1)
net2 = MinerNetwork("192.168.1.0", mask=31)
self.assertEqual(len(net2), 2)
def test_net_defaults(self):
net = MinerNetwork()
net_obj = net.get_network()
self.assertEqual(net_obj, MinerNetwork("192.168.1.0", mask=24).get_network())
self.assertEqual(net_obj, net.get_network())
if __name__ == "__main__":
unittest.main()

128
tests/test_data.py Normal file
View File

@@ -0,0 +1,128 @@
# Copyright 2022 Upstream Data Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
bosminer_api_pools = {
"STATUS": [
{
"STATUS": "S",
"Msg": "2 Pool(s)",
"Description": "",
}
],
"POOLS": [
{
"POOL": 0,
"URL": "stratum+tcp://pyasic.testpool_1.pool:3333",
"Status": "Alive",
"Quota": 1,
"User": "pyasic.test",
"Stratum URL": "pyasic.testpool_1.pool:3333",
"AsicBoost": True,
},
{
"POOL": 1,
"URL": "stratum+tcp://pyasic.testpool_2.pool:3333",
"Status": "Alive",
"Quota": 1,
"User": "pyasic.test",
"Stratum URL": "pyasic.testpool_2.pool:3333",
"AsicBoost": True,
},
],
"id": 1,
}
x19_api_pools = {
"STATUS": [
{
"STATUS": "S",
"Msg": "2 Pool(s)",
"Description": "",
}
],
"POOLS": [
{
"POOL": 0,
"URL": "stratum+tcp://pyasic.testpool_1.pool:3333",
"Status": "Alive",
"Quota": 1,
"User": "pyasic.test",
"Stratum URL": "pyasic.testpool_1.pool:3333",
},
{
"POOL": 1,
"URL": "stratum+tcp://pyasic.testpool_2.pool:3333",
"Status": "Alive",
"Quota": 1,
"User": "pyasic.test",
"Stratum URL": "pyasic.testpool_2.pool:3333",
},
],
"id": 1,
}
x19_web_pools = {
"pools": [
{
"url": "stratum+tcp://pyasic.testpool_1.pool:3333",
"user": "pyasic.test",
"pass": "123",
},
{
"url": "stratum+tcp://pyasic.testpool_2.pool:3333",
"user": "pyasic.test",
"pass": "123",
},
],
"api-listen": True,
"api-network": True,
"api-groups": "A:stats:pools:devs:summary:version",
"api-allow": "A:0/0,W:*",
"bitmain-fan-ctrl": False,
"bitmain-fan-pwm": "100",
"bitmain-use-vil": True,
"bitmain-freq": "675",
"bitmain-voltage": "1400",
"bitmain-ccdelay": "0",
"bitmain-pwth": "0",
"bitmain-work-mode": "0",
"bitmain-freq-level": "100",
}
bosminer_config_pools = {
"format": {
"version": "1.2+",
"model": "Antminer S9",
"generator": "pyasic",
},
"group": [
{
"name": "TEST",
"quota": 1,
"pool": [
{
"enabled": True,
"url": "stratum+tcp://pyasic.testpool_1.pool:3333",
"user": "pyasic.test",
"password": "123",
},
{
"enabled": True,
"url": "stratum+tcp://pyasic.testpool_2.pool:3333",
"user": "pyasic.test",
"password": "123",
},
],
},
],
}