Compare commits

...

8 Commits

Author SHA1 Message Date
UpstreamData
26ae6ebfb2 bump version number 2022-07-19 11:19:32 -06:00
UpstreamData
e65cb0573d update miner factory to handle some types of stock fw S9s 2022-07-19 11:18:55 -06:00
UpstreamData
f8590b0c5f improve more typing 2022-07-18 14:46:17 -06:00
UpstreamData
43b4992cee improve logging and some documentation 2022-07-18 14:38:54 -06:00
UpstreamData
98e2cfae84 bump version number 2022-07-18 12:05:44 -06:00
UpstreamData
cb01c1a8ee update network to scan fast even if some miners are not responding properly 2022-07-18 12:05:22 -06:00
UpstreamData
36a273ec2b bump version number 2022-07-18 11:45:14 -06:00
UpstreamData
6a0dc03b9d update to a better way to handle settings 2022-07-18 11:44:22 -06:00
25 changed files with 185 additions and 265 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ from pyasic.miners import BaseMiner
from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
class BMMiner(BaseMiner):
@@ -165,7 +165,7 @@ class BMMiner(BaseMiner):
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True
)

View File

@@ -15,7 +15,7 @@ from pyasic.data import MinerData
from pyasic.config import MinerConfig
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
class BOSMiner(BaseMiner):
@@ -97,7 +97,7 @@ class BOSMiner(BaseMiner):
return True
return False
async def get_config(self) -> str:
async def get_config(self) -> MinerConfig:
"""Gets the config for the miner and sets it as `self.config`.
Returns:
@@ -217,13 +217,14 @@ class BOSMiner(BaseMiner):
.as_bos(model=self.model.replace(" (BOS)", ""))
)
async with (await self._get_ssh_connection()) as conn:
await conn.run("/etc/init.d/bosminer stop")
logging.debug(f"{self}: Opening SFTP connection.")
async with conn.start_sftp_client() as sftp:
logging.debug(f"{self}: Opening config file.")
async with sftp.open("/etc/bosminer.toml", "w+") as file:
await file.write(toml_conf)
logging.debug(f"{self}: Restarting BOSMiner")
await conn.run("/etc/init.d/bosminer restart")
await conn.run("/etc/init.d/bosminer start")
async def get_data(self) -> MinerData:
"""Get data from the miner.
@@ -250,7 +251,7 @@ class BOSMiner(BaseMiner):
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
try:
miner_data = await self.api.multicommand(
"summary",

View File

@@ -10,7 +10,7 @@ from pyasic.API import APIError
from pyasic.data import MinerData
from pyasic.data.error_codes import WhatsminerError
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
class BTMiner(BaseMiner):
@@ -116,7 +116,7 @@ class BTMiner(BaseMiner):
data.hostname = hostname
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
try:
miner_data = await self.api.multicommand("summary", "devs", "pools")
if miner_data:

View File

@@ -9,7 +9,7 @@ from pyasic.API import APIError
from pyasic.data import MinerData
from pyasic.settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
from pyasic.settings import PyasicSettings
class CGMiner(BaseMiner):
@@ -162,7 +162,7 @@ class CGMiner(BaseMiner):
data.mac = mac
miner_data = None
for i in range(DATA_RETRIES):
for i in range(PyasicSettings().miner_get_data_retries):
miner_data = await self.api.multicommand(
"summary", "pools", "stats", ignore_x19_error=True
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
from typing import TypeVar, Tuple, List, Union
from collections.abc import AsyncIterable
from pyasic.miners import BaseMiner
import httpx
from pyasic.miners.antminer import *
from pyasic.miners.avalonminer import *
@@ -23,10 +24,7 @@ import ipaddress
import json
import logging
from pyasic.settings import (
MINER_FACTORY_GET_VERSION_RETRIES as GET_VERSION_RETRIES,
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
)
from pyasic.settings import PyasicSettings
import asyncssh
@@ -284,7 +282,7 @@ class MinerFactory(metaclass=Singleton):
ver = None
# try to get the API multiple times based on retries
for i in range(GET_VERSION_RETRIES):
for i in range(PyasicSettings().miner_factory_get_version_retries):
try:
# get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None
new_model, new_api, new_ver = await asyncio.wait_for(
@@ -421,7 +419,19 @@ class MinerFactory(metaclass=Singleton):
return model, api, None
except asyncssh.misc.PermissionDenied:
return None, None, None
try:
url = f"http://{self.ip}/cgi-bin/get_system_info.cgi"
auth = httpx.DigestAuth("root", "root")
async with httpx.AsyncClient() as client:
data = await client.get(url, auth=auth)
if data.status_code == 200:
data = data.json()
if "minertype" in data.keys():
model = data["minertype"]
if "bmminer" in "\t".join(data.keys()):
api = "BMMiner"
except:
return None, None, None
# if we have devdetails, we can get model data from there
if devdetails:

View File

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

View File

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

View File

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

View File

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

View File

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