diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index cd27d66cf0dc..31dc261748e6 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -12,6 +12,7 @@ from aiohue.v2.controllers.sensors import ( Motion, MotionController, ) +from aiohue.v2.models.behavior_instance import PresenceMimickingState from aiohue.v2.models.behavior_script import BehaviorScriptCategory from homeassistant.components.switch import ( @@ -143,8 +144,13 @@ class HueResourceEnabledEntity(HueBaseEntity, SwitchEntity): class HueBehaviorInstanceEnabledEntity(HueResourceEnabledEntity): - """Representation of a Switch entity to enable/disable a Hue Behavior Instance.""" + """Representation of a Switch entity to enable/disable a Hue Behavior Instance. + Automations that the Hue app runs with a play button, such as mimic + presence, are started and stopped. All others are enabled and disabled. + """ + + controller: BehaviorInstanceController resource: BehaviorInstance entity_description = SwitchEntityDescription( @@ -159,6 +165,30 @@ class HueBehaviorInstanceEnabledEntity(HueResourceEnabledEntity): """Return name for this entity.""" return f"Automation: {self.resource.metadata.name}" + @property + @override + def is_on(self) -> bool: + """Return true if the switch is on.""" + if (run_state := self.resource.presence_mimicking_state) is not None: + return run_state is PresenceMimickingState.STARTED + return self.resource.enabled + + @override + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + if self.resource.presence_mimicking_state is None: + await super().async_turn_on(**kwargs) + return + await self.bridge.async_request_call(self.controller.start, self.resource.id) + + @override + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + if self.resource.presence_mimicking_state is None: + await super().async_turn_off(**kwargs) + return + await self.bridge.async_request_call(self.controller.stop, self.resource.id) + class HueMotionSensorEnabledEntity(HueResourceEnabledEntity): """Representation of a Switch entity to enable/disable a Hue motion sensor.""" diff --git a/tests/components/hue/const.py b/tests/components/hue/const.py index 12cae5db14e8..884b992a0587 100644 --- a/tests/components/hue/const.py +++ b/tests/components/hue/const.py @@ -162,3 +162,28 @@ FAKE_BEHAVIOR_INSTANCE = { "status": "running", "type": "behavior_instance", } + +FAKE_PRESENCE_MIMICKING_SCRIPT = { + "configuration_schema": {}, + "description": "PM Automation", + "id": "fake_behavior_script_id_2", + "metadata": {"category": "automation", "name": "PM"}, + "state_schema": {}, + "supported_features": [], + "trigger_schema": {}, + "type": "behavior_script", + "version": "0.0.1", +} + +FAKE_PRESENCE_MIMICKING_INSTANCE = { + "configuration": {}, + "dependees": [], # codespell:ignore dependees + "enabled": True, + "id": "fake_behavior_instance_id_2", + "last_error": "", + "metadata": {"name": "Mimic presence"}, + "script_id": "fake_behavior_script_id_2", + "state": {"pm_state": "stopped"}, + "status": "running", + "type": "behavior_instance", +} diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index 2e02f5d2b858..5fd6e7c4666e 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -16,6 +16,8 @@ from .const import ( FAKE_BEHAVIOR_SCRIPT, FAKE_BINARY_SENSOR, FAKE_DEVICE, + FAKE_PRESENCE_MIMICKING_INSTANCE, + FAKE_PRESENCE_MIMICKING_SCRIPT, FAKE_ZIGBEE_CONNECTIVITY, ) @@ -198,3 +200,79 @@ async def test_internal_behavior_instance_entity_removed( await setup_platform(hass, mock_bridge_v2, Platform.SWITCH) assert entity_registry.async_get(stale_entity.entity_id) is None + + +@pytest.mark.parametrize( + ("pm_state", "expected_state"), [("started", "on"), ("stopped", "off")] +) +async def test_presence_mimicking_switch_state( + hass: HomeAssistant, + mock_bridge_v2: Mock, + v2_resources_test_data: JsonArrayType, + pm_state: str, + expected_state: str, +) -> None: + """Test the switch follows the run state instead of enabled.""" + instance = {**FAKE_PRESENCE_MIMICKING_INSTANCE, "state": {"pm_state": pm_state}} + await mock_bridge_v2.api.load_test_data( + [*v2_resources_test_data, FAKE_PRESENCE_MIMICKING_SCRIPT, instance] + ) + + await setup_platform(hass, mock_bridge_v2, Platform.SWITCH) + + test_entity = hass.states.get("switch.philips_hue_automation_mimic_presence") + assert test_entity is not None + assert test_entity.state == expected_state + + +@pytest.mark.parametrize( + ("service", "expected_trigger"), + [("turn_on", {"start": {}}), ("turn_off", {"stop": {}})], +) +async def test_presence_mimicking_switch_services( + hass: HomeAssistant, + mock_bridge_v2: Mock, + v2_resources_test_data: JsonArrayType, + service: str, + expected_trigger: dict, +) -> None: + """Test the switch starts and stops instead of touching enabled.""" + await mock_bridge_v2.api.load_test_data( + [ + *v2_resources_test_data, + FAKE_PRESENCE_MIMICKING_SCRIPT, + FAKE_PRESENCE_MIMICKING_INSTANCE, + ] + ) + + await setup_platform(hass, mock_bridge_v2, Platform.SWITCH) + + await hass.services.async_call( + "switch", + service, + {"entity_id": "switch.philips_hue_automation_mimic_presence"}, + blocking=True, + ) + + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["method"] == "put" + assert mock_bridge_v2.mock_requests[0]["json"] == {"trigger": expected_trigger} + + +async def test_regular_automation_switch_uses_enabled( + hass: HomeAssistant, mock_bridge_v2: Mock, v2_resources_test_data: JsonArrayType +) -> None: + """Test an automation without a run state still toggles enabled.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + + await setup_platform(hass, mock_bridge_v2, Platform.SWITCH) + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.philips_hue_automation_timer_test"}, + blocking=True, + ) + + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["json"] == {"enabled": False}