Files
pyasic/pyasic/miners/backends/bosminer.py

1102 lines
39 KiB
Python

# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import logging
from collections import namedtuple
from typing import List, Optional, Tuple, Union
import toml
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.web.bosminer import BOSMinerWebAPI
BOSMINER_DATA_LOC = {
"mac": {
"cmd": "get_mac",
"kwargs": {
"web_net_conf": {"web": "/cgi-bin/luci/admin/network/iface_status/lan"}
},
},
"model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {
"cmd": "get_api_ver",
"kwargs": {"api_version": {"api": "version"}},
},
"fw_ver": {
"cmd": "get_fw_ver",
"kwargs": {
"graphql_version": {"web": {"bos": {"info": {"version": {"full": None}}}}}
},
},
"hostname": {
"cmd": "get_hostname",
"kwargs": {"graphql_hostname": {"web": {"bos": {"hostname": None}}}},
},
"hashrate": {
"cmd": "get_hashrate",
"kwargs": {
"api_summary": {"api": "summary"},
"graphql_hashrate": {
"web": {
"bosminer": {
"info": {"workSolver": {"realHashrate": {"mhs1M": None}}}
}
},
},
},
},
"nominal_hashrate": {
"cmd": "get_nominal_hashrate",
"kwargs": {"api_devs": {"api": "devs"}},
},
"hashboards": {
"cmd": "get_hashboards",
"kwargs": {
"api_temps": {"api": "temps"},
"api_devdetails": {"api": "devdetails"},
"api_devs": {"api": "devs"},
"graphql_boards": {
"web": {
"bosminer": {
"info": {
"workSolver": {
"childSolvers": {
"name": None,
"realHashrate": {"mhs1M": None},
"hwDetails": {"chips": None},
"temperatures": {"degreesC": None},
}
}
}
}
},
},
},
},
"wattage": {
"cmd": "get_wattage",
"kwargs": {
"api_tunerstatus": {"api": "tunerstatus"},
"graphql_wattage": {
"web": {
"bosminer": {
"info": {"workSolver": {"power": {"approxConsumptionW": None}}}
}
}
},
},
},
"wattage_limit": {
"cmd": "get_wattage_limit",
"kwargs": {
"api_tunerstatus": {"api": "tunerstatus"},
"graphql_wattage_limit": {
"web": {
"bosminer": {"info": {"workSolver": {"power": {"limitW": None}}}}
}
},
},
},
"fans": {
"cmd": "get_fans",
"kwargs": {
"api_fans": {"api": "fans"},
"graphql_fans": {
"web": {"bosminer": {"info": {"fans": {"name": None, "rpm": None}}}}
},
},
},
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
"env_temp": {"cmd": "get_env_temp", "kwargs": {}},
"errors": {
"cmd": "get_errors",
"kwargs": {
"api_tunerstatus": {"api": "tunerstatus"},
"graphql_errors": {
"web": {
"bosminer": {
"info": {
"workSolver": {
"childSolvers": {
"name": None,
"tuner": {"statusMessages": None},
}
}
}
}
}
},
},
},
"fault_light": {
"cmd": "get_fault_light",
"kwargs": {"graphql_fault_light": {"web": {"bos": {"faultLight": None}}}},
},
"pools": {
"cmd": "get_pools",
"kwargs": {
"api_pools": {"api": "pools"},
"graphql_pools": {
"web": {
"bosminer": {
"config": {
"... on BosminerConfig": {
"groups": {
"pools": {"url": None, "user": None},
"strategy": {
"... on QuotaStrategy": {"quota": None}
},
}
}
}
}
}
},
},
},
"is_mining": {
"cmd": "is_mining",
"kwargs": {"api_devdetails": {"api": "devdetails"}},
},
"uptime": {
"cmd": "get_uptime",
"kwargs": {"api_summary": {"api": "summary"}},
},
}
class BOSMiner(BaseMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip)
# interfaces
self.api = BOSMinerAPI(ip, api_ver)
self.web = BOSMinerWebAPI(ip)
# static data
self.api_type = "BOSMiner"
# data gathering locations
self.data_locations = BOSMINER_DATA_LOC
# autotuning/shutdown support
self.supports_autotuning = True
self.supports_shutdown = True
# data storage
self.api_ver = api_ver
async def send_ssh_command(self, cmd: str) -> Optional[str]:
result = None
try:
conn = await asyncio.wait_for(self._get_ssh_connection(), timeout=10)
except (ConnectionError, asyncio.TimeoutError):
return None
# open an ssh connection
async with conn:
# 3 retries
for i in range(3):
try:
# run the command and get the result
result = await conn.run(cmd)
stderr = result.stderr
result = result.stdout
if len(stderr) > len(result):
result = stderr
except Exception as e:
# if the command fails, log it
logging.warning(f"{self} command {cmd} error: {e}")
# on the 3rd retry, return None
if i == 3:
return
continue
# return the result, either command output or None
return result
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.")
ret = await self.send_ssh_command("miner fault_light on")
logging.debug(f"{self}: fault_light on command completed.")
if isinstance(ret, str):
self.light = True
return self.light
return False
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
ret = await self.send_ssh_command("miner fault_light off")
logging.debug(f"{self}: fault_light off command completed.")
if isinstance(ret, str):
self.light = False
return True
return False
async def restart_backend(self) -> bool:
"""Restart bosminer hashing process. Wraps [`restart_bosminer`][pyasic.miners.backends.bosminer.BOSMiner.restart_bosminer] to standardize."""
return await self.restart_bosminer()
async def restart_bosminer(self) -> bool:
"""Restart bosminer hashing process."""
logging.debug(f"{self}: Sending bosminer restart command.")
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 stop_mining(self) -> bool:
try:
data = await self.api.pause()
except APIError:
return False
if data.get("PAUSE"):
if data["PAUSE"][0]:
return True
return False
async def resume_mining(self) -> bool:
try:
data = await self.api.resume()
except APIError:
return False
if data.get("RESUME"):
if data["RESUME"][0]:
return True
return False
async def reboot(self) -> bool:
"""Reboots power to the physical miner."""
logging.debug(f"{self}: Sending reboot command.")
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) -> MinerConfig:
"""Gets the config for the miner and sets it as `self.config`.
Returns:
The config from `self.config`.
"""
logging.debug(f"{self}: Getting config.")
try:
conn = await self._get_ssh_connection()
except ConnectionError:
conn = None
if conn:
async with conn:
# good ol' BBB compatibility :/
toml_data = toml.loads(
(await conn.run("cat /etc/bosminer.toml")).stdout
)
logging.debug(f"{self}: Converting config file.")
cfg = MinerConfig().from_raw(toml_data)
self.config = cfg
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
logging.debug(f"{self}: Sending config.")
self.config = config
toml_conf = config.as_bos(
model=self.model.replace(" (BOS)", ""), user_suffix=user_suffix
)
try:
conn = await self._get_ssh_connection()
except ConnectionError:
return None
async with conn:
# BBB check because bitmain suxx
bbb_check = await conn.run(
"if [ ! -f /etc/init.d/bosminer ]; then echo '1'; else echo '0'; fi;"
)
bbb = bbb_check.stdout.strip() == "1"
if not bbb:
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 start")
# I really hate BBB, please get rid of it if you have it
else:
await conn.run("/etc/init.d/S99bosminer stop")
logging.debug(f"{self}: BBB sending config")
await conn.run("echo '" + toml_conf + "' > /etc/bosminer.toml")
logging.debug(f"{self}: BBB restarting bosminer.")
await conn.run("/etc/init.d/S99bosminer start")
async def set_power_limit(self, wattage: int) -> bool:
try:
cfg = await self.get_config()
if cfg is None:
return False
cfg.autotuning_wattage = wattage
await self.send_config(cfg)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
return False
else:
return True
async def set_static_ip(
self,
ip: str,
dns: str,
gateway: str,
subnet_mask: str = "255.255.255.0",
):
cfg_data_lan = (
"config interface 'lan'\n\toption type 'bridge'\n\toption ifname 'eth0'\n\toption proto 'static'\n\toption ipaddr '"
+ ip
+ "'\n\toption netmask '"
+ subnet_mask
+ "'\n\toption gateway '"
+ gateway
+ "'\n\toption dns '"
+ dns
+ "'"
)
data = await self.send_ssh_command("cat /etc/config/network")
split_data = data.split("\n\n")
for idx in range(len(split_data)):
if "config interface 'lan'" in split_data[idx]:
split_data[idx] = cfg_data_lan
config = "\n\n".join(split_data)
conn = await self._get_ssh_connection()
async with conn:
await conn.run("echo '" + config + "' > /etc/config/network")
async def set_dhcp(self):
cfg_data_lan = "config interface 'lan'\n\toption type 'bridge'\n\toption ifname 'eth0'\n\toption proto 'dhcp'"
data = await self.send_ssh_command("cat /etc/config/network")
split_data = data.split("\n\n")
for idx in range(len(split_data)):
if "config interface 'lan'" in split_data[idx]:
split_data[idx] = cfg_data_lan
config = "\n\n".join(split_data)
conn = await self._get_ssh_connection()
async with conn:
await conn.run("echo '" + config + "' > /etc/config/network")
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(self, web_net_conf: Union[dict, list] = None) -> Optional[str]:
if not web_net_conf:
try:
web_net_conf = await self.web.send_command(
"/cgi-bin/luci/admin/network/iface_status/lan"
)
except APIError:
pass
if isinstance(web_net_conf, dict):
if "/cgi-bin/luci/admin/network/iface_status/lan" in web_net_conf.keys():
web_net_conf = web_net_conf[
"/cgi-bin/luci/admin/network/iface_status/lan"
]
if web_net_conf:
try:
return web_net_conf[0]["macaddr"]
except LookupError:
pass
# could use ssh, but its slow and buggy
# result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
# if result:
# return result.upper().strip()
async def get_model(self) -> Optional[str]:
if self.model is not None:
return self.model + " (BOS)"
return "? (BOS)"
async def get_version(
self, api_version: dict = None, graphql_version: dict = None
) -> Tuple[Optional[str], Optional[str]]:
# check if version is cached
miner_version = namedtuple("MinerVersion", "api_ver fw_ver")
api_ver = await self.get_api_ver(api_version)
fw_ver = await self.get_fw_ver(graphql_version)
return miner_version(api_ver, fw_ver)
async def get_api_ver(self, api_version: dict = None) -> Optional[str]:
if not api_version:
try:
api_version = await self.api.version()
except APIError:
pass
# Now get the API version
if api_version:
try:
api_ver = api_version["VERSION"][0]["API"]
except (KeyError, IndexError):
api_ver = None
self.api_ver = api_ver
self.api.api_ver = self.api_ver
return self.api_ver
async def get_fw_ver(self, graphql_version: dict = None) -> Optional[str]:
if not graphql_version:
try:
graphql_version = await self.web.send_command(
{"bos": {"info": {"version": {"full"}}}}
)
except APIError:
pass
fw_ver = None
if graphql_version:
try:
fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"]
except (KeyError, TypeError):
pass
if not fw_ver:
# try version data file
fw_ver = await self.send_ssh_command("cat /etc/bos_version")
# if we get the version data, parse it
if fw_ver is not None:
ver = fw_ver.split("-")[5]
if "." in ver:
self.fw_ver = ver
logging.debug(f"Found version for {self.ip}: {self.fw_ver}")
return self.fw_ver
async def get_hostname(self, graphql_hostname: dict = None) -> Union[str, None]:
hostname = None
if not graphql_hostname:
try:
graphql_hostname = await self.web.send_command({"bos": {"hostname"}})
except APIError:
pass
if graphql_hostname:
try:
hostname = graphql_hostname["data"]["bos"]["hostname"]
return hostname
except (TypeError, KeyError):
pass
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}")
hostname = host
else:
logging.warning(f"Failed to get hostname for miner: {self}")
except Exception as e:
logging.warning(f"Failed to get hostname for miner: {self}, {e}")
return hostname
async def get_hashrate(
self, api_summary: dict = None, graphql_hashrate: dict = None
) -> Optional[float]:
# get hr from graphql
if not graphql_hashrate:
try:
graphql_hashrate = await self.web.send_command(
{"bosminer": {"info": {"workSolver": {"realHashrate": {"mhs1M"}}}}}
)
except APIError:
pass
if graphql_hashrate:
try:
return round(
float(
graphql_hashrate["data"]["bosminer"]["info"]["workSolver"][
"realHashrate"
]["mhs1M"]
/ 1000000
),
2,
)
except (LookupError, ValueError, TypeError):
pass
# get hr from API
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return round(float(api_summary["SUMMARY"][0]["MHS 1m"] / 1000000), 2)
except (KeyError, IndexError, ValueError, TypeError):
pass
async def get_hashboards(
self,
api_temps: dict = None,
api_devdetails: dict = None,
api_devs: dict = None,
graphql_boards: dict = None,
):
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
]
if not graphql_boards and not (api_devs or api_temps or api_devdetails):
try:
graphql_boards = await self.web.send_command(
{
"bosminer": {
"info": {
"workSolver": {
"childSolvers": {
"name": None,
"realHashrate": {"mhs1M"},
"hwDetails": {"chips"},
"temperatures": {"degreesC"},
}
}
}
}
},
)
except APIError:
pass
if graphql_boards:
try:
boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][
"childSolvers"
]
except (TypeError, LookupError):
boards = None
if boards:
b_names = [int(b["name"]) for b in boards]
offset = 0
if 3 in b_names:
offset = 1
elif 6 in b_names:
offset = 6
for hb in boards:
_id = int(hb["name"]) - offset
board = hashboards[_id]
board.hashrate = round(hb["realHashrate"]["mhs1M"] / 1000000, 2)
temps = hb["temperatures"]
try:
if len(temps) > 0:
board.temp = round(hb["temperatures"][0]["degreesC"])
if len(temps) > 1:
board.chip_temp = round(hb["temperatures"][1]["degreesC"])
except (TypeError, KeyError, ValueError, IndexError):
pass
details = hb.get("hwDetails")
if details:
if chips := details["chips"]:
board.chips = chips
board.missing = False
return hashboards
cmds = []
if not api_temps:
cmds.append("temps")
if not api_devdetails:
cmds.append("devdetails")
if not api_devs:
cmds.append("devs")
if len(cmds) > 0:
try:
d = await self.api.multicommand(*cmds)
except APIError:
d = {}
try:
api_temps = d["temps"][0]
except (KeyError, IndexError):
api_temps = None
try:
api_devdetails = d["devdetails"][0]
except (KeyError, IndexError):
api_devdetails = None
try:
api_devs = d["devs"][0]
except (KeyError, IndexError):
api_devs = None
if api_temps:
try:
offset = 6 if api_temps["TEMPS"][0]["ID"] in [6, 7, 8] else 1
for board in api_temps["TEMPS"]:
_id = board["ID"] - offset
chip_temp = round(board["Chip"])
board_temp = round(board["Board"])
hashboards[_id].chip_temp = chip_temp
hashboards[_id].temp = board_temp
except (IndexError, KeyError, ValueError, TypeError):
pass
if api_devdetails:
try:
offset = 6 if api_devdetails["DEVDETAILS"][0]["ID"] in [6, 7, 8] else 1
for board in api_devdetails["DEVDETAILS"]:
_id = board["ID"] - offset
chips = board["Chips"]
hashboards[_id].chips = chips
hashboards[_id].missing = False
except (IndexError, KeyError):
pass
if api_devs:
try:
offset = 6 if api_devs["DEVS"][0]["ID"] in [6, 7, 8] else 1
for board in api_devs["DEVS"]:
_id = board["ID"] - offset
hashrate = round(float(board["MHS 1m"] / 1000000), 2)
hashboards[_id].hashrate = hashrate
except (IndexError, KeyError):
pass
return hashboards
async def get_env_temp(self) -> Optional[float]:
return None
async def get_wattage(
self, api_tunerstatus: dict = None, graphql_wattage: dict = None
) -> Optional[int]:
if not graphql_wattage and not api_tunerstatus:
try:
graphql_wattage = await self.web.send_command(
{
"bosminer": {
"info": {"workSolver": {"power": {"approxConsumptionW"}}}
}
}
)
except APIError:
pass
if graphql_wattage is not None:
try:
return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][
"power"
]["approxConsumptionW"]
except (LookupError, TypeError):
pass
if not api_tunerstatus:
try:
api_tunerstatus = await self.api.tunerstatus()
except APIError:
pass
if api_tunerstatus:
try:
return api_tunerstatus["TUNERSTATUS"][0][
"ApproximateMinerPowerConsumption"
]
except (KeyError, IndexError):
pass
async def get_wattage_limit(
self, api_tunerstatus: dict = None, graphql_wattage_limit: dict = None
) -> Optional[int]:
if not graphql_wattage_limit and not api_tunerstatus:
try:
graphql_wattage_limit = await self.web.send_command(
{"bosminer": {"info": {"workSolver": {"power": {"limitW"}}}}}
)
except APIError:
pass
if graphql_wattage_limit:
try:
return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][
"power"
]["limitW"]
except (LookupError, TypeError):
pass
if not api_tunerstatus:
try:
api_tunerstatus = await self.api.tunerstatus()
except APIError:
pass
if api_tunerstatus:
try:
return api_tunerstatus["TUNERSTATUS"][0]["PowerLimit"]
except (KeyError, IndexError):
pass
async def get_fans(
self, api_fans: dict = None, graphql_fans: dict = None
) -> List[Fan]:
if not graphql_fans and not api_fans:
try:
graphql_fans = await self.web.send_command(
{"bosminer": {"info": {"fans": {"name", "rpm"}}}}
)
except APIError:
pass
if graphql_fans:
fans = []
for n in range(self.fan_count):
try:
fans.append(
Fan(
speed=graphql_fans["data"]["bosminer"]["info"]["fans"][n][
"rpm"
]
)
)
except (LookupError, TypeError):
pass
return fans
if not api_fans:
try:
api_fans = await self.api.fans()
except APIError:
pass
if api_fans:
fans = []
for n in range(self.fan_count):
try:
fans.append(Fan(api_fans["FANS"][n]["RPM"]))
except (IndexError, KeyError):
pass
return fans
return [Fan() for _ in range(self.fan_count)]
async def get_fan_psu(self) -> Optional[int]:
return None
async def get_pools(
self, api_pools: dict = None, graphql_pools: dict = None
) -> List[dict]:
if not graphql_pools and not api_pools:
try:
graphql_pools = await self.web.send_command(
{
"bosminer": {
"config": {
"... on BosminerConfig": {
"groups": {
"pools": {"urluser"},
"strategy": {"... on QuotaStrategy": {"quota"}},
}
}
}
}
}
)
except APIError:
pass
if graphql_pools:
groups = []
try:
g = graphql_pools["data"]["bosminer"]["config"]["groups"]
for group in g:
pools = {"quota": group["strategy"]["quota"]}
for i, pool in enumerate(group["pools"]):
pools[f"pool_{i + 1}_url"] = (
pool["url"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
pools[f"pool_{i + 1}_user"] = pool["user"]
groups.append(pools)
return groups
except (KeyError, TypeError):
pass
if not api_pools:
try:
api_pools = await self.api.pools()
except APIError:
pass
if api_pools:
seen = []
groups = [{"quota": "0"}]
if api_pools.get("POOLS"):
for i, pool in enumerate(api_pools["POOLS"]):
if len(seen) == 0:
seen.append(pool["User"])
if not pool["User"] in seen:
# need to use get_config, as this will never read perfectly as there are some bad edge cases
groups = []
cfg = await self.get_config()
if cfg:
for group in cfg.pool_groups:
pools = {"quota": group.quota}
for _i, _pool in enumerate(group.pools):
pools[f"pool_{_i + 1}_url"] = _pool.url.replace(
"stratum+tcp://", ""
).replace("stratum2+tcp://", "")
pools[f"pool_{_i + 1}_user"] = _pool.username
groups.append(pools)
return groups
else:
groups[0][f"pool_{i + 1}_url"] = (
pool["URL"]
.replace("stratum+tcp://", "")
.replace("stratum2+tcp://", "")
)
groups[0][f"pool_{i + 1}_user"] = pool["User"]
else:
groups = []
cfg = await self.get_config()
if cfg:
for group in cfg.pool_groups:
pools = {"quota": group.quota}
for _i, _pool in enumerate(group.pools):
pools[f"pool_{_i + 1}_url"] = _pool.url.replace(
"stratum+tcp://", ""
).replace("stratum2+tcp://", "")
pools[f"pool_{_i + 1}_user"] = _pool.username
groups.append(pools)
return groups
return groups
async def get_errors(
self, api_tunerstatus: dict = None, graphql_errors: dict = None
) -> List[MinerErrorData]:
if not graphql_errors and not api_tunerstatus:
try:
graphql_errors = await self.web.send_command(
{
"bosminer": {
"info": {
"workSolver": {
"childSolvers": {
"name": None,
"tuner": {"statusMessages"},
}
}
}
}
}
)
except APIError:
pass
if graphql_errors:
errors = []
try:
boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][
"childSolvers"
]
except (LookupError, TypeError):
boards = None
if boards:
offset = 6 if int(boards[0]["name"]) in [6, 7, 8] else 0
for hb in boards:
_id = int(hb["name"]) - offset
tuner = hb["tuner"]
if tuner:
if msg := tuner.get("statusMessages"):
if len(msg) > 0:
if hb["tuner"]["statusMessages"][0] not in [
"Stable",
"Testing performance profile",
"Tuning individual chips",
]:
errors.append(
BraiinsOSError(
f"Slot {_id} {hb['tuner']['statusMessages'][0]}"
)
)
if not api_tunerstatus:
try:
api_tunerstatus = await self.api.tunerstatus()
except APIError:
pass
if api_tunerstatus:
errors = []
try:
chain_status = api_tunerstatus["TUNERSTATUS"][0]["TunerChainStatus"]
if chain_status and len(chain_status) > 0:
offset = (
6 if int(chain_status[0]["HashchainIndex"]) in [6, 7, 8] else 0
)
for board in chain_status:
_id = board["HashchainIndex"] - offset
if board["Status"] not in [
"Stable",
"Testing performance profile",
"Tuning individual chips",
]:
_error = board["Status"].split(" {")[0]
_error = _error[0].lower() + _error[1:]
errors.append(BraiinsOSError(f"Slot {_id} {_error}"))
return errors
except (KeyError, IndexError):
pass
async def get_fault_light(self, graphql_fault_light: dict = None) -> bool:
if self.light:
return self.light
if not graphql_fault_light:
if self.fw_ver:
# fw version has to be greater than 21.09 and not 21.09
if (
int(self.fw_ver.split(".")[0]) == 21
and int(self.fw_ver.split(".")[1]) > 9
) or int(self.fw_ver.split(".")[0]) > 21:
try:
graphql_fault_light = await self.web.send_command(
{"bos": {"faultLight"}}
)
except APIError:
pass
else:
logging.info(
f"FW version {self.fw_ver} is too low for fault light info in graphql."
)
else:
# worth trying
try:
graphql_fault_light = await self.web.send_command(
{"bos": {"faultLight"}}
)
except APIError:
logging.debug(
"GraphQL fault light failed, likely due to version being too low (<=21.0.9)"
)
if not graphql_fault_light:
# also a failure
logging.debug(
"GraphQL fault light failed, likely due to version being too low (<=21.0.9)"
)
# get light through GraphQL
if graphql_fault_light:
try:
self.light = graphql_fault_light["data"]["bos"]["faultLight"]
return self.light
except (TypeError, ValueError, LookupError):
pass
# get light via ssh if that fails (10x slower)
try:
data = (
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
).strip()
self.light = False
if data == "50":
self.light = True
return self.light
except TypeError:
return self.light
async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]:
if not api_devs:
try:
api_devs = await self.api.devs()
except APIError:
pass
if api_devs:
try:
offset = 6 if api_devs["DEVS"][0]["ID"] in [6, 7, 8] else 0
hr_list = []
for board in api_devs["DEVS"]:
_id = board["ID"] - offset
nominal_hashrate = round(float(board["Nominal MHS"] / 1000000), 2)
if nominal_hashrate:
hr_list.append(nominal_hashrate)
if len(hr_list) == 0:
return 0
else:
return round(
(sum(hr_list) / len(hr_list)) * self.ideal_hashboards, 2
)
except (IndexError, KeyError):
pass
async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]:
if not api_devdetails:
try:
api_devdetails = await self.api.send_command(
"devdetails", ignore_errors=True, allow_warning=False
)
except APIError:
pass
if api_devdetails:
try:
return not api_devdetails["STATUS"][0]["Msg"] == "Unavailable"
except LookupError:
pass
async def get_uptime(self, api_summary: dict = None) -> Optional[int]:
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
return int(api_summary["SUMMARY"][0]["Elapsed"])
except LookupError:
pass