Compare commits

...

20 Commits

Author SHA1 Message Date
UpstreamData
eb5d1a24ea version: bump version number. 2023-07-12 08:56:59 -06:00
UpstreamData
6c0e80265b bug: revert X19 miner mode to string. 2023-07-12 08:56:23 -06:00
UpstreamData
ad3a4ae414 docs: update some bad code, and add references to new miner types and API types. 2023-07-11 11:18:28 -06:00
UpstreamData
3484d43510 version: bump version number. 2023-07-07 14:09:22 -06:00
UpstreamData
dd7e352391 bug: fix some addition issues with MinerData sums. 2023-07-07 14:09:08 -06:00
UpstreamData
a32b61fe5d version: bump version number. 2023-07-07 12:37:34 -06:00
UpstreamData
597a178009 feature: Update MinerData to use None. 2023-07-07 12:37:20 -06:00
Michael Schmid
409b2527f0 use None instead of -1 for temps and wattages (#55)
* use `None` instead of `-1` for temps and wattages
this way it's easier for other tools like HomeAssistant to understand if the temperature is really negative or not available

* also handle cases where we look for `-1`
2023-07-07 12:06:24 -06:00
UpstreamData
58234fcf7f version: bump version number. 2023-07-07 12:03:28 -06:00
UpstreamData
1bf863cca8 bug: set miner_mode to int instead of str to fix some issues with some X19 models. 2023-07-07 12:03:00 -06:00
UpstreamData
6482d04185 version: bump version number. 2023-07-07 11:56:41 -06:00
UpstreamData
3b58b11501 bug: remove 0 frequency level when setting sleep mode on X19, as it seems to bug some types. 2023-07-07 11:56:06 -06:00
UpstreamData
7485b8ef77 version: bump version number. 2023-07-04 10:04:20 -06:00
UpstreamData
d2bea227db bug: fix an issue with a possible SSH command in BOS+. 2023-07-04 10:01:24 -06:00
UpstreamData
1b7afaaf7e version: bump version number. 2023-06-30 08:49:31 -06:00
UpstreamData
96898d639c bug: fix some handling errors with graphql. 2023-06-30 08:49:08 -06:00
UpstreamData
eb439f4dcf version: bump version number. 2023-06-30 08:44:01 -06:00
UpstreamData
69f4349393 feature: create pwd and username property in miner object that sets web and api passwords and usernames. 2023-06-30 08:43:30 -06:00
UpstreamData
e371bb577c version: bump version number. 2023-06-29 18:10:35 -06:00
UpstreamData
2500ec3869 bug: fix possible None return from some bosminer webcommands. 2023-06-29 18:10:10 -06:00
15 changed files with 211 additions and 70 deletions

View File

@@ -15,6 +15,7 @@ Use these instead -
#### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI]
#### [BTMiner API][pyasic.API.btminer.BTMinerAPI]
#### [CGMiner API][pyasic.API.cgminer.CGMinerAPI]
#### [LUXMiner API][pyasic.API.luxminer.LUXMinerAPI]
#### [Unknown API][pyasic.API.unknown.UnknownAPI]
<br>

7
docs/API/luxminer.md Normal file
View File

@@ -0,0 +1,7 @@
# pyasic
## LUXMinerAPI
::: pyasic.API.luxminer.LUXMinerAPI
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -76,13 +76,14 @@ This function will return an instance of the dataclass [`MinerData`][pyasic.data
Each piece of data in a [`MinerData`][pyasic.data.MinerData] instance can be referenced by getting it as an attribute, such as [`MinerData().hashrate`][pyasic.data.MinerData].
```python
import asyncio
from pyasic.miners.miner_factory import MinerFactory
from pyasic import get_miner
async def gather_miner_data():
miner = await MinerFactory().get_miner("192.168.1.75")
miner_data = await miner.get_data()
print(miner_data) # all data from the dataclass
print(miner_data.hashrate) # hashrate of the miner in TH/s
miner = await get_miner("192.168.1.75")
if miner is not None:
miner_data = await miner.get_data()
print(miner_data) # all data from the dataclass
print(miner_data.hashrate) # hashrate of the miner in TH/s
if __name__ == "__main__":
asyncio.run(gather_miner_data())

View File

@@ -0,0 +1,8 @@
# pyasic
## LUXMiner Backend
::: pyasic.miners.backends.luxminer.LUXMiner
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -0,0 +1,8 @@
# pyasic
## VNish Backend
::: pyasic.miners.backends.vnish.VNish
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -20,6 +20,7 @@ nav:
- BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md"
- LUXMiner: "API/luxminer.md"
- Unknown: "API/unknown.md"
- Backends:
- BMMiner: "miners/backends/bmminer.md"
@@ -27,6 +28,8 @@ nav:
- BFGMiner: "miners/backends/bfgminer.md"
- BTMiner: "miners/backends/btminer.md"
- CGMiner: "miners/backends/cgminer.md"
- LUXMiner: "miners/backends/luxminer.md"
- VNish: "miners/backends/vnish.md"
- Hiveon: "miners/backends/hiveon.md"
- Classes:
- Antminer X3: "miners/antminer/X3.md"
@@ -40,14 +43,15 @@ nav:
- Avalon 8X: "miners/avalonminer/A8X.md"
- Avalon 9X: "miners/avalonminer/A9X.md"
- Avalon 10X: "miners/avalonminer/A10X.md"
- Avalon 11X: "miners/avalonminer/A11X.md"
- Avalon 12X: "miners/avalonminer/A12X.md"
- Whatsminer M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md"
- Whatsminer M5X: "miners/whatsminer/M5X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md"
- Innosilicon A10X: "miners/innosilicon/A10X.md"
- Goldshell CKX: "miners/goldshell/CKX.md"
- Goldshell HSX: "miners/goldshell/HSX.md"
- Goldshell KDX: "miners/goldshell/KDX.md"
- Goldshell X5: "miners/goldshell/X5.md"
- Goldshell XMax: "miners/goldshell/XMax.md"
- Base Miner: "miners/base_miner.md"

View File

@@ -32,6 +32,8 @@ class BaseMinerAPI:
# ip address of the miner
self.ip = ipaddress.ip_address(ip)
self.pwd = "admin"
def __new__(cls, *args, **kwargs):
if cls is BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")

View File

@@ -560,9 +560,6 @@ class MinerConfig:
if self.fan_speed:
cfg["bitmain-fan-pwn"] = str(self.fan_speed)
if self.miner_mode == X19PowerMode.Sleep:
cfg["freq-level"] = "0"
return cfg
def as_x17(self, user_suffix: str = None) -> dict:

View File

@@ -20,7 +20,7 @@ import logging
import time
from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone
from typing import List, Union
from typing import List, Union, Any
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
@@ -40,13 +40,28 @@ class HashBoard:
"""
slot: int = 0
hashrate: float = 0.0
temp: int = -1
chip_temp: int = -1
chips: int = 0
expected_chips: int = 0
hashrate: float = None
temp: int = None
chip_temp: int = None
chips: int = None
expected_chips: int = None
missing: bool = True
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
@dataclass
class Fan:
@@ -56,7 +71,22 @@ class Fan:
speed: The speed of the fan.
"""
speed: int = -1
speed: int = None
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"{item}")
@dataclass
@@ -102,26 +132,26 @@ class MinerData:
ip: str
datetime: datetime = None
uptime: int = 0
mac: str = "00:00:00:00:00:00"
model: str = "Unknown"
make: str = "Unknown"
api_ver: str = "Unknown"
fw_ver: str = "Unknown"
hostname: str = "Unknown"
uptime: int = None
mac: str = None
model: str = None
make: str = None
api_ver: str = None
fw_ver: str = None
hostname: str = None
hashrate: float = field(init=False)
_hashrate: float = 0
nominal_hashrate: float = 0
_hashrate: float = None
nominal_hashrate: float = None
hashboards: List[HashBoard] = field(default_factory=list)
ideal_hashboards: int = 1
ideal_hashboards: int = None
temperature_avg: int = field(init=False)
env_temp: float = -1.0
wattage: int = -1
wattage_limit: int = -1
env_temp: float = None
wattage: int = None
wattage_limit: int = None
fans: List[Fan] = field(default_factory=list)
fan_psu: int = -1
fan_psu: int = None
total_chips: int = field(init=False)
ideal_chips: int = 1
ideal_chips: int = None
percent_ideal_chips: float = field(init=False)
percent_ideal_hashrate: float = field(init=False)
percent_ideal_wattage: float = field(init=False)
@@ -145,7 +175,16 @@ class MinerData:
def __post_init__(self):
self.datetime = datetime.now(timezone.utc).astimezone()
def __getitem__(self, item):
def get(self, __key: str, default: Any = None):
try:
val = self.__getitem__(__key)
if val is None:
return default
return val
except KeyError:
return default
def __getitem__(self, item: str):
try:
return getattr(self, item)
except AttributeError:
@@ -197,7 +236,12 @@ class MinerData:
@property
def hashrate(self): # noqa - Skip PyCharm inspection
if len(self.hashboards) > 0:
return round(sum(map(lambda x: x.hashrate, self.hashboards)), 2)
hr_data = []
for item in self.hashboards:
if item.hashrate is not None:
hr_data.append(item.hashrate)
if len(hr_data) > 0:
return sum(hr_data)
return self._hashrate
@hashrate.setter
@@ -206,7 +250,14 @@ class MinerData:
@property
def total_chips(self): # noqa - Skip PyCharm inspection
return sum([hb.chips for hb in self.hashboards])
if len(self.hashboards) > 0:
chip_data = []
for item in self.hashboards:
if item.chips is not None:
chip_data.append(item.chips)
if len(chip_data) > 0:
return sum(chip_data)
return None
@total_chips.setter
def total_chips(self, val):
@@ -214,6 +265,8 @@ class MinerData:
@property
def nominal(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.ideal_chips is None:
return None
return self.ideal_chips == self.total_chips
@nominal.setter
@@ -222,6 +275,8 @@ class MinerData:
@property
def percent_ideal_chips(self): # noqa - Skip PyCharm inspection
if self.total_chips is None or self.ideal_chips is None:
return None
if self.total_chips == 0 or self.ideal_chips == 0:
return 0
return round((self.total_chips / self.ideal_chips) * 100)
@@ -232,6 +287,8 @@ class MinerData:
@property
def percent_ideal_hashrate(self): # noqa - Skip PyCharm inspection
if self.hashrate is None or self.nominal_hashrate is None:
return None
if self.hashrate == 0 or self.nominal_hashrate == 0:
return 0
return round((self.hashrate / self.nominal_hashrate) * 100)
@@ -242,6 +299,8 @@ class MinerData:
@property
def percent_ideal_wattage(self): # noqa - Skip PyCharm inspection
if self.wattage_limit is None or self.wattage is None:
return None
if self.wattage_limit == 0 or self.wattage == 0:
return 0
return round((self.wattage / self.wattage_limit) * 100)
@@ -255,11 +314,11 @@ class MinerData:
total_temp = 0
temp_count = 0
for hb in self.hashboards:
if hb.temp and not hb.temp == -1:
if hb.temp is not None:
total_temp += hb.temp
temp_count += 1
if not temp_count > 0:
return 0
return None
return round(total_temp / temp_count)
@temperature_avg.setter
@@ -268,7 +327,9 @@ class MinerData:
@property
def efficiency(self): # noqa - Skip PyCharm inspection
if self.hashrate == 0 or self.wattage == -1:
if self.hashrate is None or self.wattage is None:
return None
if self.hashrate == 0 or self.wattage == 0:
return 0
return round(self.wattage / self.hashrate)
@@ -328,7 +389,7 @@ class MinerData:
tags = ["ip", "mac", "model", "hostname"]
for attribute in self:
if attribute in tags:
escaped_data = self[attribute].replace(" ", "\\ ")
escaped_data = self.get(attribute, "Unknown").replace(" ", "\\ ")
tag_data.append(f"{attribute}={escaped_data}")
continue
elif str(attribute).startswith("_"):
@@ -345,26 +406,24 @@ class MinerData:
elif isinstance(self[attribute], float):
field_data.append(f"{attribute}={self[attribute]}")
continue
elif attribute == "fault_light" and not self[attribute]:
field_data.append(f"{attribute}=false")
continue
elif attribute == "errors":
for idx, item in enumerate(self[attribute]):
field_data.append(f'error_{idx+1}="{item.error_message}"')
elif attribute == "hashboards":
for idx, item in enumerate(self[attribute]):
field_data.append(f"hashboard_{idx+1}_hashrate={item.hashrate}")
field_data.append(f"hashboard_{idx+1}_temperature={item.temp}")
field_data.append(f"hashboard_{idx+1}_hashrate={item.get('hashrate', 0.0)}")
field_data.append(f"hashboard_{idx+1}_temperature={item.get('temp', 0)}")
field_data.append(
f"hashboard_{idx+1}_chip_temperature={item.chip_temp}"
f"hashboard_{idx+1}_chip_temperature={item.get('chip_temp', 0)}"
)
field_data.append(f"hashboard_{idx+1}_chips={item.chips}")
field_data.append(f"hashboard_{idx+1}_chips={item.get('chips', 0)}")
field_data.append(
f"hashboard_{idx+1}_expected_chips={item.expected_chips}"
f"hashboard_{idx+1}_expected_chips={item.get('expected_chips', 0)}"
)
elif attribute == "fans":
for idx, item in enumerate(self[attribute]):
field_data.append(f"fan_{idx+1}={item.speed}")
if item.speed is not None:
field_data.append(f"fan_{idx+1}={item.speed}")
tags_str = ",".join(tag_data)
field_str = ",".join(field_data)

View File

@@ -149,10 +149,10 @@ class _MinerPhaseBalancer:
not self.miners[data_point.ip]["shutdown"]
):
# cant do anything with it so need to find a semi-accurate power limit
if not data_point.wattage_limit == -1:
if not data_point.wattage_limit == None:
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
elif not data_point.wattage == -1:
elif not data_point.wattage == None:
self.miners[data_point.ip]["max"] = int(data_point.wattage)
self.miners[data_point.ip]["min"] = int(data_point.wattage)

View File

@@ -58,7 +58,7 @@ class HiveonT9(Hiveon, T9):
hashrate = 0
chips = 0
for chipset in board_map[board]:
if hashboard.chip_temp == -1:
if hashboard.chip_temp == None:
try:
hashboard.board_temp = api_stats["STATS"][1][f"temp{chipset}"]
hashboard.chip_temp = api_stats["STATS"][1][f"temp2_{chipset}"]

View File

@@ -494,7 +494,7 @@ class BOSMiner(BaseMiner):
if graphql_version:
try:
fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"]
except KeyError:
except (KeyError, TypeError):
pass
if not fw_ver:
@@ -523,7 +523,7 @@ class BOSMiner(BaseMiner):
try:
hostname = graphql_hostname["data"]["bos"]["hostname"]
return hostname
except KeyError:
except (TypeError, KeyError):
pass
try:
@@ -563,7 +563,7 @@ class BOSMiner(BaseMiner):
),
2,
)
except (KeyError, IndexError, ValueError):
except (LookupError, ValueError, TypeError):
pass
# get hr from API
@@ -617,7 +617,7 @@ class BOSMiner(BaseMiner):
boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][
"childSolvers"
]
except (KeyError, IndexError):
except (TypeError, LookupError):
boards = None
if boards:
@@ -732,7 +732,7 @@ class BOSMiner(BaseMiner):
return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][
"power"
]["approxConsumptionW"]
except (KeyError, TypeError):
except (LookupError, TypeError):
pass
if not api_tunerstatus:
@@ -765,7 +765,7 @@ class BOSMiner(BaseMiner):
return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][
"power"
]["limitW"]
except (KeyError, TypeError):
except (LookupError, TypeError):
pass
if not api_tunerstatus:
@@ -801,7 +801,7 @@ class BOSMiner(BaseMiner):
]
)
)
except KeyError:
except (LookupError, TypeError):
pass
return fans
@@ -941,7 +941,7 @@ class BOSMiner(BaseMiner):
boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][
"childSolvers"
]
except (KeyError, IndexError):
except (LookupError, TypeError):
boards = None
if boards:
@@ -1034,17 +1034,20 @@ class BOSMiner(BaseMiner):
try:
self.light = graphql_fault_light["data"]["bos"]["faultLight"]
return self.light
except (TypeError, KeyError, ValueError, IndexError):
except (TypeError, ValueError, LookupError):
pass
# get light via ssh if that fails (10x slower)
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
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:

View File

@@ -72,6 +72,52 @@ class BaseMiner(ABC):
def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip)
@property
def pwd(self): # noqa - Skip PyCharm inspection
data = []
try:
if self.web is not None:
data.append(f"web={self.web.pwd}")
except TypeError:
pass
try:
if self.api is not None:
data.append(f"api={self.api.pwd}")
except TypeError:
pass
return ",".join(data)
@pwd.setter
def pwd(self, val):
try:
if self.web is not None:
self.web.pwd = val
except TypeError:
pass
try:
if self.api is not None:
self.api.pwd = val
except TypeError:
pass
@property
def username(self): # noqa - Skip PyCharm inspection
data = []
try:
if self.web is not None:
data.append(f"web={self.web.username}")
except TypeError:
pass
return ",".join(data)
@username.setter
def username(self, val):
try:
if self.web is not None:
self.web.username = val
except TypeError:
pass
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
try:

View File

@@ -76,7 +76,7 @@ class BOSMinerWebAPI(BaseWebAPI):
async def multicommand(
self, *commands: Union[dict, str], allow_warning: bool = True
):
) -> dict:
luci_commands = []
gql_commands = []
for cmd in commands:
@@ -88,6 +88,11 @@ class BOSMinerWebAPI(BaseWebAPI):
luci_data = await self.luci_multicommand(*luci_commands)
gql_data = await self.gql_multicommand(*gql_commands)
if gql_data is None:
gql_data = {}
if luci_data is None:
luci_data = {}
data = dict(**luci_data, **gql_data)
return data

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyasic"
version = "0.36.6"
version = "0.37.2"
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic"