Compare commits
10 Commits
jmcollin78
...
3.8.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c497d24fb | ||
|
|
fe85ead916 | ||
|
|
7d5ced55d3 | ||
|
|
9d099e3169 | ||
|
|
49377de248 | ||
|
|
6886dd6fb5 | ||
|
|
dde622e632 | ||
|
|
076d9eae24 | ||
|
|
3356489f9d | ||
|
|
b323d676dc |
@@ -198,7 +198,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._saved_hvac_mode = None
|
||||
self._window_call_cancel = None
|
||||
self._motion_call_cancel = None
|
||||
self._cur_ext_temp = None
|
||||
self._cur_temp = None
|
||||
self._ac_mode = None
|
||||
self._last_ext_temperature_mesure = None
|
||||
@@ -1220,7 +1219,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||
"""Set the target temperature and the target temperature of underlying climate if any
|
||||
For testing purpose you can pass an event_timestamp.
|
||||
"""
|
||||
self._target_temp = temperature
|
||||
return
|
||||
|
||||
@@ -2248,7 +2249,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating()
|
||||
self.update_custom_attributes()
|
||||
|
||||
#PR - Adding Window ByPass
|
||||
async def service_set_window_bypass_state(self, window_bypass):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_window_bypass
|
||||
|
||||
@@ -24,12 +24,12 @@ from .const import (
|
||||
SERVICE_SET_PRESENCE,
|
||||
SERVICE_SET_PRESET_TEMPERATURE,
|
||||
SERVICE_SET_SECURITY,
|
||||
#PR - Adding Window ByPass
|
||||
SERVICE_SET_WINDOW_BYPASS,
|
||||
SERVICE_SET_AUTO_REGULATION_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_THERMOSTAT_VALVE
|
||||
)
|
||||
|
||||
from .thermostat_switch import ThermostatOverSwitch
|
||||
@@ -66,8 +66,6 @@ async def async_setup_entry(
|
||||
entity = ThermostatOverValve(hass, unique_id, name, entry.data)
|
||||
|
||||
async_add_entities([entity], True)
|
||||
# No more needed
|
||||
# VersatileThermostat.add_entity(entry.entry_id, entity)
|
||||
|
||||
# Add services
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
@@ -101,7 +99,6 @@ async def async_setup_entry(
|
||||
"service_set_security",
|
||||
)
|
||||
|
||||
#PR - Adding Window ByPass
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_WINDOW_BYPASS,
|
||||
{
|
||||
@@ -110,3 +107,11 @@ async def async_setup_entry(
|
||||
},
|
||||
"service_set_window_bypass_state",
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_AUTO_REGULATION_MODE,
|
||||
{
|
||||
vol.Required("auto_regulation_mode"): vol.In(["None", "Light", "Medium", "Strong"]),
|
||||
},
|
||||
"service_set_auto_regulation_mode",
|
||||
)
|
||||
|
||||
@@ -1,18 +1,50 @@
|
||||
""" Some usefull commons class """
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
from homeassistant.core import HomeAssistant, callback, Event
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .const import DOMAIN, DEVICE_MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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))
|
||||
|
||||
def round_to_nearest(n:float, x: float)->float:
|
||||
""" Round a number to the nearest x (which should be decimal but not null)
|
||||
Example:
|
||||
nombre1 = 3.2
|
||||
nombre2 = 4.7
|
||||
x = 0.3
|
||||
|
||||
nombre_arrondi1 = round_to_nearest(nombre1, x)
|
||||
nombre_arrondi2 = round_to_nearest(nombre2, x)
|
||||
|
||||
print(nombre_arrondi1) # Output: 3.3
|
||||
print(nombre_arrondi2) # Output: 4.6
|
||||
"""
|
||||
assert x > 0
|
||||
return round(n * (1/x)) / (1/x)
|
||||
|
||||
class VersatileThermostatBaseEntity(Entity):
|
||||
"""A base class for all entities"""
|
||||
@@ -98,7 +130,7 @@ class VersatileThermostatBaseEntity(Entity):
|
||||
await try_find_climate(None)
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event):
|
||||
async def async_my_climate_changed(self, event: Event): # pylint: disable=unused-argument
|
||||
"""Called when my climate have change
|
||||
This method aims to be overriden to take the status change
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
# pylint: disable=too-many-lines
|
||||
# pylint: disable=invalid-name
|
||||
# pylint: disable=line-too-long, too-many-lines, invalid-name
|
||||
|
||||
"""Config flow for Versatile Thermostat integration."""
|
||||
from __future__ import annotations
|
||||
@@ -101,6 +99,11 @@ from .const import (
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
CONF_AUTO_REGULATION_MODES,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
UnknownEntity,
|
||||
WindowOpenDetectionMethod,
|
||||
)
|
||||
@@ -256,6 +259,16 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_AUTO_REGULATION_MODE, default=CONF_AUTO_REGULATION_NONE
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=CONF_AUTO_REGULATION_MODES, translation_key="auto_regulation_mode"
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float),
|
||||
vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
"""Constants for the Versatile Thermostat integration."""
|
||||
|
||||
from enum import Enum
|
||||
@@ -82,6 +83,13 @@ CONF_VALVE = "valve_entity_id"
|
||||
CONF_VALVE_2 = "valve_entity2_id"
|
||||
CONF_VALVE_3 = "valve_entity3_id"
|
||||
CONF_VALVE_4 = "valve_entity4_id"
|
||||
CONF_AUTO_REGULATION_MODE= "auto_regulation_mode"
|
||||
CONF_AUTO_REGULATION_NONE= "auto_regulation_none"
|
||||
CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light"
|
||||
CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
|
||||
CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong"
|
||||
CONF_AUTO_REGULATION_DTEMP="auto_regulation_dtemp"
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN="auto_regulation_periode_min"
|
||||
|
||||
CONF_PRESETS = {
|
||||
p: f"{p}_temp"
|
||||
@@ -183,7 +191,9 @@ ALL_CONF = (
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN
|
||||
]
|
||||
+ CONF_PRESETS_VALUES
|
||||
+ CONF_PRESETS_AWAY_VALUES
|
||||
@@ -195,6 +205,8 @@ CONF_FUNCTIONS = [
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
|
||||
CONF_AUTO_REGULATION_MODES = [CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_STRONG]
|
||||
|
||||
CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_VALVE]
|
||||
|
||||
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
@@ -202,8 +214,8 @@ SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
SERVICE_SET_PRESENCE = "set_presence"
|
||||
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
||||
SERVICE_SET_SECURITY = "set_security"
|
||||
#PR - Adding Window ByPass
|
||||
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
|
||||
SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode"
|
||||
|
||||
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
|
||||
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
||||
@@ -211,6 +223,33 @@ DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
||||
ATTR_TOTAL_ENERGY = "total_energy"
|
||||
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
|
||||
|
||||
class RegulationParamLight:
|
||||
""" Light parameters for regulation"""
|
||||
kp:float = 0.2
|
||||
ki:float = 0.05
|
||||
k_ext:float = 0.1
|
||||
offset_max:float = 2
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 20
|
||||
|
||||
|
||||
class RegulationParamMedium:
|
||||
""" Medium parameters for regulation"""
|
||||
kp:float = 0.4
|
||||
ki:float = 0.08
|
||||
k_ext:float = 0.1
|
||||
offset_max:float = 3
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 25
|
||||
|
||||
class RegulationParamStrong:
|
||||
""" Strong parameters for regulation"""
|
||||
kp:float = 0.6
|
||||
ki:float = 0.1
|
||||
k_ext:float = 0.2
|
||||
offset_max:float = 4
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 30
|
||||
|
||||
class EventType(Enum):
|
||||
"""The event type that can be sent"""
|
||||
|
||||
70
custom_components/versatile_thermostat/pi_algorithm.py
Normal file
70
custom_components/versatile_thermostat/pi_algorithm.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" The PI algorithm implementation """
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class PITemperatureRegulator:
|
||||
""" A class implementing a PI Algorithm
|
||||
PI algorithms calculate a target temperature by adding an offset which is calculating as follow:
|
||||
- offset = kp * error + ki * accumulated_error
|
||||
|
||||
To use it you must:
|
||||
- instanciate the class and gives the algorithm parameters: kp, ki, offset_max, stabilization_threshold, accumulated_error_threshold
|
||||
- call calculate_regulated_temperature with the internal and external temperature
|
||||
- call set_target_temp when the target temperature change.
|
||||
"""
|
||||
|
||||
def __init__(self, target_temp: float, kp: float, ki: float, k_ext: float, offset_max: float, stabilization_threshold: float, accumulated_error_threshold: float):
|
||||
self.target_temp:float = target_temp
|
||||
self.kp:float = kp # proportionnel gain
|
||||
self.ki:float = ki # integral gain
|
||||
self.k_ext:float = k_ext # exterior gain
|
||||
self.offset_max:float = offset_max
|
||||
self.stabilization_threshold:float = stabilization_threshold
|
||||
self.accumulated_error:float = 0
|
||||
self.accumulated_error_threshold:float = accumulated_error_threshold
|
||||
|
||||
def set_target_temp(self, target_temp):
|
||||
""" Set the new target_temp"""
|
||||
self.target_temp = target_temp
|
||||
self.accumulated_error = 0
|
||||
|
||||
def calculate_regulated_temperature(self, internal_temp: float, external_temp:float): # pylint: disable=unused-argument
|
||||
""" Calculate a new target_temp given some temperature"""
|
||||
if internal_temp is None or external_temp is None:
|
||||
_LOGGER.warning("Internal_temp or external_temp are not set. Regulation will be suspended")
|
||||
return self.target_temp
|
||||
|
||||
# Calculate the error factor (P)
|
||||
error = self.target_temp - internal_temp
|
||||
|
||||
# Calculate the sum of error (I)
|
||||
self.accumulated_error += error
|
||||
|
||||
# Capping of the error
|
||||
self.accumulated_error = min(self.accumulated_error_threshold, max(-self.accumulated_error_threshold, self.accumulated_error))
|
||||
|
||||
# Calculate the offset (proportionnel + intégral)
|
||||
offset = self.kp * error + self.ki * self.accumulated_error
|
||||
|
||||
# Calculate the exterior offset
|
||||
offset_ext = self.k_ext * (self.target_temp - external_temp)
|
||||
|
||||
# Capping of offset_ext
|
||||
total_offset = offset + offset_ext
|
||||
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
|
||||
|
||||
|
||||
# If temperature is near the target_temp, reset the accumulated_error
|
||||
if abs(error) < self.stabilization_threshold:
|
||||
_LOGGER.debug("Stabilisation")
|
||||
self.accumulated_error = 0
|
||||
|
||||
result = round(self.target_temp + total_offset, 1)
|
||||
|
||||
_LOGGER.debug("PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
|
||||
error, self.accumulated_error, offset, offset_ext, self.target_temp, result)
|
||||
|
||||
return result
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
UnitOfTemperature
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -24,6 +25,7 @@ from .const import (
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
)
|
||||
|
||||
@@ -63,6 +65,9 @@ async def async_setup_entry(
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
|
||||
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE:
|
||||
entities.append(RegulatedTemperatureSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -470,3 +475,53 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 2
|
||||
|
||||
class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a Energy sensor which exposes the energy"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the regulated temperature sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Regulated temperature"
|
||||
self._attr_unique_id = f"{self._device_name}_regulated_temperature"
|
||||
|
||||
@callback
|
||||
async def async_my_climate_changed(self, event: Event = None):
|
||||
"""Called when my climate have change"""
|
||||
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
|
||||
|
||||
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf(
|
||||
self.my_climate.regulated_target_temp
|
||||
):
|
||||
raise ValueError(f"Sensor has illegal state {self.my_climate.regulated_target_temp}")
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
self.my_climate.regulated_target_temp, self.suggested_display_precision
|
||||
)
|
||||
if old_state != self._attr_native_value:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
return "mdi:thermometer-auto"
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
return SensorDeviceClass.TEMPERATURE
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
@property
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 1
|
||||
|
||||
@@ -137,4 +137,25 @@ set_window_bypass:
|
||||
advanced: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
boolean:
|
||||
|
||||
set_auto_regulation_mode:
|
||||
name: Set Auto Regulation mode
|
||||
description: Change the mode of self-regulation (only for VTherm over climate)
|
||||
target:
|
||||
entity:
|
||||
integration: versatile_thermostat
|
||||
fields:
|
||||
auto_regulation_mode:
|
||||
name: Auto regulation mode
|
||||
description: Possible values
|
||||
required: true
|
||||
advanced: false
|
||||
default: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "None"
|
||||
- "Light"
|
||||
- "Medium"
|
||||
- "Strong"
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
"valve_entity_id": "1rst valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number"
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
@@ -54,7 +57,10 @@
|
||||
"valve_entity_id": "1rst valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id"
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -199,7 +205,10 @@
|
||||
"valve_entity_id": "1rst valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number"
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
@@ -215,7 +224,10 @@
|
||||
"valve_entity_id": "1rst valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id"
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -329,6 +341,14 @@
|
||||
"thermostat_over_climate": "Thermostat over a climate",
|
||||
"thermostat_over_valve": "Thermostat over a valve"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||
|
||||
from homeassistant.components.climate import HVACAction, HVACMode
|
||||
|
||||
from .commons import NowClass, round_to_nearest
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .pi_algorithm import PITemperatureRegulator
|
||||
|
||||
from .const import CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4, overrides
|
||||
from .const import (
|
||||
overrides,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
CONF_CLIMATE_4,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
RegulationParamLight,
|
||||
RegulationParamMedium,
|
||||
RegulationParamStrong
|
||||
)
|
||||
|
||||
from .underlyings import UnderlyingClimate
|
||||
|
||||
@@ -18,17 +36,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class ThermostatOverClimate(BaseThermostat):
|
||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||
_auto_regulation_mode:str = None
|
||||
_regulation_algo = None
|
||||
_regulated_target_temp: float = None
|
||||
_auto_regulation_dtemp: float = None
|
||||
_auto_regulation_period_min: int = None
|
||||
_last_regulation_change: datetime = None
|
||||
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"is_over_climate", "start_hvac_action_date", "underlying_climate_0", "underlying_climate_1",
|
||||
"underlying_climate_2", "underlying_climate_3"
|
||||
"underlying_climate_2", "underlying_climate_3", "regulation_accumulated_error"
|
||||
}))
|
||||
|
||||
# Useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, entry_infos)
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the thermostat over switch."""
|
||||
# super.__init__ calls post_init at the end. So it must be called after regulation initialization
|
||||
super().__init__(hass, unique_id, name, entry_infos)
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
self._last_regulation_change = NowClass.get_now(hass)
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool:
|
||||
@@ -55,188 +81,42 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
return HVACAction.IDLE
|
||||
return HVACAction.OFF
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""List of available operation modes."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).hvac_modes
|
||||
else:
|
||||
return super.hvac_modes
|
||||
|
||||
@property
|
||||
def mean_cycle_power(self) -> float | None:
|
||||
"""Returns the mean power consumption during the cycle"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the fan setting.
|
||||
|
||||
Requires ClimateEntityFeature.FAN_MODE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).fan_mode
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str] | None:
|
||||
"""Return the list of available fan modes.
|
||||
|
||||
Requires ClimateEntityFeature.FAN_MODE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).fan_modes
|
||||
|
||||
return []
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str | None:
|
||||
"""Return the swing setting.
|
||||
|
||||
Requires ClimateEntityFeature.SWING_MODE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).swing_mode
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def swing_modes(self) -> list[str] | None:
|
||||
"""Return the list of available swing modes.
|
||||
|
||||
Requires ClimateEntityFeature.SWING_MODE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).swing_modes
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).temperature_unit
|
||||
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).supported_features | self._support_flags
|
||||
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> float | None:
|
||||
"""Return the supported step of target temperature."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).target_temperature_step
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the highbound target temperature we try to reach.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).target_temperature_high
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the lowbound target temperature we try to reach.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).target_temperature_low
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool | None:
|
||||
"""Return true if aux heater.
|
||||
|
||||
Requires ClimateEntityFeature.AUX_HEAT.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).is_aux_heat
|
||||
|
||||
return None
|
||||
|
||||
@overrides
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).turn_aux_heat_on()
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@overrides
|
||||
async def async_turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
for under in self._underlyings:
|
||||
await under.async_turn_aux_heat_on()
|
||||
|
||||
@overrides
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
for under in self._underlyings:
|
||||
return under.turn_aux_heat_off()
|
||||
|
||||
@overrides
|
||||
async def async_turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
for under in self._underlyings:
|
||||
await under.async_turn_aux_heat_off()
|
||||
|
||||
@overrides
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||
if fan_mode is None:
|
||||
return
|
||||
|
||||
for under in self._underlyings:
|
||||
await under.set_fan_mode(fan_mode)
|
||||
self._fan_mode = fan_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
async def async_set_humidity(self, humidity: int):
|
||||
"""Set new target humidity."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||
if humidity is None:
|
||||
return
|
||||
for under in self._underlyings:
|
||||
await under.set_humidity(humidity)
|
||||
self._humidity = humidity
|
||||
self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||
if swing_mode is None:
|
||||
return
|
||||
for under in self._underlyings:
|
||||
await under.set_swing_mode(swing_mode)
|
||||
self._swing_mode = swing_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||
await super()._async_internal_set_temperature(temperature)
|
||||
|
||||
self._regulation_algo.set_target_temp(self.target_temperature)
|
||||
await self._send_regulated_temperature(force=True)
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
""" Sends the regulated temperature to all underlying """
|
||||
if not self._regulated_target_temp:
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
|
||||
new_regulated_temp = round_to_nearest(
|
||||
self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp),
|
||||
self._auto_regulation_dtemp)
|
||||
dtemp = new_regulated_temp - self._regulated_target_temp
|
||||
|
||||
if not force and abs(dtemp) < self._auto_regulation_dtemp:
|
||||
_LOGGER.debug("%s - dtemp (%.1f) is < %.1f -> forget the regulation send", self, dtemp, self._auto_regulation_dtemp)
|
||||
return
|
||||
|
||||
now:datetime = NowClass.get_now(self._hass)
|
||||
period = float((now - self._last_regulation_change).total_seconds()) / 60.
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.debug("%s - period (%.1f) is < %.0f -> forget the regulation send", self, period, self._auto_regulation_period_min)
|
||||
return
|
||||
|
||||
|
||||
self._regulated_target_temp = new_regulated_temp
|
||||
_LOGGER.info("%s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp)
|
||||
self._last_regulation_change = now
|
||||
|
||||
for under in self._underlyings:
|
||||
await under.set_temperature(
|
||||
temperature, self._attr_max_temp, self._attr_min_temp
|
||||
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
|
||||
)
|
||||
|
||||
@overrides
|
||||
@@ -259,6 +139,48 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
)
|
||||
|
||||
self.choose_auto_regulation_mode(
|
||||
entry_infos.get(CONF_AUTO_REGULATION_MODE) if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None else CONF_AUTO_REGULATION_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_dtemp = entry_infos.get(CONF_AUTO_REGULATION_DTEMP) if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None else 0.5
|
||||
self._auto_regulation_period_min = entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None else 5
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode):
|
||||
""" Choose or change the regulation mode"""
|
||||
self._auto_regulation_mode = auto_regulation_mode
|
||||
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
RegulationParamLight.kp,
|
||||
RegulationParamLight.ki,
|
||||
RegulationParamLight.k_ext,
|
||||
RegulationParamLight.offset_max,
|
||||
RegulationParamLight.stabilization_threshold,
|
||||
RegulationParamLight.accumulated_error_threshold)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
RegulationParamMedium.kp,
|
||||
RegulationParamMedium.ki,
|
||||
RegulationParamMedium.k_ext,
|
||||
RegulationParamMedium.offset_max,
|
||||
RegulationParamMedium.stabilization_threshold,
|
||||
RegulationParamMedium.accumulated_error_threshold)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
RegulationParamStrong.kp,
|
||||
RegulationParamStrong.ki,
|
||||
RegulationParamStrong.k_ext,
|
||||
RegulationParamStrong.offset_max,
|
||||
RegulationParamStrong.stabilization_threshold,
|
||||
RegulationParamStrong.accumulated_error_threshold)
|
||||
else:
|
||||
# A default empty algo (which does nothing)
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature, 0, 0, 0, 0, 0.1, 0)
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
@@ -304,6 +226,10 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
if self.is_regulated:
|
||||
self._attr_extra_state_attributes["regulated_target_temp"] = self._regulated_target_temp
|
||||
self._attr_extra_state_attributes["regulation_accumulated_error"] = self._regulation_algo.accumulated_error
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
"%s - Calling update_custom_attributes: %s",
|
||||
@@ -509,7 +435,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
new_state.attributes,
|
||||
)
|
||||
if (
|
||||
self.is_over_climate
|
||||
# we do not change target temperature on regulated VTherm
|
||||
not self.is_regulated
|
||||
and new_state.attributes
|
||||
and (new_target_temp := new_state.attributes.get("temperature"))
|
||||
and new_target_temp != self.target_temperature
|
||||
@@ -523,3 +450,222 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
changes = True
|
||||
|
||||
await end_climate_changed(changes)
|
||||
|
||||
@overrides
|
||||
async def async_control_heating(self, force=False, _=None):
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
ret = await super().async_control_heating(force, _)
|
||||
|
||||
await self._send_regulated_temperature()
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self):
|
||||
""" Get the regulation mode """
|
||||
return self._auto_regulation_mode
|
||||
|
||||
@property
|
||||
def regulated_target_temp(self):
|
||||
""" Get the regulated target temperature """
|
||||
return self._regulated_target_temp
|
||||
|
||||
@property
|
||||
def is_regulated(self):
|
||||
""" Check if the ThermostatOverClimate is regulated """
|
||||
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""List of available operation modes."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).hvac_modes
|
||||
else:
|
||||
return super.hvac_modes
|
||||
|
||||
@property
|
||||
def mean_cycle_power(self) -> float | None:
|
||||
"""Returns the mean power consumption during the cycle"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the fan setting.
|
||||
|
||||
Requires ClimateEntityFeature.FAN_MODE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).fan_mode
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str] | None:
|
||||
"""Return the list of available fan modes.
|
||||
|
||||
Requires ClimateEntityFeature.FAN_MODE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).fan_modes
|
||||
|
||||
return []
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str | None:
|
||||
"""Return the swing setting.
|
||||
|
||||
Requires ClimateEntityFeature.SWING_MODE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).swing_mode
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def swing_modes(self) -> list[str] | None:
|
||||
"""Return the list of available swing modes.
|
||||
|
||||
Requires ClimateEntityFeature.SWING_MODE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).swing_modes
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).temperature_unit
|
||||
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).supported_features | self._support_flags
|
||||
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> float | None:
|
||||
"""Return the supported step of target temperature."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).target_temperature_step
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the highbound target temperature we try to reach.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).target_temperature_high
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the lowbound target temperature we try to reach.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).target_temperature_low
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool | None:
|
||||
"""Return true if aux heater.
|
||||
|
||||
Requires ClimateEntityFeature.AUX_HEAT.
|
||||
"""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).is_aux_heat
|
||||
|
||||
return None
|
||||
|
||||
@overrides
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).turn_aux_heat_on()
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
@overrides
|
||||
async def async_turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
for under in self._underlyings:
|
||||
await under.async_turn_aux_heat_on()
|
||||
|
||||
@overrides
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
for under in self._underlyings:
|
||||
return under.turn_aux_heat_off()
|
||||
|
||||
@overrides
|
||||
async def async_turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
for under in self._underlyings:
|
||||
await under.async_turn_aux_heat_off()
|
||||
|
||||
@overrides
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||
if fan_mode is None:
|
||||
return
|
||||
|
||||
for under in self._underlyings:
|
||||
await under.set_fan_mode(fan_mode)
|
||||
self._fan_mode = fan_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
async def async_set_humidity(self, humidity: int):
|
||||
"""Set new target humidity."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||
if humidity is None:
|
||||
return
|
||||
for under in self._underlyings:
|
||||
await under.set_humidity(humidity)
|
||||
self._humidity = humidity
|
||||
self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||
if swing_mode is None:
|
||||
return
|
||||
for under in self._underlyings:
|
||||
await under.set_swing_mode(swing_mode)
|
||||
self._swing_mode = swing_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def service_set_auto_regulation_mode(self, auto_regulation_mode):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_auto_regulation_mode
|
||||
data:
|
||||
auto_regulation_mode: [None | Light | Medium | Strong]
|
||||
target:
|
||||
entity_id: climate.thermostat_1
|
||||
"""
|
||||
_LOGGER.info("%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s", self, auto_regulation_mode)
|
||||
if auto_regulation_mode == "None":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_NONE)
|
||||
elif auto_regulation_mode == "Light":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_LIGHT)
|
||||
elif auto_regulation_mode == "Medium":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_MEDIUM)
|
||||
elif auto_regulation_mode == "Strong":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_STRONG)
|
||||
|
||||
await self._send_regulated_temperature()
|
||||
self.update_custom_attributes()
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
"valve_entity_id": "1rst valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number"
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
@@ -54,7 +57,10 @@
|
||||
"valve_entity_id": "1rst valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id"
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -199,7 +205,10 @@
|
||||
"valve_entity_id": "1rst valve number",
|
||||
"valve_entity2_id": "2nd valve number",
|
||||
"valve_entity3_id": "3rd valve number",
|
||||
"valve_entity4_id": "4th valve number"
|
||||
"valve_entity4_id": "4th valve number",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimal period"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Mandatory heater entity id",
|
||||
@@ -215,7 +224,10 @@
|
||||
"valve_entity_id": "1rst valve number entity id",
|
||||
"valve_entity2_id": "2nd valve number entity id",
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id"
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -329,6 +341,14 @@
|
||||
"thermostat_over_climate": "Thermostat over a climate",
|
||||
"thermostat_over_valve": "Thermostat over a valve"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_strong": "Strong",
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
"valve_entity_id": "1ère valve number",
|
||||
"valve_entity2_id": "2ème valve number",
|
||||
"valve_entity3_id": "3ème valve number",
|
||||
"valve_entity4_id": "4ème valve number"
|
||||
"valve_entity4_id": "4ème valve number",
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
@@ -54,7 +57,10 @@
|
||||
"valve_entity_id": "Entity id de la 1ère valve",
|
||||
"valve_entity2_id": "Entity id de la 2ème valve",
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve"
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la température cible",
|
||||
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -197,10 +203,13 @@
|
||||
"climate_entity3_id": "3ème thermostat sous-jacent",
|
||||
"climate_entity4_id": "4ème thermostat sous-jacent",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "1ère valve number",
|
||||
"valve_entity2_id": "2ème valve number",
|
||||
"valve_entity3_id": "3ème valve number",
|
||||
"valve_entity4_id": "4ème valve number"
|
||||
"valve_entity_id": "1ère valve",
|
||||
"valve_entity2_id": "2ème valve",
|
||||
"valve_entity3_id": "3ème valve",
|
||||
"valve_entity4_id": "4ème valve",
|
||||
"auto_regulation_mode": "Auto-regulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||
@@ -216,7 +225,10 @@
|
||||
"valve_entity_id": "Entity id de la 1ère valve",
|
||||
"valve_entity2_id": "Entity id de la 2ème valve",
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve"
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la consigne",
|
||||
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -330,6 +342,14 @@
|
||||
"thermostat_over_climate": "Thermostat sur un autre thermostat",
|
||||
"thermostat_over_valve": "Thermostat sur une valve"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Moyenne",
|
||||
"auto_regulation_light": "Légère",
|
||||
"auto_regulation_none": "Aucune"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"valve_entity_id": "Primo valvola numero",
|
||||
"valve_entity2_id": "Secondo valvola numero",
|
||||
"valve_entity3_id": "Terzo valvola numero",
|
||||
"valve_entity4_id": "Quarto valvola numero"
|
||||
"valve_entity4_id": "Quarto valvola numero",
|
||||
"auto_regulation_mode": "Autoregolamentazione"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
|
||||
@@ -54,7 +55,8 @@
|
||||
"valve_entity_id": "Entity id del primo valvola numero",
|
||||
"valve_entity2_id": "Entity id del secondo valvola numero",
|
||||
"valve_entity3_id": "Entity id del terzo valvola numero",
|
||||
"valve_entity4_id": "Entity id del quarto valvola numero"
|
||||
"valve_entity4_id": "Entity id del quarto valvola numero",
|
||||
"auto_regulation_mode": "Regolazione automatica della temperatura target"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -192,7 +194,8 @@
|
||||
"valve_entity_id": "Primo valvola numero",
|
||||
"valve_entity2_id": "Secondo valvola numero",
|
||||
"valve_entity3_id": "Terzo valvola numero",
|
||||
"valve_entity4_id": "Quarto valvola numero"
|
||||
"valve_entity4_id": "Quarto valvola numero",
|
||||
"auto_regulation_mode": "Autoregolamentazione"
|
||||
},
|
||||
"data_description": {
|
||||
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
|
||||
@@ -208,7 +211,8 @@
|
||||
"valve_entity_id": "Entity id del primo valvola numero",
|
||||
"valve_entity2_id": "Entity id del secondo valvola numero",
|
||||
"valve_entity3_id": "Entity id del terzo valvola numero",
|
||||
"valve_entity4_id": "Entity id del quarto valvola numero"
|
||||
"valve_entity4_id": "Entity id del quarto valvola numero",
|
||||
"auto_regulation_mode": "Autoregolamentazione"
|
||||
}
|
||||
},
|
||||
"tpi": {
|
||||
@@ -315,6 +319,14 @@
|
||||
"thermostat_over_climate": "Termostato sopra un altro termostato",
|
||||
"thermostat_over_valve": "Thermostato su una valvola"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Media",
|
||||
"auto_regulation_light": "Leggera",
|
||||
"auto_regulation_none": "Nessuna autoregolamentazione"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
|
||||
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
@@ -25,6 +24,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from custom_components.versatile_thermostat.commons import get_tz, NowClass # pylint: disable=unused-import
|
||||
|
||||
from .const import ( # pylint: disable=unused-import
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||
@@ -34,6 +34,8 @@ from .const import ( # pylint: disable=unused-import
|
||||
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
|
||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||
MOCK_PRESETS_CONFIG,
|
||||
MOCK_PRESETS_AC_CONFIG,
|
||||
@@ -83,6 +85,20 @@ PARTIAL_CLIMATE_CONFIG = (
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
PARTIAL_CLIMATE_AC_CONFIG = (
|
||||
MOCK_TH_OVER_CLIMATE_USER_CONFIG
|
||||
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
)
|
||||
|
||||
FULL_4SWITCH_CONFIG = (
|
||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG
|
||||
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
|
||||
@@ -101,7 +117,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class MockClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF) -> None: # pylint: disable=unused-argument
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF, hvac_action:HVACAction = HVACAction.OFF) -> None: # pylint: disable=unused-argument
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
@@ -118,17 +134,25 @@ class MockClimate(ClimateEntity):
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
self._attr_target_temperature = 20
|
||||
self._attr_current_temperature = 15
|
||||
self._attr_hvac_action = hvac_action
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
""" Set the target temperature"""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self._attr_target_temperature = temperature
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
""" The hvac mode"""
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
""" The hvac action of the mock climate"""
|
||||
return self._attr_hvac_action
|
||||
|
||||
def set_hvac_action(self, hvac_action: HVACAction):
|
||||
""" Set the HVACaction """
|
||||
self._attr_hvac_action = hvac_action
|
||||
|
||||
class MockUnavailableClimate(ClimateEntity):
|
||||
"""A Mock Climate class used for Underlying climate mode"""
|
||||
@@ -454,13 +478,6 @@ async def send_presence_change_event(
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
async def send_climate_change_event(
|
||||
entity: BaseThermostat,
|
||||
new_hvac_mode: HVACMode,
|
||||
|
||||
@@ -50,6 +50,11 @@ from custom_components.versatile_thermostat.const import (
|
||||
CONF_PRESENCE_SENSOR,
|
||||
PRESET_AWAY_SUFFIX,
|
||||
CONF_CLIMATE,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN
|
||||
)
|
||||
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
@@ -89,14 +94,14 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_DEVICE_POWER: 1
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AC_MODE: False
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
|
||||
@@ -122,6 +127,23 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 2
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_NONE
|
||||
}
|
||||
|
||||
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_AC_MODE: True,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 1
|
||||
}
|
||||
|
||||
MOCK_PRESETS_CONFIG = {
|
||||
|
||||
334
tests/test_auto_regulation.py
Normal file
334
tests/test_auto_regulation.py
Normal file
@@ -0,0 +1,334 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the normal start of a Thermostat """
|
||||
from unittest.mock import patch #, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACAction, HVACMode
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
"""Test the regulation of an over climate thermostat"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# This is include a medium regulation
|
||||
data=PARTIAL_CLIMATE_CONFIG,
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
# Creates the regulated VTherm over climate
|
||||
# change temperature so that the heating will start
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# Activate the heating by changing HVACMode and temperature
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.hvac_action == HVACAction.OFF
|
||||
|
||||
assert entity.regulated_target_temp == entity.min_temp
|
||||
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# set manual target temp (at now - 7) -> the regulation should occurs
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
# In medium we could go up to +3 degre
|
||||
# normally the calcul gives 18 + 2.2 but we round the result to the nearest 0.5 which is 2.0
|
||||
assert entity.regulated_target_temp == 18+2.0
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await send_temperature_change_event(entity, 22, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 18-0.5 # normally 0.6 but round_to_nearest gives 0.5
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
"""Test the regulation of an over climate thermostat"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# This is include a medium regulation
|
||||
data=PARTIAL_CLIMATE_AC_CONFIG,
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
# Creates the regulated VTherm over climate
|
||||
# change temperature so that the heating will start
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
assert entity.target_temperature == entity.max_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# Activate the heating by changing HVACMode and temperature
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.hvac_action == HVACAction.OFF
|
||||
|
||||
# change temperature so that the heating will start
|
||||
await send_temperature_change_event(entity, 30, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
||||
|
||||
|
||||
# set manual target temp
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await entity.async_set_temperature(temperature=25)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.COOLING) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.COOLING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be lower
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25-3 # In medium we could go up to -3 degre
|
||||
assert entity.hvac_action == HVACAction.COOLING
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await send_temperature_change_event(entity, 26, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25-2.5 # +2.3 without round_to_nearest
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await send_temperature_change_event(entity, 20, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 25, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25+0.5 # +0.4 without round_to_nearest
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
"""Test the limitations of the regulation of an over climate thermostat:
|
||||
1. test the period_min parameter: do not send regulation event too frequently
|
||||
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
|
||||
"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# This is include a medium regulation, dtemp=0.5, period_min=2
|
||||
data=PARTIAL_CLIMATE_CONFIG,
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
# Creates the regulated VTherm over climate at t-20
|
||||
# change temperature so that the heating will start
|
||||
event_timestamp = now - timedelta(minutes=20)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def find_my_entity(entity_id) -> ClimateEntity:
|
||||
"""Find my new entity"""
|
||||
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
for entity in component.entities:
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
|
||||
# Activate the heating by changing HVACMode and temperature
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
|
||||
# it is cold today
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# set manual target temp (at now - 19) -> the regulation should be ignored because too early
|
||||
event_timestamp = now - timedelta(minutes=19)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# the regulated temperature will change because when we set temp manually it is forced
|
||||
assert entity.regulated_target_temp == 20.
|
||||
|
||||
# set manual target temp (at now - 18) -> the regulation should be taken into account
|
||||
event_timestamp = now - timedelta(minutes=18)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await entity.async_set_temperature(temperature=17)
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 18+0.5 # In medium we could go up to +3 degre. 0.7 without round_to_nearest
|
||||
old_regulated_temp = entity.regulated_target_temp
|
||||
|
||||
# change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=15)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await send_temperature_change_event(entity, 16, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp == old_regulated_temp
|
||||
|
||||
# change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=12)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
):
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 12, event_timestamp)
|
||||
|
||||
# the regulated should have been done
|
||||
assert entity.regulated_target_temp != old_regulated_temp
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 17 + 0.5 # 0.7 without round_to_nearest
|
||||
@@ -463,11 +463,11 @@ async def test_bug_101(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Underlying is in HEAT mode but should be shutdown at startup
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -495,7 +495,7 @@ async def test_bug_101(
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# because the underlying is heating. In real life the underlying should be shut-off
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
# Underlying should have been shutdown
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
@@ -539,6 +539,7 @@ async def test_bug_101(
|
||||
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, event_timestamp, 12.75)
|
||||
assert entity.target_temperature == 12.75
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
171
tests/test_pi.py
Normal file
171
tests/test_pi.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" Tests de PI algorithm used for auto-regulation """
|
||||
|
||||
from custom_components.versatile_thermostat.pi_algorithm import PITemperatureRegulator
|
||||
|
||||
def test_pi_algorithm_basics():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.2, ki=0.05, k_ext=0.1, offset_max=2, stabilization_threshold=0.1, accumulated_error_threshold=20)
|
||||
|
||||
assert the_algo
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
# Test the accumulator threshold effect and offset_max
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22
|
||||
assert the_algo.calculate_regulated_temperature(10, 10) == 22
|
||||
# Will keep infinitly 22.0
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
|
||||
|
||||
|
||||
def test_pi_algorithm_light():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.2, ki=0.05, k_ext=0.1, offset_max=2, stabilization_threshold=0.1, accumulated_error_threshold=20)
|
||||
|
||||
assert the_algo
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 21.7 # +1.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 # +0.7
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
|
||||
assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
|
||||
|
||||
def test_pi_algorithm_medium():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.5, ki=0.1, k_ext=0.1, offset_max=3, stabilization_threshold=0.1, accumulated_error_threshold=30)
|
||||
|
||||
assert the_algo
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 22.2
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 21.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.3
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 20.8
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 21.4
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 21.6
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 21.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 22.0
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 22.2
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 22.6
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 22.8
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
# Test the error acculation effect
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.1
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.2
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.3
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.4
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.5
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.6
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.7
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.8
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 22.9
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 5) == 23
|
||||
|
||||
def test_pi_algorithm_strong():
|
||||
""" Test the PI algorithm """
|
||||
|
||||
the_algo = PITemperatureRegulator(target_temp=20, kp=0.6, ki=0.2, k_ext=0.2, offset_max=4, stabilization_threshold=0.1, accumulated_error_threshold=40)
|
||||
|
||||
assert the_algo
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
|
||||
assert the_algo.calculate_regulated_temperature(18, 10) == 23.6
|
||||
assert the_algo.calculate_regulated_temperature(18.1, 10) == 23.9
|
||||
assert the_algo.calculate_regulated_temperature(18.3, 10) == 24.0
|
||||
assert the_algo.calculate_regulated_temperature(18.5, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(18.7, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, 10) == 23.9
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21.2
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 21
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.8
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.6
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20.2
|
||||
assert the_algo.calculate_regulated_temperature(21, 10) == 20
|
||||
|
||||
# Test temperature external
|
||||
assert the_algo.calculate_regulated_temperature(20, 8) == 21.0
|
||||
assert the_algo.calculate_regulated_temperature(20, 6) == 22.8
|
||||
assert the_algo.calculate_regulated_temperature(20, 4) == 23.2
|
||||
assert the_algo.calculate_regulated_temperature(20, 2) == 23.6
|
||||
assert the_algo.calculate_regulated_temperature(20, 0) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -2) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -4) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -6) == 24
|
||||
assert the_algo.calculate_regulated_temperature(20, -8) == 24
|
||||
|
||||
# to reset the accumulated erro
|
||||
the_algo.set_target_temp(20)
|
||||
# Test the error acculation effect
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 22.8
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23.2
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23.4
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23.6
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 23.8
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
assert the_algo.calculate_regulated_temperature(19, 10) == 24
|
||||
@@ -1,11 +1,15 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the Security featrure """
|
||||
from unittest.mock import patch, call
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
import logging
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
|
||||
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -55,7 +59,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
|
||||
# 1. creates a thermostat and check that security is off
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
entity: ThermostatOverSwitch = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
@@ -211,7 +215,7 @@ async def test_security_over_climate(
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
@@ -230,7 +234,7 @@ async def test_security_over_climate(
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity = find_my_entity("climate.theoverclimatemockname")
|
||||
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
@@ -295,11 +299,11 @@ async def test_security_over_climate(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on:
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False because a climate is never in security mode
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == 'none'
|
||||
assert entity._saved_preset_mode == 'none'
|
||||
assert entity._saved_preset_mode == 'none'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
""" Test the TPI algorithm """
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
|
||||
@@ -37,7 +38,7 @@ async def test_tpi_calculation(
|
||||
},
|
||||
)
|
||||
|
||||
entity: VersatileThermostat = await create_thermostat(
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
Reference in New Issue
Block a user