Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
Expand All @@ -302,7 +302,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
with:
extra-args: --all-files zizmor

Expand Down
21 changes: 16 additions & 5 deletions homeassistant/components/climate/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
Condition,
ConditionConfig,
EntityConditionBase,
EntityNumericalConditionBase,
EntityNumericalConditionWithUnitBase,
make_entity_numerical_condition,
make_entity_state_condition,
)
from homeassistant.util.unit_conversion import TemperatureConverter
Expand Down Expand Up @@ -65,6 +65,20 @@ def _get_entity_unit(self, entity_state: State) -> str | None:
return self._hass.config.units.temperature_unit


class ClimateTargetHumidityCondition(EntityNumericalConditionBase):
"""Condition for climate target humidity."""

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

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


CONDITIONS: dict[str, type[Condition]] = {
"is_hvac_mode": ClimateHVACModeCondition,
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
Expand All @@ -88,10 +102,7 @@ def _get_entity_unit(self, entity_state: State) -> str | None:
"is_heating": make_entity_state_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
),
"target_humidity": make_entity_numerical_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_humidity": ClimateTargetHumidityCondition,
"target_temperature": ClimateTargetTemperatureCondition,
}

Expand Down
10 changes: 8 additions & 2 deletions homeassistant/components/matter/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,10 @@ def _calculate_features(
return
self._feature_map = feature_map
self._attr_supported_features = FanEntityFeature(0)
# Reset to default so a featuremap change from MultiSpeed -> non-MultiSpeed
# does not leave a stale speed_count / percentage_step.
self._attr_speed_count = 100
if feature_map & FanControlFeature.kMultiSpeed:
self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._attr_speed_count = int(
self.get_matter_attribute_value(clusters.FanControl.Attributes.SpeedMax)
)
Expand Down Expand Up @@ -302,8 +304,12 @@ def _calculate_features(
if feature_map & FanControlFeature.kAirflowDirection:
self._attr_supported_features |= FanEntityFeature.DIRECTION

# PercentSetting is always a mandatory attribute of the FanControl cluster,
# so percentage-based speed control is always available.
self._attr_supported_features |= (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
FanEntityFeature.SET_SPEED
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)


Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/media_player/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@
},
"unmuted": {
"trigger": "mdi:volume-high"
},
"volume_changed": {
"trigger": "mdi:volume-medium"
},
"volume_crossed_threshold": {
"trigger": "mdi:volume-medium"
}
}
}
27 changes: 26 additions & 1 deletion homeassistant/components/media_player/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold"
},
"conditions": {
"is_not_playing": {
Expand Down Expand Up @@ -520,6 +521,30 @@
}
},
"name": "Media player unmuted"
},
"volume_changed": {
"description": "Triggers after the volume of one or more media players changes.",
"fields": {
"threshold": {
"name": "[%key:component::media_player::common::trigger_threshold_name%]"
}
},
"name": "Media player volume changed"
},
"volume_crossed_threshold": {
"description": "Triggers after the volume of one or more media players crosses a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::media_player::common::trigger_threshold_name%]"
}
},
"name": "Media player volume crossed threshold"
}
}
}
46 changes: 46 additions & 0 deletions homeassistant/components/media_player/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerBase,
EntityTriggerBase,
Trigger,
make_entity_transition_trigger,
Expand All @@ -12,6 +15,10 @@
from . import ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, MediaPlayerState
from .const import DOMAIN

VOLUME_DOMAIN_SPECS = {
DOMAIN: DomainSpec(value_source=ATTR_MEDIA_VOLUME_LEVEL),
}


class _MediaPlayerMutedStateTriggerBase(EntityTriggerBase):
"""Base class for media player muted/unmuted triggers."""
Expand Down Expand Up @@ -71,9 +78,48 @@ class MediaPlayerUnmutedTrigger(_MediaPlayerMutedStateTriggerBase):
_target_muted = False


class VolumeTriggerMixin(EntityNumericalStateTriggerBase):
"""Mixin for volume triggers."""

_domain_specs = VOLUME_DOMAIN_SPECS
_valid_unit = "%"

def _get_tracked_value(self, state: State) -> float | None:
"""Get tracked volume as a percentage."""
value = super()._get_tracked_value(state)
if value is None:
return None
# Convert 0.0-1.0 range to percentage (0-100)
return value * 100.0

def _should_include(self, state: State) -> bool:
"""Check if an entity should participate in all/count checks.

Entities without a volume level cannot have their volume tracked,
so they are excluded - otherwise an "all" check would never pass
when there are media players without volume support.
"""
return (
super()._should_include(state)
and state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) is not None
)


class VolumeChangedTrigger(EntityNumericalStateChangedTriggerBase, VolumeTriggerMixin):
"""Trigger for media player volume changes."""


class VolumeCrossedThresholdTrigger(
EntityNumericalStateCrossedThresholdTriggerBase, VolumeTriggerMixin
):
"""Trigger for media player volume crossing a threshold."""


