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!
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