* 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
233 lines
7.5 KiB
Python
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)
|