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
93 changes: 93 additions & 0 deletions homeassistant/components/alarm_control_panel/condition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Provides conditions for alarm control panels."""

from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.condition import (
Condition,
EntityStateConditionBase,
make_entity_state_condition,
)
from homeassistant.helpers.entity import get_supported_features

from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState


def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
"""Test if an entity supports the specified features."""
try:
return bool(get_supported_features(hass, entity_id) & features)
except HomeAssistantError:
return False


class EntityStateRequiredFeaturesCondition(EntityStateConditionBase):
"""State condition."""

_required_features: int

def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain with the required features."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if supports_feature(self._hass, entity_id, self._required_features)
}


def make_entity_state_required_features_condition(
domain: str, to_state: str, required_features: int
) -> type[EntityStateRequiredFeaturesCondition]:
"""Create an entity state condition class with required feature filtering."""

class CustomCondition(EntityStateRequiredFeaturesCondition):
"""Condition for entity state changes."""

_domain = domain
_states = {to_state}
_required_features = required_features

return CustomCondition


CONDITIONS: dict[str, type[Condition]] = {
"is_armed": make_entity_state_condition(
DOMAIN,
{
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_VACATION,
},
),
"is_armed_away": make_entity_state_required_features_condition(
DOMAIN,
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelEntityFeature.ARM_AWAY,
),
"is_armed_home": make_entity_state_required_features_condition(
DOMAIN,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelEntityFeature.ARM_HOME,
),
"is_armed_night": make_entity_state_required_features_condition(
DOMAIN,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelEntityFeature.ARM_NIGHT,
),
"is_armed_vacation": make_entity_state_required_features_condition(
DOMAIN,
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelEntityFeature.ARM_VACATION,
),
"is_disarmed": make_entity_state_condition(DOMAIN, AlarmControlPanelState.DISARMED),
"is_triggered": make_entity_state_condition(
DOMAIN, AlarmControlPanelState.TRIGGERED
),
}


async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the alarm control panel conditions."""
return CONDITIONS
52 changes: 52 additions & 0 deletions homeassistant/components/alarm_control_panel/conditions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.condition_common: &condition_common
target:
entity:
domain: alarm_control_panel
fields: &condition_common_fields
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any

is_armed: *condition_common

is_armed_away:
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY

is_armed_home:
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME

is_armed_night:
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT

is_armed_vacation:
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION

is_disarmed: *condition_common

is_triggered: *condition_common
23 changes: 23 additions & 0 deletions homeassistant/components/alarm_control_panel/icons.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
{
"conditions": {
"is_armed": {
"condition": "mdi:shield"
},
"is_armed_away": {
"condition": "mdi:shield-lock"
},
"is_armed_home": {
"condition": "mdi:shield-home"
},
"is_armed_night": {
"condition": "mdi:shield-moon"
},
"is_armed_vacation": {
"condition": "mdi:shield-airplane"
},
"is_disarmed": {
"condition": "mdi:shield-off"
},
"is_triggered": {
"condition": "mdi:bell-ring"
}
},
"entity_component": {
"_": {
"default": "mdi:shield",
Expand Down
80 changes: 80 additions & 0 deletions homeassistant/components/alarm_control_panel/strings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,82 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted alarms.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_armed": {
"description": "Tests if one or more alarms are armed.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed"
},
"is_armed_away": {
"description": "Tests if one or more alarms are armed in away mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed away"
},
"is_armed_home": {
"description": "Tests if one or more alarms are armed in home mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed home"
},
"is_armed_night": {
"description": "Tests if one or more alarms are armed in night mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed night"
},
"is_armed_vacation": {
"description": "Tests if one or more alarms are armed in vacation mode.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is armed vacation"
},
"is_disarmed": {
"description": "Tests if one or more alarms are disarmed.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is disarmed"
},
"is_triggered": {
"description": "Tests if one or more alarms are triggered.",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "If an alarm is triggered"
}
},
"device_automation": {
"action_type": {
"arm_away": "Arm {entity_name} away",
Expand Down Expand Up @@ -76,6 +150,12 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"

_EXPERIMENTAL_CONDITION_PLATFORMS = {
"alarm_control_panel",
"fan",
"light",
}
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/blueprint/websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ async def with_domain_blueprints(
return with_domain_blueprints


@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/list",
Expand Down Expand Up @@ -97,6 +98,7 @@ async def ws_list_blueprints(
connection.send_result(msg["id"], results)


@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/import",
Expand Down Expand Up @@ -150,6 +152,7 @@ async def ws_import_blueprint(
)


@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/save",
Expand Down Expand Up @@ -206,6 +209,7 @@ async def ws_save_blueprint(
)


@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/delete",
Expand Down Expand Up @@ -233,6 +237,7 @@ async def ws_delete_blueprint(
)


@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/substitute",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/essent/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Final

DOMAIN: Final = "essent"
UPDATE_INTERVAL: Final = timedelta(hours=12)
UPDATE_INTERVAL: Final = timedelta(hours=1)
ATTRIBUTION: Final = "Data provided by Essent"


Expand Down
29 changes: 22 additions & 7 deletions homeassistant/components/tuya/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import collections
from dataclasses import dataclass
from typing import Any, Self

Expand Down Expand Up @@ -140,6 +141,22 @@ def get_update_commands(
return commands


def _filter_hvac_mode_mappings(tuya_range: list[str]) -> dict[str, HVACMode | None]:
"""Filter TUYA_HVAC_TO_HA modes that are not in the range.

If multiple Tuya modes map to the same HA mode, set the mapping to None to avoid
ambiguity when converting back from HA to Tuya modes.
"""
modes_in_range = {
tuya_mode: TUYA_HVAC_TO_HA.get(tuya_mode) for tuya_mode in tuya_range
}
modes_occurrences = collections.Counter(modes_in_range.values())
for key, value in modes_in_range.items():
if value is not None and modes_occurrences[value] > 1:
modes_in_range[key] = None
return modes_in_range


class _HvacModeWrapper(DPCodeEnumWrapper):
"""Wrapper for managing climate HVACMode."""

Expand All @@ -148,10 +165,9 @@ class _HvacModeWrapper(DPCodeEnumWrapper):
def __init__(self, dpcode: str, type_information: EnumTypeInformation) -> None:
"""Init _HvacModeWrapper."""
super().__init__(dpcode, type_information)
self._mappings = _filter_hvac_mode_mappings(type_information.range)
self.options = [
TUYA_HVAC_TO_HA[tuya_mode]
for tuya_mode in type_information.range
if tuya_mode in TUYA_HVAC_TO_HA
ha_mode for ha_mode in self._mappings.values() if ha_mode is not None
]

def read_device_status(self, device: CustomerDevice) -> HVACMode | None:
Expand All @@ -166,7 +182,7 @@ def _convert_value_to_raw_value(
"""Convert value to raw value."""
return next(
tuya_mode
for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items()
for tuya_mode, ha_mode in self._mappings.items()
if ha_mode == value
)

Expand All @@ -179,10 +195,9 @@ class _PresetWrapper(DPCodeEnumWrapper):
def __init__(self, dpcode: str, type_information: EnumTypeInformation) -> None:
"""Init _PresetWrapper."""
super().__init__(dpcode, type_information)
mappings = _filter_hvac_mode_mappings(type_information.range)
self.options = [
tuya_mode
for tuya_mode in type_information.range
if tuya_mode not in TUYA_HVAC_TO_HA
tuya_mode for tuya_mode, ha_mode in mappings.items() if ha_mode is None
]

def read_device_status(self, device: CustomerDevice) -> str | None:
Expand Down
Loading
Loading