SmartThings Presence Sensor

None of this is in any way relivant anymore. this post is only for historical purposes. the presence sensors were integrated into zha a long time ago.

I have been working on developing a way to utilize my smartthings presence detector, specifically SmartThings tagv4. EDIT: added SmartThings PGC410 in POST 3. Today i bring you the initial code to bring that functionality. Now, this is most definately a hacked together means, and my first try, but it works for me. Let me know if you have any issues or suggestions.
Now for a couple notes about this:
* I was not able to get the last time the presence sensor was seen before a hass restart to stick, so as of now, it is set to an arbitrary time in excess of 5 minutes.
* This sensor does not update your status to away until it has been missing for 5 minutes.
* Make backups of the files you are going to change, so you can recover if it does not work with your setup.

There is nothing for you to do other than install these scripts and pair your smartthings presence detector. This is built around the latest smartthings presence detector, but i am sure it can be tweaked to work with the older ones also. both presence sensors in POST 3.

I also have the code implementing the battery reports from This Post by @dmulcahey with a few tweaks to make it work with my setup. YMMV.

Installation instructions:
1. Make backup of /homeassistant/components/zha/const.py
2. Make backup of /your_config_dir/custom_components/sensor/zha.py
3. Copy over the 2 files located below.
4. Restart hass.
5. Add the presence sensor.
* Bring your tracker close to the zha radio.
* Goto https://your_address/dev-service.
* Select zha.permit in the service drop down
* Click on “Call Service”
6. Wait for it to show up.
7. Restart required maybe??
8. Benefit! :sunglasses:

Updated code in post 3. This remains for historical purposes only

Put this in /homeassistant/components/zha/const.py

"""All constants related to the ZHA component."""

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 zigpy import zcl
    from zigpy.profiles import PROFILES, zha, zll

    DEVICE_CLASS[zha.PROFILE_ID] = {
        zha.DeviceType.SMART_PLUG: 'switch',

        zha.DeviceType.ON_OFF_LIGHT: 'light',
        zha.DeviceType.DIMMABLE_LIGHT: 'light',
        zha.DeviceType.COLOR_DIMMABLE_LIGHT: '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.RelativeHumidity: 'sensor',
        zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
        # Added -------------------------------------------------
        zcl.clusters.measurement.IlluminanceMeasurement: 'sensor',
        zcl.clusters.general.PowerConfiguration: 'sensor',
        zcl.clusters.general.BinaryInput: 'sensor',
        zcl.clusters.smartenergy.Metering: 'sensor',
        zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor',
        zcl.clusters.security.IasZone: 'binary_sensor',
        # End Added ---------------------------------------------
        zcl.clusters.hvac.Fan: 'fan',

    })

    # 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])

Updated file in post 3
Put this in /your_config_dir/custom_components/sensor/zha.py

