Compare commits

..

18 Commits

Author SHA1 Message Date
UpstreamData
097b8ed534 version: bump version number. 2022-12-02 15:57:52 -07:00
UpstreamData
da47d72749 feature: add wattage limit in get_config when getting config from whatsminers. 2022-12-02 15:57:31 -07:00
UpstreamData
abd4d18a01 feature: add whatsminer M31SV10 and V60. 2022-12-02 15:51:27 -07:00
UpstreamData
2adbce3c21 version: bump version number. 2022-12-02 09:26:53 -07:00
UpstreamData
c41324b324 bug: fix a bug with MinerAPI.commands causing an infinite recursion loop when checking a list of commands with get_commands, and ficx some weirdness where BTMiner doesnt return any data. 2022-12-02 09:26:17 -07:00
UpstreamData
151a4f6c2d version: bump version number. 2022-12-01 16:18:07 -07:00
UpstreamData
d23777a83f feature: Switch to using semaphores in miner network to rate limit as they are much more friendly. 2022-12-01 16:17:46 -07:00
UpstreamData
bb0aee8337 version: bump version number. 2022-12-01 15:54:09 -07:00
UpstreamData
2a23acb73a bug: fix BTMiner not responding to pause_mining and resume_mining causing an exception. 2022-12-01 15:53:47 -07:00
UpstreamData
ad5eb0cef6 bug: fix a bug with updated logging. 2022-12-01 15:40:40 -07:00
UpstreamData
07dd8f55fe format: improve logging. 2022-12-01 15:02:17 -07:00
UpstreamData
bafa91cc47 version: bump version number. 2022-11-30 10:48:20 -07:00
UpstreamData
16399510fa feature: add set_power_limit to all miners that support it. 2022-11-30 10:47:45 -07:00
UpstreamData
e9f54eec0a version: bump version number. 2022-11-30 10:01:27 -07:00
UpstreamData
fbbbc9f215 bug: Add VH60 to miner factory as it was missing. 2022-11-30 10:01:01 -07:00
UpstreamData
69e4f575c0 bug: fix a bug wth bosminer where it will sometimes not get data from graphql 2022-11-30 09:59:12 -07:00
UpstreamData
e95659d2e0 version: bump version number 2022-11-29 09:36:41 -07:00
UpstreamData
35f34310ec bug: fix an issue with incorrect type hinting. 2022-11-29 09:36:24 -07:00
26 changed files with 293 additions and 165 deletions

View File

@@ -117,10 +117,11 @@ These functions are
[`get_hostname`](#get-hostname),
[`get_model`](#get-model),
[`reboot`](#reboot),
[`restart_backend`](#restart-backend), and
[`stop_mining`](#stop-mining), and
[`resume_mining`](#resume-mining), and
[`send_config`](#send-config).
[`restart_backend`](#restart-backend),
[`stop_mining`](#stop-mining),
[`resume_mining`](#resume-mining),
[`send_config`](#send-config), and
[`set_power_limit`](#set-power-limit).
<br>
@@ -228,6 +229,14 @@ These functions are
<br>
### Set Power Limit
::: pyasic.miners.BaseMiner.set_power_limit
handler: python
options:
heading_level: 4
<br>
## [`MinerConfig`][pyasic.config.MinerConfig] and [`MinerData`][pyasic.data.MinerData]
Pyasic implements a few dataclasses as helpers to make data return types consistent across different miners and miner APIs. The different fields of these dataclasses can all be viewed with the classmethod `cls.fields()`.

View File

@@ -90,6 +90,8 @@ details {
</details>
<details>
<summary><a href="../whatsminer/M3X/#m31s">M31S</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv10">M31SV10</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv60">M31SV60</a></summary>
<summary><a href="../whatsminer/M3X/#m31sv70">M31SV70</a></summary>
</details>
<details>

View File

@@ -114,9 +114,25 @@
show_root_heading: false
heading_level: 4
## M31SV10
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV10
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV60
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV60
handler: python
options:
show_root_heading: false
heading_level: 4
## M31SV70
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31S
::: pyasic.miners.whatsminer.btminer.M3X.M31S.BTMinerM31SV70
handler: python
options:
show_root_heading: false

View File

@@ -35,6 +35,81 @@ class BaseMinerAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls)
def __repr__(self):
return f"{self.__class__.__name__}: {str(self.ip)}"
async def send_command(
self,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
allow_warning: bool = True,
) -> dict:
"""Send an API command to the miner and return the result.
Parameters:
command: The command to sent to the miner.
parameters: Any additional parameters to be sent with the command.
ignore_errors: Whether to raise APIError when the command returns an error.
allow_warning: Whether to warn if the command fails.
Returns:
The return data from the API command parsed from JSON into a dict.
"""
logging.debug(f"{self} - (Send Privileged Command) - {command} " + f'with args {parameters}' if parameters else '')
# create the command
cmd = {"command": command}
if parameters:
cmd["parameter"] = parameters
# send the command
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
data = self._load_api_data(data)
# check for if the user wants to allow errors to return
if not ignore_errors:
# validate the command succeeded
validation = self._validate_command_output(data)
if not validation[0]:
if allow_warning:
logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}"
)
raise APIError(validation[1])
logging.debug(f"{self} - (Send Command) - Received data.")
return data
# Privileged command handler, only used by whatsminers, defined here for consistency.
async def send_privileged_command(self, *args, **kwargs) -> dict:
return await self.send_command(*args, **kwargs)
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
allow_warning: A boolean to supress APIWarnings.
"""
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# standard format doesn't work for X19
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
return {}
logging.debug(f"{self} - (Multicommand) - Received data")
return data
@property
def commands(self) -> list:
return self.get_commands()
def get_commands(self) -> list:
"""Get a list of command accessible to a specific type of API on the miner.
@@ -46,6 +121,7 @@ class BaseMinerAPI:
for func in
# each function in self
dir(self)
if not func == "commands"
if callable(getattr(self, func)) and
# no __ or _ methods
not func.startswith("__") and not func.startswith("_") and
@@ -72,26 +148,6 @@ If you are sure you want to use this command please use API.send_command("{comma
)
return return_commands
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
*commands: The commands to send as a multicommand to the miner.
allow_warning: A boolean to supress APIWarnings.
"""
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesn't work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
return {}
logging.debug(f"{self.ip}: Received multicommand data.")
return data
async def _send_bytes(self, data: bytes) -> bytes:
try:
# get reader and writer streams
@@ -99,7 +155,7 @@ If you are sure you want to use this command please use API.send_command("{comma
# handle OSError 121
except OSError as e:
if getattr(e, "winerror") == "121":
logging.warning("Semaphore Timeout has Expired.")
logging.warning(f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired.")
return b"{}"
# send the command
@@ -117,7 +173,7 @@ If you are sure you want to use this command please use API.send_command("{comma
break
ret_data += d
except Exception as e:
logging.warning(f"{self.ip}: API Command Error: - {e}")
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
# close the connection
writer.close()
@@ -125,50 +181,6 @@ If you are sure you want to use this command please use API.send_command("{comma
return ret_data
async def send_command(
self,
command: Union[str, bytes],
parameters: Union[str, int, bool] = None,
ignore_errors: bool = False,
allow_warning: bool = True,
) -> dict:
"""Send an API command to the miner and return the result.
Parameters:
command: The command to sent to the miner.
parameters: Any additional parameters to be sent with the command.
ignore_errors: Whether to raise APIError when the command returns an error.
allow_warning: Whether to warn if the command fails.
Returns:
The return data from the API command parsed from JSON into a dict.
"""
# create the command
cmd = {"command": command}
if parameters:
cmd["parameter"] = parameters
# send the command
data = await self._send_bytes(json.dumps(cmd).encode("utf-8"))
data = self._load_api_data(data)
# check for if the user wants to allow errors to return
if not ignore_errors:
# validate the command succeeded
validation = self._validate_command_output(data)
if not validation[0]:
if allow_warning:
logging.warning(
f"{self.ip}: API Command Error: {command}: {validation[1]}"
)
raise APIError(validation[1])
return data
async def send_privileged_command(self, *args, **kwargs) -> dict:
return await self.send_command(*args, **kwargs)
@staticmethod
def _validate_command_output(data: dict) -> tuple:
# check if the data returned is correct or an error

View File

@@ -40,7 +40,6 @@ class BMMinerAPI(BaseMinerAPI):
async def multicommand(
self, *commands: str, allow_warning: bool = True
) -> dict:
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
@@ -49,9 +48,8 @@ class BMMinerAPI(BaseMinerAPI):
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
logging.debug(f"{self.ip}: Handling X19 multicommand.")
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
logging.debug(f"{self.ip}: Received multicommand data.")
return data
async def _x19_multicommand(self, *commands):
@@ -65,7 +63,7 @@ class BMMinerAPI(BaseMinerAPI):
except APIError as e:
raise APIError(e)
except Exception as e:
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
logging.warning(f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}")
return data
async def version(self) -> dict:

View File

@@ -166,7 +166,7 @@ class BOSMinerAPI(BaseMinerAPI):
return await self.send_command("estats")
async def check(self, command: str) -> dict:
"""Check if the command command exists in BOSMiner.
"""Check if the command `command` exists in BOSMiner.
<details>
<summary>Expand</summary>

View File

@@ -32,7 +32,7 @@ from pyasic.settings import PyasicSettings
# tool, then you can set them back to admin with this tool, but they
# must be changed to something else and set back to admin with this
# or the privileged API will not work using admin as the password. If
# you change the password, you can pass that to the this class as pwd,
# 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.
@@ -81,7 +81,7 @@ def _add_to_16(string: str) -> bytes:
def parse_btminer_priviledge_data(token_data: dict, data: dict):
"""Parses data returned from the BTMiner privileged API.
Parses data from the BTMiner privileged API using the the token
Parses data from the BTMiner privileged API using the token
from the API in an AES format.
Parameters:
@@ -188,6 +188,7 @@ class BTMinerAPI(BaseMinerAPI):
async def send_privileged_command(
self, command: Union[str, bytes], ignore_errors: bool = False, **kwargs
) -> dict:
logging.debug(f"{self} - (Send Privileged Command) - {command} " + f'with args {kwargs}' if len(kwargs) > 0 else '')
command = {"cmd": command}
for kwarg in kwargs:
if kwargs[kwarg]:
@@ -197,6 +198,8 @@ class BTMinerAPI(BaseMinerAPI):
enc_command = create_privileged_cmd(token_data, command)
data = await self._send_bytes(enc_command)
if not data:
raise APIError("No data was returned from the API.")
data = self._load_api_data(data)
try:
@@ -222,6 +225,7 @@ class BTMinerAPI(BaseMinerAPI):
An encoded token and md5 password, which are used for the privileged API.
</details>
"""
logging.debug(f"{self} - (Get Token) - Getting token")
# get the token
data = await self.send_command("get_token")
@@ -244,6 +248,7 @@ class BTMinerAPI(BaseMinerAPI):
"host_sign": host_sign,
"host_passwd_md5": host_passwd_md5,
}
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.current_token}")
return self.current_token
#### PRIVILEGED COMMANDS ####

View File

@@ -41,18 +41,16 @@ class CGMinerAPI(BaseMinerAPI):
async def multicommand(
self, *commands: str, allow_warning: bool = True
) -> dict:
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
# make sure we can actually run each command, otherwise they will fail
commands = self._check_commands(*commands)
# standard multicommand format is "command1+command2"
# doesnt work for S19 which uses the backup _x19_multicommand
# doesn't work for S19 which uses the backup _x19_multicommand
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
except APIError:
logging.debug(f"{self.ip}: Handling X19 multicommand.")
logging.debug(f"{self} - (Multicommand) - Handling X19 multicommand.")
data = await self._x19_multicommand(*command.split("+"))
logging.debug(f"{self.ip}: Received multicommand data.")
return data
async def _x19_multicommand(self, *commands):
@@ -66,7 +64,7 @@ class CGMinerAPI(BaseMinerAPI):
except APIError as e:
raise APIError(e)
except Exception as e:
logging.warning(f"{self.ip}: API Multicommand Error: {e}")
logging.warning(f"{self} - ([Hidden] X19 Multicommand) - API Command Error {e}")
return data
async def version(self) -> dict:

View File

@@ -18,7 +18,7 @@ from pyasic.API import BaseMinerAPI
class UnknownAPI(BaseMinerAPI):
"""An abstraction of an API for a miner which is unknown.
This class is designed to try to be a intersection of as many miner APIs
This class is designed to try to be an intersection of as many miner APIs
and API commands as possible (API ⋂ API), to ensure that it can be used
with as many APIs as possible.
"""

View File

@@ -13,11 +13,12 @@
# limitations under the License.
import json
import logging
import random
import string
import time
from dataclasses import asdict, dataclass, fields
from typing import List, Literal, Dict
from typing import Dict, List, Literal
import toml
import yaml
@@ -277,6 +278,7 @@ class MinerConfig:
def as_dict(self) -> dict:
"""Convert the data in this class to a dict."""
logging.debug(f"MinerConfig - (To Dict) - Dumping Dict config")
data_dict = asdict(self)
for key in asdict(self).keys():
if data_dict[key] is None:
@@ -285,10 +287,12 @@ class MinerConfig:
def as_toml(self) -> str:
"""Convert the data in this class to toml."""
logging.debug(f"MinerConfig - (To TOML) - Dumping TOML config")
return toml.dumps(self.as_dict())
def as_yaml(self) -> str:
"""Convert the data in this class to yaml."""
logging.debug(f"MinerConfig - (To YAML) - Dumping YAML config")
return yaml.dump(self.as_dict(), sort_keys=False)
def from_raw(self, data: dict):
@@ -298,6 +302,7 @@ class MinerConfig:
Parameters:
data: The raw config data to convert.
"""
logging.debug(f"MinerConfig - (From Raw) - Loading raw config")
pool_groups = []
for key in data.keys():
if key == "pools":
@@ -360,6 +365,12 @@ class MinerConfig:
return self
def from_api(self, pools: list):
"""Convert list output from the `AnyMiner.api.pools()` command into a usable data and save it to this class.
Parameters:
pools: The list of pool data to convert.
"""
logging.debug(f"MinerConfig - (From API) - Loading API config")
_pools = []
for pool in pools:
url = pool.get("URL")
@@ -374,6 +385,7 @@ class MinerConfig:
Parameters:
data: The dict config data to convert.
"""
logging.debug(f"MinerConfig - (From Dict) - Loading Dict config")
pool_groups = []
for group in data["pool_groups"]:
pool_groups.append(_PoolGroup().from_dict(group))
@@ -389,6 +401,7 @@ class MinerConfig:
Parameters:
data: The toml config data to convert.
"""
logging.debug(f"MinerConfig - (From TOML) - Loading TOML config")
return self.from_dict(toml.loads(data))
def from_yaml(self, data: str):
@@ -397,14 +410,16 @@ class MinerConfig:
Parameters:
data: The yaml config data to convert.
"""
logging.debug(f"MinerConfig - (From YAML) - Loading YAML config")
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
def as_wm(self, user_suffix: str = None) -> Dict[str: List[dict], str: int]:
def as_wm(self, user_suffix: str = None) -> Dict[str, int]:
"""Convert the data in this class to a config usable by a Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Whatsminer) - Generating Whatsminer config")
return {"pools": self.pool_groups[0].as_wm(user_suffix=user_suffix), "wattage": self.autotuning_wattage}
def as_inno(self, user_suffix: str = None) -> dict:
@@ -413,6 +428,7 @@ class MinerConfig:
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Inno) - Generating Innosilicon config")
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
def as_x19(self, user_suffix: str = None) -> str:
@@ -421,6 +437,7 @@ class MinerConfig:
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As X19) - Generating X19 config")
cfg = {
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
"bitmain-fan-ctrl": False,
@@ -444,6 +461,7 @@ class MinerConfig:
Parameters:
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As Avalon) - Generating AvalonMiner config")
cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
return cfg
@@ -454,6 +472,7 @@ class MinerConfig:
model: The model of the miner to be used in the format portion of the config.
user_suffix: The suffix to append to username.
"""
logging.debug(f"MinerConfig - (As BOS) - Generating BOSMiner config")
cfg = {
"format": {
"version": "1.2+",

View File

@@ -14,6 +14,7 @@
import copy
import json
import logging
import time
from dataclasses import asdict, dataclass, field, fields
from datetime import datetime, timezone
@@ -371,6 +372,7 @@ class MinerData:
Returns:
A dictionary version of this class.
"""
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
return asdict(self)
def as_json(self) -> str:
@@ -379,6 +381,7 @@ class MinerData:
Returns:
A JSON version of this class.
"""
logging.debug(f"MinerData - (To JSON) - Dumping JSON data")
data = self.asdict()
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
return json.dumps(data)
@@ -389,6 +392,7 @@ class MinerData:
Returns:
A CSV version of this class with no headers.
"""
logging.debug(f"MinerData - (To CSV) - Dumping CSV data")
data = self.asdict()
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
errs = []
@@ -407,6 +411,7 @@ class MinerData:
Returns:
A influxdb line protocol version of this class.
"""
logging.debug(f"MinerData - (To InfluxDB) - Dumping InfluxDB data")
tag_data = [measurement_name]
field_data = []

View File

@@ -349,3 +349,7 @@ class BMMiner(BaseMiner):
data.pool_split = str(quota)
return data
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

@@ -17,8 +17,8 @@ import json
import logging
from typing import List, Union
import toml
import httpx
import toml
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.config import MinerConfig
@@ -548,6 +548,8 @@ class BOSMiner(BaseMiner):
if not query_data:
return None
query_data = query_data["data"]
if not query_data:
return None
data.mac = await self.get_mac()
data.model = await self.get_model()
@@ -669,3 +671,14 @@ class BOSMiner(BaseMiner):
async def get_mac(self):
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
return result.upper().strip()
async def set_power_limit(self, wattage: int) -> bool:
try:
cfg = await self.get_config()
cfg.autotuning_wattage = wattage
await self.send_config(cfg)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
return False
else:
return True

View File

@@ -106,3 +106,6 @@ class BOSMinerOld(BaseMiner):
async def get_data(self, **kwargs) -> MinerData:
return MinerData(ip=str(self.ip))
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

@@ -196,14 +196,20 @@ class BTMiner(BaseMiner):
return False
async def stop_mining(self) -> bool:
data = await self.api.power_off(respbefore=True)
try:
data = await self.api.power_off(respbefore=True)
except APIError:
return False
if data.get("Msg"):
if data["Msg"] == "API command OK":
return True
return False
async def resume_mining(self) -> bool:
data = await self.api.power_on()
try:
data = await self.api.power_on()
except APIError:
return False
if data.get("Msg"):
if data["Msg"] == "API command OK":
return True
@@ -232,16 +238,24 @@ class BTMiner(BaseMiner):
async def get_config(self) -> MinerConfig:
pools = None
summary = None
cfg = MinerConfig()
try:
pools = await self.api.pools()
data = await self.api.multicommand("pools", "summary")
pools = data["pools"][0]
summary = data["summary"][0]
except APIError as e:
logging.warning(e)
if pools:
if "POOLS" in pools.keys():
if "POOLS" in pools:
cfg = cfg.from_api(pools["POOLS"])
if summary:
if "SUMMARY" in summary:
if wattage := summary["SUMMARY"][0].get("Power Limit"):
cfg.autotuning_wattage = wattage
return cfg
async def get_data(self, allow_warning: bool = True) -> MinerData:
@@ -438,3 +452,13 @@ class BTMiner(BaseMiner):
data.mac = mac
return data
async def set_power_limit(self, wattage: int) -> bool:
try:
await self.api.adjust_power_limit(wattage)
except Exception as e:
logging.warning(f"{self} set_power_limit: {e}")
return False
else:
return True

View File

@@ -335,3 +335,6 @@ class CGMiner(BaseMiner):
data.pool_split = str(quota)
return data
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

@@ -61,3 +61,6 @@ class Hiveon(BMMiner):
bad_boards[board] = []
bad_boards[board].append(chain)
return bad_boards
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

@@ -24,6 +24,23 @@ class M31S(BaseMiner): # noqa - ignore ABC method implementation
self.fan_count = 2
class M31SV10(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V10"
self.nominal_chips = 105
self.fan_count = 2
class M31SV60(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "M31S V60"
self.nominal_chips = 105
self.fan_count = 2
class M31SV70(BaseMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()

View File

@@ -20,7 +20,7 @@ from .M30S_Plus_Plus import (
M30SPlusPlusVG40,
M30SPlusPlusVH60,
)
from .M31S import M31S, M31SV70
from .M31S import M31S, M31SV10, M31SV60, M31SV70
from .M31S_Plus import (
M31SPlus,
M31SPlusV30,

View File

@@ -21,9 +21,7 @@ import asyncssh
from pyasic.config import MinerConfig
from pyasic.data import MinerData
from pyasic.data.error_codes import (
MinerErrorData,
)
from pyasic.data.error_codes import MinerErrorData
class BaseMiner(ABC):
@@ -216,4 +214,12 @@ class BaseMiner(ABC):
pass
@abstractmethod
async def set_power_limit(self, wattage: int) -> bool:
"""Set the power limit to be used by the miner.
Returns:
A boolean value of the success of setting the power limit.
"""
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)

View File

@@ -190,10 +190,13 @@ MINER_CLASSES = {
"BTMiner": BTMinerM30SPlusPlus,
"G40": BTMinerM30SPlusPlusVG40,
"G30": BTMinerM30SPlusPlusVG30,
"H60": BTMinerM30SPlusPlusVH60,
},
"M31S": {
"Default": BTMinerM31S,
"BTMiner": BTMinerM31S,
"V10": BTMinerM31SV10,
"V60": BTMinerM31SV60,
"V70": BTMinerM31SV70,
},
"M31S+": {

View File

@@ -74,3 +74,6 @@ class UnknownMiner(BaseMiner):
async def get_data(self, allow_warning: bool = False) -> MinerData:
return MinerData(ip=str(self.ip))
async def set_power_limit(self, wattage: int) -> bool:
return False

View File

@@ -13,7 +13,7 @@
# limitations under the License.
from pyasic.miners._backends import BTMiner # noqa - Ignore access to _module
from pyasic.miners._types import M31S, M31SV70 # noqa - Ignore access to _module
from pyasic.miners._types import M31S, M31SV10, M31SV60, M31SV70 # noqa - Ignore access to _module
class BTMinerM31S(BTMiner, M31S):
@@ -22,6 +22,18 @@ class BTMinerM31S(BTMiner, M31S):
self.ip = ip
class BTMinerM31SV10(BTMiner, M31SV10):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM31SV60(BTMiner, M31SV60):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.ip = ip
class BTMinerM31SV70(BTMiner, M31SV70):
def __init__(self, ip: str) -> None:
super().__init__(ip)

View File

@@ -31,7 +31,7 @@ from .M30S_Plus_Plus import (
BTMinerM30SPlusPlusVG40,
BTMinerM30SPlusPlusVH60,
)
from .M31S import BTMinerM31S, BTMinerM31SV70
from .M31S import BTMinerM31S, BTMinerM31SV10, BTMinerM31SV60, BTMinerM31SV70
from .M31S_Plus import (
BTMinerM31SPlus,
BTMinerM31SPlusV30,

View File

@@ -90,7 +90,7 @@ class MinerNetwork:
f"{self.ip_addr}/{subnet_mask}", strict=False
)
logging.debug(f"Setting MinerNetwork: {self.network}")
logging.debug(f"{self} - (Get Network) - Found network")
return self.network
async def scan_network_for_miners(self) -> List[AnyMiner]:
@@ -101,7 +101,7 @@ class MinerNetwork:
"""
# get the network
local_network = self.get_network()
logging.debug(f"Scanning {local_network} for miners")
logging.debug(f"{self} - (Scan Network For Miners) - Scanning")
# clear cached miners
MinerFactory().clear_cached_miners()
@@ -110,27 +110,13 @@ class MinerNetwork:
scan_tasks = []
miners = []
# for each IP in the network
for host in local_network.hosts():
# make sure we don't exceed the allowed async tasks
if len(scan_tasks) < round(PyasicSettings().network_scan_threads):
# add the task to the list
scan_tasks.append(self.ping_and_get_miner(host))
else:
# run the scan tasks
miners_scan = await asyncio.gather(*scan_tasks)
# add scanned miners to the list of found miners
miners.extend(miners_scan)
# empty the task list
scan_tasks = []
# do a final scan to empty out the list
miners_scan = await asyncio.gather(*scan_tasks)
miners.extend(miners_scan)
limit = asyncio.Semaphore(PyasicSettings().network_scan_threads)
miners = await asyncio.gather(*[self.ping_and_get_miner(host, limit) for host in local_network.hosts()])
# remove all None from the miner list
miners = list(filter(None, miners))
logging.debug(f"Found {len(miners)} connected miners")
logging.debug(f"{self} - (Scan Network For Miners) - Found {len(miners)} miners")
# return the miner objects
return miners
@@ -151,60 +137,47 @@ class MinerNetwork:
# create a list of scan tasks
scan_tasks = []
# for each ip on the network, loop through and scan it
for host in local_network.hosts():
# make sure we don't exceed the allowed async tasks
if len(scan_tasks) >= round(PyasicSettings().network_scan_threads):
# scanned is a loopable list of awaitables
scanned = asyncio.as_completed(scan_tasks)
# when we scan, empty the scan tasks
scan_tasks = []
# yield miners as they are scanned
for miner in scanned:
yield await miner
# add the ping to the list of tasks if we dont scan
scan_tasks.append(loop.create_task(self.ping_and_get_miner(host)))
# do one last scan at the end to close out the list
scanned = asyncio.as_completed(scan_tasks)
for miner in scanned:
limit = asyncio.Semaphore(PyasicSettings().network_scan_threads)
miners = asyncio.as_completed([loop.create_task(self.ping_and_get_miner(host, limit)) for host in local_network.hosts()])
for miner in miners:
yield await miner
@staticmethod
async def ping_miner(ip: ipaddress.ip_address) -> Union[None, ipaddress.ip_address]:
try:
miner = await ping_miner(ip)
if miner:
return miner
except ConnectionRefusedError:
tasks = [ping_miner(ip, port=port) for port in [4029, 8889]]
for miner in asyncio.as_completed(tasks):
try:
miner = await miner
if miner:
return miner
except ConnectionRefusedError:
pass
async def ping_miner(ip: ipaddress.ip_address, semaphore: asyncio.Semaphore) -> Union[None, ipaddress.ip_address]:
async with semaphore:
try:
miner = await ping_miner(ip)
if miner:
return miner
except ConnectionRefusedError:
tasks = [ping_miner(ip, port=port) for port in [4029, 8889]]
for miner in asyncio.as_completed(tasks):
try:
miner = await miner
if miner:
return miner
except ConnectionRefusedError:
pass
@staticmethod
async def ping_and_get_miner(
ip: ipaddress.ip_address,
ip: ipaddress.ip_address, semaphore: asyncio.Semaphore
) -> Union[None, AnyMiner]:
try:
miner = await ping_and_get_miner(ip)
if miner:
return miner
except ConnectionRefusedError:
tasks = [ping_and_get_miner(ip, port=port) for port in [4029, 8889]]
for miner in asyncio.as_completed(tasks):
try:
miner = await miner
if miner:
return miner
except ConnectionRefusedError:
pass
async with semaphore:
try:
miner = await ping_and_get_miner(ip)
if miner:
return miner
except ConnectionRefusedError:
tasks = [ping_and_get_miner(ip, port=port) for port in [4029, 8889]]
for miner in asyncio.as_completed(tasks):
try:
miner = await miner
if miner:
return miner
except ConnectionRefusedError:
pass
async def ping_miner(
@@ -262,6 +235,6 @@ async def ping_and_get_miner(
raise e
# ping failed, likely with an exception
except Exception as e:
logging.warning(f"{str(ip)}: {e}")
logging.warning(f"{str(ip)}: Ping And Get Miner Exception: {e}")
continue
return

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyasic"
version = "0.22.0"
version = "0.24.0"
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"