Merge branch 'pyqt_gui_cfg_util'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user