* maxcube: Make it possible to return to auto mode It's currently not possible to switch back to auto (schedule) mode after changing to manual or vacation. Add `PRESET_NONE` to list of presets that will return thermostat back into auto (schedule) mode. Additionally, use `PRESET_AWAY` instead of custom preset name for vacation mode. * maxcube: Provide `hvac_action` based on valve state Not supported for wall thermostats. * maxcube: Add support for Comfort, Eco, Off and On modes Off is mapped to HVAC_OFF, while On - to HVAC_HEAT. * maxcube: Add `hvac_action` support for wall thermostats We check all thermostats in the same room as the wall thermostat. If at least one of them has its valve open - the room is being heated. * maxcube: Expose valve position as state attribute Also fix a small logical error in `hvac_action`. * maxcube: Fix linter errors and formatting * maxcube: Adjust mapping between MAX! and HA modes and presets MAX! 'Manual' mode now corresponds to 'Heating' mode of HA. MAX! 'Eco' and 'Comfort' temperatures are 'Heating' mode presets. MAX! 'On' mode is now a 'Heating' preset, too. * maxcube: Address review comments
296 lines
9.3 KiB
Python
296 lines
9.3 KiB
Python
"""Support for MAX! Thermostats via MAX! Cube."""
|
|
import logging
|
|
import socket
|
|
|
|
from maxcube.device import (
|
|
MAX_DEVICE_MODE_AUTOMATIC,
|
|
MAX_DEVICE_MODE_BOOST,
|
|
MAX_DEVICE_MODE_MANUAL,
|
|
MAX_DEVICE_MODE_VACATION,
|
|
)
|
|
|
|
from homeassistant.components.climate import ClimateDevice
|
|
from homeassistant.components.climate.const import (
|
|
CURRENT_HVAC_HEAT,
|
|
CURRENT_HVAC_IDLE,
|
|
CURRENT_HVAC_OFF,
|
|
HVAC_MODE_AUTO,
|
|
HVAC_MODE_HEAT,
|
|
HVAC_MODE_OFF,
|
|
PRESET_AWAY,
|
|
PRESET_BOOST,
|
|
PRESET_COMFORT,
|
|
PRESET_ECO,
|
|
PRESET_NONE,
|
|
SUPPORT_PRESET_MODE,
|
|
SUPPORT_TARGET_TEMPERATURE,
|
|
)
|
|
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
|
|
|
from . import DATA_KEY
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ATTR_VALVE_POSITION = "valve_position"
|
|
PRESET_ON = "on"
|
|
|
|
# There are two magic temperature values, which indicate:
|
|
# Off (valve fully closed)
|
|
OFF_TEMPERATURE = 4.5
|
|
# On (valve fully open)
|
|
ON_TEMPERATURE = 30.5
|
|
|
|
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
|
|
|
HASS_PRESET_TO_MAX_MODE = {
|
|
PRESET_AWAY: MAX_DEVICE_MODE_VACATION,
|
|
PRESET_BOOST: MAX_DEVICE_MODE_BOOST,
|
|
PRESET_NONE: MAX_DEVICE_MODE_AUTOMATIC,
|
|
PRESET_ON: MAX_DEVICE_MODE_MANUAL,
|
|
}
|
|
|
|
MAX_MODE_TO_HASS_PRESET = {
|
|
MAX_DEVICE_MODE_AUTOMATIC: PRESET_NONE,
|
|
MAX_DEVICE_MODE_BOOST: PRESET_BOOST,
|
|
MAX_DEVICE_MODE_MANUAL: PRESET_NONE,
|
|
MAX_DEVICE_MODE_VACATION: PRESET_AWAY,
|
|
}
|
|
|
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
|
"""Iterate through all MAX! Devices and add thermostats."""
|
|
devices = []
|
|
for handler in hass.data[DATA_KEY].values():
|
|
cube = handler.cube
|
|
for device in cube.devices:
|
|
name = f"{cube.room_by_id(device.room_id).name} {device.name}"
|
|
|
|
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
|
|
devices.append(MaxCubeClimate(handler, name, device.rf_address))
|
|
|
|
if devices:
|
|
add_entities(devices)
|
|
|
|
|
|
class MaxCubeClimate(ClimateDevice):
|
|
"""MAX! Cube ClimateDevice."""
|
|
|
|
def __init__(self, handler, name, rf_address):
|
|
"""Initialize MAX! Cube ClimateDevice."""
|
|
self._name = name
|
|
self._rf_address = rf_address
|
|
self._cubehandle = handler
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Return the list of supported features."""
|
|
return SUPPORT_FLAGS
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Return the polling state."""
|
|
return True
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the climate device."""
|
|
return self._name
|
|
|
|
@property
|
|
def min_temp(self):
|
|
"""Return the minimum temperature."""
|
|
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
|
return self.map_temperature_max_hass(device.min_temperature)
|
|
|
|
@property
|
|
def max_temp(self):
|
|
"""Return the maximum temperature."""
|
|
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
|
return self.map_temperature_max_hass(device.max_temperature)
|
|
|
|
@property
|
|
def temperature_unit(self):
|
|
"""Return the unit of measurement."""
|
|
return TEMP_CELSIUS
|
|
|
|
@property
|
|
def current_temperature(self):
|
|
"""Return the current temperature."""
|
|
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 hvac_mode(self):
|
|
"""Return current operation mode."""
|
|
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
|
if device.mode in [MAX_DEVICE_MODE_AUTOMATIC, MAX_DEVICE_MODE_BOOST]:
|
|
return HVAC_MODE_AUTO
|
|
if (
|
|
device.mode == MAX_DEVICE_MODE_MANUAL
|
|
and device.target_temperature == OFF_TEMPERATURE
|
|
):
|
|
return HVAC_MODE_OFF
|
|
|
|
return HVAC_MODE_HEAT
|
|
|
|
@property
|
|
def hvac_modes(self):
|
|
"""Return the list of available operation modes."""
|
|
return [HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_HEAT]
|
|
|
|
def set_hvac_mode(self, hvac_mode: str):
|
|
"""Set new target hvac mode."""
|
|
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
|
temp = device.target_temperature
|
|
mode = device.mode
|
|
|
|
if hvac_mode == HVAC_MODE_OFF:
|
|
temp = OFF_TEMPERATURE
|
|
mode = MAX_DEVICE_MODE_MANUAL
|
|
elif hvac_mode == HVAC_MODE_HEAT:
|
|
mode = MAX_DEVICE_MODE_MANUAL
|
|
else:
|
|
# Reset the temperature to a sane value.
|
|
# Ideally, we should send 0 and the device will set its
|
|
# temperature according to the schedule. However, current
|
|
# version of the library has a bug which causes an
|
|
# exception when setting values below 8.
|
|
if temp in [OFF_TEMPERATURE, ON_TEMPERATURE]:
|
|
temp = device.eco_temperature
|
|
mode = MAX_DEVICE_MODE_AUTOMATIC
|
|
|
|
cube = self._cubehandle.cube
|
|
with self._cubehandle.mutex:
|
|
try:
|
|
cube.set_temperature_mode(device, temp, mode)
|
|
except (socket.timeout, OSError):
|
|
_LOGGER.error("Setting HVAC mode failed")
|
|
return
|
|
|
|
@property
|
|
def hvac_action(self):
|
|
"""Return the current running hvac operation if supported."""
|
|
cube = self._cubehandle.cube
|
|
device = cube.device_by_rf(self._rf_address)
|
|
valve = 0
|
|
|
|
if cube.is_thermostat(device):
|
|
valve = device.valve_position
|
|
elif cube.is_wallthermostat(device):
|
|
for device in cube.devices_by_room(cube.room_by_id(device.room_id)):
|
|
if cube.is_thermostat(device) and device.valve_position > 0:
|
|
valve = device.valve_position
|
|
break
|
|
else:
|
|
return None
|
|
|
|
# Assume heating when valve is open
|
|
if valve > 0:
|
|
return CURRENT_HVAC_HEAT
|
|
|
|
return (
|
|
CURRENT_HVAC_OFF if self.hvac_mode == HVAC_MODE_OFF else CURRENT_HVAC_IDLE
|
|
)
|
|
|
|
@property
|
|
def target_temperature(self):
|
|
"""Return the temperature we try to reach."""
|
|
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
|
return self.map_temperature_max_hass(device.target_temperature)
|
|
|
|
def set_temperature(self, **kwargs):
|
|
"""Set new target temperatures."""
|
|
if kwargs.get(ATTR_TEMPERATURE) is None:
|
|
return False
|
|
|
|
target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
|
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, OSError):
|
|
_LOGGER.error("Setting target temperature failed")
|
|
return False
|
|
|
|
@property
|
|
def preset_mode(self):
|
|
"""Return the current preset mode."""
|
|
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
|
if self.hvac_mode == HVAC_MODE_OFF:
|
|
return PRESET_NONE
|
|
|
|
if device.mode == MAX_DEVICE_MODE_MANUAL:
|
|
if device.target_temperature == device.comfort_temperature:
|
|
return PRESET_COMFORT
|
|
if device.target_temperature == device.eco_temperature:
|
|
return PRESET_ECO
|
|
if device.target_temperature == ON_TEMPERATURE:
|
|
return PRESET_ON
|
|
return PRESET_NONE
|
|
|
|
return MAX_MODE_TO_HASS_PRESET[device.mode]
|
|
|
|
@property
|
|
def preset_modes(self):
|
|
"""Return available preset modes."""
|
|
return [
|
|
PRESET_NONE,
|
|
PRESET_BOOST,
|
|
PRESET_COMFORT,
|
|
PRESET_ECO,
|
|
PRESET_AWAY,
|
|
PRESET_ON,
|
|
]
|
|
|
|
def set_preset_mode(self, preset_mode):
|
|
"""Set new operation mode."""
|
|
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
|
temp = device.target_temperature
|
|
mode = MAX_DEVICE_MODE_AUTOMATIC
|
|
|
|
if preset_mode in [PRESET_COMFORT, PRESET_ECO, PRESET_ON]:
|
|
mode = MAX_DEVICE_MODE_MANUAL
|
|
if preset_mode == PRESET_COMFORT:
|
|
temp = device.comfort_temperature
|
|
elif preset_mode == PRESET_ECO:
|
|
temp = device.eco_temperature
|
|
else:
|
|
temp = ON_TEMPERATURE
|
|
else:
|
|
mode = HASS_PRESET_TO_MAX_MODE[preset_mode] or MAX_DEVICE_MODE_AUTOMATIC
|
|
|
|
with self._cubehandle.mutex:
|
|
try:
|
|
self._cubehandle.cube.set_temperature_mode(device, temp, mode)
|
|
except (socket.timeout, OSError):
|
|
_LOGGER.error("Setting operation mode failed")
|
|
return
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the optional state attributes."""
|
|
cube = self._cubehandle.cube
|
|
device = cube.device_by_rf(self._rf_address)
|
|
attributes = {}
|
|
|
|
if cube.is_thermostat(device):
|
|
attributes[ATTR_VALVE_POSITION] = device.valve_position
|
|
|
|
return attributes
|
|
|
|
def update(self):
|
|
"""Get latest data from MAX! Cube."""
|
|
self._cubehandle.update()
|
|
|
|
@staticmethod
|
|
def map_temperature_max_hass(temperature):
|
|
"""Map Temperature from MAX! to Home Assistant."""
|
|
if temperature is None:
|
|
return 0.0
|
|
|
|
return temperature
|