Compare commits

...

11 Commits

Author SHA1 Message Date
UpstreamData
f3746ff756 version: bump version number. 2023-11-20 11:19:45 -07:00
UpstreamData
9f16d37c8b feature: hide GRPC and GQL if BOSer is not found. 2023-11-20 11:19:13 -07:00
UpstreamData
8a13c7940a docs: update pyproject.toml description. 2023-11-20 10:33:25 -07:00
UpstreamData
8bea76ff67 feature: add chip count for M30S+VG50. 2023-11-20 10:32:51 -07:00
Upstream Data
1504bd744c version: bump version number. 2023-11-18 22:45:38 -07:00
Upstream Data
6449f10615 feature: implement GPRC set commands properly. 2023-11-18 22:45:09 -07:00
UpstreamData
d79509bda7 version: bump version number. Pin httpx to 0.25.0 min. 2023-11-12 18:36:45 -07:00
UpstreamData
630b847466 version: bump version number. Pin httpx to 0.25.0 min. 2023-11-12 18:35:52 -07:00
Colin Crossman
ed11611919 Bump version number
Note: some issues with HTTPX may be resolved by using 1.0.0b, but I did not bump the requirement at this time to the beta.
2023-11-11 13:59:14 -07:00
Colin Crossman
e2431c938d Address unknown password issue on Whatsminers
When a whatsminer had an unknown password (not the default one), it would result in a timeout error. By moving the password check to before the data pull step, the timeout issue can be caught and addressed efficiently.
2023-11-11 13:52:04 -07:00
Colin Crossman
60f4b4a5ed Address a situation which causes many asyncio errors 2023-11-11 13:49:51 -07:00
6 changed files with 198 additions and 95 deletions

View File

@@ -207,16 +207,18 @@ If you are sure you want to use this command please use API.send_command("{comma
logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining")
await writer.drain()
try:
ret_data = await asyncio.wait_for(reader.read(4096), timeout=timeout)
except ConnectionAbortedError:
return b"{}"
try:
# TO address a situation where a whatsminer has an unknown PW -AND-
# Fix for stupid whatsminer bug, reboot/restart seem to not load properly in the loop
# have to receive, save the data, check if there is more data by reading with a short timeout
# append that data if there is more, and then onto the main loop.
ret_data += await asyncio.wait_for(reader.read(1), timeout=1)
except asyncio.TimeoutError:
return ret_data
# the password timeout might need to be longer than 1, but it seems to work for now.
ret_data = await asyncio.wait_for(reader.read(1), timeout=1)
except (asyncio.TimeoutError):
return b"{}"
try:
ret_data += await asyncio.wait_for(reader.read(4096), timeout=timeout)
except (ConnectionAbortedError):
return b"{}"
# loop to receive all the data
logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving")

View File

@@ -184,11 +184,11 @@ BOSMINER_DATA_LOC = {
class BOSMiner(BaseMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
def __init__(self, ip: str, api_ver: str = "0.0.0", boser: bool = None) -> None:
super().__init__(ip)
# interfaces
self.api = BOSMinerAPI(ip, api_ver)
self.web = BOSMinerWebAPI(ip)
self.web = BOSMinerWebAPI(ip, boser=boser)
# static data
self.api_type = "BOSMiner"

View File

@@ -438,12 +438,19 @@ class MinerFactory:
if fn is not None:
task = asyncio.create_task(fn(ip))
try:
miner_model = await asyncio.wait_for(task, timeout=30)
miner_model = await asyncio.wait_for(task, timeout=TIMEOUT)
except asyncio.TimeoutError:
task.cancel()
boser_enabled = None
if miner_type == MinerTypes.BRAIINS_OS:
boser_enabled = await self.get_boser_braiins_os(ip)
miner = self._select_miner_from_classes(
ip, miner_type=miner_type, miner_model=miner_model
ip,
miner_type=miner_type,
miner_model=miner_model,
boser_enabled=boser_enabled,
)
if miner is not None and not isinstance(miner, UnknownMiner):
@@ -476,7 +483,12 @@ class MinerFactory:
try:
resp = await session.get(url, follow_redirects=False)
return resp.text, resp
except (httpx.HTTPError, asyncio.TimeoutError, anyio.EndOfStream):
except (
httpx.HTTPError,
asyncio.TimeoutError,
anyio.EndOfStream,
anyio.ClosedResourceError,
):
pass
return None, None
@@ -687,9 +699,13 @@ class MinerFactory:
ip: ipaddress.ip_address,
miner_model: Union[str, None],
miner_type: Union[MinerTypes, None],
boser_enabled: bool = None,
) -> AnyMiner:
kwargs = {}
if boser_enabled is not None:
kwargs["boser"] = boser_enabled
try:
return MINER_CLASSES[miner_type][str(miner_model).upper()](ip)
return MINER_CLASSES[miner_type][str(miner_model).upper()](ip, **kwargs)
except LookupError:
if miner_type in MINER_CLASSES:
return MINER_CLASSES[miner_type][None](ip)
@@ -817,6 +833,15 @@ class MinerFactory:
except (httpx.HTTPError, LookupError):
pass
async def get_boser_braiins_os(self, ip: str):
# TODO: refine this check
try:
sock_json_data = await self.send_api_command(ip, "version")
return sock_json_data["STATUS"][0]["Msg"].split(" ")[0].upper() == "BOSER"
except LookupError:
# let the bosminer class decide
return None
async def get_miner_model_vnish(self, ip: str) -> Optional[str]:
sock_json_data = await self.send_api_command(ip, "stats")
try:

View File

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

View File

@@ -15,11 +15,11 @@
# ------------------------------------------------------------------------------
import json
from datetime import datetime, timedelta
from enum import Enum
from typing import List, Union
import grpc_requests
import httpx
from google.protobuf.message import Message
from grpc import RpcError
from pyasic import APIError, settings
@@ -28,22 +28,53 @@ from pyasic.web.bosminer.proto import (
get_auth_service_descriptors,
get_service_descriptors,
)
from pyasic.web.bosminer.proto.bos.v1.actions import SaveAction
from pyasic.web.bosminer.proto.bos.v1.performance import (
ManualPerformanceMode,
PerformanceMode,
from pyasic.web.bosminer.proto.bos.v1.actions_pb2 import ( # noqa: this will be defined
SetLocateDeviceStatusRequest,
)
from pyasic.web.bosminer.proto.bos.v1.authentication_pb2 import ( # noqa: this will be defined
SetPasswordRequest,
)
from pyasic.web.bosminer.proto.bos.v1.common_pb2 import ( # noqa: this will be defined
SaveAction,
)
from pyasic.web.bosminer.proto.bos.v1.cooling_pb2 import ( # noqa: this will be defined
SetImmersionModeRequest,
)
from pyasic.web.bosminer.proto.bos.v1.miner_pb2 import ( # noqa: this will be defined
DisableHashboardsRequest,
EnableHashboardsRequest,
)
from pyasic.web.bosminer.proto.bos.v1.performance_pb2 import ( # noqa: this will be defined
DecrementHashrateTargetRequest,
DecrementPowerTargetRequest,
IncrementHashrateTargetRequest,
IncrementPowerTargetRequest,
SetDefaultHashrateTargetRequest,
SetDefaultPowerTargetRequest,
SetHashrateTargetRequest,
SetPowerTargetRequest,
)
class BOSMinerWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
self.gql = BOSMinerGQLAPI(ip, settings.get("default_bosminer_password", "root"))
def __init__(self, ip: str, boser: bool = None) -> None:
if boser is None:
boser = True
if boser:
self.gql = BOSMinerGQLAPI(
ip, settings.get("default_bosminer_password", "root")
)
self.grpc = BOSMinerGRPCAPI(
ip, settings.get("default_bosminer_password", "root")
)
else:
self.gql = None
self.grpc = None
self.luci = BOSMinerLuCIAPI(
ip, settings.get("default_bosminer_password", "root")
)
self.grpc = BOSMinerGRPCAPI(
ip, settings.get("default_bosminer_password", "root")
)
self._pwd = settings.get("default_bosminer_password", "root")
super().__init__(ip)
@@ -55,7 +86,10 @@ class BOSMinerWebAPI(BaseWebAPI):
def pwd(self, other: str):
self._pwd = other
self.luci.pwd = other
self.gql.pwd = other
if self.gql is not None:
self.gql.pwd = other
if self.grpc is not None:
self.grpc.pwd = other
async def send_command(
self,
@@ -65,30 +99,46 @@ class BOSMinerWebAPI(BaseWebAPI):
**parameters: Union[str, int, bool],
) -> dict:
if isinstance(command, dict):
return await self.gql.send_command(command)
if self.gql is not None:
return await self.gql.send_command(command)
elif command.startswith("/cgi-bin/luci"):
return await self.gql.send_command(command)
else:
if self.grpc is not None:
return await self.grpc.send_command(command)
async def multicommand(
self, *commands: Union[dict, str], allow_warning: bool = True
) -> dict:
luci_commands = []
gql_commands = []
grpc_commands = []
for cmd in commands:
if isinstance(cmd, dict):
gql_commands.append(cmd)
elif cmd.startswith("/cgi-bin/luci"):
luci_commands.append(cmd)
else:
grpc_commands.append(cmd)
luci_data = await self.luci.multicommand(*luci_commands)
gql_data = await self.gql.multicommand(*gql_commands)
if self.gql is not None:
gql_data = await self.gql.multicommand(*gql_commands)
else:
gql_data = None
if self.grpc is not None:
grpc_data = await self.grpc.multicommand(*grpc_commands)
else:
grpc_data = None
if gql_data is None:
gql_data = {}
if luci_data is None:
luci_data = {}
if grpc_data is None:
grpc_data = {}
data = dict(**luci_data, **gql_data)
data = dict(**luci_data, **gql_data, **grpc_data)
return data
@@ -265,7 +315,11 @@ class BOSMinerGRPCAPI:
pass
async def send_command(
self, command: str, ignore_errors: bool = False, auth: bool = True, **parameters
self,
command: str,
message: Message = None,
ignore_errors: bool = False,
auth: bool = True,
) -> dict:
service, method = command.split("/")
metadata = []
@@ -279,7 +333,7 @@ class BOSMinerGRPCAPI:
return await client.request(
service,
method,
request=parameters,
request=message,
metadata=metadata,
)
except RpcError as e:
@@ -339,8 +393,10 @@ class BOSMinerGRPCAPI:
return await self.send_command("braiins.bos.v1.ActionsService/Reboot")
async def set_locate_device_status(self, enable: bool):
message = SetLocateDeviceStatusRequest()
message.enable = enable
return await self.send_command(
"braiins.bos.v1.ActionsService/SetLocateDeviceStatus", enable=enable
"braiins.bos.v1.ActionsService/SetLocateDeviceStatus", message=message
)
async def get_locate_device_status(self):
@@ -349,23 +405,26 @@ class BOSMinerGRPCAPI:
)
async def set_password(self, password: str = None):
kwargs = {}
message = SetPasswordRequest()
if password:
kwargs["password"] = password
message.password = password
return await self.send_command(
"braiins.bos.v1.AuthenticationService/SetPassword", **kwargs
"braiins.bos.v1.AuthenticationService/SetPassword", message=message
)
async def get_cooling_state(self):
return await self.send_command("braiins.bos.v1.CoolingService/GetCoolingState")
async def set_immersion_mode(
self, enable: bool, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
self,
enable: bool,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = SetImmersionModeRequest()
message.enable = enable
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.CoolingService/SetImmersionMode",
save_action=save_action,
enable_immersion_mode=enable,
"braiins.bos.v1.CoolingService/SetImmersionMode", message=message
)
async def get_tuner_state(self):
@@ -379,101 +438,117 @@ class BOSMinerGRPCAPI:
)
async def set_default_power_target(
self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
self, save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY
):
message = SetDefaultPowerTargetRequest()
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.PerformanceService/SetDefaultPowerTarget",
save_action=save_action,
"braiins.bos.v1.PerformanceService/SetDefaultPowerTarget", message=message
)
async def set_power_target(
self, power_target: int, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
self,
power_target: int,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = SetPowerTargetRequest()
message.power_target.watt = power_target
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.PerformanceService/SetPowerTarget",
save_action=save_action,
power_target=power_target,
"braiins.bos.v1.PerformanceService/SetPowerTarget", message=message
)
async def increment_power_target(
self,
power_target_increment: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = IncrementPowerTargetRequest()
message.power_target_increment.watt = power_target_increment
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.PerformanceService/IncrementPowerTarget",
save_action=save_action,
power_target_increment=power_target_increment,
"braiins.bos.v1.PerformanceService/IncrementPowerTarget", message=message
)
async def decrement_power_target(
self,
power_target_decrement: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = DecrementPowerTargetRequest()
message.power_target_decrement.watt = power_target_decrement
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.PerformanceService/DecrementPowerTarget",
save_action=save_action,
power_target_decrement=power_target_decrement,
message=message,
)
async def set_default_hashrate_target(
self, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
self, save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY
):
message = SetDefaultHashrateTargetRequest()
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.PerformanceService/SetDefaultHashrateTarget",
save_action=save_action,
message=message,
)
async def set_hashrate_target(
self, hashrate_target: int, save_action: SaveAction = SaveAction.SAVE_AND_APPLY
self,
hashrate_target: int,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = SetHashrateTargetRequest()
message.hashrate_target.terahash_per_second = hashrate_target
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.PerformanceService/SetHashrateTarget",
save_action=save_action,
hashrate_target=hashrate_target,
"braiins.bos.v1.PerformanceService/SetHashrateTarget", message=message
)
async def increment_hashrate_target(
self,
hashrate_target_increment: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = IncrementHashrateTargetRequest()
message.hashrate_target_increment.terahash_per_second = (
hashrate_target_increment
)
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.PerformanceService/IncrementHashrateTarget",
save_action=save_action,
hashrate_target_increment=hashrate_target_increment,
message=message,
)
async def decrement_hashrate_target(
self,
hashrate_target_decrement: int,
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = DecrementHashrateTargetRequest()
message.hashrate_target_decrement.terahash_per_second = (
hashrate_target_decrement
)
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.PerformanceService/DecrementHashrateTarget",
save_action=save_action,
hashrate_target_decrement=hashrate_target_decrement,
message=message,
)
async def set_dps(self):
raise NotImplementedError
return await self.send_command("braiins.bos.v1.PerformanceService/SetDPS")
async def set_performance_mode(
self,
power_target: int = None,
hashrate_target: float = None,
manual_config: ManualPerformanceMode = None,
):
config = PerformanceMode.create(
power_target=power_target,
hashrate_target=hashrate_target,
manual_configuration=manual_config,
)
async def set_performance_mode(self):
raise NotImplementedError
return await self.send_command(
"braiins.bos.v1.PerformanceService/SetPerformanceMode", **config
"braiins.bos.v1.PerformanceService/SetPerformanceMode"
)
async def get_active_performance_mode(self):
@@ -527,21 +602,25 @@ class BOSMinerGRPCAPI:
async def enable_hashboards(
self,
hashboard_ids: List[str],
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = EnableHashboardsRequest()
message.hashboard_ids[:] = hashboard_ids
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.MinerService/EnableHashboards",
hashboard_ids=hashboard_ids,
save_action=save_action,
"braiins.bos.v1.MinerService/EnableHashboards", message=message
)
async def disable_hashboards(
self,
hashboard_ids: List[str],
save_action: SaveAction = SaveAction.SAVE_AND_APPLY,
save_action: SaveAction = SaveAction.SAVE_ACTION_SAVE_AND_APPLY,
):
message = DisableHashboardsRequest()
message.hashboard_ids[:] = hashboard_ids
message.save_action = save_action
return await self.send_command(
"braiins.bos.v1.MinerService/DisableHashboards",
hashboard_ids=hashboard_ids,
save_action=save_action,
"braiins.bos.v1.MinerService/DisableHashboards", message=message
)

View File

@@ -1,7 +1,7 @@
[tool.poetry]
name = "pyasic"
version = "0.40.1"
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
version = "0.40.5"
description = "A simplified and standardized interface for Bitcoin ASICs."
authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic"
documentation = "https://pyasic.readthedocs.io/en/latest/"
@@ -9,12 +9,12 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.8"
asyncssh = "^2.13.1"
httpx = "^0.24.0"
passlib = "^1.7.4"
pyaml = "^23.5.9"
toml = "^0.10.2"
httpx = "^0.25.0"
asyncssh = "^2.14.1"
grpc-requests = "^0.1.11"
passlib = "^1.7.4"
pyaml = "^23.9.7"
toml = "^0.10.2"
[tool.poetry.group.dev]
optional = true