diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 2bd034318059..8be654a651ef 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -572,7 +572,8 @@ class RuntimeEntryData: for state_type, states in self.state.items() if states and state_type in STATE_TYPE_TO_COMPONENT_TYPE } - if store_data == self._storage_contents: + # A pending save may still overwrite the store with stale data. + if self._pending_storage is None and store_data == self._storage_contents: return def _memorized_storage() -> StoreData: diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 8926d35b7016..a0a8fdea1ac1 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -802,9 +802,15 @@ class ESPHomeManager: # if it's a broken connection or Z-Wave controller or a not # yet provisioned controller. zwave_home_id: int = UNPACK_UINT32_BE(request.data[0:4])[0] - assert self.entry_data.device_info is not None - self.entry_data.async_create_zwave_js_flow( - self.hass, self.entry_data.device_info, zwave_home_id + entry_data = self.entry_data + assert entry_data.device_info is not None + # DeviceInfo is a snapshot from connect time; keep its home ID current. + entry_data.device_info = EsphomeDeviceInfo.from_dict( + {**entry_data.device_info.to_dict(), "zwave_home_id": zwave_home_id} + ) + entry_data.async_save_to_store() + entry_data.async_create_zwave_js_flow( + self.hass, entry_data.device_info, zwave_home_id ) async def on_disconnect(self, expected_disconnect: bool) -> None: diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 00ec37186c78..5ec39aa80fb4 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -3991,6 +3991,7 @@ def test_zero_noise_psk_is_not_the_probe_key() -> None: async def test_zwave_proxy_request_home_id_change( hass: HomeAssistant, mock_client: APIClient, + hass_storage: dict[str, Any], mock_esphome_device: MockESPHomeDeviceType, ) -> None: """Test Z-Wave proxy request handler with HOME_ID_CHANGE request.""" @@ -4077,6 +4078,54 @@ async def test_zwave_proxy_request_home_id_change( # The noise PSK is taken from the config entry, not the live client assert call_args[0][3].noise_psk == noise_psk + assert entry.runtime_data.device_info.zwave_home_id == zwave_home_id + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + data = hass_storage[f"{DOMAIN}.{entry.entry_id}"]["data"] + assert data["device_info"]["zwave_home_id"] == zwave_home_id + + +async def test_zwave_home_id_change_saved_after_reconnect( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], + mock_esphome_device: MockESPHomeDeviceType, + freezer: FrozenDateTimeFactory, +) -> None: + """Test a home ID change replaces the pending connect-time save.""" + device = await mock_esphome_device( + mock_client=mock_client, + device_info={"zwave_proxy_feature_flags": 1}, + ) + storage_key = f"{DOMAIN}.{device.entry.entry_id}" + zwave_home_id = 3551671779 + + async def report_home_id() -> None: + callback = mock_client.subscribe_zwave_proxy_request.call_args[0][0] + callback( + ZWaveProxyRequest( + type=ZWaveProxyRequestType.HOME_ID_CHANGE, + data=zwave_home_id.to_bytes(4, byteorder="big"), + ) + ) + freezer.tick(SAVE_DELAY + 1) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + with patch("homeassistant.helpers.discovery_flow.async_create_flow"): + await report_home_id() + stored = hass_storage[storage_key]["data"] + assert stored["device_info"]["zwave_home_id"] == zwave_home_id + + # The device reconnects with home ID 0, so the connect-time save holds 0 + await device.mock_disconnect(expected_disconnect=False) + await device.mock_connect() + await report_home_id() + + # Equal to the store, so only replacing the pending save can write it + assert hass_storage[storage_key]["data"] == stored + async def test_no_zwave_proxy_subscribe_without_feature_flags( hass: HomeAssistant,