Summary
SkyKettle supports multiple config entries: every configured device receives its own entry and its own connection object.
However, version v2.8 stores some device-specific runtime state globally under hass.data[DOMAIN]. This can cause:
- entities from different config entries to be associated with the wrong Home Assistant device;
- reload or unload of one entry to stop polling for another entry;
- repeated setup to discard an entry’s existing runtime dictionary.
Current config entry structure
The connection itself is correctly stored per entry:
hass.data[DOMAIN][entry.entry_id][DATA_CONNECTION] = kettle
Entity unique IDs also generally include entry.entry_id.
However, setup initializes the entry dictionary using:
if entry.entry_id not in hass.data:
hass.data[DOMAIN][entry.entry_id] = {}
This checks the top level of hass.data, not hass.data[DOMAIN].
Because a config entry ID is normally not a top-level Home Assistant data key, this condition is almost always true. The entry dictionary is therefore recreated during setup instead of being retrieved or initialized inside the integration domain.
During a normal first setup this may appear harmless, but repeated or re-entrant setup can discard runtime data already stored for that entry. It also makes the intended per-entry isolation incomplete and non-idempotent.
Device registry metadata can belong to the wrong entry
The DeviceInfo provider is stored globally:
hass.data[DOMAIN][DATA_DEVICE_INFO] = lambda: device_info(entry)
Entities on every platform read it from the same global location:
return self.hass.data[DOMAIN][DATA_DEVICE_INFO]()
Every new config entry overwrites this provider with a closure referencing the most recently configured entry.
An entity can therefore use its own correct connection:
hass.data[DOMAIN][entry.entry_id][DATA_CONNECTION]
while returning DeviceInfo for another config entry.
Because device_info(entry) uses that entry’s MAC address as its device identifier and connection, Home Assistant may attach the entity to the wrong device registry entry. Depending on setup, restart, or reload ordering, entities from multiple physical devices may be grouped under the same Home Assistant device.
Polling lifecycle is shared globally
Each config entry creates its own polling closure and captures its own connection, but polling control is global:
hass.data[DOMAIN][DATA_WORKING] = True
hass.data[DOMAIN][DATA_CANCEL] = scheduled_callback
Every entry overwrites the same cancel callback.
During unload, one entry performs operations equivalent to:
hass.data[DOMAIN][DATA_WORKING] = False
hass.data[DOMAIN][DATA_CANCEL]()
Consequently, unloading or reloading one entry can:
- cancel a callback belonging to another entry;
- set the shared working flag to
False;
- cause polling loops for other devices to stop scheduling updates.
The connections remain separate, but their lifecycle controllers do not.
Expected behavior
Each config entry should own all runtime state associated with its physical device:
- connection;
DeviceInfo;
- polling active state;
- scheduled polling callback;
- unload and cleanup state.
Reloading one device must not affect polling or device-registry association for another device.
Suggested implementation
Initialize runtime storage inside the integration domain:
domain_data = hass.data.setdefault(DOMAIN, {})
entry_data = domain_data.setdefault(entry.entry_id, {})
Store all device-specific data in entry_data:
entry_data[DATA_CONNECTION] = kettle
entry_data[DATA_DEVICE_INFO] = lambda: device_info(entry)
entry_data[DATA_WORKING] = True
entry_data[DATA_CANCEL] = scheduled_callback
Entities should resolve metadata through their own entry:
@property
def device_info(self):
return self.hass.data[DOMAIN][self.entry.entry_id][DATA_DEVICE_INFO]()
The polling closure should read only its own entry state:
if entry_data[DATA_WORKING]:
schedule_poll(...)
Unload should stop and remove only the selected entry:
entry_data[DATA_WORKING] = False
cancel = entry_data.pop(DATA_CANCEL, None)
if cancel is not None:
cancel()
await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await entry_data[DATA_CONNECTION].stop()
hass.data[DOMAIN].pop(entry.entry_id, None)
The domain dictionary should only be removed after its final config entry has been unloaded.
Dispatcher updates could also be scoped by entry ID to avoid refreshing every SkyKettle entity whenever any configured device is polled, although correct per-entry connection and metadata access is sufficient to prevent state mixing.
Reproduction
- Add two different supported SkyKettle devices.
- Restart Home Assistant or reload both entries.
- Verify that every entity remains attached to the correct Home Assistant device.
- Reload or unload one entry.
- Verify that the other device continues polling and updating independently.
The device-registry problem may depend on setup order. The polling conflict follows directly from the globally shared working flag and cancel callback.
Summary
SkyKettle supports multiple config entries: every configured device receives its own entry and its own connection object.
However, version
v2.8stores some device-specific runtime state globally underhass.data[DOMAIN]. This can cause:Current config entry structure
The connection itself is correctly stored per entry:
Entity unique IDs also generally include
entry.entry_id.However, setup initializes the entry dictionary using:
This checks the top level of
hass.data, nothass.data[DOMAIN].Because a config entry ID is normally not a top-level Home Assistant data key, this condition is almost always true. The entry dictionary is therefore recreated during setup instead of being retrieved or initialized inside the integration domain.
During a normal first setup this may appear harmless, but repeated or re-entrant setup can discard runtime data already stored for that entry. It also makes the intended per-entry isolation incomplete and non-idempotent.
Device registry metadata can belong to the wrong entry
The
DeviceInfoprovider is stored globally:Entities on every platform read it from the same global location:
Every new config entry overwrites this provider with a closure referencing the most recently configured entry.
An entity can therefore use its own correct connection:
while returning
DeviceInfofor another config entry.Because
device_info(entry)uses that entry’s MAC address as its device identifier and connection, Home Assistant may attach the entity to the wrong device registry entry. Depending on setup, restart, or reload ordering, entities from multiple physical devices may be grouped under the same Home Assistant device.Polling lifecycle is shared globally
Each config entry creates its own polling closure and captures its own connection, but polling control is global:
Every entry overwrites the same cancel callback.
During unload, one entry performs operations equivalent to:
Consequently, unloading or reloading one entry can:
False;The connections remain separate, but their lifecycle controllers do not.
Expected behavior
Each config entry should own all runtime state associated with its physical device:
DeviceInfo;Reloading one device must not affect polling or device-registry association for another device.
Suggested implementation
Initialize runtime storage inside the integration domain:
Store all device-specific data in
entry_data:Entities should resolve metadata through their own entry:
The polling closure should read only its own entry state:
Unload should stop and remove only the selected entry:
The domain dictionary should only be removed after its final config entry has been unloaded.
Dispatcher updates could also be scoped by entry ID to avoid refreshing every SkyKettle entity whenever any configured device is polled, although correct per-entry connection and metadata access is sufficient to prevent state mixing.
Reproduction
The device-registry problem may depend on setup order. The polling conflict follows directly from the globally shared working flag and cancel callback.