Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb0aee8337 | ||
|
|
2a23acb73a | ||
|
|
ad5eb0cef6 | ||
|
|
07dd8f55fe | ||
|
|
bafa91cc47 | ||
|
|
16399510fa |
@@ -117,10 +117,11 @@ These functions are
|
|||||||
[`get_hostname`](#get-hostname),
|
[`get_hostname`](#get-hostname),
|
||||||
[`get_model`](#get-model),
|
[`get_model`](#get-model),
|
||||||
[`reboot`](#reboot),
|
[`reboot`](#reboot),
|
||||||
[`restart_backend`](#restart-backend), and
|
[`restart_backend`](#restart-backend),
|
||||||
[`stop_mining`](#stop-mining), and
|
[`stop_mining`](#stop-mining),
|
||||||
[`resume_mining`](#resume-mining), and
|
[`resume_mining`](#resume-mining),
|
||||||
[`send_config`](#send-config).
|
[`send_config`](#send-config), and
|
||||||
|
[`set_power_limit`](#set-power-limit).
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@@ -228,6 +229,14 @@ These functions are
|
|||||||
|
|
||||||
<br>
|
<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]
|
## [`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()`.
|
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()`.
|
||||||
|
|||||||
@@ -35,6 +35,81 @@ class BaseMinerAPI:
|
|||||||
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
|
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
|
||||||
return object.__new__(cls)
|
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:
|
def get_commands(self) -> list:
|
||||||
"""Get a list of command accessible to a specific type of API on the miner.
|
"""Get a list of command accessible to a specific type of API on the miner.
|
||||||
|
|
||||||
@@ -72,26 +147,6 @@ If you are sure you want to use this command please use API.send_command("{comma
|
|||||||
)
|
)
|
||||||
return return_commands
|
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:
|
async def _send_bytes(self, data: bytes) -> bytes:
|
||||||
try:
|
try:
|
||||||
# get reader and writer streams
|
# get reader and writer streams
|
||||||
@@ -99,7 +154,7 @@ If you are sure you want to use this command please use API.send_command("{comma
|
|||||||
# handle OSError 121
|
# handle OSError 121
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if getattr(e, "winerror") == "121":
|
if getattr(e, "winerror") == "121":
|
||||||
logging.warning("Semaphore Timeout has Expired.")
|
logging.warning(f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired.")
|
||||||
return b"{}"
|
return b"{}"
|
||||||
|
|
||||||
# send the command
|
# send the command
|
||||||
@@ -117,7 +172,7 @@ If you are sure you want to use this command please use API.send_command("{comma
|
|||||||
break
|
break
|
||||||
ret_data += d
|
ret_data += d
|
||||||
except Exception as e:
|
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
|
# close the connection
|
||||||
writer.close()
|
writer.close()
|
||||||
@@ -125,50 +180,6 @@ If you are sure you want to use this command please use API.send_command("{comma
|
|||||||
|
|
||||||
return ret_data
|
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
|
@staticmethod
|
||||||
def _validate_command_output(data: dict) -> tuple:
|
def _validate_command_output(data: dict) -> tuple:
|
||||||
# check if the data returned is correct or an error
|
# check if the data returned is correct or an error
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class BMMinerAPI(BaseMinerAPI):
|
|||||||
async def multicommand(
|
async def multicommand(
|
||||||
self, *commands: str, allow_warning: bool = True
|
self, *commands: str, allow_warning: bool = True
|
||||||
) -> dict:
|
) -> dict:
|
||||||
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
|
|
||||||
# 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"
|
||||||
@@ -49,9 +48,8 @@ class BMMinerAPI(BaseMinerAPI):
|
|||||||
try:
|
try:
|
||||||
data = await self.send_command(command, allow_warning=allow_warning)
|
data = await self.send_command(command, allow_warning=allow_warning)
|
||||||
except APIError:
|
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("+"))
|
data = await self._x19_multicommand(*command.split("+"))
|
||||||
logging.debug(f"{self.ip}: Received multicommand data.")
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def _x19_multicommand(self, *commands):
|
async def _x19_multicommand(self, *commands):
|
||||||
@@ -65,7 +63,7 @@ class BMMinerAPI(BaseMinerAPI):
|
|||||||
except APIError as e:
|
except APIError as e:
|
||||||
raise APIError(e)
|
raise APIError(e)
|
||||||
except Exception as 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
|
return data
|
||||||
|
|
||||||
async def version(self) -> dict:
|
async def version(self) -> dict:
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class BOSMinerAPI(BaseMinerAPI):
|
|||||||
return await self.send_command("estats")
|
return await self.send_command("estats")
|
||||||
|
|
||||||
async def check(self, command: str) -> dict:
|
async def check(self, command: str) -> dict:
|
||||||
"""Check if the command command exists in BOSMiner.
|
"""Check if the command `command` exists in BOSMiner.
|
||||||
<details>
|
<details>
|
||||||
<summary>Expand</summary>
|
<summary>Expand</summary>
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from pyasic.settings import PyasicSettings
|
|||||||
# tool, then you can set them back to admin with this tool, but they
|
# 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
|
# 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
|
# 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.
|
# 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):
|
def parse_btminer_priviledge_data(token_data: dict, data: dict):
|
||||||
"""Parses data returned from the BTMiner privileged API.
|
"""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.
|
from the API in an AES format.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
@@ -188,6 +188,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
async def send_privileged_command(
|
async def send_privileged_command(
|
||||||
self, command: Union[str, bytes], ignore_errors: bool = False, **kwargs
|
self, command: Union[str, bytes], ignore_errors: bool = False, **kwargs
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
logging.debug(f"{self} - (Send Privileged Command) - {command} " + f'with args {kwargs}' if len(kwargs) > 0 else '')
|
||||||
command = {"cmd": command}
|
command = {"cmd": command}
|
||||||
for kwarg in kwargs:
|
for kwarg in kwargs:
|
||||||
if kwargs[kwarg]:
|
if kwargs[kwarg]:
|
||||||
@@ -222,6 +223,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
An encoded token and md5 password, which are used for the privileged API.
|
An encoded token and md5 password, which are used for the privileged API.
|
||||||
</details>
|
</details>
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"{self} - (Get Token) - Getting token")
|
||||||
# get the token
|
# get the token
|
||||||
data = await self.send_command("get_token")
|
data = await self.send_command("get_token")
|
||||||
|
|
||||||
@@ -244,6 +246,7 @@ class BTMinerAPI(BaseMinerAPI):
|
|||||||
"host_sign": host_sign,
|
"host_sign": host_sign,
|
||||||
"host_passwd_md5": host_passwd_md5,
|
"host_passwd_md5": host_passwd_md5,
|
||||||
}
|
}
|
||||||
|
logging.debug(f"{self} - (Get Token) - Gathered token data: {self.current_token}")
|
||||||
return self.current_token
|
return self.current_token
|
||||||
|
|
||||||
#### PRIVILEGED COMMANDS ####
|
#### PRIVILEGED COMMANDS ####
|
||||||
|
|||||||
@@ -41,18 +41,16 @@ class CGMinerAPI(BaseMinerAPI):
|
|||||||
async def multicommand(
|
async def multicommand(
|
||||||
self, *commands: str, allow_warning: bool = True
|
self, *commands: str, allow_warning: bool = True
|
||||||
) -> dict:
|
) -> dict:
|
||||||
logging.debug(f"{self.ip}: Sending multicommand: {[*commands]}")
|
|
||||||
# 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"
|
||||||
# doesnt work for S19 which uses the backup _x19_multicommand
|
# doesn't work for S19 which uses the backup _x19_multicommand
|
||||||
command = "+".join(commands)
|
command = "+".join(commands)
|
||||||
try:
|
try:
|
||||||
data = await self.send_command(command, allow_warning=allow_warning)
|
data = await self.send_command(command, allow_warning=allow_warning)
|
||||||
except APIError:
|
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("+"))
|
data = await self._x19_multicommand(*command.split("+"))
|
||||||
logging.debug(f"{self.ip}: Received multicommand data.")
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def _x19_multicommand(self, *commands):
|
async def _x19_multicommand(self, *commands):
|
||||||
@@ -66,7 +64,7 @@ class CGMinerAPI(BaseMinerAPI):
|
|||||||
except APIError as e:
|
except APIError as e:
|
||||||
raise APIError(e)
|
raise APIError(e)
|
||||||
except Exception as 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
|
return data
|
||||||
|
|
||||||
async def version(self) -> dict:
|
async def version(self) -> dict:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from pyasic.API import BaseMinerAPI
|
|||||||
class UnknownAPI(BaseMinerAPI):
|
class UnknownAPI(BaseMinerAPI):
|
||||||
"""An abstraction of an API for a miner which is unknown.
|
"""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
|
and API commands as possible (API ⋂ API), to ensure that it can be used
|
||||||
with as many APIs as possible.
|
with as many APIs as possible.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
from dataclasses import asdict, dataclass, fields
|
from dataclasses import asdict, dataclass, fields
|
||||||
from typing import List, Literal, Dict
|
from typing import Dict, List, Literal
|
||||||
|
|
||||||
import toml
|
import toml
|
||||||
import yaml
|
import yaml
|
||||||
@@ -277,6 +278,7 @@ class MinerConfig:
|
|||||||
|
|
||||||
def as_dict(self) -> dict:
|
def as_dict(self) -> dict:
|
||||||
"""Convert the data in this class to a dict."""
|
"""Convert the data in this class to a dict."""
|
||||||
|
logging.debug(f"MinerConfig - (To Dict) - Dumping Dict config")
|
||||||
data_dict = asdict(self)
|
data_dict = asdict(self)
|
||||||
for key in asdict(self).keys():
|
for key in asdict(self).keys():
|
||||||
if data_dict[key] is None:
|
if data_dict[key] is None:
|
||||||
@@ -285,10 +287,12 @@ class MinerConfig:
|
|||||||
|
|
||||||
def as_toml(self) -> str:
|
def as_toml(self) -> str:
|
||||||
"""Convert the data in this class to toml."""
|
"""Convert the data in this class to toml."""
|
||||||
|
logging.debug(f"MinerConfig - (To TOML) - Dumping TOML config")
|
||||||
return toml.dumps(self.as_dict())
|
return toml.dumps(self.as_dict())
|
||||||
|
|
||||||
def as_yaml(self) -> str:
|
def as_yaml(self) -> str:
|
||||||
"""Convert the data in this class to yaml."""
|
"""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)
|
return yaml.dump(self.as_dict(), sort_keys=False)
|
||||||
|
|
||||||
def from_raw(self, data: dict):
|
def from_raw(self, data: dict):
|
||||||
@@ -298,6 +302,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
data: The raw config data to convert.
|
data: The raw config data to convert.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (From Raw) - Loading raw config")
|
||||||
pool_groups = []
|
pool_groups = []
|
||||||
for key in data.keys():
|
for key in data.keys():
|
||||||
if key == "pools":
|
if key == "pools":
|
||||||
@@ -360,6 +365,12 @@ class MinerConfig:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def from_api(self, pools: list):
|
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 = []
|
_pools = []
|
||||||
for pool in pools:
|
for pool in pools:
|
||||||
url = pool.get("URL")
|
url = pool.get("URL")
|
||||||
@@ -374,6 +385,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
data: The dict config data to convert.
|
data: The dict config data to convert.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (From Dict) - Loading Dict config")
|
||||||
pool_groups = []
|
pool_groups = []
|
||||||
for group in data["pool_groups"]:
|
for group in data["pool_groups"]:
|
||||||
pool_groups.append(_PoolGroup().from_dict(group))
|
pool_groups.append(_PoolGroup().from_dict(group))
|
||||||
@@ -389,6 +401,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
data: The toml config data to convert.
|
data: The toml config data to convert.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (From TOML) - Loading TOML config")
|
||||||
return self.from_dict(toml.loads(data))
|
return self.from_dict(toml.loads(data))
|
||||||
|
|
||||||
def from_yaml(self, data: str):
|
def from_yaml(self, data: str):
|
||||||
@@ -397,6 +410,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
data: The yaml config data to convert.
|
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))
|
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
|
||||||
|
|
||||||
def as_wm(self, user_suffix: str = None) -> Dict[str, int]:
|
def as_wm(self, user_suffix: str = None) -> Dict[str, int]:
|
||||||
@@ -405,6 +419,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
user_suffix: The suffix to append to username.
|
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}
|
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:
|
def as_inno(self, user_suffix: str = None) -> dict:
|
||||||
@@ -413,6 +428,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
user_suffix: The suffix to append to username.
|
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)
|
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
|
||||||
|
|
||||||
def as_x19(self, user_suffix: str = None) -> str:
|
def as_x19(self, user_suffix: str = None) -> str:
|
||||||
@@ -421,6 +437,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
user_suffix: The suffix to append to username.
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (As X19) - Generating X19 config")
|
||||||
cfg = {
|
cfg = {
|
||||||
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
|
||||||
"bitmain-fan-ctrl": False,
|
"bitmain-fan-ctrl": False,
|
||||||
@@ -444,6 +461,7 @@ class MinerConfig:
|
|||||||
Parameters:
|
Parameters:
|
||||||
user_suffix: The suffix to append to username.
|
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)
|
cfg = self.pool_groups[0].as_avalon(user_suffix=user_suffix)
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
@@ -454,6 +472,7 @@ class MinerConfig:
|
|||||||
model: The model of the miner to be used in the format portion of the config.
|
model: The model of the miner to be used in the format portion of the config.
|
||||||
user_suffix: The suffix to append to username.
|
user_suffix: The suffix to append to username.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerConfig - (As BOS) - Generating BOSMiner config")
|
||||||
cfg = {
|
cfg = {
|
||||||
"format": {
|
"format": {
|
||||||
"version": "1.2+",
|
"version": "1.2+",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
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
|
||||||
@@ -371,6 +372,7 @@ class MinerData:
|
|||||||
Returns:
|
Returns:
|
||||||
A dictionary version of this class.
|
A dictionary version of this class.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerData - (To Dict) - Dumping Dict data")
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
def as_json(self) -> str:
|
def as_json(self) -> str:
|
||||||
@@ -379,6 +381,7 @@ class MinerData:
|
|||||||
Returns:
|
Returns:
|
||||||
A JSON version of this class.
|
A JSON version of this class.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerData - (To JSON) - Dumping JSON data")
|
||||||
data = self.asdict()
|
data = self.asdict()
|
||||||
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
||||||
return json.dumps(data)
|
return json.dumps(data)
|
||||||
@@ -389,6 +392,7 @@ class MinerData:
|
|||||||
Returns:
|
Returns:
|
||||||
A CSV version of this class with no headers.
|
A CSV version of this class with no headers.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerData - (To CSV) - Dumping CSV data")
|
||||||
data = self.asdict()
|
data = self.asdict()
|
||||||
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
data["datetime"] = str(int(time.mktime(data["datetime"].timetuple())))
|
||||||
errs = []
|
errs = []
|
||||||
@@ -407,6 +411,7 @@ class MinerData:
|
|||||||
Returns:
|
Returns:
|
||||||
A influxdb line protocol version of this class.
|
A influxdb line protocol version of this class.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"MinerData - (To InfluxDB) - Dumping InfluxDB data")
|
||||||
tag_data = [measurement_name]
|
tag_data = [measurement_name]
|
||||||
field_data = []
|
field_data = []
|
||||||
|
|
||||||
|
|||||||
@@ -349,3 +349,7 @@ class BMMiner(BaseMiner):
|
|||||||
data.pool_split = str(quota)
|
data.pool_split = str(quota)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
import toml
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import toml
|
||||||
|
|
||||||
from pyasic.API.bosminer import BOSMinerAPI
|
from pyasic.API.bosminer import BOSMinerAPI
|
||||||
from pyasic.config import MinerConfig
|
from pyasic.config import MinerConfig
|
||||||
@@ -671,3 +671,14 @@ class BOSMiner(BaseMiner):
|
|||||||
async def get_mac(self):
|
async def get_mac(self):
|
||||||
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
result = await self.send_ssh_command("cat /sys/class/net/eth0/address")
|
||||||
return result.upper().strip()
|
return result.upper().strip()
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -106,3 +106,6 @@ class BOSMinerOld(BaseMiner):
|
|||||||
|
|
||||||
async def get_data(self, **kwargs) -> MinerData:
|
async def get_data(self, **kwargs) -> MinerData:
|
||||||
return MinerData(ip=str(self.ip))
|
return MinerData(ip=str(self.ip))
|
||||||
|
|
||||||
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -196,14 +196,20 @@ class BTMiner(BaseMiner):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def stop_mining(self) -> bool:
|
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.get("Msg"):
|
||||||
if data["Msg"] == "API command OK":
|
if data["Msg"] == "API command OK":
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def resume_mining(self) -> bool:
|
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.get("Msg"):
|
||||||
if data["Msg"] == "API command OK":
|
if data["Msg"] == "API command OK":
|
||||||
return True
|
return True
|
||||||
@@ -438,3 +444,13 @@ class BTMiner(BaseMiner):
|
|||||||
data.mac = mac
|
data.mac = mac
|
||||||
|
|
||||||
return data
|
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
|
||||||
|
|||||||
@@ -335,3 +335,6 @@ class CGMiner(BaseMiner):
|
|||||||
data.pool_split = str(quota)
|
data.pool_split = str(quota)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -61,3 +61,6 @@ class Hiveon(BMMiner):
|
|||||||
bad_boards[board] = []
|
bad_boards[board] = []
|
||||||
bad_boards[board].append(chain)
|
bad_boards[board].append(chain)
|
||||||
return bad_boards
|
return bad_boards
|
||||||
|
|
||||||
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ import asyncssh
|
|||||||
|
|
||||||
from pyasic.config import MinerConfig
|
from pyasic.config import MinerConfig
|
||||||
from pyasic.data import MinerData
|
from pyasic.data import MinerData
|
||||||
from pyasic.data.error_codes import (
|
from pyasic.data.error_codes import MinerErrorData
|
||||||
MinerErrorData,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMiner(ABC):
|
class BaseMiner(ABC):
|
||||||
@@ -216,4 +214,12 @@ class BaseMiner(ABC):
|
|||||||
pass
|
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)
|
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)
|
||||||
|
|||||||
@@ -74,3 +74,6 @@ class UnknownMiner(BaseMiner):
|
|||||||
|
|
||||||
async def get_data(self, allow_warning: bool = False) -> MinerData:
|
async def get_data(self, allow_warning: bool = False) -> MinerData:
|
||||||
return MinerData(ip=str(self.ip))
|
return MinerData(ip=str(self.ip))
|
||||||
|
|
||||||
|
async def set_power_limit(self, wattage: int) -> bool:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class MinerNetwork:
|
|||||||
f"{self.ip_addr}/{subnet_mask}", strict=False
|
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
|
return self.network
|
||||||
|
|
||||||
async def scan_network_for_miners(self) -> List[AnyMiner]:
|
async def scan_network_for_miners(self) -> List[AnyMiner]:
|
||||||
@@ -101,7 +101,7 @@ class MinerNetwork:
|
|||||||
"""
|
"""
|
||||||
# get the network
|
# get the network
|
||||||
local_network = self.get_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
|
# clear cached miners
|
||||||
MinerFactory().clear_cached_miners()
|
MinerFactory().clear_cached_miners()
|
||||||
@@ -130,7 +130,7 @@ class MinerNetwork:
|
|||||||
|
|
||||||
# remove all None from the miner list
|
# remove all None from the miner list
|
||||||
miners = list(filter(None, miners))
|
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 the miner objects
|
||||||
return miners
|
return miners
|
||||||
@@ -262,6 +262,6 @@ async def ping_and_get_miner(
|
|||||||
raise e
|
raise e
|
||||||
# ping failed, likely with an exception
|
# ping failed, likely with an exception
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"{str(ip)}: {e}")
|
logging.warning(f"{str(ip)}: Ping And Get Miner Exception: {e}")
|
||||||
continue
|
continue
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pyasic"
|
name = "pyasic"
|
||||||
version = "0.22.2"
|
version = "0.23.1"
|
||||||
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
|
||||||
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
||||||
repository = "https://github.com/UpstreamData/pyasic"
|
repository = "https://github.com/UpstreamData/pyasic"
|
||||||
|
|||||||
Reference in New Issue
Block a user