From 6cc05e6a280bfb7d18801d64cd2da4475701cb86 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:53:26 -0400 Subject: [PATCH] Fix Victron BLE false reauth triggered by unknown enum bitmask combinations (#167809) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/victron_ble/__init__.py | 32 ++++++++----- tests/components/victron_ble/fixtures.py | 14 ++++++ tests/components/victron_ble/test_sensor.py | 47 +++++++++++++++++++ 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/victron_ble/__init__.py b/homeassistant/components/victron_ble/__init__.py index 7eff058b7b22..7c79d7331544 100644 --- a/homeassistant/components/victron_ble/__init__.py +++ b/homeassistant/components/victron_ble/__init__.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant -from .const import REAUTH_AFTER_FAILURES +from .const import REAUTH_AFTER_FAILURES, VICTRON_IDENTIFIER _LOGGER = logging.getLogger(__name__) @@ -38,18 +38,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nonlocal consecutive_failures update = data.update(service_info) - # If the device type was recognized (devices dict populated) but - # only signal strength came back, decryption likely failed. - # Unsupported devices have an empty devices dict and won't trigger this. - if update.devices and len(update.entity_values) <= 1: - consecutive_failures += 1 - if consecutive_failures >= REAUTH_AFTER_FAILURES: - _LOGGER.debug( - "Triggering reauth for %s after %d consecutive failures", - address, - consecutive_failures, - ) - entry.async_start_reauth(hass) + # Only consider a reauth when the device type is recognised (devices + # populated) but the advertisement key fails the quick-check built into + # validate_advertisement_key. Using the key check instead of counting + # entity values avoids false positives: some devices legitimately return + # few (or zero) sensor values when in certain error or alarm states. + raw_data = service_info.manufacturer_data.get(VICTRON_IDENTIFIER) + if update.devices and raw_data is not None: + if not data.validate_advertisement_key(raw_data): + consecutive_failures += 1 + if consecutive_failures >= REAUTH_AFTER_FAILURES: + _LOGGER.debug( + "Triggering reauth for %s after %d consecutive failures", + address, + consecutive_failures, + ) + entry.async_start_reauth(hass) + consecutive_failures = 0 + else: consecutive_failures = 0 else: consecutive_failures = 0 diff --git a/tests/components/victron_ble/fixtures.py b/tests/components/victron_ble/fixtures.py index b8253c672df1..c6efeb4112b1 100644 --- a/tests/components/victron_ble/fixtures.py +++ b/tests/components/victron_ble/fixtures.py @@ -187,6 +187,20 @@ VICTRON_VEBUS_BAD_KEY_SERVICE_INFO = BluetoothServiceInfo( source="local", ) +# Same DC/DC converter but with OffReason=0x81 (NO_INPUT_POWER|ENGINE_SHUTDOWN), +# a real bitmask combination that the current OffReason enum doesn't handle. +# The key check byte is valid so validate_advertisement_key passes, but +# parsing raises ValueError → sparse update (signal strength only). +VICTRON_DC_DC_CONVERTER_UNKNOWN_OFF_REASON_SERVICE_INFO = BluetoothServiceInfo( + name="DC/DC Converter", + address="01:02:03:04:05:08", + rssi=-60, + manufacturer_data={0x02E1: bytes.fromhex("1000c0a304121d64ca8d442b90bbde6a8cba")}, + service_data={}, + service_uuids=[], + source="local", +) + VICTRON_VEBUS_SENSORS = { "inverter_charger_device_state": "float", "inverter_charger_battery_voltage": "14.45", diff --git a/tests/components/victron_ble/test_sensor.py b/tests/components/victron_ble/test_sensor.py index a44dabb969b9..ad8565cbe07c 100644 --- a/tests/components/victron_ble/test_sensor.py +++ b/tests/components/victron_ble/test_sensor.py @@ -24,6 +24,7 @@ from .fixtures import ( VICTRON_BATTERY_SENSE_TOKEN, VICTRON_DC_DC_CONVERTER_SERVICE_INFO, VICTRON_DC_DC_CONVERTER_TOKEN, + VICTRON_DC_DC_CONVERTER_UNKNOWN_OFF_REASON_SERVICE_INFO, VICTRON_DC_ENERGY_METER_SERVICE_INFO, VICTRON_DC_ENERGY_METER_TOKEN, VICTRON_SMART_BATTERY_PROTECT_SERVICE_INFO, @@ -208,6 +209,52 @@ async def test_reauth_triggered_only_once( assert len(flows) == 1 +@pytest.mark.usefixtures("enable_bluetooth") +async def test_reauth_not_triggered_on_unknown_enum_value( + hass: HomeAssistant, +) -> None: + """Test reauth is NOT triggered when a valid key yields a sparse update. + + Some devices report bitmask combinations for OffReason or AlarmReason that + are not in the enum (e.g. NO_INPUT_POWER|ENGINE_SHUTDOWN = 0x81 on a DC-DC + converter that stopped due to both conditions simultaneously). The parser + raises ValueError, producing a sparse update (signal strength only). + This must not be mistaken for a wrong encryption key. + + Regression test for https://github.com/home-assistant/core/issues/167105 + """ + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "address": VICTRON_DC_DC_CONVERTER_UNKNOWN_OFF_REASON_SERVICE_INFO.address, + CONF_ACCESS_TOKEN: VICTRON_DC_DC_CONVERTER_TOKEN, + }, + unique_id=VICTRON_DC_DC_CONVERTER_UNKNOWN_OFF_REASON_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + service_info = VICTRON_DC_DC_CONVERTER_UNKNOWN_OFF_REASON_SERVICE_INFO + for idx in range(REAUTH_AFTER_FAILURES + 1): + inject_bluetooth_service_info( + hass, + BluetoothServiceInfo( + name=service_info.name, + address=service_info.address, + rssi=service_info.rssi - idx, + manufacturer_data=service_info.manufacturer_data, + service_data=service_info.service_data, + service_uuids=service_info.service_uuids, + source=service_info.source, + ), + ) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert len(flows) == 0 + + @pytest.mark.usefixtures("enable_bluetooth") @pytest.mark.parametrize( ("payload_hex", "expected_state"),