Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d21f150ce | ||
|
|
7c0dfc49dd | ||
|
|
269b13f6c1 | ||
|
|
a9bb7d2e5a | ||
|
|
11295f27a7 | ||
|
|
55aa3dd85b | ||
|
|
20272d4360 | ||
|
|
623dc92ef2 | ||
|
|
2d59394b1e | ||
|
|
26c2095ff1 | ||
|
|
ec7d241caa | ||
|
|
d0432ed1aa | ||
|
|
8c5503d002 | ||
|
|
6d6f950c95 | ||
|
|
30745e54ba | ||
|
|
c3fd94e79e | ||
|
|
2924a8d67b | ||
|
|
9f4c4bb9cf | ||
|
|
3d6eebf06e | ||
|
|
b3d9b6ff7e | ||
|
|
60facacc48 | ||
|
|
b8a6063838 | ||
|
|
bcba2be524 | ||
|
|
f7187d2017 | ||
|
|
d91b7c4406 | ||
|
|
248a7e6d69 | ||
|
|
4f2c3e772a | ||
|
|
95f7146eef | ||
|
|
9d5d19cc6b | ||
|
|
cc38129571 | ||
|
|
3dfd9f237d | ||
|
|
f3fe478dbb | ||
|
|
e10f32ae3d | ||
|
|
4e0924aa0e | ||
|
|
d0d3fd3117 | ||
|
|
4de950d8f4 | ||
|
|
03f2a1f9ba | ||
|
|
2653db90e3 | ||
|
|
ddc8c53eb9 |
@@ -20,7 +20,7 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
from typing import Union
|
||||
from typing import Tuple, Union
|
||||
|
||||
from pyasic.errors import APIError, APIWarning
|
||||
|
||||
@@ -128,6 +128,18 @@ class BaseMinerAPI:
|
||||
data["multicommand"] = True
|
||||
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
|
||||
def commands(self) -> list:
|
||||
return self.get_commands()
|
||||
@@ -171,7 +183,11 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
)
|
||||
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")
|
||||
try:
|
||||
# get reader and writer streams
|
||||
@@ -242,9 +258,12 @@ If you are sure you want to use this command please use API.send_command("{comma
|
||||
return False, data["Msg"]
|
||||
else:
|
||||
# make sure the command succeeded
|
||||
if type(data["STATUS"]) == str:
|
||||
if isinstance(data["STATUS"], str):
|
||||
if data["STATUS"] in ["RESTART"]:
|
||||
return True, None
|
||||
elif isinstance(data["STATUS"], dict):
|
||||
if data["STATUS"].get("STATUS") in ["S", "I"]:
|
||||
return True, None
|
||||
elif data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||
# this is an error
|
||||
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from pyasic.API import APIError, BaseMinerAPI
|
||||
@@ -56,19 +56,19 @@ class BFGMinerAPI(BaseMinerAPI):
|
||||
return data
|
||||
|
||||
async def _x19_multicommand(self, *commands) -> dict:
|
||||
data = None
|
||||
try:
|
||||
data = {}
|
||||
# send all commands individually
|
||||
for cmd in commands:
|
||||
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}"
|
||||
tasks = []
|
||||
# send all commands individually
|
||||
for cmd in commands:
|
||||
tasks.append(
|
||||
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
|
||||
)
|
||||
|
||||
all_data = await asyncio.gather(*tasks)
|
||||
|
||||
data = {}
|
||||
for item in all_data:
|
||||
data.update(item)
|
||||
|
||||
return data
|
||||
|
||||
async def version(self) -> dict:
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from pyasic.API import APIError, BaseMinerAPI
|
||||
@@ -57,21 +58,19 @@ class BMMinerAPI(BaseMinerAPI):
|
||||
return data
|
||||
|
||||
async def _x19_multicommand(self, *commands, allow_warning: bool = True) -> dict:
|
||||
data = None
|
||||
try:
|
||||
data = {}
|
||||
# send all commands individually
|
||||
for cmd in commands:
|
||||
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}"
|
||||
tasks = []
|
||||
# send all commands individually
|
||||
for cmd in commands:
|
||||
tasks.append(
|
||||
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
|
||||
)
|
||||
|
||||
all_data = await asyncio.gather(*tasks)
|
||||
|
||||
data = {}
|
||||
for item in all_data:
|
||||
data.update(item)
|
||||
|
||||
return data
|
||||
|
||||
async def version(self) -> dict:
|
||||
|
||||
@@ -22,7 +22,7 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Union
|
||||
from typing import Literal, Union
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from passlib.handlers.md5_crypt import md5_crypt
|
||||
@@ -40,6 +40,12 @@ from pyasic.settings import PyasicSettings
|
||||
# you change the password, you can pass that to this class as pwd,
|
||||
# or add it as the Whatsminer_pwd in the settings.toml file.
|
||||
|
||||
PrePowerOnMessage = Union[
|
||||
Literal["wait for adjust temp"],
|
||||
Literal["adjust complete"],
|
||||
Literal["adjust continue"],
|
||||
]
|
||||
|
||||
|
||||
def _crypt(word: str, salt: str) -> str:
|
||||
"""Encrypts a word with a salt, using a standard salt format.
|
||||
@@ -203,27 +209,35 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
# make sure we can actually run each command, otherwise they will fail
|
||||
commands = self._check_commands(*commands)
|
||||
# standard multicommand format is "command1+command2"
|
||||
# commands starting with "get_" aren't supported, but we can fake that
|
||||
get_commands_data = {}
|
||||
# commands starting with "get_" and the "status" command aren't supported, but we can fake that
|
||||
|
||||
tasks = []
|
||||
|
||||
for command in list(commands):
|
||||
if command.startswith("get_"):
|
||||
if command.startswith("get_") or command == "status":
|
||||
commands.remove(command)
|
||||
# send seperately and append later
|
||||
try:
|
||||
get_commands_data[command] = [
|
||||
await self.send_command(command, allow_warning=allow_warning)
|
||||
]
|
||||
except APIError:
|
||||
get_commands_data[command] = [{}]
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
self._handle_multicommand(command, allow_warning=allow_warning)
|
||||
)
|
||||
)
|
||||
|
||||
command = "+".join(commands)
|
||||
try:
|
||||
main_data = await self.send_command(command, allow_warning=allow_warning)
|
||||
except APIError:
|
||||
main_data = {command: [{}] for command in commands}
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
self._handle_multicommand(command, allow_warning=allow_warning)
|
||||
)
|
||||
)
|
||||
|
||||
all_data = await asyncio.gather(*tasks)
|
||||
|
||||
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
|
||||
return data
|
||||
|
||||
@@ -685,7 +699,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
)
|
||||
return await self.send_privileged_command("set_power_pct", percent=str(percent))
|
||||
|
||||
async def pre_power_on(self, complete: bool, msg: str) -> dict:
|
||||
async def pre_power_on(self, complete: bool, msg: PrePowerOnMessage) -> dict:
|
||||
"""Configure or check status of pre power on.
|
||||
|
||||
<details>
|
||||
@@ -705,7 +719,7 @@ class BTMinerAPI(BaseMinerAPI):
|
||||
</details>
|
||||
"""
|
||||
|
||||
if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue":
|
||||
if msg not in ("wait for adjust temp", "adjust complete", "adjust continue"):
|
||||
raise APIError(
|
||||
"Message is incorrect, please choose one of "
|
||||
'["wait for adjust temp", '
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from pyasic.API import APIError, BaseMinerAPI
|
||||
@@ -56,19 +56,19 @@ class CGMinerAPI(BaseMinerAPI):
|
||||
return data
|
||||
|
||||
async def _x19_multicommand(self, *commands) -> dict:
|
||||
data = None
|
||||
try:
|
||||
data = {}
|
||||
# send all commands individually
|
||||
for cmd in commands:
|
||||
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}"
|
||||
tasks = []
|
||||
# send all commands individually
|
||||
for cmd in commands:
|
||||
tasks.append(
|
||||
asyncio.create_task(self._handle_multicommand(cmd, allow_warning=True))
|
||||
)
|
||||
|
||||
all_data = await asyncio.gather(*tasks)
|
||||
|
||||
data = {}
|
||||
for item in all_data:
|
||||
data.update(item)
|
||||
|
||||
return data
|
||||
|
||||
async def version(self) -> dict:
|
||||
|
||||
@@ -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, Any
|
||||
from typing import Any, List, Union
|
||||
|
||||
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
|
||||
|
||||
@@ -338,13 +338,16 @@ class MinerData:
|
||||
pass
|
||||
|
||||
def asdict(self) -> dict:
|
||||
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
|
||||
return asdict(self)
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
"""Get this dataclass as a dictionary.
|
||||
|
||||
Returns:
|
||||
A dictionary version of this class.
|
||||
"""
|
||||
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
|
||||
return asdict(self)
|
||||
return self.asdict()
|
||||
|
||||
def as_json(self) -> str:
|
||||
"""Get this dataclass as JSON.
|
||||
@@ -411,8 +414,12 @@ class MinerData:
|
||||
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.get('hashrate', 0.0)}")
|
||||
field_data.append(f"hashboard_{idx+1}_temperature={item.get('temp', 0)}")
|
||||
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.get('chip_temp', 0)}"
|
||||
)
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
|
||||
C_N_CODES = ["52", "53", "54", "55", "56"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class WhatsminerError:
|
||||
@@ -37,10 +35,8 @@ class WhatsminerError:
|
||||
|
||||
@property
|
||||
def error_message(self): # noqa - Skip PyCharm inspection
|
||||
if len(str(self.error_code)) > 3 and str(self.error_code)[:2] in C_N_CODES:
|
||||
# 55 error code base has chip numbers, so the format is
|
||||
# 55 -> board num len 1 -> chip num len 3
|
||||
err_type = 55
|
||||
if len(str(self.error_code)) == 6 and not str(self.error_code)[:1] == "1":
|
||||
err_type = int(str(self.error_code)[:2])
|
||||
err_subtype = int(str(self.error_code)[2:3])
|
||||
err_value = int(str(self.error_code)[3:])
|
||||
else:
|
||||
@@ -88,7 +84,9 @@ class WhatsminerError:
|
||||
|
||||
ERROR_CODES = {
|
||||
1: { # Fan error
|
||||
0: {0: "Fan unknown."},
|
||||
0: {
|
||||
0: "Fan unknown.",
|
||||
},
|
||||
1: { # Fan speed error of 1000+
|
||||
0: "Intake fan speed error.",
|
||||
1: "Exhaust fan speed error.",
|
||||
@@ -101,7 +99,9 @@ ERROR_CODES = {
|
||||
0: "Intake fan speed error. Fan speed deviates by more than 3000.",
|
||||
1: "Exhaust fan speed error. Fan speed deviates by more than 3000.",
|
||||
},
|
||||
4: {0: "Fan speed too high."}, # High speed
|
||||
4: {
|
||||
0: "Fan speed too high.",
|
||||
}, # High speed
|
||||
},
|
||||
2: { # Power error
|
||||
0: {
|
||||
@@ -126,6 +126,7 @@ ERROR_CODES = {
|
||||
6: "Power remained unchanged for a long time.",
|
||||
7: "Power set enable error.",
|
||||
8: "Power input voltage is lower than 230V for high power mode.",
|
||||
9: "Power input current is incorrect.",
|
||||
},
|
||||
3: {
|
||||
3: "Power output high temperature protection error.",
|
||||
@@ -159,6 +160,8 @@ ERROR_CODES = {
|
||||
6: {
|
||||
3: "Power communication warning.",
|
||||
4: "Power communication error.",
|
||||
5: "Power unknown error.",
|
||||
6: "Power unknown error.",
|
||||
7: "Power watchdog protection.",
|
||||
8: "Power output high current protection.",
|
||||
9: "Power input high current protection.",
|
||||
@@ -170,57 +173,134 @@ ERROR_CODES = {
|
||||
3: "Power input too high warning.",
|
||||
4: "Power fan warning.",
|
||||
5: "Power high temperature warning.",
|
||||
6: "Power unknown error.",
|
||||
7: "Power unknown error.",
|
||||
8: "Power unknown error.",
|
||||
9: "Power unknown error.",
|
||||
},
|
||||
8: {
|
||||
0: "Power unknown error.",
|
||||
1: "Power vendor status 1 bit 0 error.",
|
||||
2: "Power vendor status 1 bit 1 error.",
|
||||
3: "Power vendor status 1 bit 2 error.",
|
||||
4: "Power vendor status 1 bit 3 error.",
|
||||
5: "Power vendor status 1 bit 4 error.",
|
||||
6: "Power vendor status 1 bit 5 error.",
|
||||
7: "Power vendor status 1 bit 6 error.",
|
||||
8: "Power vendor status 1 bit 7 error.",
|
||||
9: "Power vendor status 2 bit 0 error.",
|
||||
},
|
||||
9: {
|
||||
0: "Power vendor status 2 bit 1 error.",
|
||||
1: "Power vendor status 2 bit 2 error.",
|
||||
2: "Power vendor status 2 bit 3 error.",
|
||||
3: "Power vendor status 2 bit 4 error.",
|
||||
4: "Power vendor status 2 bit 5 error.",
|
||||
5: "Power vendor status 2 bit 6 error.",
|
||||
6: "Power vendor status 2 bit 7 error.",
|
||||
},
|
||||
},
|
||||
3: { # temperature error
|
||||
0: { # sensor detection error
|
||||
"n": "Slot {n} temperature sensor detection error."
|
||||
"n": "Slot {n} temperature sensor detection error.",
|
||||
},
|
||||
2: { # temperature reading error
|
||||
"n": "Slot {n} temperature reading error.",
|
||||
9: "Control board temperature sensor communication error.",
|
||||
},
|
||||
5: {"n": "Slot {n} temperature protecting."}, # temperature protection
|
||||
6: {0: "Hashboard high temperature error."}, # high temp
|
||||
5: {
|
||||
"n": "Slot {n} temperature protecting.",
|
||||
}, # temperature protection
|
||||
6: {
|
||||
0: "Hashboard high temperature error.",
|
||||
1: "Hashboard high temperature error.",
|
||||
2: "Hashboard high temperature error.",
|
||||
3: "Hashboard high temperature error.",
|
||||
}, # high temp
|
||||
7: {
|
||||
0: "The environment temperature fluctuates too much.",
|
||||
}, # env temp
|
||||
8: {
|
||||
0: "Humidity sensor not found.",
|
||||
1: "Humidity sensor read error.",
|
||||
2: "Humidity sensor read error.",
|
||||
3: "Humidity sensor protecting.",
|
||||
},
|
||||
}, # humidity
|
||||
},
|
||||
4: { # EEPROM error
|
||||
0: {0: "Eeprom unknown error."},
|
||||
1: {"n": "Slot {n} eeprom detection error."}, # EEPROM detection error
|
||||
2: {"n": "Slot {n} eeprom parsing error."}, # EEPROM parsing error
|
||||
3: {"n": "Slot {n} chip bin type error."}, # chip bin error
|
||||
4: {"n": "Slot {n} eeprom chip number X error."}, # EEPROM chip number error
|
||||
5: {"n": "Slot {n} eeprom xfer error."}, # EEPROM xfer error
|
||||
0: {
|
||||
0: "Eeprom unknown error.",
|
||||
},
|
||||
1: {
|
||||
"n": "Slot {n} eeprom detection error.",
|
||||
}, # EEPROM detection error
|
||||
2: {
|
||||
"n": "Slot {n} eeprom parsing error.",
|
||||
}, # EEPROM parsing error
|
||||
3: {
|
||||
"n": "Slot {n} chip bin type error.",
|
||||
}, # chip bin error
|
||||
4: {
|
||||
"n": "Slot {n} eeprom chip number X error.",
|
||||
}, # EEPROM chip number error
|
||||
5: {
|
||||
"n": "Slot {n} eeprom xfer error.",
|
||||
}, # EEPROM xfer error
|
||||
},
|
||||
5: { # hashboard error
|
||||
0: {0: "Board unknown error."},
|
||||
1: {"n": "Slot {n} miner type error."}, # board miner type error
|
||||
2: {"n": "Slot {n} bin type error."}, # chip bin type error
|
||||
3: {"n": "Slot {n} not found."}, # board not found error
|
||||
4: {"n": "Slot {n} error reading chip id."}, # reading chip id error
|
||||
5: {"n": "Slot {n} has bad chips."}, # board has bad chips error
|
||||
6: {"n": "Slot {n} loss of balance error."}, # loss of balance error
|
||||
7: {"n": "Slot {n} xfer error chip."}, # xfer error
|
||||
8: {"n": "Slot {n} reset error."}, # reset error
|
||||
9: {"n": "Slot {n} frequency too low."}, # freq error
|
||||
0: {
|
||||
0: "Board unknown error.",
|
||||
},
|
||||
1: {
|
||||
"n": "Slot {n} miner type error.",
|
||||
}, # board miner type error
|
||||
2: {
|
||||
"n": "Slot {n} bin type error.",
|
||||
}, # chip bin type error
|
||||
3: {
|
||||
"n": "Slot {n} not found.",
|
||||
}, # board not found error
|
||||
4: {
|
||||
"n": "Slot {n} error reading chip id.",
|
||||
}, # reading chip id error
|
||||
5: {
|
||||
"n": "Slot {n} has bad chips.",
|
||||
}, # board has bad chips error
|
||||
6: {
|
||||
"n": "Slot {n} loss of balance error.",
|
||||
}, # loss of balance error
|
||||
7: {
|
||||
"n": "Slot {n} xfer error chip.",
|
||||
}, # xfer error
|
||||
8: {
|
||||
"n": "Slot {n} reset error.",
|
||||
}, # reset error
|
||||
9: {
|
||||
"n": "Slot {n} frequency too low.",
|
||||
}, # freq error
|
||||
},
|
||||
6: { # env temp error
|
||||
0: {0: "Environment temperature is too high."}, # normal env temp error
|
||||
0: {
|
||||
0: "Environment temperature is too high.",
|
||||
}, # normal env temp error
|
||||
1: { # high power env temp error
|
||||
0: "Environment temperature is too high for high performance mode."
|
||||
0: "Environment temperature is too high for high performance mode.",
|
||||
},
|
||||
},
|
||||
7: { # control board error
|
||||
0: {0: "MAC address invalid", 1: "Control board no support chip."},
|
||||
0: {
|
||||
0: "MAC address invalid",
|
||||
1: "Control board no support chip.",
|
||||
},
|
||||
1: {
|
||||
0: "Control board rebooted as an exception.",
|
||||
1: "Control board rebooted as exception and cpufreq reduced, please upgrade the firmware",
|
||||
2: "Control board rebooted as an exception.",
|
||||
3: "The network is unstable, change time.",
|
||||
4: "Unknown error.",
|
||||
},
|
||||
2: {
|
||||
"n": "Control board slot {n} frame error.",
|
||||
},
|
||||
},
|
||||
8: { # checksum error
|
||||
@@ -228,63 +308,152 @@ ERROR_CODES = {
|
||||
0: "CGMiner checksum error.",
|
||||
1: "System monitor checksum error.",
|
||||
2: "Remote daemon checksum error.",
|
||||
}
|
||||
},
|
||||
1: {0: "Air to liquid PCB serial # does not match."},
|
||||
},
|
||||
9: {0: {1: "Power rate error."}}, # power rate error
|
||||
9: {
|
||||
0: {0: "Unknown error.", 1: "Power rate error.", 2: "Unknown error."}
|
||||
}, # power rate error
|
||||
20: { # pool error
|
||||
1: {0: "All pools are disabled."}, # all disabled error
|
||||
2: {"n": "Pool {n} connection failed."}, # pool connection failed error
|
||||
3: {0: "High rejection rate on pool."}, # rejection rate error
|
||||
0: {
|
||||
0: "No pool information configured.",
|
||||
},
|
||||
1: {
|
||||
0: "All pools are disabled.",
|
||||
}, # all disabled error
|
||||
2: {
|
||||
"n": "Pool {n} connection failed.",
|
||||
}, # pool connection failed error
|
||||
3: {
|
||||
0: "High rejection rate on pool.",
|
||||
}, # rejection rate error
|
||||
4: { # asicboost not supported error
|
||||
0: "The pool does not support asicboost mode."
|
||||
0: "The pool does not support asicboost mode.",
|
||||
},
|
||||
},
|
||||
21: {1: {"n": "Slot {n} factory test step failed."}},
|
||||
21: {
|
||||
1: {
|
||||
"n": "Slot {n} factory test step failed.",
|
||||
}
|
||||
},
|
||||
23: { # hashrate error
|
||||
1: {0: "Hashrate is too low."},
|
||||
2: {0: "Hashrate is too low."},
|
||||
3: {0: "Hashrate loss is too high."},
|
||||
4: {0: "Hashrate loss is too high."},
|
||||
5: {0: "Hashrate loss."},
|
||||
1: {
|
||||
0: "Hashrate is too low.",
|
||||
},
|
||||
2: {
|
||||
0: "Hashrate is too low.",
|
||||
},
|
||||
3: {
|
||||
0: "Hashrate loss is too high.",
|
||||
},
|
||||
4: {
|
||||
0: "Hashrate loss is too high.",
|
||||
},
|
||||
5: {
|
||||
0: "Hashrate loss.",
|
||||
},
|
||||
},
|
||||
50: { # water velocity error/voltage error
|
||||
1: {"n": "Slot {n} chip voltage too low."},
|
||||
2: {"n": "Slot {n} chip voltage changed."},
|
||||
3: {"n": "Slot {n} chip temperature difference is too large."},
|
||||
4: {"n": "Slot {n} chip hottest temperature difference is too large."},
|
||||
7: {"n": "Slot {n} water velocity is abnormal."}, # abnormal water velocity
|
||||
8: {0: "Chip temp calibration failed, please restore factory settings."},
|
||||
9: {"n": "Slot {n} chip temp calibration check no balance."},
|
||||
1: {
|
||||
"n": "Slot {n} chip voltage too low.",
|
||||
},
|
||||
2: {
|
||||
"n": "Slot {n} chip voltage changed.",
|
||||
},
|
||||
3: {
|
||||
"n": "Slot {n} chip temperature difference is too large.",
|
||||
},
|
||||
4: {
|
||||
"n": "Slot {n} chip hottest temperature difference is too large.",
|
||||
},
|
||||
5: {"n": "Slot {n} stopped hashing, chips temperature protecting."},
|
||||
7: {
|
||||
"n": "Slot {n} water velocity is abnormal.",
|
||||
}, # abnormal water velocity
|
||||
8: {
|
||||
0: "Chip temp calibration failed, please restore factory settings.",
|
||||
},
|
||||
9: {
|
||||
"n": "Slot {n} chip temp calibration check no balance.",
|
||||
},
|
||||
},
|
||||
51: { # frequency error
|
||||
1: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
|
||||
7: {"n": "Slot {n} frequency up timeout."}, # frequency up timeout
|
||||
1: {
|
||||
"n": "Slot {n} frequency up timeout.",
|
||||
}, # frequency up timeout
|
||||
2: {"n": "Slot {n} too many CRC errors."},
|
||||
3: {"n": "Slot {n} unstable."},
|
||||
7: {
|
||||
"n": "Slot {n} frequency up timeout.",
|
||||
}, # frequency up timeout
|
||||
},
|
||||
52: {
|
||||
"n": {
|
||||
"c": "Slot {n} chip {c} error nonce.",
|
||||
},
|
||||
},
|
||||
53: {
|
||||
"n": {
|
||||
"c": "Slot {n} chip {c} too few nonce.",
|
||||
},
|
||||
},
|
||||
54: {
|
||||
"n": {
|
||||
"c": "Slot {n} chip {c} temp protected.",
|
||||
},
|
||||
},
|
||||
55: {
|
||||
"n": {
|
||||
"c": "Slot {n} chip {c} has been reset.",
|
||||
},
|
||||
},
|
||||
56: {
|
||||
"n": {
|
||||
"c": "Slot {n} chip {c} zero nonce.",
|
||||
},
|
||||
},
|
||||
52: {"n": {"c": "Slot {n} chip {c} error nonce."}},
|
||||
53: {"n": {"c": "Slot {n} chip {c} too few nonce."}},
|
||||
54: {"n": {"c": "Slot {n} chip {c} temp protected."}},
|
||||
55: {"n": {"c": "Slot {n} chip {c} has been reset."}},
|
||||
56: {"n": {"c": "Slot {n} chip {c} does not return to the nonce."}},
|
||||
80: {
|
||||
0: {0: "The tool version is too low, please update."},
|
||||
1: {0: "Low freq."},
|
||||
2: {0: "Low hashrate."},
|
||||
3: {5: "High env temp."},
|
||||
0: {
|
||||
0: "The tool version is too low, please update.",
|
||||
},
|
||||
1: {
|
||||
0: "Low freq.",
|
||||
},
|
||||
2: {
|
||||
0: "Low hashrate.",
|
||||
},
|
||||
3: {
|
||||
5: "High env temp.",
|
||||
},
|
||||
},
|
||||
81: {
|
||||
0: {0: "Chip data error."},
|
||||
0: {
|
||||
0: "Chip data error.",
|
||||
},
|
||||
},
|
||||
82: {
|
||||
0: {0: "Power version error."},
|
||||
1: {0: "Miner type error."},
|
||||
2: {0: "Version info error."},
|
||||
0: {
|
||||
0: "Power version error.",
|
||||
},
|
||||
1: {
|
||||
0: "Miner type error.",
|
||||
},
|
||||
2: {
|
||||
0: "Version info error.",
|
||||
},
|
||||
},
|
||||
83: {
|
||||
0: {0: "Empty level error."},
|
||||
0: {
|
||||
0: "Empty level error.",
|
||||
},
|
||||
},
|
||||
84: {
|
||||
0: {0: "Old firmware."},
|
||||
1: {0: "Software version error."},
|
||||
0: {
|
||||
0: "Old firmware.",
|
||||
},
|
||||
1: {
|
||||
0: "Software version error.",
|
||||
},
|
||||
},
|
||||
85: {
|
||||
"n": {
|
||||
@@ -296,8 +465,12 @@ ERROR_CODES = {
|
||||
},
|
||||
},
|
||||
86: {
|
||||
0: {0: "Missing product serial #."},
|
||||
1: {0: "Missing product type."},
|
||||
0: {
|
||||
0: "Missing product serial #.",
|
||||
},
|
||||
1: {
|
||||
0: "Missing product type.",
|
||||
},
|
||||
2: {
|
||||
0: "Missing miner serial #.",
|
||||
1: "Wrong miner serial # length.",
|
||||
@@ -314,12 +487,34 @@ ERROR_CODES = {
|
||||
3: "Wrong power model rate.",
|
||||
4: "Wrong power model format.",
|
||||
},
|
||||
5: {0: "Wrong hash board struct."},
|
||||
6: {0: "Wrong miner cooling type."},
|
||||
7: {0: "Missing PCB serial #."},
|
||||
5: {
|
||||
0: "Wrong hash board struct.",
|
||||
},
|
||||
6: {
|
||||
0: "Wrong miner cooling type.",
|
||||
},
|
||||
7: {
|
||||
0: "Missing PCB serial #.",
|
||||
},
|
||||
},
|
||||
87: {
|
||||
0: {
|
||||
0: "Miner power mismatch.",
|
||||
},
|
||||
},
|
||||
90: {
|
||||
0: {
|
||||
0: "Process error, exited with signal: 3.",
|
||||
},
|
||||
1: {
|
||||
0: "Process error, exited with signal: 3.",
|
||||
},
|
||||
},
|
||||
99: {
|
||||
9: {
|
||||
9: "Miner unknown error.",
|
||||
},
|
||||
},
|
||||
87: {0: {0: "Miner power mismatch."}},
|
||||
99: {9: {9: "Miner unknown error."}},
|
||||
1000: {
|
||||
0: {
|
||||
0: "Security library error, please upgrade firmware",
|
||||
@@ -328,7 +523,11 @@ ERROR_CODES = {
|
||||
3: "/antiv/dig/pf_partial.dig illegal.",
|
||||
},
|
||||
},
|
||||
1001: {0: {0: "Security BTMiner removed, please upgrade firmware."}},
|
||||
1001: {
|
||||
0: {
|
||||
0: "Security BTMiner removed, please upgrade firmware.",
|
||||
},
|
||||
},
|
||||
1100: {
|
||||
0: {
|
||||
0: "Security illegal file, please upgrade firmware.",
|
||||
|
||||
@@ -26,11 +26,17 @@ from pyasic.miners.backends.cgminer import CGMiner
|
||||
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
|
||||
|
||||
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": {}},
|
||||
"api_ver": {"cmd": "get_api_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"}}},
|
||||
"nominal_hashrate": {
|
||||
"cmd": "get_nominal_hashrate",
|
||||
@@ -42,8 +48,11 @@ ANTMINER_MODERN_DATA_LOC = {
|
||||
"wattage_limit": {"cmd": "get_wattage_limit", "kwargs": {}},
|
||||
"fans": {"cmd": "get_fans", "kwargs": {"api_stats": {"api": "stats"}}},
|
||||
"fan_psu": {"cmd": "get_fan_psu", "kwargs": {}},
|
||||
"errors": {"cmd": "get_errors", "kwargs": {}},
|
||||
"fault_light": {"cmd": "get_fault_light", "kwargs": {}},
|
||||
"errors": {"cmd": "get_errors", "kwargs": {"web_summary": {"web": "summary"}}},
|
||||
"fault_light": {
|
||||
"cmd": "get_fault_light",
|
||||
"kwargs": {"web_get_blink_status": {"web": "get_blink_status"}},
|
||||
},
|
||||
"pools": {"cmd": "get_pools", "kwargs": {"api_pools": {"api": "pools"}}},
|
||||
"is_mining": {
|
||||
"cmd": "is_mining",
|
||||
@@ -100,7 +109,7 @@ class AntminerModern(BMMiner):
|
||||
data = await self.web.blink(blink=False)
|
||||
if data:
|
||||
if data.get("code") == "B100":
|
||||
self.light = True
|
||||
self.light = False
|
||||
return self.light
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
@@ -121,21 +130,31 @@ class AntminerModern(BMMiner):
|
||||
await self.send_config(cfg)
|
||||
return True
|
||||
|
||||
async def get_hostname(self) -> Union[str, None]:
|
||||
try:
|
||||
data = await self.web.get_system_info()
|
||||
if data:
|
||||
return data["hostname"]
|
||||
except KeyError:
|
||||
pass
|
||||
async def get_hostname(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
|
||||
|
||||
async def get_mac(self) -> Union[str, None]:
|
||||
try:
|
||||
data = await self.web.get_system_info()
|
||||
if data:
|
||||
return data["macaddr"]
|
||||
except KeyError:
|
||||
pass
|
||||
if web_get_system_info:
|
||||
try:
|
||||
return web_get_system_info["hostname"]
|
||||
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:
|
||||
data = await self.web.get_network_info()
|
||||
@@ -144,12 +163,17 @@ class AntminerModern(BMMiner):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
async def get_errors(self) -> List[MinerErrorData]:
|
||||
errors = []
|
||||
data = await self.web.summary()
|
||||
if data:
|
||||
async def get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
|
||||
if not web_summary:
|
||||
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:
|
||||
if not item["status"] == "s":
|
||||
errors.append(X19Error(item["msg"]))
|
||||
@@ -159,15 +183,21 @@ class AntminerModern(BMMiner):
|
||||
pass
|
||||
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:
|
||||
return self.light
|
||||
try:
|
||||
data = await self.web.get_blink_status()
|
||||
if data:
|
||||
self.light = data["blink"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not web_get_blink_status:
|
||||
try:
|
||||
web_get_blink_status = await self.web.get_blink_status()
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
if web_get_blink_status:
|
||||
try:
|
||||
self.light = web_get_blink_status["blink"]
|
||||
except KeyError:
|
||||
pass
|
||||
return self.light
|
||||
|
||||
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
|
||||
|
||||
@@ -235,7 +235,20 @@ class BMMiner(BaseMiner):
|
||||
if board_offset == -1:
|
||||
board_offset = 1
|
||||
|
||||
for i in range(board_offset, board_offset + self.ideal_hashboards):
|
||||
real_slots = []
|
||||
|
||||
for i in range(board_offset, board_offset + 4):
|
||||
try:
|
||||
key = f'chain_acs{i}'
|
||||
if boards[1].get(key, '') != '':
|
||||
real_slots.append(i)
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
if len(real_slots) < 3:
|
||||
real_slots = list(range(board_offset, board_offset + self.ideal_hashboards))
|
||||
|
||||
for i in real_slots:
|
||||
hashboard = HashBoard(
|
||||
slot=i - board_offset, expected_chips=self.nominal_chips
|
||||
)
|
||||
@@ -259,7 +272,7 @@ class BMMiner(BaseMiner):
|
||||
if (not chips) or (not chips > 0):
|
||||
hashboard.missing = True
|
||||
hashboards.append(hashboard)
|
||||
except (IndexError, KeyError, ValueError, TypeError):
|
||||
except (LookupError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return hashboards
|
||||
|
||||
@@ -303,17 +303,12 @@ class BOSMiner(BaseMiner):
|
||||
The config from `self.config`.
|
||||
"""
|
||||
logging.debug(f"{self}: Getting config.")
|
||||
conn = None
|
||||
|
||||
try:
|
||||
conn = await self._get_ssh_connection()
|
||||
except ConnectionError:
|
||||
try:
|
||||
pools = await self.api.pools()
|
||||
except APIError:
|
||||
return self.config
|
||||
if pools:
|
||||
self.config = MinerConfig().from_api(pools["POOLS"])
|
||||
return self.config
|
||||
conn = None
|
||||
|
||||
if conn:
|
||||
async with conn:
|
||||
# good ol' BBB compatibility :/
|
||||
@@ -365,6 +360,8 @@ class BOSMiner(BaseMiner):
|
||||
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:
|
||||
@@ -1078,7 +1075,9 @@ class BOSMiner(BaseMiner):
|
||||
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)
|
||||
api_devdetails = await self.api.send_command(
|
||||
"devdetails", ignore_errors=True, allow_warning=False
|
||||
)
|
||||
except APIError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -179,11 +179,12 @@ class CGMinerAvalon(CGMiner):
|
||||
pass
|
||||
|
||||
async def get_hostname(self, mac: str = None) -> Optional[str]:
|
||||
if not mac:
|
||||
mac = await self.get_mac()
|
||||
|
||||
if mac:
|
||||
return f"Avalon{mac.replace(':', '')[-6:]}"
|
||||
return None
|
||||
# if not mac:
|
||||
# mac = await self.get_mac()
|
||||
#
|
||||
# if mac:
|
||||
# return f"Avalon{mac.replace(':', '')[-6:]}"
|
||||
|
||||
async def get_hashrate(self, api_devs: dict = None) -> Optional[float]:
|
||||
if not api_devs:
|
||||
|
||||
@@ -74,6 +74,24 @@ class VNish(BMMiner):
|
||||
pass
|
||||
return False
|
||||
|
||||
async def stop_mining(self) -> bool:
|
||||
data = await self.web.stop_mining()
|
||||
if data:
|
||||
try:
|
||||
return data["success"]
|
||||
except KeyError:
|
||||
pass
|
||||
return False
|
||||
|
||||
async def resume_mining(self) -> bool:
|
||||
data = await self.web.resume_mining()
|
||||
if data:
|
||||
try:
|
||||
return data["success"]
|
||||
except KeyError:
|
||||
pass
|
||||
return False
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
data = await self.web.reboot()
|
||||
if data:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -33,6 +33,8 @@ class BaseMiner(ABC):
|
||||
self.api = None
|
||||
self.web = None
|
||||
|
||||
self.ssh_pwd = "root"
|
||||
|
||||
# static data
|
||||
self.ip = ip
|
||||
self.api_type = None
|
||||
@@ -89,6 +91,7 @@ class BaseMiner(ABC):
|
||||
|
||||
@pwd.setter
|
||||
def pwd(self, val):
|
||||
self.ssh_pwd = val
|
||||
try:
|
||||
if self.web is not None:
|
||||
self.web.pwd = val
|
||||
@@ -125,7 +128,7 @@ class BaseMiner(ABC):
|
||||
str(self.ip),
|
||||
known_hosts=None,
|
||||
username="root",
|
||||
password="root",
|
||||
password=self.ssh_pwd,
|
||||
server_host_key_algs=["ssh-rsa"],
|
||||
)
|
||||
return conn
|
||||
@@ -410,65 +413,57 @@ class BaseMiner(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
async def _get_data(self, allow_warning: bool, data_to_get: list = None) -> dict:
|
||||
if not data_to_get:
|
||||
async def _get_data(
|
||||
self, allow_warning: bool, include: list = None, exclude: list = None
|
||||
) -> dict:
|
||||
if include is None:
|
||||
# everything
|
||||
data_to_get = [
|
||||
"mac",
|
||||
"model",
|
||||
"api_ver",
|
||||
"fw_ver",
|
||||
"hostname",
|
||||
"hashrate",
|
||||
"nominal_hashrate",
|
||||
"hashboards",
|
||||
"env_temp",
|
||||
"wattage",
|
||||
"wattage_limit",
|
||||
"fans",
|
||||
"fan_psu",
|
||||
"errors",
|
||||
"fault_light",
|
||||
"pools",
|
||||
"is_mining",
|
||||
"uptime",
|
||||
]
|
||||
api_multicommand = []
|
||||
include = list(self.data_locations.keys())
|
||||
|
||||
if exclude is not None:
|
||||
for item in exclude:
|
||||
if item in include:
|
||||
include.remove(item)
|
||||
|
||||
api_multicommand = set()
|
||||
web_multicommand = []
|
||||
for data_name in data_to_get:
|
||||
for data_name in include:
|
||||
try:
|
||||
fn_args = self.data_locations[data_name]["kwargs"]
|
||||
for arg_name in fn_args:
|
||||
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"):
|
||||
web_multicommand.append(fn_args[arg_name]["web"])
|
||||
if not fn_args[arg_name]["web"] in web_multicommand:
|
||||
web_multicommand.append(fn_args[arg_name]["web"])
|
||||
except KeyError as e:
|
||||
logger.error(e, data_name)
|
||||
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:
|
||||
api_command_data = await self.api.multicommand(
|
||||
*api_multicommand, allow_warning=allow_warning
|
||||
api_command_task = asyncio.create_task(
|
||||
self.api.multicommand(*api_multicommand, allow_warning=allow_warning)
|
||||
)
|
||||
else:
|
||||
api_command_data = {}
|
||||
api_command_task = asyncio.sleep(0)
|
||||
if len(web_multicommand) > 0:
|
||||
web_command_data = await self.web.multicommand(
|
||||
*web_multicommand, allow_warning=allow_warning
|
||||
web_command_task = asyncio.create_task(
|
||||
self.web.multicommand(*web_multicommand, allow_warning=allow_warning)
|
||||
)
|
||||
else:
|
||||
web_command_task = asyncio.sleep(0)
|
||||
|
||||
web_command_data = await web_command_task
|
||||
if web_command_data is None:
|
||||
web_command_data = {}
|
||||
|
||||
api_command_data = await api_command_task
|
||||
if api_command_data is None:
|
||||
api_command_data = {}
|
||||
|
||||
miner_data = {}
|
||||
|
||||
for data_name in data_to_get:
|
||||
for data_name in include:
|
||||
try:
|
||||
fn_args = self.data_locations[data_name]["kwargs"]
|
||||
args_to_send = {k: None for k in fn_args}
|
||||
@@ -492,7 +487,7 @@ class BaseMiner(ABC):
|
||||
args_to_send[arg_name] = web_command_data
|
||||
except LookupError:
|
||||
args_to_send[arg_name] = None
|
||||
except LookupError as e:
|
||||
except LookupError:
|
||||
continue
|
||||
|
||||
function = getattr(self, self.data_locations[data_name]["cmd"])
|
||||
@@ -522,13 +517,14 @@ class BaseMiner(ABC):
|
||||
return miner_data
|
||||
|
||||
async def get_data(
|
||||
self, allow_warning: bool = False, data_to_get: list = None
|
||||
self, allow_warning: bool = False, include: list = None, exclude: list = None
|
||||
) -> MinerData:
|
||||
"""Get data from the miner in the form of [`MinerData`][pyasic.data.MinerData].
|
||||
|
||||
Parameters:
|
||||
allow_warning: Allow warning when an API command fails.
|
||||
data_to_get: Names of data items you want to gather. Defaults to all data.
|
||||
include: Names of data items you want to gather. Defaults to all data.
|
||||
exclude: Names of data items to exclude. Exclusion happens after considering included items.
|
||||
|
||||
Returns:
|
||||
A [`MinerData`][pyasic.data.MinerData] instance containing data from the miner.
|
||||
@@ -544,7 +540,9 @@ class BaseMiner(ABC):
|
||||
],
|
||||
)
|
||||
|
||||
gathered_data = await self._get_data(allow_warning, data_to_get=data_to_get)
|
||||
gathered_data = await self._get_data(
|
||||
allow_warning, include=include, exclude=exclude
|
||||
)
|
||||
for item in gathered_data:
|
||||
if gathered_data[item] is not None:
|
||||
setattr(data, item, gathered_data[item])
|
||||
|
||||
@@ -22,7 +22,7 @@ import json
|
||||
import re
|
||||
from typing import Callable, List, Optional, Tuple, Union
|
||||
|
||||
import aiohttp
|
||||
import httpx
|
||||
|
||||
from pyasic.logger import logger
|
||||
from pyasic.miners.antminer import *
|
||||
@@ -319,6 +319,7 @@ MINER_CLASSES = {
|
||||
"ANTMINER S19J": BOSMinerS19j,
|
||||
"ANTMINER S19J88NOPIC": BOSMinerS19jNoPIC,
|
||||
"ANTMINER S19J PRO": BOSMinerS19jPro,
|
||||
"ANTMINER S19J PRO NOPIC": BOSMinerS19jPro,
|
||||
"ANTMINER T19": BOSMinerT19,
|
||||
},
|
||||
MinerTypes.VNISH: {
|
||||
@@ -455,7 +456,7 @@ class MinerFactory:
|
||||
|
||||
async def _get_miner_web(self, ip: str):
|
||||
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]
|
||||
|
||||
text, resp = await concurrent_get_first_result(
|
||||
@@ -466,26 +467,26 @@ class MinerFactory:
|
||||
|
||||
@staticmethod
|
||||
async def _web_ping(
|
||||
session: aiohttp.ClientSession, url: str
|
||||
) -> Tuple[Optional[str], Optional[aiohttp.ClientResponse]]:
|
||||
session: httpx.AsyncClient, url: str
|
||||
) -> Tuple[Optional[str], Optional[httpx.Response]]:
|
||||
try:
|
||||
resp = await session.get(url, allow_redirects=False)
|
||||
return await resp.text(), resp
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
resp = await session.get(url, follow_redirects=False)
|
||||
return resp.text, resp
|
||||
except (httpx.HTTPError, asyncio.TimeoutError):
|
||||
pass
|
||||
return None, None
|
||||
|
||||
@staticmethod
|
||||
def _parse_web_type(web_text: str, web_resp: aiohttp.ClientResponse) -> MinerTypes:
|
||||
if web_resp.status == 401 and 'realm="antMiner' in web_resp.headers.get(
|
||||
def _parse_web_type(web_text: str, web_resp: httpx.Response) -> MinerTypes:
|
||||
if web_resp.status_code == 401 and 'realm="antMiner' in web_resp.headers.get(
|
||||
"www-authenticate", ""
|
||||
):
|
||||
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", ""
|
||||
):
|
||||
return MinerTypes.WHATSMINER
|
||||
if "Braiins OS" in web_text or 'href="/cgi-bin/luci"' in web_text:
|
||||
if "Braiins OS" in web_text:
|
||||
return MinerTypes.BRAIINS_OS
|
||||
if "cloud-box" in web_text:
|
||||
return MinerTypes.GOLDSHELL
|
||||
@@ -576,26 +577,26 @@ class MinerFactory:
|
||||
self,
|
||||
ip: Union[ipaddress.ip_address, str],
|
||||
location: str,
|
||||
auth: Optional[aiohttp.BasicAuth] = None,
|
||||
auth: Optional[httpx.DigestAuth] = None,
|
||||
) -> Optional[dict]:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with httpx.AsyncClient(verify=False) as session:
|
||||
try:
|
||||
data = await session.get(
|
||||
f"http://{str(ip)}{location}",
|
||||
auth=auth,
|
||||
timeout=30,
|
||||
)
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
except (httpx.HTTPError, asyncio.TimeoutError):
|
||||
logger.info(f"{ip}: Web command timeout.")
|
||||
return
|
||||
if data is None:
|
||||
return
|
||||
try:
|
||||
json_data = await data.json()
|
||||
except (aiohttp.ContentTypeError, asyncio.TimeoutError):
|
||||
json_data = data.json()
|
||||
except (json.JSONDecodeError, asyncio.TimeoutError):
|
||||
try:
|
||||
return json.loads(await data.text())
|
||||
except (json.JSONDecodeError, aiohttp.ClientError):
|
||||
return json.loads(data.text)
|
||||
except (json.JSONDecodeError, httpx.HTTPError):
|
||||
return
|
||||
else:
|
||||
return json_data
|
||||
@@ -691,6 +692,28 @@ class MinerFactory:
|
||||
return UnknownMiner(str(ip))
|
||||
|
||||
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")
|
||||
try:
|
||||
miner_model = sock_json_data["VERSION"][0]["Type"]
|
||||
@@ -715,19 +738,6 @@ class MinerFactory:
|
||||
except (TypeError, LookupError):
|
||||
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):
|
||||
json_data = await self.send_web_command(ip, "/mcb/status")
|
||||
|
||||
@@ -760,22 +770,20 @@ class MinerFactory:
|
||||
|
||||
async def get_miner_model_innosilicon(self, ip: str) -> Optional[str]:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with httpx.AsyncClient(verify=False) as session:
|
||||
auth_req = await session.post(
|
||||
f"http://{ip}/api/auth",
|
||||
data={"username": "admin", "password": "admin"},
|
||||
)
|
||||
auth = (await auth_req.json())["jwt"]
|
||||
auth = auth_req.json()["jwt"]
|
||||
|
||||
web_data = await (
|
||||
await session.post(
|
||||
web_data = (await session.post(
|
||||
f"http://{ip}/api/type",
|
||||
headers={"Authorization": "Bearer " + auth},
|
||||
data={},
|
||||
)
|
||||
).json()
|
||||
)).json()
|
||||
return web_data["type"]
|
||||
except (aiohttp.ClientError, LookupError):
|
||||
except (httpx.HTTPError, LookupError):
|
||||
pass
|
||||
|
||||
async def get_miner_model_braiins_os(self, ip: str) -> Optional[str]:
|
||||
@@ -790,16 +798,16 @@ class MinerFactory:
|
||||
pass
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with httpx.AsyncClient(verify=False) as session:
|
||||
d = await session.post(
|
||||
f"http://{ip}/graphql",
|
||||
json={"query": "{bosminer {info{modelName}}}"},
|
||||
)
|
||||
if d.status == 200:
|
||||
json_data = await d.json()
|
||||
if d.status_code == 200:
|
||||
json_data = d.json()
|
||||
miner_model = json_data["data"]["bosminer"]["info"]["modelName"]
|
||||
return miner_model
|
||||
except (aiohttp.ClientError, LookupError):
|
||||
except (httpx.HTTPError, LookupError):
|
||||
pass
|
||||
|
||||
async def get_miner_model_vnish(self, ip: str) -> Optional[str]:
|
||||
@@ -813,6 +821,9 @@ class MinerFactory:
|
||||
if "(88)" in miner_model:
|
||||
miner_model = miner_model.replace("(88)", "NOPIC")
|
||||
|
||||
if " AML" in miner_model:
|
||||
miner_model = miner_model.replace(" AML", "")
|
||||
|
||||
return miner_model
|
||||
except (TypeError, LookupError):
|
||||
pass
|
||||
|
||||
@@ -24,8 +24,5 @@ class M29V10(WhatsMiner): # noqa - ignore ABC method implementation
|
||||
super().__init__(ip, api_ver)
|
||||
self.ip = ip
|
||||
self.model = "M29 V10"
|
||||
self.nominal_chips = 0
|
||||
warnings.warn(
|
||||
"Unknown chip count for miner type M29V10, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
|
||||
)
|
||||
self.nominal_chips = 50
|
||||
self.fan_count = 2
|
||||
|
||||
@@ -165,10 +165,7 @@ class M30SPlusVE50(WhatsMiner): # noqa - ignore ABC method implementation
|
||||
super().__init__(ip, api_ver)
|
||||
self.ip = ip
|
||||
self.model = "M30S+ VE50"
|
||||
self.nominal_chips = 0
|
||||
warnings.warn(
|
||||
"Unknown chip count for miner type M30S+ VE50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
|
||||
)
|
||||
self.nominal_chips = 164
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
|
||||
@@ -87,10 +87,7 @@ class M50VH60(WhatsMiner): # noqa - ignore ABC method implementation
|
||||
super().__init__(ip, api_ver)
|
||||
self.ip = ip
|
||||
self.model = "M50 VH60"
|
||||
self.nominal_chips = 0
|
||||
warnings.warn(
|
||||
"Unknown chip count for miner type M50 VH60, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
|
||||
)
|
||||
self.nominal_chips = 84
|
||||
self.fan_count = 2
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from pyasic.errors import APIWarning
|
||||
class BaseWebAPI(ABC):
|
||||
def __init__(self, ip: str) -> None:
|
||||
# ip address of the miner
|
||||
self.ip = ipaddress.ip_address(ip)
|
||||
self.ip = ip # ipaddress.ip_address(ip)
|
||||
self.username = "root"
|
||||
self.pwd = "root"
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Union
|
||||
|
||||
@@ -56,25 +57,37 @@ class AntminerModernWebAPI(BaseWebAPI):
|
||||
async def multicommand(
|
||||
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
|
||||
) -> dict:
|
||||
data = {k: None for k in commands}
|
||||
data["multicommand"] = True
|
||||
auth = httpx.DigestAuth(self.username, self.pwd)
|
||||
async with httpx.AsyncClient() as client:
|
||||
for command in commands:
|
||||
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()
|
||||
data[command] = json_data
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
tasks = [
|
||||
asyncio.create_task(self._handle_multicommand(client, command))
|
||||
for command in commands
|
||||
]
|
||||
all_data = await asyncio.gather(*tasks)
|
||||
|
||||
data = {}
|
||||
for item in all_data:
|
||||
data.update(item)
|
||||
|
||||
data["multicommand"] = True
|
||||
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:
|
||||
return await self.send_command("get_miner_conf")
|
||||
|
||||
|
||||
@@ -116,8 +116,32 @@ class VNishWebAPI(BaseWebAPI):
|
||||
async def reboot(self) -> dict:
|
||||
return await self.send_command("system/reboot", post=True)
|
||||
|
||||
async def pause_mining(self) -> dict:
|
||||
return await self.send_command("mining/pause", post=True)
|
||||
|
||||
async def resume_mining(self) -> dict:
|
||||
return await self.send_command("mining/resume", post=True)
|
||||
|
||||
async def stop_mining(self) -> dict:
|
||||
return await self.send_command("mining/stop", post=True)
|
||||
|
||||
async def start_mining(self) -> dict:
|
||||
return await self.send_command("mining/start", post=True)
|
||||
|
||||
async def info(self):
|
||||
return await self.send_command("info")
|
||||
|
||||
async def summary(self):
|
||||
return await self.send_command("summary")
|
||||
|
||||
async def chips(self):
|
||||
return await self.send_command("chips")
|
||||
|
||||
async def layout(self):
|
||||
return await self.send_command("layout")
|
||||
|
||||
async def status(self):
|
||||
return await self.send_command("status")
|
||||
|
||||
async def settings(self):
|
||||
return await self.send_command("settings")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.37.2"
|
||||
version = "0.38.7"
|
||||
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"
|
||||
@@ -14,7 +14,6 @@ httpx = "^0.24.0"
|
||||
passlib = "^1.7.4"
|
||||
pyaml = "^23.5.9"
|
||||
toml = "^0.10.2"
|
||||
aiohttp = "^3.8.4"
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
Reference in New Issue
Block a user