Compare commits

...

25 Commits

Author SHA1 Message Date
Brett Rowan
cd52d3aeaf version: bump version number 2025-09-23 12:40:02 -06:00
Brett Rowan
66c9b3663e feature: add serial number for antminers 2025-09-23 12:39:16 -06:00
James Hilliard
5f0e1da938 Update dependencies 2025-09-22 18:34:03 -06:00
Brett Rowan
2bd031b33d version: bump version number 2025-09-19 15:49:29 -06:00
James Hilliard
e2f07818cc Fix race conditions in RPC and web API multicommand methods
Multiple multicommand methods were double-awaiting tasks - first via
asyncio.gather() with return_exceptions=True, then calling .result() on
the same tasks. This caused ConnectionResetError and other exceptions
when connections were lost.

Changed to properly use the results from gather() instead of calling
.result() on completed tasks, preventing exceptions from being raised
after they were already caught.

Fixed in:
- pyasic/rpc/base.py:144 - RPC _send_split_multicommand
- pyasic/web/espminer.py:79 - ESPMiner multicommand
- pyasic/web/auradine.py:149 - Auradine multicommand
2025-09-19 14:31:33 -06:00
Brett Rowan
75056cfff5 version: bump version number 2025-09-19 14:18:36 -06:00
James Hilliard
7fbcb0dbd2 Fix race condition in BOSer multicommand causing CancelledError
The multicommand method was double-awaiting tasks - first via
asyncio.gather() with return_exceptions=True, then trying to await
the same tasks again. This caused CancelledError when gRPC connections
were lost.

Changed to properly use the results from gather() instead of
re-awaiting completed tasks, preventing the race condition and
properly handling exceptions.

