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
17 changes: 14 additions & 3 deletions homeassistant/components/zha/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
DATA_ZHA_DISPATCHERS,
DATA_ZHA_GATEWAY,
DATA_ZHA_PLATFORM_LOADED,
DATA_ZHA_SHUTDOWN_TASK,
DOMAIN,
PLATFORMS,
SIGNAL_ADD_ENTITIES,
Expand Down Expand Up @@ -121,14 +122,17 @@ async def async_zha_shutdown(event):
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


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)
Expand All @@ -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

Expand Down
62 changes: 62 additions & 0 deletions homeassistant/components/zha/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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

Expand Down Expand Up @@ -40,6 +41,7 @@
CLUSTER_COMMANDS_SERVER,
CLUSTER_TYPE_IN,
CLUSTER_TYPE_OUT,
CUSTOM_CONFIGURATION,
DATA_ZHA,
DATA_ZHA_GATEWAY,
DOMAIN,
Expand All @@ -52,6 +54,7 @@
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 (
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -1189,6 +1249,8 @@ async def warning_device_warn(service):
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
Expand Down
17 changes: 17 additions & 0 deletions homeassistant/components/zha/core/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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
Expand All @@ -22,6 +23,7 @@
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

Expand Down Expand Up @@ -118,13 +120,24 @@

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,
Copy link
Copy Markdown
Member

@frenck frenck Apr 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should not be a ZHA-specific thing...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what’s the protocol here? I was under the impression that things usually start out in integrations and then back their way into core...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no protocol, it has been discussed more often. If the PR title reflected the actual features in this PR, I would have responded to it way earlier.

IMHO, use the effort to solve this globally.

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"
Expand All @@ -133,6 +146,7 @@
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"
Expand Down Expand Up @@ -176,6 +190,9 @@
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."""
Expand Down
12 changes: 10 additions & 2 deletions homeassistant/components/zha/core/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
CLUSTER_COMMANDS_SERVER,
CLUSTER_TYPE_IN,
CLUSTER_TYPE_OUT,
CONF_ENABLE_IDENTIFY_ON_JOIN,
EFFECT_DEFAULT_VARIANT,
EFFECT_OKAY,
POWER_BATTERY_OR_UNKNOWN,
Expand All @@ -66,7 +67,7 @@
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
Expand Down Expand Up @@ -395,13 +396,20 @@ def device_info(self):

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
)
Expand Down
8 changes: 4 additions & 4 deletions homeassistant/components/zha/core/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def __init__(self, hass, config, config_entry):
}
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):
Expand All @@ -139,7 +139,7 @@ async def async_initialize(self):
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
Expand All @@ -150,7 +150,7 @@ async def async_initialize(self):
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:
Expand Down Expand Up @@ -506,7 +506,7 @@ def _async_get_or_create_device(
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,
Expand Down
19 changes: 18 additions & 1 deletion homeassistant/components/zha/core/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@

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

Expand Down Expand Up @@ -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()
Expand Down
19 changes: 17 additions & 2 deletions homeassistant/components/zha/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
CHANNEL_COLOR,
CHANNEL_LEVEL,
CHANNEL_ON_OFF,
CONF_DEFAULT_LIGHT_TRANSITION,
DATA_ZHA,
DATA_ZHA_DISPATCHERS,
EFFECT_BLINK,
Expand All @@ -56,7 +57,7 @@
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
Expand Down Expand Up @@ -139,6 +140,7 @@ def __init__(self, *args, **kwargs):
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]:
Expand Down Expand Up @@ -207,7 +209,13 @@ def supported_features(self):
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)
Expand Down Expand Up @@ -389,6 +397,10 @@ def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs):
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."""
Expand Down Expand Up @@ -544,6 +556,9 @@ def __init__(
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."""
Expand Down