Skip to content

Commit

Permalink
Add integration for Vallox Ventilation Units (home-assistant#24660)
Browse files Browse the repository at this point in the history
* Add integration for Vallox Ventilation Units.

* Address review comments #1

* Address review comments #2

* Replace IOError with OSError.

* Bump to fixed version of vallox_websocket_api.
  • Loading branch information
andre-richter authored and MartinHjelmare committed Jun 25, 2019
1 parent 9813396 commit 236820d
Show file tree
Hide file tree
Showing 7 changed files with 698 additions and 0 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ omit =
homeassistant/components/uptimerobot/binary_sensor.py
homeassistant/components/uscis/sensor.py
homeassistant/components/usps/*
homeassistant/components/vallox/*
homeassistant/components/vasttrafik/sensor.py
homeassistant/components/velbus/*
homeassistant/components/velux/*
Expand Down
257 changes: 257 additions & 0 deletions homeassistant/components/vallox/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
"""Support for Vallox ventilation units."""

from datetime import timedelta
import ipaddress
import logging

from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox
from vallox_websocket_api.constants import vlxDevConstants
import voluptuous as vol

from homeassistant.const import CONF_HOST, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'vallox'
DEFAULT_NAME = 'Vallox'
SIGNAL_VALLOX_STATE_UPDATE = "vallox_state_update"
SCAN_INTERVAL = timedelta(seconds=60)

# Various metric keys that are reused between profiles.
METRIC_KEY_MODE = 'A_CYC_MODE'
METRIC_KEY_PROFILE_FAN_SPEED_HOME = 'A_CYC_HOME_SPEED_SETTING'
METRIC_KEY_PROFILE_FAN_SPEED_AWAY = 'A_CYC_AWAY_SPEED_SETTING'
METRIC_KEY_PROFILE_FAN_SPEED_BOOST = 'A_CYC_BOOST_SPEED_SETTING'

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
}),
}, extra=vol.ALLOW_EXTRA)

# pylint: disable=no-member
PROFILE_TO_STR_SETTABLE = {
VALLOX_PROFILE.HOME: 'Home',
VALLOX_PROFILE.AWAY: 'Away',
VALLOX_PROFILE.BOOST: 'Boost',
VALLOX_PROFILE.FIREPLACE: 'Fireplace',
}

STR_TO_PROFILE = {v: k for (k, v) in PROFILE_TO_STR_SETTABLE.items()}

# pylint: disable=no-member
PROFILE_TO_STR_REPORTABLE = {**{
VALLOX_PROFILE.NONE: 'None',
VALLOX_PROFILE.EXTRA: 'Extra',
}, **PROFILE_TO_STR_SETTABLE}

ATTR_PROFILE = 'profile'
ATTR_PROFILE_FAN_SPEED = 'fan_speed'

SERVICE_SCHEMA_SET_PROFILE = vol.Schema({
vol.Required(ATTR_PROFILE):
vol.All(cv.string, vol.In(STR_TO_PROFILE))
})

SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED = vol.Schema({
vol.Required(ATTR_PROFILE_FAN_SPEED):
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
})

SERVICE_SET_PROFILE = 'set_profile'
SERVICE_SET_PROFILE_FAN_SPEED_HOME = 'set_profile_fan_speed_home'
SERVICE_SET_PROFILE_FAN_SPEED_AWAY = 'set_profile_fan_speed_away'
SERVICE_SET_PROFILE_FAN_SPEED_BOOST = 'set_profile_fan_speed_boost'

SERVICE_TO_METHOD = {
SERVICE_SET_PROFILE: {
'method': 'async_set_profile',
'schema': SERVICE_SCHEMA_SET_PROFILE},
SERVICE_SET_PROFILE_FAN_SPEED_HOME: {
'method': 'async_set_profile_fan_speed_home',
'schema': SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED},
SERVICE_SET_PROFILE_FAN_SPEED_AWAY: {
'method': 'async_set_profile_fan_speed_away',
'schema': SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED},
SERVICE_SET_PROFILE_FAN_SPEED_BOOST: {
'method': 'async_set_profile_fan_speed_boost',
'schema': SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED},
}

DEFAULT_FAN_SPEED_HOME = 50
DEFAULT_FAN_SPEED_AWAY = 25
DEFAULT_FAN_SPEED_BOOST = 65


async def async_setup(hass, config):
"""Set up the client and boot the platforms."""
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
name = conf.get(CONF_NAME)

client = Vallox(host)
state_proxy = ValloxStateProxy(hass, client)
service_handler = ValloxServiceHandler(client, state_proxy)

hass.data[DOMAIN] = {
'client': client,
'state_proxy': state_proxy,
'name': name
}

for vallox_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[vallox_service]['schema']
hass.services.async_register(DOMAIN, vallox_service,
service_handler.async_handle,
schema=schema)

# Fetch initial state once before bringing up the platforms.
await state_proxy.async_update(None)

hass.async_create_task(
async_load_platform(hass, 'sensor', DOMAIN, {}, config))
hass.async_create_task(
async_load_platform(hass, 'fan', DOMAIN, {}, config))

async_track_time_interval(hass, state_proxy.async_update, SCAN_INTERVAL)

return True


class ValloxStateProxy:
"""Helper class to reduce websocket API calls."""

def __init__(self, hass, client):
"""Initialize the proxy."""
self._hass = hass
self._client = client
self._metric_cache = {}
self._profile = None
self._valid = False

def fetch_metric(self, metric_key):
"""Return cached state value."""
_LOGGER.debug("Fetching metric key: %s", metric_key)

if not self._valid:
raise OSError("Device state out of sync.")

if metric_key not in vlxDevConstants.__dict__:
raise KeyError("Unknown metric key: {}".format(metric_key))

return self._metric_cache[metric_key]

def get_profile(self):
"""Return cached profile value."""
_LOGGER.debug("Returning profile")

if not self._valid:
raise OSError("Device state out of sync.")

return PROFILE_TO_STR_REPORTABLE[self._profile]

async def async_update(self, event_time):
"""Fetch state update."""
_LOGGER.debug("Updating Vallox state cache")

try:
self._metric_cache = await self._hass.async_add_executor_job(
self._client.fetch_metrics)
self._profile = await self._hass.async_add_executor_job(
self._client.get_profile)
self._valid = True

except OSError as err:
_LOGGER.error("Error during state cache update: %s", err)
self._valid = False

async_dispatcher_send(self._hass, SIGNAL_VALLOX_STATE_UPDATE)


class ValloxServiceHandler:
"""Services implementation."""

def __init__(self, client, state_proxy):
"""Initialize the proxy."""
self._client = client
self._state_proxy = state_proxy

async def async_set_profile(self, profile: str = 'Home') -> bool:
"""Set the ventilation profile."""
_LOGGER.debug("Setting ventilation profile to: %s", profile)

try:
await self._hass.async_add_executor_job(
self._client.set_profile, STR_TO_PROFILE[profile])
return True

except OSError as err:
_LOGGER.error("Error setting ventilation profile: %s", err)
return False

async def async_set_profile_fan_speed_home(
self, fan_speed: int = DEFAULT_FAN_SPEED_HOME) -> bool:
"""Set the fan speed in percent for the Home profile."""
_LOGGER.debug("Setting Home fan speed to: %d%%", fan_speed)

try:
await self._hass.async_add_executor_job(
self._client.set_values,
{METRIC_KEY_PROFILE_FAN_SPEED_HOME: fan_speed})
return True

except OSError as err:
_LOGGER.error("Error setting fan speed for Home profile: %s", err)
return False

async def async_set_profile_fan_speed_away(
self, fan_speed: int = DEFAULT_FAN_SPEED_AWAY) -> bool:
"""Set the fan speed in percent for the Home profile."""
_LOGGER.debug("Setting Away fan speed to: %d%%", fan_speed)

try:
await self._hass.async_add_executor_job(
self._client.set_values,
{METRIC_KEY_PROFILE_FAN_SPEED_AWAY: fan_speed})
return True

except OSError as err:
_LOGGER.error("Error setting fan speed for Away profile: %s", err)
return False

async def async_set_profile_fan_speed_boost(
self, fan_speed: int = DEFAULT_FAN_SPEED_BOOST) -> bool:
"""Set the fan speed in percent for the Boost profile."""
_LOGGER.debug("Setting Boost fan speed to: %d%%", fan_speed)

try:
await self._hass.async_add_executor_job(
self._client.set_values,
{METRIC_KEY_PROFILE_FAN_SPEED_BOOST: fan_speed})
return True

except OSError as err:
_LOGGER.error("Error setting fan speed for Boost profile: %s",
err)
return False

async def async_handle(self, service):
"""Dispatch a service call."""
method = SERVICE_TO_METHOD.get(service.service)
params = {key: value for key, value in service.data.items()}

if not hasattr(self, method['method']):
_LOGGER.error("Service not implemented: %s", method['method'])
return

result = await getattr(self, method['method'])(**params)

# Force state_proxy to refresh device state, so that updates are
# propagated to platforms.
if result:
await self._state_proxy.async_update(None)
Loading

0 comments on commit 236820d

Please sign in to comment.