Feature 124 add inversion pilot wire (#149)

* Add inverse switch command for ThermostatOverSwitch

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
This commit is contained in:
Jean-Marc Collin
2023-11-01 12:08:13 +01:00
committed by GitHub
parent dd7d6c97b3
commit 2ebeac30e6
16 changed files with 292 additions and 68 deletions

View File

@@ -104,6 +104,7 @@ from .const import (
CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN, CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH,
UnknownEntity, UnknownEntity,
WindowOpenDetectionMethod, WindowOpenDetectionMethod,
) )
@@ -241,6 +242,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
] ]
), ),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean, vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean,
} }
) )

View File

@@ -90,6 +90,7 @@ CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong" CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong"
CONF_AUTO_REGULATION_DTEMP="auto_regulation_dtemp" CONF_AUTO_REGULATION_DTEMP="auto_regulation_dtemp"
CONF_AUTO_REGULATION_PERIOD_MIN="auto_regulation_periode_min" CONF_AUTO_REGULATION_PERIOD_MIN="auto_regulation_periode_min"
CONF_INVERSE_SWITCH="inverse_switch_command"
CONF_PRESETS = { CONF_PRESETS = {
p: f"{p}_temp" p: f"{p}_temp"
@@ -193,7 +194,8 @@ ALL_CONF = (
CONF_VALVE_4, CONF_VALVE_4,
CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH
] ]
+ CONF_PRESETS_VALUES + CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES + CONF_PRESETS_AWAY_VALUES

View File

@@ -26,6 +26,10 @@ class PITemperatureRegulator:
self.accumulated_error:float = 0 self.accumulated_error:float = 0
self.accumulated_error_threshold:float = accumulated_error_threshold self.accumulated_error_threshold:float = accumulated_error_threshold
def reset_accumulated_error(self):
""" Reset the accumulated error """
self.accumulated_error = 0
def set_target_temp(self, target_temp): def set_target_temp(self, target_temp):
""" Set the new target_temp""" """ Set the new target_temp"""
self.target_temp = target_temp self.target_temp = target_temp

View File

@@ -41,7 +41,8 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period" "auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -60,7 +61,8 @@
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update" "auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -208,7 +210,8 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period" "auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -227,7 +230,8 @@
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update" "auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {

View File

@@ -11,6 +11,7 @@ from .const import (
CONF_HEATER_2, CONF_HEATER_2,
CONF_HEATER_3, CONF_HEATER_3,
CONF_HEATER_4, CONF_HEATER_4,
CONF_INVERSE_SWITCH,
overrides overrides
) )
@@ -34,12 +35,18 @@ class ThermostatOverSwitch(BaseThermostat):
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos) # super().__init__(hass, unique_id, name, entry_infos)
_is_inversed: bool = None
@property @property
def is_over_switch(self) -> bool: def is_over_switch(self) -> bool:
""" True if the Thermostat is over_switch""" """ True if the Thermostat is over_switch"""
return True return True
@property
def is_inversed(self) -> bool:
""" True if the switch is inversed (for pilot wire and diode)"""
return self._is_inversed is True
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, entry_infos):
""" Initialize the Thermostat""" """ Initialize the Thermostat"""
@@ -73,6 +80,7 @@ class ThermostatOverSwitch(BaseThermostat):
) )
) )
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
@overrides @overrides

View File

