Compare commits

..

1 Commits

Author SHA1 Message Date
UpstreamData
7a1936d095 improve how the fault light looks on whatsminers 2022-08-11 11:59:19 -06:00
81 changed files with 444 additions and 1293 deletions

View File

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

View File

@@ -7,9 +7,6 @@
[![Read the Docs](https://img.shields.io/readthedocs/pyasic)](https://pyasic.readthedocs.io/en/latest/) [![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) [![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) [![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
Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/) Documentation is located on Read the Docs as [pyasic](https://pyasic.readthedocs.io/en/latest/)
@@ -94,7 +91,7 @@ if __name__ == "__main__":
import asyncio import asyncio
import sys import sys
from pyasic.miners import get_miner from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug # Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy # if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -106,7 +103,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): async def get_miner_data(miner_ip: str):
# Use MinerFactory to get miner # Use MinerFactory to get miner
# MinerFactory is a singleton, so we can just get the instance in place # MinerFactory is a singleton, so we can just get the instance in place
miner = await get_miner(miner_ip) miner = await MinerFactory().get_miner(miner_ip)
# Get data from the miner # Get data from the miner
data = await miner.get_data() data = await miner.get_data()
@@ -125,7 +122,7 @@ If needed, this library exposes a wrapper for the miner API that can be used for
import asyncio import asyncio
import sys import sys
from pyasic.miners import get_miner from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug # Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy # if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -135,7 +132,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): async def get_api_commands(miner_ip: str):
# Get the miner # Get the miner
miner = await get_miner(miner_ip) miner = await MinerFactory().get_miner(miner_ip)
# List all available commands # List all available commands
print(miner.api.get_commands()) print(miner.api.get_commands())
@@ -153,7 +150,7 @@ The miner API commands will raise an `APIError` if they fail with a bad status c
import asyncio import asyncio
import sys import sys
from pyasic.miners import get_miner from pyasic.miners.miner_factory import MinerFactory
# Fix whatsminer bug # Fix whatsminer bug
# if the computer is windows, set the event loop policy to a WindowsSelector policy # if the computer is windows, set the event loop policy to a WindowsSelector policy
@@ -163,7 +160,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): async def get_api_commands(miner_ip: str):
# Get the miner # Get the miner
miner = await get_miner(miner_ip) miner = await MinerFactory().get_miner(miner_ip)
# Run the devdetails command # Run the devdetails command
# This is equivalent to await miner.api.send_command("devdetails") # This is equivalent to await miner.api.send_command("devdetails")

View File

@@ -57,61 +57,3 @@
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 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,45 +50,3 @@
options: options:
show_root_heading: false show_root_heading: false
heading_level: 4 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,16 +1,6 @@
# pyasic # pyasic
## X9 Models ## X9 Models
## X9 (BOS)
::: pyasic.miners.antminer.bosminer.X9.S9.BOSMinerS9
handler: python
options:
show_root_heading: false
heading_level: 4
## S9 ## S9
::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9 ::: pyasic.miners.antminer.bmminer.X9.S9.BMMinerS9

View File

@@ -1,10 +0,0 @@
# 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,24 +5,7 @@ Supported miner types are here on this list. If your miner (or miner version) i
##### pyasic currently supports the following miners and subtypes: ##### pyasic currently supports the following miners and subtypes:
* Braiins OS+ Devices: * Braiins OS+ Devices:
* X19 Series: * All devices supported by BraiinsOS+ are supported here.
* [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: * Stock Firmware Whatsminers:
* M3X Series: * M3X Series:
* [M30S][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30S]: * [M30S][pyasic.miners.whatsminer.btminer.M3X.M30S.BTMinerM30S]:
@@ -37,12 +20,9 @@ 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]: * [M30S++][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlus]:
* [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30] * [VG30][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG30]
* [VG40][pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40] * [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.BTMinerM31S]
* [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]: * [M31S+][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlus]:
* [VE20][pyasic.miners.whatsminer.btminer.M3X.M31S_Plus.BTMinerM31SPlusVE20] * [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] * [M32S][pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S]
* M2X Series: * M2X Series:
* [M20][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20]: * [M20][pyasic.miners.whatsminer.btminer.M2X.M20.BTMinerM20]:
@@ -91,6 +71,3 @@ Supported miner types are here on this list. If your miner (or miner version) i
* [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026] * [A1026][pyasic.miners.avalonminer.cgminer.A10X.A1026.CGMinerAvalon1026]
* [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047] * [A1047][pyasic.miners.avalonminer.cgminer.A10X.A1047.CGMinerAvalon1047]
* [A1066][pyasic.miners.avalonminer.cgminer.A10X.A1066.CGMinerAvalon1066] * [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 show_root_heading: false
heading_level: 4 heading_level: 4
## M30S++VG40 ## M30S+VG40
::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40 ::: pyasic.miners.whatsminer.btminer.M3X.M30S_Plus_Plus.BTMinerM30SPlusPlusVG40
handler: python handler: python
@@ -97,14 +97,6 @@
show_root_heading: false show_root_heading: false
heading_level: 4 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 ## M31S
@@ -130,22 +122,6 @@
show_root_heading: false show_root_heading: false
heading_level: 4 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 ## M32S
::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S ::: pyasic.miners.whatsminer.btminer.M3X.M32S.BTMinerM32S

View File

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

6
poetry.lock generated
View File

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

View File

@@ -78,23 +78,6 @@ class _Pool:
pool = {"url": self.url, "user": username, "pass": self.password} pool = {"url": self.url, "user": username, "pass": self.password}
return pool return pool
def as_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: def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a string usable by an Avalonminer device. """Convert the data in this class to a string usable by an Avalonminer device.
@@ -171,19 +154,6 @@ class _PoolGroup:
pools.append(pool.as_x19(user_suffix=user_suffix)) pools.append(pool.as_x19(user_suffix=user_suffix))
return pools 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]: def as_wm(self, user_suffix: str = None) -> List[dict]:
"""Convert the data in this class to a list usable by an Whatsminer device. """Convert the data in this class to a list usable by an Whatsminer device.
@@ -389,15 +359,7 @@ class MinerConfig:
Parameters: Parameters:
user_suffix: The suffix to append to username. user_suffix: The suffix to append to username.
""" """
return self.pool_groups[0].as_wm(user_suffix=user_suffix) return self.pool_groups[0].as_x19(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: def as_x19(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an X19 device. """Convert the data in this class to a config usable by an X19 device.

View File

@@ -14,11 +14,9 @@
from typing import Union, List from typing import Union, List
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone from datetime import datetime
import time
import json
from .error_codes import X19Error, WhatsminerError, BraiinsOSError, InnosiliconError from .error_codes import X19Error, WhatsminerError, BraiinsOSError
@dataclass @dataclass
@@ -54,7 +52,7 @@ class MinerData:
total_chips: The total number of chips on all boards. Calculated automatically. 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. 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. percent_ideal: The percent of total chips out of the ideal count. Calculated automatically.
nominal: Whether the number of chips in the miner is nominal. Calculated automatically. nominal: The nominal amount of chips in the miner. Calculated automatically.
pool_split: The pool split as a str. pool_split: The pool split as a str.
pool_1_url: The first pool url on the miner 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. pool_1_user: The first pool user on the miner as a str.
@@ -62,7 +60,6 @@ class MinerData:
pool_2_user: The second pool user on the miner as a str. pool_2_user: The second pool user on the miner as a str.
errors: A list of errors on the miner. errors: A list of errors on the miner.
fault_light: Whether or not the fault light is on as a boolean. 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 ip: str
@@ -71,11 +68,11 @@ class MinerData:
model: str = "Unknown" model: str = "Unknown"
hostname: str = "Unknown" hostname: str = "Unknown"
hashrate: float = 0 hashrate: float = 0
left_board_hashrate: float = 0.0 left_board_hashrate: float = 0
center_board_hashrate: float = 0.0 center_board_hashrate: float = 0
right_board_hashrate: float = 0.0 right_board_hashrate: float = 0
temperature_avg: int = field(init=False) temperature_avg: int = field(init=False)
env_temp: float = 0.0 env_temp: float = 0
left_board_temp: int = 0 left_board_temp: int = 0
left_board_chip_temp: int = 0 left_board_chip_temp: int = 0
center_board_temp: int = 0 center_board_temp: int = 0
@@ -100,26 +97,13 @@ class MinerData:
pool_1_user: str = "Unknown" pool_1_user: str = "Unknown"
pool_2_url: str = "" pool_2_url: str = ""
pool_2_user: str = "" pool_2_user: str = ""
errors: List[ errors: List[Union[WhatsminerError, BraiinsOSError, X19Error]] = field(
Union[WhatsminerError, BraiinsOSError, X19Error, InnosiliconError] default_factory=list
] = field(default_factory=list) )
fault_light: Union[bool, None] = None fault_light: Union[bool, None] = None
efficiency: int = field(init=False)
def __post_init__(self): def __post_init__(self):
self.datetime = datetime.now(timezone.utc).astimezone() self.datetime = datetime.now()
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 @property
def total_chips(self): # noqa - Skip PyCharm inspection def total_chips(self): # noqa - Skip PyCharm inspection
@@ -165,55 +149,5 @@ class MinerData:
def temperature_avg(self, val): def temperature_avg(self, val):
pass 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): def asdict(self):
return 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,4 +15,3 @@
from .whatsminer import WhatsminerError from .whatsminer import WhatsminerError
from .bos import BraiinsOSError from .bos import BraiinsOSError
from .X19 import X19Error from .X19 import X19Error
from .innosilicon import InnosiliconError

View File

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

View File

@@ -12,12 +12,126 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import asyncssh
import logging
import ipaddress import ipaddress
from typing import Union from abc import ABC, abstractmethod
from pyasic.miners.base import BaseMiner, AnyMiner from pyasic.data import MinerData
from pyasic.miners.miner_factory import MinerFactory from pyasic.config import MinerConfig
# abstracted version of get miner that is easier to access
async def get_miner(ip: Union[ipaddress.ip_address, str]) -> AnyMiner: class BaseMiner(ABC):
return await MinerFactory().get_miner(ip) 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))

