Compare commits

...

29 Commits

Author SHA1 Message Date
UpstreamData
3dfd9f237d version: bump version number. 2023-07-27 20:18:58 -06:00
UpstreamData
f3fe478dbb feature: add support for S19J Pro No PIC. 2023-07-27 20:18:36 -06:00
UpstreamData
e10f32ae3d feature: speed up getting older antminer types with concurrent web and api requests. 2023-07-24 21:05:07 -06:00
UpstreamData
4e0924aa0e feature: add support for AML vnish miners. 2023-07-24 20:45:30 -06:00
UpstreamData
d0d3fd3117 bug: fix failed verification of SSL cert on whatsminer. 2023-07-24 20:19:00 -06:00
UpstreamData
4de950d8f4 feature: revert miner_factory to use httpx, as it now seems to be the same speed, and aiohttp doesnt support digest auth. 2023-07-24 13:09:30 -06:00
UpstreamData
03f2a1f9ba feature: optimize multicommand on new X19 models. 2023-07-24 11:34:16 -06:00
UpstreamData
2653db90e3 feature: optimize the way multicommand is handled on BTMiner. 2023-07-24 09:44:23 -06:00
UpstreamData
ddc8c53eb9 feature: add chip count for M50 VH60. 2023-07-13 10:59:27 -06:00
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
23 changed files with 458 additions and 250 deletions

View File