@@ -41,7 +41,8 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period" "auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -60,7 +61,8 @@
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update" "auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -208,7 +210,8 @@
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold", "auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period" "auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -227,7 +230,8 @@
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update" "auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {

View File

@@ -41,7 +41,8 @@
"valve_entity4_id": "4ème valve number", "valve_entity4_id": "4ème valve number",
"auto_regulation_mode": "Auto-régulation", "auto_regulation_mode": "Auto-régulation",
"auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation" "auto_regulation_periode_min": "Période minimale de régulation",
"inverse_switch_command": "Inverser la commande"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -60,7 +61,8 @@
"valve_entity4_id": "Entity id de la 4ème valve", "valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la température cible", "auto_regulation_mode": "Ajustement automatique de la température cible",
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation" "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
} }
}, },
"tpi": { "tpi": {
@@ -209,7 +211,8 @@
"valve_entity4_id": "4ème valve", "valve_entity4_id": "4ème valve",
"auto_regulation_mode": "Auto-regulation", "auto_regulation_mode": "Auto-regulation",
"auto_regulation_dtemp": "Seuil de régulation", "auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation" "auto_regulation_periode_min": "Période minimale de régulation",
"inverse_switch_command": "Inverser la commande"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -228,7 +231,8 @@
"valve_entity4_id": "Entity id de la 4ème valve", "valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la consigne", "auto_regulation_mode": "Ajustement automatique de la consigne",
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", "auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation" "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
} }
}, },
"tpi": { "tpi": {

View File

@@ -39,7 +39,8 @@
"valve_entity2_id": "Secondo valvola numero", "valve_entity2_id": "Secondo valvola numero",
"valve_entity3_id": "Terzo valvola numero", "valve_entity3_id": "Terzo valvola numero",
"valve_entity4_id": "Quarto valvola numero", "valve_entity4_id": "Quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione" "auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore", "heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -56,7 +57,8 @@
"valve_entity2_id": "Entity id del secondo valvola numero", "valve_entity2_id": "Entity id del secondo valvola numero",
"valve_entity3_id": "Entity id del terzo valvola numero", "valve_entity3_id": "Entity id del terzo valvola numero",
"valve_entity4_id": "Entity id del quarto valvola numero", "valve_entity4_id": "Entity id del quarto valvola numero",
"auto_regulation_mode": "Regolazione automatica della temperatura target" "auto_regulation_mode": "Regolazione automatica della temperatura target",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
} }
}, },
"tpi": { "tpi": {
@@ -195,7 +197,8 @@
"valve_entity2_id": "Secondo valvola numero", "valve_entity2_id": "Secondo valvola numero",
"valve_entity3_id": "Terzo valvola numero", "valve_entity3_id": "Terzo valvola numero",
"valve_entity4_id": "Quarto valvola numero", "valve_entity4_id": "Quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione" "auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore", "heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -212,7 +215,8 @@
"valve_entity2_id": "Entity id del secondo valvola numero", "valve_entity2_id": "Entity id del secondo valvola numero",
"valve_entity3_id": "Entity id del terzo valvola numero", "valve_entity3_id": "Entity id del terzo valvola numero",
"valve_entity4_id": "Entity id del quarto valvola numero", "valve_entity4_id": "Entity id del quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione" "auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
} }
}, },
"tpi": { "tpi": {

View File

@@ -38,7 +38,11 @@
"valve_entity_id": "1. ventil číslo", "valve_entity_id": "1. ventil číslo",
"valve_entity2_id": "2. ventil číslo", "valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo", "valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo" "valve_entity4_id": "4. ventil číslo",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "ID entity povinného ohrievača", "heater_entity_id": "ID entity povinného ohrievača",
@@ -54,7 +58,11 @@
"valve_entity_id": "1. ventil číslo entity id", "valve_entity_id": "1. ventil číslo entity id",
"valve_entity2_id": "2. ventil číslo entity id", "valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id", "valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id" "valve_entity4_id": "4. ventil číslo entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -199,7 +207,11 @@
"valve_entity_id": "1. ventil číslo", "valve_entity_id": "1. ventil číslo",
"valve_entity2_id": "2. ventil číslo", "valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo", "valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo" "valve_entity4_id": "4. ventil číslo",
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "ID entity povinného ohrievača", "heater_entity_id": "ID entity povinného ohrievača",
@@ -215,7 +227,11 @@
"valve_entity_id": "1. ventil číslo entity id", "valve_entity_id": "1. ventil číslo entity id",
"valve_entity2_id": "2. ventil číslo entity id", "valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id", "valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id" "valve_entity4_id": "4. ventil číslo entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -329,6 +345,14 @@
"thermostat_over_climate": "Termostat nad iným termostatom", "thermostat_over_climate": "Termostat nad iným termostatom",
"thermostat_over_valve": "Thermostat over a valve" "thermostat_over_valve": "Thermostat over a valve"
} }
},
"auto_regulation_mode": {
"options": {
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",
"auto_regulation_none": "No auto-regulation"
}
} }
}, },
"entity": { "entity": {

View File

@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.exceptions import ServiceNotFound from homeassistant.exceptions import ServiceNotFound
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE from homeassistant.core import HomeAssistant, CALLBACK_TYPE
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
@@ -104,37 +104,27 @@ class UnderlyingEntity:
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
return None return None
async def turn_off(self):
"""Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
# This may fails if called after shutdown
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_OFF,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_on(self):
"""Turn heater toggleable device on."""
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_ON,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def set_temperature(self, temperature, max_temp, min_temp): async def set_temperature(self, temperature, max_temp, min_temp):
"""Set the target temperature""" """Set the target temperature"""
return return
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self):
""" Turn off the underlying equipement.
Need to be overriden"""
return NotImplementedError
async def turn_on(self):
""" Turn off the underlying equipement.
Need to be overriden"""
return NotImplementedError
@property
def is_inversed(self):
""" Tells if the switch command should be inversed"""
return False
def remove_entity(self): def remove_entity(self):
"""Remove the underlying entity""" """Remove the underlying entity"""
return return
@@ -212,6 +202,13 @@ class UnderlyingSwitch(UnderlyingEntity):
"""The initial delay for this class""" """The initial delay for this class"""
return self._initial_delay_sec return self._initial_delay_sec
@overrides
@property
def is_inversed(self):
""" Tells if the switch command should be inversed"""
return self._thermostat.is_inversed
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool: async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change""" """Set the HVACmode. Returns true if something have change"""
@@ -229,7 +226,41 @@ class UnderlyingSwitch(UnderlyingEntity):
@property @property
def is_device_active(self): def is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
return self._hass.states.is_state(self._entity_id, STATE_ON) real_state = self._hass.states.is_state(self._entity_id, STATE_ON)
return self.is_inversed and not real_state
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self):
"""Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
domain = self._entity_id.split('.')[0]
# This may fails if called after shutdown
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
domain,
command,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_on(self):
"""Turn heater toggleable device on."""
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
domain = self._entity_id.split('.')[0]
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
domain,
command,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
@overrides @overrides
async def start_cycle( async def start_cycle(
@@ -380,6 +411,7 @@ class UnderlyingSwitch(UnderlyingEntity):
# increment energy at the end of the cycle # increment energy at the end of the cycle
self._thermostat.incremente_energy() self._thermostat.incremente_energy()
@overrides
def remove_entity(self): def remove_entity(self):
"""Remove the entity after stopping its cycle""" """Remove the entity after stopping its cycle"""
self._cancel_cycle() self._cancel_cycle()

View File

@@ -54,7 +54,8 @@ from custom_components.versatile_thermostat.const import (
CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH
) )
MOCK_TH_OVER_SWITCH_USER_CONFIG = { MOCK_TH_OVER_SWITCH_USER_CONFIG = {
CONF_NAME: "TheOverSwitchMockName", CONF_NAME: "TheOverSwitchMockName",
@@ -101,13 +102,15 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch", CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False
} }
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = { MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_air_conditioner", CONF_HEATER: "switch.mock_air_conditioner",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: True, CONF_AC_MODE: True,
CONF_INVERSE_SWITCH: False
} }
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
@@ -117,6 +120,7 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER_4: "switch.mock_4switch3", CONF_HEATER_4: "switch.mock_4switch3",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False
} }
MOCK_TH_OVER_SWITCH_TPI_CONFIG = { MOCK_TH_OVER_SWITCH_TPI_CONFIG = {

View File

@@ -17,7 +17,7 @@ async def test_show_form(hass: HomeAssistant) -> None:
# Init the API # Init the API
# hass.data["custom_components"] = None # hass.data["custom_components"] = None
# loader.async_get_custom_components(hass) # loader.async_get_custom_components(hass)
# VersatileThermostatAPI(hass) # BaseThermostatAPI(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
@@ -369,7 +369,7 @@ async def test_user_config_flow_over_4_switches(
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False
} }
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
@@ -434,6 +434,7 @@ async def test_user_config_flow_over_4_switches(
| MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG | MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG | MOCK_ADVANCED_CONFIG
| { CONF_INVERSE_SWITCH: False }
) )
assert result["result"] assert result["result"]
assert result["result"].domain == DOMAIN assert result["result"].domain == DOMAIN

View File

@@ -0,0 +1,124 @@
# pylint: disable=unused-argument, line-too-long, protected-access
""" Test the Window management """
import asyncio
import logging
from unittest.mock import patch, call
from datetime import datetime, timedelta
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window auto management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
CONF_INVERSE_SWITCH: True
},
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=True # switch is On
):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.is_inversed
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 21
assert entity.is_device_active is False
assert mock_service_call.call_count == 0
# 1. Make the temperature down to activate the switch
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=True # switch is Off
):
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert entity.hvac_mode is HVACMode.HEAT
# not updated cause mocked assert entity.is_device_active is True
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls([
call.async_call('switch', SERVICE_TURN_OFF, {'entity_id': 'switch.mock_switch'}),
])
# 2. Make the temperature up to deactivate the switch
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=False # switch is On -> it should turned off
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 25, event_timestamp)
# The heater turns on
assert entity.hvac_mode is HVACMode.HEAT
# not updated cause mocked assert entity.is_device_active is False
# there is no change because the cycle is currenlty running.
# we should simulate the end of the cycle to see oif underlying switch turns on
await entity._underlyings[0].turn_off()
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls([
call.async_call('switch', SERVICE_TURN_ON, {'entity_id': 'switch.mock_switch'}),
])
# Clean the entity
entity.remove_thermostat()

View File

@@ -1,10 +1,12 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
""" Test the Window management """ """ Test the Window management """
import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from unittest.mock import patch, call, PropertyMock from unittest.mock import patch
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -54,7 +56,7 @@ async def test_movement_management_time_not_enough(
}, },
) )
entity: VersatileThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -251,7 +253,7 @@ async def test_movement_management_time_enough_and_presence(
}, },
) )
entity: VersatileThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -383,7 +385,7 @@ async def test_movement_management_time_enoughand_not_presence(
}, },
) )
entity: VersatileThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -517,7 +519,7 @@ async def test_movement_management_with_stop_during_condition(
}, },
) )
entity: VersatileThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -597,4 +599,3 @@ async def test_movement_management_with_stop_during_condition(
assert entity.target_temperature == 19 # Boost assert entity.target_temperature == 19 # Boost
assert entity.motion_state is "on" # switch to movement on assert entity.motion_state is "on" # switch to movement on
assert entity.presence_state is "off" # Non change assert entity.presence_state is "off" # Non change

View File

@@ -1,9 +1,12 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
""" Test the Multiple switch management """ """ Test the Multiple switch management """
import asyncio import asyncio
from unittest.mock import patch, call, ANY from unittest.mock import patch, call, ANY
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -50,7 +53,7 @@ async def test_one_switch_cycle(
}, },
) )
entity: VersatileThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname" hass, entry, "climate.theover4switchmockname"
) )
assert entity assert entity
@@ -260,7 +263,7 @@ async def test_multiple_switchs(
}, },
) )
entity: VersatileThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname" hass, entry, "climate.theover4switchmockname"
) )
assert entity assert entity
@@ -396,7 +399,7 @@ async def test_multiple_climates(
}, },
) )
entity: VersatileThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname" hass, entry, "climate.theover4climatemockname"
) )
assert entity assert entity
@@ -496,7 +499,7 @@ async def test_multiple_climates_underlying_changes(
}, },
) )
entity: VersatileThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname" hass, entry, "climate.theover4climatemockname"
) )
assert entity assert entity

View File

@@ -16,6 +16,7 @@ def test_pi_algorithm_basics():
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the accumulator threshold effect and offset_max # Test the accumulator threshold effect and offset_max
assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2 assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2
@@ -23,8 +24,8 @@ def test_pi_algorithm_basics():
assert the_algo.calculate_regulated_temperature(10, 10) == 22 assert the_algo.calculate_regulated_temperature(10, 10) == 22
# Will keep infinitly 22.0 # Will keep infinitly 22.0
# to reset the accumulated erro # to reset the accumulated error
the_algo.set_target_temp(20) the_algo.reset_accumulated_error()
assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5 assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6 assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6 assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
@@ -104,6 +105,7 @@ def test_pi_algorithm_medium():
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the error acculation effect # Test the error acculation effect
assert the_algo.calculate_regulated_temperature(19, 5) == 22.1 assert the_algo.calculate_regulated_temperature(19, 5) == 22.1
assert the_algo.calculate_regulated_temperature(19, 5) == 22.2 assert the_algo.calculate_regulated_temperature(19, 5) == 22.2
@@ -157,6 +159,7 @@ def test_pi_algorithm_strong():
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the error acculation effect # Test the error acculation effect
assert the_algo.calculate_regulated_temperature(19, 10) == 22.8 assert the_algo.calculate_regulated_temperature(19, 10) == 22.8
assert the_algo.calculate_regulated_temperature(19, 10) == 23 assert the_algo.calculate_regulated_temperature(19, 10) == 23