Compare commits

...

29 Commits

Author SHA1 Message Date
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
64 changed files with 649 additions and 428 deletions

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

@@ -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]:
@@ -23,6 +40,8 @@ Supported miner types are here on this list. If your miner (or miner version) i
* [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]:

View File

@@ -122,6 +122,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
## M32
::: 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

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

@@ -14,7 +14,9 @@
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
@@ -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
@@ -101,9 +104,22 @@ class MinerData:
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,52 @@ 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
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

@@ -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,7 +20,7 @@ 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
@@ -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.")

View File

@@ -18,7 +18,8 @@ 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
class BOSMinerOld(BaseMiner):
@@ -92,3 +93,6 @@ 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

View File

@@ -18,7 +18,7 @@ from typing import Union
from pyasic.API.btminer import BTMinerAPI
from pyasic.miners import BaseMiner
from pyasic.miners.base import BaseMiner
from pyasic.API import APIError
from pyasic.data import MinerData
@@ -165,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.miners.base import BaseMiner
from pyasic.API 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

@@ -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

@@ -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):

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

@@ -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

@@ -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

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

@@ -0,0 +1,142 @@
# 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

@@ -12,9 +12,9 @@
# 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 *
@@ -42,8 +42,6 @@ from pyasic.settings import PyasicSettings
import asyncssh
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
MINER_CLASSES = {
"ANTMINER S9": {
"Default": BOSMinerS9,
@@ -205,43 +203,43 @@ 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,
},
@@ -306,10 +304,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 +325,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 +378,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 +388,127 @@ 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 "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 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
if 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 +520,58 @@ 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
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()
@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
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
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 _validate_command(data: dict) -> Tuple[bool, Union[str, None]]:

View File

@@ -13,7 +13,8 @@
# 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
class UnknownMiner(BaseMiner):
@@ -57,3 +58,6 @@ 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

View File

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