Fix Victron BLE false reauth triggered by unknown enum bitmask combinations (#167809)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Raj Laud
2026-04-09 14:53:26 -04:00
committed by Franck Nijhof
parent 83da18b761
commit 818bde1d5e
3 changed files with 80 additions and 13 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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"),