Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0a85c10ff | |||
| 592b2ca574 | |||
| 9102b09691 | |||
| 5b64a8fba0 | |||
| 14bb7474ae | |||
| bc1c18d719 | |||
| c935ee3af5 | |||
| ce73f1275b | |||
| cd08dca913 | |||
| 93079e974f | |||
| db38392dab | |||
| ac705df862 |
@@ -1279,13 +1279,9 @@ Replace values in [[ ]] by yours.
|
||||
yaxis: y1
|
||||
name: Ema
|
||||
- entity: '[[climate]]'
|
||||
attribute: on_percent
|
||||
yaxis: y2
|
||||
name: Power percent
|
||||
fill: tozeroy
|
||||
fillcolor: rgba(200, 10, 10, 0.3)
|
||||
line:
|
||||
color: rgba(200, 10, 10, 0.9)
|
||||
attribute: regulated_target_temperature
|
||||
yaxis: y1
|
||||
name: Regulated T°
|
||||
- entity: '[[slope]]'
|
||||
name: Slope
|
||||
fill: tozeroy
|
||||
@@ -1310,19 +1306,12 @@ Replace values in [[ ]] by yours.
|
||||
yaxis:
|
||||
visible: true
|
||||
position: 0
|
||||
yaxis2:
|
||||
visible: true
|
||||
position: 0
|
||||
fixedrange: true
|
||||
range:
|
||||
- 0
|
||||
- 1
|
||||
yaxis9:
|
||||
visible: true
|
||||
fixedrange: false
|
||||
range:
|
||||
- -2
|
||||
- 2
|
||||
- -0.5
|
||||
- 0.5
|
||||
position: 1
|
||||
xaxis:
|
||||
rangeselector:
|
||||
|
||||
@@ -9,6 +9,7 @@ from datetime import timedelta, datetime
|
||||
from types import MappingProxyType
|
||||
from typing import Any, TypeVar, Generic
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
@@ -79,6 +80,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ConfigData = MappingProxyType[str, Any]
|
||||
T = TypeVar("T", bound=UnderlyingEntity)
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"""Representation of a base class for all Versatile Thermostat device."""
|
||||
|
||||
@@ -130,8 +138,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"target_temperature_step",
|
||||
"is_used_by_central_boiler",
|
||||
"temperature_slope",
|
||||
"max_on_percent",
|
||||
"have_valve_regulation",
|
||||
"max_on_percent"
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -453,8 +460,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
else DEFAULT_SECURITY_DEFAULT_ON_PERCENT
|
||||
)
|
||||
self._minimal_activation_delay = entry_infos.get(CONF_MINIMAL_ACTIVATION_DELAY)
|
||||
self._last_temperature_measure = self.now
|
||||
self._last_ext_temperature_measure = self.now
|
||||
self._last_temperature_measure = datetime.now(tz=self._current_tz)
|
||||
self._last_ext_temperature_measure = datetime.now(tz=self._current_tz)
|
||||
self._security_state = False
|
||||
|
||||
# Initiate the ProportionalAlgorithm
|
||||
@@ -1335,7 +1342,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self, old_preset_mode: str | None = None
|
||||
): # pylint: disable=unused-argument
|
||||
"""Reset to now the last change time"""
|
||||
self._last_change_time = self.now
|
||||
self._last_change_time = datetime.now(tz=self._current_tz)
|
||||
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
|
||||
|
||||
def reset_last_temperature_time(self, old_preset_mode: str | None = None):
|
||||
@@ -1345,7 +1352,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
and old_preset_mode not in HIDDEN_PRESETS
|
||||
):
|
||||
self._last_temperature_measure = self._last_ext_temperature_measure = (
|
||||
self.now
|
||||
datetime.now(tz=self._current_tz)
|
||||
)
|
||||
|
||||
def find_preset_temp(self, preset_mode: str):
|
||||
@@ -1378,10 +1385,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
)
|
||||
|
||||
if motion_preset in self._presets:
|
||||
if self._presence_on and self.presence_state in [STATE_OFF, None]:
|
||||
return self._presets_away[motion_preset + PRESET_AWAY_SUFFIX]
|
||||
else:
|
||||
return self._presets[motion_preset]
|
||||
return self._presets[motion_preset]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
@@ -1451,16 +1455,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"""Extract the last_changed state from State or return now if not available"""
|
||||
return (
|
||||
state.last_changed.astimezone(self._current_tz)
|
||||
if isinstance(state.last_changed, datetime)
|
||||
else self.now
|
||||
if state.last_changed is not None
|
||||
else datetime.now(tz=self._current_tz)
|
||||
)
|
||||
|
||||
def get_last_updated_date_or_now(self, state: State) -> datetime:
|
||||
"""Extract the last_changed state from State or return now if not available"""
|
||||
return (
|
||||
state.last_updated.astimezone(self._current_tz)
|
||||
if isinstance(state.last_updated, datetime)
|
||||
else self.now
|
||||
if state.last_updated is not None
|
||||
else datetime.now(tz=self._current_tz)
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -1902,12 +1906,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
STATE_NOT_HOME,
|
||||
):
|
||||
return
|
||||
if self._attr_preset_mode not in [
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
PRESET_ACTIVITY,
|
||||
]:
|
||||
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
|
||||
return
|
||||
|
||||
new_temp = self.find_preset_temp(self.preset_mode)
|
||||
@@ -1997,7 +1996,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
if in_cycle:
|
||||
slope = self._window_auto_algo.check_age_last_measurement(
|
||||
temperature=self._ema_temp,
|
||||
datetime_now=self.now,
|
||||
datetime_now=datetime.now(get_tz(self._hass)),
|
||||
)
|
||||
else:
|
||||
slope = self._window_auto_algo.add_temp_measurement(
|
||||
@@ -2285,11 +2284,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
@property
|
||||
def now(self) -> datetime:
|
||||
"""Get now. The local datetime or the overloaded _set_now date"""
|
||||
return self._now if self._now is not None else NowClass.get_now(self._hass)
|
||||
return self._now if self._now is not None else datetime.now(self._current_tz)
|
||||
|
||||
async def check_safety(self) -> bool:
|
||||
"""Check if last temperature date is too long"""
|
||||
|
||||
now = self.now
|
||||
delta_temp = (
|
||||
now - self._last_temperature_measure.replace(tzinfo=self._current_tz)
|
||||
@@ -2657,7 +2655,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"device_power": self._device_power,
|
||||
ATTR_MEAN_POWER_CYCLE: self.mean_cycle_power,
|
||||
ATTR_TOTAL_ENERGY: self.total_energy,
|
||||
"last_update_datetime": self.now.isoformat(),
|
||||
"last_update_datetime": datetime.now()
|
||||
.astimezone(self._current_tz)
|
||||
.isoformat(),
|
||||
"timezone": str(self._current_tz),
|
||||
"temperature_unit": self.temperature_unit,
|
||||
"is_device_active": self.is_device_active,
|
||||
@@ -2666,7 +2666,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"temperature_slope": round(self.last_temperature_slope or 0, 3),
|
||||
"hvac_off_reason": self.hvac_off_reason,
|
||||
"max_on_percent": self._max_on_percent,
|
||||
"have_valve_regulation": self.have_valve_regulation,
|
||||
}
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -2685,11 +2684,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
)
|
||||
return super().async_write_ha_state()
|
||||
|
||||
@property
|
||||
def have_valve_regulation(self) -> bool:
|
||||
"""True if the Thermostat is regulated by valve"""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self):
|
||||
"""update the entity if the config entry have been updated
|
||||
|
||||
@@ -22,12 +22,28 @@ from homeassistant.const import (
|
||||
STATE_NOT_HOME,
|
||||
)
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
CONF_PRESETS_WITH_AC,
|
||||
SERVICE_SET_PRESENCE,
|
||||
SERVICE_SET_PRESET_TEMPERATURE,
|
||||
SERVICE_SET_SECURITY,
|
||||
SERVICE_SET_WINDOW_BYPASS,
|
||||
SERVICE_SET_AUTO_REGULATION_MODE,
|
||||
SERVICE_SET_AUTO_FAN_MODE,
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_SONOFF_TRZB_MODE,
|
||||
)
|
||||
|
||||
from .thermostat_switch import ThermostatOverSwitch
|
||||
from .thermostat_climate import ThermostatOverClimate
|
||||
from .thermostat_valve import ThermostatOverValve
|
||||
from .thermostat_climate_valve import ThermostatOverClimateValve
|
||||
from .thermostat_sonoff_trvzb import ThermostatOverSonoffTRVZB
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,9 +62,7 @@ async def async_setup_entry(
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
have_valve_regulation = (
|
||||
entry.data.get(CONF_AUTO_REGULATION_MODE) == CONF_AUTO_REGULATION_VALVE
|
||||
)
|
||||
is_sonoff_trvzb = entry.data.get(CONF_SONOFF_TRZB_MODE)
|
||||
|
||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||
return
|
||||
@@ -58,8 +72,8 @@ async def async_setup_entry(
|
||||
if vt_type == CONF_THERMOSTAT_SWITCH:
|
||||
entity = ThermostatOverSwitch(hass, unique_id, name, entry.data)
|
||||
elif vt_type == CONF_THERMOSTAT_CLIMATE:
|
||||
if have_valve_regulation is True:
|
||||
entity = ThermostatOverClimateValve(hass, unique_id, name, entry.data)
|
||||
if is_sonoff_trvzb is True:
|
||||
entity = ThermostatOverSonoffTRVZB(hass, unique_id, name, entry.data)
|
||||
else:
|
||||
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
|
||||
elif vt_type == CONF_THERMOSTAT_VALVE:
|
||||
|
||||
@@ -3,20 +3,38 @@
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
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, ServiceConfigurationError
|
||||
|
||||
_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:
|
||||
|
||||
@@ -29,6 +29,27 @@ COMES_FROM = "comes_from"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Not used but can be useful in other context
|
||||
# def schema_defaults(schema, **defaults):
|
||||
# """Create a new schema with default values filled in."""
|
||||
# copy = schema.extend({})
|
||||
# for field, field_type in copy.schema.items():
|
||||
# if isinstance(field_type, vol.In):
|
||||
# value = None
|
||||
#
|
||||
# if value in field_type.container:
|
||||
# # field.default = vol.default_factory(value)
|
||||
# field.description = {"suggested_value": value}
|
||||
# continue
|
||||
#
|
||||
# if field.schema in defaults:
|
||||
# # field.default = vol.default_factory(defaults[field])
|
||||
# field.description = {"suggested_value": defaults[field]}
|
||||
# return copy
|
||||
#
|
||||
|
||||
|
||||
def add_suggested_values_to_schema(
|
||||
data_schema: vol.Schema, suggested_values: Mapping[str, Any]
|
||||
) -> vol.Schema:
|
||||
@@ -141,37 +162,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if COMES_FROM in self._infos:
|
||||
del self._infos[COMES_FROM]
|
||||
|
||||
def is_valve_regulation_selected(self, infos) -> bool:
|
||||
"""True of the valve regulation mode is selected"""
|
||||
return infos.get(CONF_AUTO_REGULATION_MODE, None) == CONF_AUTO_REGULATION_VALVE
|
||||
|
||||
def check_valve_regulation_nb_entities(self, data: dict, step_id=None) -> bool:
|
||||
"""Check the number of entities for Valve regulation"""
|
||||
if step_id not in ["type", "valve_regulation", "check_complete"]:
|
||||
return True
|
||||
|
||||
# underlyings_to_check = data if step_id == "type" else self._infos
|
||||
underlyings_to_check = self._infos # data if step_id == "type" else self._infos
|
||||
regulation_infos_to_check = (
|
||||
data if step_id == "valve_regulation" else self._infos
|
||||
)
|
||||
|
||||
def check_sonoff_trvzb_nb_entities(self, data: dict) -> bool:
|
||||
"""Check the number of entities for Sonoff TRVZB"""
|
||||
ret = True
|
||||
if self.is_valve_regulation_selected(underlyings_to_check):
|
||||
nb_unders = len(underlyings_to_check.get(CONF_UNDERLYING_LIST))
|
||||
nb_offset = len(
|
||||
regulation_infos_to_check.get(CONF_OFFSET_CALIBRATION_LIST, [])
|
||||
)
|
||||
nb_opening = len(
|
||||
regulation_infos_to_check.get(CONF_OPENING_DEGREE_LIST, [])
|
||||
)
|
||||
nb_closing = len(
|
||||
regulation_infos_to_check.get(CONF_CLOSING_DEGREE_LIST, [])
|
||||
)
|
||||
if (
|
||||
self._infos.get(CONF_SONOFF_TRZB_MODE)
|
||||
and data.get(CONF_OFFSET_CALIBRATION_LIST) is not None
|
||||
):
|
||||
nb_unders = len(self._infos.get(CONF_UNDERLYING_LIST))
|
||||
nb_offset = len(data.get(CONF_OFFSET_CALIBRATION_LIST))
|
||||
nb_opening = len(data.get(CONF_OPENING_DEGREE_LIST))
|
||||
nb_closing = len(data.get(CONF_CLOSING_DEGREE_LIST))
|
||||
if (
|
||||
nb_unders != nb_opening
|
||||
or (nb_unders != nb_offset and nb_offset > 0)
|
||||
or (nb_unders != nb_closing and nb_closing > 0)
|
||||
nb_unders != nb_offset
|
||||
or nb_unders != nb_opening
|
||||
or nb_unders != nb_closing
|
||||
):
|
||||
ret = False
|
||||
return ret
|
||||
@@ -254,8 +259,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
# Check that the number of offet_calibration and opening_degree and closing_degree are equals
|
||||
# to the number of underlying entities
|
||||
if not self.check_valve_regulation_nb_entities(data, step_id):
|
||||
raise ValveRegulationNbEntitiesIncorrect()
|
||||
if not self.check_sonoff_trvzb_nb_entities(data):
|
||||
raise SonoffTRVZBNbEntitiesIncorrect()
|
||||
|
||||
def check_config_complete(self, infos) -> bool:
|
||||
"""True if the config is now complete (ie all mandatory attributes are set)"""
|
||||
@@ -352,7 +357,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
):
|
||||
return False
|
||||
|
||||
if not self.check_valve_regulation_nb_entities(infos, "check_complete"):
|
||||
if not self.check_sonoff_trvzb_nb_entities(infos):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -395,8 +400,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
errors[str(err)] = "service_configuration_format"
|
||||
except ConfigurationNotCompleteError as err:
|
||||
errors["base"] = "configuration_not_complete"
|
||||
except ValveRegulationNbEntitiesIncorrect as err:
|
||||
errors["base"] = "valve_regulation_nb_entities_incorrect"
|
||||
except SonoffTRVZBNbEntitiesIncorrect as err:
|
||||
errors["base"] = "sonoff_trvzb_nb_entities_incorrect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -448,7 +453,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if (
|
||||
self._infos.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI
|
||||
or is_central_config
|
||||
or self.is_valve_regulation_selected(self._infos)
|
||||
):
|
||||
menu_options.append("tpi")
|
||||
|
||||
@@ -484,8 +488,8 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
]:
|
||||
menu_options.append("auto_start_stop")
|
||||
|
||||
if self.is_valve_regulation_selected(self._infos):
|
||||
menu_options.append("valve_regulation")
|
||||
if self._infos.get(CONF_SONOFF_TRZB_MODE) is True:
|
||||
menu_options.append("sonoff_trvzb")
|
||||
|
||||
menu_options.append("advanced")
|
||||
|
||||
@@ -559,7 +563,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
if (
|
||||
self._infos[CONF_THERMOSTAT_TYPE] == CONF_THERMOSTAT_CLIMATE
|
||||
and user_input is not None
|
||||
and not self.is_valve_regulation_selected(user_input)
|
||||
and not user_input.get(CONF_SONOFF_TRZB_MODE)
|
||||
):
|
||||
# Remove TPI info
|
||||
for key in [
|
||||
@@ -617,21 +621,19 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
|
||||
return await self.generic_step("auto_start_stop", schema, user_input, next_step)
|
||||
|
||||
async def async_step_valve_regulation(
|
||||
async def async_step_sonoff_trvzb(
|
||||
self, user_input: dict | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the valve regulation configuration step"""
|
||||
"""Handle the Sonoff TRVZB configuration step"""
|
||||
_LOGGER.debug(
|
||||
"Into ConfigFlow.async_step_valve_regulation user_input=%s", user_input
|
||||
"Into ConfigFlow.async_step_sonoff_trvzb user_input=%s", user_input
|
||||
)
|
||||
|
||||
schema = STEP_VALVE_REGULATION
|
||||
schema = STEP_SONOFF_TRVZB
|
||||
self._infos[COMES_FROM] = None
|
||||
next_step = self.async_step_menu
|
||||
|
||||
return await self.generic_step(
|
||||
"valve_regulation", schema, user_input, next_step
|
||||
)
|
||||
return await self.generic_step("sonoff_trvzb", schema, user_input, next_step)
|
||||
|
||||
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the TPI flow steps"""
|
||||
|
||||
@@ -141,6 +141,7 @@ STEP_THERMOSTAT_CLIMATE = vol.Schema( # pylint: disable=invalid-name
|
||||
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True),
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SONOFF_TRZB_MODE, default=False): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_AUTO_REGULATION_MODE, default=CONF_AUTO_REGULATION_NONE
|
||||
): selector.SelectSelector(
|
||||
@@ -197,19 +198,19 @@ STEP_AUTO_START_STOP = vol.Schema( # pylint: disable=invalid-name
|
||||
}
|
||||
)
|
||||
|
||||
STEP_VALVE_REGULATION = vol.Schema( # pylint: disable=invalid-name
|
||||
STEP_SONOFF_TRVZB = vol.Schema( # pylint: disable=invalid-name
|
||||
{
|
||||
vol.Required(CONF_OFFSET_CALIBRATION_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_OPENING_DEGREE_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_OFFSET_CALIBRATION_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_CLOSING_DEGREE_LIST): selector.EntitySelector(
|
||||
vol.Required(CONF_CLOSING_DEGREE_LIST): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=[NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN], multiple=True
|
||||
),
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
import logging
|
||||
import math
|
||||
from typing import Literal
|
||||
from datetime import datetime
|
||||
|
||||
from enum import Enum
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
@@ -19,7 +17,6 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .prop_algorithm import (
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
@@ -98,12 +95,12 @@ CONF_USE_POWER_FEATURE = "use_power_feature"
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE = "use_central_boiler_feature"
|
||||
CONF_USE_AUTO_START_STOP_FEATURE = "use_auto_start_stop_feature"
|
||||
CONF_AC_MODE = "ac_mode"
|
||||
CONF_SONOFF_TRZB_MODE = "sonoff_trvzb_mode"
|
||||
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
|
||||
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
|
||||
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
|
||||
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
|
||||
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
|
||||
CONF_AUTO_REGULATION_VALVE = "auto_regulation_valve"
|
||||
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
|
||||
CONF_AUTO_REGULATION_LIGHT = "auto_regulation_light"
|
||||
CONF_AUTO_REGULATION_MEDIUM = "auto_regulation_medium"
|
||||
@@ -296,6 +293,7 @@ ALL_CONF = (
|
||||
CONF_USE_POWER_FEATURE,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_AC_MODE,
|
||||
CONF_SONOFF_TRZB_MODE,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
@@ -329,7 +327,6 @@ CONF_FUNCTIONS = [
|
||||
|
||||
CONF_AUTO_REGULATION_MODES = [
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
@@ -509,24 +506,6 @@ def get_safe_float(hass, entity_id: str):
|
||||
return None if math.isinf(float_val) or not math.isfinite(float_val) else float_val
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
"""Get the current timezone"""
|
||||
|
||||
return dt_util.get_time_zone(hass.config.time_zone)
|
||||
|
||||
|
||||
class NowClass:
|
||||
"""For testing purpose only"""
|
||||
|
||||
@staticmethod
|
||||
def get_now(hass: HomeAssistant) -> datetime:
|
||||
"""A test function to get the now.
|
||||
For testing purpose this method can be overriden to get a specific
|
||||
timestamp.
|
||||
"""
|
||||
return datetime.now(get_tz(hass))
|
||||
|
||||
|
||||
class UnknownEntity(HomeAssistantError):
|
||||
"""Error to indicate there is an unknown entity_id given."""
|
||||
|
||||
@@ -547,8 +526,8 @@ class ConfigurationNotCompleteError(HomeAssistantError):
|
||||
"""Error the configuration is not complete"""
|
||||
|
||||
|
||||
class ValveRegulationNbEntitiesIncorrect(HomeAssistantError):
|
||||
"""Error to indicate there is an error in the configuration of the TRV with valve regulation.
|
||||
class SonoffTRVZBNbEntitiesIncorrect(HomeAssistantError):
|
||||
"""Error to indicate there is an error in the configuration of the Sonoff TRVZB.
|
||||
The number of specific entities is incorrect."""
|
||||
|
||||
|
||||
|
||||
@@ -49,8 +49,7 @@ from .const import (
|
||||
CONF_THERMOSTAT_TYPE,
|
||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_SONOFF_TRZB_MODE,
|
||||
overrides,
|
||||
)
|
||||
|
||||
@@ -72,9 +71,7 @@ async def async_setup_entry(
|
||||
unique_id = entry.entry_id
|
||||
name = entry.data.get(CONF_NAME)
|
||||
vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
|
||||
have_valve_regulation = (
|
||||
entry.data.get(CONF_AUTO_REGULATION_MODE) == CONF_AUTO_REGULATION_VALVE
|
||||
)
|
||||
is_sonoff_trvzb = entry.data.get(CONF_SONOFF_TRZB_MODE)
|
||||
|
||||
entities = None
|
||||
|
||||
@@ -105,13 +102,13 @@ async def async_setup_entry(
|
||||
|
||||
if (
|
||||
entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
|
||||
or have_valve_regulation
|
||||
or is_sonoff_trvzb
|
||||
):
|
||||
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
|
||||
|
||||
if (
|
||||
entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
|
||||
and not have_valve_regulation
|
||||
and not is_sonoff_trvzb
|
||||
):
|
||||
entities.append(
|
||||
RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"sonoff_trvzb": "Sonoff TRVZB configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
@@ -77,6 +77,7 @@
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"ac_mode": "AC mode",
|
||||
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
@@ -89,6 +90,7 @@
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
@@ -217,9 +219,9 @@
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve",
|
||||
"description": "Configuration for self-regulation with direct control of the valve",
|
||||
"sonoff_trvzb": {
|
||||
"title": "Sonoff TRVZB configuration",
|
||||
"description": "Specific Sonoff TRVZB configuration",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
@@ -227,9 +229,9 @@
|
||||
"proportional_function": "Algorithm"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
}
|
||||
}
|
||||
@@ -272,7 +274,7 @@
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"sonoff_trvzb": "Sonoff TRVZB configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -309,7 +311,7 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
@@ -321,6 +323,7 @@
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"ac_mode": "AC mode",
|
||||
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
@@ -333,6 +336,7 @@
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
@@ -461,9 +465,9 @@
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve - {name}",
|
||||
"description": "Configuration for self-regulation with direct control of the valve",
|
||||
"sonoff_trvzb": {
|
||||
"title": "Sonoff TRVZB configuration - {name}",
|
||||
"description": "Specific Sonoff TRVZB configuration",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
@@ -471,9 +475,9 @@
|
||||
"proportional_function": "Algorithm"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
}
|
||||
}
|
||||
@@ -484,7 +488,7 @@
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong",
|
||||
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings"
|
||||
"sonoff_trvzb_nb_entities_incorrect": "The number of specific entities for Sonoff TRVZB should be equal to the number of underlyings"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -506,8 +510,7 @@
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation",
|
||||
"auto_regulation_valve": "Direct control of valve"
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
)
|
||||
|
||||
from .commons import round_to_nearest
|
||||
from .commons import NowClass, round_to_nearest
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .pi_algorithm import PITemperatureRegulator
|
||||
|
||||
@@ -90,7 +90,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
# 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 = None # NowClass.get_now(hass)
|
||||
self._last_regulation_change = NowClass.get_now(hass)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
@@ -180,8 +180,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
await super()._async_internal_set_temperature(temperature)
|
||||
|
||||
self._regulation_algo.set_target_temp(self.target_temperature)
|
||||
# is done by control_heating method. No need to do it here
|
||||
# await self._send_regulated_temperature(force=True)
|
||||
await self._send_regulated_temperature(force=True)
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
@@ -206,18 +205,16 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
force,
|
||||
)
|
||||
|
||||
if self._last_regulation_change is not None:
|
||||
period = (
|
||||
float((self.now - self._last_regulation_change).total_seconds()) / 60.0
|
||||
now: datetime = NowClass.get_now(self._hass)
|
||||
period = float((now - self._last_regulation_change).total_seconds()) / 60.0
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
|
||||
self,
|
||||
period,
|
||||
self._auto_regulation_period_min,
|
||||
)
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
|
||||
self,
|
||||
period,
|
||||
self._auto_regulation_period_min,
|
||||
)
|
||||
return
|
||||
return
|
||||
|
||||
if not self._regulated_target_temp:
|
||||
self._regulated_target_temp = self.target_temperature
|
||||
@@ -255,7 +252,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
new_regulated_temp,
|
||||
)
|
||||
|
||||
self._last_regulation_change = self.now
|
||||
self._last_regulation_change = now
|
||||
for under in self._underlyings:
|
||||
# issue 348 - use device temperature if configured as offset
|
||||
offset_temp = 0
|
||||
@@ -1261,13 +1258,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_SLOW)
|
||||
elif auto_regulation_mode == "Expert":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_EXPERT)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"%s - auto_regulation_mode %s is not supported",
|
||||
self,
|
||||
auto_regulation_mode,
|
||||
)
|
||||
return
|
||||
|
||||
await self._send_regulated_temperature()
|
||||
self.update_custom_attributes()
|
||||
|
||||
+33
-46
@@ -1,5 +1,5 @@
|
||||
# pylint: disable=line-too-long, too-many-lines, abstract-method
|
||||
""" A climate with a direct valve regulation class """
|
||||
""" A climate over Sonoff TRVZB classe """
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
@@ -7,7 +7,7 @@ from datetime import datetime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import HVACMode, HVACAction
|
||||
|
||||
from .underlyings import UnderlyingValveRegulation
|
||||
from .underlyings import UnderlyingSonoffTRVZB
|
||||
|
||||
# from .commons import NowClass, round_to_nearest
|
||||
from .base_thermostat import ConfigData
|
||||
@@ -21,14 +21,14 @@ from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
"""This class represent a VTherm over a climate with a direct valve regulation"""
|
||||
class ThermostatOverSonoffTRVZB(ThermostatOverClimate):
|
||||
"""This class represent a VTherm over a Sonoff TRVZB climate"""
|
||||
|
||||
_entity_component_unrecorded_attributes = ThermostatOverClimate._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset(
|
||||
{
|
||||
"is_over_climate",
|
||||
"have_valve_regulation",
|
||||
"is_over_sonoff_trvzb",
|
||||
"underlying_entities",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
@@ -40,7 +40,7 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
}
|
||||
)
|
||||
)
|
||||
_underlyings_valve_regulation: list[UnderlyingValveRegulation] = []
|
||||
_underlyings_sonoff_trvzb: list[UnderlyingSonoffTRVZB] = []
|
||||
_valve_open_percent: int | None = None
|
||||
_last_calculation_timestamp: datetime | None = None
|
||||
_auto_regulation_dpercent: float | None = None
|
||||
@@ -49,8 +49,8 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the ThermostatOverClimateValve class"""
|
||||
_LOGGER.debug("%s - creating a ThermostatOverClimateValve VTherm", name)
|
||||
"""Initialize the ThermostatOverSonoffTRVZB class"""
|
||||
_LOGGER.debug("%s - creating a ThermostatOverSonoffTRVZB VTherm", name)
|
||||
super().__init__(hass, unique_id, name, entry_infos)
|
||||
# self._valve_open_percent: int = 0
|
||||
# self._last_calculation_timestamp: datetime | None = None
|
||||
@@ -60,8 +60,8 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
@overrides
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat and underlyings
|
||||
Beware that the underlyings list contains the climate which represent the TRV
|
||||
but also the UnderlyingValveRegulation which reprensent the valve"""
|
||||
Beware that the underlyings list contains the climate which represent the Sonoff TRVZB
|
||||
but also the UnderlyingSonoff which reprensent the valve"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
@@ -86,15 +86,11 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
self.name,
|
||||
)
|
||||
|
||||
offset_list = config_entry.get(CONF_OFFSET_CALIBRATION_LIST)
|
||||
opening_list = config_entry.get(CONF_OPENING_DEGREE_LIST)
|
||||
closing_list = config_entry.get(CONF_CLOSING_DEGREE_LIST)
|
||||
for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)):
|
||||
offset = offset_list[idx] if idx < len(offset_list) else None
|
||||
# number of opening should equal number of underlying
|
||||
opening = opening_list[idx]
|
||||
closing = closing_list[idx] if idx < len(closing_list) else None
|
||||
under = UnderlyingValveRegulation(
|
||||
offset = config_entry.get(CONF_OFFSET_CALIBRATION_LIST)[idx]
|
||||
opening = config_entry.get(CONF_OPENING_DEGREE_LIST)[idx]
|
||||
closing = config_entry.get(CONF_CLOSING_DEGREE_LIST)[idx]
|
||||
under = UnderlyingSonoffTRVZB(
|
||||
hass=self._hass,
|
||||
thermostat=self,
|
||||
offset_calibration_entity_id=offset,
|
||||
@@ -102,19 +98,19 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
closing_degree_entity_id=closing,
|
||||
climate_underlying=self._underlyings[idx],
|
||||
)
|
||||
self._underlyings_valve_regulation.append(under)
|
||||
self._underlyings_sonoff_trvzb.append(under)
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
self._attr_extra_state_attributes["have_valve_regulation"] = (
|
||||
self.have_valve_regulation
|
||||
self._attr_extra_state_attributes["is_over_sonoff_trvzb"] = (
|
||||
self.is_over_sonoff_trvzb
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes["underlyings_valve_regulation"] = [
|
||||
underlying.entity_id for underlying in self._underlyings_valve_regulation
|
||||
self._attr_extra_state_attributes["underlying_sonoff_trvzb_entities"] = [
|
||||
underlying.entity_id for underlying in self._underlyings_sonoff_trvzb
|
||||
]
|
||||
|
||||
self._attr_extra_state_attributes["on_percent"] = (
|
||||
@@ -221,9 +217,12 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
|
||||
self._valve_open_percent = new_valve_percent
|
||||
|
||||
for under in self._underlyings_sonoff_trvzb:
|
||||
under.set_valve_open_percent()
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
super().recalculate()
|
||||
self.update_custom_attributes()
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
@@ -231,19 +230,17 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
return
|
||||
|
||||
for under in self._underlyings:
|
||||
if self.target_temperature != under.last_sent_temperature:
|
||||
await under.set_temperature(
|
||||
self.target_temperature,
|
||||
self._attr_max_temp,
|
||||
self._attr_min_temp,
|
||||
)
|
||||
await under.set_temperature(
|
||||
self.target_temperature,
|
||||
self._attr_max_temp,
|
||||
self._attr_min_temp,
|
||||
)
|
||||
|
||||
for under in self._underlyings_valve_regulation:
|
||||
await under.set_valve_open_percent()
|
||||
self.recalculate()
|
||||
|
||||
@property
|
||||
def have_valve_regulation(self) -> bool:
|
||||
"""True if the Thermostat is regulated by valve"""
|
||||
def is_over_sonoff_trvzb(self) -> bool:
|
||||
"""True if the Thermostat is over_sonoff_trvzb"""
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -269,16 +266,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Returns the current hvac_action by checking all hvac_action of the _underlyings_valve_regulation"""
|
||||
"""Returns the current hvac_action by checking all hvac_action of the _underlyings_sonoff_trvzb"""
|
||||
|
||||
return self.calculate_hvac_action(self._underlyings_valve_regulation)
|
||||
|
||||
@property
|
||||
def is_device_active(self) -> bool:
|
||||
"""A hack to overrides the state from underlyings"""
|
||||
return self.valve_open_percent > 0
|
||||
|
||||
@overrides
|
||||
async def service_set_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""This should not be possible in valve regulation mode"""
|
||||
return
|
||||
return self.calculate_hvac_action(self._underlyings_sonoff_trvzb)
|
||||
@@ -248,9 +248,8 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
|
||||
self._valve_open_percent = new_valve_percent
|
||||
|
||||
# is one in start_cycle now
|
||||
# for under in self._underlyings:
|
||||
# under.set_valve_open_percent()
|
||||
for under in self._underlyings:
|
||||
under.set_valve_open_percent()
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"sonoff_trvzb": "Sonoff TRVZB configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
@@ -77,6 +77,7 @@
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"ac_mode": "AC mode",
|
||||
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
@@ -89,6 +90,7 @@
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
@@ -217,9 +219,9 @@
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve",
|
||||
"description": "Configuration for self-regulation with direct control of the valve",
|
||||
"sonoff_trvzb": {
|
||||
"title": "Sonoff TRVZB configuration",
|
||||
"description": "Specific Sonoff TRVZB configuration",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
@@ -227,9 +229,9 @@
|
||||
"proportional_function": "Algorithm"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
}
|
||||
}
|
||||
@@ -272,7 +274,7 @@
|
||||
"presence": "Presence detection",
|
||||
"advanced": "Advanced parameters",
|
||||
"auto_start_stop": "Auto start and stop",
|
||||
"valve_regulation": "Valve regulation configuration",
|
||||
"sonoff_trvzb": "Sonoff TRVZB configuration",
|
||||
"finalize": "All done",
|
||||
"configuration_not_complete": "Configuration not complete"
|
||||
}
|
||||
@@ -309,7 +311,7 @@
|
||||
"use_motion_feature": "Use motion detection",
|
||||
"use_power_feature": "Use power management",
|
||||
"use_presence_feature": "Use presence detection",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after selecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_central_boiler_feature": "Use a central boiler. Check to add a control to your central boiler. You will have to configure the VTherm which will have a control of the central boiler after seecting this checkbox to take effect. If one VTherm requires heating, the boiler will be turned on. If no VTherm requires heating, the boiler will be turned off. Commands for turning on/off the central boiler are given in the related configuration page",
|
||||
"use_auto_start_stop_feature": "Use the auto start and stop feature"
|
||||
}
|
||||
},
|
||||
@@ -321,6 +323,7 @@
|
||||
"heater_keep_alive": "Switch keep-alive interval in seconds",
|
||||
"proportional_function": "Algorithm",
|
||||
"ac_mode": "AC mode",
|
||||
"sonoff_trvzb_mode": "SONOFF TRVZB mode",
|
||||
"auto_regulation_mode": "Self-regulation",
|
||||
"auto_regulation_dtemp": "Regulation threshold",
|
||||
"auto_regulation_periode_min": "Regulation minimum period",
|
||||
@@ -333,6 +336,7 @@
|
||||
"heater_keep_alive": "Optional heater switch state refresh interval. Leave empty if not required.",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)",
|
||||
"ac_mode": "Use the Air Conditioning (AC) mode",
|
||||
"sonoff_trvzb_mode": "The underlyings are SONOFF TRVZB. You have to configure some extra entities in the specific menu option 'Sonoff trvzb configuration'",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
@@ -450,7 +454,7 @@
|
||||
}
|
||||
},
|
||||
"central_boiler": {
|
||||
"title": "Control of the central boiler - {name}",
|
||||
"title": "Control of the central boiler",
|
||||
"description": "Enter the services to call to turn on/off the central boiler. Leave blank if no service call is to be made (in this case, you will have to manage the turning on/off of your central boiler yourself). The service called must be formatted as follows: `entity_id/service_name[/attribute:value]` (/attribute:value is optional)\nFor example:\n- to turn on a switch: `switch.controle_chaudiere/switch.turn_on`\n- to turn off a switch: `switch.controle_chaudiere/switch.turn_off`\n- to program the boiler to 25° and thus force its ignition: `climate.thermostat_chaudiere/climate.set_temperature/temperature:25`\n- to send 10° to the boiler and thus force its extinction: `climate.thermostat_chaudiere/climate.set_temperature/temperature:10`",
|
||||
"data": {
|
||||
"central_boiler_activation_service": "Command to turn-on",
|
||||
@@ -461,9 +465,9 @@
|
||||
"central_boiler_deactivation_service": "Command to turn-off the central boiler formatted like entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Self-regulation with valve - {name}",
|
||||
"description": "Configuration for self-regulation with direct control of the valve",
|
||||
"sonoff_trvzb": {
|
||||
"title": "Sonoff TRVZB configuration",
|
||||
"description": "Specific Sonoff TRVZB configuration",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Offset calibration entities",
|
||||
"opening_degree_entity_ids": "Opening degree entities",
|
||||
@@ -471,9 +475,9 @@
|
||||
"proportional_function": "Algorithm"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"offset_calibration_entity_ids": "The list of the 'offset calibration' entities. There should be one per underlying climate entities",
|
||||
"opening_degree_entity_ids": "The list of the 'opening degree' entities. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. Set it if your TRV have the entity for better regulation. There should be one per underlying climate entities",
|
||||
"closing_degree_entity_ids": "The list of the 'closing degree' entities. There should be one per underlying climate entities",
|
||||
"proportional_function": "Algorithm to use (TPI is the only one for now)"
|
||||
}
|
||||
}
|
||||
@@ -484,7 +488,7 @@
|
||||
"window_open_detection_method": "Only one window open detection method should be used. Use either window sensor or automatic detection through temperature threshold but not both",
|
||||
"no_central_config": "You cannot check 'use central configuration' because no central configuration was found. You need to create a Versatile Thermostat of type 'Central Configuration' to use it.",
|
||||
"service_configuration_format": "The format of the service configuration is wrong",
|
||||
"valve_regulation_nb_entities_incorrect": "The number of valve entities for valve regulation should be equal to the number of underlyings"
|
||||
"sonoff_trvzb_nb_entities_incorrect": "The number of specific entities for Sonoff TRVZB should be equal to the number of underlyings"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
@@ -506,8 +510,7 @@
|
||||
"auto_regulation_medium": "Medium",
|
||||
"auto_regulation_light": "Light",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "No auto-regulation",
|
||||
"auto_regulation_valve": "Direct control of valve"
|
||||
"auto_regulation_none": "No auto-regulation"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"presence": "Détection de présence",
|
||||
"advanced": "Paramètres avancés",
|
||||
"auto_start_stop": "Allumage/extinction automatique",
|
||||
"valve_regulation": "Configuration de la regulation par vanne",
|
||||
"sonoff_trvzb": "Configuration spécifique à Sonoff TRVZB",
|
||||
"finalize": "Finaliser la création",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
@@ -77,6 +77,7 @@
|
||||
"heater_keep_alive": "keep-alive (sec)",
|
||||
"proportional_function": "Algorithme",
|
||||
"ac_mode": "AC mode ?",
|
||||
"sonoff_trvzb_mode": "Mode Sonoff TRVZB",
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
@@ -89,8 +90,9 @@
|
||||
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
|
||||
"auto_regulation_mode": "Utilisation de l'auto-régulation faite par VTherm",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les vannes) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"sonoff_trvzb_mode": "Les équipements sont des Sonoff TRVZB. Vous devez configurer les entités dédiées dans le menu 'Configuration Sonoff TRVZB'",
|
||||
"auto_regulation_mode": "Ajustement automatique de la température cible",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-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",
|
||||
"auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
@@ -217,19 +219,19 @@
|
||||
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Auto-régulation par vanne - {name}",
|
||||
"description": "Configuration de l'auto-régulation par controle direct de la vanne",
|
||||
"sonoff_trvzb": {
|
||||
"title": "Configuration Sonoff TRVZB",
|
||||
"description": "Configuration spécifique des Sonoff TRVZB",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
|
||||
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
|
||||
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
|
||||
"offset_calibration_entity_ids": "Entités de 'Offset calibration'",
|
||||
"opening_degree_entity_ids": "Entités de 'Opening degree'",
|
||||
"closing_degree_entity_ids": "Entités de 'Closing degree'",
|
||||
"proportional_function": "Algorithme"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"offset_calibration_entity_ids": "La liste des entités 'offset calibration' entities. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'opening degree' entities. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'closing degree' entities. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
|
||||
}
|
||||
}
|
||||
@@ -272,7 +274,7 @@
|
||||
"presence": "Détection de présence",
|
||||
"advanced": "Paramètres avancés",
|
||||
"auto_start_stop": "Allumage/extinction automatique",
|
||||
"valve_regulation": "Configuration de la regulation par vanne",
|
||||
"sonoff_trvzb": "Configuration spécifique à Sonoff TRVZB",
|
||||
"finalize": "Finaliser les modifications",
|
||||
"configuration_not_complete": "Configuration incomplète"
|
||||
}
|
||||
@@ -321,6 +323,7 @@
|
||||
"heater_keep_alive": "keep-alive (sec)",
|
||||
"proportional_function": "Algorithme",
|
||||
"ac_mode": "AC mode ?",
|
||||
"sonoff_trvzb_mode": "Mode Sonoff TRVZB",
|
||||
"auto_regulation_mode": "Auto-régulation",
|
||||
"auto_regulation_dtemp": "Seuil de régulation",
|
||||
"auto_regulation_periode_min": "Période minimale de régulation",
|
||||
@@ -333,8 +336,9 @@
|
||||
"heater_keep_alive": "Intervalle de rafraichissement du switch en secondes. Laisser vide pour désactiver. À n'utiliser que pour les switchs qui le nécessite.",
|
||||
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
|
||||
"ac_mode": "Utilisation du mode Air Conditionné (AC)",
|
||||
"auto_regulation_mode": "Utilisation de l'auto-régulation faite par VTherm",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les vannes) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"sonoff_trvzb_mode": "Les équipements sont des Sonoff TRVZB. Vous devez configurer les entités dédiées dans le menu 'Configuration Sonoff TRVZB'",
|
||||
"auto_regulation_mode": "Ajustement automatique de la température cible",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-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",
|
||||
"auto_regulation_use_device_temp": "Compenser la temperature interne du sous-jacent pour accélérer l'auto-régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
@@ -455,19 +459,19 @@
|
||||
"central_boiler_deactivation_service": "Commande à éxecuter pour étiendre la chaudière centrale au format entity_id/service_name[/attribut:valeur]"
|
||||
}
|
||||
},
|
||||
"valve_regulation": {
|
||||
"title": "Auto-régulation par vanne - {name}",
|
||||
"description": "Configuration de l'auto-régulation par controle direct de la vanne",
|
||||
"sonoff_trvzb": {
|
||||
"title": "Configuration Sonoff TRVZB - {name}",
|
||||
"description": "Configuration spécifique des Sonoff TRVZB",
|
||||
"data": {
|
||||
"offset_calibration_entity_ids": "Entités de 'calibrage du décalage''",
|
||||
"opening_degree_entity_ids": "Entités 'ouverture de vanne'",
|
||||
"closing_degree_entity_ids": "Entités 'fermeture de la vanne'",
|
||||
"offset_calibration_entity_ids": "Entités de 'Offset calibration'",
|
||||
"opening_degree_entity_ids": "Entités de 'Opening degree'",
|
||||
"closing_degree_entity_ids": "Entités de 'Closing degree'",
|
||||
"proportional_function": "Algorithme"
|
||||
},
|
||||
"data_description": {
|
||||
"offset_calibration_entity_ids": "La liste des entités 'calibrage du décalage' (offset calibration). Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'ouverture de vanne'. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'fermeture de la vanne'. Configurez le si votre TRV possède cette fonction pour une meilleure régulation. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"offset_calibration_entity_ids": "La liste des entités 'offset calibration' entities. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"opening_degree_entity_ids": "La liste des entités 'opening degree' entities. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"closing_degree_entity_ids": "La liste des entités 'closing degree' entities. Il doit y en avoir une par entité climate sous-jacente",
|
||||
"proportional_function": "Algorithme à utiliser (seulement TPI est disponible)"
|
||||
}
|
||||
}
|
||||
@@ -478,7 +482,7 @@
|
||||
"window_open_detection_method": "Une seule méthode de détection des ouvertures ouvertes doit être utilisée. Utilisez le détecteur d'ouverture ou les seuils de température mais pas les deux.",
|
||||
"no_central_config": "Vous ne pouvez pas cocher 'Utiliser la configuration centrale' car aucune configuration centrale n'a été trouvée. Vous devez créer un Versatile Thermostat de type 'Central Configuration' pour pouvoir l'utiliser.",
|
||||
"service_configuration_format": "Mauvais format de la configuration du service",
|
||||
"valve_regulation_nb_entities_incorrect": "Le nombre d'entités pour la régulation par vanne doit être égal au nombre d'entité sous-jacentes"
|
||||
"sonoff_trvzb_nb_entities_incorrect": "Le nombre d'entités spécifiques au Sonoff TRVZB doit être égal au nombre d'entité sous-jacentes"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
@@ -500,8 +504,7 @@
|
||||
"auto_regulation_medium": "Moyenne",
|
||||
"auto_regulation_light": "Légère",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "Aucune",
|
||||
"auto_regulation_valve": "Contrôle direct de la vanne"
|
||||
"auto_regulation_none": "Aucune"
|
||||
}
|
||||
},
|
||||
"auto_fan_mode": {
|
||||
|
||||
@@ -53,8 +53,8 @@ class UnderlyingEntityType(StrEnum):
|
||||
# a valve
|
||||
VALVE = "valve"
|
||||
|
||||
# a direct valve regulation
|
||||
VALVE_REGULATION = "valve_regulation"
|
||||
# a Sonoff TRVZB
|
||||
SONOFF_TRVZB = "sonoff_trvzb"
|
||||
|
||||
|
||||
class UnderlyingEntity:
|
||||
@@ -871,11 +871,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
_last_sent_temperature = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
thermostat: Any,
|
||||
valve_entity_id: str,
|
||||
entity_type: UnderlyingEntityType = UnderlyingEntityType.VALVE,
|
||||
self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str
|
||||
) -> None:
|
||||
"""Initialize the underlying valve"""
|
||||
|
||||
@@ -924,7 +920,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
|
||||
async def turn_on(self):
|
||||
"""Nothing to do for Valve because it cannot be turned on"""
|
||||
await self.set_valve_open_percent()
|
||||
self.set_valve_open_percent()
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
"""Set the HVACmode. Returns true if something have change"""
|
||||
@@ -962,8 +958,11 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
force=False,
|
||||
):
|
||||
"""We use this function to change the on_percent"""
|
||||
# if force:
|
||||
await self.set_valve_open_percent()
|
||||
if force:
|
||||
# self._percent_open = self.cap_sent_value(self._percent_open)
|
||||
# await self.send_percent_open()
|
||||
# avoid to send 2 times the same value at startup
|
||||
self.set_valve_open_percent()
|
||||
|
||||
@overrides
|
||||
def cap_sent_value(self, value) -> float:
|
||||
@@ -996,7 +995,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
|
||||
return new_value
|
||||
|
||||
async def set_valve_open_percent(self):
|
||||
def set_valve_open_percent(self):
|
||||
"""Update the valve open percent"""
|
||||
caped_val = self.cap_sent_value(self._thermostat.valve_open_percent)
|
||||
if self._percent_open == caped_val:
|
||||
@@ -1010,16 +1009,15 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
"%s - Setting valve ouverture percent to %s", self, self._percent_open
|
||||
)
|
||||
# Send the change to the valve, in background
|
||||
# self._hass.create_task(self.send_percent_open())
|
||||
await self.send_percent_open()
|
||||
self._hass.create_task(self.send_percent_open())
|
||||
|
||||
def remove_entity(self):
|
||||
"""Remove the entity after stopping its cycle"""
|
||||
self._cancel_cycle()
|
||||
|
||||
|
||||
class UnderlyingValveRegulation(UnderlyingValve):
|
||||
"""A specific underlying class for Valve regulation"""
|
||||
class UnderlyingSonoffTRVZB(UnderlyingValve):
|
||||
"""A specific underlying class for Sonoff TRVZB TRV"""
|
||||
|
||||
_offset_calibration_entity_id: str
|
||||
_opening_degree_entity_id: str
|
||||
@@ -1034,13 +1032,8 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
closing_degree_entity_id: str,
|
||||
climate_underlying: UnderlyingClimate,
|
||||
) -> None:
|
||||
"""Initialize the underlying TRV with valve regulation"""
|
||||
super().__init__(
|
||||
hass,
|
||||
thermostat,
|
||||
opening_degree_entity_id,
|
||||
entity_type=UnderlyingEntityType.VALVE_REGULATION,
|
||||
)
|
||||
"""Initialize the underlying Sonoff TRV"""
|
||||
super().__init__(hass, thermostat, opening_degree_entity_id)
|
||||
self._offset_calibration_entity_id = offset_calibration_entity_id
|
||||
self._opening_degree_entity_id = opening_degree_entity_id
|
||||
self._closing_degree_entity_id = closing_degree_entity_id
|
||||
@@ -1059,21 +1052,17 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
self._max_opening_degree = self._hass.states.get(
|
||||
self._opening_degree_entity_id
|
||||
).attributes.get("max")
|
||||
self._min_offset_calibration = self._hass.states.get(
|
||||
self._offset_calibration_entity_id
|
||||
).attributes.get("min")
|
||||
self._max_offset_calibration = self._hass.states.get(
|
||||
self._offset_calibration_entity_id
|
||||
).attributes.get("max")
|
||||
|
||||
if self.have_offset_calibration_entity:
|
||||
self._min_offset_calibration = self._hass.states.get(
|
||||
self._offset_calibration_entity_id
|
||||
).attributes.get("min")
|
||||
self._max_offset_calibration = self._hass.states.get(
|
||||
self._offset_calibration_entity_id
|
||||
).attributes.get("max")
|
||||
|
||||
self._is_min_max_initialized = self._max_opening_degree is not None and (
|
||||
not self.have_offset_calibration_entity
|
||||
or (
|
||||
self._min_offset_calibration is not None
|
||||
and self._max_offset_calibration is not None
|
||||
)
|
||||
self._is_min_max_initialized = (
|
||||
self._max_opening_degree is not None
|
||||
and self._min_offset_calibration is not None
|
||||
and self._max_offset_calibration is not None
|
||||
)
|
||||
|
||||
if not self._is_min_max_initialized:
|
||||
@@ -1087,7 +1076,7 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
|
||||
# Send closing_degree if set
|
||||
closing_degree = None
|
||||
if self.have_closing_degree_entity:
|
||||
if self._closing_degree_entity_id is not None:
|
||||
await self._send_value_to_number(
|
||||
self._closing_degree_entity_id,
|
||||
closing_degree := self._max_opening_degree - self._percent_open,
|
||||
@@ -1095,7 +1084,7 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
|
||||
# send offset_calibration to the difference between target temp and local temp
|
||||
offset = None
|
||||
if self.have_offset_calibration_entity:
|
||||
if self._offset_calibration_entity_id is not None:
|
||||
if (
|
||||
(local_temp := self._climate_underlying.underlying_current_temperature)
|
||||
is not None
|
||||
@@ -1120,7 +1109,7 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - valve regulation - I have sent offset_calibration=%s opening_degree=%s closing_degree=%s",
|
||||
"%s - SonoffTRVZB - I have sent offset_calibration=%s opening_degree=%s closing_degree=%s",
|
||||
self,
|
||||
offset,
|
||||
self._percent_open,
|
||||
@@ -1142,16 +1131,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
"""The offset_calibration_entity_id"""
|
||||
return self._closing_degree_entity_id
|
||||
|
||||
@property
|
||||
def have_closing_degree_entity(self) -> bool:
|
||||
"""Return True if the underlying have a closing_degree entity"""
|
||||
return self._closing_degree_entity_id is not None
|
||||
|
||||
@property
|
||||
def have_offset_calibration_entity(self) -> bool:
|
||||
"""Return True if the underlying have a offset_calibration entity"""
|
||||
return self._offset_calibration_entity_id is not None
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Get the hvac_modes"""
|
||||
@@ -1159,19 +1138,6 @@ class UnderlyingValveRegulation(UnderlyingValve):
|
||||
return []
|
||||
return [HVACMode.OFF, HVACMode.HEAT]
|
||||
|
||||
@overrides
|
||||
async def start_cycle(
|
||||
self,
|
||||
hvac_mode: HVACMode,
|
||||
_1,
|
||||
_2,
|
||||
_3,
|
||||
force=False,
|
||||
):
|
||||
"""We use this function to change the on_percent"""
|
||||
# if force:
|
||||
await self.set_valve_open_percent()
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
"""If the opening valve is open."""
|
||||
|
||||
+6
-69
@@ -3,7 +3,6 @@
|
||||
""" Some common resources """
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict, Callable
|
||||
from unittest.mock import patch, MagicMock # pylint: disable=unused-import
|
||||
import pytest # pylint: disable=unused-import
|
||||
|
||||
@@ -31,6 +30,10 @@ 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 ( # pylint: disable=unused-import
|
||||
get_tz,
|
||||
NowClass,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
|
||||
@@ -1004,50 +1007,12 @@ async def set_climate_preset_temp(
|
||||
)
|
||||
|
||||
|
||||
# The temperatures to set
|
||||
default_temperatures_ac_away = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
"eco_ac": 27.0,
|
||||
"comfort_ac": 25.0,
|
||||
"boost_ac": 23.0,
|
||||
"frost_away": 7.1,
|
||||
"eco_away": 17.1,
|
||||
"comfort_away": 17.2,
|
||||
"boost_away": 17.3,
|
||||
"eco_ac_away": 27.1,
|
||||
"comfort_ac_away": 25.1,
|
||||
"boost_ac_away": 23.1,
|
||||
}
|
||||
|
||||
default_temperatures_away = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
"frost_away": 7.1,
|
||||
"eco_away": 17.1,
|
||||
"comfort_away": 17.2,
|
||||
"boost_away": 17.3,
|
||||
}
|
||||
|
||||
default_temperatures = {
|
||||
"frost": 7.0,
|
||||
"eco": 17.0,
|
||||
"comfort": 19.0,
|
||||
"boost": 21.0,
|
||||
}
|
||||
|
||||
|
||||
async def set_all_climate_preset_temp(
|
||||
hass, vtherm: BaseThermostat, temps: dict | None, number_entity_base_name: str
|
||||
hass, vtherm: BaseThermostat, temps: dict, number_entity_base_name: str
|
||||
):
|
||||
"""Initialize all temp of preset for a VTherm entity"""
|
||||
local_temps = temps if temps is not None else default_temperatures
|
||||
# We initialize
|
||||
for preset_name, value in local_temps.items():
|
||||
for preset_name, value in temps.items():
|
||||
|
||||
await set_climate_preset_temp(vtherm, preset_name, value)
|
||||
|
||||
@@ -1063,31 +1028,3 @@ async def set_all_climate_preset_temp(
|
||||
assert temp_entity
|
||||
# Because set_value is not implemented in Number class (really don't understand why...)
|
||||
assert temp_entity.state == value
|
||||
|
||||
|
||||
#
|
||||
# Side effects management
|
||||
#
|
||||
SideEffectDict = Dict[str, Any]
|
||||
|
||||
|
||||
class SideEffects:
|
||||
"""A class to manage sideEffects for mock"""
|
||||
|
||||
def __init__(self, side_effects: SideEffectDict, default_side_effect: Any):
|
||||
"""Initialise the side effects"""
|
||||
self._current_side_effects: SideEffectDict = side_effects
|
||||
self._default_side_effect: Any = default_side_effect
|
||||
|
||||
def get_side_effects(self) -> Callable[[str], Any]:
|
||||
"""returns the method which apply the side effects"""
|
||||
|
||||
def side_effect_method(arg) -> Any:
|
||||
"""Search a side effect definition and return it"""
|
||||
return self._current_side_effects.get(arg, self._default_side_effect)
|
||||
|
||||
return side_effect_method
|
||||
|
||||
def add_or_update_side_effect(self, key: str, new_value: Any):
|
||||
"""Update the value of a side effect"""
|
||||
self._current_side_effects[key] = new_value
|
||||
|
||||
@@ -46,7 +46,7 @@ async def test_over_climate_regulation(
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
@@ -87,7 +87,7 @@ async def test_over_climate_regulation(
|
||||
# set manual target temp (at now - 7) -> the regulation should occurs
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
@@ -108,7 +108,7 @@ async def test_over_climate_regulation(
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 23, event_timestamp)
|
||||
@@ -144,7 +144,7 @@ async def test_over_climate_regulation_ac_mode(
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
@@ -183,7 +183,7 @@ async def test_over_climate_regulation_ac_mode(
|
||||
# set manual target temp
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=25)
|
||||
@@ -204,7 +204,7 @@ async def test_over_climate_regulation_ac_mode(
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 26, event_timestamp)
|
||||
@@ -219,7 +219,7 @@ async def test_over_climate_regulation_ac_mode(
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
@@ -260,7 +260,7 @@ async def test_over_climate_regulation_limitations(
|
||||
event_timestamp = now - timedelta(minutes=20)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
@@ -286,61 +286,71 @@ async def test_over_climate_regulation_limitations(
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.is_regulated is True
|
||||
|
||||
entity._set_now(event_timestamp)
|
||||
# Will initialize the _last_regulation_change
|
||||
# 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
|
||||
await entity.async_set_temperature(temperature=17)
|
||||
|
||||
# it is cold today
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
# 1. set manual target temp (at now - 19) -> the regulation should be ignored because too early
|
||||
# set manual target temp (at now - 19) -> the regulation should be ignored because too early
|
||||
event_timestamp = now - timedelta(minutes=19)
|
||||
entity._set_now(event_timestamp)
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
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
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# the regulated temperature will not change because when we set temp manually it is forced
|
||||
assert entity.regulated_target_temp == 17 # 19.5
|
||||
# the regulated temperature will change because when we set temp manually it is forced
|
||||
assert entity.regulated_target_temp == 19.5
|
||||
|
||||
# 2. set manual target temp (at now - 18) -> the regulation should be taken into account
|
||||
# set manual target temp (at now - 18) -> the regulation should be taken into account
|
||||
event_timestamp = now - timedelta(minutes=18)
|
||||
entity._set_now(event_timestamp)
|
||||
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
|
||||
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
|
||||
old_regulated_temp = entity.regulated_target_temp
|
||||
|
||||
await entity.async_set_temperature(temperature=17)
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert (
|
||||
entity.regulated_target_temp == 18 + 0
|
||||
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
|
||||
old_regulated_temp = entity.regulated_target_temp
|
||||
|
||||
# 3. change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
|
||||
# change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=15)
|
||||
entity._set_now(event_timestamp)
|
||||
await send_temperature_change_event(entity, 16, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
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
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp <= old_regulated_temp
|
||||
|
||||
# 4. change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
|
||||
# change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=12)
|
||||
entity._set_now(event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 12, event_timestamp)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, 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.5 # 0.7 without round_to_nearest
|
||||
# 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.5
|
||||
) # 0.7 without round_to_nearest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@@ -373,7 +383,7 @@ async def test_over_climate_regulation_use_device_temp(
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
@@ -406,7 +416,7 @@ async def test_over_climate_regulation_use_device_temp(
|
||||
fake_underlying_climate.set_current_temperature(15)
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await entity.async_set_temperature(temperature=16)
|
||||
@@ -452,7 +462,7 @@ async def test_over_climate_regulation_use_device_temp(
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
@@ -487,7 +497,7 @@ async def test_over_climate_regulation_use_device_temp(
|
||||
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(entity, 25, event_timestamp)
|
||||
@@ -535,7 +545,7 @@ async def test_over_climate_regulation_dtemp_null(
|
||||
event_timestamp = now - timedelta(minutes=20)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
@@ -563,7 +573,7 @@ async def test_over_climate_regulation_dtemp_null(
|
||||
# set manual target temp
|
||||
event_timestamp = now - timedelta(minutes=17)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=20)
|
||||
@@ -584,7 +594,7 @@ async def test_over_climate_regulation_dtemp_null(
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=15)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
@@ -597,7 +607,7 @@ async def test_over_climate_regulation_dtemp_null(
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=13)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 20, event_timestamp)
|
||||
@@ -611,7 +621,7 @@ async def test_over_climate_regulation_dtemp_null(
|
||||
# Test if a small temperature change is taken into account : change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.const.NowClass.get_now",
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 19.6, event_timestamp)
|
||||
|
||||
+18
-10
@@ -161,6 +161,19 @@ async def test_bug_272(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# 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 = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
@@ -202,18 +215,16 @@ async def test_bug_272(
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
event_timestamp: datetime = datetime.now(tz=tz)
|
||||
entity._set_now(now)
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
# Set room temperature to something very cold
|
||||
await send_temperature_change_event(entity, 13, now)
|
||||
await send_ext_temperature_change_event(entity, 9, now)
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
|
||||
event_timestamp = event_timestamp + timedelta(minutes=3)
|
||||
entity._set_now(event_timestamp)
|
||||
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
# Not in the accepted interval (15-19)
|
||||
await entity.async_set_temperature(temperature=10)
|
||||
@@ -237,15 +248,12 @@ async def test_bug_272(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
# Set room temperature to something very cold
|
||||
event_timestamp = event_timestamp + timedelta(minutes=1)
|
||||
entity._set_now(event_timestamp)
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
|
||||
await send_temperature_change_event(entity, 13, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 9, event_timestamp)
|
||||
|
||||
# In the accepted interval
|
||||
event_timestamp = event_timestamp + timedelta(minutes=3)
|
||||
entity._set_now(event_timestamp)
|
||||
await entity.async_set_temperature(temperature=20.8)
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
|
||||
+4
-339
@@ -1,6 +1,7 @@
|
||||
# pylint: disable=unused-argument, line-too-long, too-many-lines
|
||||
# pylint: disable=unused-argument, line-too-long
|
||||
""" Test the Versatile Thermostat config flow """
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntry
|
||||
@@ -516,7 +517,7 @@ async def test_user_config_flow_over_climate(
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
|
||||
CONF_USED_BY_CENTRAL_BOILER: False,
|
||||
CONF_USE_CENTRAL_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_SONOFF_TRZB_MODE: False,
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
@@ -1126,7 +1127,7 @@ async def test_user_config_flow_over_climate_auto_start_stop(
|
||||
CONF_USED_BY_CENTRAL_BOILER: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: True,
|
||||
CONF_AUTO_START_STOP_LEVEL: AUTO_START_STOP_LEVEL_MEDIUM,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_SONOFF_TRZB_MODE: False,
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
@@ -1385,339 +1386,3 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
|
||||
assert result["result"].version == 2
|
||||
assert result["result"].title == "TheOverSwitchMockName"
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
# @pytest.mark.skip
|
||||
async def test_user_config_flow_over_climate_valve(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the config flow with all thermostat_over_climate with the valve regulation activated.
|
||||
We don't use any features nor central config
|
||||
but we will add multiple underlying climate and valve"""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
# 1. Type
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result["menu_options"] == [
|
||||
"main",
|
||||
"features",
|
||||
"type",
|
||||
"presets",
|
||||
"advanced",
|
||||
"configuration_not_complete",
|
||||
]
|
||||
assert result.get("errors") is None
|
||||
|
||||
# 2. Main
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "main"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "main"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_CENTRAL_MODE: False,
|
||||
# Keep default values which are False
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "main"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
# 3. Main 2
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
# Keep default values which are False
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result.get("errors") is None
|
||||
|
||||
# 4. Type
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "type"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "type"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate1", "climate.mock_climate2"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result["menu_options"] == [
|
||||
"main",
|
||||
"features",
|
||||
"type",
|
||||
"tpi",
|
||||
"presets",
|
||||
"valve_regulation",
|
||||
"advanced",
|
||||
"configuration_not_complete",
|
||||
# "finalize", # because we need Advanced default parameters
|
||||
]
|
||||
assert result.get("errors") is None
|
||||
|
||||
# 5. TPI
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "tpi"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
# 6. TPI 2
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_TPI_CENTRAL_CONFIG: False}
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "tpi"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
# 7. Menu
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
)
|
||||
|
||||
# 8. Presets
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "presets"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "presets"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_USE_PRESETS_CENTRAL_CONFIG: False}
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result.get("errors") is None
|
||||
|
||||
# 9. Features
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "features"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "features"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: False,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result.get("errors") is None
|
||||
assert result["menu_options"] == [
|
||||
"main",
|
||||
"features",
|
||||
"type",
|
||||
"tpi",
|
||||
"presets",
|
||||
"valve_regulation",
|
||||
"advanced",
|
||||
"configuration_not_complete",
|
||||
# "finalize", finalize is not present waiting for advanced configuration
|
||||
]
|
||||
|
||||
# 11. Valve_regulation
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "valve_regulation"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "valve_regulation"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
# 11.1 Only one but 2 expected
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_OFFSET_CALIBRATION_LIST: ["number.offset_calibration1"],
|
||||
CONF_OPENING_DEGREE_LIST: ["number.opening_degree1"],
|
||||
CONF_CLOSING_DEGREE_LIST: ["number.closing_degree1"],
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "valve_regulation"
|
||||
assert result.get("errors") == {"base": "valve_regulation_nb_entities_incorrect"}
|
||||
|
||||
# 11.2 Give two openings but only one offset_calibration
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_OFFSET_CALIBRATION_LIST: [
|
||||
"number.offset_calibration1",
|
||||
"number.offset_calibration2",
|
||||
],
|
||||
CONF_OPENING_DEGREE_LIST: [
|
||||
"number.opening_degree1",
|
||||
"number.opening_degree2",
|
||||
],
|
||||
CONF_CLOSING_DEGREE_LIST: ["number.closing_degree1"],
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "valve_regulation"
|
||||
assert result.get("errors") == {"base": "valve_regulation_nb_entities_incorrect"}
|
||||
|
||||
# 11.3 Give two openings and 2 calibration and 0 closing
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_OFFSET_CALIBRATION_LIST: [
|
||||
"number.offset_calibration1",
|
||||
"number.offset_calibration2",
|
||||
],
|
||||
CONF_OPENING_DEGREE_LIST: [
|
||||
"number.opening_degree1",
|
||||
"number.opening_degree2",
|
||||
],
|
||||
CONF_CLOSING_DEGREE_LIST: [],
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result.get("errors") is None
|
||||
assert result["menu_options"] == [
|
||||
"main",
|
||||
"features",
|
||||
"type",
|
||||
"tpi",
|
||||
"presets",
|
||||
"valve_regulation",
|
||||
"advanced",
|
||||
"configuration_not_complete",
|
||||
# "finalize", finalize is not present waiting for advanced configuration
|
||||
]
|
||||
|
||||
# 10. Advanced
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "advanced"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_USE_ADVANCED_CENTRAL_CONFIG: False},
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "advanced"
|
||||
assert result.get("errors") == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "menu"
|
||||
assert result.get("errors") is None
|
||||
assert result["menu_options"] == [
|
||||
"main",
|
||||
"features",
|
||||
"type",
|
||||
"tpi",
|
||||
"presets",
|
||||
"valve_regulation",
|
||||
"advanced",
|
||||
"finalize", # Now it is complete
|
||||
]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "finalize"}
|
||||
)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result.get("errors") is None
|
||||
assert result[
|
||||
"data"
|
||||
] == MOCK_TH_OVER_CLIMATE_USER_CONFIG | MOCK_TH_OVER_CLIMATE_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG | MOCK_TH_OVER_CLIMATE_TYPE_CONFIG | {
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 10,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.4,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.3,
|
||||
} | MOCK_DEFAULT_FEATURE_CONFIG | {
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_AUTO_START_STOP_FEATURE: False,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE: False,
|
||||
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||
CONF_USE_MOTION_CENTRAL_CONFIG: False,
|
||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: False,
|
||||
CONF_USED_BY_CENTRAL_BOILER: False,
|
||||
CONF_USE_CENTRAL_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate1", "climate.mock_climate2"],
|
||||
CONF_OPENING_DEGREE_LIST: ["number.opening_degree1", "number.opening_degree2"],
|
||||
CONF_CLOSING_DEGREE_LIST: [],
|
||||
CONF_OFFSET_CALIBRATION_LIST: [
|
||||
"number.offset_calibration1",
|
||||
"number.offset_calibration2",
|
||||
],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
assert result["result"].version == 2
|
||||
assert result["result"].title == "TheOverClimateMockName"
|
||||
assert isinstance(result["result"], ConfigEntry)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
|
||||
|
||||
""" Test the over_climate Vtherm """
|
||||
""" Test the Window management """
|
||||
from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -517,9 +517,6 @@ async def test_bug_508(
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# Min_temp is 10 and max_temp is 31 and features contains TARGET_TEMPERATURE_RANGE
|
||||
fake_underlying_climate = MagicMockClimateWithTemperatureRange()
|
||||
|
||||
@@ -548,9 +545,6 @@ async def test_bug_508(
|
||||
# Set the hvac_mode to HEAT
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
now = now + timedelta(minutes=3) # avoid temporal filter
|
||||
entity._set_now(now)
|
||||
|
||||
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
|
||||
await entity.async_set_temperature(temperature=8.5)
|
||||
|
||||
@@ -574,9 +568,6 @@ async def test_bug_508(
|
||||
|
||||
with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
|
||||
now = now + timedelta(minutes=3) # avoid temporal filter
|
||||
entity._set_now(now)
|
||||
|
||||
await entity.async_set_temperature(temperature=32)
|
||||
|
||||
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
|
||||
|
||||
@@ -1,475 +0,0 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
|
||||
|
||||
""" Test the over_climate with valve regulation """
|
||||
from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_climate_valve import (
|
||||
ThermostatOverClimateValve,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
from .const import *
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get):
|
||||
"""Test the normal full start of a thermostat in thermostat_over_climate type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_CENTRAL_MODE: False,
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
CONF_OPENING_DEGREE_LIST: ["number.mock_opening_degree"],
|
||||
CONF_CLOSING_DEGREE_LIST: ["number.mock_closing_degree"],
|
||||
CONF_OFFSET_CALIBRATION_LIST: ["number.mock_offset_calibration"],
|
||||
}
|
||||
| MOCK_DEFAULT_FEATURE_CONFIG
|
||||
| MOCK_DEFAULT_CENTRAL_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG,
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
|
||||
|
||||
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
|
||||
|
||||
mock_get_state_side_effect = SideEffects(
|
||||
{
|
||||
"number.mock_opening_degree": State(
|
||||
"number.mock_opening_degree", "0", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_closing_degree": State(
|
||||
"number.mock_closing_degree", "0", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_offset_calibration": State(
|
||||
"number.mock_offset_calibration", "0", {"min": -12, "max": 12}
|
||||
),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
|
||||
# 1. initialize the VTherm
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", return_value=fake_underlying_climate) as mock_find_climate, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
|
||||
vtherm: ThermostatOverClimateValve = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert vtherm
|
||||
vtherm._set_now(now)
|
||||
assert isinstance(vtherm, ThermostatOverClimateValve)
|
||||
|
||||
assert vtherm.name == "TheOverClimateMockName"
|
||||
assert vtherm.is_over_climate is True
|
||||
assert vtherm.have_valve_regulation is True
|
||||
|
||||
assert vtherm.hvac_action is HVACAction.OFF
|
||||
assert vtherm.hvac_mode is HVACMode.OFF
|
||||
assert vtherm.target_temperature == vtherm.min_temp
|
||||
assert vtherm.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert vtherm.preset_mode is PRESET_NONE
|
||||
assert vtherm._security_state is False
|
||||
assert vtherm._window_state is None
|
||||
assert vtherm._motion_state is None
|
||||
assert vtherm._presence_state is None
|
||||
|
||||
assert vtherm.is_device_active is False
|
||||
assert vtherm.valve_open_percent == 0
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
mock_find_climate.assert_called_once()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_vtherm()])
|
||||
|
||||
# the underlying set temperature call but no call to valve yet because VTherm is off
|
||||
assert mock_service_call.call_count == 3
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call("climate","set_temperature",{
|
||||
"entity_id": "climate.mock_climate",
|
||||
"temperature": 15, # temp-min
|
||||
},
|
||||
),
|
||||
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
|
||||
# we have no current_temperature yet
|
||||
# call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration'}),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_get_state.call_count > 5 # each temp sensor + each valve
|
||||
|
||||
|
||||
# initialize the temps
|
||||
await set_all_climate_preset_temp(hass, vtherm, None, "theoverclimatemockname")
|
||||
|
||||
await send_temperature_change_event(vtherm, 18, now, True)
|
||||
await send_ext_temperature_change_event(vtherm, 18, now, True)
|
||||
|
||||
# 2. Starts heating slowly (18 vs 19)
|
||||
now = now + timedelta(minutes=1)
|
||||
vtherm._set_now(now)
|
||||
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
now = now + timedelta(minutes=2) # avoid temporal filter
|
||||
vtherm._set_now(now)
|
||||
|
||||
await vtherm.async_set_preset_mode(PRESET_COMFORT)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.hvac_mode is HVACMode.HEAT
|
||||
assert vtherm.preset_mode is PRESET_COMFORT
|
||||
assert vtherm.target_temperature == 19
|
||||
assert vtherm.current_temperature == 18
|
||||
assert vtherm.valve_open_percent == 40 # 0.3*1 + 0.1*1
|
||||
|
||||
|
||||
assert mock_service_call.call_count == 4
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate', 'temperature': 19.0}),
|
||||
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree'}),
|
||||
# 3 = 18 (room) - 15 (current of underlying) + 0 (current offset)
|
||||
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration'})
|
||||
]
|
||||
)
|
||||
|
||||
# set the opening to 40%
|
||||
mock_get_state_side_effect.add_or_update_side_effect(
|
||||
"number.mock_opening_degree",
|
||||
State(
|
||||
"number.mock_opening_degree", "40", {"min": 0, "max": 100}
|
||||
))
|
||||
|
||||
assert vtherm.hvac_action is HVACAction.HEATING
|
||||
assert vtherm.is_device_active is True
|
||||
|
||||
# 2. Starts heating very slowly (18.9 vs 19)
|
||||
now = now + timedelta(minutes=2)
|
||||
vtherm._set_now(now)
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
# set the offset to 3
|
||||
mock_get_state_side_effect.add_or_update_side_effect(
|
||||
"number.mock_offset_calibration",
|
||||
State(
|
||||
"number.mock_offset_calibration", "3", {"min": -12, "max": 12}
|
||||
))
|
||||
|
||||
await send_temperature_change_event(vtherm, 18.9, now, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.hvac_mode is HVACMode.HEAT
|
||||
assert vtherm.preset_mode is PRESET_COMFORT
|
||||
assert vtherm.target_temperature == 19
|
||||
assert vtherm.current_temperature == 18.9
|
||||
assert vtherm.valve_open_percent == 13 # 0.3*0.1 + 0.1*1
|
||||
|
||||
|
||||
assert mock_service_call.call_count == 3
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call(domain='number', service='set_value', service_data={'value': 13}, target={'entity_id': 'number.mock_opening_degree'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 87}, target={'entity_id': 'number.mock_closing_degree'}),
|
||||
# 6 = 18 (room) - 15 (current of underlying) + 3 (current offset)
|
||||
call(domain='number', service='set_value', service_data={'value': 6.899999999999999}, target={'entity_id': 'number.mock_offset_calibration'})
|
||||
]
|
||||
)
|
||||
|
||||
# set the opening to 13%
|
||||
mock_get_state_side_effect.add_or_update_side_effect(
|
||||
"number.mock_opening_degree",
|
||||
State(
|
||||
"number.mock_opening_degree", "13", {"min": 0, "max": 100}
|
||||
))
|
||||
|
||||
assert vtherm.hvac_action is HVACAction.HEATING
|
||||
assert vtherm.is_device_active is True
|
||||
|
||||
# 3. Stop heating 21 > 19
|
||||
now = now + timedelta(minutes=2)
|
||||
vtherm._set_now(now)
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
# set the offset to 3
|
||||
mock_get_state_side_effect.add_or_update_side_effect(
|
||||
"number.mock_offset_calibration",
|
||||
State(
|
||||
"number.mock_offset_calibration", "3", {"min": -12, "max": 12}
|
||||
))
|
||||
|
||||
await send_temperature_change_event(vtherm, 21, now, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.hvac_mode is HVACMode.HEAT
|
||||
assert vtherm.preset_mode is PRESET_COMFORT
|
||||
assert vtherm.target_temperature == 19
|
||||
assert vtherm.current_temperature == 21
|
||||
assert vtherm.valve_open_percent == 0 # 0.3* (-2) + 0.1*1
|
||||
|
||||
|
||||
assert mock_service_call.call_count == 3
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree'}),
|
||||
# 6 = 18 (room) - 15 (current of underlying) + 3 (current offset)
|
||||
call(domain='number', service='set_value', service_data={'value': 9.0}, target={'entity_id': 'number.mock_offset_calibration'})
|
||||
]
|
||||
)
|
||||
|
||||
# set the opening to 13%
|
||||
mock_get_state_side_effect.add_or_update_side_effect(
|
||||
"number.mock_opening_degree",
|
||||
State(
|
||||
"number.mock_opening_degree", "0", {"min": 0, "max": 100}
|
||||
))
|
||||
|
||||
assert vtherm.hvac_action is HVACAction.OFF
|
||||
assert vtherm.is_device_active is False
|
||||
|
||||
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_over_climate_valve_multi_presence(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
):
|
||||
"""Test the normal full start of a thermostat in thermostat_over_climate type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||
CONF_USE_CENTRAL_MODE: False,
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_UNDERLYING_LIST: ["climate.mock_climate1", "climate.mock_climate2"],
|
||||
CONF_AC_MODE: False,
|
||||
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_VALVE,
|
||||
CONF_AUTO_REGULATION_DTEMP: 0.5,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 2,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_HIGH,
|
||||
CONF_AUTO_REGULATION_USE_DEVICE_TEMP: False,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.1,
|
||||
CONF_OPENING_DEGREE_LIST: [
|
||||
"number.mock_opening_degree1",
|
||||
"number.mock_opening_degree2",
|
||||
],
|
||||
CONF_CLOSING_DEGREE_LIST: [
|
||||
"number.mock_closing_degree1",
|
||||
"number.mock_closing_degree2",
|
||||
],
|
||||
CONF_OFFSET_CALIBRATION_LIST: [
|
||||
"number.mock_offset_calibration1",
|
||||
"number.mock_offset_calibration2",
|
||||
],
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
}
|
||||
| MOCK_DEFAULT_CENTRAL_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG,
|
||||
)
|
||||
|
||||
fake_underlying_climate1 = MockClimate(
|
||||
hass, "mockUniqueId1", "MockClimateName1", {}
|
||||
)
|
||||
fake_underlying_climate2 = MockClimate(
|
||||
hass, "mockUniqueId2", "MockClimateName2", {}
|
||||
)
|
||||
|
||||
# mock_get_state will be called for each OPENING/CLOSING/OFFSET_CALIBRATION list
|
||||
mock_get_state_side_effect = SideEffects(
|
||||
{
|
||||
# Valve 1 is open
|
||||
"number.mock_opening_degree1": State(
|
||||
"number.mock_opening_degree1", "10", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_closing_degree1": State(
|
||||
"number.mock_closing_degree1", "90", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_offset_calibration1": State(
|
||||
"number.mock_offset_calibration1", "0", {"min": -12, "max": 12}
|
||||
),
|
||||
# Valve 2 is closed
|
||||
"number.mock_opening_degree2": State(
|
||||
"number.mock_opening_degree2", "0", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_closing_degree2": State(
|
||||
"number.mock_closing_degree2", "100", {"min": 0, "max": 100}
|
||||
),
|
||||
"number.mock_offset_calibration2": State(
|
||||
"number.mock_offset_calibration2", "10", {"min": -12, "max": 12}
|
||||
),
|
||||
},
|
||||
State("unknown.entity_id", "unknown"),
|
||||
)
|
||||
|
||||
# 1. initialize the VTherm
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", side_effect=[fake_underlying_climate1, fake_underlying_climate2]) as mock_find_climate, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
|
||||
vtherm: ThermostatOverClimateValve = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
assert vtherm
|
||||
assert isinstance(vtherm, ThermostatOverClimateValve)
|
||||
|
||||
assert vtherm.name == "TheOverClimateMockName"
|
||||
assert vtherm.is_over_climate is True
|
||||
assert vtherm.have_valve_regulation is True
|
||||
|
||||
vtherm._set_now(now)
|
||||
|
||||
# initialize the temps
|
||||
await set_all_climate_preset_temp(hass, vtherm, default_temperatures_away, "theoverclimatemockname")
|
||||
|
||||
await send_temperature_change_event(vtherm, 18, now, True)
|
||||
await send_ext_temperature_change_event(vtherm, 18, now, True)
|
||||
await send_presence_change_event(vtherm, False, True, now)
|
||||
|
||||
await vtherm.async_set_preset_mode(PRESET_COMFORT)
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
assert vtherm.target_temperature == 17.2
|
||||
|
||||
# 2: set presence on -> should activate the valve and change target
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
now = now + timedelta(minutes=3)
|
||||
vtherm._set_now(now)
|
||||
|
||||
await send_presence_change_event(vtherm, True, False, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.is_device_active is True
|
||||
assert vtherm.valve_open_percent == 40
|
||||
|
||||
# the underlying set temperature call and the call to the valve
|
||||
assert mock_service_call.call_count == 8
|
||||
mock_service_call.assert_has_calls([
|
||||
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 19.0}),
|
||||
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 19.0}),
|
||||
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 40}, target={'entity_id': 'number.mock_opening_degree2'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 60}, target={'entity_id': 'number.mock_closing_degree2'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
|
||||
]
|
||||
)
|
||||
|
||||
# 3: set presence off -> should deactivate the valve and change target
|
||||
# fmt: off
|
||||
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||
patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call,\
|
||||
patch("homeassistant.core.StateMachine.get", side_effect=mock_get_state_side_effect.get_side_effects()) as mock_get_state:
|
||||
# fmt: on
|
||||
now = now + timedelta(minutes=3)
|
||||
vtherm._set_now(now)
|
||||
|
||||
await send_presence_change_event(vtherm, False, True, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.is_device_active is False
|
||||
assert vtherm.valve_open_percent == 0
|
||||
|
||||
# the underlying set temperature call and the call to the valve
|
||||
assert mock_service_call.call_count == 8
|
||||
mock_service_call.assert_has_calls([
|
||||
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate1', 'temperature': 17.2}),
|
||||
call('climate', 'set_temperature', {'entity_id': 'climate.mock_climate2', 'temperature': 17.2}),
|
||||
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 3.0}, target={'entity_id': 'number.mock_offset_calibration1'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 0}, target={'entity_id': 'number.mock_opening_degree2'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 100}, target={'entity_id': 'number.mock_closing_degree2'}),
|
||||
call(domain='number', service='set_value', service_data={'value': 12}, target={'entity_id': 'number.mock_offset_calibration2'})
|
||||
]
|
||||
)
|
||||
+12
-2
@@ -58,7 +58,6 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity._prop_algorithm is not None
|
||||
assert entity.have_valve_regulation is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
@@ -95,6 +94,18 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
# 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 = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -116,7 +127,6 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity._presence_state is None
|
||||
assert entity.have_valve_regulation is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
|
||||
@@ -17,7 +17,7 @@ from custom_components.versatile_thermostat.base_thermostat import BaseThermosta
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from custom_components.versatile_thermostat.const import NowClass
|
||||
from custom_components.versatile_thermostat.commons import NowClass
|
||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||
|
||||
from .commons import *
|
||||
|
||||
@@ -103,7 +103,6 @@ async def test_over_valve_full_start(
|
||||
assert entity._motion_state is None # pylint: disable=protected-access
|
||||
assert entity._presence_state is None # pylint: disable=protected-access
|
||||
assert entity._prop_algorithm is not None # pylint: disable=protected-access
|
||||
assert entity.have_valve_regulation is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
# assert mock_send_event.call_count == 2
|
||||
|
||||
Reference in New Issue
Block a user