From 6cd9667364d4443ab534ff2b0d866616dbd7fc33 Mon Sep 17 00:00:00 2001 From: z0p Date: Fri, 7 Jun 2019 01:23:00 +0300 Subject: [PATCH] Support for Salda Smarty XV/XP Ventilation Unit (#21491) * Support for Salda Smarty XV/XP Ventilation Unit * Update binary_sensor.py * Update fan.py * Update sensor.py * Update __init__.py --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/smarty/__init__.py | 71 +++++++ .../components/smarty/binary_sensor.py | 110 +++++++++++ homeassistant/components/smarty/fan.py | 121 ++++++++++++ homeassistant/components/smarty/manifest.json | 13 ++ homeassistant/components/smarty/sensor.py | 180 ++++++++++++++++++ requirements_all.txt | 3 + 8 files changed, 500 insertions(+) create mode 100644 homeassistant/components/smarty/__init__.py create mode 100644 homeassistant/components/smarty/binary_sensor.py create mode 100644 homeassistant/components/smarty/fan.py create mode 100644 homeassistant/components/smarty/manifest.json create mode 100644 homeassistant/components/smarty/sensor.py diff --git a/.coveragerc b/.coveragerc index 7934fe64658..d2c271acd59 100644 --- a/.coveragerc +++ b/.coveragerc @@ -550,6 +550,7 @@ omit = homeassistant/components/slack/notify.py homeassistant/components/sma/sensor.py homeassistant/components/smappee/* + homeassistant/components/smarty/* homeassistant/components/smarthab/* homeassistant/components/smtp/notify.py homeassistant/components/snapcast/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index a74523aa530..a391241ed4c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -217,6 +217,7 @@ homeassistant/components/simplisafe/* @bachya homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre +homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff homeassistant/components/solaredge_local/* @drobtravels homeassistant/components/solax/* @squishykid diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py new file mode 100644 index 00000000000..d66c06de3eb --- /dev/null +++ b/homeassistant/components/smarty/__init__.py @@ -0,0 +1,71 @@ +"""Support to control a Salda Smarty XP/XV ventilation unit.""" + +from datetime import timedelta + +import ipaddress +import logging +import voluptuous as vol + +from homeassistant.const import (CONF_NAME, CONF_HOST) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval + +DOMAIN = 'smarty' +DATA_SMARTY = 'smarty' +SMARTY_NAME = 'Smarty' + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_NAME, default=SMARTY_NAME): cv.string + }), + }, + extra=vol.ALLOW_EXTRA) + +RPM = 'rpm' +SIGNAL_UPDATE_SMARTY = 'smarty_update' + + +def setup(hass, config): + """Set up the smarty environment.""" + from pysmarty import (Smarty) + conf = config[DOMAIN] + + host = conf[CONF_HOST] + name = conf[CONF_NAME] + + _LOGGER.debug("Name: %s, host: %s", name, host) + + smarty = Smarty(host=host) + + hass.data[DOMAIN] = { + 'api': smarty, + 'name': name + } + + # Initial update + smarty.update() + + # Load platforms + discovery.load_platform(hass, 'fan', DOMAIN, {}, config) + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + def poll_device_update(event_time): + """Update Smarty device.""" + _LOGGER.debug("Updating Smarty device...") + if smarty.update(): + _LOGGER.debug("Update success...") + dispatcher_send(hass, SIGNAL_UPDATE_SMARTY) + else: + _LOGGER.debug("Update failed...") + + track_time_interval(hass, poll_device_update, + timedelta(seconds=30)) + + return True diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py new file mode 100644 index 00000000000..a17e8fa85dc --- /dev/null +++ b/homeassistant/components/smarty/binary_sensor.py @@ -0,0 +1,110 @@ +"""Support for Salda Smarty XP/XV Ventilation Unit Binary Sensors.""" + +import logging + +from homeassistant.core import callback +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from . import (DOMAIN, SIGNAL_UPDATE_SMARTY) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Smarty Binary Sensor Platform.""" + smarty = hass.data[DOMAIN]['api'] + name = hass.data[DOMAIN]['name'] + + sensors = [AlarmSensor(name, smarty), + WarningSensor(name, smarty), + BoostSensor(name, smarty)] + + async_add_entities(sensors, True) + + +class SmartyBinarySensor(BinarySensorDevice): + """Representation of a Smarty Binary Sensor.""" + + def __init__(self, name, device_class, smarty): + """Initialize the entity.""" + self._name = name + self._state = None + self._sensor_type = device_class + self._smarty = smarty + + @property + def device_class(self): + """Return the class of the sensor.""" + return self._sensor_type + + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + async def async_added_to_hass(self): + """Call to update.""" + async_dispatcher_connect(self.hass, + SIGNAL_UPDATE_SMARTY, + self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + +class BoostSensor(SmartyBinarySensor): + """Boost State Binary Sensor.""" + + def __init__(self, name, smarty): + """Alarm Sensor Init.""" + super().__init__(name='{} Boost State'.format(name), + device_class=None, + smarty=smarty) + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + self._state = self._smarty.boost + + +class AlarmSensor(SmartyBinarySensor): + """Alarm Binary Sensor.""" + + def __init__(self, name, smarty): + """Alarm Sensor Init.""" + super().__init__(name='{} Alarm'.format(name), + device_class='problem', + smarty=smarty) + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + self._state = self._smarty.alarm + + +class WarningSensor(SmartyBinarySensor): + """Warning Sensor.""" + + def __init__(self, name, smarty): + """Warning Sensor Init.""" + super().__init__(name='{} Warning'.format(name), + device_class='problem', + smarty=smarty) + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + self._state = self._smarty.warning diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py new file mode 100644 index 00000000000..64a1e89ea88 --- /dev/null +++ b/homeassistant/components/smarty/fan.py @@ -0,0 +1,121 @@ +"""Platform to control a Salda Smarty XP/XV ventilation unit.""" + +import logging + +from homeassistant.core import callback +from homeassistant.components.fan import ( + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity) +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import (DOMAIN, SIGNAL_UPDATE_SMARTY) + +_LOGGER = logging.getLogger(__name__) + +SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + +SPEED_MAPPING = { + 1: SPEED_LOW, + 2: SPEED_MEDIUM, + 3: SPEED_HIGH +} +SPEED_TO_MODE = {v: k for k, v in SPEED_MAPPING.items()} + + +async def async_setup_platform(hass, config, + async_add_entities, discovery_info=None): + """Set up the Smarty Fan Platform.""" + smarty = hass.data[DOMAIN]['api'] + name = hass.data[DOMAIN]['name'] + + async_add_entities([SmartyFan(name, smarty)], True) + + +class SmartyFan(FanEntity): + """Representation of a Smarty Fan.""" + + def __init__(self, name, smarty): + """Initialize the entity.""" + self._name = name + self._speed = SPEED_OFF + self._state = None + self._smarty = smarty + + @property + def should_poll(self): + """Do not poll.""" + return False + + @property + def name(self): + """Return the name of the fan.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return 'mdi:air-conditioner' + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_SET_SPEED + + @property + def speed_list(self): + """List of available fan modes.""" + return SPEED_LIST + + @property + def is_on(self): + """Return state of the fan.""" + return self._state + + @property + def speed(self) -> str: + """Return speed of the fan.""" + return self._speed + + def turn_on(self, speed=None, **kwargs): + """Turn on the fan.""" + _LOGGER.debug('Turning on fan. Speed is %s', speed) + if speed is None: + if self._smarty.turn_on(SPEED_TO_MODE.get(self._speed)): + self._state = True + self._speed = SPEED_MEDIUM + else: + if self._smarty.set_fan_speed(SPEED_TO_MODE.get(speed)): + self._speed = speed + self._state = True + + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn off the fan.""" + _LOGGER.debug('Turning off fan') + if self._smarty.turn_off(): + self._state = False + + self.schedule_update_ha_state() + + async def async_added_to_hass(self): + """Call to update fan.""" + async_dispatcher_connect(self.hass, + SIGNAL_UPDATE_SMARTY, + self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + def update(self): + """Update state.""" + _LOGGER.debug('Updating state') + result = self._smarty.fan_speed + if result: + self._speed = SPEED_MAPPING[result] + _LOGGER.debug('Speed is %s, Mode is %s', self._speed, result) + self._state = True + else: + self._state = False diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json new file mode 100644 index 00000000000..b2e3deb4008 --- /dev/null +++ b/homeassistant/components/smarty/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "smarty", + "name": "smarty", + "documentation": "https://www.home-assistant.io/components/smarty", + "requirements": [ + "pysmarty==0.8" + ], + "dependencies": [], + "codeowners": [ + "@z0mbieprocess" + ] +} + diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py new file mode 100644 index 00000000000..5b33c9393b9 --- /dev/null +++ b/homeassistant/components/smarty/sensor.py @@ -0,0 +1,180 @@ +"""Support for Salda Smarty XP/XV Ventilation Unit Sensors.""" + +import datetime as dt +import logging + +from homeassistant.core import callback +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP) +import homeassistant.util.dt as dt_util +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from . import (DOMAIN, SIGNAL_UPDATE_SMARTY) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Smarty Sensor Platform.""" + smarty = hass.data[DOMAIN]['api'] + name = hass.data[DOMAIN]['name'] + + sensors = [SupplyAirTemperatureSensor(name, smarty), + ExtractAirTemperatureSensor(name, smarty), + OutdoorAirTemperatureSensor(name, smarty), + SupplyFanSpeedSensor(name, smarty), + ExtractFanSpeedSensor(name, smarty), + FilterDaysLeftSensor(name, smarty)] + + async_add_entities(sensors, True) + + +class SmartySensor(Entity): + """Representation of a Smarty Sensor.""" + + def __init__(self, name: str, device_class: str, + smarty, unit_of_measurement: str = ''): + """Initialize the entity.""" + self._name = name + self._state = None + self._sensor_type = device_class + self._unit_of_measurement = unit_of_measurement + self._smarty = smarty + + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._sensor_type + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + async def async_added_to_hass(self): + """Call to update.""" + async_dispatcher_connect(self.hass, + SIGNAL_UPDATE_SMARTY, + self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + +class SupplyAirTemperatureSensor(SmartySensor): + """Supply Air Temperature Sensor.""" + + def __init__(self, name, smarty): + """Supply Air Temperature Init.""" + super().__init__(name='{} Supply Air Temperature'.format(name), + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + smarty=smarty) + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + self._state = self._smarty.supply_air_temperature + + +class ExtractAirTemperatureSensor(SmartySensor): + """Extract Air Temperature Sensor.""" + + def __init__(self, name, smarty): + """Supply Air Temperature Init.""" + super().__init__(name='{} Extract Air Temperature'.format(name), + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + smarty=smarty) + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + self._state = self._smarty.extract_air_temperature + + +class OutdoorAirTemperatureSensor(SmartySensor): + """Extract Air Temperature Sensor.""" + + def __init__(self, name, smarty): + """Outdoor Air Temperature Init.""" + super().__init__(name='{} Outdoor Air Temperature'.format(name), + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + smarty=smarty) + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + self._state = self._smarty.outdoor_air_temperature + + +class SupplyFanSpeedSensor(SmartySensor): + """Supply Fan Speed RPM.""" + + def __init__(self, name, smarty): + """Supply Fan Speed RPM Init.""" + super().__init__(name='{} Supply Fan Speed'.format(name), + device_class=None, + unit_of_measurement=None, + smarty=smarty) + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + self._state = self._smarty.supply_fan_speed + + +class ExtractFanSpeedSensor(SmartySensor): + """Extract Fan Speed RPM.""" + + def __init__(self, name, smarty): + """Extract Fan Speed RPM Init.""" + super().__init__(name='{} Extract Fan Speed'.format(name), + device_class=None, + unit_of_measurement=None, + smarty=smarty) + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + self._state = self._smarty.extract_fan_speed + + +class FilterDaysLeftSensor(SmartySensor): + """Filter Days Left.""" + + def __init__(self, name, smarty): + """Filter Days Left Init.""" + super().__init__(name='{} Filter Days Left'.format(name), + device_class=DEVICE_CLASS_TIMESTAMP, + unit_of_measurement=None, + smarty=smarty) + self._days_left = 91 + + def update(self) -> None: + """Update state.""" + _LOGGER.debug('Updating sensor %s', self._name) + days_left = self._smarty.filter_timer + if days_left is not None and days_left != self._days_left: + self._state = dt_util.now() + dt.timedelta(days=days_left) + self._days_left = days_left diff --git a/requirements_all.txt b/requirements_all.txt index 6ef024957e7..5a1d7aa96d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1332,6 +1332,9 @@ pysmartapp==0.3.2 # homeassistant.components.smartthings pysmartthings==0.6.8 +# homeassistant.components.smarty +pysmarty==0.8 + # homeassistant.components.snmp pysnmp==4.4.9