1
mirror of https://github.com/home-assistant/core synced 2024-08-15 18:25:44 +02:00

Add configuration entities and device actions for Inovelli Blue Series switch to ZHA (#79106)

* Add Inovelli configutation entities to ZHA

* add device actions

* fix attribute name collision

* add device action tests

* disable remote protection per Inovelli request

* expect_reply to false

* update test for expect_reply change

* inovelli feedback

* translation keys and strings

* clean up numbers

* prevent double events

* remove individual LED defaults per inovelli

* redundant check

* update test
This commit is contained in:
David F. Mulcahey 2022-09-28 14:22:39 -04:00 committed by GitHub
parent 7042d6d35b
commit 2ed48a9b28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 822 additions and 24 deletions

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from zigpy import types
from zigpy.exceptions import ZigbeeException
import zigpy.zcl
@ -126,12 +127,135 @@ class SmartThingsAcceleration(ZigbeeChannel):
)
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFC31)
@registries.CLIENT_CHANNELS_REGISTRY.register(0xFC31)
class InovelliCluster(ClientChannel):
"""Inovelli Button Press Event channel."""
class InovelliNotificationChannel(ClientChannel):
"""Inovelli Notification channel."""
@callback
def attribute_updated(self, attrid, value):
"""Handle an attribute updated on this cluster."""
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle a cluster command received on this cluster."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFC31)
class InovelliConfigEntityChannel(ZigbeeChannel):
"""Inovelli Configuration Entity channel."""
class LEDEffectType(types.enum8):
"""Effect type for Inovelli Blue Series switch."""
Off = 0x00
Solid = 0x01
Fast_Blink = 0x02
Slow_Blink = 0x03
Pulse = 0x04
Chase = 0x05
Open_Close = 0x06
Small_To_Big = 0x07
Clear = 0xFF
REPORT_CONFIG = ()
ZCL_INIT_ATTRS = { # pylint: disable=invalid-name
"dimming_speed_up_remote": False,
"dimming_speed_up_local": False,
"ramp_rate_off_to_on_local": False,
"ramp_rate_off_to_on_remote": False,
"dimming_speed_down_remote": False,
"dimming_speed_down_local": False,
"ramp_rate_on_to_off_local": False,
"ramp_rate_on_to_off_remote": False,
"minimum_level": False,
"maximum_level": False,
"invert_switch": False,
"auto_off_timer": False,
"default_level_local": False,
"default_level_remote": False,
"state_after_power_restored": False,
"load_level_indicator_timeout": False,
"active_power_reports": False,
"periodic_power_and_energy_reports": False,
"active_energy_reports": False,
"power_type": False,
"switch_type": False,
"button_delay": False,
"device_bind_number": False,
"smart_bulb_mode": False,
"double_tap_up_for_full_brightness": False,
"default_led1_strip_color_when_on": False,
"default_led1_strip_color_when_off": False,
"default_led1_strip_intensity_when_on": False,
"default_led1_strip_intensity_when_off": False,
"default_led2_strip_color_when_on": False,
"default_led2_strip_color_when_off": False,
"default_led2_strip_intensity_when_on": False,
"default_led2_strip_intensity_when_off": False,
"default_led3_strip_color_when_on": False,
"default_led3_strip_color_when_off": False,
"default_led3_strip_intensity_when_on": False,
"default_led3_strip_intensity_when_off": False,
"default_led4_strip_color_when_on": False,
"default_led4_strip_color_when_off": False,
"default_led4_strip_intensity_when_on": False,
"default_led4_strip_intensity_when_off": False,
"default_led5_strip_color_when_on": False,
"default_led5_strip_color_when_off": False,
"default_led5_strip_intensity_when_on": False,
"default_led5_strip_intensity_when_off": False,
"default_led6_strip_color_when_on": False,
"default_led6_strip_color_when_off": False,
"default_led6_strip_intensity_when_on": False,
"default_led6_strip_intensity_when_off": False,
"default_led7_strip_color_when_on": False,
"default_led7_strip_color_when_off": False,
"default_led7_strip_intensity_when_on": False,
"default_led7_strip_intensity_when_off": False,
"led_color_when_on": False,
"led_color_when_off": False,
"led_intensity_when_on": False,
"led_intensity_when_off": False,
"local_protection": False,
"remote_protection": False,
"output_mode": False,
"on_off_led_mode": False,
"firmware_progress_led": False,
"relay_click_in_on_off_mode": False,
}
async def issue_all_led_effect(
self,
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
color: int = 200,
level: int = 100,
duration: int = 3,
**kwargs: Any,
) -> None:
"""Issue all LED effect command.
This command is used to issue an LED effect to all LEDs on the device.
"""
await self.led_effect(effect_type, color, level, duration, expect_reply=False)
async def issue_individual_led_effect(
self,
led_number: int = 1,
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
color: int = 200,
level: int = 100,
duration: int = 3,
**kwargs: Any,
) -> None:
"""Issue individual LED effect command.
This command is used to issue an LED effect to the specified LED on the device.
"""
await self.individual_led_effect(
led_number, effect_type, color, level, duration, expect_reply=False
)
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.IKEA_AIR_PURIFIER_CLUSTER)

