mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
Add support for Zigbee Home Automation (#6263)
* Add support for Zigbee Home Automation * Fewer magic numbers * Make optional device config work * Remove non-zha device_tracker stuff * Always return boolean from is_on * Only pass through JSON serializable discovery_info * Update to bellows 0.2.4 * Fewer magic numbers in binary sensor * Populate const structures from a function * Update bellows to 0.2.6 * Fewer magic numbers in light * Take all possible clusters when overriding * Update bellows to 0.2.7
This commit is contained in:
parent
699cc7213d
commit
d79f89e168
@ -154,6 +154,10 @@ omit =
|
|||||||
homeassistant/components/tado.py
|
homeassistant/components/tado.py
|
||||||
homeassistant/components/*/tado.py
|
homeassistant/components/*/tado.py
|
||||||
|
|
||||||
|
homeassistant/components/zha/__init__.py
|
||||||
|
homeassistant/components/zha/const.py
|
||||||
|
homeassistant/components/*/zha.py
|
||||||
|
|
||||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||||
homeassistant/components/alarm_control_panel/concord232.py
|
homeassistant/components/alarm_control_panel/concord232.py
|
||||||
homeassistant/components/alarm_control_panel/nx584.py
|
homeassistant/components/alarm_control_panel/nx584.py
|
||||||
|
89
homeassistant/components/binary_sensor/zha.py
Normal file
89
homeassistant/components/binary_sensor/zha.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"""
|
||||||
|
Binary sensors on Zigbee Home Automation networks.
|
||||||
|
|
||||||
|
For more details on this platform, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/binary_sensor.zha/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||||
|
from homeassistant.components import zha
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['zha']
|
||||||
|
|
||||||
|
# ZigBee Cluster Library Zone Type to Home Assistant device class
|
||||||
|
CLASS_MAPPING = {
|
||||||
|
0x000d: 'motion',
|
||||||
|
0x0015: 'opening',
|
||||||
|
0x0028: 'smoke',
|
||||||
|
0x002a: 'moisture',
|
||||||
|
0x002b: 'gas',
|
||||||
|
0x002d: 'vibration',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Setup Zigbee Home Automation binary sensors."""
|
||||||
|
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||||
|
|
||||||
|
clusters = discovery_info['clusters']
|
||||||
|
|
||||||
|
device_class = None
|
||||||
|
cluster = [c for c in clusters if isinstance(c, IasZone)][0]
|
||||||
|
if discovery_info['new_join']:
|
||||||
|
yield from cluster.bind()
|
||||||
|
ieee = cluster.endpoint.device.application.ieee
|
||||||
|
yield from cluster.write_attributes({'cie_addr': ieee})
|
||||||
|
|
||||||
|
try:
|
||||||
|
zone_type = yield from cluster['zone_type']
|
||||||
|
device_class = CLASS_MAPPING.get(zone_type, None)
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
# If we fail to read from the device, use a non-specific class
|
||||||
|
pass
|
||||||
|
|
||||||
|
sensor = BinarySensor(device_class, **discovery_info)
|
||||||
|
async_add_devices([sensor])
|
||||||
|
|
||||||
|
|
||||||
|
class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||||
|
"""ZHA Binary Sensor."""
|
||||||
|
|
||||||
|
_domain = DOMAIN
|
||||||
|
|
||||||
|
def __init__(self, device_class, **kwargs):
|
||||||
|
"""Initialize ZHA binary sensor."""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._device_class = device_class
|
||||||
|
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||||
|
self._ias_zone_cluster = self._clusters[IasZone.cluster_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return True if entity is on."""
|
||||||
|
if self._state == 'unknown':
|
||||||
|
return False
|
||||||
|
return bool(self._state)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
|
return self._device_class
|
||||||
|
|
||||||
|
def cluster_command(self, aps_frame, tsn, command_id, args):
|
||||||
|
"""Handle commands received to this cluster."""
|
||||||
|
if command_id == 0:
|
||||||
|
self._state = args[0] & 3
|
||||||
|
_LOGGER.debug("Updated alarm state: %s", self._state)
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
elif command_id == 1:
|
||||||
|
_LOGGER.debug("Enroll requested")
|
||||||
|
self.hass.add_job(self._ias_zone_cluster.enroll_response(0, 0))
|
132
homeassistant/components/light/zha.py
Normal file
132
homeassistant/components/light/zha.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
"""
|
||||||
|
Lights on Zigbee Home Automation networks.
|
||||||
|
|
||||||
|
For more details on this platform, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/light.zha/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components import light, zha
|
||||||
|
from homeassistant.util.color import HASS_COLOR_MIN, color_RGB_to_xy
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['zha']
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Setup Zigbee Home Automation lights."""
|
||||||
|
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
endpoint = discovery_info['endpoint']
|
||||||
|
try:
|
||||||
|
primaries = yield from endpoint.light_color['num_primaries']
|
||||||
|
discovery_info['num_primaries'] = primaries
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async_add_devices([Light(**discovery_info)])
|
||||||
|
|
||||||
|
|
||||||
|
class Light(zha.Entity, light.Light):
|
||||||
|
"""ZHA or ZLL light."""
|
||||||
|
|
||||||
|
_domain = light.DOMAIN
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize ZHA light."""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._supported_features = 0
|
||||||
|
self._color_temp = None
|
||||||
|
self._xy_color = None
|
||||||
|
self._brightness = None
|
||||||
|
|
||||||
|
import bellows.zigbee.zcl.clusters as zcl_clusters
|
||||||
|
if zcl_clusters.general.LevelControl.cluster_id in self._clusters:
|
||||||
|
self._supported_features |= light.SUPPORT_BRIGHTNESS
|
||||||
|
self._brightness = 0
|
||||||
|
if zcl_clusters.lighting.Color.cluster_id in self._clusters:
|
||||||
|
# Not sure all color lights necessarily support this directly
|
||||||
|
# Should we emulate it?
|
||||||
|
self._supported_features |= light.SUPPORT_COLOR_TEMP
|
||||||
|
self._color_temp = HASS_COLOR_MIN
|
||||||
|
# Silly heuristic, not sure if it works widely
|
||||||
|
if kwargs.get('num_primaries', 1) >= 3:
|
||||||
|
self._supported_features |= light.SUPPORT_XY_COLOR
|
||||||
|
self._supported_features |= light.SUPPORT_RGB_COLOR
|
||||||
|
self._xy_color = (1.0, 1.0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return True if entity is on."""
|
||||||
|
if self._state == 'unknown':
|
||||||
|
return False
|
||||||
|
return bool(self._state)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the entity on."""
|
||||||
|
duration = 5 # tenths of s
|
||||||
|
if light.ATTR_COLOR_TEMP in kwargs:
|
||||||
|
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
||||||
|
yield from self._endpoint.light_color.move_to_color_temp(
|
||||||
|
temperature, duration)
|
||||||
|
self._color_temp = temperature
|
||||||
|
|
||||||
|
if light.ATTR_XY_COLOR in kwargs:
|
||||||
|
self._xy_color = kwargs[light.ATTR_XY_COLOR]
|
||||||
|
elif light.ATTR_RGB_COLOR in kwargs:
|
||||||
|
xyb = color_RGB_to_xy(
|
||||||
|
*(int(val) for val in kwargs[light.ATTR_RGB_COLOR]))
|
||||||
|
self._xy_color = (xyb[0], xyb[1])
|
||||||
|
self._brightness = xyb[2]
|
||||||
|
if light.ATTR_XY_COLOR in kwargs or light.ATTR_RGB_COLOR in kwargs:
|
||||||
|
yield from self._endpoint.light_color.move_to_color(
|
||||||
|
int(self._xy_color[0] * 65535),
|
||||||
|
int(self._xy_color[1] * 65535),
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._brightness is not None:
|
||||||
|
brightness = kwargs.get('brightness', self._brightness or 255)
|
||||||
|
self._brightness = brightness
|
||||||
|
# Move to level with on/off:
|
||||||
|
yield from self._endpoint.level.move_to_level_with_on_off(
|
||||||
|
brightness,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
self._state = 1
|
||||||
|
return
|
||||||
|
|
||||||
|
yield from self._endpoint.on_off.on()
|
||||||
|
self._state = 1
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the entity off."""
|
||||||
|
yield from self._endpoint.on_off.off()
|
||||||
|
self._state = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of this light between 0..255."""
|
||||||
|
return self._brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def xy_color(self):
|
||||||
|
"""Return the XY color value [float, float]."""
|
||||||
|
return self._xy_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self):
|
||||||
|
"""Return the CT color value in mireds."""
|
||||||
|
return self._color_temp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return self._supported_features
|
99
homeassistant/components/sensor/zha.py
Normal file
99
homeassistant/components/sensor/zha.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
Sensors on Zigbee Home Automation networks.
|
||||||
|
|
||||||
|
For more details on this platform, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/sensor.zha/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import DOMAIN
|
||||||
|
from homeassistant.components import zha
|
||||||
|
from homeassistant.const import TEMP_CELSIUS
|
||||||
|
from homeassistant.util.temperature import convert as convert_temperature
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['zha']
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Setup Zigbee Home Automation sensors."""
|
||||||
|
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
sensor = yield from make_sensor(discovery_info)
|
||||||
|
async_add_devices([sensor])
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def make_sensor(discovery_info):
|
||||||
|
"""Factory function for ZHA sensors."""
|
||||||
|
from bellows.zigbee import zcl
|
||||||
|
if isinstance(discovery_info['clusters'][0],
|
||||||
|
zcl.clusters.measurement.TemperatureMeasurement):
|
||||||
|
sensor = TemperatureSensor(**discovery_info)
|
||||||
|
else:
|
||||||
|
sensor = Sensor(**discovery_info)
|
||||||
|
|
||||||
|
clusters = discovery_info['clusters']
|
||||||
|
attr = sensor.value_attribute
|
||||||
|
if discovery_info['new_join']:
|
||||||
|
cluster = clusters[0]
|
||||||
|
yield from cluster.bind()
|
||||||
|
yield from cluster.configure_reporting(
|
||||||
|
attr,
|
||||||
|
300,
|
||||||
|
600,
|
||||||
|
sensor.min_reportable_change,
|
||||||
|
)
|
||||||
|
|
||||||
|
return sensor
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(zha.Entity):
|
||||||
|
"""Base ZHA sensor."""
|
||||||
|
|
||||||
|
_domain = DOMAIN
|
||||||
|
value_attribute = 0
|
||||||
|
min_reportable_change = 1
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""Initialize ZHA sensor."""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
if isinstance(self._state, float):
|
||||||
|
return str(round(self._state, 2))
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def attribute_updated(self, attribute, value):
|
||||||
|
"""Handle attribute update from device."""
|
||||||
|
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
|
||||||
|
if attribute == self.value_attribute:
|
||||||
|
self._state = value
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class TemperatureSensor(Sensor):
|
||||||
|
"""ZHA temperature sensor."""
|
||||||
|
|
||||||
|
min_reportable_change = 50 # 0.5'C
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement of this entityy."""
|
||||||
|
return self.hass.config.units.temperature_unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
if self._state == 'unknown':
|
||||||
|
return 'unknown'
|
||||||
|
celsius = round(float(self._state) / 100, 1)
|
||||||
|
return convert_temperature(celsius, TEMP_CELSIUS,
|
||||||
|
self.unit_of_measurement)
|
49
homeassistant/components/switch/zha.py
Normal file
49
homeassistant/components/switch/zha.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
Switches on Zigbee Home Automation networks.
|
||||||
|
|
||||||
|
For more details on this platform, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/switch.zha/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
||||||
|
from homeassistant.components import zha
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['zha']
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup Zigbee Home Automation switches."""
|
||||||
|
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
add_devices([Switch(**discovery_info)])
|
||||||
|
|
||||||
|
|
||||||
|
class Switch(zha.Entity, SwitchDevice):
|
||||||
|
"""ZHA switch."""
|
||||||
|
|
||||||
|
_domain = DOMAIN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return if the switch is on based on the statemachine."""
|
||||||
|
if self._state == 'unknown':
|
||||||
|
return False
|
||||||
|
return bool(self._state)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the entity on."""
|
||||||
|
yield from self._endpoint.on_off.on()
|
||||||
|
self._state = 1
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the entity off."""
|
||||||
|
yield from self._endpoint.on_off.off()
|
||||||
|
self._state = 0
|
301
homeassistant/components/zha/__init__.py
Normal file
301
homeassistant/components/zha/__init__.py
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
"""
|
||||||
|
Support for ZigBee Home Automation devices.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/zha/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import const as ha_const
|
||||||
|
from homeassistant.helpers import discovery, entity
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
|
||||||
|
# Definitions for interfacing with the rest of HA
|
||||||
|
REQUIREMENTS = ['bellows==0.2.7']
|
||||||
|
|
||||||
|
DOMAIN = 'zha'
|
||||||
|
|
||||||
|
CONF_USB_PATH = 'usb_path'
|
||||||
|
CONF_DATABASE = 'database_path'
|
||||||
|
CONF_DEVICE_CONFIG = 'device_config'
|
||||||
|
DATA_DEVICE_CONFIG = 'zha_device_config'
|
||||||
|
|
||||||
|
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
|
||||||
|
vol.Optional(ha_const.CONF_TYPE): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
CONF_USB_PATH: cv.string,
|
||||||
|
CONF_DATABASE: cv.string,
|
||||||
|
vol.Optional(CONF_DEVICE_CONFIG, default={}):
|
||||||
|
vol.Schema({cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}),
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
ATTR_DURATION = "duration"
|
||||||
|
|
||||||
|
SERVICE_PERMIT = "permit"
|
||||||
|
SERVICE_DESCRIPTIONS = {
|
||||||
|
SERVICE_PERMIT: {
|
||||||
|
"description": "Allow nodes to join the Zigbee network",
|
||||||
|
"fields": {
|
||||||
|
"duration": {
|
||||||
|
"description": "Time to permit joins, in seconds",
|
||||||
|
"example": "60",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
SERVICE_SCHEMAS = {
|
||||||
|
SERVICE_PERMIT: vol.Schema({
|
||||||
|
vol.Optional(ATTR_DURATION, default=60):
|
||||||
|
vol.All(vol.Coerce(int), vol.Range(1, 254)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ZigBee definitions
|
||||||
|
CENTICELSIUS = 'C-100'
|
||||||
|
# Key in hass.data dict containing discovery info
|
||||||
|
DISCOVERY_KEY = 'zha_discovery_info'
|
||||||
|
|
||||||
|
# Internal definitions
|
||||||
|
APPLICATION_CONTROLLER = None
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
|
"""Setup ZHA.
|
||||||
|
|
||||||
|
Will automatically load components to support devices found on the network.
|
||||||
|
"""
|
||||||
|
global APPLICATION_CONTROLLER
|
||||||
|
|
||||||
|
import bellows.ezsp
|
||||||
|
from bellows.zigbee.application import ControllerApplication
|
||||||
|
|
||||||
|
ezsp_ = bellows.ezsp.EZSP()
|
||||||
|
usb_path = config[DOMAIN].get(CONF_USB_PATH)
|
||||||
|
yield from ezsp_.connect(usb_path)
|
||||||
|
|
||||||
|
database = config[DOMAIN].get(CONF_DATABASE)
|
||||||
|
APPLICATION_CONTROLLER = ControllerApplication(ezsp_, database)
|
||||||
|
listener = ApplicationListener(hass, config)
|
||||||
|
APPLICATION_CONTROLLER.add_listener(listener)
|
||||||
|
yield from APPLICATION_CONTROLLER.startup(auto_form=True)
|
||||||
|
|
||||||
|
for device in APPLICATION_CONTROLLER.devices.values():
|
||||||
|
hass.async_add_job(listener.async_device_initialized(device, False))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def permit(service):
|
||||||
|
"""Allow devices to join this network."""
|
||||||
|
duration = service.data.get(ATTR_DURATION)
|
||||||
|
_LOGGER.info("Permitting joins for %ss", duration)
|
||||||
|
yield from APPLICATION_CONTROLLER.permit(duration)
|
||||||
|
|
||||||
|
hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit,
|
||||||
|
SERVICE_DESCRIPTIONS[SERVICE_PERMIT],
|
||||||
|
SERVICE_SCHEMAS[SERVICE_PERMIT])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationListener:
|
||||||
|
"""Handlers for events that happen on the ZigBee application."""
|
||||||
|
|
||||||
|
def __init__(self, hass, config):
|
||||||
|
"""Initialize the listener."""
|
||||||
|
self._hass = hass
|
||||||
|
self._config = config
|
||||||
|
hass.data[DISCOVERY_KEY] = hass.data.get(DISCOVERY_KEY, {})
|
||||||
|
|
||||||
|
def device_joined(self, device):
|
||||||
|
"""Handle device joined.
|
||||||
|
|
||||||
|
At this point, no information about the device is known other than its
|
||||||
|
address
|
||||||
|
"""
|
||||||
|
# Wait for device_initialized, instead
|
||||||
|
pass
|
||||||
|
|
||||||
|
def device_initialized(self, device):
|
||||||
|
"""Handle device joined and basic information discovered."""
|
||||||
|
self._hass.async_add_job(self.async_device_initialized(device, True))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_device_initialized(self, device, join):
|
||||||
|
"""Handle device joined and basic information discovered (async)."""
|
||||||
|
import bellows.zigbee.profiles
|
||||||
|
import homeassistant.components.zha.const as zha_const
|
||||||
|
zha_const.populate_data()
|
||||||
|
|
||||||
|
for endpoint_id, endpoint in device.endpoints.items():
|
||||||
|
if endpoint_id == 0: # ZDO
|
||||||
|
continue
|
||||||
|
|
||||||
|
discovered_info = yield from _discover_endpoint_info(endpoint)
|
||||||
|
|
||||||
|
component = None
|
||||||
|
used_clusters = []
|
||||||
|
device_key = '%s-%s' % (str(device.ieee), endpoint_id)
|
||||||
|
node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get(
|
||||||
|
device_key, {})
|
||||||
|
|
||||||
|
if endpoint.profile_id in bellows.zigbee.profiles.PROFILES:
|
||||||
|
profile = bellows.zigbee.profiles.PROFILES[endpoint.profile_id]
|
||||||
|
if zha_const.DEVICE_CLASS.get(endpoint.profile_id,
|
||||||
|
{}).get(endpoint.device_type,
|
||||||
|
None):
|
||||||
|
used_clusters = profile.CLUSTERS[endpoint.device_type]
|
||||||
|
profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id]
|
||||||
|
component = profile_info[endpoint.device_type]
|
||||||
|
|
||||||
|
if ha_const.CONF_TYPE in node_config:
|
||||||
|
component = node_config[ha_const.CONF_TYPE]
|
||||||
|
used_clusters = zha_const.COMPONENT_CLUSTERS[component]
|
||||||
|
|
||||||
|
if component:
|
||||||
|
clusters = [endpoint.clusters[c] for c in used_clusters if c in
|
||||||
|
endpoint.clusters]
|
||||||
|
discovery_info = {
|
||||||
|
'endpoint': endpoint,
|
||||||
|
'clusters': clusters,
|
||||||
|
'new_join': join,
|
||||||
|
}
|
||||||
|
discovery_info.update(discovered_info)
|
||||||
|
self._hass.data[DISCOVERY_KEY][device_key] = discovery_info
|
||||||
|
|
||||||
|
yield from discovery.async_load_platform(
|
||||||
|
self._hass,
|
||||||
|
component,
|
||||||
|
DOMAIN,
|
||||||
|
{'discovery_key': device_key},
|
||||||
|
self._config,
|
||||||
|
)
|
||||||
|
|
||||||
|
for cluster_id, cluster in endpoint.clusters.items():
|
||||||
|
cluster_type = type(cluster)
|
||||||
|
if cluster_id in used_clusters:
|
||||||
|
continue
|
||||||
|
if cluster_type not in zha_const.SINGLE_CLUSTER_DEVICE_CLASS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
component = zha_const.SINGLE_CLUSTER_DEVICE_CLASS[cluster_type]
|
||||||
|
discovery_info = {
|
||||||
|
'endpoint': endpoint,
|
||||||
|
'clusters': [cluster],
|
||||||
|
'new_join': join,
|
||||||
|
}
|
||||||
|
discovery_info.update(discovered_info)
|
||||||
|
cluster_key = '%s-%s' % (device_key, cluster_id)
|
||||||
|
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
|
||||||
|
|
||||||
|
yield from discovery.async_load_platform(
|
||||||
|
self._hass,
|
||||||
|
component,
|
||||||
|
DOMAIN,
|
||||||
|
{'discovery_key': cluster_key},
|
||||||
|
self._config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Entity(entity.Entity):
|
||||||
|
"""A base class for ZHA entities."""
|
||||||
|
|
||||||
|
_domain = None # Must be overriden by subclasses
|
||||||
|
|
||||||
|
def __init__(self, endpoint, clusters, manufacturer, model, **kwargs):
|
||||||
|
"""Initialize ZHA entity."""
|
||||||
|
self._device_state_attributes = {}
|
||||||
|
ieeetail = ''.join([
|
||||||
|
'%02x' % (o, ) for o in endpoint.device.ieee[-4:]
|
||||||
|
])
|
||||||
|
if manufacturer and model is not None:
|
||||||
|
self.entity_id = '%s.%s_%s_%s_%s' % (
|
||||||
|
self._domain,
|
||||||
|
slugify(manufacturer),
|
||||||
|
slugify(model),
|
||||||
|
ieeetail,
|
||||||
|
endpoint.endpoint_id,
|
||||||
|
)
|
||||||
|
self._device_state_attributes['friendly_name'] = '%s %s' % (
|
||||||
|
manufacturer,
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.entity_id = "%s.zha_%s_%s" % (
|
||||||
|
self._domain,
|
||||||
|
ieeetail,
|
||||||
|
endpoint.endpoint_id,
|
||||||
|
)
|
||||||
|
for cluster in clusters:
|
||||||
|
cluster.add_listener(self)
|
||||||
|
self._endpoint = endpoint
|
||||||
|
self._clusters = {c.cluster_id: c for c in clusters}
|
||||||
|
self._state = ha_const.STATE_UNKNOWN
|
||||||
|
|
||||||
|
def attribute_updated(self, attribute, value):
|
||||||
|
"""Handle an attribute updated on this cluster."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def zdo_command(self, aps_frame, tsn, command_id, args):
|
||||||
|
"""Handle a ZDO command received on this cluster."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return device specific state attributes."""
|
||||||
|
return self._device_state_attributes
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _discover_endpoint_info(endpoint):
|
||||||
|
"""Find some basic information about an endpoint."""
|
||||||
|
extra_info = {
|
||||||
|
'manufacturer': None,
|
||||||
|
'model': None,
|
||||||
|
}
|
||||||
|
if 0 not in endpoint.clusters:
|
||||||
|
return extra_info
|
||||||
|
|
||||||
|
result, _ = yield from endpoint.clusters[0].read_attributes(
|
||||||
|
['manufacturer', 'model'],
|
||||||
|
allow_cache=True,
|
||||||
|
)
|
||||||
|
extra_info.update(result)
|
||||||
|
|
||||||
|
for key, value in extra_info.items():
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
try:
|
||||||
|
extra_info[key] = value.decode('ascii')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# Unsure what the best behaviour here is. Unset the key?
|
||||||
|
pass
|
||||||
|
|
||||||
|
return extra_info
|
||||||
|
|
||||||
|
|
||||||
|
def get_discovery_info(hass, discovery_info):
|
||||||
|
"""Get the full discovery info for a device.
|
||||||
|
|
||||||
|
Some of the info that needs to be passed to platforms is not JSON
|
||||||
|
serializable, so it cannot be put in the discovery_info dictionary. This
|
||||||
|
component places that info we need to pass to the platform in hass.data,
|
||||||
|
and this function is a helper for platforms to retrieve the complete
|
||||||
|
discovery info.
|
||||||
|
"""
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
discovery_key = discovery_info.get('discovery_key', None)
|
||||||
|
all_discovery_info = hass.data.get(DISCOVERY_KEY, {})
|
||||||
|
discovery_info = all_discovery_info.get(discovery_key, None)
|
||||||
|
return discovery_info
|
52
homeassistant/components/zha/const.py
Normal file
52
homeassistant/components/zha/const.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"""Constants related to the zha component."""
|
||||||
|
|
||||||
|
# Populated by populate_data() when zha component is initialized
|
||||||
|
DEVICE_CLASS = {}
|
||||||
|
SINGLE_CLUSTER_DEVICE_CLASS = {}
|
||||||
|
COMPONENT_CLUSTERS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def populate_data():
|
||||||
|
"""Populate data using constants from bellows.
|
||||||
|
|
||||||
|
These cannot be module level, as importing bellows must be done in a
|
||||||
|
in a function.
|
||||||
|
"""
|
||||||
|
from bellows.zigbee import zcl
|
||||||
|
from bellows.zigbee.profiles import PROFILES, zha, zll
|
||||||
|
|
||||||
|
DEVICE_CLASS[zha.PROFILE_ID] = {
|
||||||
|
zha.DeviceType.ON_OFF_SWITCH: 'switch',
|
||||||
|
zha.DeviceType.SMART_PLUG: 'switch',
|
||||||
|
|
||||||
|
zha.DeviceType.ON_OFF_LIGHT: 'light',
|
||||||
|
zha.DeviceType.DIMMABLE_LIGHT: 'light',
|
||||||
|
zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light',
|
||||||
|
zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'light',
|
||||||
|
zha.DeviceType.DIMMER_SWITCH: 'light',
|
||||||
|
zha.DeviceType.COLOR_DIMMER_SWITCH: 'light',
|
||||||
|
}
|
||||||
|
DEVICE_CLASS[zll.PROFILE_ID] = {
|
||||||
|
zll.DeviceType.ON_OFF_LIGHT: 'light',
|
||||||
|
zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch',
|
||||||
|
zll.DeviceType.DIMMABLE_LIGHT: 'light',
|
||||||
|
zll.DeviceType.DIMMABLE_PLUGIN_UNIT: 'light',
|
||||||
|
zll.DeviceType.COLOR_LIGHT: 'light',
|
||||||
|
zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light',
|
||||||
|
zll.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light',
|
||||||
|
}
|
||||||
|
|
||||||
|
SINGLE_CLUSTER_DEVICE_CLASS.update({
|
||||||
|
zcl.clusters.general.OnOff: 'switch',
|
||||||
|
zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
|
||||||
|
zcl.clusters.security.IasZone: 'binary_sensor',
|
||||||
|
})
|
||||||
|
|
||||||
|
# A map of hass components to all Zigbee clusters it could use
|
||||||
|
for profile_id, classes in DEVICE_CLASS.items():
|
||||||
|
profile = PROFILES[profile_id]
|
||||||
|
for device_type, component in classes.items():
|
||||||
|
if component not in COMPONENT_CLUSTERS:
|
||||||
|
COMPONENT_CLUSTERS[component] = set()
|
||||||
|
clusters = profile.CLUSTERS[device_type]
|
||||||
|
COMPONENT_CLUSTERS[component].update(clusters)
|
@ -83,6 +83,9 @@ batinfo==0.4.2
|
|||||||
# homeassistant.components.sensor.scrape
|
# homeassistant.components.sensor.scrape
|
||||||
beautifulsoup4==4.5.3
|
beautifulsoup4==4.5.3
|
||||||
|
|
||||||
|
# homeassistant.components.zha
|
||||||
|
bellows==0.2.7
|
||||||
|
|
||||||
# homeassistant.components.blink
|
# homeassistant.components.blink
|
||||||
blinkpy==0.5.2
|
blinkpy==0.5.2
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user