diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 64d13cf..cf77226 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.0 +current_version = 0.5.0 commit = True tag = True tag_name = v{new_version} diff --git a/CHANGELOG.md b/CHANGELOG.md index daac1b9..9b60422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.0] - 2026-02-21 + +### Fixed + +- **Missing `PROPORTIONAL_PRESSURE` in `_MODE_BYTE_MAP`**: Added mode value `1` → byte `0x01`, preventing a `KeyError` when starting or stopping the pump in proportional pressure mode. +- **Alarm/warning descriptions not populated**: `read_alarms()` now resolves each active alarm and warning code against `ERROR_CODES`, populating `alarm_description` and `warning_description` as comma-separated human-readable strings. Unknown codes fall back to `"Unknown ()"`. + +### Changed + +- **CLI alarm panel**: The alarm status panel now lists every active alarm and warning code with its description rather than showing only a single code. + +### Tests + +- Extended `test_read_alarms` to assert `alarm_description` and `warning_description` are correctly populated. +- Added `test_read_alarms_unknown_code_fallback` to verify the `"Unknown ()"` fallback for codes not present in `ERROR_CODES`. + ## [0.4.0] - 2026-02-14 ### Fixed diff --git a/docs/index.md b/docs/index.md index 43b33f5..ed46c1f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,4 +9,4 @@ It provides both a high-level Python API and a Command Line Interface (CLI) for This project is **not affiliated with, endorsed by, or associated with Grundfos**. Use this software at your own risk. Incorrect usage of motor control commands could potentially damage hardware, although safety limits in the pump firmware generally prevent this. --- -*Version: 0.4.0* +*Version: 0.5.0* diff --git a/mkdocs.yml b/mkdocs.yml index 768ce6d..e2e5a3b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ site_name: Alpha HWR Documentation site_description: Independent implementation and control documentation for Grundfos ALPHA HWR pumps. -version: "0.4.0" +version: "0.5.0" theme: name: material diff --git a/pyproject.toml b/pyproject.toml index 7c0015a..aab765f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dev-dependencies = [ [project] name = "alpha-hwr" -version = "0.4.0" +version = "0.5.0" description = "Modern Python library and CLI for Grundfos ALPHA HWR pumps via Bluetooth Low Energy" readme = "README.md" authors = [ diff --git a/src/alpha_hwr/__init__.py b/src/alpha_hwr/__init__.py index dc3fa68..297ed30 100644 --- a/src/alpha_hwr/__init__.py +++ b/src/alpha_hwr/__init__.py @@ -2,7 +2,7 @@ Grundfos ALPHA HWR Client Library """ -__version__ = "0.4.0" +__version__ = "0.5.0" from .client import AlphaHWRClient, discover_devices from .models import ( diff --git a/src/alpha_hwr/cli/output/formatters.py b/src/alpha_hwr/cli/output/formatters.py index b3035e5..dbf5d48 100644 --- a/src/alpha_hwr/cli/output/formatters.py +++ b/src/alpha_hwr/cli/output/formatters.py @@ -13,6 +13,7 @@ from rich.table import Table from rich.panel import Panel +from ...constants import ERROR_CODES from ...models import ( TelemetryData, DeviceInfo, @@ -336,25 +337,41 @@ def format_alarm_panel(alarm: AlarmInfo) -> Panel: Returns: Rich Panel object """ - if not alarm.alarm_code or alarm.alarm_code == 0: - content = "[green]✓ No active alarms[/green]" - style = "green" - else: - lines = [ - f"[bold red]⚠ ALARM {alarm.alarm_code}[/bold red]", - f"[bold]Description:[/bold] {alarm.alarm_description or 'Unknown'}", - ] - - if alarm.warning_code and alarm.warning_code != 0: - lines.append("") - lines.append(f"[yellow]⚠ WARNING {alarm.warning_code}[/yellow]") - lines.append( - f"[bold]Description:[/bold] {alarm.warning_description or 'Unknown'}" - ) + lines: list[str] = [] + style = "green" - content = "\n".join(lines) + # Active alarms + if alarm.active_alarms: style = "red" + for code in alarm.active_alarms: + desc = ERROR_CODES.get(code, f"Unknown ({code})") + lines.append(f"[bold red]⚠ ALARM {code}: {desc}[/bold red]") + elif alarm.alarm_code and alarm.alarm_code != 0: + style = "red" + desc = ERROR_CODES.get( + alarm.alarm_code, alarm.alarm_description or "Unknown" + ) + lines.append(f"[bold red]⚠ ALARM {alarm.alarm_code}: {desc}[/bold red]") + + # Active warnings + if alarm.active_warnings: + if style == "green": + style = "yellow" + for code in alarm.active_warnings: + desc = ERROR_CODES.get(code, f"Unknown ({code})") + lines.append(f"[yellow]⚠ WARNING {code}: {desc}[/yellow]") + elif alarm.warning_code and alarm.warning_code != 0: + if style == "green": + style = "yellow" + desc = ERROR_CODES.get( + alarm.warning_code, alarm.warning_description or "Unknown" + ) + lines.append(f"[yellow]⚠ WARNING {alarm.warning_code}: {desc}[/yellow]") + + if not lines: + lines.append("[green]✓ No active alarms or warnings[/green]") + content = "\n".join(lines) return Panel(content, title="Alarm Status", border_style=style) diff --git a/src/alpha_hwr/services/control.py b/src/alpha_hwr/services/control.py index 8b1c6e4..4e15bae 100644 --- a/src/alpha_hwr/services/control.py +++ b/src/alpha_hwr/services/control.py @@ -144,6 +144,7 @@ class ControlService(BaseService): # Value -> Mode Byte used in control payload _MODE_BYTE_MAP = { 0: 0x00, # CONSTANT_PRESSURE + 1: 0x01, # PROPORTIONAL_PRESSURE 2: 0x02, # CONSTANT_SPEED 8: 0x08, # CONSTANT_FLOW 25: 0x19, # DHW_ON_OFF_CONTROL diff --git a/src/alpha_hwr/services/device_info.py b/src/alpha_hwr/services/device_info.py index 36a1e8c..a6af55a 100644 --- a/src/alpha_hwr/services/device_info.py +++ b/src/alpha_hwr/services/device_info.py @@ -61,6 +61,7 @@ class DeviceInfoService { import logging from typing import TYPE_CHECKING, Optional, Any +from ..constants import ERROR_CODES from ..models import DeviceInfo, Statistics, AlarmInfo from .base import BaseService @@ -407,9 +408,28 @@ async def read_alarms(self) -> Optional[AlarmInfo]: self._parse_uint16_array(warning_data) if warning_data else [] ) + # Build descriptions from code lookup + alarm_desc = ( + ", ".join( + ERROR_CODES.get(c, f"Unknown ({c})") for c in active_alarms + ) + if active_alarms + else None + ) + warning_desc = ( + ", ".join( + ERROR_CODES.get(c, f"Unknown ({c})") + for c in active_warnings + ) + if active_warnings + else None + ) + return AlarmInfo( active_alarms=active_alarms, active_warnings=active_warnings, + alarm_description=alarm_desc, + warning_description=warning_desc, ) except Exception as e: diff --git a/tests/unit/core/test_device_info_service.py b/tests/unit/core/test_device_info_service.py index 33cddca..0c0bfd6 100644 --- a/tests/unit/core/test_device_info_service.py +++ b/tests/unit/core/test_device_info_service.py @@ -122,3 +122,30 @@ async def test_read_alarms(device_info_service, mock_transport): assert 1 in alarms.active_alarms assert 2 in alarms.active_alarms assert 3 in alarms.active_warnings + # Codes 1 and 2 are known: "Leakage Current" and "Motor Phase Missing" + assert alarms.alarm_description == "Leakage Current, Motor Phase Missing" + # Code 3 is known: "External Alarm" + assert alarms.warning_description == "External Alarm" + + +@pytest.mark.asyncio +async def test_read_alarms_unknown_code_fallback( + device_info_service, mock_transport +): + """Test that unmapped alarm codes fall back to 'Unknown ()'.""" + # Use a code (e.g. 9999) that is not in ERROR_CODES + alarm_resp = ( + b"\x24\x0c\xe7\xf8\x0a\x03\x00\x58\x00\x00" + + b"\x27\x0f" # Code 9999 (0x270F) + + b"\xaa\xbb" + ) + warning_resp = b"\x24\x0a\xe7\xf8\x0a\x03\x00\x58\x00\x0b" + b"\xaa\xbb" + + mock_transport.query.side_effect = [alarm_resp, warning_resp] + + alarms = await device_info_service.read_alarms() + + assert alarms is not None + assert 9999 in alarms.active_alarms + assert alarms.alarm_description == "Unknown (9999)" + assert alarms.warning_description is None