Compare commits

..

10 Commits

Author SHA1 Message Date
Upstream Data
b897ca8363 version: bump version number 2025-02-06 11:33:08 -07:00
Upstream Data
dba341fdae feature: add support for avalon 1566 2025-02-06 11:32:45 -07:00
Upstream Data
837794bd57 version: bump version number 2025-02-05 10:45:56 -07:00
Upstream Data
36d16c7235 feature: add support for elphapex configs 2025-02-05 10:45:35 -07:00
Upstream Data
7797023689 feature: add support for elphapex pools 2025-02-05 08:28:28 -07:00
Upstream Data
1021f200ec version: bump version number 2025-02-05 08:07:35 -07:00
Upstream Data
197d6568e3 bug: fix DG1+ naming 2025-02-05 08:07:14 -07:00
Upstream Data
71c8905674 version: bump version number 2025-02-04 16:00:18 -07:00
Upstream Data
15c3806fbf refactor: cleanup some imports 2025-02-04 15:59:32 -07:00
Upstream Data
a5c42c9c2b feature: add support for Elphapex DG1+ 2025-02-04 15:59:32 -07:00
48 changed files with 1318 additions and 20 deletions

View File

@@ -60,6 +60,8 @@ def backend_str(backend: MinerTypes) -> str:
return "Stock Firmware Hammer Miners"
case MinerTypes.VOLCMINER:
return "Stock Firmware Volcminers"
case MinerTypes.ELPHAPEX:
return "Stock Firmware Elphapex Miners"
raise TypeError("Unknown miner backend, cannot generate docs")

View File

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

View File

@@ -0,0 +1,16 @@
# pyasic
## DGX Models
## DG1+ (Stock)
- [ ] Shutdowns
- [ ] Power Modes
- [ ] Setpoints
- [ ] Presets
::: pyasic.miners.elphapex.daoge.DGX.DG1.ElphapexDG1Plus
handler: python
options:
show_root_heading: false
heading_level: 0

View File