View File

@ -95,6 +95,7 @@ CHANNEL_TEMPERATURE = "temperature"
CHANNEL_THERMOSTAT = "thermostat"
CHANNEL_ZDO = "zdo"
CHANNEL_ZONE = ZONE = "ias_zone"
CHANNEL_INOVELLI = "inovelli_vzm31sn_cluster"
CLUSTER_COMMAND_SERVER = "server"
CLUSTER_COMMANDS_CLIENT = "client_commands"

View File

@ -5,6 +5,7 @@ from typing import Any
import voluptuous as vol
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.helpers import config_validation as cv
@ -12,7 +13,8 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN
from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN
from .core.const import CHANNEL_IAS_WD
from .core.channels.manufacturerspecific import InovelliConfigEntityChannel
from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI
from .core.helpers import async_get_zha_device
# mypy: disallow-any-generics
@ -23,21 +25,83 @@ ATTR_DATA = "data"
ATTR_IEEE = "ieee"
CONF_ZHA_ACTION_TYPE = "zha_action_type"
ZHA_ACTION_TYPE_SERVICE_CALL = "service_call"
ZHA_ACTION_TYPE_CHANNEL_COMMAND = "channel_command"
INOVELLI_ALL_LED_EFFECT = "issue_all_led_effect"
INOVELLI_INDIVIDUAL_LED_EFFECT = "issue_individual_led_effect"
ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_TYPE): str}
DEFAULT_ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required(CONF_TYPE): vol.In({ACTION_SQUAWK, ACTION_WARN}),
}
)
INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
{
vol.Required(CONF_TYPE): INOVELLI_ALL_LED_EFFECT,
vol.Required(CONF_DOMAIN): DOMAIN,
vol.Required(
"effect_type"
): InovelliConfigEntityChannel.LEDEffectType.__getitem__,
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
}
)
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend(
{
vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT,
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)),
}
)
ACTION_SCHEMA = vol.Any(
INOVELLI_ALL_LED_EFFECT_SCHEMA,
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
DEFAULT_ACTION_SCHEMA,
)
DEVICE_ACTIONS = {
CHANNEL_IAS_WD: [
{CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN},
{CONF_TYPE: ACTION_WARN, CONF_DOMAIN: DOMAIN},
]
],
CHANNEL_INOVELLI: [
{CONF_TYPE: INOVELLI_ALL_LED_EFFECT, CONF_DOMAIN: DOMAIN},
{CONF_TYPE: INOVELLI_INDIVIDUAL_LED_EFFECT, CONF_DOMAIN: DOMAIN},
],
}
DEVICE_ACTION_TYPES = {
ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL,
ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL,
INOVELLI_ALL_LED_EFFECT: ZHA_ACTION_TYPE_CHANNEL_COMMAND,
INOVELLI_INDIVIDUAL_LED_EFFECT: ZHA_ACTION_TYPE_CHANNEL_COMMAND,
}
DEVICE_ACTION_SCHEMAS = {
INOVELLI_ALL_LED_EFFECT: vol.Schema(
{
vol.Required("effect_type"): vol.In(
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
),
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
}
),
INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema(
{
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)),
vol.Required("effect_type"): vol.In(
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
),
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
}
),
}
SERVICE_NAMES = {
@ -45,6 +109,11 @@ SERVICE_NAMES = {
ACTION_WARN: SERVICE_WARNING_DEVICE_WARN,
}
CHANNEL_MAPPINGS = {
INOVELLI_ALL_LED_EFFECT: CHANNEL_INOVELLI,
INOVELLI_INDIVIDUAL_LED_EFFECT: CHANNEL_INOVELLI,
}
async def async_call_action_from_config(
hass: HomeAssistant,
@ -82,6 +151,14 @@ async def async_get_actions(
return actions
async def async_get_action_capabilities(
hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
"""List action capabilities."""
return {"extra_fields": DEVICE_ACTION_SCHEMAS.get(config[CONF_TYPE], {})}
async def _execute_service_based_action(
hass: HomeAssistant,
config: dict[str, Any],
@ -102,4 +179,40 @@ async def _execute_service_based_action(
)
ZHA_ACTION_TYPES = {ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action}
async def _execute_channel_command_based_action(
hass: HomeAssistant,
config: dict[str, Any],
variables: TemplateVarsType,
context: Context | None,
) -> None:
action_type = config[CONF_TYPE]
channel_name = CHANNEL_MAPPINGS[action_type]
try:
zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID])
except (KeyError, AttributeError):
return
action_channel = None
for pool in zha_device.channels.pools:
for channel in pool.all_channels.values():
if channel.name == channel_name:
action_channel = channel
break
if action_channel is None:
raise InvalidDeviceAutomationConfig(
f"Unable to execute channel action - channel: {channel_name} action: {action_type}"
)
if not hasattr(action_channel, action_type):
raise InvalidDeviceAutomationConfig(
f"Unable to execute channel action - channel: {channel_name} action: {action_type}"
)
await getattr(action_channel, action_type)(**config)
ZHA_ACTION_TYPES = {
ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action,
ZHA_ACTION_TYPE_CHANNEL_COMMAND: _execute_channel_command_based_action,
}

View File

@ -66,6 +66,8 @@ class BaseZhaEntity(LogMixin, entity.Entity):
@property
def name(self) -> str:
"""Return Entity's default name."""
if hasattr(self, "_attr_name") and self._attr_name is not None:
return self._attr_name
return self._name
@property

View File

@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery
from .core.const import (
CHANNEL_ANALOG_OUTPUT,
CHANNEL_INOVELLI,
CHANNEL_LEVEL,
DATA_ZHA,
SIGNAL_ADD_ENTITIES,
@ -251,6 +252,8 @@ ICONS = {
12: "mdi:counter",
13: "mdi:thermometer-lines",
14: "mdi:timer",
15: "mdi:palette",
16: "mdi:brightness-percent",
}
@ -545,3 +548,252 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time")
_attr_native_max_value: float = 0xFFFFFFFF
_attr_native_unit_of_measurement: str | None = UNITS[72]
_zcl_attribute: str = "filter_life_time"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliRemoteDimmingUpSpeed(
ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_remote"
):
"""Inovelli remote dimming up speed configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 126
_zcl_attribute: str = "dimming_speed_up_remote"
_attr_name: str = "Remote dimming up speed"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliButtonDelay(ZHANumberConfigurationEntity, id_suffix="button_delay"):
"""Inovelli button delay configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 9
_zcl_attribute: str = "button_delay"
_attr_name: str = "Button delay"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliDeviceBindNumber(
ZHANumberConfigurationEntity, id_suffix="device_bind_number"
):
"""Inovelli device bind number configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_native_min_value: float = 0
_attr_native_max_value: float = 255
_zcl_attribute: str = "device_bind_number"
_attr_name: str = "Device bind number"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliLocalDimmingUpSpeed(
ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_local"
):
"""Inovelli local dimming up speed configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 127
_zcl_attribute: str = "dimming_speed_up_local"
_attr_name: str = "Local dimming up speed"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliLocalRampRateOffToOn(
ZHANumberConfigurationEntity, id_suffix="ramp_rate_off_to_on_local"
):
"""Inovelli off to on local ramp rate configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 127
_zcl_attribute: str = "ramp_rate_off_to_on_local"
_attr_name: str = "Local ramp rate off to on"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliRemoteDimmingSpeedOffToOn(
ZHANumberConfigurationEntity, id_suffix="ramp_rate_off_to_on_remote"
):
"""Inovelli off to on remote ramp rate configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 127
_zcl_attribute: str = "ramp_rate_off_to_on_remote"
_attr_name: str = "Remote ramp rate off to on"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliRemoteDimmingDownSpeed(
ZHANumberConfigurationEntity, id_suffix="dimming_speed_down_remote"
):
"""Inovelli remote dimming down speed configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 127
_zcl_attribute: str = "dimming_speed_down_remote"
_attr_name: str = "Remote dimming down speed"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliLocalDimmingDownSpeed(
ZHANumberConfigurationEntity, id_suffix="dimming_speed_down_local"
):
"""Inovelli local dimming down speed configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 127
_zcl_attribute: str = "dimming_speed_down_local"
_attr_name: str = "Local dimming down speed"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliLocalRampRateOnToOff(
ZHANumberConfigurationEntity, id_suffix="ramp_rate_on_to_off_local"
):
"""Inovelli local on to off ramp rate configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 127
_zcl_attribute: str = "ramp_rate_on_to_off_local"
_attr_name: str = "Local ramp rate on to off"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliRemoteDimmingSpeedOnToOff(
ZHANumberConfigurationEntity, id_suffix="ramp_rate_on_to_off_remote"
):
"""Inovelli remote on to off ramp rate configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[3]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 127
_zcl_attribute: str = "ramp_rate_on_to_off_remote"
_attr_name: str = "Remote ramp rate on to off"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliMinimumLoadDimmingLevel(
ZHANumberConfigurationEntity, id_suffix="minimum_level"
):
"""Inovelli minimum load dimming level configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[16]
_attr_native_min_value: float = 1
_attr_native_max_value: float = 254
_zcl_attribute: str = "minimum_level"
_attr_name: str = "Minimum load dimming level"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliMaximumLoadDimmingLevel(
ZHANumberConfigurationEntity, id_suffix="maximum_level"
):
"""Inovelli maximum load dimming level configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[16]
_attr_native_min_value: float = 2
_attr_native_max_value: float = 255
_zcl_attribute: str = "maximum_level"
_attr_name: str = "Maximum load dimming level"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliAutoShutoffTimer(
ZHANumberConfigurationEntity, id_suffix="auto_off_timer"
):
"""Inovelli automatic switch shutoff timer configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[14]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 32767
_zcl_attribute: str = "auto_off_timer"
_attr_name: str = "Automatic switch shutoff timer"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliLoadLevelIndicatorTimeout(
ZHANumberConfigurationEntity, id_suffix="load_level_indicator_timeout"
):
"""Inovelli load level indicator timeout configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[14]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 11
_zcl_attribute: str = "load_level_indicator_timeout"
_attr_name: str = "Load level indicator timeout"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliDefaultAllLEDOnColor(
ZHANumberConfigurationEntity, id_suffix="led_color_when_on"
):
"""Inovelli default all led color when on configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[15]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 255
_zcl_attribute: str = "led_color_when_on"
_attr_name: str = "Default all LED on color"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliDefaultAllLEDOffColor(
ZHANumberConfigurationEntity, id_suffix="led_color_when_off"
):
"""Inovelli default all led color when off configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[15]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 255
_zcl_attribute: str = "led_color_when_off"
_attr_name: str = "Default all LED off color"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliDefaultAllLEDOnIntensity(
ZHANumberConfigurationEntity, id_suffix="led_intensity_when_on"
):
"""Inovelli default all led intensity when on configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[16]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 100
_zcl_attribute: str = "led_intensity_when_on"
_attr_name: str = "Default all LED on intensity"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
class InovelliDefaultAllLEDOffIntensity(
ZHANumberConfigurationEntity, id_suffix="led_intensity_when_off"
):
"""Inovelli default all led intensity when off configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_icon: str = ICONS[16]
_attr_native_min_value: float = 0
_attr_native_max_value: float = 100
_zcl_attribute: str = "led_intensity_when_off"
_attr_name: str = "Default all LED off intensity"

View File

@ -21,6 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery
from .core.const import (
CHANNEL_IAS_WD,
CHANNEL_INOVELLI,
CHANNEL_ON_OFF,
DATA_ZHA,
SIGNAL_ADD_ENTITIES,
@ -68,7 +69,7 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity):
"""Representation of a ZHA select entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_name: str
_attribute: str
_enum: type[Enum]
def __init__(
@ -79,7 +80,7 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity):
**kwargs: Any,
) -> None:
"""Init this select entity."""
self._attr_name = self._enum.__name__
self._attribute = self._enum.__name__
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
self._channel: ZigbeeChannel = channels[0]
super().__init__(unique_id, zha_device, channels, **kwargs)
@ -87,21 +88,21 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity):
@property
def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state."""
option = self._channel.data_cache.get(self._attr_name)
option = self._channel.data_cache.get(self._attribute)
if option is None:
return None
return option.name.replace("_", " ")
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
self._channel.data_cache[self._attr_name] = self._enum[option.replace(" ", "_")]
self._channel.data_cache[self._attribute] = self._enum[option.replace(" ", "_")]
self.async_write_ha_state()
@callback
def async_restore_last_state(self, last_state) -> None:
"""Restore previous state."""
if last_state.state and last_state.state != STATE_UNKNOWN:
self._channel.data_cache[self._attr_name] = self._enum[
self._channel.data_cache[self._attribute] = self._enum[
last_state.state.replace(" ", "_")
]
@ -285,3 +286,40 @@ class AqaraCurtainMode(ZCLEnumSelectEntity, id_suffix="window_covering_mode"):
_select_attr = "window_covering_mode"
_enum = AqaraE1ReverseDirection
class InovelliOutputMode(types.enum1):
"""Inovelli output mode."""
Dimmer = 0x00
OnOff = 0x01
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliOutputModeEntity(ZCLEnumSelectEntity, id_suffix="output_mode"):
"""Inovelli output mode control."""
_select_attr = "output_mode"
_enum = InovelliOutputMode
_attr_name: str = "Output mode"
class InovelliSwitchType(types.enum8):
"""Inovelli output mode."""
Load_Only = 0x00
Three_Way_Dumb = 0x01
Three_Way_AUX = 0x02
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliSwitchTypeEntity(ZCLEnumSelectEntity, id_suffix="switch_type"):
"""Inovelli switch type control."""
_select_attr = "switch_type"
_enum = InovelliSwitchType
_attr_name: str = "Switch type"

View File

@ -160,7 +160,12 @@
}
},
"device_automation": {
"action_type": { "squawk": "Squawk", "warn": "Warn" },
"action_type": {
"squawk": "Squawk",
"warn": "Warn",
"issue_all_led_effect": "Issue effect for all LEDs",
"issue_individual_led_effect": "Issue effect for individual LED"
},
"trigger_type": {
"remote_button_short_press": "\"{subtype}\" button pressed",
"remote_button_short_release": "\"{subtype}\" button released",

View File

@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery
from .core.const import (
CHANNEL_INOVELLI,
CHANNEL_ON_OFF,
DATA_ZHA,
SIGNAL_ADD_ENTITIES,
@ -309,3 +310,81 @@ class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"):
"""ZHA BinarySensor."""
_zcl_attribute: str = "disable_led"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliInvertSwitch(ZHASwitchConfigurationEntity, id_suffix="invert_switch"):
"""Inovelli invert switch control."""
_zcl_attribute: str = "invert_switch"
_attr_name: str = "Invert switch"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_mode"):
"""Inovelli smart bulb mode control."""
_zcl_attribute: str = "smart_bulb_mode"
_attr_name: str = "Smart bulb mode"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliDoubleTapForFullBrightness(
ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_full_brightness"
):
"""Inovelli double tap for full brightness control."""
_zcl_attribute: str = "double_tap_up_for_full_brightness"
_attr_name: str = "Double tap full brightness"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliLocalProtection(
ZHASwitchConfigurationEntity, id_suffix="local_protection"
):
"""Inovelli local protection control."""
_zcl_attribute: str = "local_protection"
_attr_name: str = "Local protection"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliOnOffLEDMode(ZHASwitchConfigurationEntity, id_suffix="on_off_led_mode"):
"""Inovelli only 1 LED mode control."""
_zcl_attribute: str = "on_off_led_mode"
_attr_name: str = "Only 1 LED mode"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliFirmwareProgressLED(
ZHASwitchConfigurationEntity, id_suffix="firmware_progress_led"
):
"""Inovelli firmware progress LED control."""
_zcl_attribute: str = "firmware_progress_led"
_attr_name: str = "Firmware progress LED"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliRelayClickInOnOffMode(
ZHASwitchConfigurationEntity, id_suffix="relay_click_in_on_off_mode"
):
"""Inovelli relay click in on off mode control."""
_zcl_attribute: str = "relay_click_in_on_off_mode"
_attr_name: str = "Disable relay click in on off mode"

View File

@ -117,7 +117,9 @@
"device_automation": {
"action_type": {
"squawk": "Squawk",
"warn": "Warn"
"warn": "Warn",
"issue_all_led_effect": "Issue effect for all LEDs",
"issue_individual_led_effect": "Issue effect for individual LED"
},
"trigger_subtype": {
"both_buttons": "Both buttons",

View File

@ -1,7 +1,8 @@
"""The test for zha device automation actions."""
from unittest.mock import patch
from unittest.mock import call, patch
import pytest
from zhaquirks.inovelli.VZM31SN import InovelliVZM31SNv11
import zigpy.profiles.zha
import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.security as security
@ -16,7 +17,12 @@ from homeassistant.setup import async_setup_component
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE
from tests.common import async_get_device_automations, async_mock_service, mock_coro
from tests.common import (
assert_lists_same,
async_get_device_automations,
async_mock_service,
mock_coro,
)
from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401
SHORT_PRESS = "remote_button_short_press"
@ -31,10 +37,13 @@ def required_platforms_only():
"homeassistant.components.zha.PLATFORMS",
(
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.DEVICE_TRACKER,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.SIREN,
),
):
@ -62,6 +71,36 @@ async def device_ias(hass, zigpy_device_mock, zha_device_joined_restored):
return zigpy_device, zha_device
@pytest.fixture
async def device_inovelli(hass, zigpy_device_mock, zha_device_joined):
"""Inovelli device fixture."""
zigpy_device = zigpy_device_mock(
{
1: {
SIG_EP_INPUT: [
general.Basic.cluster_id,
general.Identify.cluster_id,
general.OnOff.cluster_id,
general.LevelControl.cluster_id,
0xFC31,
],
SIG_EP_OUTPUT: [],
SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT,
}
},
ieee="00:1d:8f:08:0c:90:69:6b",
manufacturer="Inovelli",
model="VZM31-SN",
quirk=InovelliVZM31SNv11,
)
zha_device = await zha_device_joined(zigpy_device)
zha_device.update_available(True)
await hass.async_block_till_done()
return zigpy_device, zha_device
async def test_get_actions(hass, device_ias):
"""Test we get the expected actions from a zha device."""
@ -112,21 +151,108 @@ async def test_get_actions(hass, device_ias):
},
]
assert actions == expected_actions
assert_lists_same(actions, expected_actions)
async def test_action(hass, device_ias):
async def test_get_inovelli_actions(hass, device_inovelli):
"""Test we get the expected actions from a zha device."""
inovelli_ieee_address = str(device_inovelli[0].ieee)
ha_device_registry = dr.async_get(hass)
inovelli_reg_device = ha_device_registry.async_get_device(
{(DOMAIN, inovelli_ieee_address)}
)
actions = await async_get_device_automations(
hass, DeviceAutomationType.ACTION, inovelli_reg_device.id
)
expected_actions = [
{
"device_id": inovelli_reg_device.id,
"domain": DOMAIN,
"metadata": {},
"type": "issue_all_led_effect",
},
{
"device_id": inovelli_reg_device.id,
"domain": DOMAIN,
"metadata": {},
"type": "issue_individual_led_effect",
},
{
"device_id": inovelli_reg_device.id,
"domain": Platform.BUTTON,
"entity_id": "button.inovelli_vzm31_sn_identifybutton",
"metadata": {"secondary": True},
"type": "press",
},
{
"device_id": inovelli_reg_device.id,
"domain": Platform.LIGHT,
"entity_id": "light.inovelli_vzm31_sn_light",
"metadata": {"secondary": False},
"type": "turn_off",
},
{
"device_id": inovelli_reg_device.id,
"domain": Platform.LIGHT,
"entity_id": "light.inovelli_vzm31_sn_light",
"metadata": {"secondary": False},
"type": "turn_on",
},
{
"device_id": inovelli_reg_device.id,
"domain": Platform.LIGHT,
"entity_id": "light.inovelli_vzm31_sn_light",
"metadata": {"secondary": False},
"type": "toggle",
},
{
"device_id": inovelli_reg_device.id,
"domain": Platform.LIGHT,
"entity_id": "light.inovelli_vzm31_sn_light",
"metadata": {"secondary": False},
"type": "brightness_increase",
},
{
"device_id": inovelli_reg_device.id,
"domain": Platform.LIGHT,
"entity_id": "light.inovelli_vzm31_sn_light",
"metadata": {"secondary": False},
"type": "brightness_decrease",
},
{
"device_id": inovelli_reg_device.id,
"domain": Platform.LIGHT,
"entity_id": "light.inovelli_vzm31_sn_light",
"metadata": {"secondary": False},
"type": "flash",
},
]
assert_lists_same(actions, expected_actions)
async def test_action(hass, device_ias, device_inovelli):
"""Test for executing a zha device action."""
zigpy_device, zha_device = device_ias
inovelli_zigpy_device, inovelli_zha_device = device_inovelli
zigpy_device.device_automation_triggers = {
(SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}
}
ieee_address = str(zha_device.ieee)
inovelli_ieee_address = str(inovelli_zha_device.ieee)
ha_device_registry = dr.async_get(hass)
reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)})
inovelli_reg_device = ha_device_registry.async_get_device(
{(DOMAIN, inovelli_ieee_address)}
)
cluster = inovelli_zigpy_device.endpoints[1].in_clusters[0xFC31]
with patch(
"zigpy.zcl.Cluster.request",
@ -145,11 +271,32 @@ async def test_action(hass, device_ias):
"type": SHORT_PRESS,
"subtype": SHORT_PRESS,
},
"action": {
"domain": DOMAIN,
"device_id": reg_device.id,
"type": "warn",
},
"action": [
{
"domain": DOMAIN,
"device_id": reg_device.id,
"type": "warn",
},
{
"domain": DOMAIN,
"device_id": inovelli_reg_device.id,
"type": "issue_all_led_effect",
"effect_type": "Open_Close",
"duration": 5,
"level": 10,
"color": 41,
},
{
"domain": DOMAIN,
"device_id": inovelli_reg_device.id,
"type": "issue_individual_led_effect",
"effect_type": "Open_Close",
"led_number": 1,
"duration": 5,
"level": 10,
"color": 41,
},
],
}
]
},
@ -167,6 +314,41 @@ async def test_action(hass, device_ias):
assert calls[0].service == "warning_device_warn"
assert calls[0].data["ieee"] == ieee_address
assert len(cluster.request.mock_calls) == 2
assert (
call(
False,
cluster.commands_by_name["led_effect"].id,
cluster.commands_by_name["led_effect"].schema,
6,
41,
10,
5,
expect_reply=False,
manufacturer=4151,
tries=1,
tsn=None,
)
in cluster.request.call_args_list
)
assert (
call(
False,
cluster.commands_by_name["individual_led_effect"].id,
cluster.commands_by_name["individual_led_effect"].schema,
1,
6,
41,
10,
5,
expect_reply=False,
manufacturer=4151,
tries=1,
tsn=None,
)
in cluster.request.call_args_list
)
async def test_invalid_zha_event_type(hass, device_ias):
"""Test that unexpected types are not passed to `zha_send_event`."""