diff --git a/pyasic/miners/backends/btminer.py b/pyasic/miners/backends/btminer.py index f7e1442d..f1bf1993 100644 --- a/pyasic/miners/backends/btminer.py +++ b/pyasic/miners/backends/btminer.py @@ -783,67 +783,67 @@ class BTMinerV2(StockFirmware): BTMINERV3_DATA_LOC = DataLocations( **{ str(DataOptions.MAC): DataFunction( - "_get_mac", [RPCAPICommand("rpc_get_device_info", "get_device_info")] + "_get_mac", [RPCAPICommand("rpc_get_device_info", "get.device.info")] ), str(DataOptions.API_VERSION): DataFunction( "_get_api_version", - [RPCAPICommand("rpc_get_device_info", "get_device_info")], + [RPCAPICommand("rpc_get_device_info", "get.device.info")], ), str(DataOptions.FW_VERSION): DataFunction( "_get_firmware_version", - [RPCAPICommand("rpc_get_device_info", "get_device_info")], + [RPCAPICommand("rpc_get_device_info", "get.device.info")], ), str(DataOptions.HOSTNAME): DataFunction( - "_get_hostname", [RPCAPICommand("rpc_get_device_info", "get_device_info")] + "_get_hostname", [RPCAPICommand("rpc_get_device_info", "get.device.info")] ), str(DataOptions.FAULT_LIGHT): DataFunction( "_get_light_flashing", - [RPCAPICommand("rpc_get_device_info", "get_device_info")], + [RPCAPICommand("rpc_get_device_info", "get.device.info")], ), str(DataOptions.WATTAGE_LIMIT): DataFunction( "_get_wattage_limit", - [RPCAPICommand("rpc_get_device_info", "get_device_info")], + [RPCAPICommand("rpc_get_device_info", "get.device.info")], ), str(DataOptions.FANS): DataFunction( "_get_fans", - [RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], + [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")], ), str(DataOptions.FAN_PSU): DataFunction( - "_get_psu_fans", [RPCAPICommand("rpc_get_device_info", "get_device_info")] + "_get_psu_fans", [RPCAPICommand("rpc_get_device_info", "get.device.info")] ), str(DataOptions.HASHBOARDS): DataFunction( "_get_hashboards", [ - RPCAPICommand("rpc_get_device_info", "get_device_info"), + RPCAPICommand("rpc_get_device_info", "get.device.info"), RPCAPICommand( "rpc_get_miner_status_edevs", - "get_miner_status_edevs", + "get.miner.status:edevs", ), ], ), str(DataOptions.POOLS): DataFunction( "_get_pools", - [RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], + [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")], ), str(DataOptions.UPTIME): DataFunction( "_get_uptime", - [RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], + [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")], ), str(DataOptions.WATTAGE): DataFunction( "_get_wattage", - [RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], + [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")], ), str(DataOptions.HASHRATE): DataFunction( "_get_hashrate", - [RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], + [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")], ), str(DataOptions.EXPECTED_HASHRATE): DataFunction( "_get_expected_hashrate", - [RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], + [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")], ), str(DataOptions.ENVIRONMENT_TEMP): DataFunction( "_get_env_temp", - [RPCAPICommand("rpc_get_miner_status_summary", "get_miner_status_summary")], + [RPCAPICommand("rpc_get_miner_status_summary", "get.miner.status:summary")], ), } ) @@ -1082,7 +1082,9 @@ class BTMinerV3(StockFirmware): temp=board_data.get("chip-temp-min"), inlet_temp=board_data.get("chip-temp-min"), outlet_temp=board_data.get("chip-temp-max"), - serial_number=board_data.get(f"pcbsn{idx}"), + serial_number=rpc_get_device_info.get("msg", {}) + .get("miner", {}) + .get(f"pcbsn{idx}"), chips=board_data.get("effective-chips"), expected_chips=self.expected_chips, active=(board_data.get("hash-average") or 0) > 0, @@ -1164,12 +1166,16 @@ class BTMinerV3(StockFirmware): rpc_get_miner_status_summary = await self.rpc.get_miner_status_summary() except APIError: return None - return ( + res = ( rpc_get_miner_status_summary.get("msg", {}) .get("summary", {}) .get("factory-hash") ) + if res == (-0.001 * self.expected_hashboards): + return None + return res + async def _get_env_temp( self, rpc_get_miner_status_summary: dict = None ) -> float | None: diff --git a/pyasic/rpc/btminer.py b/pyasic/rpc/btminer.py index 364157a9..16596f41 100644 --- a/pyasic/rpc/btminer.py +++ b/pyasic/rpc/btminer.py @@ -23,6 +23,7 @@ import json import logging import re import struct +import warnings from asyncio import Future, StreamReader, StreamWriter from typing import Any, AsyncGenerator, Callable, Literal, Union @@ -31,7 +32,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from passlib.handlers.md5_crypt import md5_crypt from pyasic import settings -from pyasic.errors import APIError +from pyasic.errors import APIError, APIWarning from pyasic.misc import api_min_version, validate_command_output from pyasic.rpc.base import BaseMinerRPCAPI @@ -1107,39 +1108,27 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI): def __init__(self, ip: str, port: int = 4433, api_ver: str = "0.0.0"): super().__init__(ip, port, api_ver=api_ver) - self.reader: StreamReader | None = None - self.writer: StreamWriter | None = None - self.reader_loop = None - self.salt = None - self.cmd_results = {} - self.cmd_callbacks = {"get.miner.report": set()} + async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict: + """Creates and sends multiple commands as one command to the miner. - async def connect(self): - self.reader, self.writer = await asyncio.open_connection( - str(self.ip), self.port - ) - self.reader_loop = asyncio.create_task(self._read_loop()) + Parameters: + *commands: The commands to send as a multicommand to the miner. + allow_warning: A boolean to supress APIWarnings. - async def disconnect(self): - self.writer.close() - await self.writer.wait_closed() - self.reader_loop.cancel() + """ + commands = self._check_commands(*commands) + data = await self._send_split_multicommand(*commands) + data["multicommand"] = True + return data async def send_command( self, command: str, parameters: Any = None, **kwargs ) -> dict: - if self.writer is None: - await self.connect() - - while command in self.cmd_results: - wait_fut = self.cmd_results[command] - await wait_fut - - result_fut = Future() - self.cmd_results[command] = result_fut - + if ":" in command: + parameters = command.split(":")[1] + command = command.split(":")[0] cmd = {"cmd": command} if parameters is not None: cmd["param"] = parameters @@ -1159,33 +1148,80 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI): # send the command ser = json.dumps(cmd).encode("utf-8") header = struct.pack(" bytes: + if port is None: + port = self.port + logging.debug(f"{self} - ([Hidden] Send Bytes) - Sending") + try: + # get reader and writer streams + reader, writer = await asyncio.open_connection(str(self.ip), port) + # handle OSError 121 + except OSError as e: + if e.errno == 121: + logging.warning( + f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired." + ) + return b"{}" - async def _read_loop(self): - while True: - result = await self._read_bytes() - data = self._load_api_data(result) - command = data["desc"] - if command in self.cmd_callbacks: - callbacks: list[Callable] = self.cmd_callbacks[command] - await asyncio.gather(*[callback(data) for callback in callbacks]) - elif command in self.cmd_results: - future: Future = self.cmd_results.pop(command) - future.set_result(data) + # send the command + try: + data_task = asyncio.create_task(self._read_bytes(reader, timeout=timeout)) + logging.debug(f"{self} - ([Hidden] Send Bytes) - Writing") + writer.write(data) + logging.debug(f"{self} - ([Hidden] Send Bytes) - Draining") + await writer.drain() + + await data_task + ret_data = data_task.result() + except TimeoutError: + logging.warning(f"{self} - ([Hidden] Send Bytes) - Read timeout expired.") + return b"{}" + + # close the connection + logging.debug(f"{self} - ([Hidden] Send Bytes) - Closing") + writer.close() + await writer.wait_closed() + + return ret_data + + def _check_commands(self, *commands) -> list: + return_commands = [] + + for command in commands: + if command.startswith("get.") or command.startswith("set."): + return_commands.append(command) else: - logging.error(f"Received unexpected data for {self}: {data}") + warnings.warn( + f"""Removing incorrect command: {command} +If you are sure you want to use this command please use API.send_command("{command}", ignore_errors=True) instead.""", + APIWarning, + ) + return return_commands - async def _read_bytes(self, **kwargs) -> bytes: - header = await self.reader.readexactly(4) - length = struct.unpack(" bytes: + ret_data = b"" - async def _send_bytes(self, data: bytes, **kwargs): - self.writer.write(data) - await self.writer.drain() + # loop to receive all the data + logging.debug(f"{self} - ([Hidden] Send Bytes) - Receiving") + try: + header = await reader.readexactly(4) + length = struct.unpack(" str: if self.salt is not None: