diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index c01dc771f3e..8f027aee033 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -1,4 +1,6 @@ """Code to handle a Hue bridge.""" +from __future__ import annotations + import asyncio from functools import partial import logging @@ -241,7 +243,8 @@ class HueBridge: key = (updated_object.ITEM_TYPE, updated_object.id) if key in self._update_callbacks: - self._update_callbacks[key]() + for callback in self._update_callbacks[key]: + callback() except GeneratorExit: pass @@ -249,18 +252,20 @@ class HueBridge: @core.callback def listen_updates(self, item_type, item_id, update_callback): """Listen to updates.""" - callbacks = self._update_callbacks key = (item_type, item_id) + callbacks: list[core.CALLBACK_TYPE] | None = self._update_callbacks.get(key) - if key in callbacks: - _LOGGER.warning("Overwriting update callback for %s", key) + if callbacks is None: + callbacks = self._update_callbacks[key] = [] - callbacks[key] = update_callback + callbacks.append(update_callback) @core.callback def unsub(): - if callbacks.get(key) == update_callback: - callbacks.pop(key) + try: + callbacks.remove(update_callback) + except ValueError: + pass return unsub diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py index f2edc129f10..7c0163f8a16 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -5,7 +5,7 @@ from aiohue.sensors import TYPE_ZGP_SWITCH, TYPE_ZLL_ROTARY, TYPE_ZLL_SWITCH from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback -from homeassistant.util import slugify +from homeassistant.util import dt as dt_util, slugify from .sensor_device import GenericHueDevice @@ -48,7 +48,13 @@ class HueEvent(GenericHueDevice): @callback def async_update_callback(self): """Fire the event if reason is that state is updated.""" - if self.sensor.state == self._last_state: + if ( + self.sensor.state == self._last_state + or + # Filter out old states. Can happen when events fire while refreshing + dt_util.parse_datetime(self.sensor.state["lastupdated"]) + <= dt_util.parse_datetime(self._last_state["lastupdated"]) + ): return # Extract the press code as state diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 18c8444ce65..345156de7d7 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -306,7 +306,11 @@ class HueLight(CoordinatorEntity, LightEntity): @property def unique_id(self): """Return the unique ID of this Hue light.""" - return self.light.uniqueid + unique_id = self.light.uniqueid + if not unique_id and self.is_group and self.light.room: + unique_id = self.light.room["id"] + + return unique_id @property def device_id(self): diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 2a46da9c52b..b61635cb408 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==2.4.2"], + "requirements": ["aiohue==2.5.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3f7529169df..5b84ddc46df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.4.2 +aiohue==2.5.0 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6244443a9a..15a8c0af298 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.4.2 +aiohue==2.5.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index ed31f00d9cc..3aecacac58d 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -81,10 +81,10 @@ def create_mock_api(hass): logger = logging.getLogger(__name__) api.config.apiversion = "9.9.9" - api.lights = Lights(logger, {}, mock_request) - api.groups = Groups(logger, {}, mock_request) - api.sensors = Sensors(logger, {}, mock_request) - api.scenes = Scenes(logger, {}, mock_request) + api.lights = Lights(logger, {}, [], mock_request) + api.groups = Groups(logger, {}, [], mock_request) + api.sensors = Sensors(logger, {}, [], mock_request) + api.scenes = Scenes(logger, {}, [], mock_request) return api diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index ee980c6bffe..eb5c93862fe 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -351,12 +351,11 @@ async def test_event_updates(hass, caplog): unsub = hue_bridge.listen_updates("lights", "2", obj_updated) unsub_false = hue_bridge.listen_updates("lights", "2", obj_updated_false) - assert "Overwriting update callback" in caplog.text - events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) await wait_empty_queue() - assert len(calls) == 2 + assert len(calls) == 3 + assert calls[-2] is True assert calls[-1] is False # Also call multiple times to make sure that works. @@ -368,7 +367,7 @@ async def test_event_updates(hass, caplog): events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) await wait_empty_queue() - assert len(calls) == 2 + assert len(calls) == 3 events.put_nowait(None) await subscription_task diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 105fd2a7271..5efb74d015f 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -269,6 +269,10 @@ async def test_groups(hass, mock_bridge): mock_bridge.allow_groups = True mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge.api.groups._v2_resources = [ + {"id_v1": "/groups/1", "id": "group-1-mock-id", "type": "room"}, + {"id_v1": "/groups/2", "id": "group-2-mock-id", "type": "room"}, + ] await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 2 @@ -285,6 +289,10 @@ async def test_groups(hass, mock_bridge): assert lamp_2 is not None assert lamp_2.state == "on" + ent_reg = er.async_get(hass) + assert ent_reg.async_get("light.group_1").unique_id == "group-1-mock-id" + assert ent_reg.async_get("light.group_2").unique_id == "group-2-mock-id" + async def test_new_group_discovered(hass, mock_bridge): """Test if 2nd update has a new group.""" diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index eb7ece241c3..46237c510f7 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -8,6 +8,8 @@ from homeassistant.components.hue.hue_event import CONF_HUE_EVENT from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge +from tests.common import async_capture_events + PRESENCE_SENSOR_1_PRESENT = { "state": {"presence": True, "lastupdated": "2019-01-01T01:00:00"}, "swupdate": {"state": "noupdates", "lastinstall": "2019-01-01T00:00:00"}, @@ -435,18 +437,17 @@ async def test_hue_events(hass, mock_bridge): """Test that hue remotes fire events when pressed.""" mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_HUE_EVENT, mock_listener) + events = async_capture_events(hass, CONF_HUE_EVENT) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 0 + assert len(events) == 0 new_sensor_response = dict(SENSOR_RESPONSE) new_sensor_response["7"]["state"] = { "buttonevent": 18, - "lastupdated": "2019-12-28T22:58:02", + "lastupdated": "2019-12-28T22:58:03", } mock_bridge.mock_sensor_responses.append(new_sensor_response) @@ -456,18 +457,18 @@ async def test_hue_events(hass, mock_bridge): assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { + assert len(events) == 1 + assert events[-1].data == { "id": "hue_tap", "unique_id": "00:00:00:00:00:44:23:08-f2", "event": 18, - "last_updated": "2019-12-28T22:58:02", + "last_updated": "2019-12-28T22:58:03", } new_sensor_response = dict(new_sensor_response) new_sensor_response["8"]["state"] = { "buttonevent": 3002, - "lastupdated": "2019-12-28T22:58:01", + "lastupdated": "2019-12-28T22:58:03", } mock_bridge.mock_sensor_responses.append(new_sensor_response) @@ -477,14 +478,30 @@ async def test_hue_events(hass, mock_bridge): assert len(mock_bridge.mock_requests) == 3 assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 2 - assert mock_listener.mock_calls[1][1][0].data == { + assert len(events) == 2 + assert events[-1].data == { "id": "hue_dimmer_switch_1", "unique_id": "00:17:88:01:10:3e:3a:dc-02-fc00", "event": 3002, - "last_updated": "2019-12-28T22:58:01", + "last_updated": "2019-12-28T22:58:03", } + # Fire old event, it should be ignored + new_sensor_response = dict(new_sensor_response) + new_sensor_response["8"]["state"] = { + "buttonevent": 18, + "lastupdated": "2019-12-28T22:58:02", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 4 + assert len(hass.states.async_all()) == 7 + assert len(events) == 2 + # Add a new remote. In discovery the new event is registered **but not fired** new_sensor_response = dict(new_sensor_response) new_sensor_response["21"] = { @@ -524,9 +541,9 @@ async def test_hue_events(hass, mock_bridge): await mock_bridge.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge.mock_requests) == 5 assert len(hass.states.async_all()) == 8 - assert len(mock_listener.mock_calls) == 2 + assert len(events) == 2 # A new press fires the event new_sensor_response["21"]["state"]["lastupdated"] = "2020-01-31T15:57:19" @@ -536,14 +553,12 @@ async def test_hue_events(hass, mock_bridge): await mock_bridge.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 5 + assert len(mock_bridge.mock_requests) == 6 assert len(hass.states.async_all()) == 8 - assert len(mock_listener.mock_calls) == 3 - assert mock_listener.mock_calls[2][1][0].data == { + assert len(events) == 3 + assert events[-1].data == { "id": "lutron_aurora_1", "unique_id": "ff:ff:00:0f:e7:fd:bc:b7-01-fc00-0014", "event": 2, "last_updated": "2020-01-31T15:57:19", } - - unsub()