From 631f7e95586c6d771dcfed5e3f688629efdab01f Mon Sep 17 00:00:00 2001 From: purcell-lab Date: Sun, 10 May 2026 12:30:54 +0000 Subject: [PATCH 1/4] Fix DG-Home1 Elphapex detection and harden hashboard parsing - factory.py: uppercase ELPHAPEX lookup keys (case-insensitive lookup); add DG-HOME1 alias; get_miner_model_elphapex falls back to type/model/hostname from get_system_info.cgi and to INFO.type/INFO.model/INFO.miner_version from stats.cgi; new _normalize_elphapex_model helper maps 'DG-Home1' / 'DG-Home1_V1.0.5' to 'DG1-Home'. - backends/elphapex.py _get_hashboards: derive expected_hashboards from STATS[0].chain_num when None (prevents range(None) TypeError); skip out-of-range board indices; guard chip_temp averaging against empty/non-numeric temp_chip entries; use .get() throughout; set hb.missing=True when asic_num == 0. Closes #428. Refs #311. --- pyasic/miners/backends/elphapex.py | 113 +++++++++++++++++++++-------- pyasic/miners/factory.py | 50 +++++++++++-- 2 files changed, 126 insertions(+), 37 deletions(-) diff --git a/pyasic/miners/backends/elphapex.py b/pyasic/miners/backends/elphapex.py index 8318d23d8..5bc944fa3 100644 --- a/pyasic/miners/backends/elphapex.py +++ b/pyasic/miners/backends/elphapex.py @@ -217,47 +217,98 @@ async def _get_errors( # type: ignore[override] return errors async def _get_hashboards(self, web_stats: dict | None = None) -> list[HashBoard]: - if self.expected_hashboards is None: + if web_stats is None: + try: + web_stats = await self.web.stats() + except APIError: + web_stats = None + + # Derive ``expected_hashboards`` from ``stats.cgi`` when the device + # model is only partially supported (``self.expected_hashboards`` is + # ``None``) and the response advertises ``chain_num``. Without this, + # ``range(self.expected_hashboards)`` raises ``TypeError`` and bubbles + # up as an ``APIError`` for DG-Home1 on pyasic 0.79.0 + # (see UpstreamData/pyasic#311, #428). + expected_hashboards = self.expected_hashboards + if expected_hashboards is None and isinstance(web_stats, dict): + try: + stats0 = web_stats["STATS"][0] + chain_num = stats0.get("chain_num") + if chain_num is None and isinstance(stats0.get("chain"), list): + chain_num = len(stats0["chain"]) + if isinstance(chain_num, int) and chain_num > 0: + expected_hashboards = chain_num + except (LookupError, TypeError): + pass + + if expected_hashboards is None: return [] hashboards = [ HashBoard(slot=idx, expected_chips=self.expected_chips) - for idx in range(self.expected_hashboards) + for idx in range(expected_hashboards) ] if web_stats is None: - try: - web_stats = await self.web.stats() - except APIError: - return hashboards + return hashboards - if web_stats is not None: + try: + chains = web_stats["STATS"][0]["chain"] + except (LookupError, TypeError): + return hashboards + + for board in chains: 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, # type: ignore[attr-defined] - ).into( - self.algo.unit.default # type: ignore[attr-defined] - ) - hashboards[board["index"]].chips = board["asic_num"] - board_temp_data = list( - filter(lambda x: not x == 0, board["temp_pcb"]) - ) - 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"]) - ) - 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: + board_index = board["index"] + except (KeyError, TypeError): + continue + if not isinstance(board_index, int) or board_index >= len(hashboards): + # Unknown / out-of-range chain index; skip rather than crash. + continue + + hb = hashboards[board_index] + try: + hb.hashrate = self.algo.hashrate( + rate=board.get("rate_real", 0), + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) + except (TypeError, ValueError): pass + + asic_num = board.get("asic_num") + if isinstance(asic_num, int): + hb.chips = asic_num + + board_temp_data = [ + t + for t in board.get("temp_pcb", []) + if isinstance(t, (int, float)) and t != 0 + ] + if board_temp_data: + hb.temp = sum(board_temp_data) / len(board_temp_data) + + # ``temp_chip`` is a list of strings on Elphapex; entries are empty + # strings on inactive chains. Only compute the average when at + # least one numeric reading is present — dividing by zero here was + # the proximate cause of the original DG-Home1 traceback. + chip_temp_data = [] + for raw in board.get("temp_chip", []): + if raw in (None, ""): + continue + try: + chip_temp_data.append(int(raw) / 1000) + except (TypeError, ValueError): + continue + if chip_temp_data: + hb.chip_temp = sum(chip_temp_data) / len(chip_temp_data) + + hb.serial_number = board.get("sn") or None + # Treat a chain with no live ASICs as missing so downstream + # consumers can distinguish a populated-but-broken board from an + # empty slot (DG-Home1 ships with 1 of 4 chains populated). + hb.missing = not (isinstance(asic_num, int) and asic_num > 0) return hashboards async def _get_fault_light( diff --git a/pyasic/miners/factory.py b/pyasic/miners/factory.py index 0dd23919d..67bbbdddd 100644 --- a/pyasic/miners/factory.py +++ b/pyasic/miners/factory.py @@ -720,7 +720,12 @@ class MinerTypes(enum.Enum): None: type("ElphapexUnknown", (ElphapexMiner, ElphapexMake), {}), "DG1+": ElphapexDG1Plus, "DG1": ElphapexDG1, - "DG1-Home": ElphapexDG1Home, + # NOTE: lookups apply ``str(miner_model).upper()`` so keys must be uppercase. + # The DG-Home1 firmware reports ``INFO.type = "DG-Home1"`` from + # ``/cgi-bin/stats.cgi``; ``get_miner_model_elphapex`` normalises that to + # ``"DG1-HOME"`` for consistency with ``DG1`` / ``DG1+`` naming. + "DG1-HOME": ElphapexDG1Home, + "DG-HOME1": ElphapexDG1Home, }, MinerTypes.FLUMINER: { None: type("FluminerUnknown", (Fluminer, FluminerMake), {}), @@ -1645,14 +1650,47 @@ async def get_miner_model_elphapex(self, ip: str) -> str | None: ip, "/cgi-bin/get_system_info.cgi", auth=auth ) + # Try ``get_system_info.cgi`` first — older Elphapex firmware exposes + # ``minertype`` here. Newer DG-Home1 firmware (V1.0.5) does not, so we + # also accept ``type`` / ``model`` / ``hostname`` from the same response. if web_json_data is not None: - try: - miner_model = web_json_data["minertype"] - return miner_model - except (TypeError, LookupError): - pass + for key in ("minertype", "type", "model", "hostname"): + try: + miner_model = web_json_data[key] + except (TypeError, LookupError): + continue + if miner_model: + return self._normalize_elphapex_model(miner_model) + + # Fallback: ``stats.cgi`` includes ``INFO.type`` (e.g. ``"DG-Home1"``) + # on firmware that omits ``minertype`` from ``get_system_info.cgi``. + stats_data = await self.send_web_command(ip, "/cgi-bin/stats.cgi", auth=auth) + if stats_data is not None: + info = stats_data.get("INFO") if isinstance(stats_data, dict) else None + if isinstance(info, dict): + for key in ("type", "model", "miner_version"): + miner_model = info.get(key) + if miner_model: + return self._normalize_elphapex_model(miner_model) return None + @staticmethod + def _normalize_elphapex_model(miner_model: str) -> str: + """Normalise Elphapex model strings to the lookup-table form. + + DG-Home1 firmware V1.0.5 reports ``"DG-Home1"`` from ``stats.cgi`` + (and the firmware version string is ``"DG-Home1_V1.0.5"``), but the + lookup table is keyed on the upstream-canonical ``"DG1-Home"``. + Strip any firmware-version suffix and remap known aliases. + """ + model = str(miner_model).strip() + # Drop firmware suffix like ``_V1.0.5`` if we got fed ``miner_version``. + model = model.split("_")[0] + # Known aliases for the DG-Home1 (also covers ``DG-HOME1``, ``DG_Home1``). + if model.upper().replace("_", "-") in ("DG-HOME1", "DG-HOME"): + return "DG1-Home" + return model + async def get_miner_model_fluminer(self, ip: str) -> str | None: web_json_data = await self.send_web_command(ip, "/api/overview") From 6945f0405a429a6d1bb7be982d523c8115edaa1b Mon Sep 17 00:00:00 2001 From: purcell-lab Date: Sun, 10 May 2026 12:53:43 +0000 Subject: [PATCH 2/4] Refactor _get_hashboards into helpers to reduce complexity CodeFactor flagged the previous implementation as a Complex Method (cyclomatic complexity 23). Split into small focused helpers: - _resolve_expected_hashboards: derive slot count from stats.cgi when expected_hashboards is None on partially-supported devices. - _iter_stats_chains: safe access to STATS[0].chain. - _apply_chain_to_board: populate one HashBoard from a chain dict. - _set_board_hashrate / _average_pcb_temp / _average_chip_temp: isolated, individually testable building blocks. _get_hashboards itself drops from cyclomatic complexity 23 (radon: D 30) to 6 (radon: B 6); no method in the file exceeds 9. Behaviour is preserved: same outputs on the DG-Home1 stats payload, same fallback to chain_num for partial-support, same APIError-safe path when stats are unavailable. Added defensive isinstance/type guards so malformed payloads (missing STATS, non-list 'chain', non-int 'index', non-list temp arrays) no longer raise. --- pyasic/miners/backends/elphapex.py | 174 +++++++++++++++++------------ 1 file changed, 101 insertions(+), 73 deletions(-) diff --git a/pyasic/miners/backends/elphapex.py b/pyasic/miners/backends/elphapex.py index 5bc944fa3..b93353d61 100644 --- a/pyasic/miners/backends/elphapex.py +++ b/pyasic/miners/backends/elphapex.py @@ -223,93 +223,121 @@ async def _get_hashboards(self, web_stats: dict | None = None) -> list[HashBoard except APIError: web_stats = None - # Derive ``expected_hashboards`` from ``stats.cgi`` when the device - # model is only partially supported (``self.expected_hashboards`` is - # ``None``) and the response advertises ``chain_num``. Without this, - # ``range(self.expected_hashboards)`` raises ``TypeError`` and bubbles - # up as an ``APIError`` for DG-Home1 on pyasic 0.79.0 - # (see UpstreamData/pyasic#311, #428). - expected_hashboards = self.expected_hashboards - if expected_hashboards is None and isinstance(web_stats, dict): - try: - stats0 = web_stats["STATS"][0] - chain_num = stats0.get("chain_num") - if chain_num is None and isinstance(stats0.get("chain"), list): - chain_num = len(stats0["chain"]) - if isinstance(chain_num, int) and chain_num > 0: - expected_hashboards = chain_num - except (LookupError, TypeError): - pass - - if expected_hashboards is None: + expected = self._resolve_expected_hashboards(web_stats) + if expected is None: return [] hashboards = [ HashBoard(slot=idx, expected_chips=self.expected_chips) - for idx in range(expected_hashboards) + for idx in range(expected) ] + for board in self._iter_stats_chains(web_stats): + self._apply_chain_to_board(hashboards, board) + return hashboards - if web_stats is None: - return hashboards + def _resolve_expected_hashboards(self, web_stats: dict | None) -> int | None: + """Return the number of hashboard slots to model. + + Falls back to ``STATS[0].chain_num`` (or ``len(STATS[0].chain)``) when + the device is only partially supported and ``self.expected_hashboards`` + is ``None``. Without this, ``range(self.expected_hashboards)`` raises + ``TypeError`` and bubbles up as ``APIError`` for DG-Home1 on pyasic + 0.79.0 (see UpstreamData/pyasic#311, #428). + """ + if self.expected_hashboards is not None: + return self.expected_hashboards + if not isinstance(web_stats, dict): + return None + try: + stats0 = web_stats["STATS"][0] + except (LookupError, TypeError): + return None + chain_num = stats0.get("chain_num") + if chain_num is None and isinstance(stats0.get("chain"), list): + chain_num = len(stats0["chain"]) + if isinstance(chain_num, int) and chain_num > 0: + return chain_num + return None + @staticmethod + def _iter_stats_chains(web_stats: dict | None) -> list[dict]: + """Return the per-chain stats list, or an empty list if missing.""" + if not isinstance(web_stats, dict): + return [] try: chains = web_stats["STATS"][0]["chain"] except (LookupError, TypeError): - return hashboards + return [] + return chains if isinstance(chains, list) else [] - for board in chains: - try: - board_index = board["index"] - except (KeyError, TypeError): - continue - if not isinstance(board_index, int) or board_index >= len(hashboards): - # Unknown / out-of-range chain index; skip rather than crash. - continue + def _apply_chain_to_board(self, hashboards: list[HashBoard], board: dict) -> None: + """Populate a single ``HashBoard`` from a ``stats.cgi`` chain entry.""" + board_index = board.get("index") if isinstance(board, dict) else None + if not isinstance(board_index, int) or board_index >= len(hashboards): + # Unknown / out-of-range chain index; skip rather than crash. + return + + hb = hashboards[board_index] + self._set_board_hashrate(hb, board.get("rate_real", 0)) - hb = hashboards[board_index] + asic_num = board.get("asic_num") + if isinstance(asic_num, int): + hb.chips = asic_num + + temp = self._average_pcb_temp(board.get("temp_pcb")) + if temp is not None: + hb.temp = temp + + chip_temp = self._average_chip_temp(board.get("temp_chip")) + if chip_temp is not None: + hb.chip_temp = chip_temp + + hb.serial_number = board.get("sn") or None + # Treat a chain with no live ASICs as missing so downstream consumers + # can distinguish a populated-but-broken board from an empty slot + # (DG-Home1 ships with 1 of 4 chains populated). + hb.missing = not (isinstance(asic_num, int) and asic_num > 0) + + def _set_board_hashrate(self, hb: HashBoard, rate_real: float | int) -> None: + try: + hb.hashrate = self.algo.hashrate( + rate=rate_real, + unit=self.algo.unit.MH, # type: ignore[attr-defined] + ).into( + self.algo.unit.default # type: ignore[attr-defined] + ) + except (TypeError, ValueError): + pass + + @staticmethod + def _average_pcb_temp(temps: object) -> float | None: + if not isinstance(temps, list): + return None + readings = [t for t in temps if isinstance(t, (int, float)) and t != 0] + if not readings: + return None + return sum(readings) / len(readings) + + @staticmethod + def _average_chip_temp(temps: object) -> float | None: + """Average ``temp_chip`` entries (millidegree strings on Elphapex). + + Inactive chains report empty strings here; a single ``None`` chain on + DG-Home1 used to trigger ``ZeroDivisionError`` in the legacy parser. + """ + if not isinstance(temps, list): + return None + readings: list[float] = [] + for raw in temps: + if raw in (None, ""): + continue try: - hb.hashrate = self.algo.hashrate( - rate=board.get("rate_real", 0), - unit=self.algo.unit.MH, # type: ignore[attr-defined] - ).into( - self.algo.unit.default # type: ignore[attr-defined] - ) + readings.append(int(raw) / 1000) except (TypeError, ValueError): - pass - - asic_num = board.get("asic_num") - if isinstance(asic_num, int): - hb.chips = asic_num - - board_temp_data = [ - t - for t in board.get("temp_pcb", []) - if isinstance(t, (int, float)) and t != 0 - ] - if board_temp_data: - hb.temp = sum(board_temp_data) / len(board_temp_data) - - # ``temp_chip`` is a list of strings on Elphapex; entries are empty - # strings on inactive chains. Only compute the average when at - # least one numeric reading is present — dividing by zero here was - # the proximate cause of the original DG-Home1 traceback. - chip_temp_data = [] - for raw in board.get("temp_chip", []): - if raw in (None, ""): - continue - try: - chip_temp_data.append(int(raw) / 1000) - except (TypeError, ValueError): - continue - if chip_temp_data: - hb.chip_temp = sum(chip_temp_data) / len(chip_temp_data) - - hb.serial_number = board.get("sn") or None - # Treat a chain with no live ASICs as missing so downstream - # consumers can distinguish a populated-but-broken board from an - # empty slot (DG-Home1 ships with 1 of 4 chains populated). - hb.missing = not (isinstance(asic_num, int) and asic_num > 0) - return hashboards + continue + if not readings: + return None + return sum(readings) / len(readings) async def _get_fault_light( self, web_get_blink_status: dict | None = None From 9f5da8218175a5a4f2de87613ca8d9b9701e06ac Mon Sep 17 00:00:00 2001 From: purcell-lab Date: Sun, 10 May 2026 12:56:56 +0000 Subject: [PATCH 3/4] Reformat docstrings to satisfy pydocstyle D213 Codacy flagged 3 multi-line docstrings (D213: 'Multi-line docstring summary should start at the second line'). Move the summary line below the opening triple-quote on: - ElphapexMiner._get_hashboards - ElphapexMiner._resolve_expected_hashboards - ElphapexMiner._average_chip_temp - MinerFactory._normalize_elphapex_model Also adds short single-line docstrings to _set_board_hashrate and _average_pcb_temp for consistency with the surrounding helpers. --- pyasic/miners/backends/elphapex.py | 17 +++++++++++++++-- pyasic/miners/factory.py | 3 ++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pyasic/miners/backends/elphapex.py b/pyasic/miners/backends/elphapex.py index b93353d61..93d3cbeb2 100644 --- a/pyasic/miners/backends/elphapex.py +++ b/pyasic/miners/backends/elphapex.py @@ -217,6 +217,15 @@ async def _get_errors( # type: ignore[override] return errors async def _get_hashboards(self, web_stats: dict | None = None) -> list[HashBoard]: + """ + Build the list of ``HashBoard`` records from ``stats.cgi``. + + Tolerates partially-supported devices (model resolved but + ``expected_hashboards`` ``None``), missing/malformed payloads, and + sparsely-populated chains (e.g. DG-Home1 ships with 1 of 4 chains + populated). See ``_resolve_expected_hashboards`` for the slot-count + fallback rationale. + """ if web_stats is None: try: web_stats = await self.web.stats() @@ -236,7 +245,8 @@ async def _get_hashboards(self, web_stats: dict | None = None) -> list[HashBoard return hashboards def _resolve_expected_hashboards(self, web_stats: dict | None) -> int | None: - """Return the number of hashboard slots to model. + """ + Return the number of hashboard slots to model. Falls back to ``STATS[0].chain_num`` (or ``len(STATS[0].chain)``) when the device is only partially supported and ``self.expected_hashboards`` @@ -299,6 +309,7 @@ def _apply_chain_to_board(self, hashboards: list[HashBoard], board: dict) -> Non hb.missing = not (isinstance(asic_num, int) and asic_num > 0) def _set_board_hashrate(self, hb: HashBoard, rate_real: float | int) -> None: + """Convert ``rate_real`` (MH/s on Elphapex) into the algo's default unit.""" try: hb.hashrate = self.algo.hashrate( rate=rate_real, @@ -311,6 +322,7 @@ def _set_board_hashrate(self, hb: HashBoard, rate_real: float | int) -> None: @staticmethod def _average_pcb_temp(temps: object) -> float | None: + """Average non-zero PCB temperature readings, or ``None`` if empty.""" if not isinstance(temps, list): return None readings = [t for t in temps if isinstance(t, (int, float)) and t != 0] @@ -320,7 +332,8 @@ def _average_pcb_temp(temps: object) -> float | None: @staticmethod def _average_chip_temp(temps: object) -> float | None: - """Average ``temp_chip`` entries (millidegree strings on Elphapex). + """ + Average ``temp_chip`` entries (millidegree strings on Elphapex). Inactive chains report empty strings here; a single ``None`` chain on DG-Home1 used to trigger ``ZeroDivisionError`` in the legacy parser. diff --git a/pyasic/miners/factory.py b/pyasic/miners/factory.py index 67bbbdddd..27fcb26db 100644 --- a/pyasic/miners/factory.py +++ b/pyasic/miners/factory.py @@ -1676,7 +1676,8 @@ async def get_miner_model_elphapex(self, ip: str) -> str | None: @staticmethod def _normalize_elphapex_model(miner_model: str) -> str: - """Normalise Elphapex model strings to the lookup-table form. + """ + Normalise Elphapex model strings to the lookup-table form. DG-Home1 firmware V1.0.5 reports ``"DG-Home1"`` from ``stats.cgi`` (and the firmware version string is ``"DG-Home1_V1.0.5"``), but the From ce9fc18beefba02141b3b3cf2cec5db8b4adc2a3 Mon Sep 17 00:00:00 2001 From: purcell-lab Date: Sun, 10 May 2026 13:00:04 +0000 Subject: [PATCH 4/4] Use single-line docstrings to satisfy both D212 and D213 Codacy enforces both pydocstyle D212 ('summary on first line') and D213 ('summary on second line'), which are mutually exclusive for any multi-line docstring. Collapse the four affected docstrings to single lines and move the elaboration into adjacent comments. No behaviour change; ruff format and ruff check both clean; existing tests pass. --- pyasic/miners/backends/elphapex.py | 41 +++++++++++++----------------- pyasic/miners/factory.py | 13 ++++------ 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/pyasic/miners/backends/elphapex.py b/pyasic/miners/backends/elphapex.py index 93d3cbeb2..1e642988d 100644 --- a/pyasic/miners/backends/elphapex.py +++ b/pyasic/miners/backends/elphapex.py @@ -217,15 +217,12 @@ async def _get_errors( # type: ignore[override] return errors async def _get_hashboards(self, web_stats: dict | None = None) -> list[HashBoard]: - """ - Build the list of ``HashBoard`` records from ``stats.cgi``. - - Tolerates partially-supported devices (model resolved but - ``expected_hashboards`` ``None``), missing/malformed payloads, and - sparsely-populated chains (e.g. DG-Home1 ships with 1 of 4 chains - populated). See ``_resolve_expected_hashboards`` for the slot-count - fallback rationale. - """ + """Build the list of ``HashBoard`` records from ``stats.cgi``.""" + # Tolerates partially-supported devices (model resolved but + # ``expected_hashboards`` is ``None``), missing/malformed payloads, + # and sparsely-populated chains (e.g. DG-Home1 ships with 1 of 4 + # chains populated). See ``_resolve_expected_hashboards`` for the + # slot-count fallback rationale. if web_stats is None: try: web_stats = await self.web.stats() @@ -245,15 +242,13 @@ async def _get_hashboards(self, web_stats: dict | None = None) -> list[HashBoard return hashboards def _resolve_expected_hashboards(self, web_stats: dict | None) -> int | None: - """ - Return the number of hashboard slots to model. - - Falls back to ``STATS[0].chain_num`` (or ``len(STATS[0].chain)``) when - the device is only partially supported and ``self.expected_hashboards`` - is ``None``. Without this, ``range(self.expected_hashboards)`` raises - ``TypeError`` and bubbles up as ``APIError`` for DG-Home1 on pyasic - 0.79.0 (see UpstreamData/pyasic#311, #428). - """ + """Return the number of hashboard slots to model.""" + # Falls back to ``STATS[0].chain_num`` (or ``len(STATS[0].chain)``) + # when the device is only partially supported and + # ``self.expected_hashboards`` is ``None``. Without this, + # ``range(self.expected_hashboards)`` raises ``TypeError`` and bubbles + # up as ``APIError`` for DG-Home1 on pyasic 0.79.0 + # (see UpstreamData/pyasic#311, #428). if self.expected_hashboards is not None: return self.expected_hashboards if not isinstance(web_stats, dict): @@ -332,12 +327,10 @@ def _average_pcb_temp(temps: object) -> float | None: @staticmethod def _average_chip_temp(temps: object) -> float | None: - """ - Average ``temp_chip`` entries (millidegree strings on Elphapex). - - Inactive chains report empty strings here; a single ``None`` chain on - DG-Home1 used to trigger ``ZeroDivisionError`` in the legacy parser. - """ + """Average ``temp_chip`` entries (millidegree strings on Elphapex).""" + # Inactive chains report empty strings here; a single ``None`` chain + # on DG-Home1 used to trigger ``ZeroDivisionError`` in the legacy + # parser. if not isinstance(temps, list): return None readings: list[float] = [] diff --git a/pyasic/miners/factory.py b/pyasic/miners/factory.py index 27fcb26db..a0887b7c0 100644 --- a/pyasic/miners/factory.py +++ b/pyasic/miners/factory.py @@ -1676,14 +1676,11 @@ async def get_miner_model_elphapex(self, ip: str) -> str | None: @staticmethod def _normalize_elphapex_model(miner_model: str) -> str: - """ - Normalise Elphapex model strings to the lookup-table form. - - DG-Home1 firmware V1.0.5 reports ``"DG-Home1"`` from ``stats.cgi`` - (and the firmware version string is ``"DG-Home1_V1.0.5"``), but the - lookup table is keyed on the upstream-canonical ``"DG1-Home"``. - Strip any firmware-version suffix and remap known aliases. - """ + """Normalise Elphapex model strings to the lookup-table form.""" + # DG-Home1 firmware V1.0.5 reports ``"DG-Home1"`` from ``stats.cgi`` + # (and the firmware version string is ``"DG-Home1_V1.0.5"``), but the + # lookup table is keyed on the upstream-canonical ``"DG1-Home"``. + # Strip any firmware-version suffix and remap known aliases. model = str(miner_model).strip() # Drop firmware suffix like ``_V1.0.5`` if we got fed ``miner_version``. model = model.split("_")[0]