"""
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 datetime import datetime, timedelta

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']



async 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 = await make_sensor(discovery_info)
    async_add_devices([sensor], update_before_add=True)



async 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.general import BinaryInput
    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 BinaryInput.cluster_id in in_clusters \
            and ( discovery_info['manufacturer'] == 'SmartThings' \
                and discovery_info['model'] == 'tagv4' ):
        sensor = SmartThingsPresenceSensor(**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]
        await cluster.bind()
        await 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


    async def async_update(self):
        """Retrieve latest state."""
        _LOGGER.debug("%s async_update", self.entity_id)

        result = await 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 SmartThingsPresenceSensor(Sensor):

    #value_attribute = 85

    @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

    # all of the endpoint data that the presence sensor supports is included, despite usefullness
    async def async_update(self):
        """Retrieve latest state."""
        _LOGGER.debug("%s async_update", self.entity_id)

        # basic
        result = await zha.safe_read(
            self._endpoint.basic,
            ['zcl_version', 'app_version', 'manufacturer', 'model', 'power_source']
        )
        self._device_state_attributes['zcl_version'] = result.get('zcl_version', 'unknown')
        self._device_state_attributes['app_version'] = result.get('app_version', 'unknown')
        self._device_state_attributes['manufacturer'] = result.get('manufacturer', b'unknown').decode('utf-8')
        self._device_state_attributes['model'] = result.get('model', b'unknown').decode('utf-8')
        self._device_state_attributes['power_source'] = result.get('power_source', 'unknown')

        # PowerConfiguration
        result = await zha.safe_read(
            self._endpoint.power,
            ['battery_voltage']
        )
        self._device_state_attributes['battery_voltage'] = result.get('battery_voltage', 'unknown')

        # Identify
        result = await zha.safe_read(
            self._endpoint.identify,
            ['identify_time']
        )
        self._device_state_attributes['identify_time'] = result.get('identify_time', 'unknown')

        # BinaryInput
        result = await zha.safe_read(
            self._endpoint.binary_input,
            ['out_of_service', 'present_value', 'status_flags']
        )
        self._device_state_attributes['out_of_service'] = result.get('out_of_service', 'unknown')
        self._device_state_attributes['present_value'] = result.get('present_value', 'unknown')
        self._device_state_attributes['status_flags'] = result.get('status_flags', 'unknown')

        # Poll Control
        result = await zha.safe_read(
            self._endpoint.poll_control,
            ['checkin_interval', 'long_poll_interval', 'short_poll_interval', 'fast_poll_timeout']
        )
        self._device_state_attributes['checkin_interval'] = result.get('checkin_interval', 'unknown')       # 0
        self._device_state_attributes['long_poll_interval'] = result.get('long_poll_interval', 'unknown')   # 28
        self._device_state_attributes['short_poll_interval'] = result.get('short_poll_interval', 'unknown') # 1
        self._device_state_attributes['fast_poll_timeout'] = result.get('fast_poll_timeout', 'unknown')     # 40

        # determine home or away
        # since each group polls seperately, grab a value from each poll and see if unknown
        zcl_version = self._device_state_attributes['zcl_version']
        battery_voltage = self._device_state_attributes['battery_voltage']
        identify_time = self._device_state_attributes['identify_time']
        out_of_service = self._device_state_attributes['out_of_service']
        long_poll = self._device_state_attributes['long_poll_interval']
        # Get time now
        self._device_state_attributes['last_poll'] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
        # If not set, then set it
        # TODO: figure out a way to save the last_seen state for reference after hass restarts
        self._device_state_attributes['last_seen'] = self._device_state_attributes['last_seen'] \
            if "last_seen" in self._device_state_attributes \
            else "2018-04-22T12:00:00"

        # if we dont see the tracker, do things
        # if we have a value set, its home
        if ((zcl_version != 'unknown') or (battery_voltage != 'unknown') or (identify_time != 'unknown') or (out_of_service != 'unknown') or (long_poll != 'unknown')):
            self._device_state_attributes['last_seen'] = self._device_state_attributes['last_poll']
            self._state = 'home'

            _LOGGER.debug( "Presence: Detected" )

        # otherwise it is not home, but give it 5 minutes to ensure its gone.
        else:
            # future is 5 minutes from when it was last seen
            future = datetime.strptime(self._device_state_attributes['last_seen'], '%Y-%m-%dT%H:%M:%S') + timedelta(minutes=5)
            last_poll = datetime.strptime(self._device_state_attributes['last_poll'], '%Y-%m-%dT%H:%M:%S')
            # if test is in the future, its not been 5 minutes yet
            test = (future > last_poll)
            if test:
                self._state = 'home'
            # otherwise, its been more than 5 minutes, set away
            else:
                self._state = 'not_home'

            _LOGGER.debug( "Presence: %s > %s, %s", future, last_poll, test )


    @property
    def state(self):
        """Return the state of the entity."""
        if self._state == 'unknown':
            return 'unknown'

        return self._state


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


    async def async_update(self):
        """Retrieve latest state."""
        _LOGGER.debug("%s async_update", self.entity_id)

        result = await 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
2 Likes

Updated code in post 3. This remains for historical purposes only

Updated zha.py
* Battery % reported.
* Cleaned up unnecessary items.

Updated file in post 3
Change class SmartThingsPresenceSensor(Sensor) to this.

class SmartThingsPresenceSensor(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 should_poll(self) -> bool:
        """Return True if entity has to be polled for state.
        False if entity pushes its state to HA.
        """
        return True

    async def async_update(self):
        """Retrieve latest state."""
        _LOGGER.debug("%s async_update", self.entity_id)

        # basic
        result = await zha.safe_read(
            self._endpoint.basic,
            ['manufacturer', 'model']
        )
        self._device_state_attributes['manufacturer'] = manufacturer = result.get('manufacturer', b'unknown').decode('utf-8')
        self._device_state_attributes['model'] = result.get('model', b'unknown').decode('utf-8')

        #PowerConfiguration
        result = await zha.safe_read(
            self._endpoint.power,
            ['battery_voltage']
        )
        self._device_state_attributes['battery_voltage'] = battery_level = battery_voltage = result.get('battery_voltage', 'unknown')

        # Get battery level %
        if battery_voltage == 'unknown':
            battery_level = 'unknown'
        elif int(float(battery_voltage)) < self.minVolts:
            battery_level = self.minVolts
        elif int(float(battery_voltage)) > self.maxVolts:
            battery_level = self.maxVolts

        battery_level = self.values.get(battery_level, 'unknown')
        if battery_level != 'unknown':
            battery_level = str(battery_level) + "%"
        self._device_state_attributes['battery_level'] = battery_level

        # determine home or away
        # Get time now
        self._device_state_attributes['last_poll'] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
        # If not set, then set it
        # TODO: figure out a way to save the last_seen state for reference after hass restarts
        self._device_state_attributes['last_seen'] = self._device_state_attributes['last_seen'] \
            if "last_seen" in self._device_state_attributes \
            else "2018-04-22T12:00:00"

        # if we dont see the tracker, do things
        # if we have a value set, its home
        if ((manufacturer != 'unknown') or (battery_voltage != 'unknown')):
            self._device_state_attributes['last_seen'] = self._device_state_attributes['last_poll']
            self._state = 'home'

            _LOGGER.debug( "Presence: Detected" )

        # otherwise it is not home, but give it 5 minutes to ensure its gone.
        else:
            # future is 5 minutes from when it was last seen
            future = datetime.strptime(self._device_state_attributes['last_seen'], '%Y-%m-%dT%H:%M:%S') + timedelta(minutes=5)
            last_poll = datetime.strptime(self._device_state_attributes['last_poll'], '%Y-%m-%dT%H:%M:%S')
            # if test is in the future, its not been 5 minutes yet
            test = (future > last_poll)
            if test:
                self._state = 'home'
            # otherwise, its been more than 5 minutes, set away
            else:
                self._state = 'not_home'

            _LOGGER.debug( "Presence: %s > %s, %s", future, last_poll, test )


    @property
    def state(self):
        """Return the state of the entity."""
        if self._state == 'unknown':
            return 'unknown'

        return self._state

OK, so today i bring you a functional presence sensor for BOTH: TAGv4(2017) and PGC410(2015) SmartThings presence sensors. The new tracker is depicted in the OP and reports battery. The second (older) tracker does not report battery on a known cluster. Therefore if you have the older tracker, you WILL NOT GET BATTERY REPORTS. Now i am sure there is a better way to incorporate this, and possibly cleaner code, but this works for my purposes.

Again, these files contain the additional sensors from @dmulcahey. I have commented out polling, as it works for his setup and is what is in his PR. If battery reporting for your additional zha devices are not reporting, i would recommend removing the comments “#” from lines 169-174. Also, i added SmartThings zha devices, as opposed to only centralite.

In addition to the notes above:

* Multiple devices will be created in your entity_registry.yaml ending in 1_0 and 2_0. I am not sure how to overcome this issue, but the ones that will work are the ones that look like sensor.smartthings_pgc410_MACADDRESS_1_0 and sensor.smartthings_tagv4_MACADDRESS_1_0.

Put this in /homeassistant/components/zha/const.py

"""All constants related to the ZHA component."""

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 zigpy import zcl
    from zigpy.profiles import PROFILES, zha, zll

    DEVICE_CLASS[zha.PROFILE_ID] = {
        zha.DeviceType.SMART_PLUG: 'switch',

        zha.DeviceType.ON_OFF_LIGHT: 'light',
        zha.DeviceType.DIMMABLE_LIGHT: 'light',
        zha.DeviceType.COLOR_DIMMABLE_LIGHT: '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.RelativeHumidity: 'sensor',
        zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
        # Added -------------------------------------------------
        zcl.clusters.measurement.IlluminanceMeasurement: 'sensor',
        zcl.clusters.general.PowerConfiguration: 'sensor',
        zcl.clusters.general.Basic: 'sensor',
        zcl.clusters.smartenergy.Metering: 'sensor',
        zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor',
        zcl.clusters.security.IasZone: 'binary_sensor',
        # End Added ---------------------------------------------
        zcl.clusters.hvac.Fan: 'fan',

    })

    # 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])

Put this in /your_config_dir/custom_components/sensor/zha.py

"""
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 logging
from datetime import datetime, timedelta

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']


