core/homeassistant/components/maxcube/climate.py

265 lines
8.4 KiB
Python

"""Support for MAX! Thermostats via MAX! Cube."""
from __future__ import annotations
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 ClimateEntity
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
# Lowest Value without turning off
MIN_TEMPERATURE = 5.0
# Largest Value without fully opening
MAX_TEMPERATURE = 30.0
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
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():
for device in handler.cube.devices:
if device.is_thermostat() or device.is_wallthermostat():
devices.append(MaxCubeClimate(handler, device))
if devices:
add_entities(devices)
class MaxCubeClimate(ClimateEntity):
"""MAX! Cube ClimateEntity."""
def __init__(self, handler, device):
"""Initialize MAX! Cube ClimateEntity."""
room = handler.cube.room_by_id(device.room_id)
self._name = f"{room.name} {device.name}"
self._cubehandle = handler
self._device = device
@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 unique_id(self):
"""Return a unique ID."""
return self._device.serial
@property
def min_temp(self):
"""Return the minimum temperature."""
temp = self._device.min_temperature or MIN_TEMPERATURE
# OFF_TEMPERATURE (always off) a is valid temperature to maxcube but not to Home Assistant.
# We use HVAC_MODE_OFF instead to represent a turned off thermostat.
return max(temp, MIN_TEMPERATURE)
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._device.max_temperature or MAX_TEMPERATURE
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self._device.actual_temperature
@property
def hvac_mode(self):
"""Return current operation mode."""
mode = self._device.mode
if mode in [MAX_DEVICE_MODE_AUTOMATIC, MAX_DEVICE_MODE_BOOST]:
return HVAC_MODE_AUTO
if (
mode == MAX_DEVICE_MODE_MANUAL
and self._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."""
if hvac_mode == HVAC_MODE_OFF:
self._set_target(MAX_DEVICE_MODE_MANUAL, OFF_TEMPERATURE)
elif hvac_mode == HVAC_MODE_HEAT:
temp = max(self._device.target_temperature, self.min_temp)
self._set_target(MAX_DEVICE_MODE_MANUAL, temp)
elif hvac_mode == HVAC_MODE_AUTO:
self._set_target(MAX_DEVICE_MODE_AUTOMATIC, None)
else:
raise ValueError(f"unsupported HVAC mode {hvac_mode}")
def _set_target(self, mode: int | None, temp: float | None) -> None:
"""
Set the mode and/or temperature of the thermostat.
@param mode: this is the mode to change to.
@param temp: the temperature to target.
Both parameters are optional. When mode is undefined, it keeps
the previous mode. When temp is undefined, it fetches the
temperature from the weekly schedule when mode is
MAX_DEVICE_MODE_AUTOMATIC and keeps the previous
temperature otherwise.
"""
with self._cubehandle.mutex:
try:
self._cubehandle.cube.set_temperature_mode(self._device, temp, mode)
except (socket.timeout, OSError):
_LOGGER.error("Setting HVAC mode failed")
@property
def hvac_action(self):
"""Return the current running hvac operation if supported."""
valve = 0
if self._device.is_thermostat():
valve = self._device.valve_position
elif self._device.is_wallthermostat():
cube = self._cubehandle.cube
room = cube.room_by_id(self._device.room_id)
for device in cube.devices_by_room(room):
if device.is_thermostat() 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."""
temp = self._device.target_temperature
if temp is None or temp < self.min_temp or temp > self.max_temp:
return None
return temp
def set_temperature(self, **kwargs):
"""Set new target temperatures."""
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is None:
raise ValueError(
f"No {ATTR_TEMPERATURE} parameter passed to set_temperature method."
)
self._set_target(None, temp)
@property
def preset_mode(self):
"""Return the current preset mode."""
if self._device.mode == MAX_DEVICE_MODE_MANUAL:
if self._device.target_temperature == self._device.comfort_temperature:
return PRESET_COMFORT
if self._device.target_temperature == self._device.eco_temperature:
return PRESET_ECO
if self._device.target_temperature == ON_TEMPERATURE:
return PRESET_ON
elif self._device.mode == MAX_DEVICE_MODE_BOOST:
return PRESET_BOOST
elif self._device.mode == MAX_DEVICE_MODE_VACATION:
return PRESET_AWAY
return PRESET_NONE
@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."""
if preset_mode == PRESET_COMFORT:
self._set_target(MAX_DEVICE_MODE_MANUAL, self._device.comfort_temperature)
elif preset_mode == PRESET_ECO:
self._set_target(MAX_DEVICE_MODE_MANUAL, self._device.eco_temperature)
elif preset_mode == PRESET_ON:
self._set_target(MAX_DEVICE_MODE_MANUAL, ON_TEMPERATURE)
elif preset_mode == PRESET_AWAY:
self._set_target(MAX_DEVICE_MODE_VACATION, None)
elif preset_mode == PRESET_BOOST:
self._set_target(MAX_DEVICE_MODE_BOOST, None)
elif preset_mode == PRESET_NONE:
self._set_target(MAX_DEVICE_MODE_AUTOMATIC, None)
else:
raise ValueError(f"unsupported preset mode {preset_mode}")
@property
def extra_state_attributes(self):
"""Return the optional state attributes."""
if not self._device.is_thermostat():
return {}
return {ATTR_VALVE_POSITION: self._device.valve_position}
def update(self):
"""Get latest data from MAX! Cube."""
self._cubehandle.update()