Files
versatile_thermostat/custom_components/versatile_thermostat/const.py
2024-12-23 12:04:22 +00:00

572 lines
18 KiB
Python

# pylint: disable=line-too-long
"""Constants for the Versatile Thermostat integration."""
import logging
import math
from typing import Literal
from datetime import datetime
from enum import Enum
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_NAME, Platform
from homeassistant.components.climate import (
# PRESET_ACTIVITY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
ClimateEntityFeature,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import dt as dt_util
from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI,
)
_LOGGER = logging.getLogger(__name__)
CONFIG_VERSION = 2
CONFIG_MINOR_VERSION = 0
PRESET_TEMP_SUFFIX = "_temp"
PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
PRESET_BOOST_AC = PRESET_BOOST + PRESET_AC_SUFFIX
DEVICE_MANUFACTURER = "JMCOLLIN"
DEVICE_MODEL = "Versatile Thermostat"
PRESET_POWER = "power"
PRESET_SECURITY = "security"
PRESET_FROST_PROTECTION = "frost"
HIDDEN_PRESETS = [PRESET_POWER, PRESET_SECURITY]
DOMAIN = "versatile_thermostat"
# The order is important.
PLATFORMS: list[Platform] = [
Platform.SELECT,
Platform.CLIMATE,
Platform.SENSOR,
# Number should be after CLIMATE
Platform.NUMBER,
Platform.BINARY_SENSOR,
Platform.SWITCH,
]
CONF_UNDERLYING_LIST = "underlying_entity_ids"
CONF_HEATER_KEEP_ALIVE = "heater_keep_alive"
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
CONF_LAST_SEEN_TEMP_SENSOR = "last_seen_temperature_sensor_entity_id"
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
CONF_POWER_SENSOR = "power_sensor_entity_id"
CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
CONF_WINDOW_SENSOR = "window_sensor_entity_id"
CONF_MOTION_SENSOR = "motion_sensor_entity_id"
CONF_DEVICE_POWER = "device_power"
CONF_CYCLE_MIN = "cycle_min"
CONF_PROP_FUNCTION = "proportional_function"
CONF_WINDOW_DELAY = "window_delay"
CONF_MOTION_DELAY = "motion_delay"
CONF_MOTION_OFF_DELAY = "motion_off_delay"
CONF_MOTION_PRESET = "motion_preset"
CONF_NO_MOTION_PRESET = "no_motion_preset"
CONF_TPI_COEF_INT = "tpi_coef_int"
CONF_TPI_COEF_EXT = "tpi_coef_ext"
CONF_PRESENCE_SENSOR = "presence_sensor_entity_id"
CONF_PRESET_POWER = "power_temp"
CONF_MINIMAL_ACTIVATION_DELAY = "minimal_activation_delay"
CONF_TEMP_MIN = "temp_min"
CONF_TEMP_MAX = "temp_max"
CONF_SECURITY_DELAY_MIN = "security_delay_min"
CONF_SECURITY_MIN_ON_PERCENT = "security_min_on_percent"
CONF_SECURITY_DEFAULT_ON_PERCENT = "security_default_on_percent"
CONF_THERMOSTAT_TYPE = "thermostat_type"
CONF_THERMOSTAT_CENTRAL_CONFIG = "thermostat_central_config"
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
CONF_THERMOSTAT_VALVE = "thermostat_over_valve"
CONF_USE_WINDOW_FEATURE = "use_window_feature"
CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
CONF_USE_POWER_FEATURE = "use_power_feature"
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature"
CONF_AC_MODE = "ac_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
CONF_AUTO_REGULATION_VALVE = "auto_regulation_valve"
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
CONF_AUTO_REGULATION_LIGHT = "auto_regulation_light"
CONF_AUTO_REGULATION_MEDIUM = "auto_regulation_medium"
CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong"
CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
CONF_AUTO_REGULATION_USE_DEVICE_TEMP = "auto_regulation_use_device_temp"
CONF_INVERSE_SWITCH = "inverse_switch_command"
CONF_AUTO_FAN_MODE = "auto_fan_mode"
CONF_AUTO_FAN_NONE = "auto_fan_none"
CONF_AUTO_FAN_LOW = "auto_fan_low"
CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
CONF_AUTO_FAN_HIGH = "auto_fan_high"
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
CONF_STEP_TEMPERATURE = "step_temperature"
CONF_OFFSET_CALIBRATION_LIST = "offset_calibration_entity_ids"
CONF_OPENING_DEGREE_LIST = "opening_degree_entity_ids"
CONF_CLOSING_DEGREE_LIST = "closing_degree_entity_ids"
CONF_MIN_OPENING_DEGREES = "min_opening_degrees"
# Deprecated
CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id"
CONF_CLIMATE = "climate_entity_id"
CONF_CLIMATE_2 = "climate_entity2_id"
CONF_CLIMATE_3 = "climate_entity3_id"
CONF_CLIMATE_4 = "climate_entity4_id"
CONF_VALVE = "valve_entity_id"
CONF_VALVE_2 = "valve_entity2_id"
CONF_VALVE_3 = "valve_entity3_id"
CONF_VALVE_4 = "valve_entity4_id"
# Global params into configuration.yaml
CONF_SHORT_EMA_PARAMS = "short_ema_params"
CONF_SAFETY_MODE = "safety_mode"
CONF_MAX_ON_PERCENT = "max_on_percent"
CONF_USE_MAIN_CENTRAL_CONFIG = "use_main_central_config"
CONF_USE_TPI_CENTRAL_CONFIG = "use_tpi_central_config"
CONF_USE_WINDOW_CENTRAL_CONFIG = "use_window_central_config"
CONF_USE_MOTION_CENTRAL_CONFIG = "use_motion_central_config"
CONF_USE_POWER_CENTRAL_CONFIG = "use_power_central_config"
CONF_USE_PRESENCE_CENTRAL_CONFIG = "use_presence_central_config"
CONF_USE_PRESETS_CENTRAL_CONFIG = "use_presets_central_config"
CONF_USE_ADVANCED_CENTRAL_CONFIG = "use_advanced_central_config"
CONF_USE_CENTRAL_MODE = "use_central_mode"
CONF_CENTRAL_BOILER_ACTIVATION_SRV = "central_boiler_activation_service"
CONF_CENTRAL_BOILER_DEACTIVATION_SRV = "central_boiler_deactivation_service"
CONF_USED_BY_CENTRAL_BOILER = "used_by_controls_central_boiler"
CONF_WINDOW_ACTION = "window_action"
CONF_AUTO_START_STOP_LEVEL = "auto_start_stop_level"
AUTO_START_STOP_LEVEL_NONE = "auto_start_stop_none"
AUTO_START_STOP_LEVEL_SLOW = "auto_start_stop_slow"
AUTO_START_STOP_LEVEL_MEDIUM = "auto_start_stop_medium"
AUTO_START_STOP_LEVEL_FAST = "auto_start_stop_fast"
CONF_AUTO_START_STOP_LEVELS = [
AUTO_START_STOP_LEVEL_NONE,
AUTO_START_STOP_LEVEL_SLOW,
AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_FAST,
]
# For explicit typing purpose only
TYPE_AUTO_START_STOP_LEVELS = Literal[ # pylint: disable=invalid-name
AUTO_START_STOP_LEVEL_FAST,
AUTO_START_STOP_LEVEL_MEDIUM,
AUTO_START_STOP_LEVEL_SLOW,
AUTO_START_STOP_LEVEL_NONE,
]
HVAC_OFF_REASON_NAME = "hvac_off_reason"
HVAC_OFF_REASON_MANUAL = "manual"
HVAC_OFF_REASON_AUTO_START_STOP = "auto_start_stop"
HVAC_OFF_REASON_WINDOW_DETECTION = "window_detection"
HVAC_OFF_REASONS = Literal[ # pylint: disable=invalid-name
HVAC_OFF_REASON_MANUAL,
HVAC_OFF_REASON_AUTO_START_STOP,
HVAC_OFF_REASON_WINDOW_DETECTION,
]
DEFAULT_SHORT_EMA_PARAMS = {
"max_alpha": 0.5,
# In sec
"halflife_sec": 300,
"precision": 2,
}
CONF_PRESETS = {
p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in (
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
)
}
CONF_PRESETS_WITH_AC = {
p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in (
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ECO_AC,
PRESET_COMFORT_AC,
PRESET_BOOST_AC,
)
}
PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = {
p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
)
}
CONF_PRESETS_AWAY_WITH_AC = {
p: f"{p}{PRESET_TEMP_SUFFIX}"
for p in (
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX,
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_ECO_AC + PRESET_AWAY_SUFFIX,
PRESET_COMFORT_AC + PRESET_AWAY_SUFFIX,
PRESET_BOOST_AC + PRESET_AWAY_SUFFIX,
)
}
CONF_PRESETS_SELECTIONABLE = [
PRESET_FROST_PROTECTION,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values())
CONF_PRESETS_WITH_AC_VALUES = list(CONF_PRESETS_WITH_AC.values())
CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
ALL_CONF = (
[
CONF_NAME,
CONF_HEATER_KEEP_ALIVE,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_WINDOW_SENSOR,
CONF_WINDOW_DELAY,
CONF_WINDOW_AUTO_OPEN_THRESHOLD,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD,
CONF_WINDOW_AUTO_MAX_DURATION,
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER,
CONF_CYCLE_MIN,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_INT,
CONF_TPI_COEF_EXT,
CONF_PRESENCE_SENSOR,
CONF_MINIMAL_ACTIVATION_DELAY,
CONF_TEMP_MIN,
CONF_TEMP_MAX,
CONF_SECURITY_DELAY_MIN,
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SECURITY_DEFAULT_ON_PERCENT,
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_USE_CENTRAL_BOILER_FEATURE,
CONF_AC_MODE,
CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_AUTO_REGULATION_USE_DEVICE_TEMP,
CONF_INVERSE_SWITCH,
CONF_AUTO_FAN_MODE,
CONF_USE_MAIN_CENTRAL_CONFIG,
CONF_USE_TPI_CENTRAL_CONFIG,
CONF_USE_PRESETS_CENTRAL_CONFIG,
CONF_USE_WINDOW_CENTRAL_CONFIG,
CONF_USE_MOTION_CENTRAL_CONFIG,
CONF_USE_POWER_CENTRAL_CONFIG,
CONF_USE_PRESENCE_CENTRAL_CONFIG,
CONF_USE_ADVANCED_CENTRAL_CONFIG,
CONF_USE_CENTRAL_MODE,
CONF_USED_BY_CENTRAL_BOILER,
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
CONF_WINDOW_ACTION,
CONF_STEP_TEMPERATURE,
]
+ CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES
+ CONF_PRESETS_WITH_AC_VALUES
+ CONF_PRESETS_AWAY_WITH_AC_VALUES,
)
CONF_FUNCTIONS = [
PROPORTIONAL_FUNCTION_TPI,
]
CONF_AUTO_REGULATION_MODES = [
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_VALVE,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_EXPERT,
]
CONF_THERMOSTAT_TYPES = [
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE,
]
CONF_AUTO_FAN_MODES = [
CONF_AUTO_FAN_NONE,
CONF_AUTO_FAN_LOW,
CONF_AUTO_FAN_MEDIUM,
CONF_AUTO_FAN_HIGH,
CONF_AUTO_FAN_TURBO,
]
CONF_WINDOW_TURN_OFF = "window_turn_off"
CONF_WINDOW_FAN_ONLY = "window_fan_only"
CONF_WINDOW_FROST_TEMP = "window_frost_temp"
CONF_WINDOW_ECO_TEMP = "window_eco_temp"
CONF_WINDOW_ACTIONS = [
CONF_WINDOW_TURN_OFF,
CONF_WINDOW_FAN_ONLY,
CONF_WINDOW_FROST_TEMP,
CONF_WINDOW_ECO_TEMP,
]
SUPPORT_FLAGS = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
SERVICE_SET_SECURITY = "set_security"
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode"
SERVICE_SET_AUTO_FAN_MODE = "set_auto_fan_mode"
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
ATTR_TOTAL_ENERGY = "total_energy"
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
AUTO_FAN_DTEMP_THRESHOLD = 2
AUTO_FAN_DEACTIVATED_MODES = ["mute", "auto", "low"]
CENTRAL_CONFIG_NAME = "Central configuration"
CENTRAL_MODE_AUTO = "Auto"
CENTRAL_MODE_STOPPED = "Stopped"
CENTRAL_MODE_HEAT_ONLY = "Heat only"
CENTRAL_MODE_COOL_ONLY = "Cool only"
CENTRAL_MODE_FROST_PROTECTION = "Frost protection"
CENTRAL_MODES = [
CENTRAL_MODE_AUTO,
CENTRAL_MODE_STOPPED,
CENTRAL_MODE_HEAT_ONLY,
CENTRAL_MODE_COOL_ONLY,
CENTRAL_MODE_FROST_PROTECTION,
]
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
class RegulationParamSlow:
"""Light parameters for slow latency regulation"""
kp: float = (
0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
)
ki: float = (
0.8 / 288.0
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: float = (
1.0 / 25.0
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: float = (
0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
)
accumulated_error_threshold: float = (
2.0 * 288
) # this allows up to 2°C long term offset in both directions
class RegulationParamLight:
"""Light parameters for regulation"""
kp: float = 0.2
ki: float = 0.05
k_ext: float = 0.05
offset_max: float = 1.5
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 10
class RegulationParamMedium:
"""Light parameters for regulation"""
kp: float = 0.3
ki: float = 0.05
k_ext: float = 0.1
offset_max: float = 2
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 20
class RegulationParamStrong:
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: float = 0.4
ki: float = 0.08
k_ext: float = 0.0
offset_max: float = 5
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 50
# Not used now
class RegulationParamVeryStrong:
"""Strong parameters for regulation"""
kp: float = 0.6
ki: float = 0.1
k_ext: float = 0.2
offset_max: float = 8
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 80
class EventType(Enum):
"""The event type that can be sent"""
SECURITY_EVENT: str = "versatile_thermostat_security_event"
POWER_EVENT: str = "versatile_thermostat_power_event"
TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event"
HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_event"
CENTRAL_BOILER_EVENT: str = "versatile_thermostat_central_boiler_event"
PRESET_EVENT: str = "versatile_thermostat_preset_event"
WINDOW_AUTO_EVENT: str = "versatile_thermostat_window_auto_event"
AUTO_START_STOP_EVENT: str = "versatile_thermostat_auto_start_stop_event"
def send_vtherm_event(hass, event_type: EventType, entity, data: dict):
"""Send an event"""
_LOGGER.info("%s - Sending event %s with data: %s", entity, event_type, data)
data["entity_id"] = entity.entity_id
data["name"] = entity.name
data["state_attributes"] = entity.state_attributes
hass.bus.fire(event_type.value, data)
def get_safe_float(hass, entity_id: str):
"""Get a safe float state value for an entity.
Return None if entity is not available"""
if (
entity_id is None
or not (state := hass.states.get(entity_id))
or state.state == "unknown"
or state.state == "unavailable"
):
return None
float_val = float(state.state)
return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
class NowClass:
"""For testing purpose only"""
@staticmethod
def get_now(hass: HomeAssistant) -> datetime:
"""A test function to get the now.
For testing purpose this method can be overriden to get a specific
timestamp.
"""
return datetime.now(get_tz(hass))
class UnknownEntity(HomeAssistantError):
"""Error to indicate there is an unknown entity_id given."""
class WindowOpenDetectionMethod(HomeAssistantError):
"""Error to indicate there is an error in the window open detection method given."""
class NoCentralConfig(HomeAssistantError):
"""Error to indicate that we try to use a central configuration but no VTherm of type CENTRAL CONFIGURATION has been found"""
class ServiceConfigurationError(HomeAssistantError):
"""Error in the service configuration to control the central boiler"""
class ConfigurationNotCompleteError(HomeAssistantError):
"""Error the configuration is not complete"""
class ValveRegulationNbEntitiesIncorrect(HomeAssistantError):
"""Error to indicate there is an error in the configuration of the TRV with valve regulation.
The number of specific entities is incorrect."""
class ValveRegulationMinOpeningDegreesIncorrect(HomeAssistantError):
"""Error to indicate that the minimal opening degrees is not a list of int separated by coma"""
class overrides: # pylint: disable=invalid-name
"""An annotation to inform overrides"""
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
return self.func.__get__(instance, owner)
def __call__(self, *args, **kwargs):
raise RuntimeError(f"Method {self.func.__name__} should have been overridden")