From d7af43b87da2aca3ee5717a6dd3a0fe7e56ac731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolf-Bastian=20P=C3=B6ttner?= <bastian@poettner.de> Date: Mon, 27 Feb 2017 06:35:33 +0100 Subject: [PATCH] Add support for MAX!Cube thermostats and window shutter sensors (#6105) --- .coveragerc | 3 + .../components/binary_sensor/maxcube.py | 76 ++++++ homeassistant/components/climate/maxcube.py | 216 ++++++++++++++++++ homeassistant/components/maxcube.py | 94 ++++++++ requirements_all.txt | 3 + 5 files changed, 392 insertions(+) create mode 100644 homeassistant/components/binary_sensor/maxcube.py create mode 100644 homeassistant/components/climate/maxcube.py create mode 100644 homeassistant/components/maxcube.py diff --git a/.coveragerc b/.coveragerc index 50bf08b0279..820c53d81ee 100644 --- a/.coveragerc +++ b/.coveragerc @@ -132,6 +132,9 @@ omit = homeassistant/components/zabbix.py homeassistant/components/*/zabbix.py + homeassistant/components/maxcube.py + homeassistant/components/*/maxcube.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/nx584.py diff --git a/homeassistant/components/binary_sensor/maxcube.py b/homeassistant/components/binary_sensor/maxcube.py new file mode 100644 index 00000000000..77448fd6adc --- /dev/null +++ b/homeassistant/components/binary_sensor/maxcube.py @@ -0,0 +1,76 @@ +""" +Support for MAX! Window Shutter via MAX! Cube. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/maxcube/ +""" + +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.maxcube import MAXCUBE_HANDLE +from homeassistant.const import STATE_UNKNOWN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Iterate through all MAX! Devices and add window shutters to HASS.""" + cube = hass.data[MAXCUBE_HANDLE].cube + + # List of devices + devices = [] + + for device in cube.devices: + # Create device name by concatenating room name + device name + name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name) + + # Only add Window Shutters + if cube.is_windowshutter(device): + # add device to HASS + devices.append(MaxCubeShutter(hass, name, device.rf_address)) + + if len(devices) > 0: + add_devices(devices) + + +class MaxCubeShutter(BinarySensorDevice): + """MAX! Cube BinarySensor device.""" + + def __init__(self, hass, name, rf_address): + """Initialize MAX! Cube BinarySensorDevice.""" + self._name = name + self._sensor_type = 'opening' + self._rf_address = rf_address + self._cubehandle = hass.data[MAXCUBE_HANDLE] + self._state = STATE_UNKNOWN + + @property + def should_poll(self): + """Polling is required.""" + return True + + @property + def name(self): + """Return the name of the BinarySensorDevice.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._sensor_type + + @property + def is_on(self): + """Return true if the binary sensor is on/open.""" + return self._state + + def update(self): + """Get latest data from MAX! Cube.""" + self._cubehandle.update() + + # Get the device we want to update + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Update our internal state + self._state = device.is_open diff --git a/homeassistant/components/climate/maxcube.py b/homeassistant/components/climate/maxcube.py new file mode 100644 index 00000000000..a04a547f534 --- /dev/null +++ b/homeassistant/components/climate/maxcube.py @@ -0,0 +1,216 @@ +""" +Support for MAX! Thermostats via MAX! Cube. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/maxcube/ +""" + +import socket +import logging + +from homeassistant.components.climate import ClimateDevice, STATE_AUTO +from homeassistant.components.maxcube import MAXCUBE_HANDLE +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.const import STATE_UNKNOWN + +_LOGGER = logging.getLogger(__name__) + +STATE_MANUAL = "manual" +STATE_BOOST = "boost" +STATE_VACATION = "vacation" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Iterate through all MAX! Devices and add thermostats to HASS.""" + cube = hass.data[MAXCUBE_HANDLE].cube + + # List of devices + devices = [] + + for device in cube.devices: + # Create device name by concatenating room name + device name + name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name) + + # Only add thermostats and wallthermostats + if cube.is_thermostat(device) or cube.is_wallthermostat(device): + # Add device to HASS + devices.append(MaxCubeClimate(hass, name, device.rf_address)) + + # Add all devices at once + if len(devices) > 0: + add_devices(devices) + + +class MaxCubeClimate(ClimateDevice): + """MAX! Cube ClimateDevice.""" + + def __init__(self, hass, name, rf_address): + """Initialize MAX! Cube ClimateDevice.""" + self._name = name + self._unit_of_measurement = TEMP_CELSIUS + self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST, + STATE_VACATION] + self._rf_address = rf_address + self._cubehandle = hass.data[MAXCUBE_HANDLE] + + @property + def should_poll(self): + """Polling is required.""" + return True + + @property + def name(self): + """Return the name of the ClimateDevice.""" + return self._name + + @property + def min_temp(self): + """Return the minimum temperature.""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return minimum temperature + return self.map_temperature_max_hass(device.min_temperature) + + @property + def max_temp(self): + """Return the maximum temperature.""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return maximum temperature + return self.map_temperature_max_hass(device.max_temperature) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def current_temperature(self): + """Return the current temperature.""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return current temperature + return self.map_temperature_max_hass(device.actual_temperature) + + @property + def current_operation(self): + """Return current operation (auto, manual, boost, vacation).""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Mode Mapping + return self.map_mode_max_hass(device.mode) + + @property + def operation_list(self): + """List of available operation modes.""" + return self._operation_list + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return target temperature + return self.map_temperature_max_hass(device.target_temperature) + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + # Fail is target temperature has not been supplied as argument + if kwargs.get(ATTR_TEMPERATURE) is None: + return False + + # Determine the new target temperature + target_temperature = kwargs.get(ATTR_TEMPERATURE) + + # Write the target temperature to the MAX! Cube. + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + cube = self._cubehandle.cube + + with self._cubehandle.mutex: + try: + cube.set_target_temperature(device, target_temperature) + except (socket.timeout, socket.error): + _LOGGER.error("Setting target temperature failed") + return False + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + # Get the device we want to update + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Mode Mapping + mode = self.map_mode_hass_max(operation_mode) + + # Write new mode to thermostat + if mode is None: + return False + + with self._cubehandle.mutex: + try: + self._cubehandle.cube.set_mode(device, mode) + except (socket.timeout, socket.error): + _LOGGER.error("Setting operation mode failed") + return False + + def update(self): + """Get latest data from MAX! Cube.""" + # Update the CubeHandle + self._cubehandle.update() + + @staticmethod + def map_temperature_max_hass(temperature): + """Map Temperature from MAX! to HASS.""" + if temperature is None: + return STATE_UNKNOWN + + return temperature + + @staticmethod + def map_mode_hass_max(operation_mode): + """Map HASS Operation Modes to MAX! Operation Modes.""" + from maxcube.device import \ + MAX_DEVICE_MODE_AUTOMATIC, \ + MAX_DEVICE_MODE_MANUAL, \ + MAX_DEVICE_MODE_VACATION, \ + MAX_DEVICE_MODE_BOOST + + if operation_mode == STATE_AUTO: + mode = MAX_DEVICE_MODE_AUTOMATIC + elif operation_mode == STATE_MANUAL: + mode = MAX_DEVICE_MODE_MANUAL + elif operation_mode == STATE_VACATION: + mode = MAX_DEVICE_MODE_VACATION + elif operation_mode == STATE_BOOST: + mode = MAX_DEVICE_MODE_BOOST + else: + mode = None + + return mode + + @staticmethod + def map_mode_max_hass(mode): + """Map MAX! Operation Modes to HASS Operation Modes.""" + from maxcube.device import \ + MAX_DEVICE_MODE_AUTOMATIC, \ + MAX_DEVICE_MODE_MANUAL, \ + MAX_DEVICE_MODE_VACATION, \ + MAX_DEVICE_MODE_BOOST + + if mode == MAX_DEVICE_MODE_AUTOMATIC: + operation_mode = STATE_AUTO + elif mode == MAX_DEVICE_MODE_MANUAL: + operation_mode = STATE_MANUAL + elif mode == MAX_DEVICE_MODE_VACATION: + operation_mode = STATE_VACATION + elif mode == MAX_DEVICE_MODE_BOOST: + operation_mode = STATE_BOOST + else: + operation_mode = None + + return operation_mode diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py new file mode 100644 index 00000000000..bc201825e83 --- /dev/null +++ b/homeassistant/components/maxcube.py @@ -0,0 +1,94 @@ +""" +Platform for the MAX! Cube LAN Gateway. + +For more details about this component, please refer to the documentation +https://home-assistant.io/components/maxcube/ +""" + +from socket import timeout +import logging +import time +from threading import Lock + +from homeassistant.components.discovery import load_platform +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +REQUIREMENTS = ['maxcube-api==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'maxcube' +MAXCUBE_HANDLE = 'maxcube' + +DEFAULT_PORT = 62910 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Establish connection to MAX! Cube.""" + from maxcube.connection import MaxCubeConnection + from maxcube.cube import MaxCube + + # Read Config + host = config.get(DOMAIN).get(CONF_HOST) + port = config.get(DOMAIN).get(CONF_PORT) + + # Assign Cube Handle to global variable + try: + cube = MaxCube(MaxCubeConnection(host, port)) + except timeout: + _LOGGER.error("Connection to Max!Cube could not be established") + cube = None + return False + + hass.data[MAXCUBE_HANDLE] = MaxCubeHandle(cube) + + # Load Climate (for Thermostats) + load_platform(hass, 'climate', DOMAIN) + + # Load BinarySensor (for Window Shutter) + load_platform(hass, 'binary_sensor', DOMAIN) + + # Initialization successfull + return True + + +class MaxCubeHandle(object): + """Keep the cube instance in one place and centralize the update.""" + + def __init__(self, cube): + """Initialize the Cube Handle.""" + # Cube handle + self.cube = cube + + # Instantiate Mutex + self.mutex = Lock() + + # Update Timestamp + self._updatets = time.time() + + def update(self): + """Pull the latest data from the MAX! Cube.""" + # Acquire mutex to prevent simultaneous update from multiple threads + with self.mutex: + # Only update every 60s + if (time.time() - self._updatets) >= 60: + _LOGGER.debug("UPDATE: Updating") + + try: + self.cube.update() + except timeout: + _LOGGER.error("Max!Cube connection failed") + return False + + self._updatets = time.time() + else: + _LOGGER.debug("UPDATE: Skipping") diff --git a/requirements_all.txt b/requirements_all.txt index 9f8ba5d2bcc..2fe7c9046fa 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -336,6 +336,9 @@ liveboxplaytv==1.4.9 # homeassistant.components.notify.matrix matrix-client==0.0.5 +# homeassistant.components.maxcube +maxcube-api==0.1.0 + # homeassistant.components.notify.message_bird messagebird==1.2.0 -- GitLab