Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 175 additions & 2 deletions homeassistant/components/sensor/zha.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,33 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return

sensor = yield from make_sensor(discovery_info)
async_add_devices([sensor])
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
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':
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)

Expand Down Expand Up @@ -66,6 +79,14 @@ def state(self) -> str:
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)
Expand Down Expand Up @@ -111,3 +132,155 @@ def state(self):
return 'unknown'

return round(float(self._state) / 100, 1)


class GenericBatterySensor(Sensor):
"""ZHA generic battery sensor."""

value_attribute = 32
battery_sizes = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go into zigpy as an enum, as it is part of the standard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll submit a PR to zigpy for this

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

@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):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should live in Home Assistant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"I'd be OKish with this going in and changing to use the normal BatterySensor when zigpy quirks handling is ready, though."

Will leave here until quirks mode is ready.

"""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'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return None to represent unknown state.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are multiple input values mapped to the same output percentage?
28, 27 and 26 are 100%
Why not 28=100, 27=97, 26=93? (or something like that)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanwinter please move your comment to a general PR comment. I don't see that it's related to my comment or this code line.


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')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.



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'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.


return self._state
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no newline at end of file



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'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.


return round(float(self._state) / 10, 1)

@property
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this kind of device is normally wired, and so polling it is OK? Have you tried configuring reporting on your device? That'd be a better option.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, they're wired devices and the electrical measurement cluster attributes don't support reporting according to the ZCL docs.

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'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return None.


return self._state
4 changes: 4 additions & 0 deletions homeassistant/components/zha/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def populate_data():
zcl.clusters.general.OnOff: 'switch',
zcl.clusters.measurement.RelativeHumidity: 'sensor',
zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
zcl.clusters.measurement.IlluminanceMeasurement: 'sensor',
zcl.clusters.general.PowerConfiguration: 'sensor',
zcl.clusters.smartenergy.Metering: 'sensor',
zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor',
zcl.clusters.security.IasZone: 'binary_sensor',
zcl.clusters.hvac.Fan: 'fan',
})
Expand Down