async 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 = await make_sensor(discovery_info)
    async_add_devices([sensor], update_before_add=True)


async 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.general import Basic
    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 Basic.cluster_id in in_clusters \
        and (discovery_info['manufacturer'] == 'SmartThings'
             and (discovery_info['model'] == 'tagv4'
                  or discovery_info['model'] == 'PGC410')):
        sensor = SmartThingsPresenceSensor(**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]
        await cluster.bind()
        await 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)
        # https://community.home-assistant.io/t/zha-zigbee-tested-devices-please-add-your-device-results/17718/322
        # return convert_temperature(
        #    celsius, TEMP_CELSIUS, self.unit_of_measurement)
        return round(
            convert_temperature(
                celsius, TEMP_CELSIUS, self.unit_of_measurement), 1)


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

    async def async_update(self):
        """Retrieve latest state."""
        _LOGGER.debug("%s async_update", self.entity_id)

        result = await 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 SmartThingsPresenceSensor(Sensor):
    """ZHA SmartThings Presence sensor."""

    # currently restricted to SmartThings Tagv4 sensors because the value
    # conversion is specific to SmartThings/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 should_poll(self) -> bool:
        """Return True if entity has to be polled for state.
        False if entity pushes its state to HA.
        """
        return True

    async def async_update(self):
        """Retrieve latest state."""
        _LOGGER.debug("%s async_update", self.entity_id)

        # basic
        result = await zha.safe_read(
            self._endpoint.basic,
            ['manufacturer', 'model']
        )
        self._device_state_attributes['manufacturer'] = manufacturer = \
            result.get('manufacturer', b'unknown').decode('utf-8')
        self._device_state_attributes['model'] = model = \
            result.get('model', b'unknown').decode('utf-8')

        # Only TAGV4 reports battery on non manufacturer specific cluster
        if model == 'tagv4':
            # PowerConfiguration
            result = await zha.safe_read(
                self._endpoint.power,
                ['battery_voltage']
            )
            self._device_state_attributes['battery_voltage'] = battery_level = \
                battery_voltage = result.get('battery_voltage', 'unknown')

            # Get battery level %
            if battery_voltage == 'unknown':
                battery_level = 'unknown'
            elif int(float(battery_voltage)) < self.minVolts:
                battery_level = self.minVolts
            elif int(float(battery_voltage)) > self.maxVolts:
                battery_level = self.maxVolts

            battery_level = self.values.get(battery_level, 'unknown')
            if battery_level != 'unknown':
                battery_level = str(battery_level) + "%"
            self._device_state_attributes['battery_level'] = battery_level
        else:
            battery_voltage = 'unknown'

        # determine home or away
        # Get time now
        self._device_state_attributes['last_poll'] = \
            datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
        # If not set, then set it
        # TODO: figure out a way to save the last_seen state for reference
        # after hass restarts
        self._device_state_attributes['last_seen'] = \
            self._device_state_attributes['last_seen'] \
            if "last_seen" in self._device_state_attributes \
            else "2018-04-22T12:00:00"

        # if we dont see the tracker, do things
        # if we have a value set, its home
        if ((manufacturer != 'unknown') or (battery_voltage != 'unknown')):
            self._device_state_attributes['last_seen'] = \
                self._device_state_attributes['last_poll']
            self._state = 'home'

            _LOGGER.debug("Presence: Detected")

        # otherwise it is not home, but give it 5 minutes to ensure its gone.
        else:
            # future is 5 minutes from when it was last seen
            future = datetime.strptime(
                self._device_state_attributes['last_seen'],
                '%Y-%m-%dT%H:%M:%S') + timedelta(minutes=5)
            last_poll = datetime.strptime(
                self._device_state_attributes['last_poll'],
                '%Y-%m-%dT%H:%M:%S')
            # if test is in the future, its not been 5 minutes yet
            test = (future > last_poll)
            if test:
                self._state = 'home'
            # otherwise, its been more than 5 minutes, set away
            else:
                self._state = 'not_home'

            _LOGGER.debug("Presence: %s > %s, %s", future, last_poll, test)

    @property
    def state(self):
        """Return the state of the entity."""
        if self._state == 'unknown':
            return 'unknown'

        return self._state


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

    async def async_update(self):
        """Retrieve latest state."""
        _LOGGER.debug("%s async_update", self.entity_id)

        result = await 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

