From e7c39f144b90cabb9e62456178988d3c01b28743 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Fri, 27 Oct 2023 06:57:40 +0000 Subject: [PATCH] Add valve change tests (ko) --- .../versatile_thermostat/base_thermostat.py | 57 +++--- .../versatile_thermostat/const.py | 11 ++ .../thermostat_climate.py | 4 +- .../versatile_thermostat/thermostat_switch.py | 4 +- .../versatile_thermostat/thermostat_valve.py | 13 +- .../versatile_thermostat/underlyings.py | 181 ++++-------------- tests/conftest.py | 2 +- tests/test_bugs.py | 8 +- tests/test_movement.py | 8 +- tests/test_multiple_switch.py | 28 +-- tests/test_valve.py | 74 ++++++- 11 files changed, 187 insertions(+), 203 deletions(-) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 0511c7e..a623131 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -732,17 +732,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity): return f"VersatileThermostat-{self.name}" @property - def is_over_climate(self): + def is_over_climate(self) -> bool: """ True if the Thermostat is over_climate""" return False @property - def is_over_switch(self): + def is_over_switch(self) -> bool: """ True if the Thermostat is over_switch""" return False @property - def is_over_valve(self): + def is_over_valve(self) -> bool: """ True if the Thermostat is over_valve""" return False @@ -853,7 +853,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): """ if self._hvac_mode == HVACMode.OFF: return HVACAction.OFF - if not self._is_device_active: + if not self.is_device_active: return HVACAction.IDLE if self._hvac_mode == HVACMode.COOL: return HVACAction.COOLING @@ -873,7 +873,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): return self._support_flags @property - def _is_device_active(self): + def is_device_active(self): """Returns true if one underlying is active""" for under in self._underlyings: if under.is_device_active: @@ -1093,7 +1093,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): await self._async_set_preset_mode_internal(self._attr_preset_mode, True) if need_control_heating and sub_need_control_heating: - await self._async_control_heating(force=True) + await self.async_control_heating(force=True) # Ensure we update the current operation after changing the mode self.reset_last_temperature_time() @@ -1107,7 +1107,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): async def async_set_preset_mode(self, preset_mode): """Set new preset mode.""" await self._async_set_preset_mode_internal(preset_mode) - await self._async_control_heating(force=True) + await self.async_control_heating(force=True) async def _async_set_preset_mode_internal(self, preset_mode, force=False): """Set new preset mode.""" @@ -1240,7 +1240,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._attr_preset_mode = PRESET_NONE self.recalculate() self.reset_last_change_time() - await self._async_control_heating(force=True) + await self.async_control_heating(force=True) async def _async_internal_set_temperature(self, temperature): """Set the target temperature and the target temperature of underlying climate if any""" @@ -1290,7 +1290,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): await self._async_update_temp(new_state) self.recalculate() - await self._async_control_heating(force=False) + await self.async_control_heating(force=False) async def _async_ext_temperature_changed(self, event: Event): """Handle external temperature opf the sensor changes.""" @@ -1305,7 +1305,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): await self._async_update_ext_temp(new_state) self.recalculate() - await self._async_control_heating(force=False) + await self.async_control_heating(force=False) @callback async def _async_windows_changed(self, event): @@ -1431,7 +1431,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self.find_preset_temp(new_preset) ) self.recalculate() - await self._async_control_heating(force=True) + await self.async_control_heating(force=True) self._motion_call_cancel = None im_on = self._motion_state == STATE_ON @@ -1519,7 +1519,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): if changes: self.async_write_ha_state() self.update_custom_attributes() - await self._async_control_heating() + await self.async_control_heating() new_state = event.data.get("new_state") _LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state) @@ -1746,7 +1746,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._current_power = current_power if self._attr_preset_mode == PRESET_POWER: - await self._async_control_heating() + await self.async_control_heating() except ValueError as ex: _LOGGER.error("Unable to update current_power from sensor: %s", ex) @@ -1771,7 +1771,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): raise ValueError(f"Sensor has illegal state {new_state.state}") self._current_power_max = current_power_max if self._attr_preset_mode == PRESET_POWER: - await self._async_control_heating() + await self.async_control_heating() except ValueError as ex: _LOGGER.error("Unable to update current_power from sensor: %s", ex) @@ -1791,7 +1791,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): return await self._async_update_presence(new_state.state) - await self._async_control_heating(force=True) + await self.async_control_heating(force=True) async def _async_update_presence(self, new_state): _LOGGER.debug("%s - Updating presence. New state is %s", self, new_state) @@ -2237,7 +2237,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): return shouldBeInSecurity - async def _async_control_heating(self, force=False, _=None): + async def async_control_heating(self, force=False, _=None): """The main function used to run the calculation at each cycle""" _LOGGER.debug( @@ -2278,18 +2278,18 @@ class BaseThermostat(ClimateEntity, RestoreEntity): if self._hvac_mode == HVACMode.OFF: _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF)", self) # A security to force stop heater if still active - if self._is_device_active: + if self.is_device_active: await self._async_underlying_entity_turn_off() return True - if not self.is_over_climate: - for under in self._underlyings: - await under.start_cycle( - self._hvac_mode, - self._prop_algorithm.on_time_sec, - self._prop_algorithm.off_time_sec, - force, - ) + for under in self._underlyings: + await under.start_cycle( + self._hvac_mode, + self._prop_algorithm.on_time_sec if self._prop_algorithm else None, + self._prop_algorithm.off_time_sec if self._prop_algorithm else None, + self._prop_algorithm.on_percent if self._prop_algorithm else None, + force, + ) self.update_custom_attributes() return True @@ -2329,6 +2329,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): self._total_energy, ) + # TODO implement this with overrides def update_custom_attributes(self): """Update the custom extra attributes for the entity""" @@ -2459,7 +2460,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): """ _LOGGER.info("%s - Calling service_set_presence, presence: %s", self, presence) await self._async_update_presence(presence) - await self._async_control_heating(force=True) + await self.async_control_heating(force=True) async def service_set_preset_temperature( self, preset, temperature=None, temperature_away=None @@ -2498,7 +2499,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): await self._async_set_preset_mode_internal( preset.rstrip(PRESET_AC_SUFFIX), force=True ) - await self._async_control_heating(force=True) + await self.async_control_heating(force=True) async def service_set_security(self, delay_min, min_on_percent, default_on_percent): """Called by a service call: @@ -2527,7 +2528,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity): if self._prop_algorithm and self._security_state: self._prop_algorithm.set_security(self._security_default_on_percent) - await self._async_control_heating() + await self.async_control_heating() self.update_custom_attributes() def send_event(self, event_type: EventType, data: dict): diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index 87900e5..7929489 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -227,3 +227,14 @@ class UnknownEntity(HomeAssistantError): class WindowOpenDetectionMethod(HomeAssistantError): """Error to indicate there is an error in the window open detection method given.""" + +class overrides: # pylint: disable=invalid-name + """ An annotation to inform overrides """ + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + return self.func.__get__(instance, owner) + + def __call__(self, *args, **kwargs): + raise RuntimeError(f"Method {self.func.__name__} should have been overridden") diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index a8d7d75..280657e 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -23,7 +23,7 @@ class ThermostatOverClimate(BaseThermostat): super().__init__(hass, unique_id, name, entry_infos) @property - def is_over_climate(self): + def is_over_climate(self) -> bool: """ True if the Thermostat is over_climate""" return True @@ -93,7 +93,7 @@ class ThermostatOverClimate(BaseThermostat): self.async_on_remove( async_track_time_interval( self.hass, - self._async_control_heating, + self.async_control_heating, interval=timedelta(minutes=self._cycle_min), ) ) diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py index 14f9ee1..c3f2946 100644 --- a/custom_components/versatile_thermostat/thermostat_switch.py +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -24,7 +24,7 @@ class ThermostatOverSwitch(BaseThermostat): super().__init__(hass, unique_id, name, entry_infos) @property - def is_over_switch(self): + def is_over_switch(self) -> bool: """ True if the Thermostat is over_switch""" return True @@ -65,4 +65,4 @@ class ThermostatOverSwitch(BaseThermostat): ) ) - self.hass.create_task(self._async_control_heating()) + self.hass.create_task(self.async_control_heating()) diff --git a/custom_components/versatile_thermostat/thermostat_valve.py b/custom_components/versatile_thermostat/thermostat_valve.py index 7282918..fa4e42b 100644 --- a/custom_components/versatile_thermostat/thermostat_valve.py +++ b/custom_components/versatile_thermostat/thermostat_valve.py @@ -24,10 +24,15 @@ class ThermostatOverValve(BaseThermostat): super().__init__(hass, unique_id, name, entry_infos) @property - def is_over_valve(self): + def is_over_valve(self) -> bool: """ True if the Thermostat is over_valve""" return True + @property + def valve_open_percent(self) -> int: + """ Gives the percentage of valve needed""" + return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100) + def post_init(self, entry_infos): """ Initialize the Thermostat""" @@ -40,7 +45,7 @@ class ThermostatOverValve(BaseThermostat): if entry_infos.get(CONF_VALVE_4): lst_valves.append(entry_infos.get(CONF_VALVE_4)) - for valve in enumerate(lst_valves): + for _, valve in enumerate(lst_valves): self._underlyings.append( UnderlyingValve( hass=self._hass, @@ -68,7 +73,7 @@ class ThermostatOverValve(BaseThermostat): self.async_on_remove( async_track_time_interval( self.hass, - self._async_control_heating, + self.async_control_heating, interval=timedelta(minutes=self._cycle_min), ) ) @@ -90,7 +95,7 @@ class ThermostatOverValve(BaseThermostat): if changes: self.async_write_ha_state() self.update_custom_attributes() - await self._async_control_heating() + await self.async_control_heating() new_state = event.data.get("new_state") _LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state) diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index 553d397..a39e99f 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -24,10 +24,13 @@ from homeassistant.components.climate import ( SERVICE_TURN_ON, SERVICE_SET_TEMPERATURE, ) + +from homeassistant.components.number import SERVICE_SET_VALUE + from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_call_later -from .const import UnknownEntity +from .const import UnknownEntity, overrides _LOGGER = logging.getLogger(__name__) @@ -160,6 +163,19 @@ class UnderlyingEntity: """Call the method after a delay""" return async_call_later(hass, delay_sec, called_method) + async def start_cycle( + self, + hvac_mode: HVACMode, + on_time_sec: int, + off_time_sec: int, + on_percent: int, + force=False, + ): + """Starting cycle for switch""" + + def _cancel_cycle(self): + """ Stops an eventual cycle """ + class UnderlyingSwitch(UnderlyingEntity): """Represent a underlying switch""" @@ -215,11 +231,13 @@ class UnderlyingSwitch(UnderlyingEntity): """If the toggleable device is currently active.""" return self._hass.states.is_state(self._entity_id, STATE_ON) + @overrides async def start_cycle( self, hvac_mode: HVACMode, on_time_sec: int, off_time_sec: int, + on_percent: int, force=False, ): """Starting cycle for switch""" @@ -270,6 +288,7 @@ class UnderlyingSwitch(UnderlyingEntity): else: _LOGGER.debug("%s - nothing to do", self) + @overrides def _cancel_cycle(self): """Cancel the cycle""" if self._async_cancel_cycle: @@ -303,15 +322,6 @@ class UnderlyingSwitch(UnderlyingEntity): time = self._on_time_sec action_label = "start" - # if self._should_relaunch_control_heating: - # _LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label) - # self._should_relaunch_control_heating = False - # # self.hass.create_task(self._async_control_heating()) - # await self.start_cycle( - # self._hvac_mode, self._on_time_sec, self._off_time_sec - # ) - # _LOGGER.debug("%s - End of cycle (3)", self) - # return if time > 0: _LOGGER.info( @@ -348,16 +358,6 @@ class UnderlyingSwitch(UnderlyingEntity): return action_label = "stop" - # if self._should_relaunch_control_heating: - # _LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label) - # self._should_relaunch_control_heating = False - # # self.hass.create_task(self._async_control_heating()) - # await self.start_cycle( - # self._hvac_mode, self._on_time_sec, self._off_time_sec - # ) - # _LOGGER.debug("%s - End of cycle (3)", self) - # return - time = self._off_time_sec if time > 0: @@ -636,7 +636,7 @@ class UnderlyingValve(UnderlyingEntity): """Represent a underlying switch""" _hvac_mode: HVACMode - # The percentage of opening the valve + # This is the percentage of opening int integer (from 0 to 100) _percent_open: int def __init__( @@ -656,7 +656,7 @@ class UnderlyingValve(UnderlyingEntity): self._async_cancel_cycle = None self._should_relaunch_control_heating = False self._hvac_mode = None - self._percent_open = 0 + self._percent_open = self._thermostat.valve_open_percent async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool: """Set the HVACmode. Returns true if something have change""" @@ -680,132 +680,35 @@ class UnderlyingValve(UnderlyingEntity): except Exception: # pylint: disable=broad-exception-caught return False + @overrides async def start_cycle( self, hvac_mode: HVACMode, + _1, + _2, + _3, force=False, ): - """Starting cycle for switch""" - _LOGGER.debug( - "%s - Starting new cycle hvac_mode=%s percent_open=%d force=%s", - self, - hvac_mode, - self._percent_open, - force, - ) + """We use this function to change the on_percent""" + caped_val = self._thermostat.valve_open_percent + if not force and self._percent_open == caped_val: + # No changes + return - self._hvac_mode = hvac_mode + self._percent_open = caped_val + # Send the new command to valve via a service call - # Cancel eventual previous cycle if any - if self._async_cancel_cycle is not None: - if force: - _LOGGER.debug("%s - we force a new cycle", self) - self._cancel_cycle() - else: - _LOGGER.debug( - "%s - A previous cycle is alredy running and no force -> waits for its end", - self, - ) - # self._should_relaunch_control_heating = True - _LOGGER.debug("%s - End of cycle (2)", self) - return + try: + _LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open) - # If we should heat, starts the cycle with delay - if self._hvac_mode in [HVACMode.HEAT, HVACMode.COOL] and self._percent_open > 0: - # Starts the cycle after the initial delay - self._async_cancel_cycle = self.call_later( - self._hass, 0, self._turn_on_later + data = { "value": self._percent_open } + await self._hass.services.async_call( + HA_DOMAIN, + SERVICE_SET_VALUE, + data, ) - _LOGGER.debug("%s - _async_cancel_cycle=%s", self, self._async_cancel_cycle) - - # if we not heat but device is active - elif self.is_device_active: - _LOGGER.info( - "%s - stop heating (2)", - self, - ) - await self.turn_off() - else: - _LOGGER.debug("%s - nothing to do", self) - - def _cancel_cycle(self): - """Cancel the cycle""" - if self._async_cancel_cycle: - self._async_cancel_cycle() - self._async_cancel_cycle = None - _LOGGER.debug("%s - Stopping cycle during calculation", self) - - async def _turn_on_later(self, _): - """Turn the heater on after a delay""" - _LOGGER.debug( - "%s - calling turn_on_later hvac_mode=%s, should_relaunch_later=%s", - self, - self._hvac_mode, - self._should_relaunch_control_heating, - ) - - self._cancel_cycle() - - if self._hvac_mode == HVACMode.OFF: - _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self) - if self.is_device_active: - await self.turn_off() - return - - if await self._thermostat.check_overpowering(): - _LOGGER.debug("%s - End of cycle (3)", self) - return - # Security mode could have change the on_time percent - await self._thermostat.check_security() - - action_label = "start" - - _LOGGER.info( - "%s - %s heating", - self, - action_label, - ) - await self.turn_on() - - self._async_cancel_cycle = self.call_later( - self._hass, - 0, - self._turn_off_later, - ) - - async def _turn_off_later(self, _): - """Turn the heater off and call the next cycle after the delay""" - _LOGGER.debug( - "%s - calling turn_off_later hvac_mode=%s, should_relaunch_later=%s", - self, - self._hvac_mode, - self._should_relaunch_control_heating, - ) - self._cancel_cycle() - - if self._hvac_mode == HVACMode.OFF: - _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self) - if self.is_device_active: - await self.turn_off() - return - - action_label = "stop" - - _LOGGER.info( - "%s - %s heating", - self, - action_label - ) - await self.turn_off() - - self._async_cancel_cycle = self.call_later( - self._hass, - 0, - self._turn_on_later - ) - - # increment energy at the end of the cycle - self._thermostat.incremente_energy() + except ServiceNotFound as err: + _LOGGER.error(err) def remove_entity(self): """Remove the entity after stopping its cycle""" diff --git a/tests/conftest.py b/tests/conftest.py index 52d07d5..4e5aa59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -82,7 +82,7 @@ def skip_hass_states_get_fixture(): def skip_control_heating_fixture(): """Skip the control_heating of VersatileThermostat""" with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ): yield diff --git a/tests/test_bugs.py b/tests/test_bugs.py index 042ea99..630f391 100644 --- a/tests/test_bugs.py +++ b/tests/test_bugs.py @@ -62,9 +62,9 @@ async def test_bug_56( # Should not failed entity.update_custom_attributes() - # try to call _async_control_heating + # try to call async_control_heating try: - ret = await entity._async_control_heating() + ret = await entity.async_control_heating() # an exception should be send assert ret is False except Exception: # pylint: disable=broad-exception-caught @@ -75,9 +75,9 @@ async def test_bug_56( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", return_value=the_mock_underlying, # dont find the underlying climate ): - # try to call _async_control_heating + # try to call async_control_heating try: - await entity._async_control_heating() + await entity.async_control_heating() except UnknownEntity: assert False except Exception: # pylint: disable=broad-exception-caught diff --git a/tests/test_movement.py b/tests/test_movement.py index 3aa4f56..658e698 100644 --- a/tests/test_movement.py +++ b/tests/test_movement.py @@ -64,7 +64,7 @@ async def test_movement_management_time_not_enough( # start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ): await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_ACTIVITY) @@ -261,7 +261,7 @@ async def test_movement_management_time_enough_and_presence( # start heating, in boost mode. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ): await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_ACTIVITY) @@ -393,7 +393,7 @@ async def test_movement_management_time_enoughand_not_presence( # start heating, in boost mode. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ): await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_ACTIVITY) @@ -527,7 +527,7 @@ async def test_movement_management_with_stop_during_condition( # start heating, in boost mode. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ): await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_ACTIVITY) diff --git a/tests/test_multiple_switch.py b/tests/test_multiple_switch.py index 211f331..89668f9 100644 --- a/tests/test_multiple_switch.py +++ b/tests/test_multiple_switch.py @@ -58,7 +58,7 @@ async def test_one_switch_cycle( # start heating, in boost mode. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ): await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_BOOST) @@ -75,7 +75,7 @@ async def test_one_switch_cycle( with patch( "homeassistant.core.StateMachine.is_state", return_value=False ) as mock_is_state: - assert entity._is_device_active is False # pylint: disable=protected-access + assert entity.is_device_active is False # pylint: disable=protected-access # Should be call for the Switch assert mock_is_state.call_count == 1 @@ -269,7 +269,7 @@ async def test_multiple_switchs( # start heating, in boost mode. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.set_hvac_mode" ) as mock_underlying_set_hvac_mode: @@ -285,7 +285,7 @@ async def test_multiple_switchs( await send_temperature_change_event(entity, 15, event_timestamp) # Checks that all climates are off - assert entity._is_device_active is False # pylint: disable=protected-access + assert entity.is_device_active is False # pylint: disable=protected-access # Should be call for all Switch assert mock_underlying_set_hvac_mode.call_count == 4 @@ -405,7 +405,7 @@ async def test_multiple_climates( # start heating, in boost mode. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" ) as mock_underlying_set_hvac_mode: @@ -427,11 +427,11 @@ async def test_multiple_climates( call.set_hvac_mode(HVACMode.HEAT), ] ) - assert entity._is_device_active is False # pylint: disable=protected-access + assert entity.is_device_active is False # pylint: disable=protected-access # Stop heating, in boost mode. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" ) as mock_underlying_set_hvac_mode: @@ -452,7 +452,7 @@ async def test_multiple_climates( call.set_hvac_mode(HVACMode.OFF), ] ) - assert entity._is_device_active is False # pylint: disable=protected-access + assert entity.is_device_active is False # pylint: disable=protected-access @pytest.mark.parametrize("expected_lingering_tasks", [True]) @@ -505,7 +505,7 @@ async def test_multiple_climates_underlying_changes( # start heating, in boost mode. We block the control_heating to avoid running a cycle with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" ) as mock_underlying_set_hvac_mode: @@ -527,11 +527,11 @@ async def test_multiple_climates_underlying_changes( call.set_hvac_mode(HVACMode.HEAT), ] ) - assert entity._is_device_active is False # pylint: disable=protected-access + assert entity.is_device_active is False # pylint: disable=protected-access # Stop heating on one underlying climate with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" ) as mock_underlying_set_hvac_mode: @@ -554,11 +554,11 @@ async def test_multiple_climates_underlying_changes( ] ) assert entity.hvac_mode == HVACMode.OFF - assert entity._is_device_active is False # pylint: disable=protected-access + assert entity.is_device_active is False # pylint: disable=protected-access # Start heating on one underlying climate with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat._async_control_heating" + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" ), patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" ) as mock_underlying_set_hvac_mode, patch( @@ -587,4 +587,4 @@ async def test_multiple_climates_underlying_changes( ) assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_action == HVACAction.IDLE - assert entity._is_device_active is False # pylint: disable=protected-access + assert entity.is_device_active is False # pylint: disable=protected-access diff --git a/tests/test_valve.py b/tests/test_valve.py index 88300a8..62c905c 100644 --- a/tests/test_valve.py +++ b/tests/test_valve.py @@ -1,3 +1,5 @@ +# pylint: disable=line-too-long + """ Test the normal start of a Switch AC Thermostat """ from unittest.mock import patch, call from datetime import datetime, timedelta @@ -11,7 +13,6 @@ 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_valve import ThermostatOverValve from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import @@ -84,7 +85,7 @@ async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_st return entity # The name is in the CONF and not the title of the entry - entity: BaseThermostat = find_my_entity("climate.theovervalvemockname") + entity: ThermostatOverValve = find_my_entity("climate.theovervalvemockname") assert entity assert isinstance(entity, ThermostatOverValve) @@ -114,7 +115,6 @@ async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_st # 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}), @@ -125,9 +125,73 @@ async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_st ] ) + # Set the HVACMode to HEAT, with manual preset and target_temp to 18 before receiving temperature + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event: # Select a hvacmode, presence and preset - await entity.async_set_hvac_mode(HVACMode.COOL) - assert entity.hvac_mode is HVACMode.COOL + await entity.async_set_hvac_mode(HVACMode.HEAT) + # + assert entity.hvac_mode is HVACMode.HEAT + # No heating now + assert entity.valve_open_percent == 0 + assert entity.hvac_action == HVACAction.IDLE + assert mock_send_event.call_count == 1 + mock_send_event.assert_has_calls( + [ + call.send_event( + EventType.HVAC_MODE_EVENT, + {"hvac_mode": HVACMode.HEAT}, + ), + ] + ) + + # set manual target temp + await entity.async_set_temperature(temperature=18) + assert entity.preset_mode == PRESET_NONE # Manual mode + assert entity.target_temperature == 18 + # Nothing have changed cause we don't have room and external temperature + assert mock_send_event.call_count == 1 + + + # Set temperature and external temperature + with patch( + "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" + ) as mock_send_event, patch( + "homeassistant.core.ServiceRegistry.async_call" + ) as mock_service_call, patch( + "custom_components.versatile_thermostat.underlyings.UnderlyingValve.is_device_active", return_value=True + ): + # Change temperature + event_timestamp = now - timedelta(minutes=10) + await send_temperature_change_event(entity, 15, datetime.now()) + await send_ext_temperature_change_event(entity, 10, datetime.now()) + # Should heating strongly now + assert entity.valve_open_percent == 98 + assert entity.is_device_active is True + assert entity.hvac_action == HVACAction.HEATING + + assert mock_service_call.call_count == 2 + mock_service_call.assert_has_calls([ + call.async_call( + HA_DOMAIN, + SERVICE_SET_VALUE, + { + "value": 90 + } + ), + call.async_call( + HA_DOMAIN, + SERVICE_SET_VALUE, + { + "value": 98 + } + ) + ]) + + assert mock_send_event.call_count == 0 + + # ICI event_timestamp = now - timedelta(minutes=4) await send_presence_change_event(entity, True, False, event_timestamp)