ZHA Zigbee Tested Devices...Please add your device results

in components/sensor/zha.py the temperature sensor 1st rounds sensor data and then converts the result from Celsius to Fahrenheit which may produces a non round result, see around line 92

        celsius = round(float(self._state) / 100, 1)
        return convert_temperature(
            celsius, TEMP_CELSIUS, self.unit_of_measurement)

as a quick fix I just round the result of convert_temperature one more time:

        celsius = round(float(self._state) / 100, 1)
        return round(convert_temperature(celsius, TEMP_CELSIUS,
                                         self.unit_of_measurement), 1)

I am running hassio and can not find a /components directory to edit the zha.py file.

Submit a PR so we can get this in and hopefully it will get merged. There are a few zha items that have been pending for a couple of months now so maybe they will all merge one day lol.

Submit it as a Feature request?

If you updated the code and you verified it is working submit a Pull Request in Github so the project can get the updated code and everyone can use it instead of users needing to edit the files the way it was described. Anyone can submit a PR.

My comment was actually directed to @Quatuor who provided the code changes.

Got it.
Until it gets merged is their any way to do this with a value_template
in the configuration.yaml or entity_registry.yaml for now?

1 Like

you can do a custom component with the folder and changes mentioned above.

1 Like

Did you get dimming to work for the Osram Lightify switch in your code above?

Nope. After a closer look I found some issues with the original PR. So I was working on a few fixes and some improvements (at least I think so :slight_smile: added state restore upon hass restart). Can you give it a try and let me know? here’s a new homeassistant/components/binary_sensor/zha.py which I was working on. You still are going to need the __init__.py and const.py from my previous post.