1 Like

hey - thanks for sharing this. The presence sensors were SUPER handy for tracking car presence with ST.

I’ve added the files, but zha permit is not an option in the dev service?
any thoughts (using ubuntu 16.10 and HA 0.70)

This is awesome! How well does it work? Reliable? Fast? When on an ST Hub they had a reputation of being very unreliable (as does most of the ST sensors) but being on H.A. I could see it working much better? Does it? If so, I’ll definitely pick up a few of these.

Actually this is currently being worked on for proper integration into home assistant vice my hack above. The tagv4 works and reports battery in the zha quirks by @dmulcahey.

Hi Bigrob,

So I am able to pair PGC410 directly using the ZHA add device option from the configuration.I mean i have not made any modifications to the hassio 0.91.0 file as per your original post.

image

I am just wondering would it work this way, I mean without your code snippets. Or do I still make the code modifications.

Dont know much about how to configure any automation based on the presence now, can I add it as a sensor badge on the home page.

Any guidance would be appreciated.

I guess after pairing it, the following message started appearing in the home assistant log

2019-04-24 18:14:06 WARNING (MainThread) [zigpy.endpoint] [0x3b72:2] Message on unknown cluster 0xfc05
2019-04-24 18:14:26 WARNING (MainThread) [zigpy.endpoint] [0x3b72:2] Message on unknown cluster 0xfc05
2019-04-24 18:14:46 WARNING (MainThread) [zigpy.endpoint] [0x3b72:2] Message on unknown cluster 0xfc05
2019-04-24 18:15:06 WARNING (MainThread) [zigpy.endpoint] [0x3b72:2] Message on unknown cluster 0xfc05

