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
10 changes: 5 additions & 5 deletions homeassistant/components/zha/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,31 +80,31 @@ def code_arm_required(self) -> bool:
"""Whether the code is required for arm actions."""
return self.entity_data.entity.code_arm_required

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
await self.entity_data.entity.async_alarm_disarm(code)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self.entity_data.entity.async_alarm_arm_home(code)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
await self.entity_data.entity.async_alarm_arm_away(code)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
await self.entity_data.entity.async_alarm_arm_night(code)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Send alarm trigger command."""
await self.entity_data.entity.async_alarm_trigger(code)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/zha/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def __init__(self, entity_data: EntityData) -> None:
self.entity_data.entity.info_object.device_class
)

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_press(self) -> None:
"""Send out a update command."""
await self.entity_data.entity.async_press()
8 changes: 4 additions & 4 deletions homeassistant/components/zha/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,25 +203,25 @@ def _handle_entity_events(self, event: Any) -> None:
)
super()._handle_entity_events(event)

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set fan mode."""
await self.entity_data.entity.async_set_fan_mode(fan_mode=fan_mode)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target operation mode."""
await self.entity_data.entity.async_set_hvac_mode(hvac_mode=hvac_mode)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
await self.entity_data.entity.async_set_preset_mode(preset_mode=preset_mode)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
await self.entity_data.entity.async_set_temperature(
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/zha/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,7 @@

ZHA_ALARM_OPTIONS = "zha_alarm_options"
ZHA_OPTIONS = "zha_options"

# Dispatcher signal carrying device reconfigure progress events (bind result,
# attribute reporting result, configure complete) to the websocket subscriber.
SIGNAL_DEVICE_RECONFIGURE_EVENT = "zha_device_reconfigure_event"
16 changes: 8 additions & 8 deletions homeassistant/components/zha/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,53 +122,53 @@ def current_cover_tilt_position(self) -> int | None:
"""Return the current tilt position of the cover."""
return self.entity_data.entity.current_cover_tilt_position

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self.entity_data.entity.async_open_cover()
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
await self.entity_data.entity.async_open_cover_tilt()
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
await self.entity_data.entity.async_close_cover()
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
await self.entity_data.entity.async_close_cover_tilt()
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
await self.entity_data.entity.async_set_cover_position(
position=kwargs.get(ATTR_POSITION)
)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
await self.entity_data.entity.async_set_cover_tilt_position(
tilt_position=kwargs.get(ATTR_TILT_POSITION)
)
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
await self.entity_data.entity.async_stop_cover()
self.async_write_ha_state()

@convert_zha_error_to_ha_error
@convert_zha_error_to_ha_error()
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop the cover tilt."""
await self.entity_data.entity.async_stop_cover_tilt()
Expand Down
164 changes: 74 additions & 90 deletions homeassistant/components/zha/device_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,26 @@
from typing import Any

import voluptuous as vol
from zha.exceptions import ZHAException
from zha.zigbee.cluster_handlers.const import (
CLUSTER_HANDLER_IAS_WD,
CLUSTER_HANDLER_INOVELLI,
)
from zha.zigbee.cluster_handlers.manufacturerspecific import (
AllLEDEffectType,
SingleLEDEffectType,
)
from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType
from zigpy.zcl.clusters.security import IasWd

Comment thread
puddly marked this conversation as resolved.
from homeassistant.components.device_automation import InvalidDeviceAutomationConfig
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType, TemplateVarsType

from .const import DOMAIN
from .helpers import async_get_zha_device_proxy
from .helpers import async_get_zha_device_proxy, convert_zha_error_to_ha_error
from .websocket_api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN

# mypy: disallow-any-generics

INOVELLI_CLUSTER_ID = 0xFC31

ACTION_SQUAWK = "squawk"
ACTION_WARN = "warn"
ATTR_DATA = "data"
ATTR_IEEE = "ieee"
CONF_ZHA_ACTION_TYPE = "zha_action_type"
ZHA_ACTION_TYPE_SERVICE_CALL = "service_call"
ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND = "cluster_handler_command"
INOVELLI_ALL_LED_EFFECT = "issue_all_led_effect"
INOVELLI_INDIVIDUAL_LED_EFFECT = "issue_individual_led_effect"

Expand Down Expand Up @@ -73,24 +63,18 @@
DEFAULT_ACTION_SCHEMA,
)

