feature: add support for Elphapex DG1+

This commit is contained in:
Upstream Data
2025-02-04 10:19:45 -07:00
committed by Brett Rowan
parent 90d8a795e6
commit a5c42c9c2b
24 changed files with 891 additions and 1 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
## 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

@@ -914,4 +914,15 @@ details {
</ul>
</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

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

View File

@@ -550,6 +550,10 @@ class BraiinsModels(MinerModelType):
BMM101 = "BMM101"
class ElphapexModels(MinerModelType):
DG1Plus = "DG1+"
class MinerModel:
ANTMINER = AntminerModels
WHATSMINER = WhatsminerModels
@@ -563,4 +567,5 @@ class MinerModel:
ICERIVER = IceRiverModels
HAMMER = HammerModels
VOLCMINER = VolcMinerModels
ELPHAPEX = ElphapexModels
BRAIINS = BraiinsModels

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

@@ -0,0 +1,319 @@
# ------------------------------------------------------------------------------
# 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
from pyasic.data import Fan, HashBoard, X19Error
from pyasic.data.error_codes import MinerErrorData
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")],
),
}
)
class ElphapexMiner(StockFirmware):
"""Handler for Elphapex miners."""
_web_cls = ElphapexWebAPI
web: ElphapexWebAPI
data_locations = ELPHAPEX_DATA_LOC
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

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 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, HammerMake
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 = {
@@ -664,6 +666,10 @@ MINER_CLASSES = {
None: type("VolcMinerUnknown", (BlackMiner, VolcMinerMake), {}),
"VOLCMINER D1": VolcMinerD1,
},
MinerTypes.ELPHAPEX: {
None: type("ElphapexUnknown", (ElphapexMiner, ElphapexMake), {}),
"DG1+1": ElphapexDG1Plus,
},
}
@@ -745,6 +751,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 +835,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 (
@@ -1384,6 +1395,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",
}

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

@@ -0,0 +1,216 @@
# ------------------------------------------------------------------------------
# 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")

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 .alphapex_tests import *
from .hammer_tests import *

View File

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

View File

@@ -0,0 +1,243 @@
"""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
from pyasic.miners.hammer import HammerD10
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",
},
}
}
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)