From 7ad094b0a726abc5ab944ce38ff600d1fb85ca65 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Fri, 7 Sep 2018 18:16:19 +0200 Subject: [PATCH] Add OpenTherm Gateway climate platform (#16299) * Initial OpenTherm Gateway support. * Fix coveragerc and requirements_all*.txt Overall cleanup and polishing * Make hound/flake/travis happy * Basic improvements to comply with Home Assistant's style guidelines Changed wording from "component" to more appropriate terms where necessary Fixed small mistakes that snuck in during testing and/or due to my own ignorance ;) * Fixed overwriting state property * Fixed a bug with ROOM_SETPOINT_OVRD Updated dependency pyotgw to latest version * Remove unit_of_measurement from OpenThermGateway class * Cleanup after previous commits * Moved initialisation and configuration from async_setup_platform to async_added_to_hass * Make travis happy * Disable polling for this platform Improve update flow * Small improvements/optimisations --- .coveragerc | 1 + .../components/climate/opentherm_gw.py | 189 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 193 insertions(+) create mode 100644 homeassistant/components/climate/opentherm_gw.py diff --git a/.coveragerc b/.coveragerc index 85ed4e1fcf4..ca36f4a8dbb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -415,6 +415,7 @@ omit = homeassistant/components/climate/honeywell.py homeassistant/components/climate/knx.py homeassistant/components/climate/oem.py + homeassistant/components/climate/opentherm_gw.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py homeassistant/components/climate/sensibo.py diff --git a/homeassistant/components/climate/opentherm_gw.py b/homeassistant/components/climate/opentherm_gw.py new file mode 100644 index 00000000000..c1f7afa61b0 --- /dev/null +++ b/homeassistant/components/climate/opentherm_gw.py @@ -0,0 +1,189 @@ +""" +Support for OpenTherm Gateway devices. + +For more details about this component, please refer to the documentation at +http://home-assistant.io/components/climate.opentherm_gw/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA, + STATE_IDLE, STATE_HEAT, + STATE_COOL, + SUPPORT_TARGET_TEMPERATURE) +from homeassistant.const import (ATTR_TEMPERATURE, CONF_DEVICE, CONF_NAME, + PRECISION_HALVES, PRECISION_TENTHS, + TEMP_CELSIUS, PRECISION_WHOLE) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyotgw==0.1b0'] + +CONF_FLOOR_TEMP = "floor_temperature" +CONF_PRECISION = 'precision' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_NAME, default="OpenTherm Gateway"): cv.string, + vol.Optional(CONF_PRECISION): vol.In([PRECISION_TENTHS, PRECISION_HALVES, + PRECISION_WHOLE]), + vol.Optional(CONF_FLOOR_TEMP, default=False): cv.boolean, +}) + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the opentherm_gw device.""" + gateway = OpenThermGateway(config) + async_add_entities([gateway]) + + +class OpenThermGateway(ClimateDevice): + """Representation of a climate device.""" + + def __init__(self, config): + """Initialize the sensor.""" + import pyotgw + self.pyotgw = pyotgw + self.gateway = self.pyotgw.pyotgw() + self._device = config[CONF_DEVICE] + self.friendly_name = config.get(CONF_NAME) + self.floor_temp = config.get(CONF_FLOOR_TEMP) + self.temp_precision = config.get(CONF_PRECISION) + self._current_operation = STATE_IDLE + self._current_temperature = 0.0 + self._target_temperature = 0.0 + self._away_mode_a = None + self._away_mode_b = None + self._away_state_a = False + self._away_state_b = False + + async def async_added_to_hass(self): + """Connect to the OpenTherm Gateway device.""" + await self.gateway.connect(self.hass.loop, self._device) + self.gateway.subscribe(self.receive_report) + _LOGGER.debug("Connected to %s on %s", self.friendly_name, + self._device) + + async def receive_report(self, status): + """Receive and handle a new report from the Gateway.""" + _LOGGER.debug("Received report: %s", status) + ch_active = status.get(self.pyotgw.DATA_SLAVE_CH_ACTIVE) + cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE) + if ch_active: + self._current_operation = STATE_HEAT + elif cooling_active: + self._current_operation = STATE_COOL + else: + self._current_operation = STATE_IDLE + self._current_temperature = status.get(self.pyotgw.DATA_ROOM_TEMP) + + temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT_OVRD) + if temp is None: + temp = status.get(self.pyotgw.DATA_ROOM_SETPOINT) + self._target_temperature = temp + + # GPIO mode 5: 0 == Away + # GPIO mode 6: 1 == Away + gpio_a_state = status.get(self.pyotgw.OTGW_GPIO_A) + if gpio_a_state == 5: + self._away_mode_a = 0 + elif gpio_a_state == 6: + self._away_mode_a = 1 + else: + self._away_mode_a = None + gpio_b_state = status.get(self.pyotgw.OTGW_GPIO_B) + if gpio_b_state == 5: + self._away_mode_b = 0 + elif gpio_b_state == 6: + self._away_mode_b = 1 + else: + self._away_mode_b = None + if self._away_mode_a is not None: + self._away_state_a = (status.get(self.pyotgw.OTGW_GPIO_A_STATE) == + self._away_mode_a) + if self._away_mode_b is not None: + self._away_state_b = (status.get(self.pyotgw.OTGW_GPIO_B_STATE) == + self._away_mode_b) + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the friendly name.""" + return self.friendly_name + + @property + def precision(self): + """Return the precision of the system.""" + if self.temp_precision is not None: + return self.temp_precision + if self.hass.config.units.temperature_unit == TEMP_CELSIUS: + return PRECISION_HALVES + return PRECISION_WHOLE + + @property + def should_poll(self): + """Disable polling for this entity.""" + return False + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_operation + + @property + def current_temperature(self): + """Return the current temperature.""" + if self.floor_temp is True: + if self.temp_precision == PRECISION_HALVES: + return int(2 * self._current_temperature) / 2 + if self.temp_precision == PRECISION_TENTHS: + return int(10 * self._current_temperature) / 10 + return int(self._current_temperature) + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self.temp_precision + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._away_state_a or self._away_state_b + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs: + temp = float(kwargs[ATTR_TEMPERATURE]) + self._target_temperature = await self.gateway.set_target_temp( + temp) + self.async_schedule_update_ha_state() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def min_temp(self): + """Return the minimum temperature.""" + return 1 + + @property + def max_temp(self): + """Return the maximum temperature.""" + return 30 diff --git a/requirements_all.txt b/requirements_all.txt index d280fa14f15..b4a70fd0be2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -998,6 +998,9 @@ pyopenuv==1.0.4 # homeassistant.components.iota pyota==2.0.5 +# homeassistant.components.climate.opentherm_gw +pyotgw==0.1b0 + # homeassistant.auth.mfa_modules.totp # homeassistant.components.sensor.otp pyotp==2.2.6