"""
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
import time

from enum import IntEnum
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
from homeassistant.components import zha
from homeassistant.const import STATE_OFF
from homeassistant.helpers.event import async_track_time_change
from homeassistant.helpers.restore_state import async_get_last_state


_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',
}

MOVING_STEP = 15


class OnOffCommands(IntEnum):
    OFF = 0x00
    ON = 0x01
    TOGGLE = 0x02
    OFF_WITH_EFFECT = 0x40
    ON_WITH_RECALL_GLOBAL_SCENE = 0x41
    ON_WITH_TIMED_OFF = 0x42


class LevelCommands(IntEnum):
    MOVE_TO_LEVEL = 0x00
    MOVE = 0x01
    STEP = 0x02
    STOP = 0x03
    MOVE_TO_LEVEL_WITH_ON_OFF = 0x04
    MOVE_WITH_ON_OFF = 0x05
    STEP_WITH_ON_OFF = 0x06
    STOP_WITH_ON_OFF = 0x07


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
            self._state = True

        def cluster_command(self, tsn, command_id, args):
            """Handle commands received to this cluster."""
            cmds = OnOffCommands
            _LOGGER.debug('{}: rx {} command with {} args on OnOff cluster'.
                          format(self._entity.entity_id, command_id, args))
            if command_id in (cmds.OFF, cmds.OFF_WITH_EFFECT):
                self.set_state(False)
            elif command_id in (cmds.ON, cmds.ON_WITH_RECALL_GLOBAL_SCENE,
                                cmds.ON_WITH_TIMED_OFF):
                self.set_state(True)
            elif command_id == cmds.TOGGLE:
                self.toggle()

        def attribute_updated(self, attrid, value):
            """Handle attribute updates on this cluster."""
            _LOGGER.debug('{}: reports {}/{} attr_id/value on OnOff cluster'.
                          format(self._entity.entity_id, attrid, value))
            if attrid == 0:
                self._state = value
                self._entity.schedule_update_ha_state()

        @property
        def is_on(self) -> bool:
            """state of the switch"""
            return self._state

        def set_state(self, state):
            """Set the state."""
            self._state = state
            if self._state and not bool(self._entity.level.level):
                self._entity.level.set_level(1, True)
            self._entity.schedule_update_ha_state()

        def toggle(self):
            """Toggle the state"""
            self._state = not self._state
            if self._state and not bool(self._entity.level.level):
                self._entity.level.set_level(1, True)
            self._entity.schedule_update_ha_state()

    class LevelListener:
        """Listener for the LevelControl ZigBee cluster."""

        class LvlChange:
            """ammount of Level change"""

            def __init__(self, direction=0, rate=0, delta=MOVING_STEP,
                         start_moving=True):
                self._direction = direction
                if direction == 0x00:
                    self._delta = delta
                else:
                    self._delta = -delta
                self.still_moving = start_moving
                if start_moving:
                    self.when_started = time.time()
                self._rate = rate

            @property
            def delta(self):
                """amount of level change"""
                return self._delta

            @property
            def delta_since_start(self):
                """return amount of change since start of level move"""
                dt = time.time() - self.when_started
                return int(dt * self.delta)

            def stop(self):
                """Stop moving and returns delta since the start"""
                self.still_moving = False
                return self.delta_since_start

            def __radd__(self, other):
                return other + self._delta

            def __rmul__(self, other):
                return other * self._delta

        def __init__(self, entity):
            """Initialize LevelListener."""
            self._entity = entity
            self._level = 255
            self._orig_level = self._level
            self.delta = self.LvlChange(start_moving=False)

        def cluster_command(self, tsn, command_id, args):
            """Handle commands received to this cluster."""
            _LOGGER.debug('{}: rx {} command with {} args on Level cluster'.
                          format(self._entity.entity_id, command_id, args))
            if command_id == LevelCommands.MOVE_TO_LEVEL:  # move_to_level
                self.set_level(args[0])
            elif command_id == LevelCommands.MOVE:  # move
                # We should dim slowly -- for now, just step once
                self.delta = self.LvlChange(*args)
                self._orig_level = self._level
                self.move_level(self.delta)
            elif command_id == LevelCommands.STEP:  # step
                # Step (technically shouldn't change on/off)
                delta = self.LvlChange(*args)
                self.move_level(delta)
            elif command_id in (LevelCommands.STOP,
                                LevelCommands.STOP_WITH_ON_OFF):
                stop_delta = self.delta.stop()
                self.set_level(self._orig_level + stop_delta)
            elif command_id == LevelCommands.MOVE_TO_LEVEL_WITH_ON_OFF:
                # move_to_level_with_on_off
                self.set_level(args[0], True)
            elif command_id == LevelCommands.MOVE_WITH_ON_OFF:  # move_with_on_off
                # We should dim slowly -- for now, just step once
                self.delta = self.LvlChange(*args)
                self._orig_level = self._level
                self.move_level(self.delta, True)
            elif command_id == LevelCommands.STEP_WITH_ON_OFF:  # step_with_on_off
                delta = self.LvlChange(*args)
                self.move_level(delta, True)

        def attribute_update(self, attrid, value):
            """Handle attribute updates on this cluster."""
            _LOGGER.debug('{}: reports {}/{} attr_id/value on level cluster'.
                          format(self._entity.entity_id, attrid, value))
            if attrid == 0:
                self.set_level(value)

        @property
        def level(self):
            """current level"""
            return self._level

        def move_level(self, change, onoff=False):
            """Increment the level, setting state if appropriate."""
            self._level = min(255, max(0, self._level + change))
            if onoff:
                self._entity.onoff.set_state(bool(self._level))
            self._entity.schedule_update_ha_state()

        def set_level(self, level, onoff=False):
            """Set the level, setting state if appropriate."""
            self._level = min(255, max(0, level))
            if onoff:
                self._entity.onoff.set_state(bool(self._level))
            self._entity.schedule_update_ha_state()

        async def async_time_interval(self, tracked_time):
            """if button is held, estimate level change"""
            if not self.delta.still_moving:
                return
            delta = self.delta.delta_since_start
            self.set_level(self._level + delta)

    async def async_added_to_hass(self):
        """Run when about to be added to Hass"""
        # Check if we have an old state
        old_state = await async_get_last_state(self.hass, self.entity_id)
        if old_state is not None:
            _LOGGER.debug('{}: restoring old state {}'.format(self.entity_id,
                                                              old_state))
            if old_state.attributes.get('level') is not None:
                self.level.set_level(old_state.attributes['level'])
            if old_state.state == STATE_OFF:
                self.onoff.set_state(False)
        async_track_time_change(self.hass, self.level.async_time_interval)

    def __init__(self, **kwargs):
        """Initialize Switch."""
        self.onoff = self.OnOffListener(self)
        self.level = self.LevelListener(self)
        from zigpy.zcl.clusters import general
        self._out_listeners = {
            general.OnOff.cluster_id: self.onoff,
            general.LevelControl.cluster_id: self.level,
        }
        super().__init__(**kwargs)

    @property
    def is_on(self) -> bool:
        """Return true if the binary sensor is on."""
        return self.onoff.is_on and bool(self.level.level)

    @property
    def device_state_attributes(self):
        """Return the device state attributes."""
        attrs = {}
        if self.is_on:
            attrs['level'] = self.level.level
        return attrs

with the above binary_sensor I’m using the following automation to control the lights. The idea is: triggered any time the state changes (since we want to catch level change) and if the state of the binary_switch is on then we turn on the lights to the level corresponding to binary_switch attributes[‘level’]

- id: floor_light_switch_on
  alias: Turn on floor lamp when switched on
  trigger:
    - platform: state
      entity_id: binary_sensor.dimming_switch
  condition:
    - condition: state
      entity_id: binary_sensor.dimming_switch
      state: 'on'
  action:
    - service: light.turn_on
      data_template:
        entity_id: light.floor_lamp
        brightness: "{{ trigger.to_state.attributes.level }}"

- id: light_switch_off
  alias: Turn off floor lamp when switched off
  trigger:
    - platform: state
      entity_id: binary_sensor.dimming_switch
      to: 'off'
  action:
    - service: light.turn_off
      entity_id:
        - light.floor_lamp

** edit: fixed set level to stay within 0…255
*** edit: added automation to link binary_switch level change to light control
**** edit: update to turn switch off when level is 0
PS: I’m just starting with python so I understand some things i’m doing could be very stupid. Any constructive dialog is welcome

It works well, however dimming is a little slow. One suggestion would be to toggle the state of the switch to off when the level gets to 0.

I am running Hassio on a raspberry pi and ssh into edit config files
can you tell me where to find the components/sensor/zha.py file
you are suggesting to edit.

Started having weird behavior. I have 3 iris open close sensors and 3 iris motion sensors. I’ve been moving over my devices this week, and finally decided to move everything else over. So before I started 2 motion and one open close were added. I have not been able to get the other ones to add at all. It’s like zha permit is not even working.

I even went and deleted the zigbee.db, I was able to add one of the previous devices, but still not the other devices. I then removed the db one more time, and now nothing will add.

I’ve spent all week on this stuff, and not the last few devices I expected to be easy are causing my greif. Any suggestions, or places to look for what may be going on? Thanks!

Yep, you are right. Doesn’t turn off when reaches 0. This is broken.
The dimming is linear and it does feel slow. You could change the “MOVING_STEP” which is currently at 15, but perhaps we should implement “exponential” progression instead of linear one. Stay tuned…

I don’t have experience with docker based installs. If I understand it correctly you really can’t “hack” those setups, which I was doing with my venv based setups.
I need to rewrite these as a custom_component. I started to explore that option, but I wish i had more time to spare :slight_smile: I think as a short term this could be an option, but it really makes sense to get this into Yoda-x’s zha_new custom component.

I just added the husbzb-1 to my home-assistant setup. The first device attempt is the old smartthings motion (original). It is able to pair/join, but I don’t see it as an available entity in hass. I haven’t had a chance to my other devices as my venture from smartthings to just purely hass.

Blockquote
2018-04-29 09:01:01 WARNING (MainThread) [bellows.zigbee.application] Unexpected response TSN=91 command=256 args=b’\xc5’
2018-04-29 09:01:01 WARNING (MainThread) [zigpy.endpoint] [0xa291:2] Message on unknown cluster 0xfc04
2018-04-29 09:01:01 INFO (MainThread) [homeassistant.core] Bus:Handling <Event system_log_event[L]: timestamp=1525017661.8279727, level=WARNING, message=Unexpected response TSN=91 command=256 args=b’\xc5’, exception=, source=core.py>
2018-04-29 09:01:01 INFO (MainThread) [homeassistant.core] Bus:Handling <Event system_log_event[L]: timestamp=1525017661.8951173, level=WARNING, message=[0xa291:2] Message on unknown cluster 0xfc04, exception=, source=core.py>

Blockquote
Device:
NWK: 0xa291
IEEE: d0:52:a8:00:2e:75:00:04
Endpoints:
1: profile=0x104, device_type=314
Input Clusters:
Basic (0)
Output Clusters:
Ota (25)
2: profile=0xfc01, device_type=314

Still having issues adding new zigbee devices. Turned off zwave and was able to add back a previously added iris open close sensor but unable to add anything new. Any recommendations, I’ve delete the db a couple times, but that did not help.

I do have a lot of errors like this in my log

Message on unknown device

Trying to pair a door sensor I get this:

2018-05-04 12:59:32 WARNING (MainThread) [bellows.zigbee.application] Unexpected response TSN=19 command=1 args=[[]] 2018-05-04 12:59:32 WARNING (MainThread) [zigpy.endpoint] [0x7f1c:1] Message on unknown cluster 0x0500 2018-05-04 12:59:42 WARNING (MainThread) [bellows.zigbee.application] Unexpected response TSN=20 command=1 args=[[]] 

2018-05-04 12:59:42 WARNING (MainThread) [zigpy.endpoint] [0x0aba:1] Message on unknown cluster 0x0500 2018-05-04 12:59:53 WARNING (MainThread) [bellows.zigbee.application] Unexpected response TSN=21 command=1 args=[[]] 

2018-05-04 12:59:53 WARNING (MainThread) [zigpy.endpoint] [0x3140:1] Message on unknown cluster 0x0500 2018-05-04 13:00:03 WARNING (MainThread) [bellows.zigbee.application] Unexpected response TSN=22 command=1 args=[[]] 

2018-05-04 13:00:03 WARNING (MainThread) [zigpy.endpoint] [0xb6cd:1] Message on unknown cluster 0x0500 2018-05-04 13:00:14 WARNING (MainThread) [bellows.zigbee.application] Unexpected response TSN=23 command=1 args=[[]] 

2018-05-04 13:00:14 WARNING (MainThread) [zigpy.endpoint] [0x2e99:1] Message on unknown cluster 0x0500 2018-05-04 13:00:24 WARNING (MainThread) [bellows.zigbee.application] Unexpected response TSN=24 command=1 args=[[]] 

2018-05-04 13:00:24 WARNING (MainThread) [zigpy.endpoint] [0xa70a:1] Message on unknown cluster 0x0500

What does that mean ?

Updated code for SmartThings arrival sensor including 2015 and 2017 models. Its a hack but works for me!

Anyone ever get the SmartThings water sensor to work with HA. I’ve been able to get an older ST motion sensor and ST Open close sensor to work, but no luck with the water/leak detector.

These sensors were quite stable for me on ST, and they are currently on sale for $25, which is a good value, if they could work directly with HA

@ptdalen @pitchdarkice

Fairly certain that when you see messages along the lines of Message on unknown device 0x1af6 you need to perform a reset on the device in question, then join the device with a zha.permit and whatever the device needs. Might help @Argoson also?