Compare commits

...

9 Commits

Author SHA1 Message Date
Brett Rowan
a0fdec3ce5 version: bump version number 2025-08-16 10:06:58 -06:00
Brett Rowan
1a8928de18 feature: add retries to elphapex 2025-08-16 10:06:46 -06:00
Brett Rowan
bce0058930 version: bump version number 2025-08-16 09:38:24 -06:00
Brett Rowan
850656fce4 bug: fix elphapex hashboard parsing 2025-08-16 09:38:06 -06:00
Brett Rowan
8bb35d6d7c version: bump version number 2025-08-16 09:24:17 -06:00
Brett Rowan
e85f06dbc2 feature: add misidentified L9_i 2025-08-16 09:23:12 -06:00
Brett Rowan
a566801316 feature: add chip count for IceRiver KS5M 2025-08-16 09:16:07 -06:00
Brett Rowan
e1a9cc5d19 bug: fix some issues with whatsminer BTMinerV3 implementation 2025-08-16 09:14:11 -06:00
Brett Rowan
27bb06de2b version: bump version number 2025-08-15 14:36:04 -06:00
8 changed files with 138 additions and 82 deletions

View File

@@ -53,6 +53,7 @@ details {
<li><a href="../antminer/X9#s9j-stock">S9j (Stock)</a></li>
<li><a href="../antminer/X9#t9-stock">T9 (Stock)</a></li>
<li><a href="../antminer/X9#l9-stock">L9 (Stock)</a></li>
<li><a href="../antminer/X9#l9-stock">L9 (Stock)</a></li>
</ul>
</details>
<details>

View File

@@ -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:

View File

@@ -233,9 +233,10 @@ class ElphapexMiner(StockFirmware):
board_temp_data = list(
filter(lambda x: not x == 0, board["temp_pcb"])
)
hashboards[board["index"]].temp = sum(board_temp_data) / len(
board_temp_data
)
if not len(board_temp_data) == 0:
hashboards[board["index"]].temp = sum(board_temp_data) / len(
board_temp_data
)
chip_temp_data = list(
filter(lambda x: not x == "", board["temp_chip"])
)

View File

@@ -40,5 +40,6 @@ class KS5M(IceRiverMake):
raw_model = MinerModel.ICERIVER.KS5M
expected_fans = 4
expected_chips = 18
expected_hashboards = 3
algo = MinerAlgo.KHEAVYHASH

View File

@@ -91,6 +91,7 @@ MINER_CLASSES = {
"ANTMINER S9J": BMMinerS9j,
"ANTMINER T9": BMMinerT9,
"ANTMINER L9": BMMinerL9,
"ANTMINER L9_I": BMMinerL9,
"ANTMINER Z15": CGMinerZ15,
"ANTMINER Z15 PRO": BMMinerZ15Pro,
"ANTMINER S17": BMMinerS17,

View File

@@ -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("<I", len(ser))
await self._send_bytes(header + json.dumps(cmd).encode("utf-8"))
return json.loads(
await self._send_bytes(header + json.dumps(cmd).encode("utf-8"))
)
await result_fut
return result_fut.result()
async def _send_bytes(
self,
data: bytes,
*,
port: int = None,
timeout: int = 100,
) -> 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("<I", header)[0]
return await self.reader.readexactly(length)
async def _read_bytes(self, reader: asyncio.StreamReader, timeout: int) -> 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("<I", header)[0]
ret_data = await reader.readexactly(length)
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
raise e
except Exception as e:
logging.warning(f"{self} - ([Hidden] Send Bytes) - API Command Error {e}")
return ret_data
async def get_salt(self) -> str:
if self.salt is not None:

View File

@@ -120,18 +120,28 @@ class ElphapexWebAPI(BaseWebAPI):
"""
auth = httpx.DigestAuth(self.username, self.pwd)
try:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
ret = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if ret.status_code == 200:
try:
json_data = ret.json()
return {command: json_data}
except json.decoder.JSONDecodeError:
pass
async def _send():
try:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
ret = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if ret.status_code == 200:
try:
json_data = ret.json()
if json_data.get("STATUS", {}).get("STATUS") not in ["S", "I"]:
return None
return {command: json_data}
except json.decoder.JSONDecodeError:
pass
return None
# retry 3 times
for i in range(3):
res = await _send()
if res is not None:
return res
return {command: {}}
async def get_miner_conf(self) -> dict:

View File

@@ -1,6 +1,6 @@
[project]
name = "pyasic"
version = "0.75.2"
version = "0.76.3"
description = "A simplified and standardized interface for Bitcoin ASICs."
authors = [{name = "UpstreamData", email = "brett@upstreamdata.ca"}]