Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.4.0
current_version = 0.5.0
commit = True
tag = True
tag_name = v{new_version}
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<code>)"`.

### 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 (<code>)"` fallback for codes not present in `ERROR_CODES`.

## [0.4.0] - 2026-02-14

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion src/alpha_hwr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
49 changes: 33 additions & 16 deletions src/alpha_hwr/cli/output/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rich.table import Table
from rich.panel import Panel

from ...constants import ERROR_CODES
from ...models import (
TelemetryData,
DeviceInfo,
Expand Down Expand Up @@ -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)


Expand Down
1 change: 1 addition & 0 deletions src/alpha_hwr/services/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions src/alpha_hwr/services/device_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Comment thread
eman marked this conversation as resolved.
)

except Exception as e:
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/core/test_device_info_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<code>)'."""
# 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
Loading