Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
be4f492
Bump satel_integra to 1.3.1 (#169889)
Tommatheussen May 6, 2026
1e9244f
Update frontend to 20260429.3 (#169893)
bramkragten May 6, 2026
cf8bc55
Add media_player muted conditions (#169892)
emontnemery May 6, 2026
824f520
Record notification from legacy notify action in Mobile App (#169749)
tr4nt0r May 6, 2026
52e1d94
Exclude incompatible entities from humidity automations (#169898)
emontnemery May 6, 2026
5f1201d
Exclude incompatible entities from temperature automations (#169901)
emontnemery May 6, 2026
26714c6
Add media_player volume condition (#169897)
emontnemery May 6, 2026
923e099
Unload scripts and conditions created by template entities (#169366)
emontnemery May 6, 2026
7f447ab
Exclude incompatible climate entities from climate automations (#169903)
emontnemery May 6, 2026
bbe00ef
De-duplicate code to build Tuya device info (#169899)
epenet May 6, 2026
059d701
Exclude incompatible water_heater entities from water_heater automati…
emontnemery May 6, 2026
b6096a7
Exclude incompatible humidifier entities from humidifier automations …
emontnemery May 6, 2026
b0eb699
Bump pyTibber to 0.37.4 (#169907)
Danielhiversen May 6, 2026
19aa1b6
Remove advanced options dependency from MQTT integration (#169833)
jbouwh May 6, 2026
cef918d
Remove _get_tracked_value method from EntityConditionBase (#169906)
emontnemery May 6, 2026
3be5906
Register Hive Hub MAC address as device connection (#169040)
tomwilkie May 6, 2026
13dd831
Update gardena ble to 2.8.1 (#169914)
elupus May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions homeassistant/components/climate/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter

def _should_include(self, state: State) -> bool:
"""Skip climate entities that do not expose a target temperature."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_TEMPERATURE) is not None
)

def _get_entity_unit(self, entity_state: State) -> str | None:
"""Get the temperature unit of a climate entity from its state."""
# Climate entities convert temperatures to the system unit via show_temp
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/climate/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitB
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter

def _should_include(self, state: State) -> bool:
"""Skip climate entities that do not expose a target temperature."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_TEMPERATURE) is not None
)

def _get_entity_unit(self, state: State) -> str | None:
"""Get the temperature unit of a climate entity from its state."""
# Climate entities convert temperatures to the system unit via show_temp
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260429.2"]
"requirements": ["home-assistant-frontend==20260429.3"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/gardena_bluetooth/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
"requirements": ["gardena-bluetooth==2.4.0"]
"requirements": ["gardena-bluetooth==2.8.1"]
}
16 changes: 11 additions & 5 deletions homeassistant/components/hive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> bool
except HiveReauthRequired as err:
raise ConfigEntryAuthFailed from err

hub_data = devices["parent"][0]
connections: set[tuple[str, str]] = set()
if mac := hub_data.get("macAddress"):
connections.add((dr.CONNECTION_NETWORK_MAC, dr.format_mac(mac)))

device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, devices["parent"][0]["device_id"])},
name=devices["parent"][0]["hiveName"],
model=devices["parent"][0]["deviceData"]["model"],
sw_version=devices["parent"][0]["deviceData"]["version"],
manufacturer=devices["parent"][0]["deviceData"]["manufacturer"],
identifiers={(DOMAIN, hub_data["device_id"])},
connections=connections,
name=hub_data["hiveName"],
model=hub_data["deviceData"]["model"],
sw_version=hub_data["deviceData"]["version"],
manufacturer=hub_data["deviceData"]["manufacturer"],
)

await hass.config_entries.async_forward_entry_setups(
Expand Down
23 changes: 17 additions & 6 deletions homeassistant/components/humidifier/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
import voluptuous as vol

from homeassistant.const import ATTR_MODE, CONF_OPTIONS, PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
Condition,
ConditionConfig,
EntityNumericalConditionBase,
EntityStateConditionBase,
make_entity_numerical_condition,
make_entity_state_condition,
)
from homeassistant.helpers.entity import get_supported_features
Expand Down Expand Up @@ -46,6 +46,20 @@ def _supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> boo
return False


class IsTargetHumidityCondition(EntityNumericalConditionBase):
"""Condition for humidifier target humidity."""

_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)}
_valid_unit = PERCENTAGE

def _should_include(self, state: State) -> bool:
"""Skip humidifier entities that do not expose a target humidity."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_HUMIDITY) is not None
)


class IsModeCondition(EntityStateConditionBase):
"""Condition for humidifier mode."""

Expand Down Expand Up @@ -79,10 +93,7 @@ def entity_filter(self, entities: set[str]) -> set[str]:
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.HUMIDIFYING
),
"is_mode": IsModeCondition,
"is_target_humidity": make_entity_numerical_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit=PERCENTAGE,
),
"is_target_humidity": IsTargetHumidityCondition,
}


Expand Down
29 changes: 26 additions & 3 deletions homeassistant/components/humidity/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import Condition, make_entity_numerical_condition
from homeassistant.helpers.condition import Condition, EntityNumericalConditionBase

HUMIDITY_DOMAIN_SPECS = {
CLIMATE_DOMAIN: DomainSpec(
Expand All @@ -31,8 +31,31 @@
),
}


class HumidityCondition(EntityNumericalConditionBase):
"""Condition for humidity value across multiple domains."""

_domain_specs = HUMIDITY_DOMAIN_SPECS
_valid_unit = PERCENTAGE

def _should_include(self, state: State) -> bool:
"""Skip attribute-source entities that lack the humidity attribute.

Mirrors the humidity trigger: for climate / humidifier / weather
(attribute-based), the entity is filtered when the source attribute
is absent; sensor entities (state-value-based) fall through to the
base impl.
"""
if not super()._should_include(state):
return False
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_source is None:
return True
return state.attributes.get(domain_spec.value_source) is not None


CONDITIONS: dict[str, type[Condition]] = {
"is_value": make_entity_numerical_condition(HUMIDITY_DOMAIN_SPECS, PERCENTAGE),
"is_value": HumidityCondition,
}


Expand Down
52 changes: 43 additions & 9 deletions homeassistant/components/humidity/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
ATTR_WEATHER_HUMIDITY,
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerBase,
Trigger,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
)

HUMIDITY_DOMAIN_SPECS: dict[str, DomainSpec] = {
Expand All @@ -36,13 +37,46 @@
),
}


class _HumidityTriggerMixin(EntityNumericalStateTriggerBase):
"""Mixin for humidity triggers providing entity filtering."""

_domain_specs = HUMIDITY_DOMAIN_SPECS
_valid_unit = "%"

def _should_include(self, state: State) -> bool:
"""Skip attribute-source entities that lack the humidity attribute.

For domains whose tracked value comes from an attribute
(climate / humidifier / weather), require the attribute to be
present; otherwise the all/count check would treat an entity that
cannot report a humidity as a non-match and block behavior=last.
Sensor entities source their value from `state.state`, so they
fall through to the base impl.
"""
if not super()._should_include(state):
return False
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_source is None:
return True
return state.attributes.get(domain_spec.value_source) is not None


class HumidityChangedTrigger(
_HumidityTriggerMixin, EntityNumericalStateChangedTriggerBase
):
"""Trigger for humidity value changes across multiple domains."""


class HumidityCrossedThresholdTrigger(
_HumidityTriggerMixin, EntityNumericalStateCrossedThresholdTriggerBase
):
"""Trigger for humidity value crossing a threshold across multiple domains."""


TRIGGERS: dict[str, type[Trigger]] = {
"changed": make_entity_numerical_state_changed_trigger(
HUMIDITY_DOMAIN_SPECS, valid_unit="%"
),
"crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
HUMIDITY_DOMAIN_SPECS, valid_unit="%"
),
"changed": HumidityChangedTrigger,
"crossed_threshold": HumidityCrossedThresholdTrigger,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.4.0"]
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.8.1"]
}
103 changes: 96 additions & 7 deletions homeassistant/components/media_player/condition.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,123 @@
"""Provides conditions for media players."""

from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from datetime import datetime
from typing import Any

from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
EntityConditionBase,
EntityNumericalConditionBase,
make_entity_state_condition,
)

from . import ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED
from .const import DOMAIN, MediaPlayerState


class _MediaPlayerMutedConditionBase(EntityConditionBase):
"""Base class for media player is_muted/is_unmuted conditions."""

_domain_specs = {DOMAIN: DomainSpec()}
_target_muted: bool

def _state_valid_since(self, state: State) -> datetime:
"""Anchor `for:` durations to `last_updated` for the muted attribute.

Needed because the domain spec does not reflect that the condition
reads from the muted and volume attributes.
"""
return state.last_updated

def _has_volume_attributes(self, state: State) -> bool:
"""Check if the state has volume muted or volume level attributes."""
return (
state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is not None
or state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) is not None
)

def _should_include(self, state: State) -> bool:
"""Skip entities without volume attributes from the all/count check."""
return super()._should_include(state) and self._has_volume_attributes(state)

def _is_muted(self, state: State) -> bool:
"""Check if the media player is muted."""
return (
state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True
or state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 0
)

def is_valid_state(self, entity_state: State) -> bool:
"""Check if the entity state matches the targeted muted state."""
if not self._has_volume_attributes(entity_state):
return False
return self._is_muted(entity_state) is self._target_muted


class MediaPlayerIsMutedCondition(_MediaPlayerMutedConditionBase):
"""Condition that passes when the media player is muted."""

_target_muted = True


class MediaPlayerIsUnmutedCondition(_MediaPlayerMutedConditionBase):
"""Condition that passes when the media player is not muted."""

_target_muted = False


class MediaPlayerIsVolumeCondition(EntityNumericalConditionBase):
"""Condition for media player volume level with 0.0-1.0 to percentage conversion."""

_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_MEDIA_VOLUME_LEVEL)}
_valid_unit = "%"

def _get_tracked_value(self, entity_state: State) -> Any:
"""Get the volume value converted from 0.0-1.0 to percentage (0-100)."""
raw = super()._get_tracked_value(entity_state)
if raw is None:
return None
try:
return float(raw) * 100.0
except TypeError, ValueError:
return None

def _should_include(self, state: State) -> bool:
"""Skip media players that do not expose a volume_level attribute."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) is not None
)


CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, MediaPlayerState.OFF),
"is_on": make_entity_state_condition(
"is_muted": MediaPlayerIsMutedCondition,
"is_not_playing": make_entity_state_condition(
DOMAIN,
{
MediaPlayerState.BUFFERING,
MediaPlayerState.IDLE,
MediaPlayerState.OFF,
MediaPlayerState.ON,
MediaPlayerState.PAUSED,
MediaPlayerState.PLAYING,
},
),
"is_not_playing": make_entity_state_condition(
"is_off": make_entity_state_condition(DOMAIN, MediaPlayerState.OFF),
"is_on": make_entity_state_condition(
DOMAIN,
{
MediaPlayerState.BUFFERING,
MediaPlayerState.IDLE,
MediaPlayerState.OFF,
MediaPlayerState.ON,
MediaPlayerState.PAUSED,
MediaPlayerState.PLAYING,
},
),
"is_paused": make_entity_state_condition(DOMAIN, MediaPlayerState.PAUSED),
"is_playing": make_entity_state_condition(DOMAIN, MediaPlayerState.PLAYING),
"is_unmuted": MediaPlayerIsUnmutedCondition,
"is_volume": MediaPlayerIsVolumeCondition,
}


Expand Down
Loading
Loading