From e2811098325db2610a950aeb9f287513463fa40f Mon Sep 17 00:00:00 2001 From: Raj Laud Date: Tue, 27 Jan 2026 15:33:10 -0700 Subject: [PATCH 1/2] Handle unknown OffReason and AlarmReason values gracefully Add parse_enum helper function and UNKNOWN members to OffReason and AlarmReason enums. These were changed from Flag to Enum in v0.9.3, which means unknown values now raise ValueError instead of being representable as flag combinations. Changes: - Added parse_enum(enum_class, value) helper in base.py - Added UNKNOWN member to OffReason and AlarmReason - Updated parsing to use parse_enum for these enums Affected devices: - DcDcConverter, SmartBatteryProtect, OrionXS (OffReason) - BatteryMonitor, SmartBatteryProtect, DcEnergyMeter (AlarmReason) --- victron_ble/devices/base.py | 18 +++++++++++++++++- victron_ble/devices/battery_monitor.py | 3 ++- victron_ble/devices/dc_energy_meter.py | 4 +++- victron_ble/devices/dcdc_converter.py | 3 ++- victron_ble/devices/orion_xs.py | 3 ++- victron_ble/devices/smart_battery_protect.py | 7 ++++--- 6 files changed, 30 insertions(+), 8 deletions(-) diff --git a/victron_ble/devices/base.py b/victron_ble/devices/base.py index 1b58e2e..79602d9 100644 --- a/victron_ble/devices/base.py +++ b/victron_ble/devices/base.py @@ -2,7 +2,7 @@ import struct from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, Type +from typing import Any, Dict, Optional, Type, TypeVar from Crypto.Cipher import AES from Crypto.Util import Counter @@ -11,6 +11,20 @@ from victron_ble.exceptions import AdvertisementKeyMismatchError +E = TypeVar("E", bound=Enum) + + +def parse_enum(enum_class: Type[E], value: int) -> E: + """ + Safely convert an int to an enum, returning the UNKNOWN member if the value is not valid. + The enum_class must have an UNKNOWN member defined. + """ + try: + return enum_class(value) + except ValueError: + return enum_class.UNKNOWN + + # Sourced from VE.Direct docs class OperationMode(Enum): OFF = 0 @@ -171,6 +185,7 @@ class ChargerError(Enum): class OffReason(Enum): + UNKNOWN = -1 NO_REASON = 0x00000000 NO_INPUT_POWER = 0x00000001 SWITCHED_OFF_SWITCH = 0x00000002 @@ -186,6 +201,7 @@ class OffReason(Enum): class AlarmReason(Enum): + UNKNOWN = -1 NO_ALARM = 0 LOW_VOLTAGE = 1 HIGH_VOLTAGE = 2 diff --git a/victron_ble/devices/battery_monitor.py b/victron_ble/devices/battery_monitor.py index 6c34ea6..f91cd08 100644 --- a/victron_ble/devices/battery_monitor.py +++ b/victron_ble/devices/battery_monitor.py @@ -7,6 +7,7 @@ Device, DeviceData, kelvin_to_celsius, + parse_enum, ) @@ -107,7 +108,7 @@ def parse_decrypted(self, decrypted: bytes) -> dict: parsed = { "remaining_mins": remaining_mins if remaining_mins != 0xFFFF else None, "voltage": voltage / 100 if voltage != 0x7FFF else None, - "alarm": AlarmReason(alarm), + "alarm": parse_enum(AlarmReason, alarm), "aux_mode": AuxMode(aux_mode), "current": current / 1000 if current != 0x3FFFFF else None, "consumed_ah": -consumed_ah / 10 if consumed_ah != 0xFFFFF else None, diff --git a/victron_ble/devices/dc_energy_meter.py b/victron_ble/devices/dc_energy_meter.py index abd2c09..24bb5b1 100644 --- a/victron_ble/devices/dc_energy_meter.py +++ b/victron_ble/devices/dc_energy_meter.py @@ -7,6 +7,7 @@ Device, DeviceData, kelvin_to_celsius, + parse_enum, ) from victron_ble.devices.battery_monitor import AuxMode @@ -54,7 +55,8 @@ def get_alarm(self) -> Optional[AlarmReason]: """ Return an enum indicating the current alarm reason or None otherwise """ - return AlarmReason(self._data["alarm"]) if self._data["alarm"] > 0 else None + alarm = self._data["alarm"] + return parse_enum(AlarmReason, alarm) if alarm > 0 else None def get_aux_mode(self) -> AuxMode: """ diff --git a/victron_ble/devices/dcdc_converter.py b/victron_ble/devices/dcdc_converter.py index 82d9db8..a79c251 100644 --- a/victron_ble/devices/dcdc_converter.py +++ b/victron_ble/devices/dcdc_converter.py @@ -7,6 +7,7 @@ DeviceData, OffReason, OperationMode, + parse_enum, ) @@ -73,5 +74,5 @@ def parse_decrypted(self, decrypted: bytes) -> dict: "output_voltage": ( output_voltage / 100 if output_voltage != 0x7FFF else None ), - "off_reason": OffReason(off_reason), + "off_reason": parse_enum(OffReason, off_reason), } diff --git a/victron_ble/devices/orion_xs.py b/victron_ble/devices/orion_xs.py index 0e12e1a..dd70841 100644 --- a/victron_ble/devices/orion_xs.py +++ b/victron_ble/devices/orion_xs.py @@ -7,6 +7,7 @@ DeviceData, OffReason, OperationMode, + parse_enum, ) @@ -94,5 +95,5 @@ def parse_decrypted(self, decrypted: bytes) -> dict: "output_current": output_current / 10 if output_current != 0xFFFF else None, "input_voltage": input_voltage / 100 if input_voltage != 0xFFFF else None, "input_current": input_current / 10 if input_current != 0xFFFF else None, - "off_reason": OffReason(off_reason), + "off_reason": parse_enum(OffReason, off_reason), } diff --git a/victron_ble/devices/smart_battery_protect.py b/victron_ble/devices/smart_battery_protect.py index 1b8f2e6..3079603 100644 --- a/victron_ble/devices/smart_battery_protect.py +++ b/victron_ble/devices/smart_battery_protect.py @@ -9,6 +9,7 @@ DeviceData, OffReason, OperationMode, + parse_enum, ) @@ -91,11 +92,11 @@ def parse_decrypted(self, decrypted: bytes) -> dict: OutputState(output_state) if output_state != 0xFF else None ), "error_code": (ChargerError(error_code) if error_code != 0xFF else None), - "alarm_reason": AlarmReason(alarm_reason), - "warning_reason": AlarmReason(warning_reason), + "alarm_reason": parse_enum(AlarmReason, alarm_reason), + "warning_reason": parse_enum(AlarmReason, warning_reason), "input_voltage": (input_voltage / 100 if input_voltage != 0x7FFF else None), "output_voltage": ( output_voltage / 100 if output_voltage != 0xFFFF else None ), - "off_reason": OffReason(off_reason), + "off_reason": parse_enum(OffReason, off_reason), } From dce76184596ac7acbf03f7acb1ada915a18415d9 Mon Sep 17 00:00:00 2001 From: Raj Laud Date: Tue, 27 Jan 2026 16:45:55 -0700 Subject: [PATCH 2/2] Fix linter issues --- victron_ble/devices/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/victron_ble/devices/base.py b/victron_ble/devices/base.py index 79602d9..a6dec6f 100644 --- a/victron_ble/devices/base.py +++ b/victron_ble/devices/base.py @@ -2,7 +2,7 @@ import struct from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, Optional, Type, TypeVar +from typing import Any, Dict, Type, TypeVar from Crypto.Cipher import AES from Crypto.Util import Counter @@ -10,7 +10,6 @@ from victron_ble.exceptions import AdvertisementKeyMismatchError - E = TypeVar("E", bound=Enum) @@ -22,7 +21,7 @@ def parse_enum(enum_class: Type[E], value: int) -> E: try: return enum_class(value) except ValueError: - return enum_class.UNKNOWN + return getattr(enum_class, "UNKNOWN") # Sourced from VE.Direct docs