TRIGGERS: dict[str, type[Trigger]] = {
"muted": MediaPlayerMutedTrigger,
"unmuted": MediaPlayerUnmutedTrigger,
"volume_changed": VolumeChangedTrigger,
"volume_crossed_threshold": VolumeCrossedThresholdTrigger,
"paused_playing": make_entity_transition_trigger(
DOMAIN,
from_states={
Expand Down
44 changes: 41 additions & 3 deletions homeassistant/components/media_player/triggers.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,62 @@
.trigger_common: &trigger_common
target:
target: &trigger_media_player_target
entity:
domain: media_player
fields:
behavior:
behavior: &trigger_behavior
required: true
default: any
selector:
automation_behavior:
mode: trigger
for:
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:

.volume_threshold_entity: &volume_threshold_entity
- domain: input_number
unit_of_measurement: "%"
- domain: number
unit_of_measurement: "%"
- domain: sensor
unit_of_measurement: "%"

.volume_threshold_number: &volume_threshold_number
min: 0
max: 100
mode: box
unit_of_measurement: "%"

muted: *trigger_common
unmuted: *trigger_common
paused_playing: *trigger_common
started_playing: *trigger_common
stopped_playing: *trigger_common
turned_off: *trigger_common
turned_on: *trigger_common

volume_changed:
target: *trigger_media_player_target
fields:
threshold:
required: true
selector:
numeric_threshold:
entity: *volume_threshold_entity
mode: changed
number: *volume_threshold_number

volume_crossed_threshold:
target: *trigger_media_player_target
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
numeric_threshold:
entity: *volume_threshold_entity
mode: crossed
number: *volume_threshold_number
5 changes: 2 additions & 3 deletions homeassistant/components/netatmo/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .const import CONF_URL_CONTROL, NETATMO_CREATE_BUTTON
from .data_handler import HOME, SIGNAL_NAME, NetatmoConfigEntry, NetatmoDevice
from .entity import NetatmoModuleEntity
from .helper import device_type_to_str

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -55,9 +56,7 @@ def __init__(self, netatmo_device: NetatmoDevice) -> None:
},
]
)
self._attr_unique_id = (
f"{self.device.entity_id}-{self.device_type}-preferred_position"
)
self._attr_unique_id = f"{self.device.entity_id}-{device_type_to_str(self.device_type)}-preferred_position"

@callback
def async_update_callback(self) -> None:
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/netatmo/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
)
from .data_handler import EVENT, HOME, SIGNAL_NAME, NetatmoConfigEntry, NetatmoDevice
from .entity import NetatmoModuleEntity
from .helper import device_type_to_str

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -102,7 +103,9 @@ def __init__(
Camera.__init__(self)
super().__init__(netatmo_device)

self._attr_unique_id = f"{netatmo_device.device.entity_id}-{self.device_type}"
self._attr_unique_id = (
f"{netatmo_device.device.entity_id}-{device_type_to_str(self.device_type)}"
)
self._light_state = None

self._publishers.extend(
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/netatmo/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
)
from .data_handler import HOME, SIGNAL_NAME, NetatmoConfigEntry, NetatmoRoom
from .entity import NetatmoRoomEntity
from .helper import device_type_to_str

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -219,7 +220,9 @@ def __init__(self, room: NetatmoRoom) -> None:
if self.device_type is NA_THERM:
self._attr_hvac_modes.append(HVACMode.OFF)

self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}"
self._attr_unique_id = (
f"{self.device.entity_id}-{device_type_to_str(self.device_type)}"
)

async def async_added_to_hass(self) -> None:
"""Entity created."""
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/netatmo/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .const import CONF_URL_CONTROL, NETATMO_CREATE_COVER
from .data_handler import HOME, SIGNAL_NAME, NetatmoConfigEntry, NetatmoDevice
from .entity import NetatmoModuleEntity
from .helper import device_type_to_str

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -70,7 +71,9 @@ def __init__(self, netatmo_device: NetatmoDevice) -> None:
},
]
)
self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}"
self._attr_unique_id = (
f"{self.device.entity_id}-{device_type_to_str(self.device_type)}"
)

async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/netatmo/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .const import CONF_URL_CONTROL, NETATMO_CREATE_FAN
from .data_handler import HOME, SIGNAL_NAME, NetatmoConfigEntry, NetatmoDevice
from .entity import NetatmoModuleEntity
from .helper import device_type_to_str

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -62,7 +63,9 @@ def __init__(self, netatmo_device: NetatmoDevice) -> None:
]
)

self._attr_unique_id = f"{self.device.entity_id}-{self.device_type}"
self._attr_unique_id = (
f"{self.device.entity_id}-{device_type_to_str(self.device_type)}"
)

async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
Expand Down
10 changes: 10 additions & 0 deletions homeassistant/components/netatmo/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
from dataclasses import dataclass
from uuid import UUID, uuid4

from pyatmo.modules.device_types import DeviceType as NetatmoDeviceType


def device_type_to_str(device_type: NetatmoDeviceType) -> str:
"""Convert a device type to a string.

Used to generate backwards compatible unique ids.
"""
return f"{type(device_type).__name__}.{device_type}"


@dataclass
class NetatmoArea:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/netatmo/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pyatmo"],
"requirements": ["pyatmo==9.2.3"]
"requirements": ["pyatmo==9.4.0"]
}
Loading
Loading