DEVICE_ACTIONS = {
CLUSTER_HANDLER_IAS_WD: [
# Maps a cluster_id the device must expose to the available actions.
DEVICE_ACTIONS_BY_CLUSTER_ID: dict[int, list[dict[str, str]]] = {
IasWd.cluster_id: [
{CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN},
{CONF_TYPE: ACTION_WARN, CONF_DOMAIN: DOMAIN},
],
CLUSTER_HANDLER_INOVELLI: [
INOVELLI_CLUSTER_ID: [
{CONF_TYPE: INOVELLI_ALL_LED_EFFECT, CONF_DOMAIN: DOMAIN},
{CONF_TYPE: INOVELLI_INDIVIDUAL_LED_EFFECT, CONF_DOMAIN: DOMAIN},
],
}
Comment on lines 21 to 76

DEVICE_ACTION_TYPES = {
ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL,
ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL,
INOVELLI_ALL_LED_EFFECT: ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND,
INOVELLI_INDIVIDUAL_LED_EFFECT: ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND,
}

DEVICE_ACTION_SCHEMAS = {
INOVELLI_ALL_LED_EFFECT: vol.Schema(
{
Expand All @@ -116,11 +100,6 @@
ACTION_WARN: SERVICE_WARNING_DEVICE_WARN,
}

CLUSTER_HANDLER_MAPPINGS = {
INOVELLI_ALL_LED_EFFECT: CLUSTER_HANDLER_INOVELLI,
INOVELLI_INDIVIDUAL_LED_EFFECT: CLUSTER_HANDLER_INOVELLI,
}


async def async_call_action_from_config(
hass: HomeAssistant,
Expand All @@ -129,9 +108,9 @@ async def async_call_action_from_config(
context: Context | None,
) -> None:
"""Perform an action based on configuration."""
await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]](
hass, config, variables, context
)
action_type = config[CONF_TYPE]
handler = ACTION_HANDLERS[action_type]
await handler(hass, config, context)


async def async_validate_action_config(
Expand All @@ -150,19 +129,18 @@ async def async_get_actions(
zha_device = async_get_zha_device_proxy(hass, device_id).device
except KeyError, AttributeError:
return []
cluster_handlers = [
ch.name
for endpoint in zha_device.endpoints.values()
for ch in endpoint.claimed_cluster_handlers.values()
]
actions = [
action
for cluster_handler, cluster_handler_actions in DEVICE_ACTIONS.items()
for action in cluster_handler_actions
if cluster_handler in cluster_handlers
]
for action in actions:
action[CONF_DEVICE_ID] = device_id
cluster_ids = {
cluster_id
for ep_id, endpoint in zha_device.device.endpoints.items()
if ep_id != 0
for cluster_id in endpoint.in_clusters
}
actions: list[dict[str, str]] = []
for required_cluster_id, cluster_actions in DEVICE_ACTIONS_BY_CLUSTER_ID.items():
if required_cluster_id in cluster_ids:
actions.extend(
{**action, CONF_DEVICE_ID: device_id} for action in cluster_actions
)
return actions
Comment thread
puddly marked this conversation as resolved.


Expand All @@ -175,69 +153,75 @@ async def async_get_action_capabilities(
return {"extra_fields": fields}


async def _execute_service_based_action(
async def _execute_siren_service(
hass: HomeAssistant,
config: dict[str, Any],
variables: TemplateVarsType,
context: Context | None,
) -> None:
action_type = config[CONF_TYPE]
service_name = SERVICE_NAMES[action_type]
try:
zha_device = async_get_zha_device_proxy(hass, config[CONF_DEVICE_ID]).device
except KeyError, AttributeError:
return

service_data = {ATTR_IEEE: str(zha_device.ieee)}

await hass.services.async_call(
DOMAIN, service_name, service_data, blocking=True, context=context
DOMAIN,
SERVICE_NAMES[config[CONF_TYPE]],
{ATTR_IEEE: str(zha_device.ieee)},
blocking=True,
context=context,
)


async def _execute_cluster_handler_command_based_action(
hass: HomeAssistant,
config: dict[str, Any],
variables: TemplateVarsType,
context: Context | None,
) -> None:
action_type = config[CONF_TYPE]
cluster_handler_name = CLUSTER_HANDLER_MAPPINGS[action_type]
def _find_inovelli_cluster(hass: HomeAssistant, config: dict[str, Any]) -> Any:
try:
zha_device = async_get_zha_device_proxy(hass, config[CONF_DEVICE_ID]).device
except KeyError, AttributeError:
return
except (KeyError, AttributeError) as err:
raise InvalidDeviceAutomationConfig(
f"ZHA device {config[CONF_DEVICE_ID]} not found"
) from err
try:
return zha_device.device.find_cluster(cluster_id=INOVELLI_CLUSTER_ID)
except ValueError as err:
raise InvalidDeviceAutomationConfig(
f"Device does not expose Inovelli cluster 0x{INOVELLI_CLUSTER_ID:04x}"
) from err

action_cluster_handler = None
for endpoint in zha_device.endpoints.values():
for cluster_handler in endpoint.all_cluster_handlers.values():
if cluster_handler.name == cluster_handler_name:
action_cluster_handler = cluster_handler
break

if action_cluster_handler is None:
raise InvalidDeviceAutomationConfig(
f"Unable to execute cluster handler action -"
f" cluster handler: {cluster_handler_name} action:"
f" {action_type}"
async def _execute_inovelli_all_led_effect(
hass: HomeAssistant,
config: dict[str, Any],
context: Context | None,
) -> None:
cluster = _find_inovelli_cluster(hass, config)

async with convert_zha_error_to_ha_error():
await cluster.led_effect(
led_effect=config["effect_type"],
led_color=config["color"],
led_level=config["level"],
led_duration=config["duration"],
)

if not hasattr(action_cluster_handler, action_type):
raise InvalidDeviceAutomationConfig(
f"Unable to execute cluster handler -"
f" cluster handler: {cluster_handler_name} action:"
f" {action_type}"
)

try:
await getattr(action_cluster_handler, action_type)(**config)
except ZHAException as err:
raise HomeAssistantError(err) from err
async def _execute_inovelli_individual_led_effect(
hass: HomeAssistant,
config: dict[str, Any],
context: Context | None,
) -> None:
cluster = _find_inovelli_cluster(hass, config)

async with convert_zha_error_to_ha_error():
await cluster.individual_led_effect(
led_effect=config["effect_type"],
led_color=config["color"],
led_level=config["level"],
led_duration=config["duration"],
led_number=config["led_number"],
)


ZHA_ACTION_TYPES = {
ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action,
ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND: (
_execute_cluster_handler_command_based_action
),
ACTION_HANDLERS = {
ACTION_SQUAWK: _execute_siren_service,
ACTION_WARN: _execute_siren_service,
INOVELLI_ALL_LED_EFFECT: _execute_inovelli_all_led_effect,
INOVELLI_INDIVIDUAL_LED_EFFECT: _execute_inovelli_individual_led_effect,
}
Loading
Loading