Compare commits

..

21 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
39 changed files with 997 additions and 580 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>
@@ -624,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

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

@@ -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,
@@ -532,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

@@ -252,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,
@@ -293,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}")
@@ -1109,6 +1110,7 @@ class BTMinerV3RPCAPI(BaseMinerRPCAPI):
super().__init__(ip, port, api_ver=api_ver)
self.salt = None
self.pwd = "super"
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
"""Creates and sends multiple commands as one command to the miner.
@@ -1141,9 +1143,11 @@ 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")

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.1"
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",