Merge branch 'pyqt_gui_cfg_util'

This commit is contained in:
UpstreamData
2022-05-06 16:22:28 -06:00
30 changed files with 1750 additions and 204 deletions

View File

@@ -21,6 +21,7 @@ class BaseMiner:
self.api_type = None
self.model = None
self.light = None
self.hostname = None
self.nominal_chips = 1
async def _get_ssh_connection(self) -> asyncssh.connect:
@@ -45,15 +46,21 @@ class BaseMiner:
)
return conn
except Exception as e:
logging.warning(f"{self} raised an exception: {e}")
# logging.warning(f"{self} raised an exception: {e}")
raise e
except OSError:
logging.warning(f"Connection refused: {self}")
return None
except Exception as e:
logging.warning(f"{self} raised an exception: {e}")
# logging.warning(f"{self} raised an exception: {e}")
raise e
async def fault_light_on(self) -> bool:
return False
async def fault_light_off(self) -> bool:
return False
async def send_file(self, src, dest):
async with (await self._get_ssh_connection()) as conn:
await asyncssh.scp(src, (conn, dest))
@@ -74,10 +81,27 @@ class BaseMiner:
return None
async def reboot(self):
return None
return False
async def restart_backend(self):
return None
return False
async def send_config(self, yaml_config):
return None
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Split": 0,
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
return data

View File

