diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index eee24b8ad1c..99037f60107 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -101,7 +101,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if DENSITY in data: self._density = int(data.get(DENSITY)) @@ -139,8 +139,16 @@ class XiaomiMotionSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" + if raw_data['cmd'] == 'heartbeat': + _LOGGER.debug( + 'Skipping heartbeat of the motion sensor. ' + 'It can introduce an incorrect state because of a firmware ' + 'bug (https://github.com/home-assistant/home-assistant/pull/' + '11631#issuecomment-357507744).') + return + self._should_poll = False if NO_MOTION in data: # handle push from the hub self._no_motion_since = data[NO_MOTION] @@ -186,7 +194,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" self._should_poll = False if NO_CLOSE in data: # handle push from the hub @@ -219,7 +227,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): XiaomiBinarySensor.__init__(self, device, 'Water Leak Sensor', xiaomi_hub, 'status', 'moisture') - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" self._should_poll = False @@ -256,7 +264,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if DENSITY in data: self._density = int(data.get(DENSITY)) @@ -293,7 +301,7 @@ class XiaomiButton(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(self._data_key) if value is None: @@ -343,7 +351,7 @@ class XiaomiCube(XiaomiBinarySensor): attrs.update(super().device_state_attributes) return attrs - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if 'status' in data: self._hass.bus.fire('cube_action', { diff --git a/homeassistant/components/cover/xiaomi_aqara.py b/homeassistant/components/cover/xiaomi_aqara.py index 5b51371346b..29cb707fef5 100644 --- a/homeassistant/components/cover/xiaomi_aqara.py +++ b/homeassistant/components/cover/xiaomi_aqara.py @@ -59,7 +59,7 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice): """Move the cover to a specific position.""" self._write_to_hub(self._sid, **{self._data_key['pos']: str(position)}) - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if ATTR_CURTAIN_LEVEL in data: self._pos = int(data[ATTR_CURTAIN_LEVEL]) diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index 63770fbf9b7..d1664d13072 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -39,7 +39,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light): """Return true if it is on.""" return self._state - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(self._data_key) if value is None: diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 6dc574d75b0..f5c20fa5a1c 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -61,7 +61,7 @@ class XiaomiSensor(XiaomiDevice): """Return the state of the sensor.""" return self._state - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(self._data_key) if value is None: diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py index 67a56829bec..578036a1677 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/switch/xiaomi_aqara.py @@ -107,7 +107,7 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): self._state = False self.schedule_update_ha_state() - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" if IN_USE in data: self._in_use = int(data[IN_USE]) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 2aa56f7957f..8076975ab0e 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -7,17 +7,22 @@ https://home-assistant.io/components/xiaomi_aqara/ import asyncio import logging +from datetime import timedelta + import voluptuous as vol from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_HOST, CONF_MAC, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow -REQUIREMENTS = ['PyXiaomiGateway==0.7.1'] +REQUIREMENTS = ['PyXiaomiGateway==0.8.0'] _LOGGER = logging.getLogger(__name__) @@ -33,7 +38,9 @@ CONF_KEY = 'key' DOMAIN = 'xiaomi_aqara' -PY_XIAOMI_GATEWAY = 'xiaomi_gw' +PY_XIAOMI_GATEWAY = "xiaomi_gw" + +TIME_TILL_UNAVAILABLE = timedelta(minutes=150) SERVICE_PLAY_RINGTONE = 'play_ringtone' SERVICE_STOP_RINGTONE = 'stop_ringtone' @@ -201,20 +208,35 @@ class XiaomiDevice(Entity): def __init__(self, device, name, xiaomi_hub): """Initialize the Xiaomi device.""" self._state = None + self._is_available = True self._sid = device['sid'] self._name = '{}_{}'.format(name, self._sid) self._write_to_hub = xiaomi_hub.write_to_hub self._get_from_hub = xiaomi_hub.get_from_hub self._device_state_attributes = {} - xiaomi_hub.callbacks[self._sid].append(self.push_data) - self.parse_data(device['data']) + self._remove_unavailability_tracker = None + xiaomi_hub.callbacks[self._sid].append(self._add_push_data_job) + self.parse_data(device['data'], device['raw_data']) self.parse_voltage(device['data']) + def _add_push_data_job(self, *args): + self.hass.async_add_job(self.push_data, *args) + + @asyncio.coroutine + def async_added_to_hass(self): + """Start unavailability tracking.""" + self._async_track_unavailable() + @property def name(self): """Return the name of the device.""" return self._name + @property + def available(self): + """Return True if entity is available.""" + return self._is_available + @property def should_poll(self): """Return the polling state. No polling needed.""" @@ -225,13 +247,34 @@ class XiaomiDevice(Entity): """Return the state attributes.""" return self._device_state_attributes - def push_data(self, data): + @callback + def _async_set_unavailable(self, now): + """Set state to UNAVAILABLE.""" + self._remove_unavailability_tracker = None + self._is_available = False + self.async_schedule_update_ha_state() + + @callback + def _async_track_unavailable(self): + if self._remove_unavailability_tracker: + self._remove_unavailability_tracker() + self._remove_unavailability_tracker = async_track_point_in_utc_time( + self.hass, self._async_set_unavailable, + utcnow() + TIME_TILL_UNAVAILABLE) + if not self._is_available: + self._is_available = True + return True + return False + + @callback + def push_data(self, data, raw_data): """Push from Hub.""" _LOGGER.debug("PUSH >> %s: %s", self, data) - is_data = self.parse_data(data) + was_unavailable = self._async_track_unavailable() + is_data = self.parse_data(data, raw_data) is_voltage = self.parse_voltage(data) - if is_data or is_voltage: - self.schedule_update_ha_state() + if is_data or is_voltage or was_unavailable: + self.async_schedule_update_ha_state() def parse_voltage(self, data): """Parse battery level data sent by gateway.""" @@ -246,7 +289,7 @@ class XiaomiDevice(Entity): self._device_state_attributes[ATTR_BATTERY_LEVEL] = round(percent, 1) return True - def parse_data(self, data): + def parse_data(self, data, raw_data): """Parse data sent by gateway.""" raise NotImplementedError() diff --git a/requirements_all.txt b/requirements_all.txt index 0a42ea57372..ea3b05c48b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.7.1 +PyXiaomiGateway==0.8.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1