I saw something like this, give it some time and you may be able to pair more. Personally I am stuck at 12 iris contact sensors.
OK. Thank you. Iâll just wait then.
Please check if you are on bellows 0.5.1. This version increased the number of allowed endpoints and some other table adjustments, and should have fixed this issue.
I had the same problem, I could install 8 endponts and no more, If I removed one, I could install another. But the total number was 8.
And where you pair them? Near your controller or somewhere in the house. Just asking because currenty you need to pair near your controller as joining over routers is not supported.
Thank you guys for your help. To actualy contribute to this post I have 9 5000K Sengled Element Bulbs and they work. The only weird issue is when going from 100% to off then to 30% thereâs a weird delay. Iâm also getting a ânexpected response TSN=4 command=1 args=[[]]â error. Other than that they work well and are a nice replacement to the LIFX I had.
Seems like it was resolved in the update to 64.3 (at least it worked again for me) and it worked with the 2nd one I bought and tested just recently.
I had to modify dmulcaheyâs pr for adding battery levels for smartthings devices. Mine just was not updating, and very very infrequently displaying anything. After days of trial and error, i made it work. Im a python novice at best, so it took much longer to figure out than had it been other languages that im accustomed to. None the less these are the corrections that are successfully working for me.
First, smartthings rebranded several of the centralite devices to the manufacturer of âSmartThingsâ. So in order to get these to display, i had to allow âSmartThingsâ as a manufacturer.
Secondly my devices would not display or update, so i had to make the device poll. Of 15 devices, at most 2 devices might report values, but this was the most i ever saw. Now, every device displays. Now the down side that i could see is that this MAY cause battery drain. But i would rather have my devices drain faster and be able to see the value vice not know when it dies.
Here is the updated /sensor/zha.py script which i put in custom_components/sensor/zha.py:
NOTE: This is not the complete installation. This is just one file. Please reffer to this post for more information.
"""
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):
"""Set up 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], update_before_add=True)
@asyncio.coroutine
def make_sensor(discovery_info):
"""Create ZHA sensors factory."""
from zigpy.zcl.clusters.measurement import (
RelativeHumidity, TemperatureMeasurement, IlluminanceMeasurement
)
from zigpy.zcl.clusters.general import PowerConfiguration
from zigpy.zcl.clusters.smartenergy import Metering
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
in_clusters = discovery_info['in_clusters']
if RelativeHumidity.cluster_id in in_clusters:
sensor = RelativeHumiditySensor(**discovery_info)
elif TemperatureMeasurement.cluster_id in in_clusters:
sensor = TemperatureSensor(**discovery_info)
elif PowerConfiguration.cluster_id in in_clusters \
and ( discovery_info['manufacturer'] == 'CentraLite' \
or discovery_info['manufacturer'] == 'SmartThings' ):
sensor = CentraliteBatterySensor(**discovery_info)
elif Metering.cluster_id in in_clusters:
sensor = MeteringSensor(**discovery_info)
elif IlluminanceMeasurement.cluster_id in in_clusters:
sensor = IlluminanceMeasurementSensor(**discovery_info)
elif ElectricalMeasurement.cluster_id in in_clusters:
sensor = ElectricalMeasurementSensor(**discovery_info)
return sensor
else:
sensor = Sensor(**discovery_info)
if discovery_info['new_join']:
cluster = list(in_clusters.values())[0]
yield from cluster.bind()
yield from cluster.configure_reporting(
sensor.value_attribute, 300, 600, sensor.min_reportable_change,
)
return sensor
class Sensor(zha.Entity):
"""Base ZHA sensor."""
_domain = DOMAIN
value_attribute = 0
min_reportable_change = 1
@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
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return False
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 entity."""
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)
class RelativeHumiditySensor(Sensor):
"""ZHA relative humidity sensor."""
min_reportable_change = 50 # 0.5%
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return '%'
@property
def state(self):
"""Return the state of the entity."""
if self._state == 'unknown':
return 'unknown'
return round(float(self._state) / 100, 1)
class GenericBatterySensor(Sensor):
"""ZHA generic battery sensor."""
value_attribute = 32
battery_sizes = {
0: 'No battery',
1: 'Built in',
2: 'Other',
3: 'AA',
4: 'AAA',
5: 'C',
6: 'D',
7: 'CR2',
8: 'CR123A',
255: 'Unknown'
}
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return '%'
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return True
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("%s async_update", self.entity_id)
result = yield from zha.safe_read(
self._endpoint.power,
['battery_size', 'battery_quantity', 'battery_voltage']
)
self._device_state_attributes['battery_size'] = \
self.battery_sizes.get(
result.get('battery_size', 255),
'Unknown'
)
self._device_state_attributes['battery_quantity'] = result.get(
'battery_quantity', 'Unknown')
self._state = result.get('battery_voltage', self._state)
class CentraliteBatterySensor(GenericBatterySensor):
"""ZHA battery sensor."""
# currently restricted to centralite sensors because the value
# conversion is specific to centralite sensors.
minVolts = 15
maxVolts = 28
values = {
28: 100,
27: 100,
26: 100,
25: 90,
24: 90,
23: 70,
22: 70,
21: 50,
20: 50,
19: 30,
18: 30,
17: 15,
16: 1,
15: 0
}
@property
def state(self):
"""Return the state of the entity."""
if self._state == 'unknown':
return 'unknown'
if self._state < self.minVolts:
self._state = self.minVolts
elif self._state > self.maxVolts:
self._state = self.maxVolts
return self.values.get(self._state, 'unknown')
class MeteringSensor(Sensor):
"""ZHA Metering sensor."""
value_attribute = 1024
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return 'W'
@property
def state(self):
"""Return the state of the entity."""
if self._state == 'unknown':
return 'unknown'
return self._state
class ElectricalMeasurementSensor(Sensor):
"""ZHA Electrical Measurement sensor."""
value_attribute = 1291
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return 'W'
@property
def state(self):
"""Return the state of the entity."""
if self._state == 'unknown':
return 'unknown'
return round(float(self._state) / 10, 1)
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return True
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("%s async_update", self.entity_id)
result = yield from zha.safe_read(
self._endpoint.electrical_measurement, ['active_power'])
self._state = result.get('active_power', self._state)
class IlluminanceMeasurementSensor(Sensor):
"""ZHA lux sensor."""
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return 'lux'
@property
def state(self):
"""Return the state of the entity."""
if self._state == 'unknown':
return 'unknown'
return self._state
Hi All,
The following bulb works perfectly with my Linear HUSBZB-1 USB adapter:
Sengled Element Classic Programmable Smart Bulb
Model: E11-G13
ZigBee Certified Product
Iâm running Hassbian on a Rasberry Pi 3 B
Hope this helps
Thanks @Bigrob8181. I knew that some changes were required to support the different manufacturer. I wasnât expecting to have to poll the devices through! I guess this isnât a big deal as likely a daily poll would be enough to keep battery up to date and this wouldnât use much power.
I wonder if the devices are only returning battery data when they detect a change?
I will give this a try some time soon, then maybe we can get this pushed into the main repo!
@winter,
That was my initial thoughts with regards to when the batteries reported level, however, i tried sensing motion or opening the contacts to get a report and nothing seemed to change without poll. I would go indefinitely without any values reporting even though the other sensors were changing (ie. motion or open/close). Before my logs were very quiet and now⊠every minute i get multiple
Unexpected response TSN=234 command=1 args=[[<ReadAttributeRecord attrid=49 status=134>, <ReadAttributeRecord attrid=51 status=134>, <ReadAttributeRecord attrid=32 status=0 value=17>]]
I think about 5 of my 15 devices are all rebranded âSmartThingsâ, so thatâs why those never worked.
It would be awesome if it were added into the main repo as i have quite the intricate workaround devised to reinstall everything on docker image upgrade. None the less it is all hands offâŠ
Yeah, I just realised the PR still hasnât been committed to the main branch yetâŠ
So do you suspect these unexpected responses you are seeing are related to the battery levels being reported, but not correctly interpreted by HA?
@winter
I am not sure where the unexpected responses are coming from, but they are absolutely from the battery reports. I found this zigbee document which made these make sense to me.
attrid=49 is 0x0031 (HEX) BatterySize Page 3-17
attrid=51 is 0x0033 (HEX) BatteryQuantity Page 3-17
attrid=32 is 0x0020 (HEX) BatteryVoltage Page 3-16
Not that i really know much about what i was looking at, but i could see where these numbers correlated.
Beyond that, i am not sure where or what is causing this log ânoiseâ. Although it seems pretty insignificant in the sense that it all still works. I did find a sensor was about dead, i replaced the batteries, and it appropriately updated the new 100% value despite all the noise. So i know that it all still works despite the logs.
@winter I tried to get a push for that PR to merge but no luck will hopefully get battery reporting soon.
@Bigrob8181 man thats some awesome research! Hopefully we can get that merged in too
I got something goodâŠ
Hi,
I am just trying to move a few of my zigbee devices from my SmartThings hub over to HA. I first did my SmartThings brand outlet and it worked flawlessly. I then tried my Lightify dimmer button and based on the lights of the switch it appeared to pair but I couldnât locate it in my entity list. Is there a log file or something I can look for for zigbee?
I also have the Hampton Bay ceiling fan controller (https://www.homedepot.com/p/Hampton-Bay-Universal-Wink-Enabled-White-Ceiling-Fan-Premier-Remote-Control-99432/206591100). I am hesitant to try moving it as I am guessing it wonât work. Has anyone tried it?
Thanks!
Edit: Here is the bellows output from the switch if that helps:
Device:
NWK: 0x0b38
IEEE: 84:18:26:00:00:e8:fa:90
Endpoints:
1: profile=0x104, device_type=DeviceType.LEVEL_CONTROL_SWITCH
Input Clusters:
Basic (0)
Power Configuration (1)
Identify (3)
Poll Control (32)
Temperature Measurement (1026)
Diagnostic (2821)
Output Clusters:
Identify (3)
On/Off (6)
Level control (8)
Ota (25)
for this switch to work, you need to have Pull Request #12528, which conflicts with current 0.67.1 I have tested it in my private branch and have it working, although currently iâm using it for just on/off controlling the lights through the automation.
Regarding the FAN control, I have not tried it personally, but Zigbee FAN pull request #12289 was merged into HA so Iâd expect it to work
Thanks! What do you mean by it conflicts with 0.67.1? Does that mean if I am running that version (which I am), I canât use it?
All I need is the on/off control so this would be perfect!
And then for the real noob question, how does one apply the pull request?
I mean since that pull request was created, the original source base have diverged and now changes made by RCloran donât apply cleanly to the existing 0.67.1 source.
I wonât be able to explain in a single post how to apply a pull request, but can provide the âdirectionâ to what to google. Pretty much it is all about âgitâ and mastering git. I can only recommend to start with some git tutorials and once you comprehend the basics, articles like best way to merge a github pull request start making sense I know, iâve been there
iâve tried a rebase of that request against 0.67.1 and if you are willing to experiment, you could try to replace the following files in your 0.67.1 hass installation, assuming you are not running a âdockerâ installation.
./homeassistant/components/binary_sensor/zha.py
"""
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 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',
}
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Zigbee Home Automation binary sensors."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']:
await _async_setup_iaszone(hass, config, async_add_devices,
discovery_info)
elif OnOff.cluster_id in discovery_info['out_clusters']:
await _async_setup_remote(hass, config, async_add_devices,
discovery_info)
async def _async_setup_iaszone(hass, config, async_add_devices, discovery_info=None):
device_class = None
from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
if discovery_info['new_join']:
await cluster.bind()
ieee = cluster.endpoint.device.application.ieee
await cluster.write_attributes({'cie_addr': ieee})
try:
zone_type = await 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], update_before_add=True)
async def _async_setup_remote(hass, config, async_add_devices, discovery_info):
async def safe(coro):
"""Run coro, catching ZigBee delivery errors, and ignoring them."""
import zigpy.exceptions
try:
await coro
except zigpy.exceptions.DeliveryError as exc:
_LOGGER.info("Ignoring error during setup: %s", exc)
if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 0, 600, 1))
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
sensor = Switch(**discovery_info)
async_add_devices([sensor])
class BinarySensor(zha.Entity, BinarySensorDevice):
"""The ZHA Binary Sensor."""
_domain = DOMAIN
def __init__(self, device_class, **kwargs):
"""Initialize the ZHA binary sensor."""
super().__init__(**kwargs)
self._device_class = device_class
from zigpy.zcl.clusters.security import IasZone
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
@property
def should_poll(self) -> bool:
"""Let zha handle polling."""
return False
@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, 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.async_schedule_update_ha_state()
elif command_id == 1:
_LOGGER.debug("Enroll requested")
res = self._ias_zone_cluster.enroll_response(0, 0)
self.hass.async_add_job(res)
async def async_update(self):
"""Retrieve latest state."""
from bellows.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status'])
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
class Switch(zha.Entity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN
class OnOffListener:
"""Listener for the OnOff ZigBee cluster."""
def __init__(self, entity):
"""Initialize OnOffListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0040):
self._entity._set_state(False)
elif command_id in (0x0001, 0x0041, 0x0042):
self._entity._set_state(True)
elif command_id == 0x0002:
self._entity._set_state(not self._entity._state)
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity._state = value
self._entity.schedule_update_ha_state()
class LevelListener:
"""Listener for the LevelControl ZigBee cluster."""
def __init__(self, entity):
"""Initialize LevelListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id == 0x0001: # move_to_level
self._entity._set_level(args[0])
elif command_id == 0x0002: # step
# Step (technically shouldn't change on/off)
if args[0] == 0:
self._entity._move_level(args[1])
else:
self._entity._move_level(-args[1])
elif command_id == 0x0004: # move_to_level_with_on_off
self._entity._set_level(args[0])
elif command_id == 0x0005: # move_with_on_off
# We should dim slowly -- for now, just step once
if args[0] == 0:
self._entity._move_level(10)
else:
self._entity._move_level(-10)
def attribute_update(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity._set_level(value)
def __init__(self, **kwargs):
"""Initialize Switch."""
self._state = True
self._level = 255
from zigpy.zcl.clusters import general
self._out_listeners = {
general.OnOff.cluster_id: self.OnOffListener(self),
general.LevelControl.cluster_id: self.LevelListener(self),
}
super().__init__(**kwargs)
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._state
@property
def device_state_attributes(self):
"""Return the device state attributes."""
return {'level': self._state and self._level or 0}
def _move_level(self, change):
"""Increment the level, setting state if appropriate."""
if not self._state and change > 0:
self._level = 0
self._level += min(255, max(0, self._level + change))
self._state = bool(self._level)
self.schedule_update_ha_state()
def _set_level(self, level):
"""Set the level, setting state if appropriate."""
self._level = level
self._state = bool(self._level)
self.schedule_update_ha_state()
def _set_state(self, state):
"""Set the state."""
self._state = state
self.schedule_update_ha_state()
./homeassistant/components/zha/__init__.py
"""
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 collections
import enum
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import const as ha_const
from homeassistant.helpers import discovery, entity
from homeassistant.util import slugify
REQUIREMENTS = [
'bellows==0.5.1',
'zigpy==0.0.3',
'zigpy-xbee==0.0.2',
]
DOMAIN = 'zha'
class RadioType(enum.Enum):
"""Possible options for radio type in config."""
ezsp = 'ezsp'
xbee = 'xbee'
CONF_BAUDRATE = 'baudrate'
CONF_DATABASE = 'database_path'
CONF_DEVICE_CONFIG = 'device_config'
CONF_RADIO_TYPE = 'radio_type'
CONF_USB_PATH = 'usb_path'
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({
vol.Optional(CONF_RADIO_TYPE, default='ezsp'): cv.enum(RadioType),
CONF_USB_PATH: cv.string,
vol.Optional(CONF_BAUDRATE, default=57600): cv.positive_int,
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'
ATTR_IEEE = 'ieee_address'
SERVICE_PERMIT = 'permit'
SERVICE_REMOVE = 'remove'
SERVICE_SCHEMAS = {
SERVICE_PERMIT: vol.Schema({
vol.Optional(ATTR_DURATION, default=60):
vol.All(vol.Coerce(int), vol.Range(1, 254)),
}),
SERVICE_REMOVE: vol.Schema({
vol.Required(ATTR_IEEE): cv.string,
}),
}
# 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__)
async def async_setup(hass, config):
"""Set up ZHA.
Will automatically load components to support devices found on the network.
"""
global APPLICATION_CONTROLLER
usb_path = config[DOMAIN].get(CONF_USB_PATH)
baudrate = config[DOMAIN].get(CONF_BAUDRATE)
radio_type = config[DOMAIN].get(CONF_RADIO_TYPE)
if radio_type == RadioType.ezsp:
import bellows.ezsp
from bellows.zigbee.application import ControllerApplication
radio = bellows.ezsp.EZSP()
elif radio_type == RadioType.xbee:
import zigpy_xbee.api
from zigpy_xbee.zigbee.application import ControllerApplication
radio = zigpy_xbee.api.XBee()
await radio.connect(usb_path, baudrate)
database = config[DOMAIN].get(CONF_DATABASE)
APPLICATION_CONTROLLER = ControllerApplication(radio, database)
listener = ApplicationListener(hass, config)
APPLICATION_CONTROLLER.add_listener(listener)
await APPLICATION_CONTROLLER.startup(auto_form=True)
for device in APPLICATION_CONTROLLER.devices.values():
hass.async_add_job(listener.async_device_initialized(device, False))
async def permit(service):
"""Allow devices to join this network."""
duration = service.data.get(ATTR_DURATION)
_LOGGER.info("Permitting joins for %ss", duration)
await APPLICATION_CONTROLLER.permit(duration)
hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit,
schema=SERVICE_SCHEMAS[SERVICE_PERMIT])
async def remove(service):
"""Remove a node from the network."""
from bellows.types import EmberEUI64, uint8_t
ieee = service.data.get(ATTR_IEEE)
ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')])
_LOGGER.info("Removing node %s", ieee)
await APPLICATION_CONTROLLER.remove(ieee)
hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove,
schema=SERVICE_SCHEMAS[SERVICE_REMOVE])
return True
class ApplicationListener:
"""All handlers for events that happen on the ZigBee application."""
def __init__(self, hass, config):
"""Initialize the listener."""
self._hass = hass
self._config = config
self._device_registry = collections.defaultdict(list)
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))
def device_left(self, device):
"""Handle device leaving the network."""
pass
def device_removed(self, device):
"""Handle device being removed from the network."""
for device_entity in self._device_registry[device.ieee]:
self._hass.async_add_job(device_entity.async_remove())
async def async_device_initialized(self, device, join):
"""Handle device joined and basic information discovered (async)."""
import zigpy.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 = await _discover_endpoint_info(endpoint)
component = None
profile_clusters = ([], [])
device_key = "{}-{}".format(device.ieee, endpoint_id)
node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get(
device_key, {})
if endpoint.profile_id in zigpy.profiles.PROFILES:
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
if zha_const.DEVICE_CLASS.get(endpoint.profile_id,
{}).get(endpoint.device_type,
None):
profile_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]
profile_clusters = zha_const.COMPONENT_CLUSTERS[component]
if component:
in_clusters = [endpoint.in_clusters[c]
for c in profile_clusters[0]
if c in endpoint.in_clusters]
out_clusters = [endpoint.out_clusters[c]
for c in profile_clusters[1]
if c in endpoint.out_clusters]
discovery_info = {
'application_listener': self,
'endpoint': endpoint,
'in_clusters': {c.cluster_id: c for c in in_clusters},
'out_clusters': {c.cluster_id: c for c in out_clusters},
'new_join': join,
'unique_id': device_key,
}
discovery_info.update(discovered_info)
self._hass.data[DISCOVERY_KEY][device_key] = discovery_info
await discovery.async_load_platform(
self._hass,
component,
DOMAIN,
{'discovery_key': device_key},
self._config,
)
async def attempt_single_cluster_device(cluster, profile_clusters,
device_classes, discovery_attr):
if cluster.cluster_id in profile_clusters:
return
if type(cluster) not in device_classes:
return
component = device_classes[type(cluster)]
cluster_key = "{}-{}".format(device_key, cluster_id)
discovery_info = {
'application_listener': self,
'endpoint': endpoint,
'in_clusters': {},
'out_clusters': {},
'new_join': join,
'unique_id': cluster_key,
'entity_suffix': '_{}'.format(cluster_id),
}
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
discovery_info.update(discovered_info)
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
await discovery.async_load_platform(
self._hass,
component,
DOMAIN,
{'discovery_key': cluster_key},
self._config,
)
for cluster_id, cluster in endpoint.in_clusters.items():
await attempt_single_cluster_device(
cluster,
profile_clusters[0],
zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
'in_clusters',
)
for cluster_id, cluster in endpoint.out_clusters.items():
await attempt_single_cluster_device(
cluster,
profile_clusters[1],
zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
'out_clusters',
)
def register_entity(self, ieee, entity_obj):
"""Record the creation of a hass entity associated with ieee."""
self._device_registry[ieee].append(entity_obj)
class Entity(entity.Entity):
"""A base class for ZHA entities."""
_domain = None # Must be overridden by subclasses
# Normally the entity itself is the listener. Base classes may set this to
# a dict of cluster ID -> listener to receive messages for specific
# clusters separately
_in_listeners = {}
_out_listeners = {}
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
model, application_listener, unique_id, **kwargs):
"""Init ZHA entity."""
self._device_state_attributes = {}
ieee = endpoint.device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer and model is not None:
self.entity_id = "{}.{}_{}_{}_{}{}".format(
self._domain,
slugify(manufacturer),
slugify(model),
ieeetail,
endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
self._device_state_attributes['friendly_name'] = "{} {}".format(
manufacturer,
model,
)
else:
self.entity_id = "{}.zha_{}_{}{}".format(
self._domain,
ieeetail,
endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
for cluster_id, cluster in in_clusters.items():
cluster.add_listener(self._in_listeners.get(cluster_id, self))
for cluster_id, cluster in out_clusters.items():
cluster.add_listener(self._out_listeners.get(cluster_id, self))
self._endpoint = endpoint
self._in_clusters = in_clusters
self._out_clusters = out_clusters
self._state = ha_const.STATE_UNKNOWN
self._unique_id = unique_id
application_listener.register_entity(ieee, self)
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
return self._device_state_attributes
def attribute_updated(self, attribute, value):
"""Handle an attribute updated on this cluster."""
pass
def zdo_command(self, tsn, command_id, args):
"""Handle a ZDO command received on this cluster."""
pass
async def _discover_endpoint_info(endpoint):
"""Find some basic information about an endpoint."""
extra_info = {
'manufacturer': None,
'model': None,
}
if 0 not in endpoint.in_clusters:
return extra_info
async def read(attributes):
"""Read attributes and update extra_info convenience function."""
result, _ = await endpoint.in_clusters[0].read_attributes(
attributes,
allow_cache=True,
)
extra_info.update(result)
await read(['manufacturer', 'model'])
if extra_info['manufacturer'] is None or extra_info['model'] is None:
# Some devices fail at returning multiple results. Attempt separately.
await read(['manufacturer'])
await read(['model'])
for key, value in extra_info.items():
if isinstance(value, bytes):
try:
extra_info[key] = value.decode('ascii').strip()
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, {})
return all_discovery_info.get(discovery_key, None)
async def safe_read(cluster, attributes):
"""Swallow all exceptions from network read.
If we throw during initialization, setup fails. Rather have an entity that
exists, but is in a maybe wrong state, than no entity. This method should
probably only be used during initialization.
"""
try:
result, _ = await cluster.read_attributes(
attributes,
allow_cache=False,
)
return result
except Exception: # pylint: disable=broad-except
return {}
./homeassistant/components/zha/const.py
"""All constants related to the ZHA component."""
DEVICE_CLASS = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
SINGLE_OUTPUT_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 zigpy import zcl
from zigpy.profiles import PROFILES, zha, zll
DEVICE_CLASS[zha.PROFILE_ID] = {
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor',
zha.DeviceType.REMOTE_CONTROL: 'binary_sensor',
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: 'binary_sensor',
zha.DeviceType.DIMMER_SWITCH: 'binary_sensor',
zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor',
}
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',
zll.DeviceType.COLOR_CONTROLLER: 'binary_sensor',
zll.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor',
zll.DeviceType.CONTROLLER: 'binary_sensor',
zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor',
zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor',
}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: 'switch',
zcl.clusters.measurement.RelativeHumidity: 'sensor',
zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
zcl.clusters.security.IasZone: 'binary_sensor',
zcl.clusters.hvac.Fan: 'fan',
})
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: '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(), set())
clusters = profile.CLUSTERS[device_type]
COMPONENT_CLUSTERS[component][0].update(clusters[0])
COMPONENT_CLUSTERS[component][1].update(clusters[1])
before you replace the files above, you need to make sure to exclude the existing switch from zigbee network, because thereâs some configuration done to the device only what a device joins the network, so it wonât work if you just replace the files with device already existing in zigbee.db .
You can exclude the dimmer switch by calling âzha.removeâ with {âieee_addressâ: â84:18:26:00:00:e8:fa:90â} JSON data. Confirm the device was excluded with bellows devices -D ~/.homeassistant/zigbee.db
(adjust for your specific file path and zigbee database filename). After that, replaces the files above, start homeassistant, call service zha.permit and join the switch. If everything successful, you should get
Good luck! And yep, things may break, but this is what makes it more fun, isnât it
Can Ikea TrĂ„dfri remotes be used alone as âphysical buttonsâ to activate different automations? If I understand correctly, this would require them to be Zigbee binary sensors.
The temp is still not rounded on this sensor.
Any chance someone could take a look at it.