@@ -1,6 +1,7 @@
from API.bmminer import BMMinerAPI
from miners import BaseMiner
import logging
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
class BMMiner(BaseMiner):
@@ -44,6 +45,8 @@ class BMMiner(BaseMiner):
:return: The hostname of the miner as a string or "?"
"""
if self.hostname:
return self.hostname
try:
# open an ssh connection
async with (await self._get_ssh_connection()) as conn:
@@ -55,7 +58,8 @@ class BMMiner(BaseMiner):
# return hostname data
logging.debug(f"Found hostname for {self.ip}: {host}")
return host
self.hostname = host
return self.hostname
else:
# return ? if we fail to get hostname with no ssh connection
logging.warning(f"Failed to get hostname for miner: {self}")
@@ -81,6 +85,8 @@ class BMMiner(BaseMiner):
try:
# run the command and get the result
result = await conn.run(cmd)
result = result.stdout
except Exception as e:
# if the command fails, log it
logging.warning(f"{self} command {cmd} error: {e}")
@@ -110,7 +116,116 @@ class BMMiner(BaseMiner):
pool_data.append({"url": pool["URL"], "user": pool["User"], "pwd": "123"})
return pool_data
async def reboot(self) -> None:
async def reboot(self) -> bool:
logging.debug(f"{self}: Sending reboot command.")
await self.send_ssh_command("reboot")
_ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.")
if isinstance(_ret, str):
return True
return False
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Split": 0,
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
model = await self.get_model()
hostname = await self.get_hostname()
if model:
data["Model"] = model
if hostname:
data["Hostname"] = hostname
miner_data = None
for i in range(DATA_RETRIES):
miner_data = await self.api.multicommand("summary", "pools", "stats")
if miner_data:
break
if not miner_data:
return data
summary = miner_data.get("summary")[0]
pools = miner_data.get("pools")[0]
stats = miner_data.get("stats")[0]
if summary:
hr = summary.get("SUMMARY")
if hr:
if len(hr) > 0:
hr = hr[0].get("GHS 5s")
if hr:
data["Hashrate"] = round(hr / 1000, 2)
if stats:
temp = stats.get("STATS")
if temp:
if len(temp) > 1:
for item in ["temp2", "temp1", "temp3"]:
temperature = temp[1].get(item)
if temperature and not temperature == 0.0:
data["Temperature"] = round(temperature)
if pools:
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
pool_1_quota = pool["Quota"]
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
if pool_1.startswith("stratum+tcp://"):
pool_1.replace("stratum+tcp://", "")
if pool_1.startswith("stratum2+tcp://"):
pool_1.replace("stratum2+tcp://", "")
data["Pool 1"] = pool_1
if pool_1_user:
data["Pool 1 User"] = pool_1_user
data["Pool User"] = pool_1_user
if pool_2:
if pool_2.startswith("stratum+tcp://"):
pool_2.replace("stratum+tcp://", "")
if pool_2.startswith("stratum2+tcp://"):
pool_2.replace("stratum2+tcp://", "")
data["Pool 2"] = pool_2
if pool_2_user:
data["Pool 2 User"] = pool_2_user
if quota:
data["Split"] = quota
return data

View File

@@ -3,6 +3,7 @@ from API.bosminer import BOSMinerAPI
import toml
from config.bos import bos_config_convert, general_config_convert_bos
import logging
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
class BOSMiner(BaseMiner):
@@ -33,6 +34,7 @@ class BOSMiner(BaseMiner):
try:
# run the command and get the result
result = await conn.run(cmd)
result = result.stdout
except Exception as e:
# if the command fails, log it
logging.warning(f"{self} command {cmd} error: {e}")
@@ -42,36 +44,48 @@ class BOSMiner(BaseMiner):
return
continue
# return the result, either command output or None
return result
return str(result)
async def fault_light_on(self) -> None:
async def fault_light_on(self) -> bool:
"""Sends command to turn on fault light on the miner."""
logging.debug(f"{self}: Sending fault_light on command.")
self.light = True
await self.send_ssh_command("miner fault_light on")
_ret = await self.send_ssh_command("miner fault_light on")
logging.debug(f"{self}: fault_light on command completed.")
if isinstance(_ret, str):
return True
return False
async def fault_light_off(self) -> None:
async def fault_light_off(self) -> bool:
"""Sends command to turn off fault light on the miner."""
logging.debug(f"{self}: Sending fault_light off command.")
self.light = False
await self.send_ssh_command("miner fault_light off")
_ret = await self.send_ssh_command("miner fault_light off")
logging.debug(f"{self}: fault_light off command completed.")
if isinstance(_ret, str):
return True
return False
async def restart_backend(self) -> None:
await self.restart_bosminer()
async def restart_backend(self) -> bool:
return await self.restart_bosminer()
async def restart_bosminer(self) -> None:
async def restart_bosminer(self) -> bool:
"""Restart bosminer hashing process."""
logging.debug(f"{self}: Sending bosminer restart command.")
await self.send_ssh_command("/etc/init.d/bosminer restart")
_ret = await self.send_ssh_command("/etc/init.d/bosminer restart")
logging.debug(f"{self}: bosminer restart command completed.")
if isinstance(_ret, str):
return True
return False
async def reboot(self) -> None:
async def reboot(self) -> bool:
"""Reboots power to the physical miner."""
logging.debug(f"{self}: Sending reboot command.")
await self.send_ssh_command("/sbin/reboot")
_ret = await self.send_ssh_command("/sbin/reboot")
logging.debug(f"{self}: Reboot command completed.")
if isinstance(_ret, str):
return True
return False
async def get_config(self) -> None:
logging.debug(f"{self}: Getting config.")
@@ -82,7 +96,7 @@ class BOSMiner(BaseMiner):
async with sftp.open("/etc/bosminer.toml") as file:
toml_data = toml.loads(await file.read())
logging.debug(f"{self}: Converting config file.")
cfg = await bos_config_convert(toml_data)
cfg = bos_config_convert(toml_data)
self.config = cfg
async def get_hostname(self) -> str:
@@ -90,13 +104,16 @@ class BOSMiner(BaseMiner):
:return: The hostname of the miner as a string or "?"
"""
if self.hostname:
return self.hostname
try:
async with (await self._get_ssh_connection()) as conn:
if conn is not None:
data = await conn.run("cat /proc/sys/kernel/hostname")
host = data.stdout.strip()
logging.debug(f"Found hostname for {self.ip}: {host}")
return host
self.hostname = host
return self.hostname
else:
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
@@ -160,10 +177,10 @@ class BOSMiner(BaseMiner):
if ip_user:
suffix = str(self.ip).split(".")[-1]
toml_conf = toml.dumps(
await general_config_convert_bos(yaml_config, user_suffix=suffix)
general_config_convert_bos(yaml_config, user_suffix=suffix)
)
else:
toml_conf = toml.dumps(await general_config_convert_bos(yaml_config))
toml_conf = toml.dumps(general_config_convert_bos(yaml_config))
async with (await self._get_ssh_connection()) as conn:
logging.debug(f"{self}: Opening SFTP connection.")
async with conn.start_sftp_client() as sftp:
@@ -222,3 +239,112 @@ class BOSMiner(BaseMiner):
bad += 1
if not bad > 0:
return str(self.ip)
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Split": 0,
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
model = await self.get_model()
hostname = await self.get_hostname()
if model:
data["Model"] = model
if hostname:
data["Hostname"] = hostname
miner_data = None
for i in range(DATA_RETRIES):
miner_data = await self.api.multicommand(
"summary", "temps", "tunerstatus", "pools"
)
if miner_data:
break
if not miner_data:
return data
summary = miner_data.get("summary")[0]
temps = miner_data.get("temps")[0]
tunerstatus = miner_data.get("tunerstatus")[0]
pools = miner_data.get("pools")[0]
if summary:
hr = summary.get("SUMMARY")
if hr:
if len(hr) > 0:
hr = hr[0].get("MHS 5s")
if hr:
data["Hashrate"] = round(hr / 1000000, 2)
if temps:
temp = temps.get("TEMPS")
if temp:
if len(temp) > 0:
temp = temp[0].get("Chip")
if temp:
data["Temperature"] = round(temp)
if pools:
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
pool_1_quota = pool["Quota"]
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
pool_1 = pool_1.replace("stratum+tcp://", "")
pool_1 = pool_1.replace("stratum2+tcp://", "")
data["Pool 1"] = pool_1
if pool_1_user:
data["Pool 1 User"] = pool_1_user
data["Pool User"] = pool_1_user
if pool_2:
pool_2 = pool_2.replace("stratum+tcp://", "")
pool_2 = pool_2.replace("stratum2+tcp://", "")
data["Pool 2"] = pool_2
if pool_2_user:
data["Pool 2 User"] = pool_2_user
if quota:
data["Split"] = quota
if tunerstatus:
tuner = tunerstatus.get("TUNERSTATUS")
if tuner:
if len(tuner) > 0:
wattage = tuner[0].get("PowerLimit")
if wattage:
data["Wattage"] = wattage
return data

View File

@@ -2,6 +2,7 @@ from API.btminer import BTMinerAPI
from miners import BaseMiner
from API import APIError
import logging
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
class BTMiner(BaseMiner):
@@ -27,12 +28,15 @@ class BTMiner(BaseMiner):
return None
async def get_hostname(self) -> str:
if self.hostname:
return self.hostname
try:
host_data = await self.api.get_miner_info()
if host_data:
host = host_data["Msg"]["hostname"]
logging.debug(f"Found hostname for {self.ip}: {host}")
return host
self.hostname = host
return self.hostname
except APIError:
logging.warning(f"Failed to get hostname for miner: {self}")
return "?"
@@ -70,3 +74,112 @@ class BTMiner(BaseMiner):
print(board)
logging.debug(f"Found board data for {self}: {boards}")
return boards
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Split": 0,
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
model = await self.get_model()
hostname = await self.get_hostname()
if model:
data["Model"] = model
if hostname:
data["Hostname"] = hostname
miner_data = None
for i in range(DATA_RETRIES):
miner_data = await self.api.multicommand("summary", "devs", "pools")
if miner_data:
break
if not miner_data:
return data
summary = miner_data.get("summary")[0]
devs = miner_data.get("devs")[0]
pools = miner_data.get("pools")[0]
if summary:
summary_data = summary.get("SUMMARY")
if summary_data:
if len(summary_data) > 0:
hr = summary_data[0].get("MHS 5s")
if hr:
data["Hashrate"] = round(hr / 1000000, 2)
wattage = summary_data[0].get("Power")
if wattage:
data["Wattage"] = round(wattage)
if devs:
temp_data = devs.get("DEVS")
if temp_data:
for board in temp_data:
temp = board.get("Chip Temp Avg")
if temp and not temp == 0.0:
data["Temperature"] = round(temp)
break
if pools:
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
pool_1_quota = pool["Quota"]
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
if pool_1.startswith("stratum+tcp://"):
pool_1.replace("stratum+tcp://", "")
if pool_1.startswith("stratum2+tcp://"):
pool_1.replace("stratum2+tcp://", "")
data["Pool 1"] = pool_1
if pool_1_user:
data["Pool 1 User"] = pool_1_user
data["Pool User"] = pool_1_user
if pool_2:
if pool_2.startswith("stratum+tcp://"):
pool_2.replace("stratum+tcp://", "")
if pool_2.startswith("stratum2+tcp://"):
pool_2.replace("stratum2+tcp://", "")
data["Pool 2"] = pool_2
if pool_2_user:
data["Pool 2 User"] = pool_2_user
if quota:
data["Split"] = quota
return data

