mirror of
https://github.com/home-assistant/core.git
synced 2025-11-13 13:00:11 +00:00
Compare commits
29 Commits
add-includ
...
claude/tri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f3c3e5b0d | ||
|
|
b46640a1c2 | ||
|
|
f7727e8192 | ||
|
|
296c41c46c | ||
|
|
a7d5140f80 | ||
|
|
e8cd2ad1e6 | ||
|
|
10bd2ffc5f | ||
|
|
b9dac02e8e | ||
|
|
8605eb046a | ||
|
|
26b4fa5d39 | ||
|
|
be23d3d43d | ||
|
|
a4fbb597f4 | ||
|
|
ee11fc37d5 | ||
|
|
9900e49bcc | ||
|
|
3feb3fefef | ||
|
|
b3e4f6dc43 | ||
|
|
a9ba0bea8f | ||
|
|
bafa1e250d | ||
|
|
734c6f27c6 | ||
|
|
37eed7fae8 | ||
|
|
37aa4c68d9 | ||
|
|
80f8d94db4 | ||
|
|
7c0a7f4f9d | ||
|
|
5d5d7f7acf | ||
|
|
f0feb93fe1 | ||
|
|
9361ccbfc2 | ||
|
|
e9c76f1053 | ||
|
|
36b100c40a | ||
|
|
60107a1492 |
@@ -143,5 +143,28 @@
|
||||
"name": "Trigger"
|
||||
}
|
||||
},
|
||||
"title": "Alarm control panel"
|
||||
"title": "Alarm control panel",
|
||||
"triggers": {
|
||||
"armed": {
|
||||
"description": "Triggers when an alarm is armed.",
|
||||
"description_configured": "Triggers when an alarm is armed",
|
||||
"fields": {
|
||||
"mode": {
|
||||
"description": "The arm modes to trigger on. If empty, triggers on all arm modes.",
|
||||
"name": "Arm modes"
|
||||
}
|
||||
},
|
||||
"name": "When an alarm is armed"
|
||||
},
|
||||
"disarmed": {
|
||||
"description": "Triggers when an alarm is disarmed.",
|
||||
"description_configured": "Triggers when an alarm is disarmed",
|
||||
"name": "When an alarm is disarmed"
|
||||
},
|
||||
"triggered": {
|
||||
"description": "Triggers when an alarm is triggered.",
|
||||
"description_configured": "Triggers when an alarm is triggered",
|
||||
"name": "When an alarm is triggered"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
283
homeassistant/components/alarm_control_panel/trigger.py
Normal file
283
homeassistant/components/alarm_control_panel/trigger.py
Normal file
@@ -0,0 +1,283 @@
|
||||
"""Provides triggers for alarm control panels."""
|
||||
|
||||
from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.target import (
|
||||
TargetStateChangedData,
|
||||
async_track_target_selector_state_change_event,
|
||||
)
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, AlarmControlPanelState
|
||||
|
||||
CONF_MODE = "mode"
|
||||
|
||||
ARMED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Optional(CONF_MODE, default=[]): vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.In(
|
||||
[
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
DISARMED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
TRIGGERED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AlarmArmedTrigger(Trigger):
|
||||
"""Trigger for when an alarm control panel is armed."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, ARMED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the alarm armed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
mode_filter = self._options[CONF_MODE]
|
||||
|
||||
# All armed states
|
||||
armed_states = {
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||
}
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if the new state is an armed state
|
||||
if to_state.state not in armed_states:
|
||||
return
|
||||
|
||||
# If mode filter is specified, check if the mode matches
|
||||
if mode_filter and to_state.state not in mode_filter:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"alarm armed on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class AlarmDisarmedTrigger(Trigger):
|
||||
"""Trigger for when an alarm control panel is disarmed."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, DISARMED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the alarm disarmed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if the new state is disarmed
|
||||
if to_state.state != AlarmControlPanelState.DISARMED:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"alarm disarmed on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class AlarmTriggeredTrigger(Trigger):
|
||||
"""Trigger for when an alarm control panel is triggered."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, TRIGGERED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the alarm triggered trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if the new state is triggered
|
||||
if to_state.state != AlarmControlPanelState.TRIGGERED:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"alarm triggered on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"armed": AlarmArmedTrigger,
|
||||
"disarmed": AlarmDisarmedTrigger,
|
||||
"triggered": AlarmTriggeredTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for alarm control panels."""
|
||||
return TRIGGERS
|
||||
30
homeassistant/components/alarm_control_panel/triggers.yaml
Normal file
30
homeassistant/components/alarm_control_panel/triggers.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
armed:
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
fields:
|
||||
mode:
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
options:
|
||||
- value: armed_home
|
||||
label: Home
|
||||
- value: armed_away
|
||||
label: Away
|
||||
- value: armed_night
|
||||
label: Night
|
||||
- value: armed_vacation
|
||||
label: Vacation
|
||||
- value: armed_custom_bypass
|
||||
label: Custom bypass
|
||||
disarmed:
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
triggered:
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
@@ -98,5 +98,27 @@
|
||||
"name": "Start conversation"
|
||||
}
|
||||
},
|
||||
"title": "Assist satellite"
|
||||
"title": "Assist satellite",
|
||||
"triggers": {
|
||||
"listening": {
|
||||
"description": "Triggers when a satellite starts listening for a command.",
|
||||
"description_configured": "Triggers when a satellite starts listening for a command",
|
||||
"name": "When a satellite starts listening"
|
||||
},
|
||||
"processing": {
|
||||
"description": "Triggers when a satellite starts processing a command.",
|
||||
"description_configured": "Triggers when a satellite starts processing a command",
|
||||
"name": "When a satellite starts processing"
|
||||
},
|
||||
"responding": {
|
||||
"description": "Triggers when a satellite starts responding to a command.",
|
||||
"description_configured": "Triggers when a satellite starts responding to a command",
|
||||
"name": "When a satellite starts responding"
|
||||
},
|
||||
"idle": {
|
||||
"description": "Triggers when a satellite goes back to idle.",
|
||||
"description_configured": "Triggers when a satellite goes back to idle",
|
||||
"name": "When a satellite goes back to idle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
140
homeassistant/components/assist_satellite/trigger.py
Normal file
140
homeassistant/components/assist_satellite/trigger.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Provides triggers for assist satellites."""
|
||||
|
||||
from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_TARGET,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import process_state_match
|
||||
from homeassistant.helpers.target import (
|
||||
TargetStateChangedData,
|
||||
async_track_target_selector_state_change_event,
|
||||
)
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
STATE_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class StateTriggerBase(Trigger):
|
||||
"""Trigger for assist satellite state changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, STATE_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig, state: str) -> None:
|
||||
"""Initialize the state trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
self._state = state
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
match_config_state = process_state_match(self._state)
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if the new state matches the trigger state
|
||||
if not match_config_state(to_state.state):
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"{entity_id} {self._state}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ListeningTrigger(StateTriggerBase):
|
||||
"""Trigger for when a satellite starts listening."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the listening trigger."""
|
||||
super().__init__(hass, config, "listening")
|
||||
|
||||
|
||||
class ProcessingTrigger(StateTriggerBase):
|
||||
"""Trigger for when a satellite starts processing."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the processing trigger."""
|
||||
super().__init__(hass, config, "processing")
|
||||
|
||||
|
||||
class RespondingTrigger(StateTriggerBase):
|
||||
"""Trigger for when a satellite starts responding."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the responding trigger."""
|
||||
super().__init__(hass, config, "responding")
|
||||
|
||||
|
||||
class IdleTrigger(StateTriggerBase):
|
||||
"""Trigger for when a satellite goes back to idle."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the idle trigger."""
|
||||
super().__init__(hass, config, "idle")
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"listening": ListeningTrigger,
|
||||
"processing": ProcessingTrigger,
|
||||
"responding": RespondingTrigger,
|
||||
"idle": IdleTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for assist satellites."""
|
||||
return TRIGGERS
|
||||
19
homeassistant/components/assist_satellite/triggers.yaml
Normal file
19
homeassistant/components/assist_satellite/triggers.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
listening:
|
||||
target:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
|
||||
processing:
|
||||
target:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
|
||||
responding:
|
||||
target:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
|
||||
idle:
|
||||
target:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
@@ -285,5 +285,93 @@
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Climate"
|
||||
"title": "Climate",
|
||||
"triggers": {
|
||||
"cooling": {
|
||||
"description": "Triggers when a climate starts cooling.",
|
||||
"name": "When a climate starts cooling"
|
||||
},
|
||||
"current_humidity_changed": {
|
||||
"description": "Triggers when the current humidity of a climate changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when the current humidity goes above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when the current humidity goes below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "When current humidity changes"
|
||||
},
|
||||
"current_temperature_changed": {
|
||||
"description": "Triggers when the current temperature of a climate changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when the current temperature goes above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when the current temperature goes below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "When current temperature changes"
|
||||
},
|
||||
"drying": {
|
||||
"description": "Triggers when a climate starts drying.",
|
||||
"name": "When a climate starts drying"
|
||||
},
|
||||
"heating": {
|
||||
"description": "Triggers when a climate starts heating.",
|
||||
"name": "When a climate starts heating"
|
||||
},
|
||||
"mode_changed": {
|
||||
"description": "Triggers when the HVAC mode of a climate changes.",
|
||||
"fields": {
|
||||
"hvac_mode": {
|
||||
"description": "The HVAC modes to trigger on. If empty, triggers on all mode changes.",
|
||||
"name": "HVAC modes"
|
||||
}
|
||||
},
|
||||
"name": "When HVAC mode changes"
|
||||
},
|
||||
"target_humidity_changed": {
|
||||
"description": "Triggers when the target humidity of a climate changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when the target humidity goes above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when the target humidity goes below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "When target humidity changes"
|
||||
},
|
||||
"target_temperature_changed": {
|
||||
"description": "Triggers when the target temperature of a climate changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when the target temperature goes above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when the target temperature goes below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "When target temperature changes"
|
||||
},
|
||||
"turns_off": {
|
||||
"description": "Triggers when a climate turns off.",
|
||||
"name": "When a climate turns off"
|
||||
},
|
||||
"turns_on": {
|
||||
"description": "Triggers when a climate turns on.",
|
||||
"name": "When a climate turns on"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
817
homeassistant/components/climate/trigger.py
Normal file
817
homeassistant/components/climate/trigger.py
Normal file
@@ -0,0 +1,817 @@
|
||||
"""Provides triggers for climate."""
|
||||
|
||||
from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.target import (
|
||||
TargetStateChangedData,
|
||||
async_track_target_selector_state_change_event,
|
||||
)
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_HUMIDITY,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODE,
|
||||
DOMAIN,
|
||||
HVAC_MODES,
|
||||
HVACMode,
|
||||
)
|
||||
|
||||
CLIMATE_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
MODE_CHANGED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Optional(ATTR_HVAC_MODE, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.In(HVAC_MODES)]
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
THRESHOLD_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ClimateTurnsOnTrigger(Trigger):
|
||||
"""Trigger for when a climate turns on."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, CLIMATE_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate turns on trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if climate turned on (from off to any other mode)
|
||||
if (
|
||||
from_state is not None
|
||||
and from_state.state == HVACMode.OFF
|
||||
and to_state.state != HVACMode.OFF
|
||||
):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} turned on",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateTurnsOffTrigger(Trigger):
|
||||
"""Trigger for when a climate turns off."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, CLIMATE_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate turns off trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if climate turned off (from any mode to off)
|
||||
if (
|
||||
from_state is not None
|
||||
and from_state.state != HVACMode.OFF
|
||||
and to_state.state == HVACMode.OFF
|
||||
):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} turned off",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateModeChangedTrigger(Trigger):
|
||||
"""Trigger for when a climate mode changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, MODE_CHANGED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate mode changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
hvac_modes_filter = self._options[ATTR_HVAC_MODE]
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if hvac_mode changed
|
||||
if from_state is not None and from_state.state != to_state.state:
|
||||
# If hvac_modes filter is specified, check if the new mode matches
|
||||
if hvac_modes_filter and to_state.state not in hvac_modes_filter:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} mode changed to {to_state.state}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateCoolingTrigger(Trigger):
|
||||
"""Trigger for when a climate starts cooling."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, CLIMATE_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate cooling trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if climate started cooling
|
||||
from_action = from_state.attributes.get(ATTR_HVAC_ACTION) if from_state else None
|
||||
to_action = to_state.attributes.get(ATTR_HVAC_ACTION)
|
||||
|
||||
if from_action != "cooling" and to_action == "cooling":
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} started cooling",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateHeatingTrigger(Trigger):
|
||||
"""Trigger for when a climate starts heating."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, CLIMATE_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate heating trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if climate started heating
|
||||
from_action = from_state.attributes.get(ATTR_HVAC_ACTION) if from_state else None
|
||||
to_action = to_state.attributes.get(ATTR_HVAC_ACTION)
|
||||
|
||||
if from_action != "heating" and to_action == "heating":
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} started heating",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateDryingTrigger(Trigger):
|
||||
"""Trigger for when a climate starts drying."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, CLIMATE_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate drying trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if climate started drying
|
||||
from_action = from_state.attributes.get(ATTR_HVAC_ACTION) if from_state else None
|
||||
to_action = to_state.attributes.get(ATTR_HVAC_ACTION)
|
||||
|
||||
if from_action != "drying" and to_action == "drying":
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} started drying",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateTargetTemperatureChangedTrigger(Trigger):
|
||||
"""Trigger for when a climate target temperature changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, THRESHOLD_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate target temperature changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
above = self._options.get(CONF_ABOVE)
|
||||
below = self._options.get(CONF_BELOW)
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if target temperature changed
|
||||
from_temp = (
|
||||
from_state.attributes.get(ATTR_TEMPERATURE) if from_state else None
|
||||
)
|
||||
to_temp = to_state.attributes.get(ATTR_TEMPERATURE)
|
||||
|
||||
if to_temp is None or from_temp == to_temp:
|
||||
return
|
||||
|
||||
# Apply threshold filters if specified
|
||||
if above is not None and to_temp <= above:
|
||||
return
|
||||
if below is not None and to_temp >= below:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} target temperature changed to {to_temp}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateCurrentTemperatureChangedTrigger(Trigger):
|
||||
"""Trigger for when a climate current temperature changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, THRESHOLD_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate current temperature changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
above = self._options.get(CONF_ABOVE)
|
||||
below = self._options.get(CONF_BELOW)
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if current temperature changed
|
||||
from_temp = (
|
||||
from_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
|
||||
if from_state
|
||||
else None
|
||||
)
|
||||
to_temp = to_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
|
||||
|
||||
if to_temp is None or from_temp == to_temp:
|
||||
return
|
||||
|
||||
# Apply threshold filters if specified
|
||||
if above is not None and to_temp <= above:
|
||||
return
|
||||
if below is not None and to_temp >= below:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} current temperature changed to {to_temp}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateTargetHumidityChangedTrigger(Trigger):
|
||||
"""Trigger for when a climate target humidity changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, THRESHOLD_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate target humidity changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
above = self._options.get(CONF_ABOVE)
|
||||
below = self._options.get(CONF_BELOW)
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if target humidity changed
|
||||
from_humidity = (
|
||||
from_state.attributes.get(ATTR_HUMIDITY) if from_state else None
|
||||
)
|
||||
to_humidity = to_state.attributes.get(ATTR_HUMIDITY)
|
||||
|
||||
if to_humidity is None or from_humidity == to_humidity:
|
||||
return
|
||||
|
||||
# Apply threshold filters if specified
|
||||
if above is not None and to_humidity <= above:
|
||||
return
|
||||
if below is not None and to_humidity >= below:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} target humidity changed to {to_humidity}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class ClimateCurrentHumidityChangedTrigger(Trigger):
|
||||
"""Trigger for when a climate current humidity changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, THRESHOLD_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the climate current humidity changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
above = self._options.get(CONF_ABOVE)
|
||||
below = self._options.get(CONF_BELOW)
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Check if current humidity changed
|
||||
from_humidity = (
|
||||
from_state.attributes.get(ATTR_CURRENT_HUMIDITY)
|
||||
if from_state
|
||||
else None
|
||||
)
|
||||
to_humidity = to_state.attributes.get(ATTR_CURRENT_HUMIDITY)
|
||||
|
||||
if to_humidity is None or from_humidity == to_humidity:
|
||||
return
|
||||
|
||||
# Apply threshold filters if specified
|
||||
if above is not None and to_humidity <= above:
|
||||
return
|
||||
if below is not None and to_humidity >= below:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"climate {entity_id} current humidity changed to {to_humidity}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turns_on": ClimateTurnsOnTrigger,
|
||||
"turns_off": ClimateTurnsOffTrigger,
|
||||
"mode_changed": ClimateModeChangedTrigger,
|
||||
"cooling": ClimateCoolingTrigger,
|
||||
"heating": ClimateHeatingTrigger,
|
||||
"drying": ClimateDryingTrigger,
|
||||
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
|
||||
"current_temperature_changed": ClimateCurrentTemperatureChangedTrigger,
|
||||
"target_humidity_changed": ClimateTargetHumidityChangedTrigger,
|
||||
"current_humidity_changed": ClimateCurrentHumidityChangedTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for climate."""
|
||||
return TRIGGERS
|
||||
128
homeassistant/components/climate/triggers.yaml
Normal file
128
homeassistant/components/climate/triggers.yaml
Normal file
@@ -0,0 +1,128 @@
|
||||
turns_on:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
|
||||
turns_off:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
|
||||
mode_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
fields:
|
||||
hvac_mode:
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
mode: dropdown
|
||||
options:
|
||||
- label: "Off"
|
||||
value: "off"
|
||||
- label: "Heat"
|
||||
value: "heat"
|
||||
- label: "Cool"
|
||||
value: "cool"
|
||||
- label: "Heat/Cool"
|
||||
value: "heat_cool"
|
||||
- label: "Auto"
|
||||
value: "auto"
|
||||
- label: "Dry"
|
||||
value: "dry"
|
||||
- label: "Fan only"
|
||||
value: "fan_only"
|
||||
|
||||
cooling:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
|
||||
heating:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
|
||||
drying:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
|
||||
target_temperature_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
fields:
|
||||
above:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
step: 0.1
|
||||
below:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
step: 0.1
|
||||
|
||||
current_temperature_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
fields:
|
||||
above:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
step: 0.1
|
||||
below:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
step: 0.1
|
||||
|
||||
target_humidity_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
fields:
|
||||
above:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
min: 0
|
||||
max: 100
|
||||
below:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
min: 0
|
||||
max: 100
|
||||
|
||||
current_humidity_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
fields:
|
||||
above:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
min: 0
|
||||
max: 100
|
||||
below:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
min: 0
|
||||
max: 100
|
||||
@@ -136,5 +136,75 @@
|
||||
"name": "Toggle tilt"
|
||||
}
|
||||
},
|
||||
"title": "Cover"
|
||||
"title": "Cover",
|
||||
"triggers": {
|
||||
"opens": {
|
||||
"description": "Triggers when a cover opens.",
|
||||
"description_configured": "Triggers when a cover opens",
|
||||
"fields": {
|
||||
"fully_opened": {
|
||||
"description": "Only trigger when the cover is fully opened (position at 100%).",
|
||||
"name": "Fully opened"
|
||||
},
|
||||
"device_class": {
|
||||
"description": "The device classes to trigger on. If empty, triggers on all device classes.",
|
||||
"name": "Device classes"
|
||||
}
|
||||
},
|
||||
"name": "When a cover opens"
|
||||
},
|
||||
"closes": {
|
||||
"description": "Triggers when a cover closes.",
|
||||
"description_configured": "Triggers when a cover closes",
|
||||
"fields": {
|
||||
"fully_closed": {
|
||||
"description": "Only trigger when the cover is fully closed (position at 0%).",
|
||||
"name": "Fully closed"
|
||||
},
|
||||
"device_class": {
|
||||
"description": "The device classes to trigger on. If empty, triggers on all device classes.",
|
||||
"name": "Device classes"
|
||||
}
|
||||
},
|
||||
"name": "When a cover closes"
|
||||
},
|
||||
"stops": {
|
||||
"description": "Triggers when a cover stops moving.",
|
||||
"description_configured": "Triggers when a cover stops moving",
|
||||
"fields": {
|
||||
"device_class": {
|
||||
"description": "The device classes to trigger on. If empty, triggers on all device classes.",
|
||||
"name": "Device classes"
|
||||
}
|
||||
},
|
||||
"name": "When a cover stops moving"
|
||||
},
|
||||
"position_changed": {
|
||||
"description": "Triggers when the position of a cover changes.",
|
||||
"description_configured": "Triggers when the position of a cover changes",
|
||||
"fields": {
|
||||
"lower": {
|
||||
"description": "The minimum position value to trigger on. Only triggers when position is at or above this value.",
|
||||
"name": "Lower limit"
|
||||
},
|
||||
"upper": {
|
||||
"description": "The maximum position value to trigger on. Only triggers when position is at or below this value.",
|
||||
"name": "Upper limit"
|
||||
},
|
||||
"above": {
|
||||
"description": "Only trigger when position is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when position is below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"device_class": {
|
||||
"description": "The device classes to trigger on. If empty, triggers on all device classes.",
|
||||
"name": "Device classes"
|
||||
}
|
||||
},
|
||||
"name": "When the position of a cover changes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
453
homeassistant/components/cover/trigger.py
Normal file
453
homeassistant/components/cover/trigger.py
Normal file
@@ -0,0 +1,453 @@
|
||||
"""Provides triggers for covers."""
|
||||
|
||||
from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.target import (
|
||||
TargetStateChangedData,
|
||||
async_track_target_selector_state_change_event,
|
||||
)
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import ATTR_CURRENT_POSITION, CoverDeviceClass, CoverState
|
||||
from .const import DOMAIN
|
||||
|
||||
CONF_LOWER = "lower"
|
||||
CONF_UPPER = "upper"
|
||||
CONF_ABOVE = "above"
|
||||
CONF_BELOW = "below"
|
||||
CONF_FULLY_OPENED = "fully_opened"
|
||||
CONF_FULLY_CLOSED = "fully_closed"
|
||||
|
||||
OPENS_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Optional(CONF_FULLY_OPENED, default=False): cv.boolean,
|
||||
vol.Optional(CONF_DEVICE_CLASS, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.Coerce(CoverDeviceClass)]
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
CLOSES_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Optional(CONF_FULLY_CLOSED, default=False): cv.boolean,
|
||||
vol.Optional(CONF_DEVICE_CLASS, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.Coerce(CoverDeviceClass)]
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
STOPS_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Optional(CONF_DEVICE_CLASS, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.Coerce(CoverDeviceClass)]
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
POSITION_CHANGED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Exclusive(CONF_LOWER, "position_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=100)
|
||||
),
|
||||
vol.Exclusive(CONF_UPPER, "position_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=100)
|
||||
),
|
||||
vol.Exclusive(CONF_ABOVE, "position_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=100)
|
||||
),
|
||||
vol.Exclusive(CONF_BELOW, "position_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=100)
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_CLASS, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.Coerce(CoverDeviceClass)]
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CoverOpensTrigger(Trigger):
|
||||
"""Trigger for when a cover opens."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, OPENS_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the cover opens trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
assert config.options is not None
|
||||
self._target = config.target
|
||||
self._options = config.options
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
fully_opened = self._options[CONF_FULLY_OPENED]
|
||||
device_classes_filter = self._options[CONF_DEVICE_CLASS]
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Filter by device class if specified
|
||||
if device_classes_filter:
|
||||
device_class = to_state.attributes.get(CONF_DEVICE_CLASS)
|
||||
if device_class not in device_classes_filter:
|
||||
return
|
||||
|
||||
# Trigger when cover opens or is opening
|
||||
if to_state.state in (CoverState.OPEN, CoverState.OPENING):
|
||||
# If fully_opened is True, only trigger when position reaches 100
|
||||
if fully_opened:
|
||||
current_position = to_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if current_position != 100:
|
||||
return
|
||||
|
||||
# Only trigger on state change, not if already in that state
|
||||
if from_state and from_state.state == to_state.state:
|
||||
# For fully_opened, allow triggering when position changes to 100
|
||||
if fully_opened:
|
||||
from_position = from_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
to_position = to_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if from_position == to_position:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"cover opened on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class CoverClosesTrigger(Trigger):
|
||||
"""Trigger for when a cover closes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, CLOSES_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the cover closes trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
assert config.options is not None
|
||||
self._target = config.target
|
||||
self._options = config.options
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
fully_closed = self._options[CONF_FULLY_CLOSED]
|
||||
device_classes_filter = self._options[CONF_DEVICE_CLASS]
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Filter by device class if specified
|
||||
if device_classes_filter:
|
||||
device_class = to_state.attributes.get(CONF_DEVICE_CLASS)
|
||||
if device_class not in device_classes_filter:
|
||||
return
|
||||
|
||||
# Trigger when cover closes or is closing
|
||||
if to_state.state in (CoverState.CLOSED, CoverState.CLOSING):
|
||||
# If fully_closed is True, only trigger when position reaches 0
|
||||
if fully_closed:
|
||||
current_position = to_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if current_position != 0:
|
||||
return
|
||||
|
||||
# Only trigger on state change, not if already in that state
|
||||
if from_state and from_state.state == to_state.state:
|
||||
# For fully_closed, allow triggering when position changes to 0
|
||||
if fully_closed:
|
||||
from_position = from_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
to_position = to_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if from_position == to_position:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"cover closed on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class CoverStopsTrigger(Trigger):
|
||||
"""Trigger for when a cover stops moving."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, STOPS_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the cover stops trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
assert config.options is not None
|
||||
self._target = config.target
|
||||
self._options = config.options
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
device_classes_filter = self._options[CONF_DEVICE_CLASS]
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Filter by device class if specified
|
||||
if device_classes_filter:
|
||||
device_class = to_state.attributes.get(CONF_DEVICE_CLASS)
|
||||
if device_class not in device_classes_filter:
|
||||
return
|
||||
|
||||
# Trigger when cover stops (from opening/closing to open/closed)
|
||||
if from_state and from_state.state in (
|
||||
CoverState.OPENING,
|
||||
CoverState.CLOSING,
|
||||
):
|
||||
if to_state.state in (CoverState.OPEN, CoverState.CLOSED):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"cover stopped on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class CoverPositionChangedTrigger(Trigger):
|
||||
"""Trigger for when a cover's position changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, POSITION_CHANGED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the cover position changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
self._options = config.options or {}
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
lower_limit = self._options.get(CONF_LOWER)
|
||||
upper_limit = self._options.get(CONF_UPPER)
|
||||
above_limit = self._options.get(CONF_ABOVE)
|
||||
below_limit = self._options.get(CONF_BELOW)
|
||||
device_classes_filter = self._options.get(CONF_DEVICE_CLASS, [])
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Filter by device class if specified
|
||||
if device_classes_filter:
|
||||
device_class = to_state.attributes.get(CONF_DEVICE_CLASS)
|
||||
if device_class not in device_classes_filter:
|
||||
return
|
||||
|
||||
# Get position values
|
||||
from_position = (
|
||||
from_state.attributes.get(ATTR_CURRENT_POSITION) if from_state else None
|
||||
)
|
||||
to_position = to_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
|
||||
# Only trigger if position value exists and has changed
|
||||
if to_position is None or from_position == to_position:
|
||||
return
|
||||
|
||||
# Apply threshold filters if configured
|
||||
if lower_limit is not None and to_position < lower_limit:
|
||||
return
|
||||
if upper_limit is not None and to_position > upper_limit:
|
||||
return
|
||||
if above_limit is not None and to_position <= above_limit:
|
||||
return
|
||||
if below_limit is not None and to_position >= below_limit:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
"from_position": from_position,
|
||||
"to_position": to_position,
|
||||
},
|
||||
f"position changed on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"opens": CoverOpensTrigger,
|
||||
"closes": CoverClosesTrigger,
|
||||
"stops": CoverStopsTrigger,
|
||||
"position_changed": CoverPositionChangedTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for covers."""
|
||||
return TRIGGERS
|
||||
101
homeassistant/components/cover/triggers.yaml
Normal file
101
homeassistant/components/cover/triggers.yaml
Normal file
@@ -0,0 +1,101 @@
|
||||
opens:
|
||||
target:
|
||||
entity:
|
||||
domain: cover
|
||||
fields:
|
||||
fully_opened:
|
||||
required: false
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
device_class:
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
options:
|
||||
- curtain
|
||||
- shutter
|
||||
- blind
|
||||
|
||||
closes:
|
||||
target:
|
||||
entity:
|
||||
domain: cover
|
||||
fields:
|
||||
fully_closed:
|
||||
required: false
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
device_class:
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
options:
|
||||
- curtain
|
||||
- shutter
|
||||
- blind
|
||||
|
||||
stops:
|
||||
target:
|
||||
entity:
|
||||
domain: cover
|
||||
fields:
|
||||
device_class:
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
options:
|
||||
- curtain
|
||||
- shutter
|
||||
- blind
|
||||
|
||||
position_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: cover
|
||||
fields:
|
||||
lower:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
upper:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
above:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
below:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 100
|
||||
mode: box
|
||||
device_class:
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
options:
|
||||
- curtain
|
||||
- shutter
|
||||
- blind
|
||||
@@ -462,5 +462,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Light"
|
||||
"title": "Light",
|
||||
"triggers": {
|
||||
"turns_on": {
|
||||
"description": "Triggers when a light turns on.",
|
||||
"description_configured": "Triggers when a light turns on",
|
||||
"name": "When a light turns on"
|
||||
},
|
||||
"turns_off": {
|
||||
"description": "Triggers when a light turns off.",
|
||||
"description_configured": "Triggers when a light turns off",
|
||||
"name": "When a light turns off"
|
||||
},
|
||||
"brightness_changed": {
|
||||
"description": "Triggers when the brightness of a light changes.",
|
||||
"description_configured": "Triggers when the brightness of a light changes",
|
||||
"fields": {
|
||||
"lower": {
|
||||
"description": "The minimum brightness value to trigger on. Only triggers when brightness is at or above this value.",
|
||||
"name": "Lower limit"
|
||||
},
|
||||
"upper": {
|
||||
"description": "The maximum brightness value to trigger on. Only triggers when brightness is at or below this value.",
|
||||
"name": "Upper limit"
|
||||
},
|
||||
"above": {
|
||||
"description": "Only trigger when brightness is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when brightness is below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "When the brightness of a light changes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
288
homeassistant/components/light/trigger.py
Normal file
288
homeassistant/components/light/trigger.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""Provides triggers for lights."""
|
||||
|
||||
from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.target import (
|
||||
TargetStateChangedData,
|
||||
async_track_target_selector_state_change_event,
|
||||
)
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
ATTR_BRIGHTNESS = "brightness"
|
||||
CONF_LOWER = "lower"
|
||||
CONF_UPPER = "upper"
|
||||
CONF_ABOVE = "above"
|
||||
CONF_BELOW = "below"
|
||||
|
||||
TURNS_ON_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
TURNS_OFF_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
BRIGHTNESS_CHANGED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Exclusive(CONF_LOWER, "brightness_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=255)
|
||||
),
|
||||
vol.Exclusive(CONF_UPPER, "brightness_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=255)
|
||||
),
|
||||
vol.Exclusive(CONF_ABOVE, "brightness_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=255)
|
||||
),
|
||||
vol.Exclusive(CONF_BELOW, "brightness_range"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=255)
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class LightTurnsOnTrigger(Trigger):
|
||||
"""Trigger for when a light turns on."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, TURNS_ON_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the light turns on trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when light turns on (from off to on)
|
||||
if from_state and from_state.state == STATE_OFF and to_state.state == STATE_ON:
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"light turned on on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class LightTurnsOffTrigger(Trigger):
|
||||
"""Trigger for when a light turns off."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, TURNS_OFF_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the light turns off trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when light turns off (from on to off)
|
||||
if from_state and from_state.state == STATE_ON and to_state.state == STATE_OFF:
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"light turned off on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class LightBrightnessChangedTrigger(Trigger):
|
||||
"""Trigger for when a light's brightness changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, BRIGHTNESS_CHANGED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the light brightness changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
self._options = config.options or {}
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
lower_limit = self._options.get(CONF_LOWER)
|
||||
upper_limit = self._options.get(CONF_UPPER)
|
||||
above_limit = self._options.get(CONF_ABOVE)
|
||||
below_limit = self._options.get(CONF_BELOW)
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Get brightness values
|
||||
from_brightness = (
|
||||
from_state.attributes.get(ATTR_BRIGHTNESS) if from_state else None
|
||||
)
|
||||
to_brightness = to_state.attributes.get(ATTR_BRIGHTNESS)
|
||||
|
||||
# Only trigger if brightness value exists and has changed
|
||||
if to_brightness is None or from_brightness == to_brightness:
|
||||
return
|
||||
|
||||
# Apply threshold filters if configured
|
||||
if lower_limit is not None and to_brightness < lower_limit:
|
||||
return
|
||||
if upper_limit is not None and to_brightness > upper_limit:
|
||||
return
|
||||
if above_limit is not None and to_brightness <= above_limit:
|
||||
return
|
||||
if below_limit is not None and to_brightness >= below_limit:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
"from_brightness": from_brightness,
|
||||
"to_brightness": to_brightness,
|
||||
},
|
||||
f"brightness changed on {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turns_on": LightTurnsOnTrigger,
|
||||
"turns_off": LightTurnsOffTrigger,
|
||||
"brightness_changed": LightBrightnessChangedTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for lights."""
|
||||
return TRIGGERS
|
||||
43
homeassistant/components/light/triggers.yaml
Normal file
43
homeassistant/components/light/triggers.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
turns_on:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
|
||||
turns_off:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
|
||||
brightness_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
fields:
|
||||
lower:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: box
|
||||
upper:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: box
|
||||
above:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: box
|
||||
below:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
mode: box
|
||||
@@ -367,5 +367,63 @@
|
||||
"name": "Turn up volume"
|
||||
}
|
||||
},
|
||||
"title": "Media player"
|
||||
"title": "Media player",
|
||||
"triggers": {
|
||||
"turns_on": {
|
||||
"description": "Triggers when a media player turns on.",
|
||||
"description_configured": "Triggers when a media player turns on",
|
||||
"name": "When a media player turns on"
|
||||
},
|
||||
"turns_off": {
|
||||
"description": "Triggers when a media player turns off.",
|
||||
"description_configured": "Triggers when a media player turns off",
|
||||
"name": "When a media player turns off"
|
||||
},
|
||||
"playing": {
|
||||
"description": "Triggers when a media player starts playing.",
|
||||
"description_configured": "Triggers when a media player starts playing",
|
||||
"fields": {
|
||||
"media_content_type": {
|
||||
"description": "The media content types to trigger on. If empty, triggers on all content types.",
|
||||
"name": "Media content types"
|
||||
}
|
||||
},
|
||||
"name": "When a media player starts playing"
|
||||
},
|
||||
"paused": {
|
||||
"description": "Triggers when a media player pauses.",
|
||||
"description_configured": "Triggers when a media player pauses",
|
||||
"name": "When a media player pauses"
|
||||
},
|
||||
"stopped": {
|
||||
"description": "Triggers when a media player stops playing.",
|
||||
"description_configured": "Triggers when a media player stops playing",
|
||||
"name": "When a media player stops playing"
|
||||
},
|
||||
"muted": {
|
||||
"description": "Triggers when a media player gets muted.",
|
||||
"description_configured": "Triggers when a media player gets muted",
|
||||
"name": "When a media player gets muted"
|
||||
},
|
||||
"unmuted": {
|
||||
"description": "Triggers when a media player gets unmuted.",
|
||||
"description_configured": "Triggers when a media player gets unmuted",
|
||||
"name": "When a media player gets unmuted"
|
||||
},
|
||||
"volume_changed": {
|
||||
"description": "Triggers when a media player volume changes.",
|
||||
"description_configured": "Triggers when a media player volume changes",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when volume is above this level (0.0-1.0).",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when volume is below this level (0.0-1.0).",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "When a media player volume changes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
676
homeassistant/components/media_player/trigger.py
Normal file
676
homeassistant/components/media_player/trigger.py
Normal file
@@ -0,0 +1,676 @@
|
||||
"""Provides triggers for media players."""
|
||||
|
||||
from typing import TYPE_CHECKING, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.target import (
|
||||
TargetStateChangedData,
|
||||
async_track_target_selector_state_change_event,
|
||||
)
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
TURNS_ON_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
TURNS_OFF_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
PLAYING_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Optional(ATTR_MEDIA_CONTENT_TYPE, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
STOPPED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
MUTED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
UNMUTED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
VOLUME_CHANGED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {
|
||||
vol.Optional("above"): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)),
|
||||
vol.Optional("below"): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0)),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
PAUSED_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_OPTIONS, default={}): {},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerTurnsOnTrigger(Trigger):
|
||||
"""Trigger for when a media player turns on."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, TURNS_ON_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the media player turns on trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when turning on from off state
|
||||
if (
|
||||
from_state is not None
|
||||
and from_state.state == STATE_OFF
|
||||
and to_state.state != STATE_OFF
|
||||
):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"media player {entity_id} turned on",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerTurnsOffTrigger(Trigger):
|
||||
"""Trigger for when a media player turns off."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, TURNS_OFF_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the media player turns off trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when turning off
|
||||
if (
|
||||
from_state is not None
|
||||
and from_state.state != STATE_OFF
|
||||
and to_state.state == STATE_OFF
|
||||
):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"media player {entity_id} turned off",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerPlayingTrigger(Trigger):
|
||||
"""Trigger for when a media player starts playing."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, PLAYING_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the media player playing trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
media_content_types_filter = self._options[ATTR_MEDIA_CONTENT_TYPE]
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when starting to play
|
||||
if (
|
||||
from_state is not None
|
||||
and from_state.state != STATE_PLAYING
|
||||
and to_state.state == STATE_PLAYING
|
||||
):
|
||||
# If media_content_type filter is specified, check if it matches
|
||||
if media_content_types_filter:
|
||||
media_content_type = to_state.attributes.get(ATTR_MEDIA_CONTENT_TYPE)
|
||||
if media_content_type not in media_content_types_filter:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"media player {entity_id} started playing",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerPausedTrigger(Trigger):
|
||||
"""Trigger for when a media player pauses."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, PAUSED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the media player paused trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when pausing
|
||||
if (
|
||||
from_state is not None
|
||||
and from_state.state != STATE_PAUSED
|
||||
and to_state.state == STATE_PAUSED
|
||||
):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"media player {entity_id} paused",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerStoppedTrigger(Trigger):
|
||||
"""Trigger for when a media player stops playing."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, STOPPED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the media player stopped trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when stopping (to idle or off from playing/paused states)
|
||||
if (
|
||||
from_state is not None
|
||||
and from_state.state in (STATE_PLAYING, STATE_PAUSED)
|
||||
and to_state.state in (STATE_IDLE, STATE_OFF)
|
||||
):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"media player {entity_id} stopped",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerMutedTrigger(Trigger):
|
||||
"""Trigger for when a media player gets muted."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, MUTED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the media player muted trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when muting
|
||||
if (
|
||||
from_state is not None
|
||||
and not from_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED, False)
|
||||
and to_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED, False)
|
||||
):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"media player {entity_id} muted",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerUnmutedTrigger(Trigger):
|
||||
"""Trigger for when a media player gets unmuted."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, UNMUTED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the media player unmuted trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Trigger when unmuting
|
||||
if (
|
||||
from_state is not None
|
||||
and from_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED, False)
|
||||
and not to_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED, False)
|
||||
):
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"media player {entity_id} unmuted",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerVolumeChangedTrigger(Trigger):
|
||||
"""Trigger for when a media player volume changes."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, VOLUME_CHANGED_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the media player volume changed trigger."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.options is not None
|
||||
assert config.target is not None
|
||||
self._options = config.options
|
||||
self._target = config.target
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach the trigger to an action runner."""
|
||||
above_threshold = self._options.get("above")
|
||||
below_threshold = self._options.get("below")
|
||||
|
||||
@callback
|
||||
def state_change_listener(
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Listen for state changes and call action."""
|
||||
event = target_state_change_data.state_change_event
|
||||
entity_id = event.data["entity_id"]
|
||||
from_state = event.data["old_state"]
|
||||
to_state = event.data["new_state"]
|
||||
|
||||
# Ignore unavailable states
|
||||
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||
return
|
||||
|
||||
# Get volume levels
|
||||
old_volume = (
|
||||
from_state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL)
|
||||
if from_state is not None
|
||||
else None
|
||||
)
|
||||
new_volume = to_state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
# Volume must have changed
|
||||
if old_volume == new_volume or new_volume is None:
|
||||
return
|
||||
|
||||
# Check thresholds if specified
|
||||
if above_threshold is not None and new_volume <= above_threshold:
|
||||
return
|
||||
|
||||
if below_threshold is not None and new_volume >= below_threshold:
|
||||
return
|
||||
|
||||
run_action(
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
},
|
||||
f"media player {entity_id} volume changed",
|
||||
event.context,
|
||||
)
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain."""
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
return async_track_target_selector_state_change_event(
|
||||
self._hass, self._target, state_change_listener, entity_filter
|
||||
)
|
||||
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turns_on": MediaPlayerTurnsOnTrigger,
|
||||
"turns_off": MediaPlayerTurnsOffTrigger,
|
||||
"playing": MediaPlayerPlayingTrigger,
|
||||
"paused": MediaPlayerPausedTrigger,
|
||||
"stopped": MediaPlayerStoppedTrigger,
|
||||
"muted": MediaPlayerMutedTrigger,
|
||||
"unmuted": MediaPlayerUnmutedTrigger,
|
||||
"volume_changed": MediaPlayerVolumeChangedTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for media players."""
|
||||
return TRIGGERS
|
||||
65
homeassistant/components/media_player/triggers.yaml
Normal file
65
homeassistant/components/media_player/triggers.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
turns_on:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
|
||||
turns_off:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
|
||||
playing:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
fields:
|
||||
media_content_type:
|
||||
required: false
|
||||
default: []
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
custom_value: true
|
||||
options: []
|
||||
|
||||
paused:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
|
||||
stopped:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
|
||||
muted:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
|
||||
unmuted:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
|
||||
volume_changed:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
fields:
|
||||
above:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0.0
|
||||
max: 1.0
|
||||
step: 0.01
|
||||
mode: slider
|
||||
below:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0.0
|
||||
max: 1.0
|
||||
step: 0.01
|
||||
mode: slider
|
||||
600
tests/components/alarm_control_panel/test_trigger.py
Normal file
600
tests/components/alarm_control_panel/test_trigger.py
Normal file
@@ -0,0 +1,600 @@
|
||||
"""Test alarm control panel trigger."""
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.alarm_control_panel.const import AlarmControlPanelState
|
||||
from homeassistant.const import CONF_ENTITY_ID, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_alarm_armed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm armed trigger fires when an alarm is armed."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.armed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Trigger armed home
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Trigger armed away
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_AWAY)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Trigger armed night
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_NIGHT)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Trigger armed vacation
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_VACATION)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Trigger armed custom bypass
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_CUSTOM_BYPASS)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_alarm_armed_trigger_with_mode_filter(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm armed trigger with mode filter."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.armed",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: entity_id,
|
||||
},
|
||||
"options": {
|
||||
"mode": [
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
],
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Trigger matching armed home mode
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Trigger matching armed away mode
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_AWAY)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Trigger non-matching armed night mode - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_NIGHT)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Trigger non-matching armed vacation mode - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_VACATION)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_alarm_armed_trigger_ignores_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm armed trigger ignores unavailable states."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.armed",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: entity_id,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to unavailable - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Trigger armed after unavailable - should trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_alarm_armed_trigger_ignores_non_armed_states(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm armed trigger ignores non-armed states."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.armed",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: entity_id,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to disarmed - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Set to triggered - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.TRIGGERED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Set to arming - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMING)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Set to pending - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.PENDING)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Set to disarming - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMING)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_alarm_armed_trigger_from_unknown_state(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the trigger fires when entity goes from unknown/None to first armed state."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Do NOT set any initial state - entity starts with None state
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.armed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# First armed state should trigger even though entity had no previous state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_alarm_disarmed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm disarmed trigger fires when an alarm is disarmed."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.disarmed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Trigger disarmed
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_alarm_disarmed_trigger_ignores_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm disarmed trigger ignores unavailable states."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.disarmed",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: entity_id,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to unavailable - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Trigger disarmed after unavailable - should trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_alarm_disarmed_trigger_ignores_non_disarmed_states(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm disarmed trigger ignores non-disarmed states."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.disarmed",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: entity_id,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to armed home - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Set to triggered - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.TRIGGERED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Set to arming - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMING)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_alarm_disarmed_trigger_from_unknown_state(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the trigger fires when entity goes from unknown/None to disarmed state."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Do NOT set any initial state - entity starts with None state
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.disarmed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# First disarmed state should trigger even though entity had no previous state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_alarm_triggered_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm triggered trigger fires when an alarm is triggered."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.triggered",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Trigger alarm triggered
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.TRIGGERED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_alarm_triggered_trigger_ignores_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm triggered trigger ignores unavailable states."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.triggered",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: entity_id,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to unavailable - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Trigger alarm triggered after unavailable - should trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.TRIGGERED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_alarm_triggered_trigger_ignores_non_triggered_states(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the alarm triggered trigger ignores non-triggered states."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Set initial state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.triggered",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: entity_id,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to armed home - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMED_HOME)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Set to disarmed - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Set to arming - should not trigger
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.ARMING)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_alarm_triggered_trigger_from_unknown_state(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the trigger fires when entity goes from unknown/None to triggered state."""
|
||||
entity_id = "alarm_control_panel.test_alarm"
|
||||
await async_setup_component(hass, "alarm_control_panel", {})
|
||||
|
||||
# Do NOT set any initial state - entity starts with None state
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "alarm_control_panel.triggered",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# First triggered state should trigger even though entity had no previous state
|
||||
hass.states.async_set(entity_id, AlarmControlPanelState.TRIGGERED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
219
tests/components/homeassistant/triggers/test_assist_satellite.py
Normal file
219
tests/components/homeassistant/triggers/test_assist_satellite.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""The tests for the Assist satellite automation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_TARGET
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import mock_component
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_comp(hass: HomeAssistant) -> None:
|
||||
"""Initialize components."""
|
||||
mock_component(hass, "group")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "state"),
|
||||
[
|
||||
("assist_satellite.listening", "listening"),
|
||||
("assist_satellite.processing", "processing"),
|
||||
("assist_satellite.responding", "responding"),
|
||||
("assist_satellite.idle", "idle"),
|
||||
],
|
||||
)
|
||||
async def test_trigger_fires_on_state_change(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
trigger: str,
|
||||
state: str,
|
||||
) -> None:
|
||||
"""Test that the trigger fires when satellite changes to the specified state."""
|
||||
hass.states.async_set("assist_satellite.satellite_1", "idle")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: trigger,
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "assist_satellite.satellite_1"},
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {"id": "{{ trigger.id}}"},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("assist_satellite.satellite_1", state, context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_listening_does_not_fire_on_other_states(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that listening trigger does not fire on other states."""
|
||||
hass.states.async_set("assist_satellite.satellite_1", "idle")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "assist_satellite.listening",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "assist_satellite.satellite_1"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("assist_satellite.satellite_1", "processing")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
hass.states.async_set("assist_satellite.satellite_1", "responding")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
hass.states.async_set("assist_satellite.satellite_1", "idle")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_does_not_fire_on_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that trigger does not fire when state becomes unavailable."""
|
||||
hass.states.async_set("assist_satellite.satellite_1", "idle")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "assist_satellite.listening",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "assist_satellite.satellite_1"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("assist_satellite.satellite_1", "unavailable")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_fires_with_multiple_entities(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the firing with multiple entities."""
|
||||
hass.states.async_set("assist_satellite.satellite_1", "idle")
|
||||
hass.states.async_set("assist_satellite.satellite_2", "idle")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "assist_satellite.listening",
|
||||
CONF_TARGET: {
|
||||
CONF_ENTITY_ID: [
|
||||
"assist_satellite.satellite_1",
|
||||
"assist_satellite.satellite_2",
|
||||
]
|
||||
},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("assist_satellite.satellite_1", "listening")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
hass.states.async_set("assist_satellite.satellite_2", "listening")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 2
|
||||
|
||||
|
||||
async def test_trigger_data_available(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that trigger data is available in action."""
|
||||
hass.states.async_set("assist_satellite.satellite_1", "idle")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "assist_satellite.listening",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "assist_satellite.satellite_1"},
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"entity_id": "{{ trigger.entity_id }}",
|
||||
"from_state": "{{ trigger.from_state.state }}",
|
||||
"to_state": "{{ trigger.to_state.state }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("assist_satellite.satellite_1", "listening")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data["entity_id"] == "assist_satellite.satellite_1"
|
||||
assert service_calls[0].data["from_state"] == "idle"
|
||||
assert service_calls[0].data["to_state"] == "listening"
|
||||
|
||||
|
||||
async def test_idle_trigger_fires_when_returning_to_idle(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that idle trigger fires when satellite returns to idle."""
|
||||
hass.states.async_set("assist_satellite.satellite_1", "listening")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "assist_satellite.idle",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "assist_satellite.satellite_1"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Go to processing state first
|
||||
hass.states.async_set("assist_satellite.satellite_1", "processing")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Go back to idle
|
||||
hass.states.async_set("assist_satellite.satellite_1", "idle", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
416
tests/components/homeassistant/triggers/test_climate.py
Normal file
416
tests/components/homeassistant/triggers/test_climate.py
Normal file
@@ -0,0 +1,416 @@
|
||||
"""The tests for the Climate automation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HUMIDITY,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_TARGET,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import mock_component
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_comp(hass: HomeAssistant) -> None:
|
||||
"""Initialize components."""
|
||||
mock_component(hass, "group")
|
||||
|
||||
|
||||
async def test_turns_on_trigger_fires_when_turning_on(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that turns_on trigger fires when climate turns on."""
|
||||
hass.states.async_set("climate.test", "off")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.turns_on",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "heat", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_turns_on_trigger_does_not_fire_when_already_on(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that turns_on trigger does not fire when already on."""
|
||||
hass.states.async_set("climate.test", "heat")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.turns_on",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "cool")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_turns_off_trigger_fires_when_turning_off(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that turns_off trigger fires when climate turns off."""
|
||||
hass.states.async_set("climate.test", "heat")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.turns_off",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "off", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_mode_changed_trigger_fires_on_mode_change(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that mode_changed trigger fires when mode changes."""
|
||||
hass.states.async_set("climate.test", "heat")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.mode_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "cool", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_mode_changed_trigger_filters_by_hvac_mode(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that mode_changed trigger filters by hvac_mode."""
|
||||
hass.states.async_set("climate.test", "off")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.mode_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
"hvac_mode": ["heat", "cool"],
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should trigger for heat
|
||||
hass.states.async_set("climate.test", "heat")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
# Should trigger for cool
|
||||
hass.states.async_set("climate.test", "cool")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 2
|
||||
|
||||
# Should not trigger for auto
|
||||
hass.states.async_set("climate.test", "auto")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 2
|
||||
|
||||
|
||||
async def test_cooling_trigger_fires_when_cooling_starts(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that cooling trigger fires when climate starts cooling."""
|
||||
hass.states.async_set("climate.test", "cool", {ATTR_HVAC_ACTION: "idle"})
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.cooling",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "cool", {ATTR_HVAC_ACTION: "cooling"}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_heating_trigger_fires_when_heating_starts(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that heating trigger fires when climate starts heating."""
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_HVAC_ACTION: "idle"})
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.heating",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_HVAC_ACTION: "heating"}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_drying_trigger_fires_when_drying_starts(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that drying trigger fires when climate starts drying."""
|
||||
hass.states.async_set("climate.test", "dry", {ATTR_HVAC_ACTION: "idle"})
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.drying",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "dry", {ATTR_HVAC_ACTION: "drying"}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_target_temperature_changed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that target_temperature_changed trigger fires."""
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_TEMPERATURE: 20})
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.target_temperature_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_TEMPERATURE: 22}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_target_temperature_changed_trigger_with_above(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that target_temperature_changed trigger filters by above threshold."""
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_TEMPERATURE: 20})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.target_temperature_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
"above": 22,
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should not trigger at 21
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_TEMPERATURE: 21})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Should trigger at 23
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_TEMPERATURE: 23})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_current_temperature_changed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that current_temperature_changed trigger fires."""
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_CURRENT_TEMPERATURE: 20})
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.current_temperature_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "heat", {ATTR_CURRENT_TEMPERATURE: 21}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_target_humidity_changed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that target_humidity_changed trigger fires."""
|
||||
hass.states.async_set("climate.test", "dry", {ATTR_HUMIDITY: 50})
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.target_humidity_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "dry", {ATTR_HUMIDITY: 60}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_current_humidity_changed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that current_humidity_changed trigger fires."""
|
||||
hass.states.async_set("climate.test", "dry", {ATTR_CURRENT_HUMIDITY: 50})
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.current_humidity_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "dry", {ATTR_CURRENT_HUMIDITY: 55}, context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_does_not_fire_on_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that trigger does not fire when state becomes unavailable."""
|
||||
hass.states.async_set("climate.test", "heat")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "climate.turns_off",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "climate.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("climate.test", "unavailable")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
591
tests/components/homeassistant/triggers/test_cover.py
Normal file
591
tests/components/homeassistant/triggers/test_cover.py
Normal file
@@ -0,0 +1,591 @@
|
||||
"""The tests for the Cover automation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.cover import ATTR_CURRENT_POSITION
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_TARGET
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import mock_component
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_comp(hass: HomeAssistant) -> None:
|
||||
"""Initialize components."""
|
||||
mock_component(hass, "group")
|
||||
|
||||
|
||||
async def test_opens_trigger_fires_on_opening(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that opens trigger fires when cover starts opening."""
|
||||
hass.states.async_set("cover.test", "closed")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.opens",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "opening", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_opens_trigger_fires_on_open(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that opens trigger fires when cover becomes open."""
|
||||
hass.states.async_set("cover.test", "closed")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.opens",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "open", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_opens_trigger_fully_opened_option(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that opens trigger with fully_opened only fires at position 100."""
|
||||
hass.states.async_set("cover.test", "closed", {ATTR_CURRENT_POSITION: 0})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.opens",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
"fully_opened": True,
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should not trigger at position 50
|
||||
hass.states.async_set("cover.test", "opening", {ATTR_CURRENT_POSITION: 50})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Should trigger at position 100
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_opens_trigger_device_class_filter(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that opens trigger filters by device class."""
|
||||
hass.states.async_set("cover.curtain", "closed", {"device_class": "curtain"})
|
||||
hass.states.async_set("cover.garage", "closed", {"device_class": "garage"})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.opens",
|
||||
CONF_TARGET: {
|
||||
CONF_ENTITY_ID: ["cover.curtain", "cover.garage"]
|
||||
},
|
||||
"device_class": ["curtain"],
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should trigger for curtain
|
||||
hass.states.async_set("cover.curtain", "opening", {"device_class": "curtain"})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
# Should not trigger for garage
|
||||
hass.states.async_set("cover.garage", "opening", {"device_class": "garage"})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_does_not_fire_on_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that trigger does not fire when state becomes unavailable."""
|
||||
hass.states.async_set("cover.test", "closed")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.opens",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "unavailable")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_trigger_data_available(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that trigger data is available in action."""
|
||||
hass.states.async_set("cover.test", "closed")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.opens",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"entity_id": "{{ trigger.entity_id }}",
|
||||
"from_state": "{{ trigger.from_state.state }}",
|
||||
"to_state": "{{ trigger.to_state.state }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "opening")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data["entity_id"] == "cover.test"
|
||||
assert service_calls[0].data["from_state"] == "closed"
|
||||
assert service_calls[0].data["to_state"] == "opening"
|
||||
|
||||
|
||||
async def test_fires_with_multiple_entities(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the firing with multiple entities."""
|
||||
hass.states.async_set("cover.test1", "closed")
|
||||
hass.states.async_set("cover.test2", "closed")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.opens",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: ["cover.test1", "cover.test2"]},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test1", "opening")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
hass.states.async_set("cover.test2", "opening")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 2
|
||||
|
||||
|
||||
async def test_closes_trigger_fires_on_closing(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that closes trigger fires when cover starts closing."""
|
||||
hass.states.async_set("cover.test", "open")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.closes",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "closing", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_closes_trigger_fires_on_closed(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that closes trigger fires when cover becomes closed."""
|
||||
hass.states.async_set("cover.test", "open")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.closes",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "closed", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_closes_trigger_fully_closed_option(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that closes trigger with fully_closed only fires at position 0."""
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 100})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.closes",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
"fully_closed": True,
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should not trigger at position 50
|
||||
hass.states.async_set("cover.test", "closing", {ATTR_CURRENT_POSITION: 50})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Should trigger at position 0
|
||||
hass.states.async_set("cover.test", "closed", {ATTR_CURRENT_POSITION: 0})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_stops_trigger_fires_from_opening_to_open(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that stops trigger fires when cover stops from opening."""
|
||||
hass.states.async_set("cover.test", "opening")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.stops",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "open", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_stops_trigger_fires_from_closing_to_closed(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that stops trigger fires when cover stops from closing."""
|
||||
hass.states.async_set("cover.test", "closing")
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.stops",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "closed", context=context)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_stops_trigger_does_not_fire_on_other_changes(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that stops trigger does not fire on other state changes."""
|
||||
hass.states.async_set("cover.test", "closed")
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.stops",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should not trigger when going from closed to opening
|
||||
hass.states.async_set("cover.test", "opening")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Should not trigger when going from open to closing
|
||||
hass.states.async_set("cover.test", "open")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("cover.test", "closing")
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_position_changed_trigger_fires(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that position_changed trigger fires when position changes."""
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 100})
|
||||
context = Context()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.position_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set(
|
||||
"cover.test", "open", {ATTR_CURRENT_POSITION: 75}, context=context
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].context.parent_id == context.id
|
||||
|
||||
|
||||
async def test_position_changed_trigger_with_lower_limit(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test position_changed trigger with lower limit."""
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 100})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.position_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
"lower": 50,
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should trigger at position 75 (>= 50)
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 75})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
# Should not trigger at position 25 (< 50)
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 25})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_position_changed_trigger_with_upper_limit(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test position_changed trigger with upper limit."""
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 0})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.position_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
"upper": 50,
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should trigger at position 25 (<= 50)
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 25})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
# Should not trigger at position 75 (> 50)
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 75})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_position_changed_trigger_with_above_limit(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test position_changed trigger with above limit."""
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 0})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.position_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
"above": 50,
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should trigger at position 75 (> 50)
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 75})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
# Should not trigger at position 50 (= 50)
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 50})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_position_changed_trigger_with_below_limit(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test position_changed trigger with below limit."""
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 100})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.position_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
"below": 50,
|
||||
},
|
||||
"action": {"service": "test.automation"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should trigger at position 25 (< 50)
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 25})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
# Should not trigger at position 50 (= 50)
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 50})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_position_trigger_data_includes_positions(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that position trigger data includes position values."""
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 100})
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "cover.position_changed",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "cover.test"},
|
||||
},
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"from_position": "{{ trigger.from_position }}",
|
||||
"to_position": "{{ trigger.to_position }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set("cover.test", "open", {ATTR_CURRENT_POSITION: 75})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data["from_position"] == "100"
|
||||
assert service_calls[0].data["to_position"] == "75"
|
||||
650
tests/components/light/test_trigger.py
Normal file
650
tests/components/light/test_trigger.py
Normal file
@@ -0,0 +1,650 @@
|
||||
"""Test light trigger."""
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_light_turns_on_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the light turns on trigger fires when a light turns on."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state to off
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.turns_on",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Turn light on - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Turn light on again while already on - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_light_turns_on_trigger_ignores_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the light turns on trigger ignores unavailable states."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state to off
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.turns_on",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to unavailable - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Turn light on after unavailable - should not trigger (from unavailable)
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Turn light off
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Turn light on after being off - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_light_turns_on_multiple_lights(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the turns on trigger with multiple lights."""
|
||||
entity_id_1 = "light.test_light_1"
|
||||
entity_id_2 = "light.test_light_2"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial states to off
|
||||
hass.states.async_set(entity_id_1, STATE_OFF)
|
||||
hass.states.async_set(entity_id_2, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.turns_on",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: [entity_id_1, entity_id_2],
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Turn first light on - should trigger
|
||||
hass.states.async_set(entity_id_1, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id_1
|
||||
service_calls.clear()
|
||||
|
||||
# Turn second light on - should trigger
|
||||
hass.states.async_set(entity_id_2, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id_2
|
||||
|
||||
|
||||
async def test_light_turns_off_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the light turns off trigger fires when a light turns off."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state to on
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.turns_off",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Turn light off - should trigger
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Turn light off again while already off - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_light_turns_off_trigger_ignores_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the light turns off trigger ignores unavailable states."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state to on
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.turns_off",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to unavailable - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Turn light off after unavailable - should not trigger (from unavailable)
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Turn light on
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Turn light off after being on - should trigger
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the brightness changed trigger fires when brightness changes."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
"from_brightness": "{{ trigger.from_brightness }}",
|
||||
"to_brightness": "{{ trigger.to_brightness }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change brightness - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 150})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
assert service_calls[0].data["from_brightness"] == "100"
|
||||
assert service_calls[0].data["to_brightness"] == "150"
|
||||
service_calls.clear()
|
||||
|
||||
# Change brightness again - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 200})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data["from_brightness"] == "150"
|
||||
assert service_calls[0].data["to_brightness"] == "200"
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_same_brightness(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the brightness changed trigger does not fire when brightness is the same."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Update state but keep brightness the same - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_with_lower_limit(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the brightness changed trigger with lower limit."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 50})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"lower": 100,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change to brightness below lower limit - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 75})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change to brightness at lower limit - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# Change to brightness above lower limit - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 150})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_with_upper_limit(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the brightness changed trigger with upper limit."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 200})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"upper": 150,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change to brightness above upper limit - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 180})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change to brightness at upper limit - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 150})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# Change to brightness below upper limit - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_with_above(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the brightness changed trigger with above threshold."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 50})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"above": 100,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change to brightness at threshold - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change to brightness above threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 101})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# Change to brightness well above threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 200})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_with_below(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test the brightness changed trigger with below threshold."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 200})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"below": 100,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change to brightness at threshold - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change to brightness below threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 99})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
service_calls.clear()
|
||||
|
||||
# Change to brightness well below threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 50})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_ignores_unavailable(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the brightness changed trigger ignores unavailable states."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state with brightness
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Set to unavailable - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
# Change brightness after unavailable - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 150})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_from_no_brightness(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the trigger fires when brightness is added."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state without brightness (on/off only light)
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
"from_brightness": "{{ trigger.from_brightness }}",
|
||||
"to_brightness": "{{ trigger.to_brightness }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Add brightness attribute - should trigger
|
||||
hass.states.async_set(entity_id, STATE_ON, {"brightness": 100})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data["from_brightness"] == "None"
|
||||
assert service_calls[0].data["to_brightness"] == "100"
|
||||
|
||||
|
||||
async def test_light_brightness_changed_trigger_no_brightness(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the trigger does not fire when brightness is not present."""
|
||||
entity_id = "light.test_light"
|
||||
await async_setup_component(hass, "light", {})
|
||||
|
||||
# Set initial state without brightness
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "light.brightness_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Turn light off and on without brightness - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
536
tests/components/media_player/test_trigger.py
Normal file
536
tests/components/media_player/test_trigger.py
Normal file
@@ -0,0 +1,536 @@
|
||||
"""Test media_player trigger."""
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_TYPE
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_turns_on_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the turns on trigger fires when a media player turns on."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state to off
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.turns_on",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Turn on - should trigger
|
||||
hass.states.async_set(entity_id, STATE_IDLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Already on, change to playing - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_turns_off_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the turns off trigger fires when a media player turns off."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state to playing
|
||||
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.turns_off",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Turn off - should trigger
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Already off - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_playing_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the playing trigger fires when a media player starts playing."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state to idle
|
||||
hass.states.async_set(entity_id, STATE_IDLE)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.playing",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Start playing - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Pause then play again - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PAUSED)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_playing_trigger_with_media_content_type_filter(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the playing trigger with media content type filter."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state to idle
|
||||
hass.states.async_set(entity_id, STATE_IDLE)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.playing",
|
||||
"target": {
|
||||
CONF_ENTITY_ID: entity_id,
|
||||
},
|
||||
"options": {
|
||||
"media_content_type": ["music", "video"],
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Start playing music - should trigger
|
||||
hass.states.async_set(
|
||||
entity_id, STATE_PLAYING, {ATTR_MEDIA_CONTENT_TYPE: "music"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Stop and play video - should trigger
|
||||
hass.states.async_set(entity_id, STATE_IDLE)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set(
|
||||
entity_id, STATE_PLAYING, {ATTR_MEDIA_CONTENT_TYPE: "video"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Stop and play podcast - should not trigger (not in filter)
|
||||
hass.states.async_set(entity_id, STATE_IDLE)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set(
|
||||
entity_id, STATE_PLAYING, {ATTR_MEDIA_CONTENT_TYPE: "podcast"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_paused_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the paused trigger fires when a media player pauses."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state to playing
|
||||
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.paused",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Pause - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PAUSED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Already paused - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_PAUSED)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_stopped_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the stopped trigger fires when a media player stops playing."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state to playing
|
||||
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.stopped",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Stop to idle - should trigger
|
||||
hass.states.async_set(entity_id, STATE_IDLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Start playing again and then stop to off - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set(entity_id, STATE_OFF)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Stop from paused - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set(entity_id, STATE_PAUSED)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set(entity_id, STATE_IDLE)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
|
||||
|
||||
async def test_muted_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the muted trigger fires when a media player gets muted."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state with volume unmuted
|
||||
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_MUTED
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_MUTED: False})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.muted",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Mute - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_MUTED: True})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Already muted - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_MUTED: True})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_unmuted_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the unmuted trigger fires when a media player gets unmuted."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state with volume muted
|
||||
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_MUTED
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_MUTED: True})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.unmuted",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Unmute - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_MUTED: False})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Already unmuted - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_MUTED: False})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_volume_changed_trigger(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the volume changed trigger fires when volume changes."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state with volume
|
||||
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_LEVEL
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.5})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.volume_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change volume - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.7})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Same volume - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.7})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_volume_changed_trigger_with_above_threshold(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the volume changed trigger with above threshold."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state with volume
|
||||
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_LEVEL
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.3})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.volume_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"above": 0.5,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change volume above threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.7})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Change volume but still below threshold - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.4})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
async def test_volume_changed_trigger_with_below_threshold(
|
||||
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||
) -> None:
|
||||
"""Test that the volume changed trigger with below threshold."""
|
||||
entity_id = "media_player.test"
|
||||
await async_setup_component(hass, "media_player", {})
|
||||
|
||||
# Set initial state with volume
|
||||
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_LEVEL
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.7})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
{
|
||||
automation.DOMAIN: {
|
||||
"triggers": {
|
||||
"trigger": "media_player.volume_changed",
|
||||
"target": {CONF_ENTITY_ID: entity_id},
|
||||
"options": {
|
||||
"below": 0.5,
|
||||
},
|
||||
},
|
||||
"actions": {
|
||||
"action": "test.automation",
|
||||
"data": {
|
||||
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Change volume below threshold - should trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.3})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 1
|
||||
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Change volume but still above threshold - should not trigger
|
||||
hass.states.async_set(entity_id, STATE_PLAYING, {ATTR_MEDIA_VOLUME_LEVEL: 0.6})
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
Reference in New Issue
Block a user