Fixes StreamTerminatedError occurring in pyasic/web/braiins_os/boser.py:91
2025-09-19 14:17:59 -06:00
Brett Rowan
7329aeace2 version: bump version number 2025-09-17 19:18:51 -06:00
Brett Rowan
e8c3953106 bug: fix btminer V3 password 2025-09-17 19:18:27 -06:00
James Hilliard
a1a7562bdb Handle invalid unicode in json response 2025-09-17 19:11:31 -06:00
Brett Rowan
b2726c77a0 version: bump version number 2025-09-09 08:13:09 -06:00
Ryan Heideman
68299fa54d Avalon Nano3s Fixes (#374)
* Avalon Nano3s Fixes

* Fix to handle both cases

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-30 22:04:58 -06:00
UpstreamData
1466039e08 version: bump version number 2025-08-28 21:43:00 -06:00
Ryan Heideman
0aa72c6c85 Fix Byte Folder Names (#376) 2025-08-26 21:18:57 -06:00
Ryan Heideman
24134a5991 Goldshell Mini Doge Support (#375)
* Docs for Mini Doge

* Mini Doge Recognition Support

* Loading fixes

* Fix for number of fans

* Fixed expected_hashrate

* Fixes for hashboards

* Implemented uptime

* Fixed uptime

* Doc fixes

* Fixes for byte

* Copyright update

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Typo fix

* File renames

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-26 21:06:10 -06:00
Brett Rowan
038208efa6 version: bump version number 2025-08-16 10:25:55 -06:00
Brett Rowan
7e319b79df feature: add misidentified L7_i 2025-08-16 10:25:40 -06:00
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
41 changed files with 1108 additions and 646 deletions

View File

@@ -5,13 +5,13 @@ ci:
- generate-docs
repos:
- repo: https://github.com/python-poetry/poetry
rev: 2.1.2
rev: 2.2.1
hooks:
- id: poetry-check
- id: poetry-lock
- id: poetry-install
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: check-yaml
@@ -23,7 +23,7 @@ repos:
exclude: ^mkdocs\.yml$
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 25.1.0
rev: 25.9.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort

View File

@@ -0,0 +1,16 @@
# pyasic
## Q Models
## Avalon Q Home (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.avalonminer.cgminer.Q.Q.CGMinerAvalonQHome
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -1,14 +1,14 @@
# pyasic
## Byte Models
## byte Models
## Byte (Stock)
- [x] Shutdowns
- [x] Power Modes
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.Byte.Byte.GoldshellByte
::: pyasic.miners.goldshell.bfgminer.byte.byte.GoldshellByte
handler: python
options:
show_root_heading: false

View File

@@ -0,0 +1,16 @@
# pyasic
## Mini Doge Models
## Mini Doge (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.MiniDoge.MiniDoge.GoldshellMiniDoge
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -0,0 +1,16 @@
# pyasic
## mini_doge Models
## Mini Doge (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.goldshell.bfgminer.mini_doge.mini_doge.GoldshellMiniDoge
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -0,0 +1,16 @@
# pyasic
## ALX Models
## AL3 (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.iceriver.iceminer.ALX.AL3.IceRiverAL3
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -38,6 +38,7 @@ details {
<details>
<summary>X7 Series:</summary>
<ul>
<li><a href="../antminer/X7#l7-stock">L7 (Stock)</a></li>
<li><a href="../antminer/X7#l7-stock">L7 (Stock)</a></li>
<li><a href="../antminer/X7#k7-stock">K7 (Stock)</a></li>
<li><a href="../antminer/X7#d7-stock">D7 (Stock)</a></li>
@@ -53,6 +54,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>
@@ -623,9 +625,15 @@ details {
</ul>
</details>
<details>
<summary>Byte Series:</summary>
<summary>byte Series:</summary>
<ul>
<li><a href="../goldshell/Byte#byte-stock">Byte (Stock)</a></li>
<li><a href="../goldshell/byte#byte-stock">Byte (Stock)</a></li>
</ul>
</details>
<details>
<summary>mini_doge Series:</summary>
<ul>
<li><a href="../goldshell/mini_doge#mini-doge-stock">Mini Doge (Stock)</a></li>
</ul>
</details>
</ul>

View File

@@ -98,6 +98,7 @@ nav:
- Innosilicon A10X: "miners/innosilicon/A10X.md"
- Innosilicon A11X: "miners/innosilicon/A11X.md"
- Goldshell Byte: "miners/goldshell/Byte.md"
- Goldshell Mini Doge: "miners/goldshell/MiniDoge.md"
- Goldshell X5: "miners/goldshell/X5.md"
- Goldshell XMax: "miners/goldshell/XMax.md"
- Goldshell XBox: "miners/goldshell/XBox.md"

968
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -254,6 +254,11 @@ class MinerConfig(BaseModel):
"""Constructs a MinerConfig object from web configuration for Goldshell miners."""
return cls(pools=PoolConfig.from_am_modern(web_conf))
@classmethod
def from_goldshell_list(cls, web_conf: list) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for Goldshell miners."""
return cls(pools=PoolConfig.from_goldshell(web_conf))
@classmethod
def from_goldshell_byte(cls, web_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for Goldshell Byte miners."""

View File

@@ -83,6 +83,7 @@ class MinerData(BaseModel):
# about
device_info: DeviceInfo | None = None
serial_number: str | None = None
mac: str | None = None
api_ver: str | None = None
fw_ver: str | None = None

View File

@@ -480,6 +480,7 @@ class GoldshellModels(MinerModelType):
KDBoxII = "KD Box II"
KDBoxPro = "KD Box Pro"
Byte = "Byte"
MiniDoge = "Mini Doge"
def __str__(self):
return self.value

View File

@@ -178,8 +178,7 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
if rpc_estats is not None:
try:
unparsed_estats = rpc_estats["STATS"][0]["MM ID0"]
parsed_estats = self.parse_estats(unparsed_estats)
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
return int(parsed_estats["PS"][6])
except (IndexError, KeyError, ValueError, TypeError):
pass
@@ -193,8 +192,7 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
if rpc_estats is not None:
try:
unparsed_estats = rpc_estats["STATS"][0]["MM ID0"]
parsed_estats = self.parse_estats(unparsed_estats)
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
return self.algo.hashrate(
rate=float(parsed_estats["GHSspd"]), unit=self.algo.unit.GH
).into(self.algo.unit.default)
@@ -212,14 +210,13 @@ class CGMinerAvalonNano3s(AvalonMiner, AvalonNano3s):
if rpc_estats is not None:
try:
unparsed_estats = rpc_estats["STATS"][0]["MM ID0"]
parsed_estats = self.parse_estats(unparsed_estats)
parsed_estats = self.parse_estats(rpc_estats)["STATS"][0]["MM ID0"]
except (IndexError, KeyError, ValueError, TypeError):
return hashboards
for board in range(len(hashboards)):
try:
board_hr = parsed_estats["GHSspd"][board]
board_hr = parsed_estats["GHSspd"]
hashboards[board].hashrate = self.algo.hashrate(
rate=float(board_hr), unit=self.algo.unit.GH
).into(self.algo.unit.default)

View File

@@ -39,6 +39,10 @@ from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
ANTMINER_MODERN_DATA_LOC = DataLocations(
**{
str(DataOptions.SERIAL_NUMBER): DataFunction(
"_get_serial_number",
[WebAPICommand("web_get_system_info", "get_system_info")],
),
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_get_system_info", "get_system_info")],
@@ -360,6 +364,21 @@ class AntminerModern(BMMiner):
except LookupError:
pass
async def _get_serial_number(
self, web_get_system_info: dict = None
) -> Optional[str]:
if web_get_system_info is None:
try:
web_get_system_info = await self.web.get_system_info()
except APIError:
pass
if web_get_system_info is not None:
try:
return web_get_system_info["serinum"]
except LookupError:
pass
async def set_static_ip(
self,
ip: str,

View File

@@ -305,10 +305,12 @@ class AvalonMiner(CGMiner):
hashboards[board].chip_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board]
)
except LookupError:
except (LookupError, TypeError):
try:
hashboards[board].chip_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["Tmax"]
parsed_estats["STATS"][0]["MM ID0"].get(
"Tmax", parsed_estats["STATS"][0]["MM ID0"]["TMax"]
)
)
except LookupError:
pass
@@ -317,10 +319,12 @@ class AvalonMiner(CGMiner):
hashboards[board].temp = int(
parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board]
)
except LookupError:
except (LookupError, TypeError):
try:
hashboards[board].temp = int(
parsed_estats["STATS"][0]["MM ID0"]["Tavg"]
parsed_estats["STATS"][0]["MM ID0"].get(
"Tavg", parsed_estats["STATS"][0]["MM ID0"]["TAvg"]
)
)
except LookupError:
pass
@@ -329,7 +333,7 @@ class AvalonMiner(CGMiner):
hashboards[board].inlet_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["MTavg"][board]
)
except LookupError:
except (LookupError, TypeError):
try:
hashboards[board].inlet_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["HBITemp"]
@@ -341,7 +345,7 @@ class AvalonMiner(CGMiner):
hashboards[board].outlet_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["MTmax"][board]
)
except LookupError:
except (LookupError, TypeError):
try:
hashboards[board].outlet_temp = int(
parsed_estats["STATS"][0]["MM ID0"]["HBOTemp"]
@@ -356,7 +360,7 @@ class AvalonMiner(CGMiner):
hashboards[board].chips = len(
[item for item in chip_data if not item == "0"]
)
except LookupError:
except (LookupError, TypeError):
try:
chip_data = parsed_estats["STATS"][0]["HBinfo"][f"HB{board}"][
f"PVT_T{board}"

View File

@@ -216,7 +216,7 @@ class BFGMiner(StockFirmware):
)
except LookupError:
pass
fans = [Fan(speed=d) if d else Fan() for d in fans_data]
fans = [Fan(speed=d) for d in fans_data if d is not None]
return fans

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

@@ -209,6 +209,14 @@ class MinerProtocol(Protocol):
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_serial_number(self) -> Optional[str]:
"""Get the serial number of the miner and return it as a string.
Returns:
A string representing the serial number of the miner.
"""
return await self._get_serial_number()
async def get_mac(self) -> Optional[str]:
"""Get the MAC address of the miner and return it as a string.
@@ -379,6 +387,9 @@ class MinerProtocol(Protocol):
"""
return await self._get_pools()
async def _get_serial_number(self) -> Optional[str]:
pass
async def _get_mac(self) -> Optional[str]:
pass

View File

@@ -20,6 +20,7 @@ from typing import List, Union
class DataOptions(Enum):
SERIAL_NUMBER = "serial_number"
MAC = "mac"
API_VERSION = "api_ver"
FW_VERSION = "fw_ver"

View File

@@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Byte import *
from .byte import *
from .mini_doge import *
from .X5 import *
from .XBox import *
from .XMax import *

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
@@ -13,4 +13,4 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Byte import Byte
from .byte import Byte

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
@@ -13,4 +13,4 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Byte import GoldshellByte
from .mini_doge import MiniDoge

View File

@@ -0,0 +1,27 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.device.algorithm import MinerAlgo
from pyasic.device.models import MinerModel
from pyasic.miners.device.makes import GoldshellMake
class MiniDoge(GoldshellMake):
raw_model = MinerModel.GOLDSHELL.MiniDoge
expected_chips = 40
expected_fans = 2
expected_hashboards = 1
algo = MinerAlgo.SCRYPT

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

@@ -82,6 +82,7 @@ MINER_CLASSES = {
"ANTMINER KS5": BMMinerKS5,
"ANTMINER KS5 PRO": BMMinerKS5Pro,
"ANTMINER L7": BMMinerL7,
"ANTMINER L7_I": BMMinerL7,
"ANTMINER K7": BMMinerK7,
"ANTMINER D7": BMMinerD7,
"ANTMINER E9 PRO": BMMinerE9Pro,
@@ -91,6 +92,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,
@@ -531,6 +533,7 @@ MINER_CLASSES = {
"GOLDSHELL KDBOXII": GoldshellKDBoxII,
"GOLDSHELL KDBOXPRO": GoldshellKDBoxPro,
"GOLDSHELL BYTE": GoldshellByte,
"GOLDSHELL MINIDOGE": GoldshellMiniDoge,
},
MinerTypes.BRAIINS_OS: {
None: BOSMiner,

View File

@@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Byte import *
from .byte import *
from .mini_doge import *
from .X5 import *
from .XBox import *
from .XMax import *

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .byte import GoldshellByte

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
@@ -73,14 +73,23 @@ GOLDSHELL_BYTE_DATA_LOC = DataLocations(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_devs", "devs")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[WebAPICommand("web_devs", "devs")],
),
}
)
class GoldshellByte(GoldshellMiner, Byte):
data_locations = GOLDSHELL_BYTE_DATA_LOC
cgdev: dict | None = None
supports_shutdown = False
supports_power_modes = False
web_devs: dict | None = None
async def get_data(
self,
@@ -88,27 +97,22 @@ class GoldshellByte(GoldshellMiner, Byte):
include: List[Union[str, DataOptions]] = None,
exclude: List[Union[str, DataOptions]] = None,
) -> MinerData:
if self.cgdev is None:
if self.web_devs is None:
try:
self.cgdev = await self.web.send_command("cgminer?cgminercmd=devs")
self.web_devs = await self.web.devs()
except APIError:
pass
scrypt_board_count = 0
zksnark_board_count = 0
total_wattage = 0
total_uptime_mins = 0
for minfo in self.cgdev.get("minfos", []):
for minfo in self.web_devs.get("minfos", []):
algo_name = minfo.get("name")
for info in minfo.get("infos", []):
for _ in minfo.get("infos", []):
self.expected_hashboards += 1
self.expected_fans += 1
total_wattage = int(float(info.get("power", 0)))
total_uptime_mins = int(info.get("time", 0))
if algo_name == ALGORITHM_SCRYPT_NAME:
scrypt_board_count += 1
elif algo_name == ALGORITHM_ZKSNARK_NAME:
@@ -125,12 +129,6 @@ class GoldshellByte(GoldshellMiner, Byte):
data = await super().get_data(allow_warning, include, exclude)
data.expected_chips = self.expected_chips
data.wattage = total_wattage
data.uptime = total_uptime_mins
data.voltage = 0
for board in data.hashboards:
data.voltage += board.voltage
return data
@@ -313,3 +311,39 @@ class GoldshellByte(GoldshellMiner, Byte):
fans = [Fan(speed=d) if d else Fan() for d in fans_data]
return fans
async def _get_uptime(self, web_devs: dict = None) -> Optional[int]:
if web_devs is None:
try:
web_devs = await self.web.devs()
except APIError:
pass
if web_devs is not None:
try:
for minfo in self.web_devs.get("minfos", []):
for info in minfo.get("infos", []):
uptime = int(float(info["time"]))
return uptime
except KeyError:
pass
return None
async def _get_wattage(self, web_devs: dict = None) -> Optional[int]:
if web_devs is None:
try:
web_devs = await self.web.devs()
except APIError:
pass
if web_devs is not None:
try:
for minfo in self.web_devs.get("minfos", []):
for info in minfo.get("infos", []):
wattage = int(float(info["power"]))
return wattage
except KeyError:
pass
return None

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .mini_doge import GoldshellMiniDoge

View File

@@ -0,0 +1,179 @@
# ------------------------------------------------------------------------------
# Copyright 2025 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data.boards import HashBoard
from pyasic.device.algorithm import AlgoHashRate
from pyasic.errors import APIError
from pyasic.logger import logger
from pyasic.miners.backends import GoldshellMiner
from pyasic.miners.data import (
DataFunction,
DataLocations,
DataOptions,
RPCAPICommand,
WebAPICommand,
)
from pyasic.miners.device.models import MiniDoge
GOLDSHELL_MINI_DOGE_DATA_LOC = DataLocations(
**{
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_setting", "setting")],
),
str(DataOptions.API_VERSION): DataFunction(
"_get_api_ver",
[RPCAPICommand("rpc_version", "version")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[WebAPICommand("web_status", "status")],
),
str(DataOptions.HASHRATE): DataFunction(
"_get_hashrate",
[RPCAPICommand("rpc_summary", "summary")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[RPCAPICommand("rpc_devs", "devs")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[
RPCAPICommand("rpc_devs", "devs"),
RPCAPICommand("rpc_devdetails", "devdetails"),
],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_devs", "devs")],
),
}
)
class GoldshellMiniDoge(GoldshellMiner, MiniDoge):
data_locations = GOLDSHELL_MINI_DOGE_DATA_LOC
supports_shutdown = False
supports_power_modes = False
async def get_config(self) -> MinerConfig:
try:
pools = await self.web.pools()
except APIError:
return self.config
self.config = MinerConfig.from_goldshell_list(pools)
return self.config
async def _get_expected_hashrate(
self, rpc_devs: dict = None
) -> Optional[AlgoHashRate]:
if rpc_devs is None:
try:
rpc_devs = await self.rpc.devs()
except APIError:
pass
if rpc_devs is not None:
try:
hash_rate = rpc_devs["DEVS"][0]["estimate_hash_rate"]
return self.algo.hashrate(
rate=float(hash_rate), unit=self.algo.unit.H
).into(self.algo.unit.default)
except KeyError:
pass
return None
async def _get_hashboards(
self, rpc_devs: dict = None, rpc_devdetails: dict = None
) -> List[HashBoard]:
if rpc_devs is None:
try:
rpc_devs = await self.rpc.devs()
except APIError:
pass
hashboards = [
HashBoard(slot=i, expected_chips=self.expected_chips)
for i in range(self.expected_hashboards)
]
if rpc_devs is not None:
if rpc_devs.get("DEVS"):
for board in rpc_devs["DEVS"]:
if board.get("ID") is not None:
try:
b_id = board["ID"]
hashboards[b_id].hashrate = self.algo.hashrate(
rate=float(board["MHS 20s"]), unit=self.algo.unit.MH
).into(self.algo.unit.default)
hashboards[b_id].chip_temp = board["tstemp-0"]
hashboards[b_id].temp = board["tstemp-1"]
hashboards[b_id].voltage = board["voltage"]
hashboards[b_id].active = board["Status"] == "Alive"
hashboards[b_id].missing = False
except KeyError:
pass
else:
logger.error(self, rpc_devs)
if rpc_devdetails is None:
try:
rpc_devdetails = await self.rpc.devdetails()
except APIError:
pass
if rpc_devdetails is not None:
if rpc_devdetails.get("DEVS"):
for board in rpc_devdetails["DEVS"]:
if board.get("ID") is not None:
try:
b_id = board["ID"]
hashboards[b_id].chips = board["chips-nr"]
except KeyError:
pass
else:
logger.error(self, rpc_devdetails)
return hashboards
async def _get_uptime(self, web_devs: dict = None) -> Optional[int]:
if web_devs is None:
try:
web_devs = await self.web.devs()
except APIError:
pass
if web_devs is not None:
try:
uptime = int(web_devs["data"][0]["time"])
return uptime
except KeyError:
pass
return None

View File

@@ -136,17 +136,16 @@ class BaseMinerRPCAPI:
self.send_command(cmd, allow_warning=allow_warning)
)
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
results = await asyncio.gather(
*[tasks[cmd] for cmd in tasks], return_exceptions=True
)
data = {}
for cmd in tasks:
try:
result = tasks[cmd].result()
for cmd, result in zip(tasks.keys(), results):
if not isinstance(result, (APIError, Exception)):
if result is None or result == {}:
result = {}
data[cmd] = [result]
except APIError:
pass
return data
@@ -253,10 +252,10 @@ If you are sure you want to use this command please use API.send_command("{comma
# some json from the API returns with a null byte (\x00) on the end
if data.endswith(b"\x00"):
# handle the null byte
str_data = data.decode("utf-8")[:-1]
str_data = data.decode("utf-8", errors="replace")[:-1]
else:
# no null byte
str_data = data.decode("utf-8")
str_data = data.decode("utf-8", errors="replace")
# fix an error with a btminer return having an extra comma that breaks json.loads()
str_data = str_data.replace(",}", "}")
# fix an error with a btminer return having a newline that breaks json.loads()

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
@@ -251,13 +252,13 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
except APIError as e:
if not e.message == "can't access write cmd":
raise
try:
await self.open_api()
except Exception as e:
raise APIError("Failed to open whatsminer API.") from e
return await self._send_privileged_command(
command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
)
# try:
# await self.open_api()
# except Exception as e:
# raise APIError("Failed to open whatsminer API.") from e
# return await self._send_privileged_command(
# command=command, ignore_errors=ignore_errors, timeout=timeout, **kwargs
# )
async def _send_privileged_command(
self,
@@ -292,6 +293,7 @@ class BTMinerRPCAPI(BaseMinerRPCAPI):
try:
data = parse_btminer_priviledge_data(self.token, data)
print(data)
except Exception as e:
logging.info(f"{str(self.ip)}: {e}")
@@ -1107,39 +1109,28 @@ 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.pwd = "super"
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
@@ -1152,40 +1143,89 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
token_hashed = bytearray(
base64.b64encode(hashlib.sha256(token_str.encode("utf-8")).digest())
)
token_hashed[8] = 0
b_arr = bytearray(token_hashed)
b_arr[8] = 0
str_token = b_arr.split(b"\x00")[0].decode("utf-8")
cmd["account"] = "super"
cmd["token"] = token_hashed.decode("ascii")
cmd["token"] = str_token
# 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

@@ -141,17 +141,16 @@ class AuradineWebAPI(BaseWebAPI):
self.send_command(cmd, allow_warning=allow_warning)
)
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
results = await asyncio.gather(
*[tasks[cmd] for cmd in tasks], return_exceptions=True
)
data = {"multicommand": True}
for cmd in tasks:
try:
result = tasks[cmd].result()
for cmd, result in zip(tasks.keys(), results):
if not isinstance(result, (APIError, Exception)):
if result is None or result == {}:
result = {}
data[cmd] = result
except APIError:
pass
return data

View File

@@ -84,13 +84,13 @@ class BOSerWebAPI(BaseWebAPI):
except AttributeError:
pass
await asyncio.gather(*[t for t in tasks.values()], return_exceptions=True)
results = await asyncio.gather(
*[t for t in tasks.values()], return_exceptions=True
)
for cmd in tasks:
try:
result[cmd] = await tasks[cmd]
except (GRPCError, APIError, ConnectionError):
pass
for cmd, task_result in zip(tasks.keys(), results):
if not isinstance(task_result, (GRPCError, APIError, ConnectionError)):
result[cmd] = task_result
return result

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

@@ -71,17 +71,16 @@ class ESPMinerWebAPI(BaseWebAPI):
self.send_command(cmd, allow_warning=allow_warning)
)
await asyncio.gather(*[tasks[cmd] for cmd in tasks], return_exceptions=True)
results = await asyncio.gather(
*[tasks[cmd] for cmd in tasks], return_exceptions=True
)
data = {"multicommand": True}
for cmd in tasks:
try:
result = tasks[cmd].result()
for cmd, result in zip(tasks.keys(), results):
if not isinstance(result, (APIError, Exception)):
if result is None or result == {}:
result = {}
data[cmd] = result
except APIError:
pass
return data

View File

@@ -103,8 +103,12 @@ class GoldshellWebAPI(BaseWebAPI):
async with httpx.AsyncClient(transport=settings.transport()) as client:
for command in commands:
try:
uri_commnand = command
if command == "devs":
uri_commnand = "cgminer?cgminercmd=devs"
response = await client.get(
f"http://{self.ip}:{self.port}/mcb/{command}",
f"http://{self.ip}:{self.port}/mcb/{uri_commnand}",
headers={"Authorization": "Bearer " + self.token},
timeout=settings.get("api_function_timeout", 5),
)
@@ -143,3 +147,6 @@ class GoldshellWebAPI(BaseWebAPI):
async def status(self) -> dict:
return await self.send_command("status")
async def devs(self) -> dict:
return await self.send_command("cgminer?cgminercmd=devs")

View File

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

View File

@@ -92,6 +92,7 @@ class MinersTest(unittest.TestCase):
"hashrate",
"hostname",
"is_mining",
"serial_number",
"mac",
"expected_hashrate",
"uptime",