Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
464bd6be65 | ||
|
|
031d7e2186 | ||
|
|
126b0d124c | ||
|
|
81c84a3e8f | ||
|
|
406d5bd549 | ||
|
|
cd5fe09fd9 | ||
|
|
766fc4efed | ||
|
|
b70fed40c8 | ||
|
|
255d98fd08 | ||
|
|
78304631f7 | ||
|
|
ab230844fc | ||
|
|
8a58cb9fd3 | ||
|
|
70b6ed73dc | ||
|
|
d2400bf44e | ||
|
|
db780fe876 | ||
|
|
cd84ae828a | ||
|
|
970d5d1031 | ||
|
|
da0e327ec7 | ||
|
|
4c84a8d572 | ||
|
|
1cfd895deb |
@@ -18,6 +18,7 @@ import logging
|
|||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
import toml
|
import toml
|
||||||
|
import httpx
|
||||||
|
|
||||||
from pyasic.API.bosminer import BOSMinerAPI
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
from pyasic.config import MinerConfig
|
from pyasic.config import MinerConfig
|
||||||
@@ -65,6 +66,24 @@ class BOSMiner(BaseMiner):
|
|||||||
# return the result, either command output or None
|
# return the result, either command output or None
|
||||||
return str(result)
|
return str(result)
|
||||||
|
|
||||||
|
async def send_graphql_query(self, query) -> Union[dict, None]:
|
||||||
|
url = f"http://{self.ip}/graphql"
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
_auth = await client.post(
|
||||||
|
url,
|
||||||
|
json={
|
||||||
|
"query": 'mutation{auth{login(username:"'
|
||||||
|
+ self.uname
|
||||||
|
+ '", password:"'
|
||||||
|
+ self.pwd
|
||||||
|
+ '"){__typename}}}'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
d = await client.post(url, json={"query": query})
|
||||||
|
if d.status_code == 200:
|
||||||
|
return d.json()
|
||||||
|
return None
|
||||||
|
|
||||||
async def fault_light_on(self) -> bool:
|
async def fault_light_on(self) -> bool:
|
||||||
"""Sends command to turn on fault light on the miner."""
|
"""Sends command to turn on fault light on the miner."""
|
||||||
logging.debug(f"{self}: Sending fault_light on command.")
|
logging.debug(f"{self}: Sending fault_light on command.")
|
||||||
@@ -148,6 +167,11 @@ class BOSMiner(BaseMiner):
|
|||||||
"""
|
"""
|
||||||
if self.hostname:
|
if self.hostname:
|
||||||
return self.hostname
|
return self.hostname
|
||||||
|
# get hostname through GraphQL
|
||||||
|
if data := await self.send_graphql_query("{bos {hostname}}"):
|
||||||
|
self.hostname = data["data"]["bos"]["hostname"]
|
||||||
|
return self.hostname
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with (await self._get_ssh_connection()) as conn:
|
async with (await self._get_ssh_connection()) as conn:
|
||||||
if conn is not None:
|
if conn is not None:
|
||||||
@@ -211,9 +235,15 @@ class BOSMiner(BaseMiner):
|
|||||||
if self.version:
|
if self.version:
|
||||||
logging.debug(f"Found version for {self.ip}: {self.version}")
|
logging.debug(f"Found version for {self.ip}: {self.version}")
|
||||||
return self.version
|
return self.version
|
||||||
|
version_data = None
|
||||||
|
# try to get data from graphql
|
||||||
|
data = await self.send_graphql_query("{bos{info{version{full}}}}")
|
||||||
|
if data:
|
||||||
|
version_data = data["bos"]["info"]["version"]["full"]
|
||||||
|
|
||||||
# get output of bos version file
|
if not version_data:
|
||||||
version_data = await self.send_ssh_command("cat /etc/bos_version")
|
# try version data file
|
||||||
|
version_data = await self.send_ssh_command("cat /etc/bos_version")
|
||||||
|
|
||||||
# if we get the version data, parse it
|
# if we get the version data, parse it
|
||||||
if version_data:
|
if version_data:
|
||||||
@@ -244,6 +274,15 @@ class BOSMiner(BaseMiner):
|
|||||||
async def check_light(self) -> bool:
|
async def check_light(self) -> bool:
|
||||||
if self.light:
|
if self.light:
|
||||||
return self.light
|
return self.light
|
||||||
|
# get light through GraphQL
|
||||||
|
if data := await self.send_graphql_query("{bos {faultLight}}"):
|
||||||
|
try:
|
||||||
|
self.light = data["data"]["bos"]["faultLight"]
|
||||||
|
return self.light
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# get light via ssh if that fails (10x slower)
|
||||||
data = (
|
data = (
|
||||||
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
|
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
|
||||||
).strip()
|
).strip()
|
||||||
@@ -285,6 +324,7 @@ class BOSMiner(BaseMiner):
|
|||||||
if board["Status"] not in [
|
if board["Status"] not in [
|
||||||
"Stable",
|
"Stable",
|
||||||
"Testing performance profile",
|
"Testing performance profile",
|
||||||
|
"Tuning individual chips"
|
||||||
]:
|
]:
|
||||||
_error = board["Status"].split(" {")[0]
|
_error = board["Status"].split(" {")[0]
|
||||||
_error = _error[0].lower() + _error[1:]
|
_error = _error[0].lower() + _error[1:]
|
||||||
@@ -299,6 +339,10 @@ class BOSMiner(BaseMiner):
|
|||||||
Returns:
|
Returns:
|
||||||
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
A [`MinerData`][pyasic.data.MinerData] instance containing the miners data.
|
||||||
"""
|
"""
|
||||||
|
d = await self._graphql_get_data()
|
||||||
|
if d:
|
||||||
|
return d
|
||||||
|
|
||||||
data = MinerData(
|
data = MinerData(
|
||||||
ip=str(self.ip),
|
ip=str(self.ip),
|
||||||
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||||
@@ -338,7 +382,7 @@ class BOSMiner(BaseMiner):
|
|||||||
"devdetails",
|
"devdetails",
|
||||||
"fans",
|
"fans",
|
||||||
"devs",
|
"devs",
|
||||||
allow_warning=allow_warning
|
allow_warning=allow_warning,
|
||||||
)
|
)
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
if str(e.message) == "Not ready":
|
if str(e.message) == "Not ready":
|
||||||
@@ -489,6 +533,136 @@ class BOSMiner(BaseMiner):
|
|||||||
data.hashboards[_id].hashrate = hashrate
|
data.hashboards[_id].hashrate = hashrate
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
async def _graphql_get_data(self) -> Union[MinerData, None]:
|
||||||
|
data = MinerData(
|
||||||
|
ip=str(self.ip),
|
||||||
|
ideal_chips=self.nominal_chips * self.ideal_hashboards,
|
||||||
|
ideal_hashboards=self.ideal_hashboards,
|
||||||
|
hashboards=[
|
||||||
|
HashBoard(slot=i, expected_chips=self.nominal_chips, missing=True)
|
||||||
|
for i in range(self.ideal_hashboards)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
query = "{bos {hostname}, bosminer{config{... on BosminerConfig{groups{pools{url, user}, strategy{... on QuotaStrategy {quota}}}}}, info{fans{name, rpm}, workSolver{realHashrate{mhs1M}, temperatures{degreesC}, power{limitW, approxConsumptionW}, childSolvers{name, realHashrate{mhs1M}, hwDetails{chips}, tuner{statusMessages}, temperatures{degreesC}}}}}}"
|
||||||
|
query_data = await self.send_graphql_query(query)
|
||||||
|
if not query_data:
|
||||||
|
return None
|
||||||
|
query_data = query_data["data"]
|
||||||
|
|
||||||
|
data.mac = await self.get_mac()
|
||||||
|
data.model = await self.get_model()
|
||||||
|
if query_data.get("bos"):
|
||||||
|
if query_data["bos"].get("hostname"):
|
||||||
|
data.hostname = query_data["bos"]["hostname"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if query_data["bosminer"]["info"]["workSolver"]["realHashrate"].get("mhs1M"):
|
||||||
|
data.hashrate = round(
|
||||||
|
query_data["bosminer"]["info"]["workSolver"]["realHashrate"]["mhs1M"]
|
||||||
|
/ 1000000,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
boards = None
|
||||||
|
if query_data.get("bosminer"):
|
||||||
|
if query_data["bosminer"].get("info"):
|
||||||
|
if query_data["bosminer"]["info"].get("workSolver"):
|
||||||
|
boards = query_data["bosminer"]["info"]["workSolver"].get("childSolvers")
|
||||||
|
if boards:
|
||||||
|
offset = 6 if int(boards[0]["name"]) in [6, 7, 8] else int(boards[0]["name"])
|
||||||
|
for hb in boards:
|
||||||
|
_id = int(hb["name"]) - offset
|
||||||
|
|
||||||
|
board = data.hashboards[_id]
|
||||||
|
board.hashrate = round(hb["realHashrate"]["mhs1M"] / 1000000, 2)
|
||||||
|
temps = hb["temperatures"]
|
||||||
|
if len(temps) > 0:
|
||||||
|
board.temp = round(hb["temperatures"][0]["degreesC"])
|
||||||
|
if len(temps) > 1:
|
||||||
|
board.chip_temp = round(hb["temperatures"][1]["degreesC"])
|
||||||
|
details = hb.get("hwDetails")
|
||||||
|
if details:
|
||||||
|
if chips := details["chips"]:
|
||||||
|
board.chips = chips
|
||||||
|
board.missing = False
|
||||||
|
|
||||||
|
tuner = hb.get("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"
|
||||||
|
]:
|
||||||
|
data.errors.append(
|
||||||
|
BraiinsOSError(f"Slot {_id} {hb['tuner']['statusMessages'][0]}")
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data.wattage = query_data["bosminer"]["info"]["workSolver"]["power"]["approxConsumptionW"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
data.wattage_limit = query_data["bosminer"]["info"]["workSolver"]["power"]["limitW"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
for n in range(self.fan_count):
|
||||||
|
try:
|
||||||
|
setattr(data, f"fan_{n + 1}", query_data["bosminer"]["info"]["fans"][n]["rpm"])
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
groups = None
|
||||||
|
if query_data.get("bosminer"):
|
||||||
|
if query_data["bosminer"].get("config"):
|
||||||
|
groups = query_data["bosminer"]["config"].get("groups")
|
||||||
|
if groups:
|
||||||
|
if len(groups) == 1:
|
||||||
|
try:
|
||||||
|
data.pool_1_user = groups[0]["pools"][0]["user"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
data.pool_1_url = groups[0]["pools"][0]["url"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
data.pool_2_user = groups[0]["pools"][1]["user"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
data.pool_2_url = groups[0]["pools"][1]["url"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
data.quota = 0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
data.pool_1_user = groups[0]["pools"][0]["user"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
data.pool_1_url = groups[0]["pools"][0]["url"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
data.pool_2_user = groups[1]["pools"][0]["user"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
data.pool_2_url = groups[1]["pools"][0]["url"]
|
||||||
|
except (TypeError, KeyError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
if groups[0]["strategy"].get("quota"):
|
||||||
|
data.quota = groups[0]["strategy"]["quota"] + "/" + groups[1]["strategy"]["quota"]
|
||||||
|
|
||||||
|
data.fault_light = await self.check_light()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
async def get_mac(self):
|
async def get_mac(self):
|
||||||
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
||||||
return result.upper().strip()
|
return result.upper().strip()
|
||||||
|
|||||||
@@ -433,6 +433,12 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
|
|
||||||
# if we have devdetails, we can get model data from there
|
# if we have devdetails, we can get model data from there
|
||||||
if devdetails:
|
if devdetails:
|
||||||
|
if devdetails == {"Msg": "Disconnected"}:
|
||||||
|
model = await self.__get_model_from_graphql(ip)
|
||||||
|
if model:
|
||||||
|
api = "BOSMiner+"
|
||||||
|
return model, api, ver
|
||||||
|
|
||||||
for _devdetails_key in ["Model", "Driver"]:
|
for _devdetails_key in ["Model", "Driver"]:
|
||||||
try:
|
try:
|
||||||
model = devdetails["DEVDETAILS"][0][_devdetails_key].upper()
|
model = devdetails["DEVDETAILS"][0][_devdetails_key].upper()
|
||||||
@@ -540,6 +546,11 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
# validate success
|
# validate success
|
||||||
validation = await self._validate_command(data)
|
validation = await self._validate_command(data)
|
||||||
if not validation[0]:
|
if not validation[0]:
|
||||||
|
try:
|
||||||
|
if data["version"][0]["STATUS"][0]["Msg"] == "Disconnected":
|
||||||
|
return {"Msg": "Disconnected"}, None
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
raise APIError(validation[1])
|
raise APIError(validation[1])
|
||||||
# copy each part of the main command to devdetails and version
|
# copy each part of the main command to devdetails and version
|
||||||
devdetails = data["devdetails"][0]
|
devdetails = data["devdetails"][0]
|
||||||
@@ -587,6 +598,16 @@ class MinerFactory(metaclass=Singleton):
|
|||||||
model = "ANTMINER S17"
|
model = "ANTMINER S17"
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def __get_model_from_graphql(ip: ipaddress.ip_address) -> Union[str, None]:
|
||||||
|
model = None
|
||||||
|
url = f"http://{ip}/graphql"
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
d = await client.post(url, json={"query": "{bosminer {info{modelName}}}"})
|
||||||
|
if d.status_code == 200:
|
||||||
|
model = d.json()["data"]["bosminer"]["info"]["modelName"].upper()
|
||||||
|
return model
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def __get_system_info_from_web(ip) -> dict:
|
async def __get_system_info_from_web(ip) -> dict:
|
||||||
url = f"http://{ip}/cgi-bin/get_system_info.cgi"
|
url = f"http://{ip}/cgi-bin/get_system_info.cgi"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pyasic"
|
name = "pyasic"
|
||||||
version = "0.20.2"
|
version = "0.21.4"
|
||||||
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
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>"]
|
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
||||||
repository = "https://github.com/UpstreamData/pyasic"
|
repository = "https://github.com/UpstreamData/pyasic"
|
||||||
|
|||||||
Reference in New Issue
Block a user