Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26ae6ebfb2 | ||
|
|
e65cb0573d | ||
|
|
f8590b0c5f | ||
|
|
43b4992cee | ||
|
|
98e2cfae84 | ||
|
|
cb01c1a8ee | ||
|
|
36a273ec2b | ||
|
|
6a0dc03b9d |
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user