diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index a694da1..f944bc8 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -104,6 +104,7 @@ from .const import ( CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_DTEMP, CONF_AUTO_REGULATION_PERIOD_MIN, + CONF_INVERSE_SWITCH, UnknownEntity, WindowOpenDetectionMethod, ) @@ -241,6 +242,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): ] ), vol.Optional(CONF_AC_MODE, default=False): cv.boolean, + vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean, } ) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 51390e1..929e2c8 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -90,6 +90,7 @@ CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium" CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong" CONF_AUTO_REGULATION_DTEMP="auto_regulation_dtemp" CONF_AUTO_REGULATION_PERIOD_MIN="auto_regulation_periode_min" +CONF_INVERSE_SWITCH="inverse_switch_command" CONF_PRESETS = { p: f"{p}_temp" @@ -193,7 +194,8 @@ ALL_CONF = ( CONF_VALVE_4, CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_DTEMP, - CONF_AUTO_REGULATION_PERIOD_MIN + CONF_AUTO_REGULATION_PERIOD_MIN, + CONF_INVERSE_SWITCH ] + CONF_PRESETS_VALUES + CONF_PRESETS_AWAY_VALUES diff --git a/custom_components/versatile_thermostat/pi_algorithm.py b/custom_components/versatile_thermostat/pi_algorithm.py index 79d7d4a..05403cb 100644 --- a/custom_components/versatile_thermostat/pi_algorithm.py +++ b/custom_components/versatile_thermostat/pi_algorithm.py @@ -26,6 +26,10 @@ class PITemperatureRegulator: self.accumulated_error:float = 0 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): """ Set the new target_temp""" self.target_temp = target_temp diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index 506b1fa..3f200c1 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -41,7 +41,8 @@ "valve_entity4_id": "4th valve number", "auto_regulation_mode": "Self-regulation", "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": { "heater_entity_id": "Mandatory heater entity id", @@ -60,7 +61,8 @@ "valve_entity4_id": "4th valve number entity id", "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", - "auto_regulation_periode_min": "Duration in minutes between two regulation update" + "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": { @@ -208,7 +210,8 @@ "valve_entity4_id": "4th valve number", "auto_regulation_mode": "Self-regulation", "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": { "heater_entity_id": "Mandatory heater entity id", @@ -227,7 +230,8 @@ "valve_entity4_id": "4th valve number entity id", "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", - "auto_regulation_periode_min": "Duration in minutes between two regulation update" + "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": { diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py index f088a47..45d4b6b 100644 --- a/custom_components/versatile_thermostat/thermostat_switch.py +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -11,6 +11,7 @@ from .const import ( CONF_HEATER_2, CONF_HEATER_3, CONF_HEATER_4, + CONF_INVERSE_SWITCH, overrides ) @@ -34,12 +35,18 @@ class ThermostatOverSwitch(BaseThermostat): # def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # """Initialize the thermostat over switch.""" # super().__init__(hass, unique_id, name, entry_infos) + _is_inversed: bool = None @property def is_over_switch(self) -> bool: """ True if the Thermostat is over_switch""" 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 def post_init(self, entry_infos): """ 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 @overrides diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index 506b1fa..3f200c1 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -41,7 +41,8 @@ "valve_entity4_id": "4th valve number", "auto_regulation_mode": "Self-regulation", "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": { "heater_entity_id": "Mandatory heater entity id", @@ -60,7 +61,8 @@ "valve_entity4_id": "4th valve number entity id", "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", - "auto_regulation_periode_min": "Duration in minutes between two regulation update" + "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": { @@ -208,7 +210,8 @@ "valve_entity4_id": "4th valve number", "auto_regulation_mode": "Self-regulation", "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": { "heater_entity_id": "Mandatory heater entity id", @@ -227,7 +230,8 @@ "valve_entity4_id": "4th valve number entity id", "auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send", - "auto_regulation_periode_min": "Duration in minutes between two regulation update" + "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": { diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index 1bc1896..6b3dfc7 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -41,7 +41,8 @@ "valve_entity4_id": "4ème valve number", "auto_regulation_mode": "Auto-régulation", "auto_regulation_dtemp": "Seuil de régulation", - "auto_regulation_periode_min": "Période minimale de régulation" + "auto_regulation_periode_min": "Période minimale de régulation", + "inverse_switch_command": "Inverser la commande" }, "data_description": { "heater_entity_id": "Entity id du 1er radiateur obligatoire", @@ -60,7 +61,8 @@ "valve_entity4_id": "Entity id de la 4ème valve", "auto_regulation_mode": "Ajustement automatique de la température cible", "auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", - "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation" + "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": { @@ -209,7 +211,8 @@ "valve_entity4_id": "4ème valve", "auto_regulation_mode": "Auto-regulation", "auto_regulation_dtemp": "Seuil de régulation", - "auto_regulation_periode_min": "Période minimale de régulation" + "auto_regulation_periode_min": "Période minimale de régulation", + "inverse_switch_command": "Inverser la commande" }, "data_description": { "heater_entity_id": "Entity id du 1er radiateur obligatoire", @@ -228,7 +231,8 @@ "valve_entity4_id": "Entity id de la 4ème valve", "auto_regulation_mode": "Ajustement automatique de la consigne", "auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée", - "auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation" + "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": { diff --git a/custom_components/versatile_thermostat/translations/it.json b/custom_components/versatile_thermostat/translations/it.json index ce724e6..5f594e3 100644 --- a/custom_components/versatile_thermostat/translations/it.json +++ b/custom_components/versatile_thermostat/translations/it.json @@ -39,7 +39,8 @@ "valve_entity2_id": "Secondo valvola numero", "valve_entity3_id": "Terzo valvola numero", "valve_entity4_id": "Quarto valvola numero", - "auto_regulation_mode": "Autoregolamentazione" + "auto_regulation_mode": "Autoregolamentazione", + "inverse_switch_command": "Comando inverso" }, "data_description": { "heater_entity_id": "Entity id obbligatoria del primo riscaldatore", @@ -56,7 +57,8 @@ "valve_entity2_id": "Entity id del secondo valvola numero", "valve_entity3_id": "Entity id del terzo valvola numero", "valve_entity4_id": "Entity id del quarto valvola numero", - "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": { @@ -195,7 +197,8 @@ "valve_entity2_id": "Secondo valvola numero", "valve_entity3_id": "Terzo valvola numero", "valve_entity4_id": "Quarto valvola numero", - "auto_regulation_mode": "Autoregolamentazione" + "auto_regulation_mode": "Autoregolamentazione", + "inverse_switch_command": "Comando inverso" }, "data_description": { "heater_entity_id": "Entity id obbligatoria del primo riscaldatore", @@ -212,7 +215,8 @@ "valve_entity2_id": "Entity id del secondo valvola numero", "valve_entity3_id": "Entity id del terzo valvola numero", "valve_entity4_id": "Entity id del quarto valvola numero", - "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": { diff --git a/custom_components/versatile_thermostat/translations/sk.json b/custom_components/versatile_thermostat/translations/sk.json index ed07bc2..cbfbf5e 100644 --- a/custom_components/versatile_thermostat/translations/sk.json +++ b/custom_components/versatile_thermostat/translations/sk.json @@ -38,7 +38,11 @@ "valve_entity_id": "1. ventil číslo", "valve_entity2_id": "2. 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": { "heater_entity_id": "ID entity povinného ohrievača", @@ -54,7 +58,11 @@ "valve_entity_id": "1. ventil číslo entity id", "valve_entity2_id": "2. 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": { @@ -199,7 +207,11 @@ "valve_entity_id": "1. ventil číslo", "valve_entity2_id": "2. 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": { "heater_entity_id": "ID entity povinného ohrievača", @@ -215,7 +227,11 @@ "valve_entity_id": "1. ventil číslo entity id", "valve_entity2_id": "2. 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": { @@ -329,6 +345,14 @@ "thermostat_over_climate": "Termostat nad iným termostatom", "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": { diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index 0526529..842c433 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -9,7 +9,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature 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 ( ClimateEntity, ClimateEntityFeature, @@ -104,37 +104,27 @@ class UnderlyingEntity: """If the toggleable device is currently active.""" 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): """Set the target temperature""" 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): """Remove the underlying entity""" return @@ -212,6 +202,13 @@ class UnderlyingSwitch(UnderlyingEntity): """The initial delay for this class""" 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: """Set the HVACmode. Returns true if something have change""" @@ -229,7 +226,41 @@ class UnderlyingSwitch(UnderlyingEntity): @property def is_device_active(self): """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 async def start_cycle( @@ -380,6 +411,7 @@ class UnderlyingSwitch(UnderlyingEntity): # increment energy at the end of the cycle self._thermostat.incremente_energy() + @overrides def remove_entity(self): """Remove the entity after stopping its cycle""" self._cancel_cycle() diff --git a/tests/const.py b/tests/const.py index e73bbfa..71c117a 100644 --- a/tests/const.py +++ b/tests/const.py @@ -54,7 +54,8 @@ from custom_components.versatile_thermostat.const import ( CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_DTEMP, - CONF_AUTO_REGULATION_PERIOD_MIN + CONF_AUTO_REGULATION_PERIOD_MIN, + CONF_INVERSE_SWITCH ) MOCK_TH_OVER_SWITCH_USER_CONFIG = { CONF_NAME: "TheOverSwitchMockName", @@ -101,13 +102,15 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = { MOCK_TH_OVER_SWITCH_TYPE_CONFIG = { CONF_HEATER: "switch.mock_switch", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, - CONF_AC_MODE: False + CONF_AC_MODE: False, + CONF_INVERSE_SWITCH: False } MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = { CONF_HEATER: "switch.mock_air_conditioner", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_AC_MODE: True, + CONF_INVERSE_SWITCH: False } MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { @@ -117,6 +120,7 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { CONF_HEATER_4: "switch.mock_4switch3", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_AC_MODE: False, + CONF_INVERSE_SWITCH: False } MOCK_TH_OVER_SWITCH_TPI_CONFIG = { diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 1630c6a..f0b7b6b 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -17,7 +17,7 @@ async def test_show_form(hass: HomeAssistant) -> None: # Init the API # hass.data["custom_components"] = None # loader.async_get_custom_components(hass) - # VersatileThermostatAPI(hass) + # BaseThermostatAPI(hass) result = await hass.config_entries.flow.async_init( 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_MOTION_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 @@ -434,6 +434,7 @@ async def test_user_config_flow_over_4_switches( | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG + | { CONF_INVERSE_SWITCH: False } ) assert result["result"] assert result["result"].domain == DOMAIN diff --git a/tests/test_inverted_switch.py b/tests/test_inverted_switch.py new file mode 100644 index 0000000..48aa0e9 --- /dev/null +++ b/tests/test_inverted_switch.py @@ -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() diff --git a/tests/test_movement.py b/tests/test_movement.py index 658e698..0ca453e 100644 --- a/tests/test_movement.py +++ b/tests/test_movement.py @@ -1,10 +1,12 @@ +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable + """ Test the Window management """ -import asyncio from datetime import datetime, timedelta import logging -from unittest.mock import patch, call, PropertyMock -from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import +from unittest.mock import patch +from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import 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" ) 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" ) 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" ) 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" ) assert entity @@ -597,4 +599,3 @@ async def test_movement_management_with_stop_during_condition( assert entity.target_temperature == 19 # Boost assert entity.motion_state is "on" # switch to movement on assert entity.presence_state is "off" # Non change - diff --git a/tests/test_multiple_switch.py b/tests/test_multiple_switch.py index 89668f9..f1bf7fc 100644 --- a/tests/test_multiple_switch.py +++ b/tests/test_multiple_switch.py @@ -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 """ import asyncio from unittest.mock import patch, call, ANY from datetime import datetime, timedelta import logging +from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import 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" ) 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" ) 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" ) 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" ) assert entity diff --git a/tests/test_pi.py b/tests/test_pi.py index a4afcaf..0e35b22 100644 --- a/tests/test_pi.py +++ b/tests/test_pi.py @@ -16,6 +16,7 @@ def test_pi_algorithm_basics(): # to reset the accumulated erro the_algo.set_target_temp(20) + the_algo.reset_accumulated_error() # Test the accumulator threshold effect and offset_max 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 # Will keep infinitly 22.0 - # to reset the accumulated erro - the_algo.set_target_temp(20) + # to reset the accumulated error + the_algo.reset_accumulated_error() assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5 assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6 assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6 @@ -104,6 +105,7 @@ def test_pi_algorithm_medium(): # to reset the accumulated erro the_algo.set_target_temp(20) + the_algo.reset_accumulated_error() # Test the error acculation effect assert the_algo.calculate_regulated_temperature(19, 5) == 22.1 assert the_algo.calculate_regulated_temperature(19, 5) == 22.2 @@ -157,6 +159,7 @@ def test_pi_algorithm_strong(): # to reset the accumulated erro the_algo.set_target_temp(20) + the_algo.reset_accumulated_error() # Test the error acculation effect assert the_algo.calculate_regulated_temperature(19, 10) == 22.8 assert the_algo.calculate_regulated_temperature(19, 10) == 23