Compare commits
6 Commits
3.8.0.alph
...
3.8.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
663a13809c | ||
|
|
3c497d24fb | ||
|
|
fe85ead916 | ||
|
|
7d5ced55d3 | ||
|
|
9d099e3169 | ||
|
|
49377de248 |
@@ -1219,7 +1219,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
await self.async_control_heating(force=True)
|
await self.async_control_heating(force=True)
|
||||||
|
|
||||||
async def _async_internal_set_temperature(self, temperature):
|
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
|
self._target_temp = temperature
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2247,7 +2249,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
|||||||
await self.async_control_heating()
|
await self.async_control_heating()
|
||||||
self.update_custom_attributes()
|
self.update_custom_attributes()
|
||||||
|
|
||||||
#PR - Adding Window ByPass
|
|
||||||
async def service_set_window_bypass_state(self, window_bypass):
|
async def service_set_window_bypass_state(self, window_bypass):
|
||||||
"""Called by a service call:
|
"""Called by a service call:
|
||||||
service: versatile_thermostat.set_window_bypass
|
service: versatile_thermostat.set_window_bypass
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ from .const import (
|
|||||||
SERVICE_SET_PRESENCE,
|
SERVICE_SET_PRESENCE,
|
||||||
SERVICE_SET_PRESET_TEMPERATURE,
|
SERVICE_SET_PRESET_TEMPERATURE,
|
||||||
SERVICE_SET_SECURITY,
|
SERVICE_SET_SECURITY,
|
||||||
#PR - Adding Window ByPass
|
|
||||||
SERVICE_SET_WINDOW_BYPASS,
|
SERVICE_SET_WINDOW_BYPASS,
|
||||||
|
SERVICE_SET_AUTO_REGULATION_MODE,
|
||||||
CONF_THERMOSTAT_TYPE,
|
CONF_THERMOSTAT_TYPE,
|
||||||
CONF_THERMOSTAT_SWITCH,
|
CONF_THERMOSTAT_SWITCH,
|
||||||
CONF_THERMOSTAT_CLIMATE,
|
CONF_THERMOSTAT_CLIMATE,
|
||||||
CONF_THERMOSTAT_VALVE,
|
CONF_THERMOSTAT_VALVE
|
||||||
)
|
)
|
||||||
|
|
||||||
from .thermostat_switch import ThermostatOverSwitch
|
from .thermostat_switch import ThermostatOverSwitch
|
||||||
@@ -99,7 +99,6 @@ async def async_setup_entry(
|
|||||||
"service_set_security",
|
"service_set_security",
|
||||||
)
|
)
|
||||||
|
|
||||||
#PR - Adding Window ByPass
|
|
||||||
platform.async_register_entity_service(
|
platform.async_register_entity_service(
|
||||||
SERVICE_SET_WINDOW_BYPASS,
|
SERVICE_SET_WINDOW_BYPASS,
|
||||||
{
|
{
|
||||||
@@ -108,3 +107,11 @@ async def async_setup_entry(
|
|||||||
},
|
},
|
||||||
"service_set_window_bypass_state",
|
"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 """
|
""" Some usefull commons class """
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta, datetime
|
||||||
from homeassistant.core import HomeAssistant, callback, Event
|
from homeassistant.core import HomeAssistant, callback, Event
|
||||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||||
from homeassistant.helpers.event import async_track_state_change_event, async_call_later
|
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 .base_thermostat import BaseThermostat
|
||||||
from .const import DOMAIN, DEVICE_MANUFACTURER
|
from .const import DOMAIN, DEVICE_MANUFACTURER
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
class VersatileThermostatBaseEntity(Entity):
|
||||||
"""A base class for all entities"""
|
"""A base class for all entities"""
|
||||||
@@ -98,7 +130,7 @@ class VersatileThermostatBaseEntity(Entity):
|
|||||||
await try_find_climate(None)
|
await try_find_climate(None)
|
||||||
|
|
||||||
@callback
|
@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
|
"""Called when my climate have change
|
||||||
This method aims to be overriden to take the status change
|
This method aims to be overriden to take the status change
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ from .const import (
|
|||||||
CONF_AUTO_REGULATION_MODES,
|
CONF_AUTO_REGULATION_MODES,
|
||||||
CONF_AUTO_REGULATION_MODE,
|
CONF_AUTO_REGULATION_MODE,
|
||||||
CONF_AUTO_REGULATION_NONE,
|
CONF_AUTO_REGULATION_NONE,
|
||||||
|
CONF_AUTO_REGULATION_DTEMP,
|
||||||
|
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||||
UnknownEntity,
|
UnknownEntity,
|
||||||
WindowOpenDetectionMethod,
|
WindowOpenDetectionMethod,
|
||||||
)
|
)
|
||||||
@@ -264,6 +266,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
options=CONF_AUTO_REGULATION_MODES, translation_key="auto_regulation_mode"
|
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
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ CONF_AUTO_REGULATION_NONE= "auto_regulation_none"
|
|||||||
CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light"
|
CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light"
|
||||||
CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
|
CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
|
||||||
CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong"
|
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 = {
|
CONF_PRESETS = {
|
||||||
p: f"{p}_temp"
|
p: f"{p}_temp"
|
||||||
@@ -189,7 +191,9 @@ ALL_CONF = (
|
|||||||
CONF_VALVE_2,
|
CONF_VALVE_2,
|
||||||
CONF_VALVE_3,
|
CONF_VALVE_3,
|
||||||
CONF_VALVE_4,
|
CONF_VALVE_4,
|
||||||
CONF_AUTO_REGULATION_MODE
|
CONF_AUTO_REGULATION_MODE,
|
||||||
|
CONF_AUTO_REGULATION_DTEMP,
|
||||||
|
CONF_AUTO_REGULATION_PERIOD_MIN
|
||||||
]
|
]
|
||||||
+ CONF_PRESETS_VALUES
|
+ CONF_PRESETS_VALUES
|
||||||
+ CONF_PRESETS_AWAY_VALUES
|
+ CONF_PRESETS_AWAY_VALUES
|
||||||
@@ -210,8 +214,8 @@ SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
|
|||||||
SERVICE_SET_PRESENCE = "set_presence"
|
SERVICE_SET_PRESENCE = "set_presence"
|
||||||
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
|
||||||
SERVICE_SET_SECURITY = "set_security"
|
SERVICE_SET_SECURITY = "set_security"
|
||||||
#PR - Adding Window ByPass
|
|
||||||
SERVICE_SET_WINDOW_BYPASS = "set_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_MIN_ON_PERCENT = 0.5
|
||||||
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
||||||
@@ -223,29 +227,40 @@ class RegulationParamLight:
|
|||||||
""" Light parameters for regulation"""
|
""" Light parameters for regulation"""
|
||||||
kp:float = 0.2
|
kp:float = 0.2
|
||||||
ki:float = 0.05
|
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
|
k_ext:float = 0.1
|
||||||
offset_max:float = 2
|
offset_max:float = 2
|
||||||
stabilization_threshold:float = 0.1
|
stabilization_threshold:float = 0.1
|
||||||
accumulated_error_threshold:float = 20
|
accumulated_error_threshold:float = 20
|
||||||
|
|
||||||
|
|
||||||
class RegulationParamMedium:
|
class RegulationParamStrong:
|
||||||
""" Medium parameters for regulation"""
|
""" Medium parameters for regulation"""
|
||||||
kp:float = 0.5
|
kp:float = 0.4
|
||||||
ki:float = 0.1
|
ki:float = 0.08
|
||||||
k_ext:float = 0.1
|
k_ext:float = 0.1
|
||||||
offset_max:float = 3
|
offset_max:float = 3
|
||||||
stabilization_threshold:float = 0.1
|
stabilization_threshold:float = 0.1
|
||||||
accumulated_error_threshold:float = 30
|
accumulated_error_threshold:float = 25
|
||||||
|
|
||||||
class RegulationParamStrong:
|
# Not used now
|
||||||
|
class RegulationParamVeryStrong:
|
||||||
""" Strong parameters for regulation"""
|
""" Strong parameters for regulation"""
|
||||||
kp:float = 0.6
|
kp:float = 0.6
|
||||||
ki:float = 0.2
|
ki:float = 0.1
|
||||||
k_ext:float = 0.2
|
k_ext:float = 0.2
|
||||||
offset_max:float = 4
|
offset_max:float = 4
|
||||||
stabilization_threshold:float = 0.1
|
stabilization_threshold:float = 0.1
|
||||||
accumulated_error_threshold:float = 40
|
accumulated_error_threshold:float = 30
|
||||||
|
|
||||||
class EventType(Enum):
|
class EventType(Enum):
|
||||||
"""The event type that can be sent"""
|
"""The event type that can be sent"""
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class PITemperatureRegulator:
|
|||||||
def set_target_temp(self, target_temp):
|
def set_target_temp(self, target_temp):
|
||||||
""" Set the new target_temp"""
|
""" Set the new target_temp"""
|
||||||
self.target_temp = target_temp
|
self.target_temp = target_temp
|
||||||
self.accumulated_error = 0
|
# Do not reset the accumulated error
|
||||||
|
# self.accumulated_error = 0
|
||||||
|
|
||||||
def calculate_regulated_temperature(self, internal_temp: float, external_temp:float): # pylint: disable=unused-argument
|
def calculate_regulated_temperature(self, internal_temp: float, external_temp:float): # pylint: disable=unused-argument
|
||||||
""" Calculate a new target_temp given some temperature"""
|
""" Calculate a new target_temp given some temperature"""
|
||||||
|
|||||||
@@ -138,3 +138,24 @@ set_window_bypass:
|
|||||||
default: true
|
default: true
|
||||||
selector:
|
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"
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"valve_entity2_id": "2nd valve number",
|
"valve_entity2_id": "2nd valve number",
|
||||||
"valve_entity3_id": "3rd 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_mode": "Self-regulation",
|
||||||
|
"auto_regulation_dtemp": "Regulation threshold",
|
||||||
|
"auto_regulation_periode_min": "Regulation minimal period"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"heater_entity_id": "Mandatory heater entity id",
|
"heater_entity_id": "Mandatory heater entity id",
|
||||||
@@ -56,7 +58,9 @@
|
|||||||
"valve_entity2_id": "2nd valve number entity id",
|
"valve_entity2_id": "2nd valve number entity id",
|
||||||
"valve_entity3_id": "3rd 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_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": {
|
"tpi": {
|
||||||
@@ -202,7 +206,9 @@
|
|||||||
"valve_entity2_id": "2nd valve number",
|
"valve_entity2_id": "2nd valve number",
|
||||||
"valve_entity3_id": "3rd 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_mode": "Self-regulation",
|
||||||
|
"auto_regulation_dtemp": "Regulation threshold",
|
||||||
|
"auto_regulation_periode_min": "Regulation minimal period"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"heater_entity_id": "Mandatory heater entity id",
|
"heater_entity_id": "Mandatory heater entity id",
|
||||||
@@ -219,7 +225,9 @@
|
|||||||
"valve_entity2_id": "2nd valve number entity id",
|
"valve_entity2_id": "2nd valve number entity id",
|
||||||
"valve_entity3_id": "3rd 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_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": {
|
"tpi": {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
""" A climate over switch classe """
|
""" A climate over switch classe """
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||||
|
|
||||||
from homeassistant.components.climate import HVACAction, HVACMode
|
from homeassistant.components.climate import HVACAction, HVACMode
|
||||||
|
|
||||||
|
from .commons import NowClass, round_to_nearest
|
||||||
from .base_thermostat import BaseThermostat
|
from .base_thermostat import BaseThermostat
|
||||||
from .pi_algorithm import PITemperatureRegulator
|
from .pi_algorithm import PITemperatureRegulator
|
||||||
|
|
||||||
@@ -22,6 +23,8 @@ from .const import (
|
|||||||
CONF_AUTO_REGULATION_LIGHT,
|
CONF_AUTO_REGULATION_LIGHT,
|
||||||
CONF_AUTO_REGULATION_MEDIUM,
|
CONF_AUTO_REGULATION_MEDIUM,
|
||||||
CONF_AUTO_REGULATION_STRONG,
|
CONF_AUTO_REGULATION_STRONG,
|
||||||
|
CONF_AUTO_REGULATION_DTEMP,
|
||||||
|
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||||
RegulationParamLight,
|
RegulationParamLight,
|
||||||
RegulationParamMedium,
|
RegulationParamMedium,
|
||||||
RegulationParamStrong
|
RegulationParamStrong
|
||||||
@@ -33,9 +36,12 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class ThermostatOverClimate(BaseThermostat):
|
class ThermostatOverClimate(BaseThermostat):
|
||||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||||
_regulation_mode:str = None
|
_auto_regulation_mode:str = None
|
||||||
_regulation_algo = None
|
_regulation_algo = None
|
||||||
_regulated_target_temp: float = 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(
|
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
||||||
{
|
{
|
||||||
@@ -48,6 +54,7 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
# super.__init__ calls post_init at the end. So it must be called after regulation initialization
|
# super.__init__ calls post_init at the end. So it must be called after regulation initialization
|
||||||
super().__init__(hass, unique_id, name, entry_infos)
|
super().__init__(hass, unique_id, name, entry_infos)
|
||||||
self._regulated_target_temp = self.target_temperature
|
self._regulated_target_temp = self.target_temperature
|
||||||
|
self._last_regulation_change = NowClass.get_now(hass)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_over_climate(self) -> bool:
|
def is_over_climate(self) -> bool:
|
||||||
@@ -80,21 +87,37 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
await super()._async_internal_set_temperature(temperature)
|
await super()._async_internal_set_temperature(temperature)
|
||||||
|
|
||||||
self._regulation_algo.set_target_temp(self.target_temperature)
|
self._regulation_algo.set_target_temp(self.target_temperature)
|
||||||
await self._send_regulated_temperature()
|
await self._send_regulated_temperature(force=True)
|
||||||
|
|
||||||
async def _send_regulated_temperature(self):
|
async def _send_regulated_temperature(self, force=False):
|
||||||
""" Sends the regulated temperature to all underlying """
|
""" Sends the regulated temperature to all underlying """
|
||||||
new_regulated_temp = self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp)
|
if not self._regulated_target_temp:
|
||||||
if new_regulated_temp != self._regulated_target_temp:
|
self._regulated_target_temp = self.target_temperature
|
||||||
_LOGGER.info("%s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp)
|
|
||||||
self._regulated_target_temp = new_regulated_temp
|
|
||||||
|
|
||||||
for under in self._underlyings:
|
new_regulated_temp = round_to_nearest(
|
||||||
await under.set_temperature(
|
self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp),
|
||||||
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
|
self._auto_regulation_dtemp)
|
||||||
)
|
dtemp = new_regulated_temp - self._regulated_target_temp
|
||||||
else:
|
|
||||||
_LOGGER.debug("%s - No change on regulated temperature (%.1f)", self, 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(
|
||||||
|
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
|
||||||
|
)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, entry_infos):
|
def post_init(self, entry_infos):
|
||||||
@@ -116,9 +139,17 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._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.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
|
||||||
|
)
|
||||||
|
|
||||||
if self._regulation_mode == CONF_AUTO_REGULATION_LIGHT:
|
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._regulation_algo = PITemperatureRegulator(
|
||||||
self.target_temperature,
|
self.target_temperature,
|
||||||
RegulationParamLight.kp,
|
RegulationParamLight.kp,
|
||||||
@@ -127,7 +158,7 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
RegulationParamLight.offset_max,
|
RegulationParamLight.offset_max,
|
||||||
RegulationParamLight.stabilization_threshold,
|
RegulationParamLight.stabilization_threshold,
|
||||||
RegulationParamLight.accumulated_error_threshold)
|
RegulationParamLight.accumulated_error_threshold)
|
||||||
elif self._regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
|
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
|
||||||
self._regulation_algo = PITemperatureRegulator(
|
self._regulation_algo = PITemperatureRegulator(
|
||||||
self.target_temperature,
|
self.target_temperature,
|
||||||
RegulationParamMedium.kp,
|
RegulationParamMedium.kp,
|
||||||
@@ -136,7 +167,7 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
RegulationParamMedium.offset_max,
|
RegulationParamMedium.offset_max,
|
||||||
RegulationParamMedium.stabilization_threshold,
|
RegulationParamMedium.stabilization_threshold,
|
||||||
RegulationParamMedium.accumulated_error_threshold)
|
RegulationParamMedium.accumulated_error_threshold)
|
||||||
elif self._regulation_mode == CONF_AUTO_REGULATION_STRONG:
|
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG:
|
||||||
self._regulation_algo = PITemperatureRegulator(
|
self._regulation_algo = PITemperatureRegulator(
|
||||||
self.target_temperature,
|
self.target_temperature,
|
||||||
RegulationParamStrong.kp,
|
RegulationParamStrong.kp,
|
||||||
@@ -430,9 +461,9 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def regulation_mode(self):
|
def auto_regulation_mode(self):
|
||||||
""" Get the regulation mode """
|
""" Get the regulation mode """
|
||||||
return self._regulation_mode
|
return self._auto_regulation_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def regulated_target_temp(self):
|
def regulated_target_temp(self):
|
||||||
@@ -442,7 +473,7 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
@property
|
@property
|
||||||
def is_regulated(self):
|
def is_regulated(self):
|
||||||
""" Check if the ThermostatOverClimate is regulated """
|
""" Check if the ThermostatOverClimate is regulated """
|
||||||
return self.regulation_mode != CONF_AUTO_REGULATION_NONE
|
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_modes(self):
|
def hvac_modes(self):
|
||||||
@@ -617,3 +648,24 @@ class ThermostatOverClimate(BaseThermostat):
|
|||||||
await under.set_swing_mode(swing_mode)
|
await under.set_swing_mode(swing_mode)
|
||||||
self._swing_mode = swing_mode
|
self._swing_mode = swing_mode
|
||||||
self.async_write_ha_state()
|
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()
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"valve_entity2_id": "2nd valve number",
|
"valve_entity2_id": "2nd valve number",
|
||||||
"valve_entity3_id": "3rd 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_mode": "Self-regulation",
|
||||||
|
"auto_regulation_dtemp": "Regulation threshold",
|
||||||
|
"auto_regulation_periode_min": "Regulation minimal period"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"heater_entity_id": "Mandatory heater entity id",
|
"heater_entity_id": "Mandatory heater entity id",
|
||||||
@@ -56,7 +58,9 @@
|
|||||||
"valve_entity2_id": "2nd valve number entity id",
|
"valve_entity2_id": "2nd valve number entity id",
|
||||||
"valve_entity3_id": "3rd 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_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": {
|
"tpi": {
|
||||||
@@ -202,7 +206,9 @@
|
|||||||
"valve_entity2_id": "2nd valve number",
|
"valve_entity2_id": "2nd valve number",
|
||||||
"valve_entity3_id": "3rd 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_mode": "Self-regulation",
|
||||||
|
"auto_regulation_dtemp": "Regulation threshold",
|
||||||
|
"auto_regulation_periode_min": "Regulation minimal period"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"heater_entity_id": "Mandatory heater entity id",
|
"heater_entity_id": "Mandatory heater entity id",
|
||||||
@@ -219,7 +225,9 @@
|
|||||||
"valve_entity2_id": "2nd valve number entity id",
|
"valve_entity2_id": "2nd valve number entity id",
|
||||||
"valve_entity3_id": "3rd 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_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": {
|
"tpi": {
|
||||||
|
|||||||
@@ -39,7 +39,9 @@
|
|||||||
"valve_entity2_id": "2ème valve number",
|
"valve_entity2_id": "2ème valve number",
|
||||||
"valve_entity3_id": "3è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_mode": "Auto-régulation",
|
||||||
|
"auto_regulation_dtemp": "Seuil de régulation",
|
||||||
|
"auto_regulation_periode_min": "Période minimale de régulation"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||||
@@ -56,7 +58,9 @@
|
|||||||
"valve_entity2_id": "Entity id de la 2ème valve",
|
"valve_entity2_id": "Entity id de la 2ème valve",
|
||||||
"valve_entity3_id": "Entity id de la 3è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_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": {
|
"tpi": {
|
||||||
@@ -203,7 +207,9 @@
|
|||||||
"valve_entity2_id": "2ème valve",
|
"valve_entity2_id": "2ème valve",
|
||||||
"valve_entity3_id": "3ème valve",
|
"valve_entity3_id": "3ème valve",
|
||||||
"valve_entity4_id": "4ème valve",
|
"valve_entity4_id": "4ème valve",
|
||||||
"auto_regulation_mode": "Auto-regulation"
|
"auto_regulation_mode": "Auto-regulation",
|
||||||
|
"auto_regulation_dtemp": "Seuil de régulation",
|
||||||
|
"auto_regulation_periode_min": "Période minimale de régulation"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
|
||||||
@@ -220,7 +226,9 @@
|
|||||||
"valve_entity2_id": "Entity id de la 2ème valve",
|
"valve_entity2_id": "Entity id de la 2ème valve",
|
||||||
"valve_entity3_id": "Entity id de la 3è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_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": {
|
"tpi": {
|
||||||
|
|||||||
@@ -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.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
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.base_thermostat import BaseThermostat
|
||||||
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
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.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
|
from .const import ( # pylint: disable=unused-import
|
||||||
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
MOCK_TH_OVER_SWITCH_USER_CONFIG,
|
||||||
@@ -478,13 +478,6 @@ async def send_presence_change_event(
|
|||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
return ret
|
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(
|
async def send_climate_change_event(
|
||||||
entity: BaseThermostat,
|
entity: BaseThermostat,
|
||||||
new_hvac_mode: HVACMode,
|
new_hvac_mode: HVACMode,
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ from custom_components.versatile_thermostat.const import (
|
|||||||
PRESET_AWAY_SUFFIX,
|
PRESET_AWAY_SUFFIX,
|
||||||
CONF_CLIMATE,
|
CONF_CLIMATE,
|
||||||
CONF_AUTO_REGULATION_MODE,
|
CONF_AUTO_REGULATION_MODE,
|
||||||
CONF_AUTO_REGULATION_MEDIUM,
|
CONF_AUTO_REGULATION_STRONG,
|
||||||
CONF_AUTO_REGULATION_NONE,
|
CONF_AUTO_REGULATION_NONE,
|
||||||
|
CONF_AUTO_REGULATION_DTEMP,
|
||||||
|
CONF_AUTO_REGULATION_PERIOD_MIN
|
||||||
)
|
)
|
||||||
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
MOCK_TH_OVER_SWITCH_USER_CONFIG = {
|
||||||
CONF_NAME: "TheOverSwitchMockName",
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
@@ -125,7 +127,9 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
|
|||||||
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
|
||||||
CONF_CLIMATE: "climate.mock_climate",
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
CONF_AC_MODE: False,
|
CONF_AC_MODE: False,
|
||||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM
|
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||||
|
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||||
|
CONF_AUTO_REGULATION_PERIOD_MIN: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
|
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
|
||||||
@@ -137,7 +141,9 @@ MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
|
|||||||
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
|
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
|
||||||
CONF_CLIMATE: "climate.mock_climate",
|
CONF_CLIMATE: "climate.mock_climate",
|
||||||
CONF_AC_MODE: True,
|
CONF_AC_MODE: True,
|
||||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM
|
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||||
|
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||||
|
CONF_AUTO_REGULATION_PERIOD_MIN: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
MOCK_PRESETS_CONFIG = {
|
MOCK_PRESETS_CONFIG = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor
|
|||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_state):
|
async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||||
"""Test the regulation of an over climate thermostat"""
|
"""Test the regulation of an over climate thermostat"""
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@@ -37,8 +37,11 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
|
|||||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||||
|
|
||||||
# Creates the regulated VTherm over climate
|
# Creates the regulated VTherm over climate
|
||||||
|
# change temperature so that the heating will start
|
||||||
|
event_timestamp = now - timedelta(minutes=10)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||||
), patch(
|
), patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||||
return_value=fake_underlying_climate,
|
return_value=fake_underlying_climate,
|
||||||
@@ -73,45 +76,50 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
|
|||||||
]
|
]
|
||||||
assert entity.preset_mode is PRESET_NONE
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
|
||||||
# Activate the heating by changing HVACMode and temperature
|
# Activate the heating by changing HVACMode and temperature
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
):
|
|
||||||
# Select a hvacmode, presence and preset
|
# Select a hvacmode, presence and preset
|
||||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
assert entity.hvac_mode is HVACMode.HEAT
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.hvac_action == HVACAction.OFF
|
assert entity.hvac_action == HVACAction.OFF
|
||||||
|
|
||||||
# change temperature so that the heating will start
|
assert entity.regulated_target_temp == entity.min_temp
|
||||||
event_timestamp = now - timedelta(minutes=10)
|
|
||||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||||
await send_ext_temperature_change_event(entity, 10, 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)
|
||||||
|
|
||||||
# set manual target temp
|
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
|
||||||
await entity.async_set_temperature(temperature=18)
|
assert entity.hvac_action == HVACAction.HEATING
|
||||||
|
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||||
|
|
||||||
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
|
# the regulated temperature should be greater
|
||||||
assert entity.hvac_action == HVACAction.HEATING
|
assert entity.regulated_target_temp > entity.target_temperature
|
||||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
# 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
|
||||||
# the regulated temperature should be greater
|
assert entity.regulated_target_temp == 18+2.0
|
||||||
assert entity.regulated_target_temp > entity.target_temperature
|
assert entity.hvac_action == HVACAction.HEATING
|
||||||
assert entity.regulated_target_temp == 18+2.9 # In medium we could go up to +3 degre
|
|
||||||
assert entity.hvac_action == HVACAction.HEATING
|
|
||||||
|
|
||||||
# change temperature so that the regulated temperature should slow down
|
# change temperature so that the regulated temperature should slow down
|
||||||
event_timestamp = now - timedelta(minutes=9)
|
event_timestamp = now - timedelta(minutes=5)
|
||||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
with patch(
|
||||||
await send_ext_temperature_change_event(entity, 18, event_timestamp)
|
"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
|
# the regulated temperature should be under
|
||||||
assert entity.regulated_target_temp < entity.target_temperature
|
assert entity.regulated_target_temp < entity.target_temperature
|
||||||
assert entity.regulated_target_temp == 18-0.1
|
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_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_states_is_state):
|
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"""
|
"""Test the regulation of an over climate thermostat"""
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
@@ -128,8 +136,11 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
|
|||||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||||
|
|
||||||
# Creates the regulated VTherm over climate
|
# Creates the regulated VTherm over climate
|
||||||
|
# change temperature so that the heating will start
|
||||||
|
event_timestamp = now - timedelta(minutes=10)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||||
), patch(
|
), patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||||
return_value=fake_underlying_climate,
|
return_value=fake_underlying_climate,
|
||||||
@@ -164,47 +175,160 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
|
|||||||
]
|
]
|
||||||
assert entity.preset_mode is PRESET_NONE
|
assert entity.preset_mode is PRESET_NONE
|
||||||
|
|
||||||
# Activate the heating by changing HVACMode and temperature
|
# Activate the heating by changing HVACMode and temperature
|
||||||
with patch(
|
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
|
||||||
):
|
|
||||||
# Select a hvacmode, presence and preset
|
# Select a hvacmode, presence and preset
|
||||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
assert entity.hvac_mode is HVACMode.HEAT
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.hvac_action == HVACAction.OFF
|
assert entity.hvac_action == HVACAction.OFF
|
||||||
|
|
||||||
# change temperature so that the heating will start
|
# change temperature so that the heating will start
|
||||||
event_timestamp = now - timedelta(minutes=10)
|
|
||||||
await send_temperature_change_event(entity, 30, event_timestamp)
|
await send_temperature_change_event(entity, 30, event_timestamp)
|
||||||
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
||||||
|
|
||||||
|
|
||||||
# set manual target temp
|
# set manual target temp
|
||||||
await entity.async_set_temperature(temperature=25)
|
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
|
fake_underlying_climate.set_hvac_action(HVACAction.COOLING) # simulate under heating
|
||||||
assert entity.hvac_action == HVACAction.COOLING
|
assert entity.hvac_action == HVACAction.COOLING
|
||||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||||
|
|
||||||
# the regulated temperature should be lower
|
# the regulated temperature should be lower
|
||||||
assert entity.regulated_target_temp < entity.target_temperature
|
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.regulated_target_temp == 25-3 # In medium we could go up to -3 degre
|
||||||
assert entity.hvac_action == HVACAction.COOLING
|
assert entity.hvac_action == HVACAction.COOLING
|
||||||
|
|
||||||
# change temperature so that the regulated temperature should slow down
|
# change temperature so that the regulated temperature should slow down
|
||||||
event_timestamp = now - timedelta(minutes=9)
|
event_timestamp = now - timedelta(minutes=5)
|
||||||
await send_temperature_change_event(entity, 26, event_timestamp)
|
with patch(
|
||||||
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
"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
|
# the regulated temperature should be under
|
||||||
assert entity.regulated_target_temp < entity.target_temperature
|
assert entity.regulated_target_temp < entity.target_temperature
|
||||||
assert entity.regulated_target_temp == 25-2.7
|
assert entity.regulated_target_temp == 25-2.5 # +2.3 without round_to_nearest
|
||||||
|
|
||||||
# change temperature so that the regulated temperature should slow down
|
# change temperature so that the regulated temperature should slow down
|
||||||
event_timestamp = now - timedelta(minutes=9)
|
event_timestamp = now - timedelta(minutes=3)
|
||||||
await send_temperature_change_event(entity, 20, event_timestamp)
|
with patch(
|
||||||
await send_ext_temperature_change_event(entity, 30, event_timestamp)
|
"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
|
# the regulated temperature should be greater
|
||||||
assert entity.regulated_target_temp > entity.target_temperature
|
assert entity.regulated_target_temp > entity.target_temperature
|
||||||
assert entity.regulated_target_temp == 25+1.8
|
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+1 # In strong 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 + 1 # 0.7 without round_to_nearest
|
||||||
Reference in New Issue
Block a user