Add support for custom configurations in ZHA (#48423)
* initial configuration options * first crack at saving the data * constants * implement initial options * make more dynamic * fix unload and reload of the config entry * update unload
This commit is contained in:
parent
9c11f6547a
commit
fe80afdb86
7 changed files with 142 additions and 12 deletions
|
@ -29,6 +29,7 @@ from .core.const import (
|
|||
DATA_ZHA_DISPATCHERS,
|
||||
DATA_ZHA_GATEWAY,
|
||||
DATA_ZHA_PLATFORM_LOADED,
|
||||
DATA_ZHA_SHUTDOWN_TASK,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
|
@ -121,7 +122,9 @@ async def async_setup_entry(hass, config_entry):
|
|||
await zha_data[DATA_ZHA_GATEWAY].shutdown()
|
||||
await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage()
|
||||
|
||||
hass.bus.async_listen_once(ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown)
|
||||
zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once(
|
||||
ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown
|
||||
)
|
||||
asyncio.create_task(async_load_entities(hass))
|
||||
return True
|
||||
|
||||
|
@ -129,6 +132,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload ZHA config entry."""
|
||||
await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown()
|
||||
await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_update_device_storage()
|
||||
|
||||
GROUP_PROBE.cleanup()
|
||||
api.async_unload_api(hass)
|
||||
|
@ -137,8 +141,15 @@ async def async_unload_entry(hass, config_entry):
|
|||
for unsub_dispatcher in dispatchers:
|
||||
unsub_dispatcher()
|
||||
|
||||
for platform in PLATFORMS:
|
||||
await hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
# our components don't have unload methods so no need to look at return values
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
for platform in PLATFORMS
|
||||
]
|
||||
)
|
||||
|
||||
hass.data[DATA_ZHA][DATA_ZHA_SHUTDOWN_TASK]()
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import logging
|
|||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from zigpy.config.validators import cv_boolean
|
||||
from zigpy.types.named import EUI64
|
||||
import zigpy.zdo.types as zdo_types
|
||||
|
||||
|
@ -40,6 +41,7 @@ from .core.const import (
|
|||
CLUSTER_COMMANDS_SERVER,
|
||||
CLUSTER_TYPE_IN,
|
||||
CLUSTER_TYPE_OUT,
|
||||
CUSTOM_CONFIGURATION,
|
||||
DATA_ZHA,
|
||||
DATA_ZHA_GATEWAY,
|
||||
DOMAIN,
|
||||
|
@ -52,6 +54,7 @@ from .core.const import (
|
|||
WARNING_DEVICE_SQUAWK_MODE_ARMED,
|
||||
WARNING_DEVICE_STROBE_HIGH,
|
||||
WARNING_DEVICE_STROBE_YES,
|
||||
ZHA_CONFIG_SCHEMAS,
|
||||
)
|
||||
from .core.group import GroupMember
|
||||
from .core.helpers import (
|
||||
|
@ -882,6 +885,63 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati
|
|||
zdo.debug(fmt, *(log_msg[2] + (outcome,)))
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({vol.Required(TYPE): "zha/configuration"})
|
||||
async def websocket_get_configuration(hass, connection, msg):
|
||||
"""Get ZHA configuration."""
|
||||
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||
import voluptuous_serialize # pylint: disable=import-outside-toplevel
|
||||
|
||||
def custom_serializer(schema: Any) -> Any:
|
||||
"""Serialize additional types for voluptuous_serialize."""
|
||||
if schema is cv_boolean:
|
||||
return {"type": "bool"}
|
||||
if schema is vol.Schema:
|
||||
return voluptuous_serialize.convert(
|
||||
schema, custom_serializer=custom_serializer
|
||||
)
|
||||
|
||||
return cv.custom_serializer(schema)
|
||||
|
||||
data = {"schemas": {}, "data": {}}
|
||||
for section, schema in ZHA_CONFIG_SCHEMAS.items():
|
||||
data["schemas"][section] = voluptuous_serialize.convert(
|
||||
schema, custom_serializer=custom_serializer
|
||||
)
|
||||
data["data"][section] = zha_gateway.config_entry.options.get(
|
||||
CUSTOM_CONFIGURATION, {}
|
||||
).get(section, {})
|
||||
connection.send_result(msg[ID], data)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zha/configuration/update",
|
||||
vol.Required("data"): ZHA_CONFIG_SCHEMAS,
|
||||
}
|
||||
)
|
||||
async def websocket_update_zha_configuration(hass, connection, msg):
|
||||
"""Update the ZHA configuration."""
|
||||
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||
options = zha_gateway.config_entry.options
|
||||
data_to_save = {**options, **{CUSTOM_CONFIGURATION: msg["data"]}}
|
||||
|
||||
_LOGGER.info(
|
||||
"Updating ZHA custom configuration options from %s to %s",
|
||||
options,
|
||||
data_to_save,
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
zha_gateway.config_entry, options=data_to_save
|
||||
)
|
||||
status = await hass.config_entries.async_reload(zha_gateway.config_entry.entry_id)
|
||||
connection.send_result(msg[ID], status)
|
||||
|
||||
|
||||
@callback
|
||||
def async_load_api(hass):
|
||||
"""Set up the web socket API."""
|
||||
|
@ -1189,6 +1249,8 @@ def async_load_api(hass):
|
|||
websocket_api.async_register_command(hass, websocket_bind_devices)
|
||||
websocket_api.async_register_command(hass, websocket_unbind_devices)
|
||||
websocket_api.async_register_command(hass, websocket_update_topology)
|
||||
websocket_api.async_register_command(hass, websocket_get_configuration)
|
||||
websocket_api.async_register_command(hass, websocket_update_zha_configuration)
|
||||
|
||||
|
||||
@callback
|
||||
|
|
|
@ -5,6 +5,7 @@ import enum
|
|||
import logging
|
||||
|
||||
import bellows.zigbee.application
|
||||
import voluptuous as vol
|
||||
from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import
|
||||
import zigpy_cc.zigbee.application
|
||||
import zigpy_deconz.zigbee.application
|
||||
|
@ -22,6 +23,7 @@ from homeassistant.components.lock import DOMAIN as LOCK
|
|||
from homeassistant.components.number import DOMAIN as NUMBER
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .typing import CALLABLE_T
|
||||
|
||||
|
@ -118,13 +120,24 @@ PLATFORMS = (
|
|||
|
||||
CONF_BAUDRATE = "baudrate"
|
||||
CONF_DATABASE = "database_path"
|
||||
CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition"
|
||||
CONF_DEVICE_CONFIG = "device_config"
|
||||
CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join"
|
||||
CONF_ENABLE_QUIRKS = "enable_quirks"
|
||||
CONF_FLOWCONTROL = "flow_control"
|
||||
CONF_RADIO_TYPE = "radio_type"
|
||||
CONF_USB_PATH = "usb_path"
|
||||
CONF_ZIGPY = "zigpy_config"
|
||||
|
||||
CONF_ZHA_OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int,
|
||||
vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
CUSTOM_CONFIGURATION = "custom_configuration"
|
||||
|
||||
DATA_DEVICE_CONFIG = "zha_device_config"
|
||||
DATA_ZHA = "zha"
|
||||
DATA_ZHA_CONFIG = "config"
|
||||
|
@ -133,6 +146,7 @@ DATA_ZHA_CORE_EVENTS = "zha_core_events"
|
|||
DATA_ZHA_DISPATCHERS = "zha_dispatchers"
|
||||
DATA_ZHA_GATEWAY = "zha_gateway"
|
||||
DATA_ZHA_PLATFORM_LOADED = "platform_loaded"
|
||||
DATA_ZHA_SHUTDOWN_TASK = "zha_shutdown_task"
|
||||
|
||||
DEBUG_COMP_BELLOWS = "bellows"
|
||||
DEBUG_COMP_ZHA = "homeassistant.components.zha"
|
||||
|
@ -176,6 +190,9 @@ POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown"
|
|||
PRESET_SCHEDULE = "schedule"
|
||||
PRESET_COMPLEX = "complex"
|
||||
|
||||
ZHA_OPTIONS = "zha_options"
|
||||
ZHA_CONFIG_SCHEMAS = {ZHA_OPTIONS: CONF_ZHA_OPTIONS_SCHEMA}
|
||||
|
||||
|
||||
class RadioType(enum.Enum):
|
||||
"""Possible options for radio type."""
|
||||
|
|
|
@ -56,6 +56,7 @@ from .const import (
|
|||
CLUSTER_COMMANDS_SERVER,
|
||||
CLUSTER_TYPE_IN,
|
||||
CLUSTER_TYPE_OUT,
|
||||
CONF_ENABLE_IDENTIFY_ON_JOIN,
|
||||
EFFECT_DEFAULT_VARIANT,
|
||||
EFFECT_OKAY,
|
||||
POWER_BATTERY_OR_UNKNOWN,
|
||||
|
@ -66,7 +67,7 @@ from .const import (
|
|||
UNKNOWN_MANUFACTURER,
|
||||
UNKNOWN_MODEL,
|
||||
)
|
||||
from .helpers import LogMixin
|
||||
from .helpers import LogMixin, async_get_zha_config_value
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours
|
||||
|
@ -395,13 +396,20 @@ class ZHADevice(LogMixin):
|
|||
|
||||
async def async_configure(self):
|
||||
"""Configure the device."""
|
||||
should_identify = async_get_zha_config_value(
|
||||
self._zha_gateway.config_entry, CONF_ENABLE_IDENTIFY_ON_JOIN, True
|
||||
)
|
||||
self.debug("started configuration")
|
||||
await self._channels.async_configure()
|
||||
self.debug("completed configuration")
|
||||
entry = self.gateway.zha_storage.async_create_or_update_device(self)
|
||||
self.debug("stored in registry: %s", entry)
|
||||
|
||||
if self._channels.identify_ch is not None and not self.skip_configuration:
|
||||
if (
|
||||
should_identify
|
||||
and self._channels.identify_ch is not None
|
||||
and not self.skip_configuration
|
||||
):
|
||||
await self._channels.identify_ch.trigger_effect(
|
||||
EFFECT_OKAY, EFFECT_DEFAULT_VARIANT
|
||||
)
|
||||
|
|
|
@ -127,7 +127,7 @@ class ZHAGateway:
|
|||
}
|
||||
self.debug_enabled = False
|
||||
self._log_relay_handler = LogRelayHandler(hass, self)
|
||||
self._config_entry = config_entry
|
||||
self.config_entry = config_entry
|
||||
self._unsubs = []
|
||||
|
||||
async def async_initialize(self):
|
||||
|
@ -139,7 +139,7 @@ class ZHAGateway:
|
|||
self.ha_device_registry = await get_dev_reg(self._hass)
|
||||
self.ha_entity_registry = await get_ent_reg(self._hass)
|
||||
|
||||
radio_type = self._config_entry.data[CONF_RADIO_TYPE]
|
||||
radio_type = self.config_entry.data[CONF_RADIO_TYPE]
|
||||
|
||||
app_controller_cls = RadioType[radio_type].controller
|
||||
self.radio_description = RadioType[radio_type].description
|
||||
|
@ -150,7 +150,7 @@ class ZHAGateway:
|
|||
os.path.join(self._hass.config.config_dir, DEFAULT_DATABASE_NAME),
|
||||
)
|
||||
app_config[CONF_DATABASE] = database
|
||||
app_config[CONF_DEVICE] = self._config_entry.data[CONF_DEVICE]
|
||||
app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE]
|
||||
|
||||
app_config = app_controller_cls.SCHEMA(app_config)
|
||||
try:
|
||||
|
@ -506,7 +506,7 @@ class ZHAGateway:
|
|||
zha_device = ZHADevice.new(self._hass, zigpy_device, self, restored)
|
||||
self._devices[zigpy_device.ieee] = zha_device
|
||||
device_registry_device = self.ha_device_registry.async_get_or_create(
|
||||
config_entry_id=self._config_entry.entry_id,
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
connections={(CONNECTION_ZIGBEE, str(zha_device.ieee))},
|
||||
identifiers={(DOMAIN, str(zha_device.ieee))},
|
||||
name=zha_device.name,
|
||||
|
|
|
@ -24,7 +24,14 @@ import zigpy.zdo.types as zdo_types
|
|||
|
||||
from homeassistant.core import State, callback
|
||||
|
||||
from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY
|
||||
from .const import (
|
||||
CLUSTER_TYPE_IN,
|
||||
CLUSTER_TYPE_OUT,
|
||||
CUSTOM_CONFIGURATION,
|
||||
DATA_ZHA,
|
||||
DATA_ZHA_GATEWAY,
|
||||
ZHA_OPTIONS,
|
||||
)
|
||||
from .registries import BINDABLE_CLUSTERS
|
||||
from .typing import ZhaDeviceType, ZigpyClusterType
|
||||
|
||||
|
@ -122,6 +129,16 @@ def async_is_bindable_target(source_zha_device, target_zha_device):
|
|||
return False
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_zha_config_value(config_entry, config_key, default):
|
||||
"""Get the value for the specified configuration from the zha config entry."""
|
||||
return (
|
||||
config_entry.options.get(CUSTOM_CONFIGURATION, {})
|
||||
.get(ZHA_OPTIONS, {})
|
||||
.get(config_key, default)
|
||||
)
|
||||
|
||||
|
||||
async def async_get_zha_device(hass, device_id):
|
||||
"""Get a ZHA device for the given device registry id."""
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
|
|
|
@ -47,6 +47,7 @@ from .core.const import (
|
|||
CHANNEL_COLOR,
|
||||
CHANNEL_LEVEL,
|
||||
CHANNEL_ON_OFF,
|
||||
CONF_DEFAULT_LIGHT_TRANSITION,
|
||||
DATA_ZHA,
|
||||
DATA_ZHA_DISPATCHERS,
|
||||
EFFECT_BLINK,
|
||||
|
@ -56,7 +57,7 @@ from .core.const import (
|
|||
SIGNAL_ATTR_UPDATED,
|
||||
SIGNAL_SET_LEVEL,
|
||||
)
|
||||
from .core.helpers import LogMixin
|
||||
from .core.helpers import LogMixin, async_get_zha_config_value
|
||||
from .core.registries import ZHA_ENTITIES
|
||||
from .core.typing import ZhaDeviceType
|
||||
from .entity import ZhaEntity, ZhaGroupEntity
|
||||
|
@ -139,6 +140,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
self._level_channel = None
|
||||
self._color_channel = None
|
||||
self._identify_channel = None
|
||||
self._default_transition = None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
|
@ -207,7 +209,13 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
transition = kwargs.get(light.ATTR_TRANSITION)
|
||||
duration = transition * 10 if transition else DEFAULT_TRANSITION
|
||||
duration = (
|
||||
transition * 10
|
||||
if transition
|
||||
else self._default_transition * 10
|
||||
if self._default_transition
|
||||
else DEFAULT_TRANSITION
|
||||
)
|
||||
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
|
||||
effect = kwargs.get(light.ATTR_EFFECT)
|
||||
flash = kwargs.get(light.ATTR_FLASH)
|
||||
|
@ -389,6 +397,10 @@ class Light(BaseLight, ZhaEntity):
|
|||
if effect_list:
|
||||
self._effect_list = effect_list
|
||||
|
||||
self._default_transition = async_get_zha_config_value(
|
||||
zha_device.gateway.config_entry, CONF_DEFAULT_LIGHT_TRANSITION, 0
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_set_state(self, attr_id, attr_name, value):
|
||||
"""Set the state."""
|
||||
|
@ -544,6 +556,9 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
|||
self._color_channel = group.endpoint[Color.cluster_id]
|
||||
self._identify_channel = group.endpoint[Identify.cluster_id]
|
||||
self._debounced_member_refresh = None
|
||||
self._default_transition = async_get_zha_config_value(
|
||||
zha_device.gateway.config_entry, CONF_DEFAULT_LIGHT_TRANSITION, 0
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when about to be added to hass."""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue