diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 0a7278cb5d5..ad3d1ff18ad 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -398,7 +398,8 @@ class ZHADevice(LogMixin): @callback def async_update_last_seen(self, last_seen): """Set last seen on the zigpy device.""" - self._zigpy_device.last_seen = last_seen + if self._zigpy_device.last_seen is None and last_seen is not None: + self._zigpy_device.last_seen = last_seen @callback def async_get_info(self): diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 19a83c3b6bc..90ec0e6e250 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -198,7 +198,7 @@ class GroupProbe: entity_class, ( group.get_domain_entity_ids(domain), - f"{domain}_group_{group.group_id}", + f"{domain}_zha_group_0x{group.group_id:04x}", group.group_id, zha_gateway.coordinator_zha_device, ), diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index fcc8a52360b..14a6a5c839e 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -258,7 +258,7 @@ class ZHAGateway: zha_group.info("group_member_removed - endpoint: %s", endpoint) self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_REMOVED) async_dispatcher_send( - self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{zigpy_group.group_id}" + self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}" ) def group_member_added( @@ -270,7 +270,7 @@ class ZHAGateway: zha_group.info("group_member_added - endpoint: %s", endpoint) self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_ADDED) async_dispatcher_send( - self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{zigpy_group.group_id}" + self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}" ) def group_added(self, zigpy_group: ZigpyGroupType) -> None: @@ -286,7 +286,7 @@ class ZHAGateway: zha_group = self._groups.pop(zigpy_group.group_id, None) zha_group.info("group_removed") async_dispatcher_send( - self._hass, f"{SIGNAL_REMOVE_GROUP}_{zigpy_group.group_id}" + self._hass, f"{SIGNAL_REMOVE_GROUP}_0x{zigpy_group.group_id:04x}" ) def _send_group_gateway_message( diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 00a4942c7b7..3838e9b6a50 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -78,6 +78,9 @@ class ZhaStorage: ieee_str: str = str(device.ieee) old = self.devices[ieee_str] + if old is not None and device.last_seen is None: + return + changes = {} changes["last_seen"] = device.last_seen diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index fda26f54d58..2d098d60bfb 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -226,7 +226,9 @@ class ZhaGroupEntity(BaseZhaEntity): ) -> None: """Initialize a light group.""" super().__init__(unique_id, zha_device, **kwargs) - self._name = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" + self._name = ( + f"{zha_device.gateway.groups.get(group_id).name}_zha_group_0x{group_id:04x}" + ) self._group_id: int = group_id self._entity_ids: List[str] = entity_ids self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None @@ -236,30 +238,30 @@ class ZhaGroupEntity(BaseZhaEntity): await super().async_added_to_hass() await self.async_accept_signal( None, - f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", + f"{SIGNAL_REMOVE_GROUP}_0x{self._group_id:04x}", self.async_remove, signal_override=True, ) await self.async_accept_signal( None, - f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{self._group_id}", + f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{self._group_id:04x}", self._update_group_entities, signal_override=True, ) - @callback - def async_state_changed_listener( - entity_id: str, old_state: State, new_state: State - ): - """Handle child updates.""" - self.async_schedule_update_ha_state(True) - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, async_state_changed_listener + self.hass, self._entity_ids, self.async_state_changed_listener ) await self.async_update() + @callback + def async_state_changed_listener( + self, entity_id: str, old_state: State, new_state: State + ): + """Handle child updates.""" + self.async_schedule_update_ha_state(True) + def _update_group_entities(self): """Update tracked entities when membership changes.""" group = self.zha_device.gateway.get_group(self._group_id) @@ -267,15 +269,8 @@ class ZhaGroupEntity(BaseZhaEntity): if self._async_unsub_state_changed is not None: self._async_unsub_state_changed() - @callback - def async_state_changed_listener( - entity_id: str, old_state: State, new_state: State - ): - """Handle child updates.""" - self.async_schedule_update_ha_state(True) - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, async_state_changed_listener + self.hass, self._entity_ids, self.async_state_changed_listener ) async def async_will_remove_from_hass(self) -> None: diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 9c57b57419a..2f0966ae739 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -159,9 +159,7 @@ async def find_entity_id(domain, zha_device, hass): def async_find_group_entity_id(hass, domain, group): """Find the group entity id under test.""" - entity_id = ( - f"{domain}.{group.name.lower().replace(' ','_')}_group_0x{group.group_id:04x}" - ) + entity_id = f"{domain}.{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}" entity_ids = hass.states.async_entity_ids(domain) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 80d96fa55bd..c5ae9142ff0 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,5 +1,6 @@ """Test ZHA Gateway.""" import logging +import time import pytest import zigpy.profiles.zha as zha @@ -167,3 +168,40 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord # the group entity should not have been cleaned up assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) + + +async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): + """Test saving data after a delay.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + await async_enable_traffic(hass, [zha_dev_basic]) + + assert zha_dev_basic.last_seen is not None + entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) + assert entry.last_seen == zha_dev_basic.last_seen + + assert zha_dev_basic.last_seen is not None + last_seen = zha_dev_basic.last_seen + + # test that we can't set None as last seen any more + zha_dev_basic.async_update_last_seen(None) + assert last_seen == zha_dev_basic.last_seen + + # test that we won't put None in storage + zigpy_dev_basic.last_seen = None + assert zha_dev_basic.last_seen is None + await zha_gateway.async_update_device_storage() + await hass.async_block_till_done() + entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) + assert entry.last_seen == last_seen + + # test that we can still set a good last_seen + last_seen = time.time() + zha_dev_basic.async_update_last_seen(last_seen) + assert last_seen == zha_dev_basic.last_seen + + # test that we still put good values in storage + await zha_gateway.async_update_device_storage() + await hass.async_block_till_done() + entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) + assert entry.last_seen == last_seen