@@ -554,6 +554,12 @@ details {
<li><a href="../avalonminer/nano#avalon-nano-3-stock">Avalon Nano 3 (Stock)</a></li>
</ul>
</details>
<details>
<summary>A15X Series:</summary>
<ul>
<li><a href="../avalonminer/A15X#avalon-1566-stock">Avalon 1566 (Stock)</a></li>
</ul>
</details>
</ul>
</details>
<details>
@@ -915,3 +921,14 @@ details {
</details>
</ul>
</details>
<details>
<summary>Stock Firmware Elphapex Miners:</summary>
<ul>
<details>
<summary>DGX Series:</summary>
<ul>
<li><a href="../elphapex/DGX#dg1_1-stock">DG1+ (Stock)</a></li>
</ul>
</details>
</ul>
</details>

View File

@@ -56,6 +56,16 @@ class MinerConfig(BaseModel):
**self.temperature.as_am_modern(),
}
def as_elphapex(self, user_suffix: str | None = None) -> dict:
"""Generates the configuration in the format suitable for modern Elphapex."""
return {
**self.fan_mode.as_elphapex(),
"fc-freq-level": "100",
**self.mining_mode.as_elphapex(),
**self.pools.as_elphapex(user_suffix=user_suffix),
**self.temperature.as_elphapex(),
}
def as_wm(self, user_suffix: str | None = None) -> dict:
"""Generates the configuration in the format suitable for Whatsminers."""
return {
@@ -199,6 +209,15 @@ class MinerConfig(BaseModel):
fan_mode=FanModeConfig.from_am_modern(web_conf),
)
@classmethod
def from_elphapex(cls, web_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for modern Antminers."""
return cls(
pools=PoolConfig.from_elphapex(web_conf),
mining_mode=MiningModeConfig.from_elphapex(web_conf),
fan_mode=FanModeConfig.from_elphapex(web_conf),
)
@classmethod
def from_am_old(cls, web_conf: dict) -> "MinerConfig":
"""Constructs a MinerConfig object from web configuration for old versions of Antminers."""

View File

@@ -67,6 +67,9 @@ class MinerConfigOption(Enum):
def as_luxos(self) -> dict:
return self.value.as_luxos()
def as_elphapex(self) -> dict:
return self.value.as_elphapex()
def __call__(self, *args, **kwargs):
return self.value(*args, **kwargs)
@@ -131,6 +134,9 @@ class MinerConfigValue(BaseModel):
def as_luxos(self) -> dict:
return {}
def as_elphapex(self) -> dict:
return {}
def __getitem__(self, item):
try:
return getattr(self, item)

View File

@@ -55,6 +55,9 @@ class FanModeNormal(MinerConfigValue):
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": False, "bitmain-fan-pwn": "100"}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": False, "fc-fan-pwn": "100"}
def as_bosminer(self) -> dict:
return {
"temp_control": {"mode": "auto"},
@@ -135,6 +138,9 @@ class FanModeManual(MinerConfigValue):
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": str(self.speed)}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": True, "fc-fan-pwm": str(self.speed)}
def as_bosminer(self) -> dict:
return {
"temp_control": {"mode": "manual"},
@@ -185,6 +191,9 @@ class FanModeImmersion(MinerConfigValue):
def as_am_modern(self) -> dict:
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": "0"}
def as_elphapex(self) -> dict:
return {"fc-fan-ctrl": True, "fc-fan-pwm": "0"}
def as_bosminer(self) -> dict:
return {
"fan_control": {"min_fans": 0},
@@ -239,6 +248,20 @@ class FanModeConfig(MinerConfigOption):
else:
return cls.default()
@classmethod
def from_elphapex(cls, web_conf: dict):
if web_conf.get("fc-fan-ctrl") is not None:
fan_manual = web_conf["fc-fan-ctrl"]
if fan_manual:
speed = int(web_conf["fc-fan-pwm"])
if speed == 0:
return cls.immersion()
return cls.manual(speed=speed)
else:
return cls.normal()
else:
return cls.default()
@classmethod
def from_epic(cls, web_conf: dict):
try:

View File

@@ -52,6 +52,9 @@ class MiningModeNormal(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_wm(self) -> dict:
return {"mode": self.mode}
@@ -87,6 +90,9 @@ class MiningModeSleep(MinerConfigValue):
return {"miner-mode": "1"}
return {"miner-mode": 1}
def as_elphapex(self) -> dict:
return {"miner-mode": 1}
def as_wm(self) -> dict:
return {"mode": self.mode}
@@ -119,6 +125,9 @@ class MiningModeLPM(MinerConfigValue):
return {"miner-mode": "3"}
return {"miner-mode": 3}
def as_elphapex(self) -> dict:
return {"miner-mode": 3}
def as_wm(self) -> dict:
return {"mode": self.mode}
@@ -141,6 +150,9 @@ class MiningModeHPM(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_wm(self) -> dict:
return {"mode": self.mode}
@@ -174,6 +186,9 @@ class MiningModePowerTune(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_wm(self) -> dict:
if self.power is not None:
return {"mode": self.mode, self.mode: {"wattage": self.power}}
@@ -273,6 +288,9 @@ class MiningModeHashrateTune(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_bosminer(self) -> dict:
conf = {"enabled": True, "mode": "hashrate_target"}
if self.hashrate is not None:
@@ -404,6 +422,9 @@ class ManualBoardSettings(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_vnish(self) -> dict:
return {"freq": self.freq}
@@ -428,6 +449,9 @@ class MiningModeManual(MinerConfigValue):
return {"miner-mode": "0"}
return {"miner-mode": 0}
def as_elphapex(self) -> dict:
return {"miner-mode": 0}
def as_vnish(self) -> dict:
chains = [b.as_vnish() for b in self.boards.values() if b.freq != 0]
return {
@@ -525,6 +549,20 @@ class MiningModeConfig(MinerConfigOption):
return cls.low()
return cls.default()
@classmethod
def from_elphapex(cls, web_conf: dict):
if web_conf.get("fc-work-mode") is not None:
work_mode = web_conf["fc-work-mode"]
if work_mode == "":
return cls.default()
if int(work_mode) == 0:
return cls.normal()
elif int(work_mode) == 1:
return cls.sleep()
elif int(work_mode) == 3:
return cls.low()
return cls.default()
@classmethod
def from_epic(cls, web_conf: dict):
try:

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import field
from typing import TypeVar, Union
from pyasic.config.base import MinerConfigOption, MinerConfigValue

View File

@@ -15,8 +15,6 @@
# ------------------------------------------------------------------------------
from __future__ import annotations
from dataclasses import dataclass
from pyasic.config.base import MinerConfigValue

View File

@@ -43,6 +43,13 @@ class Pool(MinerConfigValue):
"pass": self.password,
}
def as_elphapex(self, user_suffix: str | None = None) -> dict:
return {
"url": self.url,
"user": f"{self.user}{user_suffix or ''}",
"pass": self.password,
}
def as_wm(self, idx: int = 1, user_suffix: str | None = None) -> dict:
return {
f"pool_{idx}": self.url,
@@ -146,6 +153,12 @@ class Pool(MinerConfigValue):
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
@classmethod
def from_elphapex(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["url"], user=web_pool["user"], password=web_pool["pass"]
)
# TODO: check if this is accurate, user/username, pass/password
@classmethod
def from_goldshell(cls, web_pool: dict) -> "Pool":
@@ -235,6 +248,17 @@ class PoolGroup(MinerConfigValue):
idx += 1
return pools
def as_elphapex(self, user_suffix: str | None = None) -> list:
pools = []
idx = 0
while idx < 3:
if len(self.pools) > idx:
pools.append(self.pools[idx].as_elphapex(user_suffix=user_suffix))
else:
pools.append(Pool(url="", user="", password="").as_elphapex())
idx += 1
return pools
def as_wm(self, user_suffix: str | None = None) -> dict:
pools = {}
idx = 0
@@ -351,6 +375,13 @@ class PoolGroup(MinerConfigValue):
pools.append(Pool.from_am_modern(pool))
return cls(pools=pools)
@classmethod
def from_elphapex(cls, web_pool_list: list) -> "PoolGroup":
pools = []
for pool in web_pool_list:
pools.append(Pool.from_elphapex(pool))
return cls(pools=pools)
@classmethod
def from_goldshell(cls, web_pools: list) -> "PoolGroup":
return cls(pools=[Pool.from_goldshell(p) for p in web_pools])
@@ -436,6 +467,11 @@ class PoolConfig(MinerConfigValue):
return {"pools": self.groups[0].as_am_modern(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_am_modern()}
def as_elphapex(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_elphapex(user_suffix=user_suffix)}
return {"pools": PoolGroup().as_elphapex()}
def as_wm(self, user_suffix: str | None = None) -> dict:
if len(self.groups) > 0:
return {"pools": self.groups[0].as_wm(user_suffix=user_suffix)}
@@ -537,6 +573,12 @@ class PoolConfig(MinerConfigValue):
return cls(groups=[PoolGroup.from_am_modern(pool_data)])
@classmethod
def from_elphapex(cls, web_conf: dict) -> "PoolConfig":
pool_data = web_conf["pools"]
return cls(groups=[PoolGroup.from_elphapex(pool_data)])
@classmethod
def from_goldshell(cls, web_pools: list) -> "PoolConfig":
return cls(groups=[PoolGroup.from_goldshell(web_pools)])

View File

@@ -15,8 +15,6 @@
# ------------------------------------------------------------------------------
from __future__ import annotations
from dataclasses import dataclass
from pyasic.config.base import MinerConfigValue

View File

@@ -24,9 +24,7 @@ from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune
from pyasic.data.pools import PoolMetrics, Scheme
from pyasic.device.algorithm.hashrate import AlgoHashRateType
from pyasic.device.algorithm.hashrate.base import GenericHashrate
from ..device.algorithm.hashrate.unit.base import GenericUnit
from .boards import HashBoard
from .device import DeviceInfo
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error

View File

@@ -17,7 +17,7 @@ from __future__ import annotations
from typing import Any
from pydantic import BaseModel, field_serializer
from pydantic import BaseModel
from pyasic.device.algorithm.hashrate import AlgoHashRateType

View File

@@ -30,6 +30,7 @@ class MinerMake(str, Enum):
ICERIVER = "IceRiver"
HAMMER = "Hammer"
VOLCMINER = "VolcMiner"
ELPHAPEX = "Elphapex"
BRAIINS = "Braiins"
def __str__(self):

View File

@@ -448,6 +448,7 @@ class AvalonminerModels(MinerModelType):
Avalon1166Pro = "Avalon 1166 Pro"
Avalon1126Pro = "Avalon 1126 Pro"
Avalon1246 = "Avalon 1246"
Avalon1566 = "Avalon 1566"
AvalonNano3 = "Avalon Nano 3"
def __str__(self):
@@ -550,6 +551,10 @@ class BraiinsModels(MinerModelType):
BMM101 = "BMM101"
class ElphapexModels(MinerModelType):
DG1Plus = "DG1+"
class MinerModel:
ANTMINER = AntminerModels
WHATSMINER = WhatsminerModels
@@ -563,4 +568,5 @@ class MinerModel:
ICERIVER = IceRiverModels
HAMMER = HammerModels
VOLCMINER = VolcMinerModels
ELPHAPEX = ElphapexModels
BRAIINS = BraiinsModels

View File

@@ -16,8 +16,6 @@
from typing import List, Optional
import asyncssh
from pyasic.data import HashBoard
from pyasic.device.algorithm import AlgoHashRate, HashUnit
from pyasic.errors import APIError

View File

@@ -0,0 +1,22 @@
# ------------------------------------------------------------------------------
# Copyright 2022 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.miners.backends import AvalonMiner
from pyasic.miners.device.models import Avalon1566
class CGMinerAvalon1566(AvalonMiner, Avalon1566):
pass

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# Copyright 2022 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 .A1566 import CGMinerAvalon1566

View File

@@ -20,4 +20,5 @@ from .A9X import *
from .A10X import *
from .A11X import *
from .A12X import *
from .A15X import *
from .nano import *

View File

@@ -61,6 +61,10 @@ AVALON_NANO_DATA_LOC = DataLocations(
"_get_wattage_limit",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_stats", "stats")],

View File

@@ -22,6 +22,7 @@ from .bmminer import BMMiner
from .braiins_os import BOSer, BOSMiner
from .btminer import BTMiner
from .cgminer import CGMiner
from .elphapex import ElphapexMiner
from .epic import ePIC
from .goldshell import GoldshellMiner
from .hammer import BlackMiner

View File

@@ -75,6 +75,10 @@ ANTMINER_MODERN_DATA_LOC = DataLocations(
"_get_fault_light",
[WebAPICommand("web_get_blink_status", "get_blink_status")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[],
),
str(DataOptions.IS_MINING): DataFunction(
"_is_mining",
[WebAPICommand("web_get_conf", "get_miner_conf")],

View File

@@ -57,6 +57,10 @@ AVALON_DATA_LOC = DataLocations(
"_get_wattage_limit",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.WATTAGE): DataFunction(
"_get_wattage",
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[RPCAPICommand("rpc_stats", "stats")],
@@ -289,6 +293,21 @@ class AvalonMiner(CGMiner):
except (IndexError, KeyError, ValueError, TypeError):
pass
async def _get_wattage(self, rpc_stats: dict = None) -> Optional[int]:
if rpc_stats is None:
try:
rpc_stats = await self.rpc.stats()
except APIError:
pass
if rpc_stats is not None:
try:
unparsed_stats = rpc_stats["STATS"][0]["MM ID0"]
parsed_stats = self.parse_stats(unparsed_stats)
return int(parsed_stats["WALLPOWER"][0])
except (IndexError, KeyError, ValueError, TypeError):
pass
async def _get_fans(self, rpc_stats: dict = None) -> List[Fan]:
if rpc_stats is None:
try:

View File

@@ -0,0 +1,374 @@
# ------------------------------------------------------------------------------
# Copyright 2022 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 import APIError, MinerConfig
from pyasic.data import Fan, HashBoard, X19Error
from pyasic.data.error_codes import MinerErrorData
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.device.algorithm import AlgoHashRate
from pyasic.miners.data import (
DataFunction,
DataLocations,
DataOptions,
WebAPICommand,
)
from pyasic.miners.device.firmware import StockFirmware
from pyasic.web.elphapex import ElphapexWebAPI
ELPHAPEX_DATA_LOC = DataLocations(
**{
str(DataOptions.MAC): DataFunction(
"_get_mac",
[WebAPICommand("web_get_system_info", "get_system_info")],
),
str(DataOptions.API_VERSION): DataFunction(
"_get_api_ver",
[WebAPICommand("web_summary", "summary")],
),
str(DataOptions.FW_VERSION): DataFunction(
"_get_fw_ver",
[WebAPICommand("web_get_system_info", "get_system_info")],
),
str(DataOptions.HOSTNAME): DataFunction(
"_get_hostname",
[WebAPICommand("web_get_system_info", "get_system_info")],
),
str(DataOptions.HASHBOARDS): DataFunction(
"_get_hashboards",
[WebAPICommand("web_stats", "stats")],
),
str(DataOptions.EXPECTED_HASHRATE): DataFunction(
"_get_expected_hashrate",
[WebAPICommand("web_stats", "stats")],
),
str(DataOptions.FANS): DataFunction(
"_get_fans",
[WebAPICommand("web_stats", "stats")],
),
str(DataOptions.ERRORS): DataFunction(
"_get_errors",
[WebAPICommand("web_summary", "summary")],
),
str(DataOptions.FAULT_LIGHT): DataFunction(
"_get_fault_light",
[WebAPICommand("web_get_blink_status", "get_blink_status")],
),
str(DataOptions.IS_MINING): DataFunction(
"_is_mining",
[WebAPICommand("web_get_miner_conf", "get_miner_conf")],
),
str(DataOptions.UPTIME): DataFunction(
"_get_uptime",
[WebAPICommand("web_summary", "summary")],
),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[WebAPICommand("web_pools", "pools")],
),
}
)
class ElphapexMiner(StockFirmware):
"""Handler for Elphapex miners."""
_web_cls = ElphapexWebAPI
web: ElphapexWebAPI
data_locations = ELPHAPEX_DATA_LOC
async def get_config(self) -> MinerConfig:
data = await self.web.get_miner_conf()
if data:
self.config = MinerConfig.from_elphapex(data)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
self.config = config
await self.web.set_miner_conf(config.as_elphapex(user_suffix=user_suffix))
async def fault_light_on(self) -> bool:
data = await self.web.blink(blink=True)
if data:
if data.get("code") == "B000":
self.light = True
return self.light
async def fault_light_off(self) -> bool:
data = await self.web.blink(blink=False)
if data:
if data.get("code") == "B100":
self.light = False
return self.light
async def reboot(self) -> bool:
data = await self.web.reboot()
if data:
return True
return False
async def _get_api_ver(self, web_summary: dict = None) -> Optional[str]:
if web_summary is None:
try:
web_summary = await self.web.summary()
except APIError:
pass
if web_summary is not None:
try:
self.api_ver = web_summary["STATUS"]["api_version"]
except LookupError:
pass
return self.api_ver
async def _get_fw_ver(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:
self.fw_ver = (
web_get_system_info["system_filesystem_version"]
.upper()
.split("V")[-1]
)
except LookupError:
pass
return self.fw_ver
async def _get_hostname(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["hostname"]
except KeyError:
pass
async def _get_mac(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["macaddr"]
except KeyError:
pass
try:
data = await self.web.get_network_info()
if data:
return data["macaddr"]
except KeyError:
pass
async def _get_errors(self, web_summary: dict = None) -> List[MinerErrorData]:
if web_summary is None:
try:
web_summary = await self.web.summary()
except APIError:
pass
errors = []
if web_summary is not None:
try:
for item in web_summary["SUMMARY"][0]["status"]:
try:
if not item["status"] == "s":
errors.append(X19Error(error_message=item["msg"]))
except KeyError:
continue
except LookupError:
pass
return errors
async def _get_hashboards(self, web_stats: dict | None = None) -> List[HashBoard]:
hashboards = [
HashBoard(slot=idx, expected_chips=self.expected_chips)
for idx in range(self.expected_hashboards)
]
if web_stats is None:
try:
web_stats = await self.web.stats()
except APIError:
return hashboards
if web_stats is not None:
try:
for board in web_stats["STATS"][0]["chain"]:
hashboards[board["index"]].hashrate = self.algo.hashrate(
rate=board["rate_real"], unit=self.algo.unit.MH
).into(self.algo.unit.default)
hashboards[board["index"]].chips = board["asic_num"]
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
)
chip_temp_data = list(
filter(lambda x: not x == "", board["temp_chip"])
)
hashboards[board["index"]].chip_temp = sum(
[int(i) / 1000 for i in chip_temp_data]
) / len(chip_temp_data)
hashboards[board["index"]].serial_number = board["sn"]
hashboards[board["index"]].missing = False
except LookupError:
pass
return hashboards
async def _get_fault_light(
self, web_get_blink_status: dict = None
) -> Optional[bool]:
if self.light:
return self.light
if web_get_blink_status is None:
try:
web_get_blink_status = await self.web.get_blink_status()
except APIError:
pass
if web_get_blink_status is not None:
try:
self.light = web_get_blink_status["blink"]
except KeyError:
pass
return self.light
async def _get_expected_hashrate(
self, web_stats: dict = None
) -> Optional[AlgoHashRate]:
if web_stats is None:
try:
web_stats = await self.web.stats()
except APIError:
pass
if web_stats is not None:
try:
expected_rate = web_stats["STATS"][1]["total_rateideal"]
try:
rate_unit = web_stats["STATS"][1]["rate_unit"]
except KeyError:
rate_unit = "MH"
return self.algo.hashrate(
rate=float(expected_rate), unit=self.algo.unit.from_str(rate_unit)
).into(self.algo.unit.default)
except LookupError:
pass
async def _is_mining(self, web_get_miner_conf: dict = None) -> Optional[bool]:
if web_get_miner_conf is None:
try:
web_get_miner_conf = await self.web.get_miner_conf()
except APIError:
pass
if web_get_miner_conf is not None:
try:
if str(web_get_miner_conf["fc-work-mode"]).isdigit():
return (
False if int(web_get_miner_conf["fc-work-mode"]) == 1 else True
)
return False
except LookupError:
pass
async def _get_uptime(self, web_summary: dict = None) -> Optional[int]:
if web_summary is None:
try:
web_summary = await self.web.summary()
except APIError:
pass
if web_summary is not None:
try:
return int(web_summary["SUMMARY"][1]["elapsed"])
except LookupError:
pass
async def _get_fans(self, web_stats: dict = None) -> List[Fan]:
if web_stats is None:
try:
web_stats = await self.web.stats()
except APIError:
pass
fans = [Fan() for _ in range(self.expected_fans)]
if web_stats is not None:
for fan_n in range(self.expected_fans):
try:
fans[fan_n].speed = int(web_stats["STATS"][0]["fan"][fan_n])
except LookupError:
pass
return fans
async def _get_pools(self, web_pools: list = None) -> List[PoolMetrics]:
if web_pools is None:
try:
web_pools = await self.web.pools()
except APIError:
return []
active_pool_index = None
highest_priority = float("inf")
for pool_info in web_pools["POOLS"]:
if (
pool_info.get("status") == "Alive"
and pool_info.get("priority", float("inf")) < highest_priority
):
highest_priority = pool_info["priority"]
active_pool_index = pool_info["index"]
pools_data = []
if web_pools is not None:
try:
for pool_info in web_pools["POOLS"]:
url = pool_info.get("url")
pool_url = PoolUrl.from_str(url) if url else None
pool_data = PoolMetrics(
accepted=pool_info.get("accepted"),
rejected=pool_info.get("rejected"),
get_failures=pool_info.get("stale"),
remote_failures=pool_info.get("discarded"),
active=pool_info.get("index") == active_pool_index,
alive=pool_info.get("status") == "Alive",
url=pool_url,
user=pool_info.get("user"),
index=pool_info.get("index"),
)
pools_data.append(pool_data)
except LookupError:
pass
return pools_data

View File

@@ -68,3 +68,7 @@ class VolcMinerMake(BaseMiner):
class BraiinsMake(BaseMiner):
make = MinerMake.BRAIINS
class ElphapexMake(BaseMiner):
make = MinerMake.ELPHAPEX

View File

@@ -18,6 +18,7 @@ from .antminer import *
from .auradine import *
from .avalonminer import *
from .braiins import *
from .elphapex import *
from .epic import *
from .goldshell import *
from .hammer import *

View File

@@ -0,0 +1,27 @@
# ------------------------------------------------------------------------------
# Copyright 2022 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 AvalonMinerMake
class Avalon1566(AvalonMinerMake):
raw_model = MinerModel.AVALONMINER.Avalon1566
expected_chips = 160
expected_fans = 4
expected_hashboards = 3
algo = MinerAlgo.SHA256

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# Copyright 2022 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 .A1566 import Avalon1566

View File

@@ -20,4 +20,5 @@ from .A9X import *
from .A10X import *
from .A11X import *
from .A12X import *
from .A15X import *
from .nano import *

View File

@@ -0,0 +1,27 @@
# ------------------------------------------------------------------------------
# Copyright 2024 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 ElphapexMake
class DG1Plus(ElphapexMake):
raw_model = MinerModel.ELPHAPEX.DG1Plus
expected_chips = 204
expected_hashboards = 4
expected_fans = 4
algo = MinerAlgo.SCRYPT

View File

@@ -0,0 +1 @@
from .DG1 import DG1Plus

View File

@@ -0,0 +1 @@
from .DGX import *

View File

@@ -0,0 +1 @@
from .daoge import *

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends.elphapex import ElphapexMiner
from pyasic.miners.device.models import DG1Plus
class ElphapexDG1Plus(ElphapexMiner, DG1Plus):
pass

View File

@@ -0,0 +1 @@
from .DG1 import ElphapexDG1Plus

View File

@@ -0,0 +1 @@
from .DGX import *

View File

@@ -37,6 +37,7 @@ from pyasic.miners.bitaxe import *
from pyasic.miners.blockminer import *
from pyasic.miners.braiins import *
from pyasic.miners.device.makes import *
from pyasic.miners.elphapex import *
from pyasic.miners.goldshell import *
from pyasic.miners.hammer import *
from pyasic.miners.iceriver import *
@@ -64,6 +65,7 @@ class MinerTypes(enum.Enum):
HAMMER = 14
VOLCMINER = 15
LUCKYMINER = 16
ELPHAPEX = 17
MINER_CLASSES = {
@@ -502,6 +504,7 @@ MINER_CLASSES = {
"AVALONMINER 1166PRO": CGMinerAvalon1166Pro,
"AVALONMINER 1246": CGMinerAvalon1246,
"AVALONMINER NANO3": CGMinerAvalonNano3,
"AVALONMINER 15-194": CGMinerAvalon1566,
},
MinerTypes.INNOSILICON: {
None: type("InnosiliconUnknown", (Innosilicon, InnosiliconMake), {}),
@@ -664,6 +667,10 @@ MINER_CLASSES = {
None: type("VolcMinerUnknown", (BlackMiner, VolcMinerMake), {}),
"VOLCMINER D1": VolcMinerD1,
},
MinerTypes.ELPHAPEX: {
None: type("ElphapexUnknown", (ElphapexMiner, ElphapexMake), {}),
"DG1+": ElphapexDG1Plus,
},
}
@@ -745,6 +752,7 @@ class MinerFactory:
MinerTypes.ICERIVER: self.get_miner_model_iceriver,
MinerTypes.HAMMER: self.get_miner_model_hammer,
MinerTypes.VOLCMINER: self.get_miner_model_volcminer,
MinerTypes.ELPHAPEX: self.get_miner_model_elphapex,
}
fn = miner_model_fns.get(miner_type)
@@ -828,6 +836,10 @@ class MinerFactory:
"www-authenticate", ""
):
return MinerTypes.HAMMER
if web_resp.status_code == 401 and 'realm="Daoge' in web_resp.headers.get(
"www-authenticate", ""
):
return MinerTypes.ELPHAPEX
if len(web_resp.history) > 0:
history_resp = web_resp.history[0]
if (
@@ -1149,9 +1161,9 @@ class MinerFactory:
miner_model = sock_json_data["VERSION"][0]["PROD"].upper()
if "-" in miner_model:
miner_model = miner_model.split("-")[0]
if miner_model in ["AVALONNANO", "AVALON0O"]:
nano_subtype = sock_json_data["VERSION"][0]["MODEL"].upper()
miner_model = f"AVALONMINER {nano_subtype}"
if miner_model in ["AVALONNANO", "AVALON0O", "AVALONMINER 15"]:
subtype = sock_json_data["VERSION"][0]["MODEL"].upper()
miner_model = f"AVALONMINER {subtype}"
return miner_model
except (TypeError, LookupError):
pass
@@ -1384,6 +1396,21 @@ class MinerFactory:
except (TypeError, LookupError):
pass
async def get_miner_model_elphapex(self, ip: str) -> str | None:
auth = httpx.DigestAuth(
"root", settings.get("default_elphapex_web_password", "root")
)
web_json_data = await self.send_web_command(
ip, "/cgi-bin/get_system_info.cgi", auth=auth
)
try:
miner_model = web_json_data["minertype"]
return miner_model
except (TypeError, LookupError):
pass
miner_factory = MinerFactory()

View File

@@ -40,6 +40,7 @@ _settings = { # defaults
"default_epic_web_password": "letmein",
"default_hive_web_password": "root",
"default_iceriver_web_password": "12345678",
"default_elphapex_web_password": "root",
"default_antminer_ssh_password": "miner",
"default_bosminer_ssh_password": "root",
}

View File

@@ -18,10 +18,8 @@ from __future__ import annotations
import asyncio
import hashlib
import json
from pathlib import Path
from typing import Any
import aiofiles
import httpx
from pyasic import settings

224
pyasic/web/elphapex.py Normal file
View File

@@ -0,0 +1,224 @@
# ------------------------------------------------------------------------------
# Copyright 2022 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 __future__ import annotations
import asyncio
import json
from typing import Any
import httpx
from pyasic import settings
from pyasic.web.base import BaseWebAPI
class ElphapexWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
"""Initialize the modern Elphapex API client with a specific IP address.
Args:
ip (str): IP address of the Elphapex device.
"""
super().__init__(ip)
self.username = "root"
self.pwd = settings.get("default_elphapex_web_password", "root")
async def send_command(
self,
command: str | bytes,
ignore_errors: bool = False,
allow_warning: bool = True,
privileged: bool = False,
**parameters: Any,
) -> dict:
"""Send a command to the Elphapex device using HTTP digest authentication.
Args:
command (str | bytes): The CGI command to send.
ignore_errors (bool): If True, ignore any HTTP errors.
allow_warning (bool): If True, proceed with warnings.
privileged (bool): If set to True, requires elevated privileges.
**parameters: Arbitrary keyword arguments to be sent as parameters in the request.
Returns:
dict: The JSON response from the device or an empty dictionary if an error occurs.
"""
url = f"http://{self.ip}:{self.port}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.username, self.pwd)
try:
async with httpx.AsyncClient(transport=settings.transport()) as client:
if parameters:
data = await client.post(
url,
auth=auth,
timeout=settings.get("api_function_timeout", 3),
json=parameters,
)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError as e:
return {"success": False, "message": f"HTTP error occurred: {str(e)}"}
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
return {"success": False, "message": "Failed to decode JSON"}
return {"success": False, "message": "Unknown error occurred"}
async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict:
"""Execute multiple commands simultaneously.
Args:
*commands (str): Multiple command strings to be executed.
ignore_errors (bool): If True, ignore any HTTP errors.
allow_warning (bool): If True, proceed with warnings.
Returns:
dict: A dictionary containing the results of all commands executed.
"""
async with httpx.AsyncClient(transport=settings.transport()) as client:
tasks = [
asyncio.create_task(self._handle_multicommand(client, command))
for command in commands
]
all_data = await asyncio.gather(*tasks)
data = {}
for item in all_data:
data.update(item)
data["multicommand"] = True
return data
async def _handle_multicommand(
self, client: httpx.AsyncClient, command: str
) -> dict:
"""Helper function for handling individual commands in a multicommand execution.
Args:
client (httpx.AsyncClient): The HTTP client to use for the request.
command (str): The command to be executed.
Returns:
dict: A dictionary containing the response of the executed command.
"""
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
return {command: {}}
async def get_miner_conf(self) -> dict:
"""Retrieve the miner configuration from the Elphapex device.
Returns:
dict: A dictionary containing the current configuration of the miner.
"""
return await self.send_command("get_miner_conf")
async def set_miner_conf(self, conf: dict) -> dict:
"""Set the configuration for the miner.
Args:
conf (dict): A dictionary of configuration settings to apply to the miner.
Returns:
dict: A dictionary response from the device after setting the configuration.
"""
return await self.send_command("set_miner_conf", **conf)
async def blink(self, blink: bool) -> dict:
"""Control the blinking of the LED on the miner device.
Args:
blink (bool): True to start blinking, False to stop.
Returns:
dict: A dictionary response from the device after the command execution.
"""
if blink:
return await self.send_command("blink", blink="true")
return await self.send_command("blink", blink="false")
async def reboot(self) -> dict:
"""Reboot the miner device.
Returns:
dict: A dictionary response from the device confirming the reboot command.
"""
return await self.send_command("reboot")
async def get_system_info(self) -> dict:
"""Retrieve system information from the miner.
Returns:
dict: A dictionary containing system information of the miner.
"""
return await self.send_command("get_system_info")
async def get_network_info(self) -> dict:
"""Retrieve network configuration information from the miner.
Returns:
dict: A dictionary containing the network configuration of the miner.
"""
return await self.send_command("get_network_info")
async def summary(self) -> dict:
"""Get a summary of the miner's status and performance.
Returns:
dict: A summary of the miner's current operational status.
"""
return await self.send_command("summary")
async def stats(self) -> dict:
"""Get miners stats.
Returns:
dict: A summary of the miner's current operational status.
"""
return await self.send_command("stats")
async def get_blink_status(self) -> dict:
"""Check the status of the LED blinking on the miner.
Returns:
dict: A dictionary indicating whether the LED is currently blinking.
"""
return await self.send_command("get_blink_status")
async def pools(self) -> dict:
"""Check the status of the miner's pools.
Returns:
dict: A dictionary containing the pool status as information.
"""
return await self.send_command("pools")

View File

@@ -15,7 +15,6 @@
# ------------------------------------------------------------------------------
from __future__ import annotations
import asyncio
import json
from pathlib import Path
from typing import Any

View File

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

View File

@@ -15,7 +15,7 @@
# ------------------------------------------------------------------------------
from tests.config_tests import TestConfig
from tests.miners_tests import MinersTest, TestHammerMiners
from tests.miners_tests import MinersTest, TestElphapexMiners, TestHammerMiners
from tests.network_tests import NetworkTest
from tests.rpc_tests import *

View File

@@ -1 +1,2 @@
from .elphapex_tests import *
from .hammer_tests import *

View File

@@ -0,0 +1 @@
from .version_1_0_2 import TestElphapexMiners

View File

@@ -0,0 +1,339 @@
"""Tests for hammer miners with firmware dating 2023-05-28 17-20-35 CST"""
import unittest
from dataclasses import fields
from unittest.mock import patch
from pyasic import APIError, MinerData
from pyasic.data import Fan, HashBoard
from pyasic.device.algorithm.hashrate.unit.scrypt import ScryptUnit
from pyasic.miners.elphapex import ElphapexDG1Plus
POOLS = [
{
"url": "stratum+tcp://stratum.pool.io:3333",
"user": "pool_username.real_worker",
"pwd": "123",
},
{
"url": "stratum+tcp://stratum.pool.io:3334",
"user": "pool_username.real_worker",
"pwd": "123",
},
{
"url": "stratum+tcp://stratum.pool.io:3335",
"user": "pool_username.real_worker",
"pwd": "123",
},
]
data = {
ElphapexDG1Plus: {
"web_get_system_info": {
"ipaddress": "172.19.203.183",
"system_mode": "GNU/Linux",
"netmask": "255.255.255.0",
"gateway": "",
"Algorithm": "Scrypt",
"system_kernel_version": "4.4.194 #1 SMP Sat Sep 7 16:59:20 CST 2024",
"system_filesystem_version": "DG1+_SW_V1.0.2",
"nettype": "DHCP",
"dnsservers": "",
"netdevice": "eth0",
"minertype": "DG1+",
"macaddr": "12:34:56:78:90:12",
"firmware_type": "Release",
"hostname": "DG1+",
},
"web_summary": {
"STATUS": {
"STATUS": "S",
"when": 2557706,
"timestamp": 1731569527,
"api_version": "1.0.0",
"Msg": "summary",
},
"SUMMARY": [
{
"rate_unit": "MH/s",
"elapsed": 357357,
"rate_30m": 0,
"rate_5s": 14920.940000000001,
"bestshare": 0,
"rate_ideal": 14229,
"status": [
{"status": "s", "type": "rate", "msg": "", "code": 0},
{"status": "s", "type": "network", "msg": "", "code": 0},
{"status": "s", "type": "fans", "msg": "", "code": 0},
{"status": "s", "type": "temp", "msg": "", "code": 0},
],
"hw_all": 14199.040000000001,
"rate_avg": 14199.040000000001,
"rate_15m": 14415,
}
],
"INFO": {
"miner_version": "DG1+_SW_V1.0.2",
"CompileTime": "",
"dev_sn": "28HY245192N000245C23B",
"type": "DG1+",
"hw_version": "DG1+_HW_V1.0",
},
},
"web_stats": {
"STATUS": {
"STATUS": "S",
"when": 2557700,
"timestamp": 1731569521,
"api_version": "1.0.0",
"Msg": "stats",
},
"INFO": {
"miner_version": "DG1+_SW_V1.0.2",
"CompileTime": "",
"dev_sn": "28HY245192N000245C23B",
"type": "DG1+",
"hw_version": "DG1+_HW_V1.0",
},
"STATS": [
{
"rate_unit": "MH/s",
"elapsed": 357352,
"rate_30m": 0,
"rate_5s": 11531.879999999999,
"hwp_total": 0.11550000000000001,
"rate_ideal": 14229,
"chain": [
{
"freq_avg": 62000,
"index": 0,
"sn": "13HY245156N000581H11JB52",
"temp_chip": ["47125", "50500", "", ""],
"eeprom_loaded": True,
"rate_15m": 3507,
"hw": 204,
"temp_pcb": [47, 46, 67, 66],
"failrate": 0.029999999999999999,
"asic": "ooooooooo oooooooo oooooooo oooooooo oooo",
"rate_real": 3553.5,
"asic_num": 204,
"temp_pic": [47, 46, 67, 66],
"rate_ideal": 3557.25,
"hashrate": 3278.5999999999999,
},
{
"freq_avg": 62000,
"index": 1,
"sn": "13HY245156N000579H11JB52",
"temp_chip": ["52812", "56937", "", ""],
"eeprom_loaded": True,
"rate_15m": 3736,
"hw": 204,
"temp_pcb": [47, 46, 67, 66],
"failrate": 0.02,
"asic": "ooooooooo oooooooo oooooooo oooooooo oooo",
"rate_real": 3550.1100000000001,
"asic_num": 204,
"temp_pic": [47, 46, 67, 66],
"rate_ideal": 3557.25,
"hashrate": 3491.8400000000001,
},
{
"freq_avg": 62000,
"index": 2,
"sn": "13HY245156N000810H11JB52",
"temp_chip": ["48312", "51687", "", ""],
"eeprom_loaded": True,
"rate_15m": 3531,
"hw": 204,
"temp_pcb": [47, 46, 67, 66],
"failrate": 0.51000000000000001,
"asic": "ooooooooo oooooooo oooooooo oooooooo oooo",
"rate_real": 3551.8000000000002,
"asic_num": 204,
"temp_pic": [47, 46, 67, 66],
"rate_ideal": 3557.25,
"hashrate": 3408.6999999999998,
},
{
"freq_avg": 62000,
"index": 3,
"sn": "13HY245156N000587H11JB52",
"temp_chip": ["46500", "49062", "", ""],
"eeprom_loaded": True,
"rate_15m": 3641,
"hw": 204,
"temp_pcb": [47, 46, 67, 66],
"failrate": 0.029999999999999999,
"asic": "ooooooooo oooooooo oooooooo oooooooo oooo",
"rate_real": 3543.6300000000001,
"asic_num": 204,
"temp_pic": [47, 46, 67, 66],
"rate_ideal": 3557.25,
"hashrate": 3463.6799999999998,
},
],
"rate_15m": 14415,
"chain_num": 4,
"fan": ["5340", "5400", "5400", "5400"],
"rate_avg": 14199.040000000001,
"fan_num": 4,
}
],
},
"web_get_blink_status": {"blink": False},
"web_get_miner_conf": {
"pools": [
{
"url": "stratum+tcp://ltc.trustpool.ru:3333",
"pass": "123",
"user": "Nikita9231.fworker",
},
{
"url": "stratum+tcp://ltc.trustpool.ru:443",
"pass": "123",
"user": "Nikita9231.fworker",
},
{
"url": "stratum+tcp://ltc.trustpool.ru:25",
"pass": "123",
"user": "Nikita9231.fworker",
},
],
"fc-voltage": "1470",
"fc-fan-ctrl": False,
"fc-freq-level": "100",
"fc-fan-pwm": "80",
"algo": "ltc",
"fc-work-mode": 0,
"fc-freq": "1850",
},
"web_pools": {
"STATUS": {
"STATUS": "S",
"when": 5411762,
"timestamp": 1738768594,
"api_version": "1.0.0",
"Msg": "pools",
},
"Device Total Rejected": 8888,
"POOLS": [
{
"diffs": 0,
"diffr": 524288,
"index": 0,
"user": "pool_username.real_worker",
"lsdiff": 524288,
"lstime": "00:00:18",
"diffa": 524288,
"accepted": 798704,
"diff1": 0,
"stale": 0,
"diff": "",
"rejected": 3320,
"status": "Unreachable",
"getworks": 802024,
"priority": 0,
"url": "stratum+tcp://stratum.pool.io:3333",
},
{
"diffs": 0,
"diffr": 524288,
"index": 1,
"user": "pool_username.real_worker",
"lsdiff": 524288,
"lstime": "00:00:00",
"diffa": 524288,
"accepted": 604803,
"diff1": 0,
"stale": 0,
"diff": "",
"rejected": 2492,
"status": "Alive",
"getworks": 607295,
"priority": 1,
"url": "stratum+tcp://stratum.pool.io:3334",
},
{
"diffs": 0,
"diffr": 524288,
"index": 2,
"user": "pool_username.real_worker",
"lsdiff": 524288,
"lstime": "00:00:05",
"diffa": 524288,
"accepted": 691522,
"diff1": 0,
"stale": 0,
"diff": "",
"rejected": 3076,
"status": "Unreachable",
"getworks": 694598,
"priority": 2,
"url": "stratum+tcp://stratum.pool.io:3335",
},
],
"Device Rejected%": 0.41999999999999998,
"Device Total Work": 2103917,
"INFO": {
"miner_version": "DG1+_SW_V1.0.2",
"CompileTime": "",
"dev_sn": "28HY245192N000245C23B",
"type": "DG1+",
"hw_version": "DG1+_HW_V1.0",
},
},
}
}
class TestElphapexMiners(unittest.IsolatedAsyncioTestCase):
@patch("pyasic.rpc.base.BaseMinerRPCAPI._send_bytes")
async def test_all_data_gathering(self, mock_send_bytes):
mock_send_bytes.raises = APIError()
for m_type in data:
gathered_data = {}
miner = m_type("127.0.0.1")
for data_name in fields(miner.data_locations):
if data_name.name == "config":
# skip
continue
data_func = getattr(miner.data_locations, data_name.name)
fn_args = data_func.kwargs
args_to_send = {k.name: data[m_type][k.name] for k in fn_args}
function = getattr(miner, data_func.cmd)
gathered_data[data_name.name] = await function(**args_to_send)
result = MinerData(
ip=str(miner.ip),
device_info=miner.device_info,
expected_chips=(
miner.expected_chips * miner.expected_hashboards
if miner.expected_chips is not None
else 0
),
expected_hashboards=miner.expected_hashboards,
expected_fans=miner.expected_fans,
hashboards=[
HashBoard(slot=i, expected_chips=miner.expected_chips)
for i in range(miner.expected_hashboards)
],
)
for item in gathered_data:
if gathered_data[item] is not None:
setattr(result, item, gathered_data[item])
self.assertEqual(result.mac, "12:34:56:78:90:12")
self.assertEqual(result.api_ver, "1.0.0")
self.assertEqual(result.fw_ver, "1.0.2")
self.assertEqual(result.hostname, "DG1+")
self.assertEqual(round(result.hashrate.into(ScryptUnit.MH)), 14199)
self.assertEqual(
result.fans,
[Fan(speed=5340), Fan(speed=5400), Fan(speed=5400), Fan(speed=5400)],
)
self.assertEqual(result.total_chips, result.expected_chips)
self.assertEqual(
set([str(p.url) for p in result.pools]), set(p["url"] for p in POOLS)
)

View File

@@ -16,12 +16,12 @@ POOLS = [
"pwd": "123",
},
{
"url": "stratum+tcp://stratum.pool.io:3333",
"url": "stratum+tcp://stratum.pool.io:3334",
"user": "pool_username.real_worker",
"pwd": "123",
},
{
"url": "stratum+tcp://stratum.pool.io:3333",
"url": "stratum+tcp://stratum.pool.io:3335",
"user": "pool_username.real_worker",
"pwd": "123",
},