hass-core/homeassistant/components/vallox/sensor.py
Andre Richter 738d00fb05 Increase vallox robustness on startup (#25382)
* Vallox: Increase robustness on startup

Experiments showed that timing of websocket requests to the Vallox firmware is
critical when fetching new metrics. Tests on different Raspberry Pis and x86
machines showed that those machines with little processing power tend to fail
the timing requirments during the busy startup phase of Home Assistant,
resulting in the Vallox integration failing to set itself up.

This patch catches Websocket's InvalidMessage, which is a symptom of failing the
timing requirements. Experiments again showed that on the Raspberry's, this
exception is catched once at startup, but the integration is running fine
afterwards.

* Update __init__.py

* Bump to new 2.1.0 version of api.

* Bump to api 2.2.0
2019-07-23 23:32:48 +02:00

233 lines
7.5 KiB
Python

"""Support for Vallox ventilation unit sensors."""
from datetime import datetime, timedelta
import logging
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP,
TEMP_CELSIUS)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from . import DOMAIN, METRIC_KEY_MODE, SIGNAL_VALLOX_STATE_UPDATE
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the sensors."""
if discovery_info is None:
return
name = hass.data[DOMAIN]['name']
state_proxy = hass.data[DOMAIN]['state_proxy']
sensors = [
ValloxProfileSensor(
name="{} Current Profile".format(name),
state_proxy=state_proxy,
device_class=None,
unit_of_measurement=None,
icon='mdi:gauge'
),
ValloxFanSpeedSensor(
name="{} Fan Speed".format(name),
state_proxy=state_proxy,
metric_key='A_CYC_FAN_SPEED',
device_class=None,
unit_of_measurement='%',
icon='mdi:fan'
),
ValloxSensor(
name="{} Extract Air".format(name),
state_proxy=state_proxy,
metric_key='A_CYC_TEMP_EXTRACT_AIR',
device_class=DEVICE_CLASS_TEMPERATURE,
unit_of_measurement=TEMP_CELSIUS,
icon=None
),
ValloxSensor(
name="{} Exhaust Air".format(name),
state_proxy=state_proxy,
metric_key='A_CYC_TEMP_EXHAUST_AIR',
device_class=DEVICE_CLASS_TEMPERATURE,
unit_of_measurement=TEMP_CELSIUS,
icon=None
),
ValloxSensor(
name="{} Outdoor Air".format(name),
state_proxy=state_proxy,
metric_key='A_CYC_TEMP_OUTDOOR_AIR',
device_class=DEVICE_CLASS_TEMPERATURE,
unit_of_measurement=TEMP_CELSIUS,
icon=None
),
ValloxSensor(
name="{} Supply Air".format(name),
state_proxy=state_proxy,
metric_key='A_CYC_TEMP_SUPPLY_AIR',
device_class=DEVICE_CLASS_TEMPERATURE,
unit_of_measurement=TEMP_CELSIUS,
icon=None
),
ValloxSensor(
name="{} Humidity".format(name),
state_proxy=state_proxy,
metric_key='A_CYC_RH_VALUE',
device_class=DEVICE_CLASS_HUMIDITY,
unit_of_measurement='%',
icon=None
),
ValloxFilterRemainingSensor(
name="{} Remaining Time For Filter".format(name),
state_proxy=state_proxy,
metric_key='A_CYC_REMAINING_TIME_FOR_FILTER',
device_class=DEVICE_CLASS_TIMESTAMP,
unit_of_measurement=None,
icon='mdi:filter'
),
]
async_add_entities(sensors, update_before_add=False)
class ValloxSensor(Entity):
"""Representation of a Vallox sensor."""
def __init__(self, name, state_proxy, metric_key, device_class,
unit_of_measurement, icon) -> None:
"""Initialize the Vallox sensor."""
self._name = name
self._state_proxy = state_proxy
self._metric_key = metric_key
self._device_class = device_class
self._unit_of_measurement = unit_of_measurement
self._icon = icon
self._available = None
self._state = None
@property
def should_poll(self):
"""Do not poll the device."""
return False
@property
def name(self):
"""Return the name."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def device_class(self):
"""Return the device class."""
return self._device_class
@property
def icon(self):
"""Return the icon."""
return self._icon
@property
def available(self):
"""Return true when state is known."""
return self._available
@property
def state(self):
"""Return the state."""
return self._state
async def async_added_to_hass(self):
"""Call to update."""
async_dispatcher_connect(self.hass, SIGNAL_VALLOX_STATE_UPDATE,
self._update_callback)
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
async def async_update(self):
"""Fetch state from the ventilation unit."""
try:
self._state = self._state_proxy.fetch_metric(self._metric_key)
self._available = True
except (OSError, KeyError) as err:
self._available = False
_LOGGER.error("Error updating sensor: %s", err)
# There seems to be a quirk with respect to the fan speed reporting. The device
# keeps on reporting the last valid fan speed from when the device was in
# regular operation mode, even if it left that state and has been shut off in
# the meantime.
#
# Therefore, first query the overall state of the device, and report zero
# percent fan speed in case it is not in regular operation mode.
class ValloxFanSpeedSensor(ValloxSensor):
"""Child class for fan speed reporting."""
async def async_update(self):
"""Fetch state from the ventilation unit."""
try:
# If device is in regular operation, continue.
if self._state_proxy.fetch_metric(METRIC_KEY_MODE) == 0:
await super().async_update()
else:
# Report zero percent otherwise.
self._state = 0
self._available = True
except (OSError, KeyError) as err:
self._available = False
_LOGGER.error("Error updating sensor: %s", err)
class ValloxProfileSensor(ValloxSensor):
"""Child class for profile reporting."""
def __init__(self, name, state_proxy, device_class, unit_of_measurement,
icon) -> None:
"""Initialize the Vallox sensor."""
super().__init__(name, state_proxy, None, device_class,
unit_of_measurement, icon)
async def async_update(self):
"""Fetch state from the ventilation unit."""
try:
self._state = self._state_proxy.get_profile()
self._available = True
except OSError as err:
self._available = False
_LOGGER.error("Error updating sensor: %s", err)
class ValloxFilterRemainingSensor(ValloxSensor):
"""Child class for filter remaining time reporting."""
async def async_update(self):
"""Fetch state from the ventilation unit."""
try:
days_remaining = int(
self._state_proxy.fetch_metric(self._metric_key))
days_remaining_delta = timedelta(days=days_remaining)
# Since only a delta of days is received from the device, fix the
# time so the timestamp does not change with every update.
now = datetime.utcnow().replace(
hour=13, minute=0, second=0, microsecond=0)
self._state = (now + days_remaining_delta).isoformat()
self._available = True
except (OSError, KeyError) as err:
self._available = False
_LOGGER.error("Error updating sensor: %s", err)