mirror of
https://github.com/home-assistant/core.git
synced 2026-09-02 02:35:09 -05:00
Add trigger media_player.unmuted (#169797)
This commit is contained in:
@@ -140,6 +140,9 @@
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:power"
|
||||
},
|
||||
"unmuted": {
|
||||
"trigger": "mdi:volume-high"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +508,18 @@
|
||||
}
|
||||
},
|
||||
"name": "Media player turned on"
|
||||
},
|
||||
"unmuted": {
|
||||
"description": "Triggers after one or more media players are unmuted.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::media_player::common::trigger_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Media player unmuted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ from . import ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, MediaPlayerState
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class MediaPlayerMutedTrigger(EntityTriggerBase):
|
||||
"""Class for media player muted triggers."""
|
||||
class _MediaPlayerMutedStateTriggerBase(EntityTriggerBase):
|
||||
"""Base class for media player muted/unmuted triggers."""
|
||||
|
||||
_domain_specs = {DOMAIN: DomainSpec()}
|
||||
_target_muted: bool
|
||||
|
||||
def _has_volume_attributes(self, state: State) -> bool:
|
||||
"""Check if the state has volume muted or volume level attributes."""
|
||||
@@ -75,11 +76,24 @@ class MediaPlayerMutedTrigger(EntityTriggerBase):
|
||||
"""Check if the new state matches the expected state."""
|
||||
if not self._has_volume_attributes(state):
|
||||
return False
|
||||
return self.is_muted(state)
|
||||
return self.is_muted(state) is self._target_muted
|
||||
|
||||
|
||||
class MediaPlayerMutedTrigger(_MediaPlayerMutedStateTriggerBase):
|
||||
"""Class for media player muted triggers."""
|
||||
|
||||
_target_muted = True
|
||||
|
||||
|
||||
class MediaPlayerUnmutedTrigger(_MediaPlayerMutedStateTriggerBase):
|
||||
"""Class for media player unmuted triggers."""
|
||||
|
||||
_target_muted = False
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"muted": MediaPlayerMutedTrigger,
|
||||
"unmuted": MediaPlayerUnmutedTrigger,
|
||||
"paused_playing": make_entity_transition_trigger(
|
||||
DOMAIN,
|
||||
from_states={
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
duration:
|
||||
|
||||
muted: *trigger_common
|
||||
unmuted: *trigger_common
|
||||
paused_playing: *trigger_common
|
||||
started_playing: *trigger_common
|
||||
stopped_playing: *trigger_common
|
||||
|
||||
@@ -36,6 +36,7 @@ async def target_media_players(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"trigger_key",
|
||||
[
|
||||
"media_player.muted",
|
||||
"media_player.unmuted",
|
||||
"media_player.paused_playing",
|
||||
"media_player.started_playing",
|
||||
"media_player.stopped_playing",
|
||||
@@ -103,6 +104,7 @@ def parametrize_muted_trigger_states() -> list[
|
||||
("trigger_key", "base_options", "supports_behavior", "supports_duration"),
|
||||
[
|
||||
("media_player.muted", {}, True, True),
|
||||
("media_player.unmuted", {}, True, True),
|
||||
("media_player.paused_playing", {}, True, True),
|
||||
("media_player.started_playing", {}, True, True),
|
||||
("media_player.stopped_playing", {}, True, True),
|
||||
@@ -385,47 +387,56 @@ async def test_muted_trigger_fires_when_entity_gains_volume_attributes(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "initial_muted", "target_muted"),
|
||||
[
|
||||
("media_player.muted", False, True),
|
||||
("media_player.unmuted", True, False),
|
||||
],
|
||||
)
|
||||
async def test_muted_trigger_last_skips_entities_without_volume_attributes(
|
||||
hass: HomeAssistant,
|
||||
trigger: str,
|
||||
initial_muted: bool,
|
||||
target_muted: bool,
|
||||
) -> None:
|
||||
"""Test that 'last' behavior skips entities without volume attributes.
|
||||
|
||||
With entities a (has volume), b (has volume), c (no volume):
|
||||
The trigger should fire when both a and b are muted, regardless of c.
|
||||
The trigger should fire when both a and b transition, regardless of c.
|
||||
"""
|
||||
entity_a = "media_player.with_volume_a"
|
||||
entity_b = "media_player.with_volume_b"
|
||||
entity_c = "media_player.no_volume"
|
||||
calls: list[str] = []
|
||||
|
||||
# Set initial states
|
||||
hass.states.async_set(
|
||||
entity_a, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: False}
|
||||
entity_a, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: initial_muted}
|
||||
)
|
||||
hass.states.async_set(
|
||||
entity_b, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: False}
|
||||
entity_b, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: initial_muted}
|
||||
)
|
||||
hass.states.async_set(entity_c, MediaPlayerState.PLAYING, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
"media_player.muted",
|
||||
trigger,
|
||||
{"behavior": "last"},
|
||||
{CONF_ENTITY_ID: [entity_a, entity_b, entity_c]},
|
||||
calls,
|
||||
)
|
||||
|
||||
# Mute entity a — not all mutable entities muted yet
|
||||
# Transition entity a — not all mutable entities transitioned yet
|
||||
hass.states.async_set(
|
||||
entity_a, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: True}
|
||||
entity_a, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: target_muted}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
# Mute entity b — now all mutable entities are muted, trigger fires
|
||||
# Transition entity b — now all mutable entities have transitioned, fires
|
||||
hass.states.async_set(
|
||||
entity_b, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: True}
|
||||
entity_b, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: target_muted}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
@@ -458,3 +469,57 @@ async def test_muted_trigger_does_not_fire_on_losing_volume_attributes(
|
||||
hass.states.async_set(entity_id, MediaPlayerState.PLAYING, {})
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "initial_muted", "target_muted"),
|
||||
[
|
||||
("media_player.muted", False, True),
|
||||
("media_player.unmuted", True, False),
|
||||
],
|
||||
)
|
||||
async def test_muted_trigger_first_skips_entities_without_volume_attributes(
|
||||
hass: HomeAssistant,
|
||||
trigger: str,
|
||||
initial_muted: bool,
|
||||
target_muted: bool,
|
||||
) -> None:
|
||||
"""Test that 'first' behavior skips entities without volume attributes."""
|
||||
entity_a = "media_player.with_volume_a"
|
||||
entity_b = "media_player.with_volume_b"
|
||||
entity_c = "media_player.no_volume"
|
||||
calls: list[str] = []
|
||||
|
||||
hass.states.async_set(
|
||||
entity_a, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: initial_muted}
|
||||
)
|
||||
hass.states.async_set(
|
||||
entity_b, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: initial_muted}
|
||||
)
|
||||
hass.states.async_set(entity_c, MediaPlayerState.PLAYING, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(
|
||||
hass,
|
||||
trigger,
|
||||
{"behavior": "first"},
|
||||
{CONF_ENTITY_ID: [entity_a, entity_b, entity_c]},
|
||||
calls,
|
||||
)
|
||||
|
||||
# Transition entity a — first mutable entity transitions, fires
|
||||
hass.states.async_set(
|
||||
entity_a, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: target_muted}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 1
|
||||
assert calls[0] == entity_a
|
||||
calls.clear()
|
||||
|
||||
# Transition entity b — first behavior already armed, does not fire again
|
||||
hass.states.async_set(
|
||||
entity_b, MediaPlayerState.PLAYING, {ATTR_MEDIA_VOLUME_MUTED: target_muted}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(calls) == 0
|
||||
|
||||
Reference in New Issue
Block a user