@@ -15,6 +15,7 @@ Use these instead -
#### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI] #### [BOSMiner API][pyasic.API.bosminer.BOSMinerAPI]
#### [BTMiner API][pyasic.API.btminer.BTMinerAPI] #### [BTMiner API][pyasic.API.btminer.BTMinerAPI]
#### [CGMiner API][pyasic.API.cgminer.CGMinerAPI] #### [CGMiner API][pyasic.API.cgminer.CGMinerAPI]
#### [LUXMiner API][pyasic.API.luxminer.LUXMinerAPI]
#### [Unknown API][pyasic.API.unknown.UnknownAPI] #### [Unknown API][pyasic.API.unknown.UnknownAPI]
<br> <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]. 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 ```python
import asyncio import asyncio
from pyasic.miners.miner_factory import MinerFactory from pyasic import get_miner
async def gather_miner_data(): async def gather_miner_data():
miner = await MinerFactory().get_miner("192.168.1.75") miner = await get_miner("192.168.1.75")
miner_data = await miner.get_data() if miner is not None:
print(miner_data) # all data from the dataclass miner_data = await miner.get_data()
print(miner_data.hashrate) # hashrate of the miner in TH/s print(miner_data) # all data from the dataclass
print(miner_data.hashrate) # hashrate of the miner in TH/s
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(gather_miner_data()) 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" - BOSMiner: "API/bosminer.md"
- BTMiner: "API/btminer.md" - BTMiner: "API/btminer.md"
- CGMiner: "API/cgminer.md" - CGMiner: "API/cgminer.md"
- LUXMiner: "API/luxminer.md"
- Unknown: "API/unknown.md" - Unknown: "API/unknown.md"
- Backends: - Backends:
- BMMiner: "miners/backends/bmminer.md" - BMMiner: "miners/backends/bmminer.md"
@@ -27,6 +28,8 @@ nav:
- BFGMiner: "miners/backends/bfgminer.md" - BFGMiner: "miners/backends/bfgminer.md"
- BTMiner: "miners/backends/btminer.md" - BTMiner: "miners/backends/btminer.md"
- CGMiner: "miners/backends/cgminer.md" - CGMiner: "miners/backends/cgminer.md"
- LUXMiner: "miners/backends/luxminer.md"
- VNish: "miners/backends/vnish.md"
- Hiveon: "miners/backends/hiveon.md" - Hiveon: "miners/backends/hiveon.md"
- Classes: - Classes:
- Antminer X3: "miners/antminer/X3.md" - Antminer X3: "miners/antminer/X3.md"
@@ -40,14 +43,15 @@ nav:
- Avalon 8X: "miners/avalonminer/A8X.md" - Avalon 8X: "miners/avalonminer/A8X.md"
- Avalon 9X: "miners/avalonminer/A9X.md" - Avalon 9X: "miners/avalonminer/A9X.md"
- Avalon 10X: "miners/avalonminer/A10X.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 M2X: "miners/whatsminer/M2X.md"
- Whatsminer M3X: "miners/whatsminer/M3X.md" - Whatsminer M3X: "miners/whatsminer/M3X.md"
- Whatsminer M5X: "miners/whatsminer/M5X.md" - Whatsminer M5X: "miners/whatsminer/M5X.md"
- Innosilicon T3X: "miners/innosilicon/T3X.md" - Innosilicon T3X: "miners/innosilicon/T3X.md"
- Innosilicon A10X: "miners/innosilicon/A10X.md" - Innosilicon A10X: "miners/innosilicon/A10X.md"
- Goldshell CKX: "miners/goldshell/CKX.md" - Goldshell X5: "miners/goldshell/X5.md"
- Goldshell HSX: "miners/goldshell/HSX.md" - Goldshell XMax: "miners/goldshell/XMax.md"
- Goldshell KDX: "miners/goldshell/KDX.md"
- Base Miner: "miners/base_miner.md" - Base Miner: "miners/base_miner.md"

View File

@@ -20,7 +20,7 @@ import json
import logging import logging
import re import re
import warnings import warnings
from typing import Union from typing import Tuple, Union
from pyasic.errors import APIError, APIWarning from pyasic.errors import APIError, APIWarning
@@ -32,6 +32,8 @@ class BaseMinerAPI:
# ip address of the miner # ip address of the miner
self.ip = ipaddress.ip_address(ip) self.ip = ipaddress.ip_address(ip)
self.pwd = "admin"
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls is BaseMinerAPI: if cls is BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated") raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
@@ -126,6 +128,18 @@ class BaseMinerAPI:
data["multicommand"] = True data["multicommand"] = True
return data return data
async def _handle_multicommand(self, command: str, allow_warning: bool = True):
try:
data = await self.send_command(command, allow_warning=allow_warning)
if not "+" in command:
return {command: [data]}
return data
except APIError:
if "+" in command:
return {command: [{}] for command in command.split("+")}
return {command: [{}]}
@property @property
def commands(self) -> list: def commands(self) -> list:
return self.get_commands() return self.get_commands()
@@ -169,7 +183,11 @@ If you are sure you want to use this command please use API.send_command("{comma
) )
return return_commands return return_commands
async def _send_bytes(self, data: bytes, timeout: int = 100) -> bytes: async def _send_bytes(
self,
data: bytes,
timeout: int = 100,
) -> bytes:
logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending") logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending")
try: try:
# get reader and writer streams # get reader and writer streams

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import logging import logging
from pyasic.API import APIError, BaseMinerAPI from pyasic.API import APIError, BaseMinerAPI
@@ -56,19 +56,19 @@ class BFGMinerAPI(BaseMinerAPI):
return data return data
async def _x19_multicommand(self, *commands) -> dict: async def _x19_multicommand(self, *commands) -> dict:
data = None tasks = []
try: # send all commands individually
data = {} for cmd in commands:
# send all commands individually tasks.append(
for cmd in commands: asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
) )
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data return data
async def version(self) -> dict: async def version(self) -> dict:

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import logging import logging
from pyasic.API import APIError, BaseMinerAPI from pyasic.API import APIError, BaseMinerAPI
@@ -57,21 +58,19 @@ class BMMinerAPI(BaseMinerAPI):
return data return data
async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict: async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict:
data = None tasks = []
try: # send all commands individually
data = {} for cmd in commands:
# send all commands individually tasks.append(
for cmd in commands: asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
data[cmd] = []
data[cmd].append(
await self.send_command(cmd, allow_warning=allow_warning)
)
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
) )
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data return data
async def version(self) -> dict: async def version(self) -> dict:

View File

@@ -203,27 +203,35 @@ class BTMinerAPI(BaseMinerAPI):
# make sure we can actually run each command, otherwise they will fail # make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands) commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2" # standard multicommand format is "command1+command2"
# commands starting with "get_" aren't supported, but we can fake that # commands starting with "get_" and the "status" command aren't supported, but we can fake that
get_commands_data = {}
tasks = []
for command in list(commands): for command in list(commands):
if command.startswith("get_"): if command.startswith("get_") or command == "status":
commands.remove(command) commands.remove(command)
# send seperately and append later # send seperately and append later
try: tasks.append(
get_commands_data[command] = [ asyncio.create_task(
await self.send_command(command, allow_warning=allow_warning) self._handle_multicommand(command, allow_warning=allow_warning)
] )
except APIError: )
get_commands_data[command] = [{}]
command = "+".join(commands) command = "+".join(commands)
try: tasks.append(
main_data = await self.send_command(command, allow_warning=allow_warning) asyncio.create_task(
except APIError: self._handle_multicommand(command, allow_warning=allow_warning)
main_data = {command: [{}] for command in commands} )
)
all_data = await asyncio.gather(*tasks)
logging.debug(f"{self} - (Multicommand) - Received data") logging.debug(f"{self} - (Multicommand) - Received data")
data = dict(**main_data, **get_commands_data) data = {}
for item in all_data:
data.update(item)
data["multicommand"] = True data["multicommand"] = True
return data return data

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import logging import logging
from pyasic.API import APIError, BaseMinerAPI from pyasic.API import APIError, BaseMinerAPI
@@ -56,19 +56,19 @@ class CGMinerAPI(BaseMinerAPI):
return data return data
async def _x19_multicommand(self, *commands) -> dict: async def _x19_multicommand(self, *commands) -> dict:
data = None tasks = []
try: # send all commands individually
data = {} for cmd in commands:
# send all commands individually tasks.append(
for cmd in commands: asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError:
pass
except Exception as e:
logging.warning(
f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}"
) )
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
return data return data
async def version(self) -> dict: async def version(self) -> dict:

View File

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

View File

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

View File

@@ -149,10 +149,10 @@ class _MinerPhaseBalancer:
not self.miners[data_point.ip]["shutdown"] not self.miners[data_point.ip]["shutdown"]
): ):
# cant do anything with it so need to find a semi-accurate power limit # 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]["max"] = int(data_point.wattage_limit)
self.miners[data_point.ip]["min"] = 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]["max"] = int(data_point.wattage)
self.miners[data_point.ip]["min"] = 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 hashrate = 0
chips = 0 chips = 0
for chipset in board_map[board]: for chipset in board_map[board]:
if hashboard.chip_temp == -1: if hashboard.chip_temp == None:
try: try:
hashboard.board_temp = api_stats["STATS"][1][f"temp{chipset}"] hashboard.board_temp = api_stats["STATS"][1][f"temp{chipset}"]
hashboard.chip_temp = api_stats["STATS"][1][f"temp2_{chipset}"] hashboard.chip_temp = api_stats["STATS"][1][f"temp2_{chipset}"]

View File

@@ -26,11 +26,17 @@ from pyasic.miners.backends.cgminer import CGMiner
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
ANTMINER_MODERN_DATA_LOC = { ANTMINER_MODERN_DATA_LOC = {
"mac": {"cmd": "get_mac", "kwargs": {}}, "mac": {
"cmd": "get_mac",
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
},
"model": {"cmd": "get_model", "kwargs": {}}, "model": {"cmd": "get_model", "kwargs": {}},
"api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}}, "api_ver": {"cmd": "get_api_ver", "kwargs": {"api_version": {"api": "version"}}},
"fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}}, "fw_ver": {"cmd": "get_fw_ver", "kwargs": {"api_version": {"api": "version"}}},
"hostname": {"cmd": "get_hostname", "kwargs": {}}, "hostname": {
"cmd": "get_hostname",
"kwargs": {"web_get_system_info": {"web": "get_system_info"}},
},
"hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}}, "hashrate": {"cmd": "get_hashrate", "kwargs": {"api_summary": {"api": "summary"}}},
"nominal_hashrate": { "nominal_hashrate": {
"cmd": "get_nominal_hashrate", "cmd": "get_nominal_hashrate",
@@ -42,8 +48,11 @@ ANTMINER_MODERN_DATA_LOC = {
"wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}}, "wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}},
"fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}}, "fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}},
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}}, "fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
"errors": {"cmd": "get_errors", "kwargs": {}}, "errors": {"cmd": "get_errors", "kwargs": {"web_summary": {"web": "summary"}}},
"fault_light": {"cmd": "get_fault_light", "kwargs": {}}, "fault_light": {
"cmd": "get_fault_light",
"kwargs": {"web_get_blink_status": {"web": "get_blink_status"}},
},
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}}, "pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
"is_mining": { "is_mining": {
"cmd": "is_mining", "cmd": "is_mining",
@@ -121,21 +130,31 @@ class AntminerModern(BMMiner):
await self.send_config(cfg) await self.send_config(cfg)
return True return True
async def get_hostname(self) -> Union[str, None]: async def get_hostname(self, web_get_system_info: dict = None) -> Union[str, None]:
try: if not web_get_system_info:
data = await self.web.get_system_info() try:
if data: web_get_system_info = await self.web.get_system_info()
return data["hostname"] except APIError:
except KeyError: pass
pass
async def get_mac(self) -> Union[str, None]: if web_get_system_info:
try: try:
data = await self.web.get_system_info() return web_get_system_info["hostname"]
if data: except KeyError:
return data["macaddr"] pass
except KeyError:
pass async def get_mac(self, web_get_system_info: dict = None) -> Union[str, None]:
if not web_get_system_info:
try:
web_get_system_info = await self.web.get_system_info()
except APIError:
pass
if web_get_system_info:
try:
return web_get_system_info["macaddr"]
except KeyError:
pass
try: try:
data = await self.web.get_network_info() data = await self.web.get_network_info()
@@ -144,12 +163,17 @@ class AntminerModern(BMMiner):
except KeyError: except KeyError:
pass pass
async def get_errors(self) -> List[MinerErrorData]: async def get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
errors = [] if not web_summary:
data = await self.web.summary()
if data:
try: try:
for item in data["SUMMARY"][0]["status"]: web_summary = await self.web.summary()
except APIError:
pass
errors = []
if web_summary:
try:
for item in web_summary["SUMMARY"][0]["status"]:
try: try:
if not item["status"] == "s": if not item["status"] == "s":
errors.append(X19Error(item["msg"])) errors.append(X19Error(item["msg"]))
@@ -159,15 +183,21 @@ class AntminerModern(BMMiner):
pass pass
return errors return errors
async def get_fault_light(self) -> bool: async def get_fault_light(self, web_get_blink_status: dict = None) -> bool:
if self.light: if self.light:
return self.light return self.light
try:
data = await self.web.get_blink_status() if not web_get_blink_status:
if data: try:
self.light = data["blink"] web_get_blink_status = await self.web.get_blink_status()
except KeyError: except APIError:
pass pass
if web_get_blink_status:
try:
self.light = web_get_blink_status["blink"]
except KeyError:
pass
return self.light return self.light
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]: async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:

View File

@@ -494,7 +494,7 @@ class BOSMiner(BaseMiner):
if graphql_version: if graphql_version:
try: try:
fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"] fw_ver = graphql_version["data"]["bos"]["info"]["version"]["full"]
except KeyError: except (KeyError, TypeError):
pass pass
if not fw_ver: if not fw_ver:
@@ -523,7 +523,7 @@ class BOSMiner(BaseMiner):
try: try:
hostname = graphql_hostname["data"]["bos"]["hostname"] hostname = graphql_hostname["data"]["bos"]["hostname"]
return hostname return hostname
except KeyError: except (TypeError, KeyError):
pass pass
try: try:
@@ -563,7 +563,7 @@ class BOSMiner(BaseMiner):
), ),
2, 2,
) )
except (KeyError, IndexError, ValueError): except (LookupError, ValueError, TypeError):
pass pass
# get hr from API # get hr from API
@@ -617,7 +617,7 @@ class BOSMiner(BaseMiner):
boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][ boards = graphql_boards["data"]["bosminer"]["info"]["workSolver"][
"childSolvers" "childSolvers"
] ]
except (KeyError, IndexError): except (TypeError, LookupError):
boards = None boards = None
if boards: if boards:
@@ -732,7 +732,7 @@ class BOSMiner(BaseMiner):
return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][ return graphql_wattage["data"]["bosminer"]["info"]["workSolver"][
"power" "power"
]["approxConsumptionW"] ]["approxConsumptionW"]
except (KeyError, TypeError): except (LookupError, TypeError):
pass pass
if not api_tunerstatus: if not api_tunerstatus:
@@ -765,7 +765,7 @@ class BOSMiner(BaseMiner):
return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][ return graphql_wattage_limit["data"]["bosminer"]["info"]["workSolver"][
"power" "power"
]["limitW"] ]["limitW"]
except (KeyError, TypeError): except (LookupError, TypeError):
pass pass
if not api_tunerstatus: if not api_tunerstatus:
@@ -801,7 +801,7 @@ class BOSMiner(BaseMiner):
] ]
) )
) )
except KeyError: except (LookupError, TypeError):
pass pass
return fans return fans
@@ -941,7 +941,7 @@ class BOSMiner(BaseMiner):
boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][ boards = graphql_errors["data"]["bosminer"]["info"]["workSolver"][
"childSolvers" "childSolvers"
] ]
except (KeyError, IndexError): except (LookupError, TypeError):
boards = None boards = None
if boards: if boards:
@@ -1034,17 +1034,20 @@ class BOSMiner(BaseMiner):
try: try:
self.light = graphql_fault_light["data"]["bos"]["faultLight"] self.light = graphql_fault_light["data"]["bos"]["faultLight"]
return self.light return self.light
except (TypeError, KeyError, ValueError, IndexError): except (TypeError, ValueError, LookupError):
pass pass
# get light via ssh if that fails (10x slower) # get light via ssh if that fails (10x slower)
data = ( try:
await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off") data = (
).strip() await self.send_ssh_command("cat /sys/class/leds/'Red LED'/delay_off")
self.light = False ).strip()
if data == "50": self.light = False
self.light = True if data == "50":
return self.light self.light = True
return self.light
except TypeError:
return self.light
async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]: async def get_nominal_hashrate(self, api_devs: dict = None) -> Optional[float]:
if not api_devs: if not api_devs:
@@ -1075,7 +1078,9 @@ class BOSMiner(BaseMiner):
async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]: async def is_mining(self, api_devdetails: dict = None) -> Optional[bool]:
if not api_devdetails: if not api_devdetails:
try: try:
api_devdetails = await self.api.send_command("devdetails", ignore_errors=True, allow_warning=False) api_devdetails = await self.api.send_command(
"devdetails", ignore_errors=True, allow_warning=False
)
except APIError: except APIError:
pass pass

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import ipaddress import ipaddress
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@@ -72,6 +72,52 @@ class BaseMiner(ABC):
def __eq__(self, other): def __eq__(self, other):
return ipaddress.ip_address(self.ip) == ipaddress.ip_address(other.ip) 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: async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection""" """Create a new asyncssh connection"""
try: try:
@@ -367,59 +413,45 @@ class BaseMiner(ABC):
async def _get_data(self, allow_warning: bool, data_to_get: list = None) -> dict: async def _get_data(self, allow_warning: bool, data_to_get: list = None) -> dict:
if not data_to_get: if not data_to_get:
# everything # everything
data_to_get = [ data_to_get = list(self.data_locations.keys())
"mac",
"model", api_multicommand = set()
"api_ver", web_multicommand = set()
"fw_ver",
"hostname",
"hashrate",
"nominal_hashrate",
"hashboards",
"env_temp",
"wattage",
"wattage_limit",
"fans",
"fan_psu",
"errors",
"fault_light",
"pools",
"is_mining",
"uptime",
]
api_multicommand = []
web_multicommand = []
for data_name in data_to_get: for data_name in data_to_get:
try: try:
fn_args = self.data_locations[data_name]["kwargs"] fn_args = self.data_locations[data_name]["kwargs"]
for arg_name in fn_args: for arg_name in fn_args:
if fn_args[arg_name].get("api"): if fn_args[arg_name].get("api"):
api_multicommand.append(fn_args[arg_name]["api"]) api_multicommand.add(fn_args[arg_name]["api"])
if fn_args[arg_name].get("web"): if fn_args[arg_name].get("web"):
web_multicommand.append(fn_args[arg_name]["web"]) web_multicommand.add(fn_args[arg_name]["web"])
except KeyError as e: except KeyError as e:
logger.error(e, data_name) logger.error(e, data_name)
continue continue
api_multicommand = list(set(api_multicommand))
_web_multicommand = web_multicommand
for item in web_multicommand:
if item not in _web_multicommand:
_web_multicommand.append(item)
web_multicommand = _web_multicommand
if len(api_multicommand) > 0: if len(api_multicommand) > 0:
api_command_data = await self.api.multicommand( api_command_task = asyncio.create_task(
*api_multicommand, allow_warning=allow_warning self.api.multicommand(*api_multicommand, allow_warning=allow_warning)
) )
else: else:
api_command_data = {} api_command_task = asyncio.sleep(0)
if len(web_multicommand) > 0: if len(web_multicommand) > 0:
web_command_data = await self.web.multicommand( web_command_task = asyncio.create_task(
*web_multicommand, allow_warning=allow_warning self.web.multicommand(*web_multicommand, allow_warning=allow_warning)
) )
else: else:
web_command_task = asyncio.sleep(0)
from datetime import datetime
web_command_data = await web_command_task
if web_command_data is None:
web_command_data = {} web_command_data = {}
api_command_data = await api_command_task
if api_command_data is None:
api_command_data = {}
miner_data = {} miner_data = {}
for data_name in data_to_get: for data_name in data_to_get:
@@ -446,7 +478,7 @@ class BaseMiner(ABC):
args_to_send[arg_name] = web_command_data args_to_send[arg_name] = web_command_data
except LookupError: except LookupError:
args_to_send[arg_name] = None args_to_send[arg_name] = None
except LookupError as e: except LookupError:
continue continue
function = getattr(self, self.data_locations[data_name]["cmd"]) function = getattr(self, self.data_locations[data_name]["cmd"])

View File

@@ -22,7 +22,7 @@ import json
import re import re
from typing import Callable, List, Optional, Tuple, Union from typing import Callable, List, Optional, Tuple, Union
import aiohttp import httpx
from pyasic.logger import logger from pyasic.logger import logger
from pyasic.miners.antminer import * from pyasic.miners.antminer import *
@@ -319,6 +319,7 @@ MINER_CLASSES = {
"ANTMINER S19J": BOSMinerS19j, "ANTMINER S19J": BOSMinerS19j,
"ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC, "ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC,
"ANTMINER S19J PRO": BOSMinerS19jPro, "ANTMINER S19J PRO": BOSMinerS19jPro,
"ANTMINER S19J PRO NOPIC": BOSMinerS19jPro,
"ANTMINER T19": BOSMinerT19, "ANTMINER T19": BOSMinerT19,
}, },
MinerTypes.VNISH: { MinerTypes.VNISH: {
@@ -455,7 +456,7 @@ class MinerFactory:
async def _get_miner_web(self, ip: str): async def _get_miner_web(self, ip: str):
urls = [f"http://{ip}/", f"https://{ip}/"] urls = [f"http://{ip}/", f"https://{ip}/"]
async with aiohttp.ClientSession() as session: async with httpx.AsyncClient(verify=False) as session:
tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls] tasks = [asyncio.create_task(self._web_ping(session, url)) for url in urls]
text, resp = await concurrent_get_first_result( text, resp = await concurrent_get_first_result(
@@ -466,22 +467,22 @@ class MinerFactory:
@staticmethod @staticmethod
async def _web_ping( async def _web_ping(
session: aiohttp.ClientSession, url: str session: httpx.AsyncClient, url: str
) -> Tuple[Optional[str], Optional[aiohttp.ClientResponse]]: ) -> Tuple[Optional[str], Optional[httpx.Response]]:
try: try:
resp = await session.get(url, allow_redirects=False) resp = await session.get(url, follow_redirects=False)
return await resp.text(), resp return resp.text, resp
except (aiohttp.ClientError, asyncio.TimeoutError): except (httpx.HTTPError, asyncio.TimeoutError):
pass pass
return None, None return None, None
@staticmethod @staticmethod
def _parse_web_type(web_text: str, web_resp: aiohttp.ClientResponse) -> MinerTypes: def _parse_web_type(web_text: str, web_resp: httpx.Response) -> MinerTypes:
if web_resp.status == 401 and 'realm="antMiner' in web_resp.headers.get( if web_resp.status_code == 401 and 'realm="antMiner' in web_resp.headers.get(
"www-authenticate", "" "www-authenticate", ""
): ):
return MinerTypes.ANTMINER return MinerTypes.ANTMINER
if web_resp.status == 307 and "https://" in web_resp.headers.get( if web_resp.status_code == 307 and "https://" in web_resp.headers.get(
"location", "" "location", ""
): ):
return MinerTypes.WHATSMINER return MinerTypes.WHATSMINER
@@ -576,26 +577,26 @@ class MinerFactory:
self, self,
ip: Union[ipaddress.ip_address, str], ip: Union[ipaddress.ip_address, str],
location: str, location: str,
auth: Optional[aiohttp.BasicAuth] = None, auth: Optional[httpx.DigestAuth] = None,
) -> Optional[dict]: ) -> Optional[dict]:
async with aiohttp.ClientSession() as session: async with httpx.AsyncClient(verify=False) as session:
try: try:
data = await session.get( data = await session.get(
f"http://{str(ip)}{location}", f"http://{str(ip)}{location}",
auth=auth, auth=auth,
timeout=30, timeout=30,
) )
except (aiohttp.ClientError, asyncio.TimeoutError): except (httpx.HTTPError, asyncio.TimeoutError):
logger.info(f"{ip}: Web command timeout.") logger.info(f"{ip}: Web command timeout.")
return return
if data is None: if data is None:
return return
try: try:
json_data = await data.json() json_data = data.json()
except (aiohttp.ContentTypeError, asyncio.TimeoutError): except (json.JSONDecodeError, asyncio.TimeoutError):
try: try:
return json.loads(await data.text()) return json.loads(data.text)
except (json.JSONDecodeError, aiohttp.ClientError): except (json.JSONDecodeError, httpx.HTTPError):
return return
else: else:
return json_data return json_data
@@ -691,6 +692,28 @@ class MinerFactory:
return UnknownMiner(str(ip)) return UnknownMiner(str(ip))
async def get_miner_model_antminer(self, ip: str): async def get_miner_model_antminer(self, ip: str):
tasks = [
asyncio.create_task(self._get_model_antminer_web(ip)),
asyncio.create_task(self._get_model_antminer_sock(ip)),
]
return await concurrent_get_first_result(tasks, lambda x: x is not None)
async def _get_model_antminer_web(self, ip: str):
# last resort, this is slow
auth = httpx.DigestAuth("root", "root")
web_json_data = await self.send_web_command(
ip, "/cgi-bin/get_system_info.cgi", auth=auth
)
try:
miner_model = web_json_data["minertype"]
return miner_model
except (TypeError, LookupError):
pass
async def _get_model_antminer_sock(self, ip: str):
sock_json_data = await self.send_api_command(ip, "version") sock_json_data = await self.send_api_command(ip, "version")
try: try:
miner_model = sock_json_data["VERSION"][0]["Type"] miner_model = sock_json_data["VERSION"][0]["Type"]
@@ -715,19 +738,6 @@ class MinerFactory:
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass
# last resort, this is slow
auth = aiohttp.BasicAuth("root", "root")
web_json_data = await self.send_web_command(
ip, "/cgi-bin/get_system_info.cgi", auth=auth
)
try:
miner_model = web_json_data["minertype"]
return miner_model
except (TypeError, LookupError):
pass
async def get_miner_model_goldshell(self, ip: str): async def get_miner_model_goldshell(self, ip: str):
json_data = await self.send_web_command(ip, "/mcb/status") json_data = await self.send_web_command(ip, "/mcb/status")
@@ -760,7 +770,7 @@ class MinerFactory:
async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]: async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]:
try: try:
async with aiohttp.ClientSession() as session: async with httpx.AsyncClient(verify=False) as session:
auth_req = await session.post( auth_req = await session.post(
f"http://{ip}/api/auth", f"http://{ip}/api/auth",
data={"username": "admin", "password": "admin"}, data={"username": "admin", "password": "admin"},
@@ -775,7 +785,7 @@ class MinerFactory:
) )
).json() ).json()
return web_data["type"] return web_data["type"]
except (aiohttp.ClientError, LookupError): except (httpx.HTTPError, LookupError):
pass pass
async def get_miner_model_braiins_os(self, ip: str) -> Optional[str]: async def get_miner_model_braiins_os(self, ip: str) -> Optional[str]:
@@ -790,16 +800,16 @@ class MinerFactory:
pass pass
try: try:
async with aiohttp.ClientSession() as session: async with httpx.AsyncClient(verify=False) as session:
d = await session.post( d = await session.post(
f"http://{ip}/graphql", f"http://{ip}/graphql",
json={"query": "{bosminer {info{modelName}}}"}, json={"query": "{bosminer {info{modelName}}}"},
) )
if d.status == 200: if d.status_code == 200:
json_data = await d.json() json_data = await d.json()
miner_model = json_data["data"]["bosminer"]["info"]["modelName"] miner_model = json_data["data"]["bosminer"]["info"]["modelName"]
return miner_model return miner_model
except (aiohttp.ClientError, LookupError): except (httpx.HTTPError, LookupError):
pass pass
async def get_miner_model_vnish(self, ip: str) -> Optional[str]: async def get_miner_model_vnish(self, ip: str) -> Optional[str]:
@@ -813,6 +823,9 @@ class MinerFactory:
if "(88)" in miner_model: if "(88)" in miner_model:
miner_model = miner_model.replace("(88)", "NOPIC") miner_model = miner_model.replace("(88)", "NOPIC")
if " AML" in miner_model:
miner_model = miner_model.replace(" AML", "")
return miner_model return miner_model
except (TypeError, LookupError): except (TypeError, LookupError):
pass pass

View File

@@ -87,10 +87,7 @@ class M50VH60(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__(ip, api_ver) super().__init__(ip, api_ver)
self.ip = ip self.ip = ip
self.model = "M50 VH60" self.model = "M50 VH60"
self.nominal_chips = 0 self.nominal_chips = 84
warnings.warn(
"Unknown chip count for miner type M50 VH60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.fan_count = 2 self.fan_count = 2

View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and - # See the License for the specific language governing permissions and -
# limitations under the License. - # limitations under the License. -
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import asyncio
import json import json
from typing import Union from typing import Union
@@ -56,25 +57,37 @@ class AntminerModernWebAPI(BaseWebAPI):
async def multicommand( async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict: ) -> dict:
data = {k: None for k in commands}
data["multicommand"] = True
auth = httpx.DigestAuth(self.username, self.pwd)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for command in commands: tasks = [
try: asyncio.create_task(self._handle_multicommand(client, command))
url = f"http://{self.ip}/cgi-bin/{command}.cgi" for command in commands
ret = await client.get(url, auth=auth) ]
except httpx.HTTPError: all_data = await asyncio.gather(*tasks)
pass
else: data = {}
if ret.status_code == 200: for item in all_data:
try: data.update(item)
json_data = ret.json()
data[command] = json_data data["multicommand"] = True
except json.decoder.JSONDecodeError:
pass
return data return data
async def _handle_multicommand(self, client: httpx.AsyncClient, command: str):
auth = httpx.DigestAuth(self.username, self.pwd)
try:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
ret = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if ret.status_code == 200:
try:
json_data = ret.json()
return {command: json_data}
except json.decoder.JSONDecodeError:
pass
return {command: {}}
async def get_miner_conf(self) -> dict: async def get_miner_conf(self) -> dict:
return await self.send_command("get_miner_conf") return await self.send_command("get_miner_conf")

View File

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

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pyasic" name = "pyasic"
version = "0.36.6" version = "0.37.3"
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"
@@ -14,7 +14,6 @@ httpx = "^0.24.0"
passlib = "^1.7.4" passlib = "^1.7.4"
pyaml = "^23.5.9" pyaml = "^23.5.9"
toml = "^0.10.2" toml = "^0.10.2"
aiohttp = "^3.8.4"
[tool.poetry.group.dev] [tool.poetry.group.dev]
optional = true optional = true