View File

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

View File

@@ -20,7 +20,7 @@ from typing import Union
import toml import toml
from pyasic.miners.base import BaseMiner from pyasic.miners import BaseMiner
from pyasic.API.bosminer import BOSMinerAPI from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API import APIError from pyasic.API import APIError
@@ -215,12 +215,22 @@ class BOSMiner(BaseMiner):
logging.warning(f"Failed to get model for miner: {self}") logging.warning(f"Failed to get model for miner: {self}")
return None return None
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: async def send_config(self, yaml_config, ip_user: bool = False) -> None:
"""Configures miner with yaml config.""" """Configures miner with yaml config."""
logging.debug(f"{self}: Sending config.") logging.debug(f"{self}: Sending config.")
toml_conf = config.as_bos( if ip_user:
model=self.model.replace(" (BOS)", ""), user_suffix=user_suffix 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)", ""))
)
async with (await self._get_ssh_connection()) as conn: async with (await self._get_ssh_connection()) as conn:
await conn.run("/etc/init.d/bosminer stop") await conn.run("/etc/init.d/bosminer stop")
logging.debug(f"{self}: Opening SFTP connection.") logging.debug(f"{self}: Opening SFTP connection.")
@@ -232,14 +242,8 @@ class BOSMiner(BaseMiner):
await conn.run("/etc/init.d/bosminer start") await conn.run("/etc/init.d/bosminer start")
async def check_light(self) -> bool: async def check_light(self) -> bool:
if self.light: if not self.light:
return self.light self.light = False
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 return self.light
async def get_errors(self) -> list: async def get_errors(self) -> list:

View File

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

View File

@@ -18,7 +18,7 @@ from typing import Union
from pyasic.API.btminer import BTMinerAPI from pyasic.API.btminer import BTMinerAPI
from pyasic.miners.base import BaseMiner from pyasic.miners import BaseMiner
from pyasic.API import APIError from pyasic.API import APIError
from pyasic.data import MinerData from pyasic.data import MinerData
@@ -165,8 +165,12 @@ class BTMiner(BaseMiner):
async def restart_backend(self) -> bool: async def restart_backend(self) -> bool:
return False return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: async def send_config(self, yaml_config, ip_user: bool = False):
conf = config.as_wm(user_suffix=user_suffix) 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()
await self.api.update_pools( await self.api.update_pools(
conf[0]["url"], conf[0]["url"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +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.
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

@@ -1,15 +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.
from .T3H_Plus import InnosiliconT3HPlus

View File

@@ -1,15 +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.
from .T3X import *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,10 +55,14 @@ class BMMinerX19(BMMiner):
self.config = MinerConfig().from_raw(data) self.config = MinerConfig().from_raw(data)
return self.config return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None: async def send_config(self, yaml_config, ip_user: bool = False) -> None:
url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi" url = f"http://{self.ip}/cgi-bin/set_miner_conf.cgi"
auth = httpx.DigestAuth(self.uname, self.pwd) auth = httpx.DigestAuth(self.uname, self.pwd)
conf = config.as_x19(user_suffix=user_suffix) 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()
try: try:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,142 +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 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
# 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
@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
async def get_data(self) -> MinerData:
return MinerData(ip=str(self.ip))
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)

View File

@@ -1,15 +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.
from .cgminer import *

View File

@@ -1,327 +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.
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.API 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

@@ -1,15 +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.
from .T3H_Plus import CGMinerInnosiliconT3HPlus

View File

@@ -1,15 +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.
from .T3X import *

View File

@@ -12,15 +12,14 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import Tuple, List, Union from typing import TypeVar, Tuple, List, Union
from collections.abc import AsyncIterable from collections.abc import AsyncIterable
from pyasic.miners.base import AnyMiner from pyasic.miners import BaseMiner
import httpx import httpx
from pyasic.miners.antminer import * from pyasic.miners.antminer import *
from pyasic.miners.avalonminer import * from pyasic.miners.avalonminer import *
from pyasic.miners.whatsminer 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.cgminer import CGMiner # noqa - Ignore _module import
from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import from pyasic.miners._backends.bmminer import BMMiner # noqa - Ignore _module import
@@ -43,6 +42,8 @@ from pyasic.settings import PyasicSettings
import asyncssh import asyncssh
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
MINER_CLASSES = { MINER_CLASSES = {
"ANTMINER S9": { "ANTMINER S9": {
"Default": BOSMinerS9, "Default": BOSMinerS9,
@@ -204,50 +205,46 @@ MINER_CLASSES = {
"BTMiner": BTMinerM32, "BTMiner": BTMinerM32,
"20": BTMinerM32V20, "20": BTMinerM32V20,
}, },
"AVALONMINER 721": { "AvalonMiner 721": {
"Default": CGMinerAvalon721, "Default": CGMinerAvalon721,
"CGMiner": CGMinerAvalon721, "CGMiner": CGMinerAvalon721,
}, },
"AVALONMINER 741": { "AvalonMiner 741": {
"Default": CGMinerAvalon741, "Default": CGMinerAvalon741,
"CGMiner": CGMinerAvalon741, "CGMiner": CGMinerAvalon741,
}, },
"AVALONMINER 761": { "AvalonMiner 761": {
"Default": CGMinerAvalon761, "Default": CGMinerAvalon761,
"CGMiner": CGMinerAvalon761, "CGMiner": CGMinerAvalon761,
}, },
"AVALONMINER 821": { "AvalonMiner 821": {
"Default": CGMinerAvalon821, "Default": CGMinerAvalon821,
"CGMiner": CGMinerAvalon821, "CGMiner": CGMinerAvalon821,
}, },
"AVALONMINER 841": { "AvalonMiner 841": {
"Default": CGMinerAvalon841, "Default": CGMinerAvalon841,
"CGMiner": CGMinerAvalon841, "CGMiner": CGMinerAvalon841,
}, },
"AVALONMINER 851": { "AvalonMiner 851": {
"Default": CGMinerAvalon851, "Default": CGMinerAvalon851,
"CGMiner": CGMinerAvalon851, "CGMiner": CGMinerAvalon851,
}, },
"AVALONMINER 921": { "AvalonMiner 921": {
"Default": CGMinerAvalon921, "Default": CGMinerAvalon921,
"CGMiner": CGMinerAvalon921, "CGMiner": CGMinerAvalon921,
}, },
"AVALONMINER 1026": { "AvalonMiner 1026": {
"Default": CGMinerAvalon1026, "Default": CGMinerAvalon1026,
"CGMiner": CGMinerAvalon1026, "CGMiner": CGMinerAvalon1026,
}, },
"AVALONMINER 1047": { "AvalonMiner 1047": {
"Default": CGMinerAvalon1047, "Default": CGMinerAvalon1047,
"CGMiner": CGMinerAvalon1047, "CGMiner": CGMinerAvalon1047,
}, },
"AVALONMINER 1066": { "AvalonMiner 1066": {
"Default": CGMinerAvalon1066, "Default": CGMinerAvalon1066,
"CGMiner": CGMinerAvalon1066, "CGMiner": CGMinerAvalon1066,
}, },
"T3H+": {
"Default": CGMinerInnosiliconT3HPlus,
"CGMiner": CGMinerInnosiliconT3HPlus,
},
"Unknown": {"Default": UnknownMiner}, "Unknown": {"Default": UnknownMiner},
} }
@@ -309,7 +306,10 @@ class MinerFactory(metaclass=Singleton):
if ip in self.miners: if ip in self.miners:
return self.miners[ip] return self.miners[ip]
# if everything fails, the miner is already set to unknown # if everything fails, the miner is already set to unknown
model, api, ver = None, None, None miner = UnknownMiner(str(ip))
api = None
model = None
ver = None
# try to get the API multiple times based on retries # try to get the API multiple times based on retries
for i in range(PyasicSettings().miner_factory_get_version_retries): for i in range(PyasicSettings().miner_factory_get_version_retries):
@@ -330,24 +330,6 @@ class MinerFactory(metaclass=Singleton):
break break
except asyncio.TimeoutError: except asyncio.TimeoutError:
logging.warning(f"{ip}: Get Miner Timed Out") 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 # make sure we have model information
if model: if model:
if not api: if not api:
@@ -383,6 +365,11 @@ class MinerFactory(metaclass=Singleton):
elif "BMMiner" in api: elif "BMMiner" in api:
miner = BMMiner(str(ip)) 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 return miner
def clear_cached_miners(self) -> None: def clear_cached_miners(self) -> None:
@@ -393,139 +380,13 @@ class MinerFactory(metaclass=Singleton):
async def _get_miner_type( async def _get_miner_type(
self, ip: Union[ipaddress.ip_address, str] self, ip: Union[ipaddress.ip_address, str]
) -> Tuple[Union[str, None], Union[str, None], Union[str, None]]: ) -> Tuple[Union[str, None], Union[str, None], Union[str, None]]:
model, api, ver = None, None, None data = None
try: model = None
devdetails, version = await self.__get_devdetails_and_version(ip) api = None
except APIError as e: ver = None
# 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
# if we have devdetails, we can get model data from there devdetails = None
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 version = None
try: try:
# get device details and version data # get device details and version data
@@ -537,83 +398,179 @@ class MinerFactory(metaclass=Singleton):
# copy each part of the main command to devdetails and version # copy each part of the main command to devdetails and version
devdetails = data["devdetails"][0] devdetails = data["devdetails"][0]
version = data["version"][0] version = data["version"][0]
return devdetails, version
except APIError: except APIError:
# 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]:
# 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]:
raise APIError(validation[1])
return devdetails, version
@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: try:
auth = ( # try devdetails and version separately (X19s mainly require this)
await client.post( # get devdetails and validate
f"http://{ip}/api/auth", devdetails = await self._send_api_command(str(ip), "devdetails")
data={"username": "admin", "password": "admin"}, validation = await self._validate_command(devdetails)
) if not validation[0]:
).json()["jwt"] # if devdetails fails try version instead
response = ( devdetails = None
await client.post(
f"http://{ip}/api/type", # get version and validate
headers={"Authorization": "Bearer " + auth}, version = await self._send_api_command(str(ip), "version")
data={}, validation = await self._validate_command(version)
) if not validation[0]:
).json() # finally try get_version (Whatsminers) and validate
except Exception as e: version = await self._send_api_command(str(ip), "get_version")
logging.info(e) validation = await self._validate_command(version)
if response:
return response["type"] # 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
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
@staticmethod @staticmethod
async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]: async def _validate_command(data: dict) -> Tuple[bool, Union[str, None]]:

View File

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

View File

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

View File

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

View File

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

Binary file not shown.