View File

@@ -1,6 +1,8 @@
from miners import BaseMiner
from API.cgminer import CGMinerAPI
from API import APIError
from settings import MINER_FACTORY_GET_VERSION_RETRIES as DATA_RETRIES
import logging
class CGMiner(BaseMiner):
@@ -28,11 +30,15 @@ class CGMiner(BaseMiner):
return None
async def get_hostname(self) -> str:
if self.hostname:
return self.hostname
try:
async with (await self._get_ssh_connection()) as conn:
if conn is not None:
data = await conn.run("cat /proc/sys/kernel/hostname")
return data.stdout.strip()
host = data.stdout.strip()
self.hostname = host
return self.hostname
else:
return "?"
except Exception:
@@ -44,6 +50,7 @@ class CGMiner(BaseMiner):
for i in range(3):
try:
result = await conn.run(cmd)
result = result.stdout
except Exception as e:
print(f"{cmd} error: {e}")
if i == 3:
@@ -51,16 +58,24 @@ class CGMiner(BaseMiner):
continue
return result
async def restart_backend(self) -> None:
await self.restart_cgminer()
async def restart_backend(self) -> bool:
return await self.restart_cgminer()
async def restart_cgminer(self) -> None:
async def restart_cgminer(self) -> bool:
commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"]
commands = ";".join(commands)
await self.send_ssh_command(commands)
_ret = await self.send_ssh_command(commands)
if isinstance(_ret, str):
return True
return False
async def reboot(self) -> None:
await self.send_ssh_command("reboot")
async def reboot(self) -> bool:
logging.debug(f"{self}: Sending reboot command.")
_ret = await self.send_ssh_command("reboot")
logging.debug(f"{self}: Reboot command completed.")
if isinstance(_ret, str):
return True
return False
async def start_cgminer(self) -> None:
commands = [
@@ -88,3 +103,108 @@ class CGMiner(BaseMiner):
result = await conn.run(command, check=True)
self.config = result.stdout
print(str(self.config))
async def get_data(self):
data = {
"IP": str(self.ip),
"Model": "Unknown",
"Hostname": "Unknown",
"Hashrate": 0,
"Temperature": 0,
"Pool User": "Unknown",
"Wattage": 0,
"Split": 0,
"Pool 1": "Unknown",
"Pool 1 User": "Unknown",
"Pool 2": "",
"Pool 2 User": "",
}
model = await self.get_model()
hostname = await self.get_hostname()
if model:
data["Model"] = model
if hostname:
data["Hostname"] = hostname
miner_data = None
for i in range(DATA_RETRIES):
miner_data = await self.api.multicommand("summary", "pools", "stats")
if miner_data:
break
if not miner_data:
return data
summary = miner_data.get("summary")[0]
pools = miner_data.get("pools")[0]
stats = miner_data.get("stats")[0]
if summary:
hr = summary.get("SUMMARY")
if hr:
if len(hr) > 0:
hr = hr[0].get("GHS 5s")
if hr:
data["Hashrate"] = round(hr / 1000, 2)
if stats:
temp = stats.get("STATS")
if temp:
if len(temp) > 1:
for item in ["temp2", "temp1", "temp3"]:
temperature = temp[1].get(item)
if temperature and not temperature == 0.0:
data["Temperature"] = round(temperature)
if pools:
pool_1 = None
pool_2 = None
pool_1_user = None
pool_2_user = None
pool_1_quota = 1
pool_2_quota = 1
quota = 0
for pool in pools.get("POOLS"):
if not pool_1_user:
pool_1_user = pool.get("User")
pool_1 = pool["URL"]
pool_1_quota = pool["Quota"]
elif not pool_2_user:
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if not pool.get("User") == pool_1_user:
if not pool_2_user == pool.get("User"):
pool_2_user = pool.get("User")
pool_2 = pool["URL"]
pool_2_quota = pool["Quota"]
if pool_2_user and not pool_2_user == pool_1_user:
quota = f"{pool_1_quota}/{pool_2_quota}"
if pool_1:
if pool_1.startswith("stratum+tcp://"):
pool_1.replace("stratum+tcp://", "")
if pool_1.startswith("stratum2+tcp://"):
pool_1.replace("stratum2+tcp://", "")
data["Pool 1"] = pool_1
if pool_1_user:
data["Pool 1 User"] = pool_1_user
data["Pool User"] = pool_1_user
if pool_2:
if pool_2.startswith("stratum+tcp://"):
pool_2.replace("stratum+tcp://", "")
if pool_2.startswith("stratum2+tcp://"):
pool_2.replace("stratum2+tcp://", "")
data["Pool 2"] = pool_2
if pool_2_user:
data["Pool 2 User"] = pool_2_user
if quota:
data["Split"] = quota
return data

View File

@@ -36,20 +36,25 @@ import ipaddress
import json
import logging
from settings import MINER_FACTORY_GET_VERSION_RETRIES as GET_VERSION_RETRIES
from settings import (
MINER_FACTORY_GET_VERSION_RETRIES as GET_VERSION_RETRIES,
NETWORK_PING_TIMEOUT as PING_TIMEOUT,
)
class MinerFactory:
_instance = None
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class MinerFactory(metaclass=Singleton):
def __init__(self):
self.miners = {}
def __new__(cls):
if not cls._instance:
cls._instance = super(MinerFactory, cls).__new__(cls)
return cls._instance
async def get_miner_generator(self, ips: list):
"""
Get Miner objects from ip addresses using an async generator.
@@ -72,8 +77,10 @@ class MinerFactory:
for miner in scanned:
yield await miner
async def get_miner(self, ip: ipaddress.ip_address):
async def get_miner(self, ip: ipaddress.ip_address or str):
"""Decide a miner type using the IP address of the miner."""
if isinstance(ip, str):
ip = ipaddress.ip_address(ip)
# check if the miner already exists in cache
if ip in self.miners:
return self.miners[ip]
@@ -84,31 +91,35 @@ class MinerFactory:
# try to get the API multiple times based on retries
for i in range(GET_VERSION_RETRIES):
# get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None
api = await self._get_api_type(ip)
# if we find the API type, dont need to loop anymore
if api:
break
try:
# get the API type, should be BOSMiner, CGMiner, BMMiner, BTMiner, or None
new_model, new_api = await asyncio.wait_for(
self._get_miner_type(ip), timeout=PING_TIMEOUT
)
# keep track of the API and model we found first
if new_api and not api:
api = new_api
if new_model and not model:
model = new_model
# if we find the API and model, dont need to loop anymore
if api and model:
break
except asyncio.TimeoutError:
pass
# try to get the model multiple times based on retries
for i in range(GET_VERSION_RETRIES):
# get the model, should return some miner model type, e.g. Antminer S9
model = await self._get_miner_model(ip)
# if we find the model type, dont need to loop anymore
if model:
break
# make sure we have model information
if model:
# check if the miner is an Antminer
if "Antminer" in model:
# S9 logic
if "Antminer S9" in model:
# handle the different API types
if not api:
logging.warning(f"{str(ip)}: No API data found, using BraiinsOS.")
logging.warning(
f"{str(ip)}: No API data found, using BraiinsOS."
)
miner = BOSMinerS9(str(ip))
elif "BOSMiner" in api:
miner = BOSMinerS9(str(ip))
@@ -129,7 +140,6 @@ class MinerFactory:
# X17 model logic
elif "17" in model:
# handle the different API types
if "BOSMiner" in api:
miner = BOSMinerX17(str(ip))
@@ -190,65 +200,102 @@ class MinerFactory:
# empty out self.miners
self.miners = {}
async def _get_miner_model(self, ip: ipaddress.ip_address or str) -> str or None:
# instantiate model as being nothing if getting it fails
async def _get_miner_type(self, ip: ipaddress.ip_address or str) -> tuple:
model = None
api = None
devdetails = None
version = None
# try block in case of APIError or OSError 121 (Semaphore timeout)
try:
data = await self._send_api_command(str(ip), "devdetails+version")
# send the devdetails command to the miner (will fail with no boards/devices)
data = await self._send_api_command(str(ip), "devdetails")
# sometimes data is b'', check for that
if data:
# status check, make sure the command succeeded
if data.get("STATUS"):
if not isinstance(data["STATUS"], str):
# if status is E, its an error
if data["STATUS"][0].get("STATUS") not in ["I", "S"]:
validation = await self._validate_command(data)
if not validation[0]:
raise APIError(validation[1])
# try an alternate method if devdetails fails
data = await self._send_api_command(str(ip), "version")
devdetails = data["devdetails"][0]
version = data["version"][0]
# make sure we have data
if data:
# check the keys are there to get the version
if data.get("VERSION"):
if data["VERSION"][0].get("Type"):
# save the model to be returned later
model = data["VERSION"][0]["Type"]
else:
# make sure devdetails actually contains data, if its empty, there are no devices
if (
"DEVDETAILS" in data.keys()
and not data["DEVDETAILS"] == []
):
# check for model, for most miners
if not data["DEVDETAILS"][0]["Model"] == "":
# model of most miners
model = data["DEVDETAILS"][0]["Model"]
# if model fails, try driver
else:
# some avalonminers have model in driver
model = data["DEVDETAILS"][0]["Driver"]
else:
# if all that fails, try just version
data = await self._send_api_command(str(ip), "version")
if "VERSION" in data.keys():
model = data["VERSION"][0]["Type"]
else:
print(data)
return model
# if there are errors, we just return None
except APIError as e:
logging.debug(f"{str(ip)}: {e}")
except OSError as e:
logging.debug(f"{str(ip)}: {e}")
return model
data = None
if not data:
try:
devdetails = await self._send_api_command(str(ip), "devdetails")
validation = await self._validate_command(devdetails)
if not validation[0]:
version = await self._send_api_command(str(ip), "version")
validation = await self._validate_command(version)
if not validation[0]:
raise APIError(validation[1])
except APIError as e:
logging.warning(f"{ip}: API Command Error: {e}")
return None, None
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"]
# if model fails, try driver
else:
# some avalonminers have model in driver
model = devdetails["DEVDETAILS"][0]["Driver"]
if version:
# 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"
# 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 all that fails, check the Description to see if it is a whatsminer
elif version.get("Description") and "whatsminer" in version.get("Description"):
api = "BTMiner"
if version and not model:
if (
"VERSION" in version.keys()
and version.get("VERSION")
and not version.get("VERSION") == []
):
model = version["VERSION"][0]["Type"]
return model, api
async def _validate_command(self, data: dict) -> tuple:
"""Check if the returned command output is correctly formatted."""
# check if the data returned is correct or an error
if not data:
return False, "No API data."
# if status isn't a key, it is a multicommand
if "STATUS" not in data.keys():
for key in data.keys():
# make sure not to try to turn id into a dict
if not key == "id":
# make sure they succeeded
if "STATUS" in data[key][0].keys():
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
# this is an error
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
elif "id" not in data.keys():
if data["STATUS"] not in ["S", "I"]:
return False, data["Msg"]
else:
# make sure the command succeeded
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
# this is an error
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
return False, data["STATUS"][0]["Msg"]
return True, None
async def _send_api_command(self, ip: ipaddress.ip_address or str, command: str):
try:
@@ -304,55 +351,3 @@ class MinerFactory:
await writer.wait_closed()
return data
async def _get_api_type(self, ip: ipaddress.ip_address or str) -> dict or None:
"""Get data on the version of the miner to return the right miner."""
# instantiate API as None in case something fails
api = None
# try block to handle OSError 121 (Semaphore timeout)
try:
# try the version command,works on most miners
data = await self._send_api_command(str(ip), "version")
# if we got data back, try to parse it
if data:
# make sure the command succeeded
if data.get("STATUS") and not data.get("STATUS") == "E":
if data["STATUS"][0].get("STATUS") in ["I", "S"]:
# check if there are any BMMiner strings in any of the dict keys
if any(
"BMMiner" in string for string in data["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 data["VERSION"][0].keys()
):
api = "CGMiner"
# check if there are any BOSMiner strings in any of the dict keys
elif any(
"BOSminer" in string for string in data["VERSION"][0].keys()
):
api = "BOSMiner"
# if all that fails, check the Description to see if it is a whatsminer
elif data.get("Description") and "whatsminer" in data.get(
"Description"
):
api = "BTMiner"
# return the API if we found it
if api:
return api
# if there are errors, return None
except OSError as e:
if e.winerror == 121:
return None
else:
logging.debug(f"{str(ip)}: {e}")
return None