diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index f5bdce6652ce..f389bf56d762 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -165,6 +165,15 @@ class PassiveBluetoothDataUpdate[_T]: if current.get(key, UNDEFINED) != data: changed_entity_keys.add(key) current[key] = data # type: ignore[assignment] + # A key present in the update without a name means the integration + # does not name that entity; clear any stale name restored from storage. + for key in new_data.entity_data.keys() | new_data.entity_descriptions.keys(): + if ( + key not in new_data.entity_names + and self.entity_names.get(key) is not None + ): + changed_entity_keys.add(key) + self.entity_names[key] = None # If the device changed we don't need to return the changed # entity keys as all entities will be updated return None if device_change else changed_entity_keys diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 1ca85367a364..6305a690618b 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -1982,3 +1982,50 @@ def test_deserialize_entity_description( """Test deserializing an entity description.""" description = deserialize_entity_description(description_type, description_dict) assert description == expected_description + + +def test_update_clears_names_missing_from_the_update() -> None: + """Test stored entity names are cleared when an update stops providing them. + + Names restored from storage must not stick around once the integration + stops naming its entities, else entities keep stale names forever. + """ + temperature_key = PassiveBluetoothEntityKey("temperature", None) + pressure_key = PassiveBluetoothEntityKey("pressure", None) + temperature_description = SensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ) + data = PassiveBluetoothDataUpdate( + devices={None: DeviceInfo(name="Test Device")}, + entity_descriptions={temperature_key: temperature_description}, + entity_names={temperature_key: "Temperature", pressure_key: "Pressure"}, + entity_data={temperature_key: 14.5, pressure_key: 1234}, + ) + update_without_names = PassiveBluetoothDataUpdate( + devices={None: DeviceInfo(name="Test Device")}, + entity_descriptions={temperature_key: temperature_description}, + entity_names={}, + entity_data={temperature_key: 15.5}, + ) + + # The pressure name is untouched since the update does not include the key + assert data.update(update_without_names) == {temperature_key} + assert data.entity_names == {temperature_key: None, pressure_key: "Pressure"} + + # A repeated update reports no changes + assert data.update(update_without_names) == set() + + update_with_name = PassiveBluetoothDataUpdate( + devices={None: DeviceInfo(name="Test Device")}, + entity_descriptions={temperature_key: temperature_description}, + entity_names={temperature_key: "Custom name"}, + entity_data={temperature_key: 15.5}, + ) + + assert data.update(update_with_name) == {temperature_key} + assert data.entity_names == { + temperature_key: "Custom name", + pressure_key: "Pressure", + }