2019-02-14 15:01:46 +00:00
|
|
|
"""Support for ISY994 sensors."""
|
2015-04-04 08:33:03 +00:00
|
|
|
import logging
|
2018-06-25 17:05:07 +00:00
|
|
|
from typing import Callable
|
2015-04-04 08:33:03 +00:00
|
|
|
|
2019-02-14 15:01:46 +00:00
|
|
|
from homeassistant.components.isy994 import (
|
|
|
|
ISY994_NODES, ISY994_WEATHER, ISYDevice)
|
2017-12-26 08:26:37 +00:00
|
|
|
from homeassistant.components.sensor import DOMAIN
|
2019-02-14 15:01:46 +00:00
|
|
|
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_UV_INDEX
|
2016-09-11 18:18:53 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
UOM_FRIENDLY_NAME = {
|
|
|
|
'1': 'amp',
|
|
|
|
'3': 'btu/h',
|
|
|
|
'4': TEMP_CELSIUS,
|
|
|
|
'5': 'cm',
|
|
|
|
'6': 'ft³',
|
|
|
|
'7': 'ft³/min',
|
|
|
|
'8': 'm³',
|
|
|
|
'9': 'day',
|
|
|
|
'10': 'days',
|
|
|
|
'12': 'dB',
|
|
|
|
'13': 'dB A',
|
|
|
|
'14': '°',
|
|
|
|
'16': 'macroseismic',
|
|
|
|
'17': TEMP_FAHRENHEIT,
|
|
|
|
'18': 'ft',
|
|
|
|
'19': 'hour',
|
|
|
|
'20': 'hours',
|
|
|
|
'21': 'abs. humidity (%)',
|
|
|
|
'22': 'rel. humidity (%)',
|
|
|
|
'23': 'inHg',
|
|
|
|
'24': 'in/hr',
|
|
|
|
'25': 'index',
|
|
|
|
'26': 'K',
|
|
|
|
'27': 'keyword',
|
|
|
|
'28': 'kg',
|
|
|
|
'29': 'kV',
|
|
|
|
'30': 'kW',
|
|
|
|
'31': 'kPa',
|
|
|
|
'32': 'KPH',
|
|
|
|
'33': 'kWH',
|
|
|
|
'34': 'liedu',
|
|
|
|
'35': 'l',
|
2018-05-05 13:37:40 +00:00
|
|
|
'36': 'lx',
|
2016-09-11 18:18:53 +00:00
|
|
|
'37': 'mercalli',
|
|
|
|
'38': 'm',
|
|
|
|
'39': 'm³/hr',
|
|
|
|
'40': 'm/s',
|
|
|
|
'41': 'mA',
|
|
|
|
'42': 'ms',
|
|
|
|
'43': 'mV',
|
|
|
|
'44': 'min',
|
|
|
|
'45': 'min',
|
|
|
|
'46': 'mm/hr',
|
|
|
|
'47': 'month',
|
|
|
|
'48': 'MPH',
|
|
|
|
'49': 'm/s',
|
|
|
|
'50': 'ohm',
|
|
|
|
'51': '%',
|
|
|
|
'52': 'lb',
|
|
|
|
'53': 'power factor',
|
|
|
|
'54': 'ppm',
|
|
|
|
'55': 'pulse count',
|
|
|
|
'57': 's',
|
|
|
|
'58': 's',
|
|
|
|
'59': 'seimens/m',
|
|
|
|
'60': 'body wave magnitude scale',
|
|
|
|
'61': 'Ricter scale',
|
|
|
|
'62': 'moment magnitude scale',
|
|
|
|
'63': 'surface wave magnitude scale',
|
|
|
|
'64': 'shindo',
|
|
|
|
'65': 'SML',
|
|
|
|
'69': 'gal',
|
2017-10-14 18:45:32 +00:00
|
|
|
'71': UNIT_UV_INDEX,
|
2016-09-11 18:18:53 +00:00
|
|
|
'72': 'V',
|
|
|
|
'73': 'W',
|
|
|
|
'74': 'W/m²',
|
|
|
|
'75': 'weekday',
|
|
|
|
'76': 'Wind Direction (°)',
|
|
|
|
'77': 'year',
|
|
|
|
'82': 'mm',
|
|
|
|
'83': 'km',
|
|
|
|
'85': 'ohm',
|
|
|
|
'86': 'kOhm',
|
|
|
|
'87': 'm³/m³',
|
|
|
|
'88': 'Water activity',
|
|
|
|
'89': 'RPM',
|
|
|
|
'90': 'Hz',
|
|
|
|
'91': '° (Relative to North)',
|
|
|
|
'92': '° (Relative to South)',
|
|
|
|
}
|
|
|
|
|
|
|
|
UOM_TO_STATES = {
|
|
|
|
'11': {
|
|
|
|
'0': 'unlocked',
|
|
|
|
'100': 'locked',
|
|
|
|
'102': 'jammed',
|
|
|
|
},
|
|
|
|
'15': {
|
|
|
|
'1': 'master code changed',
|
|
|
|
'2': 'tamper code entry limit',
|
|
|
|
'3': 'escutcheon removed',
|
|
|
|
'4': 'key/manually locked',
|
|
|
|
'5': 'locked by touch',
|
|
|
|
'6': 'key/manually unlocked',
|
|
|
|
'7': 'remote locking jammed bolt',
|
|
|
|
'8': 'remotely locked',
|
|
|
|
'9': 'remotely unlocked',
|
|
|
|
'10': 'deadbolt jammed',
|
|
|
|
'11': 'battery too low to operate',
|
|
|
|
'12': 'critical low battery',
|
|
|
|
'13': 'low battery',
|
|
|
|
'14': 'automatically locked',
|
|
|
|
'15': 'automatic locking jammed bolt',
|
|
|
|
'16': 'remotely power cycled',
|
|
|
|
'17': 'lock handling complete',
|
|
|
|
'19': 'user deleted',
|
|
|
|
'20': 'user added',
|
|
|
|
'21': 'duplicate pin',
|
|
|
|
'22': 'jammed bolt by locking with keypad',
|
|
|
|
'23': 'locked by keypad',
|
|
|
|
'24': 'unlocked by keypad',
|
|
|
|
'25': 'keypad attempt outside schedule',
|
|
|
|
'26': 'hardware failure',
|
|
|
|
'27': 'factory reset'
|
|
|
|
},
|
|
|
|
'66': {
|
|
|
|
'0': 'idle',
|
|
|
|
'1': 'heating',
|
|
|
|
'2': 'cooling',
|
|
|
|
'3': 'fan only',
|
|
|
|
'4': 'pending heat',
|
|
|
|
'5': 'pending cool',
|
|
|
|
'6': 'vent',
|
|
|
|
'7': 'aux heat',
|
|
|
|
'8': '2nd stage heating',
|
|
|
|
'9': '2nd stage cooling',
|
|
|
|
'10': '2nd stage aux heat',
|
|
|
|
'11': '3rd stage aux heat'
|
|
|
|
},
|
|
|
|
'67': {
|
|
|
|
'0': 'off',
|
|
|
|
'1': 'heat',
|
|
|
|
'2': 'cool',
|
|
|
|
'3': 'auto',
|
|
|
|
'4': 'aux/emergency heat',
|
|
|
|
'5': 'resume',
|
|
|
|
'6': 'fan only',
|
|
|
|
'7': 'furnace',
|
|
|
|
'8': 'dry air',
|
|
|
|
'9': 'moist air',
|
|
|
|
'10': 'auto changeover',
|
|
|
|
'11': 'energy save heat',
|
|
|
|
'12': 'energy save cool',
|
|
|
|
'13': 'away'
|
|
|
|
},
|
|
|
|
'68': {
|
|
|
|
'0': 'auto',
|
|
|
|
'1': 'on',
|
|
|
|
'2': 'auto high',
|
|
|
|
'3': 'high',
|
|
|
|
'4': 'auto medium',
|
|
|
|
'5': 'medium',
|
|
|
|
'6': 'circulation',
|
|
|
|
'7': 'humidity circulation'
|
|
|
|
},
|
|
|
|
'93': {
|
|
|
|
'1': 'power applied',
|
|
|
|
'2': 'ac mains disconnected',
|
|
|
|
'3': 'ac mains reconnected',
|
|
|
|
'4': 'surge detection',
|
|
|
|
'5': 'volt drop or drift',
|
|
|
|
'6': 'over current detected',
|
|
|
|
'7': 'over voltage detected',
|
|
|
|
'8': 'over load detected',
|
|
|
|
'9': 'load error',
|
|
|
|
'10': 'replace battery soon',
|
|
|
|
'11': 'replace battery now',
|
|
|
|
'12': 'battery is charging',
|
|
|
|
'13': 'battery is fully charged',
|
|
|
|
'14': 'charge battery soon',
|
|
|
|
'15': 'charge battery now'
|
|
|
|
},
|
|
|
|
'94': {
|
|
|
|
'1': 'program started',
|
|
|
|
'2': 'program in progress',
|
|
|
|
'3': 'program completed',
|
|
|
|
'4': 'replace main filter',
|
|
|
|
'5': 'failure to set target temperature',
|
|
|
|
'6': 'supplying water',
|
|
|
|
'7': 'water supply failure',
|
|
|
|
'8': 'boiling',
|
|
|
|
'9': 'boiling failure',
|
|
|
|
'10': 'washing',
|
|
|
|
'11': 'washing failure',
|
|
|
|
'12': 'rinsing',
|
|
|
|
'13': 'rinsing failure',
|
|
|
|
'14': 'draining',
|
|
|
|
'15': 'draining failure',
|
|
|
|
'16': 'spinning',
|
|
|
|
'17': 'spinning failure',
|
|
|
|
'18': 'drying',
|
|
|
|
'19': 'drying failure',
|
|
|
|
'20': 'fan failure',
|
|
|
|
'21': 'compressor failure'
|
|
|
|
},
|
|
|
|
'95': {
|
|
|
|
'1': 'leaving bed',
|
|
|
|
'2': 'sitting on bed',
|
|
|
|
'3': 'lying on bed',
|
|
|
|
'4': 'posture changed',
|
|
|
|
'5': 'sitting on edge of bed'
|
|
|
|
},
|
|
|
|
'96': {
|
|
|
|
'1': 'clean',
|
|
|
|
'2': 'slightly polluted',
|
|
|
|
'3': 'moderately polluted',
|
|
|
|
'4': 'highly polluted'
|
|
|
|
},
|
|
|
|
'97': {
|
|
|
|
'0': 'closed',
|
|
|
|
'100': 'open',
|
|
|
|
'102': 'stopped',
|
|
|
|
'103': 'closing',
|
|
|
|
'104': 'opening'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def setup_platform(hass, config: ConfigType,
|
2018-08-24 14:37:30 +00:00
|
|
|
add_entities: Callable[[list], None], discovery_info=None):
|
2017-04-24 03:41:09 +00:00
|
|
|
"""Set up the ISY994 sensor platform."""
|
2016-09-11 18:18:53 +00:00
|
|
|
devices = []
|
|
|
|
|
2017-12-26 08:26:37 +00:00
|
|
|
for node in hass.data[ISY994_NODES][DOMAIN]:
|
|
|
|
_LOGGER.debug("Loading %s", node.name)
|
|
|
|
devices.append(ISYSensorDevice(node))
|
2015-04-13 05:47:32 +00:00
|
|
|
|
2017-12-26 08:26:37 +00:00
|
|
|
for node in hass.data[ISY994_WEATHER]:
|
2017-01-05 22:33:52 +00:00
|
|
|
devices.append(ISYWeatherDevice(node))
|
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
add_entities(devices)
|
2015-04-04 08:33:03 +00:00
|
|
|
|
|
|
|
|
2017-12-26 08:26:37 +00:00
|
|
|
class ISYSensorDevice(ISYDevice):
|
2016-09-11 18:18:53 +00:00
|
|
|
"""Representation of an ISY994 sensor device."""
|
2015-04-04 08:33:03 +00:00
|
|
|
|
2016-09-11 18:18:53 +00:00
|
|
|
@property
|
|
|
|
def raw_unit_of_measurement(self) -> str:
|
|
|
|
"""Get the raw unit of measurement for the ISY994 sensor device."""
|
|
|
|
if len(self._node.uom) == 1:
|
|
|
|
if self._node.uom[0] in UOM_FRIENDLY_NAME:
|
|
|
|
friendly_name = UOM_FRIENDLY_NAME.get(self._node.uom[0])
|
2018-07-17 17:34:29 +00:00
|
|
|
if friendly_name in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
2016-09-11 18:18:53 +00:00
|
|
|
friendly_name = self.hass.config.units.temperature_unit
|
|
|
|
return friendly_name
|
2018-07-23 08:16:05 +00:00
|
|
|
return self._node.uom[0]
|
|
|
|
return None
|
2015-04-04 08:33:03 +00:00
|
|
|
|
2016-09-11 18:18:53 +00:00
|
|
|
@property
|
|
|
|
def state(self) -> str:
|
|
|
|
"""Get the state of the ISY994 sensor device."""
|
ISY994 sensor improvements (#10805)
* Fire events for ISY994 control events
This allows hass to react directly to Insteon button presses (on switches and remotes), including presses, double-presses, and long holds
* Move change event subscription to after entity is added to hass
The event handler method requires `self.hass` to exist, which doesn't have a value until the async_added_to_hass method is called. Should eliminate a race condition.
* Overhaul binary sensors in ISY994 to be functional "out of the box"
We now smash all of the subnodes from the ISY994 in to one Hass binary_sensor, and automatically support both paradigms of state reporting that Insteon sensors can do. Sometimes a single node's state represents the sensor's state, other times two nodes are used and only "ON" events are sent from each. The logic between the two forunately do not conflict so we can support both without knowing which mode the device is in.
This also allows us to handle the heartbeat functionality that certain sensors have - we simply store the timestamp of the heartbeat as an attribute on the sensor device. It defaults to Unknown on bootup if and only if the device supports heartbeats, due to the presence of subnode 4.
* Parse the binary sensor device class from the ISY's device "type"
Now we automatically know which sensors are moisture, motion, and openings! (We also reverse the moisture sensor state, because Insteon reports ON for dry on the primary node.)
* Code review tweaks
The one material change here is that the event subscribers were moved to the `async_added_to_hass` method, as the handlers depend on things that only exist after the entity has been added.
* Handle cases where a sensor's state is unknown
When the ISY first boots up, if a battery-powered sensor has not reported in yet (due to heartbeat or a change in state), the state is unknown until it does.
* Clean up from code review
Fix coroutine await, remove unnecessary exception check, and return None when state is unknown
* Unknown value from PyISY is now -inf rather than -1
* Move heartbeat handling to a separate sensor
Now all heartbeat-compatible sensors will have a separate `binary_sensor` device that represents the battery state (on = dead)
* Add support for Unknown state, which is being added in next PyISY
PyISY will report unknown states as the number "-inf". This is implemented in the base ISY994 component, but subcomponents that override the `state` method needed some extra logic to handle it as well.
* Change a couple try blocks to explicit None checks
* Bump PyISY to 1.1.0, now that it has been published!
* Remove -inf checking from base component
The implementation of the -inf checking was improved in another branch which has been merged in to this branch already.
* Restrict negative-node and heartbeat support to known compatible types
Not all Insteon sensors use the same subnode IDs for the same things, so we need to use different logic depending on device type. Negative node and heartbeat support is now only used for leak sensors and open/close sensors.
* Use new style string formatting
* Add binary sensor detection for pre-5.x firmware
Meant to do this originally; writing documentation revealed that this requirement was missed!
2017-12-14 04:14:56 +00:00
|
|
|
if self.is_unknown():
|
|
|
|
return None
|
|
|
|
|
2016-09-11 18:18:53 +00:00
|
|
|
if len(self._node.uom) == 1:
|
|
|
|
if self._node.uom[0] in UOM_TO_STATES:
|
|
|
|
states = UOM_TO_STATES.get(self._node.uom[0])
|
|
|
|
if self.value in states:
|
|
|
|
return states.get(self.value)
|
|
|
|
elif self._node.prec and self._node.prec != [0]:
|
|
|
|
str_val = str(self.value)
|
|
|
|
int_prec = int(self._node.prec)
|
|
|
|
decimal_part = str_val[-int_prec:]
|
|
|
|
whole_part = str_val[:len(str_val) - int_prec]
|
|
|
|
val = float('{}.{}'.format(whole_part, decimal_part))
|
|
|
|
raw_units = self.raw_unit_of_measurement
|
|
|
|
if raw_units in (
|
|
|
|
TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
|
|
|
val = self.hass.config.units.temperature(val, raw_units)
|
2016-03-08 15:46:34 +00:00
|
|
|
|
2016-09-11 18:18:53 +00:00
|
|
|
return str(val)
|
|
|
|
else:
|
|
|
|
return self.value
|
2015-04-13 05:47:32 +00:00
|
|
|
|
2016-09-11 18:18:53 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unit_of_measurement(self) -> str:
|
|
|
|
"""Get the unit of measurement for the ISY994 sensor device."""
|
|
|
|
raw_units = self.raw_unit_of_measurement
|
|
|
|
if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS):
|
|
|
|
return self.hass.config.units.temperature_unit
|
2017-07-06 06:30:01 +00:00
|
|
|
return raw_units
|
2017-01-05 22:33:52 +00:00
|
|
|
|
|
|
|
|
2017-12-26 08:26:37 +00:00
|
|
|
class ISYWeatherDevice(ISYDevice):
|
2017-01-05 22:33:52 +00:00
|
|
|
"""Representation of an ISY994 weather device."""
|
|
|
|
|
|
|
|
@property
|
|
|
|
def raw_units(self) -> str:
|
|
|
|
"""Return the raw unit of measurement."""
|
|
|
|
if self._node.uom == 'F':
|
|
|
|
return TEMP_FAHRENHEIT
|
|
|
|
if self._node.uom == 'C':
|
|
|
|
return TEMP_CELSIUS
|
|
|
|
return self._node.uom
|
|
|
|
|
|
|
|
@property
|
|
|
|
def state(self) -> object:
|
|
|
|
"""Return the value of the node."""
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
val = self._node.status._val
|
|
|
|
raw_units = self._node.uom
|
|
|
|
|
|
|
|
if raw_units in [TEMP_CELSIUS, TEMP_FAHRENHEIT]:
|
|
|
|
return self.hass.config.units.temperature(val, raw_units)
|
|
|
|
return val
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unit_of_measurement(self) -> str:
|
|
|
|
"""Return the unit of measurement for the node."""
|
|
|
|
raw_units = self.raw_units
|
|
|
|
|
|
|
|
if raw_units in [TEMP_CELSIUS, TEMP_FAHRENHEIT]:
|
|
|
|
return self.hass.config.units.temperature_unit
|
|
|
|
return raw_units
|