diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 3d322f54882b..84d2ba3a035a 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -146,6 +146,9 @@ "light_sensor_enabled": { "name": "Light sensor enabled" }, + "motion_aware": { + "name": "MotionAware" + }, "motion_sensor_enabled": { "name": "Motion sensor enabled" } diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index 31dc261748e6..caf4ca0008c6 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -4,7 +4,12 @@ from collections.abc import Callable from typing import Any, override from aiohue.v2 import HueBridgeV2 -from aiohue.v2.controllers.config import BehaviorInstance, BehaviorInstanceController +from aiohue.v2.controllers.config import ( + BehaviorInstance, + BehaviorInstanceController, + MotionAreaConfiguration, + MotionAreaConfigurationController, +) from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.sensors import ( LightLevel, @@ -23,9 +28,10 @@ from homeassistant.components.switch import ( from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .bridge import HueConfigEntry +from .bridge import HueBridge, HueConfigEntry from .const import DOMAIN from .v2.entity import HueBaseEntity @@ -47,17 +53,20 @@ async def async_setup_entry( def register_items( controller: BehaviorInstanceController | LightLevelController + | MotionAreaConfigurationController | MotionController, switch_class: type[ HueBehaviorInstanceEnabledEntity | HueLightSensorEnabledEntity + | HueMotionAreaConfigurationEnabledEntity | HueMotionSensorEnabledEntity ], resource_filter: Callable[[Any], bool] | None = None, ): @callback def async_add_entity( - event_type: EventType, resource: BehaviorInstance | LightLevel | Motion + event_type: EventType, + resource: BehaviorInstance | LightLevel | MotionAreaConfiguration | Motion, ) -> None: """Add entity from Hue resource.""" if resource_filter is not None and not resource_filter(resource): @@ -107,13 +116,21 @@ async def async_setup_entry( HueBehaviorInstanceEnabledEntity, is_user_automation, ) + register_items( + api.config.motion_area_configuration, HueMotionAreaConfigurationEnabledEntity + ) class HueResourceEnabledEntity(HueBaseEntity, SwitchEntity): """Represent a Switch entity from a Hue resource that toggles.""" - controller: BehaviorInstanceController | LightLevelController | MotionController - resource: BehaviorInstance | LightLevel | Motion + controller: ( + BehaviorInstanceController + | LightLevelController + | MotionAreaConfigurationController + | MotionController + ) + resource: BehaviorInstance | LightLevel | MotionAreaConfiguration | Motion entity_description = SwitchEntityDescription( key="sensing_service_enabled", @@ -190,6 +207,35 @@ class HueBehaviorInstanceEnabledEntity(HueResourceEnabledEntity): await self.bridge.async_request_call(self.controller.stop, self.resource.id) +class HueMotionAreaConfigurationEnabledEntity(HueResourceEnabledEntity): + """Representation of a Switch entity to enable/disable a Hue MotionAware zone.""" + + controller: MotionAreaConfigurationController + resource: MotionAreaConfiguration + + entity_description = SwitchEntityDescription( + key="motion_area_configuration", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + has_entity_name=True, + translation_key="motion_aware", + ) + + def __init__( + self, + bridge: HueBridge, + controller: MotionAreaConfigurationController, + resource: MotionAreaConfiguration, + ) -> None: + """Initialize the switch.""" + super().__init__(bridge, controller, resource) + # link the switch to the group the MotionAware zone is associated with + self.hue_group = controller.get_group(resource.id) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.hue_group.id)}, + ) + + class HueMotionSensorEnabledEntity(HueResourceEnabledEntity): """Representation of a Switch entity to enable/disable a Hue motion sensor.""" diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index 5fd6e7c4666e..66e8e7389440 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.hue.const import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.json import JsonArrayType from .conftest import setup_platform @@ -21,6 +21,8 @@ from .const import ( FAKE_ZIGBEE_CONNECTIVITY, ) +TEST_ROOM_ID = "6ddc9066-7e7d-4a03-a773-c73937968296" + async def test_switch( hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType @@ -31,8 +33,8 @@ async def test_switch( await setup_platform(hass, mock_bridge_v2, Platform.SWITCH) # there shouldn't have been any requests at this point assert len(mock_bridge_v2.mock_requests) == 0 - # 4 entities should be created from test data - assert len(hass.states.async_all()) == 4 + # 5 entities should be created from test data + assert len(hass.states.async_all()) == 5 # test config switch to enable/disable motion sensor test_entity = hass.states.get("switch.hue_motion_sensor_motion_sensor_enabled") @@ -49,6 +51,35 @@ async def test_switch( assert test_entity.state == "on" assert test_entity.attributes["device_class"] == "switch" + # test config switch to enable/disable a MotionAware zone + test_entity = hass.states.get("switch.test_room_test_room_motionaware") + assert test_entity is not None + assert test_entity.name == "Test Room MotionAware" + assert test_entity.state == "on" + assert test_entity.attributes["device_class"] == "switch" + + +async def test_motionaware_switch_device( + hass: HomeAssistant, + mock_bridge_v2: Mock, + v2_resources_test_data: JsonArrayType, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the MotionAware switch is attached to the zone device, not the bridge.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, Platform.SWITCH) + + entity_entry = entity_registry.async_get("switch.test_room_test_room_motionaware") + assert entity_entry is not None + + zone_device = device_registry.async_get_device_by_identifier( + (DOMAIN, TEST_ROOM_ID), mock_bridge_v2.config_entry.entry_id + ) + assert zone_device is not None + assert entity_entry.device_id == zone_device.id + async def test_switch_turn_on_service( hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType @@ -115,6 +146,62 @@ async def test_switch_turn_off_service( assert test_entity.state == "off" +async def test_motionaware_switch_turn_on_off_service( + hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType +) -> None: + """Test enabling/disabling a MotionAware zone.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, Platform.SWITCH) + + test_entity_id = "switch.test_room_test_room_motionaware" + + # verify the switch is on before we start + assert hass.states.get(test_entity_id).state == "on" + + # call the HA turn_off service + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": test_entity_id}, + blocking=True, + ) + + # PUT request should have been sent to the motion_area_configuration resource + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["method"] == "put" + assert ( + mock_bridge_v2.mock_requests[0]["path"] + == "clip/v2/resource/motion_area_configuration/" + "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b" + ) + assert mock_bridge_v2.mock_requests[0]["json"]["enabled"] is False + + # Now generate update event by emitting the json we've sent as incoming event + event = { + "id": "5e6f7a8b-9c1d-4e2f-b3a4-5c6d7e8f9a0b", + "type": "motion_area_configuration", + **mock_bridge_v2.mock_requests[0]["json"], + } + mock_bridge_v2.api.emit_event("update", event) + await hass.async_block_till_done() + + # the switch should now be off + assert hass.states.get(test_entity_id).state == "off" + + # call the HA turn_on service + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": test_entity_id}, + blocking=True, + ) + + assert len(mock_bridge_v2.mock_requests) == 2 + assert mock_bridge_v2.mock_requests[1]["method"] == "put" + assert mock_bridge_v2.mock_requests[1]["json"]["enabled"] is True + + async def test_switch_added(hass: HomeAssistant, mock_bridge_v2: Mock) -> None: """Test new switch added to bridge.""" await mock_bridge_v2.api.load_test_data([FAKE_DEVICE, FAKE_ZIGBEE_CONNECTIVITY]) @@ -179,7 +266,7 @@ async def test_internal_behavior_instance_not_added( assert hass.states.get("switch.philips_hue_automation_wall_switch_hallway") is None assert hass.states.get("switch.philips_hue_automation_timer_test") is not None - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 async def test_internal_behavior_instance_entity_removed(