diff --git a/.vscode/settings.json b/.vscode/settings.json index 674d495..1f76fb8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "[python]": { - "editor.defaultFormatter": "mikoz.black-py" + "editor.defaultFormatter": "ms-python.python" }, "python.linting.pylintEnabled": true, "python.linting.enabled": true, diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 4694142..38a7919 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -189,7 +189,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._security_state = None self._thermostat_type = None - self._is_over_climate = False self._attr_translation_key = "versatile_thermostat" @@ -262,7 +261,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._underlyings = [] self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE) if self._thermostat_type == CONF_THERMOSTAT_CLIMATE: - self._is_over_climate = True for climate in [ CONF_CLIMATE, CONF_CLIMATE_2, @@ -426,7 +424,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): # Initiate the ProportionalAlgorithm if self._prop_algorithm is not None: del self._prop_algorithm - if not self._is_over_climate: + if not self.is_over_climate: self._prop_algorithm = PropAlgorithm( self._proportional_function, self._tpi_coef_int, @@ -711,7 +709,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self.hass.create_task(self._check_switch_initial_state()) # Start the control_heating # starts a cycle if we are in over_climate type - if self._is_over_climate: + if self.is_over_climate: self.async_on_remove( async_track_time_interval( self.hass, @@ -809,6 +807,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity): def __str__(self): return f"VersatileThermostat-{self.name}" + @property + def is_over_climate(self): + """ True if the Thermostat is over_climate""" + return False + + @property + def is_over_switch(self): + """ True if the Thermostat is over_switch""" + return False + + @property + def is_over_valve(self): + """ True if the Thermostat is over_valve""" + return False + @property def device_info(self) -> DeviceInfo: """Return the device info.""" @@ -835,7 +848,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): @property def hvac_modes(self): """List of available operation modes.""" - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).hvac_modes return self._hvac_list @@ -851,7 +864,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): Requires ClimateEntityFeature.FAN_MODE. """ - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).fan_mode return None @@ -862,7 +875,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): Requires ClimateEntityFeature.FAN_MODE. """ - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).fan_modes return [] @@ -873,7 +886,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): Requires ClimateEntityFeature.SWING_MODE. """ - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).swing_mode return None @@ -884,7 +897,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): Requires ClimateEntityFeature.SWING_MODE. """ - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).swing_modes return None @@ -892,7 +905,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).temperature_unit return self._unit @@ -902,7 +915,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): """Return current operation.""" # Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different # delta will be managed by climate_state_change event. - # if self._is_over_climate: + # if self.is_over_climate: # if one not OFF -> return it # else OFF # for under in self._underlyings: @@ -918,7 +931,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): Need to be one of CURRENT_HVAC_*. """ - if self._is_over_climate: + if self.is_over_climate: # if one not IDLE or OFF -> return it # else if one IDLE -> IDLE # else OFF @@ -951,7 +964,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): @property def supported_features(self): """Return the list of supported features.""" - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).supported_features | self._support_flags return self._support_flags @@ -972,7 +985,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): @property def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).target_temperature_step return None @@ -983,7 +996,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE. """ - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).target_temperature_high return None @@ -994,7 +1007,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE. """ - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).target_temperature_low return None @@ -1005,7 +1018,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): Requires ClimateEntityFeature.AUX_HEAT. """ - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).is_aux_heat return None @@ -1013,7 +1026,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): @property def mean_cycle_power(self) -> float | None: """Returns the mean power consumption during the cycle""" - if not self._device_power or self._is_over_climate: + if not self._device_power or self.is_over_climate: return None return float( @@ -1093,12 +1106,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity): """ return self._attr_preset_modes - @property - def is_over_climate(self) -> bool | None: - """return True is the thermostat is over a climate - or False is over switch""" - return self._is_over_climate - @property def last_temperature_slope(self) -> float | None: """Return the last temperature slope curve if any""" @@ -1133,14 +1140,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity): def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" - if self._is_over_climate and self.underlying_entity(0): + if self.is_over_climate and self.underlying_entity(0): return self.underlying_entity(0).turn_aux_heat_on() raise NotImplementedError() async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" - if self._is_over_climate: + if self.is_over_climate: for under in self._underlyings: await under.async_turn_aux_heat_on() @@ -1148,7 +1155,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" - if self._is_over_climate: + if self.is_over_climate: for under in self._underlyings: return under.turn_aux_heat_off() @@ -1156,7 +1163,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" - if self._is_over_climate: + if self.is_over_climate: for under in self._underlyings: await under.async_turn_aux_heat_off() @@ -1273,7 +1280,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): else: # Select _ac presets if in COOL Mode (or over_switch with _ac_mode) if self._ac_mode and ( - self._hvac_mode == HVACMode.COOL or not self._is_over_climate + self._hvac_mode == HVACMode.COOL or not self.is_over_climate ): preset_mode = preset_mode + PRESET_AC_SUFFIX @@ -1292,7 +1299,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" _LOGGER.info("%s - Set fan mode: %s", self, fan_mode) - if fan_mode is None or not self._is_over_climate: + if fan_mode is None or not self.is_over_climate: return for under in self._underlyings: @@ -1303,7 +1310,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): async def async_set_humidity(self, humidity: int): """Set new target humidity.""" _LOGGER.info("%s - Set fan mode: %s", self, humidity) - if humidity is None or not self._is_over_climate: + if humidity is None or not self.is_over_climate: return for under in self._underlyings: await under.set_humidity(humidity) @@ -1313,7 +1320,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): async def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" _LOGGER.info("%s - Set fan mode: %s", self, swing_mode) - if swing_mode is None or not self._is_over_climate: + if swing_mode is None or not self.is_over_climate: return for under in self._underlyings: await under.set_swing_mode(swing_mode) @@ -1335,7 +1342,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): async def _async_internal_set_temperature(self, temperature): """Set the target temperature and the target temperature of underlying climate if any""" self._target_temp = temperature - if not self._is_over_climate: + if not self.is_over_climate: return for under in self._underlyings: @@ -1737,7 +1744,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): changes = True self._hvac_mode = new_hvac_mode # Update all underlyings state - if self._is_over_climate: + if self.is_over_climate: for under in self._underlyings: await under.set_hvac_mode(new_hvac_mode) @@ -1749,7 +1756,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): new_state.attributes, ) if ( - self._is_over_climate + self.is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature @@ -2094,7 +2101,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): if ( old_hvac_mode == HVACMode.OFF and self.hvac_mode != HVACMode.OFF - and self._is_over_climate + and self.is_over_climate ): _LOGGER.info( "%s - force resent target temp cause we turn on some over climate" @@ -2136,7 +2143,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): "%s - overpowering is detected. Heater preset will be set to 'power'", self, ) - if self._is_over_climate: + if self.is_over_climate: self.save_hvac_mode() self.save_preset_mode() await self._async_underlying_entity_turn_off() @@ -2162,7 +2169,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self, self._saved_preset_mode, ) - if self._is_over_climate: + if self.is_over_climate: await self.restore_hvac_mode(False) await self.restore_preset_mode() self.send_event( @@ -2194,12 +2201,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity): delta_temp > self._security_delay_min or delta_ext_temp > self._security_delay_min ) - climate_cond: bool = self._is_over_climate and self.hvac_action not in [ + climate_cond: bool = self.is_over_climate and self.hvac_action not in [ HVACAction.COOLING, HVACAction.IDLE, ] switch_cond: bool = ( - not self._is_over_climate + not self.is_over_climate and self._prop_algorithm is not None and self._prop_algorithm.calculated_on_percent >= self._security_min_on_percent @@ -2274,7 +2281,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self.save_preset_mode() await self._async_set_preset_mode_internal(PRESET_SECURITY) # Turn off the underlying climate or heater if security default on_percent is 0 - if self._is_over_climate or self._security_default_on_percent <= 0.0: + if self.is_over_climate or self._security_default_on_percent <= 0.0: await self.async_set_hvac_mode(HVACMode.OFF, False) if self._prop_algorithm: self._prop_algorithm.set_security(self._security_default_on_percent) @@ -2304,7 +2311,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): ) self._security_state = False # Restore hvac_mode if previously saved - if self._is_over_climate or self._security_default_on_percent <= 0.0: + if self.is_over_climate or self._security_default_on_percent <= 0.0: await self.restore_hvac_mode(False) await self.restore_preset_mode() if self._prop_algorithm: @@ -2360,7 +2367,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): return True security: bool = await self.check_security() - if security and self._is_over_climate: + if security and self.is_over_climate: _LOGGER.debug("%s - End of cycle (security and over climate)", self) return True @@ -2372,7 +2379,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): await self._async_underlying_entity_turn_off() return True - if not self._is_over_climate: + if not self.is_over_climate: for under in self._underlyings: await under.start_cycle( self._hvac_mode, @@ -2389,7 +2396,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): update the custom attributes and write the state """ _LOGGER.debug("%s - recalculate all", self) - if not self._is_over_climate: + if not self.is_over_climate: self._prop_algorithm.calculate( self._target_temp, self._cur_temp, @@ -2405,10 +2412,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity): return added_energy = 0 - if self._is_over_climate and self._underlying_climate_delta_t is not None: + if self.is_over_climate and self._underlying_climate_delta_t is not None: added_energy = self._device_power * self._underlying_climate_delta_t - if not self._is_over_climate and self.mean_cycle_power is not None: + if not self.is_over_climate and self.mean_cycle_power is not None: added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0 self._total_energy += added_energy @@ -2481,7 +2488,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): "power_sensor_entity_id": self._power_sensor_entity_id, "max_power_sensor_entity_id": self._max_power_sensor_entity_id, } - if self._is_over_climate: + if self.is_over_climate: self._attr_extra_state_attributes[ "underlying_climate_0" ] = self._underlyings[0].entity_id diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 955ac53..dbc1c2d 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -9,3 +9,8 @@ class ThermostatOverClimate(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) + + @property + def is_over_climate(self): + """ True if the Thermostat is over_climate""" + return True diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py index 3a9171c..d47c0d7 100644 --- a/custom_components/versatile_thermostat/thermostat_switch.py +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -11,3 +11,8 @@ 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) + + @property + def is_over_switch(self): + """ True if the Thermostat is over_switch""" + return True diff --git a/custom_components/versatile_thermostat/thermostat_valve.py b/custom_components/versatile_thermostat/thermostat_valve.py index 4bbb824..fa1f67f 100644 --- a/custom_components/versatile_thermostat/thermostat_valve.py +++ b/custom_components/versatile_thermostat/thermostat_valve.py @@ -9,3 +9,8 @@ class ThermostatOverValve(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) + + @property + def is_over_valve(self): + """ True if the Thermostat is over_valve""" + return True diff --git a/tests/commons.py b/tests/commons.py index 089019f..adc2047 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -1,3 +1,5 @@ +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long + """ Some common resources """ import asyncio import logging @@ -550,7 +552,7 @@ async def send_climate_change_event_with_temperature( def cancel_switchs_cycles(entity: BaseThermostat): """This method will cancel all running cycle on all underlying switch entity""" - if entity._is_over_climate: + if entity.is_over_climate: return for under in entity._underlyings: under._cancel_cycle() diff --git a/tests/test_bugs.py b/tests/test_bugs.py index 831071c..042ea99 100644 --- a/tests/test_bugs.py +++ b/tests/test_bugs.py @@ -1,10 +1,12 @@ +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long + """ Test the Window management """ from unittest.mock import patch, call -from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from datetime import datetime, timedelta import logging +from .commons import * logging.getLogger().setLevel(logging.DEBUG) @@ -49,7 +51,7 @@ async def test_bug_56( }, ) - entity: VersatileThermostat = await create_thermostat( + entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverclimatemockname" ) assert entity @@ -126,7 +128,7 @@ async def test_bug_63( }, ) - entity: VersatileThermostat = await create_thermostat( + entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity @@ -178,7 +180,7 @@ async def test_bug_64( }, ) - entity: VersatileThermostat = await create_thermostat( + entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity @@ -230,7 +232,7 @@ async def test_bug_66( }, ) - entity: VersatileThermostat = await create_thermostat( + entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity @@ -387,7 +389,7 @@ async def test_bug_82( assert entity assert entity.name == "TheOverClimateMockName" - assert entity._is_over_climate is True + assert entity.is_over_climate is True # assert entity.hvac_action is HVACAction.OFF assert entity.hvac_mode is HVACMode.OFF # assert entity.hvac_mode is None @@ -434,7 +436,7 @@ async def test_bug_82( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on: + ): event_timestamp = now - timedelta(minutes=6) # set temperature to 15 so that on_percent will be > security_min_on_percent (0.2) @@ -491,7 +493,7 @@ async def test_bug_101( assert entity assert entity.name == "TheOverClimateMockName" - assert entity._is_over_climate is True + assert entity.is_over_climate is True assert entity.hvac_mode is HVACMode.OFF # because the underlying is heating. In real life the underlying should be shut-off assert entity.hvac_action is HVACAction.HEATING @@ -540,6 +542,3 @@ async def test_bug_101( await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, event_timestamp, 12.75) assert entity.target_temperature == 12.75 assert entity.preset_mode is PRESET_NONE - - - diff --git a/tests/test_security.py b/tests/test_security.py index bc62456..a13bc12 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -235,7 +235,7 @@ async def test_security_over_climate( assert entity assert entity.name == "TheOverClimateMockName" - assert entity._is_over_climate is True + assert entity.is_over_climate is True # Because the underlying is HEATING. In real life the underlying will be shut-off assert entity.hvac_action is HVACAction.HEATING diff --git a/tests/test_sensors.py b/tests/test_sensors.py index d5ac7cc..c6467ef 100644 --- a/tests/test_sensors.py +++ b/tests/test_sensors.py @@ -1,3 +1,5 @@ +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long + """ Test the normal start of a Thermostat """ from datetime import timedelta, datetime @@ -66,7 +68,7 @@ async def test_sensors_over_switch( }, ) - entity: VersatileThermostat = await create_thermostat( + entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity @@ -229,7 +231,7 @@ async def test_sensors_over_climate( }, ) - entity: VersatileThermostat = await create_thermostat( + entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverclimatemockname" ) assert entity @@ -361,7 +363,7 @@ async def test_sensors_over_climate_minimal( }, ) - entity: VersatileThermostat = await create_thermostat( + entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverclimatemockname" ) assert entity diff --git a/tests/test_start.py b/tests/test_start.py index 7fbb6e4..819ad30 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -1,3 +1,5 @@ +# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long + """ Test the normal start of a Thermostat """ from unittest.mock import patch, call @@ -11,6 +13,8 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate +from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import @@ -41,12 +45,13 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s if entity.entity_id == entity_id: return entity - entity: VersatileThermostat = find_my_entity("climate.theoverswitchmockname") + entity: BaseThermostat = find_my_entity("climate.theoverswitchmockname") assert entity + assert isinstance(entity, ThermostatOverSwitch) assert entity.name == "TheOverSwitchMockName" - assert entity._is_over_climate is False + assert entity.is_over_climate is False assert entity.hvac_action is HVACAction.OFF assert entity.hvac_mode is HVACMode.OFF assert entity.target_temperature == entity.min_temp @@ -112,9 +117,10 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_ entity = find_my_entity("climate.theoverclimatemockname") assert entity + assert isinstance(entity, ThermostatOverClimate) assert entity.name == "TheOverClimateMockName" - assert entity._is_over_climate is True + assert entity.is_over_climate is True assert entity.hvac_action is HVACAction.OFF assert entity.hvac_mode is HVACMode.OFF assert entity.target_temperature == entity.min_temp @@ -173,12 +179,12 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_ if entity.entity_id == entity_id: return entity - entity: VersatileThermostat = find_my_entity("climate.theover4switchmockname") + entity: BaseThermostat = find_my_entity("climate.theover4switchmockname") assert entity assert entity.name == "TheOver4SwitchMockName" - assert entity._is_over_climate is False + assert entity.is_over_climate is False assert entity.hvac_action is HVACAction.OFF assert entity.hvac_mode is HVACMode.OFF assert entity.target_temperature == entity.min_temp diff --git a/tests/test_switch_ac.py b/tests/test_switch_ac.py index b389a05..8608205 100644 --- a/tests/test_switch_ac.py +++ b/tests/test_switch_ac.py @@ -12,6 +12,7 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import @@ -48,9 +49,10 @@ async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_i entity: BaseThermostat = find_my_entity("climate.theoverswitchmockname") assert entity + assert isinstance(entity, ThermostatOverSwitch) assert entity.name == "TheOverSwitchMockName" - assert entity._is_over_climate is False # pylint: disable=protected-access + assert entity.is_over_climate is False # pylint: disable=protected-access assert entity.ac_mode is True assert entity.hvac_action is HVACAction.OFF assert entity.hvac_mode is HVACMode.OFF @@ -136,5 +138,3 @@ async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_i assert entity.hvac_mode is HVACMode.COOL assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE) assert entity.target_temperature == 27 # eco_ac_away - - diff --git a/tests/test_valve.py b/tests/test_valve.py new file mode 100644 index 0000000..2bb1310 --- /dev/null +++ b/tests/test_valve.py @@ -0,0 +1,162 @@ +""" Test the normal start of a Switch AC Thermostat """ +from unittest.mock import patch, call +from datetime import datetime, timedelta + +from homeassistant.core import HomeAssistant +from homeassistant.components.climate import HVACAction, HVACMode +from homeassistant.config_entries import ConfigEntryState + +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN + +from pytest_homeassistant_custom_component.common import MockConfigEntry + +from custom_components.versatile_thermostat.base_thermostat import BaseThermostat +from custom_components.versatile_thermostat.thermostat_valve import ThermostatOverValve + +from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import + +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +@pytest.mark.parametrize("expected_lingering_timers", [True]) +async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_state): # pylint: disable=unused-argument + """Test the normal full start of a thermostat in thermostat_over_switch type""" + + entry = MockConfigEntry( + domain=DOMAIN, + title="TheOverValveMockName", + unique_id="uniqueId", + data={ + CONF_NAME: "TheOverValveMockName", + CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE, + 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, + CONF_USE_WINDOW_FEATURE: False, + CONF_USE_MOTION_FEATURE: False, + CONF_USE_POWER_FEATURE: False, + CONF_USE_PRESENCE_FEATURE: False, + CONF_VALVE: "number.mock_valve", + 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_DEVICE_POWER: 100, + }, + ) + + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event: + 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 + + # The name is in the CONF and not the title of the entry + entity: BaseThermostat = find_my_entity("climate.theovervalvemockname") + + assert entity + assert isinstance(entity, ThermostatOverValve) + + assert entity.name == "TheOverValveMockName" + assert entity.is_over_climate is False + assert entity.is_over_switch is False + assert entity.is_over_valve is True + assert entity.ac_mode is False + assert entity.hvac_action is HVACAction.OFF + assert entity.hvac_mode is HVACMode.OFF + assert entity.hvac_modes == [HVACMode.COOL, HVACMode.OFF] + assert entity.target_temperature == entity.max_temp + assert entity.preset_modes == [ + PRESET_NONE, + PRESET_ECO, + PRESET_COMFORT, + PRESET_BOOST, + PRESET_ACTIVITY, + ] + assert entity.preset_mode is PRESET_NONE + assert entity._security_state is False # pylint: disable=protected-access + assert entity._window_state is None # pylint: disable=protected-access + 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 + + # 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}, + ), + ] + ) + + # Select a hvacmode, presence and preset + await entity.async_set_hvac_mode(HVACMode.COOL) + assert entity.hvac_mode is HVACMode.COOL + + event_timestamp = now - timedelta(minutes=4) + await send_presence_change_event(entity, True, False, event_timestamp) + assert entity._presence_state == STATE_ON # pylint: disable=protected-access + + await entity.async_set_hvac_mode(HVACMode.COOL) + assert entity.hvac_mode is HVACMode.COOL + + await entity.async_set_preset_mode(PRESET_COMFORT) + assert entity.preset_mode is PRESET_COMFORT + assert entity.target_temperature == 23 + + # switch to Eco + await entity.async_set_preset_mode(PRESET_ECO) + assert entity.preset_mode is PRESET_ECO + assert entity.target_temperature == 25 + + # Unset the presence + event_timestamp = now - timedelta(minutes=3) + await send_presence_change_event(entity, False, True, event_timestamp) + assert entity._presence_state == STATE_OFF # pylint: disable=protected-access + assert entity.target_temperature == 27 # eco_ac_away + + # Open a window + with patch( + "homeassistant.helpers.condition.state", return_value=True + ): + event_timestamp = now - timedelta(minutes=2) + try_condition = await send_window_change_event(entity, True, False, event_timestamp) + + # Confirme the window event + await try_condition(None) + + assert entity.hvac_mode is HVACMode.OFF + assert entity.hvac_action is HVACAction.OFF + assert entity.target_temperature == 27 # eco_ac_away + + # Close a window + with patch( + "homeassistant.helpers.condition.state", return_value=True + ): + event_timestamp = now - timedelta(minutes=2) + try_condition = await send_window_change_event(entity, False, True, event_timestamp) + + # Confirme the window event + await try_condition(None) + + assert entity.hvac_mode is HVACMode.COOL + assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE) + assert entity.target_temperature == 27 # eco_ac_away