From 281f439bc959650e8e8433e5eaad5cd2ba29c6cb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 7 Mar 2026 14:18:46 +0100 Subject: [PATCH] Add trigger door.closed (#165057) --- homeassistant/components/door/icons.json | 3 + homeassistant/components/door/strings.json | 10 +++ homeassistant/components/door/trigger.py | 10 ++- homeassistant/components/door/triggers.yaml | 9 +++ tests/components/door/test_trigger.py | 85 +++++++++++++++++++++ 5 files changed, 116 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/door/icons.json b/homeassistant/components/door/icons.json index 8548c1150ed7..3bd365eb0fdf 100644 --- a/homeassistant/components/door/icons.json +++ b/homeassistant/components/door/icons.json @@ -1,5 +1,8 @@ { "triggers": { + "closed": { + "trigger": "mdi:door-closed" + }, "opened": { "trigger": "mdi:door-open" } diff --git a/homeassistant/components/door/strings.json b/homeassistant/components/door/strings.json index 5ce5ef7ca330..038a24d81a15 100644 --- a/homeassistant/components/door/strings.json +++ b/homeassistant/components/door/strings.json @@ -14,6 +14,16 @@ }, "title": "Door", "triggers": { + "closed": { + "description": "Triggers after one or more doors close.", + "fields": { + "behavior": { + "description": "[%key:component::door::common::trigger_behavior_description%]", + "name": "[%key:component::door::common::trigger_behavior_name%]" + } + }, + "name": "Door closed" + }, "opened": { "description": "Triggers after one or more doors open.", "fields": { diff --git a/homeassistant/components/door/trigger.py b/homeassistant/components/door/trigger.py index b1ef0a8d43d5..e4c73f0dbdd2 100644 --- a/homeassistant/components/door/trigger.py +++ b/homeassistant/components/door/trigger.py @@ -2,7 +2,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.cover import ATTR_IS_CLOSED, DOMAIN as COVER_DOMAIN -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import get_device_class @@ -65,8 +65,16 @@ class DoorOpenedTrigger(DoorTriggerBase): _cover_is_closed_target_value = False +class DoorClosedTrigger(DoorTriggerBase): + """Trigger for door closed state changes.""" + + _binary_sensor_target_state = STATE_OFF + _cover_is_closed_target_value = True + + TRIGGERS: dict[str, type[Trigger]] = { "opened": DoorOpenedTrigger, + "closed": DoorClosedTrigger, } diff --git a/homeassistant/components/door/triggers.yaml b/homeassistant/components/door/triggers.yaml index 1b1420f1f604..770a79f22215 100644 --- a/homeassistant/components/door/triggers.yaml +++ b/homeassistant/components/door/triggers.yaml @@ -10,6 +10,15 @@ - last - any +closed: + fields: *trigger_common_fields + target: + entity: + - domain: binary_sensor + device_class: door + - domain: cover + device_class: door + opened: fields: *trigger_common_fields target: diff --git a/tests/components/door/test_trigger.py b/tests/components/door/test_trigger.py index c9a4c07f5924..74646744351f 100644 --- a/tests/components/door/test_trigger.py +++ b/tests/components/door/test_trigger.py @@ -42,6 +42,7 @@ async def target_covers(hass: HomeAssistant) -> dict[str, list[str]]: "trigger_key", [ "door.opened", + "door.closed", ], ) async def test_door_triggers_gated_by_labs_flag( @@ -72,6 +73,13 @@ async def test_door_triggers_gated_by_labs_flag( additional_attributes={ATTR_DEVICE_CLASS: "door"}, trigger_from_none=False, ), + *parametrize_trigger_states( + trigger="door.closed", + target_states=[STATE_OFF], + other_states=[STATE_ON], + additional_attributes={ATTR_DEVICE_CLASS: "door"}, + trigger_from_none=False, + ), ], ) async def test_door_trigger_binary_sensor_behavior_any( @@ -144,6 +152,24 @@ async def test_door_trigger_binary_sensor_behavior_any( additional_attributes={ATTR_DEVICE_CLASS: "door"}, trigger_from_none=False, ), + *parametrize_trigger_states( + trigger="door.closed", + target_states=[ + (CoverState.CLOSED, {ATTR_IS_CLOSED: True}), + (CoverState.CLOSING, {ATTR_IS_CLOSED: True}), + ], + other_states=[ + (CoverState.OPEN, {ATTR_IS_CLOSED: False}), + (CoverState.OPENING, {ATTR_IS_CLOSED: False}), + (CoverState.CLOSED, {ATTR_IS_CLOSED: False}), + ], + extra_invalid_states=[ + (CoverState.OPEN, {ATTR_IS_CLOSED: None}), + (CoverState.OPEN, {}), + ], + additional_attributes={ATTR_DEVICE_CLASS: "door"}, + trigger_from_none=False, + ), ], ) async def test_door_trigger_cover_behavior_any( @@ -205,6 +231,13 @@ async def test_door_trigger_cover_behavior_any( additional_attributes={ATTR_DEVICE_CLASS: "door"}, trigger_from_none=False, ), + *parametrize_trigger_states( + trigger="door.closed", + target_states=[STATE_OFF], + other_states=[STATE_ON], + additional_attributes={ATTR_DEVICE_CLASS: "door"}, + trigger_from_none=False, + ), ], ) async def test_door_trigger_binary_sensor_behavior_first( @@ -265,6 +298,13 @@ async def test_door_trigger_binary_sensor_behavior_first( additional_attributes={ATTR_DEVICE_CLASS: "door"}, trigger_from_none=False, ), + *parametrize_trigger_states( + trigger="door.closed", + target_states=[STATE_OFF], + other_states=[STATE_ON], + additional_attributes={ATTR_DEVICE_CLASS: "door"}, + trigger_from_none=False, + ), ], ) async def test_door_trigger_binary_sensor_behavior_last( @@ -338,6 +378,24 @@ async def test_door_trigger_binary_sensor_behavior_last( additional_attributes={ATTR_DEVICE_CLASS: "door"}, trigger_from_none=False, ), + *parametrize_trigger_states( + trigger="door.closed", + target_states=[ + (CoverState.CLOSED, {ATTR_IS_CLOSED: True}), + (CoverState.CLOSING, {ATTR_IS_CLOSED: True}), + ], + other_states=[ + (CoverState.OPEN, {ATTR_IS_CLOSED: False}), + (CoverState.OPENING, {ATTR_IS_CLOSED: False}), + (CoverState.CLOSED, {ATTR_IS_CLOSED: False}), + ], + extra_invalid_states=[ + (CoverState.OPEN, {ATTR_IS_CLOSED: None}), + (CoverState.OPEN, {}), + ], + additional_attributes={ATTR_DEVICE_CLASS: "door"}, + trigger_from_none=False, + ), ], ) async def test_door_trigger_cover_behavior_first( @@ -409,6 +467,24 @@ async def test_door_trigger_cover_behavior_first( additional_attributes={ATTR_DEVICE_CLASS: "door"}, trigger_from_none=False, ), + *parametrize_trigger_states( + trigger="door.closed", + target_states=[ + (CoverState.CLOSED, {ATTR_IS_CLOSED: True}), + (CoverState.CLOSING, {ATTR_IS_CLOSED: True}), + ], + other_states=[ + (CoverState.OPEN, {ATTR_IS_CLOSED: False}), + (CoverState.OPENING, {ATTR_IS_CLOSED: False}), + (CoverState.CLOSED, {ATTR_IS_CLOSED: False}), + ], + extra_invalid_states=[ + (CoverState.OPEN, {ATTR_IS_CLOSED: None}), + (CoverState.OPEN, {}), + ], + additional_attributes={ATTR_DEVICE_CLASS: "door"}, + trigger_from_none=False, + ), ], ) async def test_door_trigger_cover_behavior_last( @@ -477,6 +553,15 @@ async def test_door_trigger_cover_behavior_last( CoverState.OPEN, False, ), + ( + "door.closed", + STATE_ON, + STATE_OFF, + CoverState.OPEN, + False, + CoverState.CLOSED, + True, + ), ], ) async def test_door_trigger_excludes_non_door_device_class(