in fact the log is now getting crammed by these repeated messages, guess would have to delete the pairing to avoid getting log to grow gigantic overnight.

They are exactly 20 sec apart, maybe the polling interval of the presence sensor.

Thanks.

For others who find this thread while Googling how to set up SmartThings presence sensors with Home Assistant, I have a happy update!

  • Make sure ZHA is installed.
  • Add the presence sensors using the ZHA configure panel.

That’s really it. They weren’t automatically added to Lovelace, but I went ahead and added them and named the entities. They now show up as “Home” and “Away” in my dashboard.

1 Like

Migrating from Smartthings and stumbled on this post. I have an original Smartthings Prescence sensor (PGC410). I can add it but doesn’t show up with any associated entities. Are you saying this should work with the older (PGC410) without any modifications?

1 Like

Anyone having issues using the V4 ST Tag when away for a long while?
Home and Away works fine, however if the tag is away for 6 hours (exactly pretty much), HA sets tag to home even though it is not!!
Driving me mad because I think the Wife is home :slight_smile:

Im using a ConBee 2 stick via ZHA.

FYI, The older tag (PGC410) I have never managed to connect without a work around.

All of this was integrated into zha a while back. none of this is relevant anymore. i at the time had no issues with either. my pgc died some time ago and all i have now are the tags and they work as intended.

Sorry for reopening this old thread, but I’m desperate for a solution…
The presence sensors in my case are detected ok but don’t get any entities to use in automations.
These are incredibly useful to me and wouldn’t want to have to find an alternative. In fact I’m always looking them up on eBay to buy more to have as spares. The are so reliable and useful to me…
Any help or pointing me in the right direction would be very much appreciated!