From eb54f2826fb37346bc1b73a70cee989164412876 Mon Sep 17 00:00:00 2001 From: Andrea Nicotra Date: Sat, 21 Oct 2023 08:31:45 +0200 Subject: [PATCH] move AC mode config under the right configuration step (#108) * move AC mode config under the right configuration step --- .devcontainer/devcontainer.json | 1 - .gitignore | 2 + container | 68 ++++--- .../versatile_thermostat/climate.py | 172 ++++++++++++------ .../versatile_thermostat/config_flow.py | 2 +- .../versatile_thermostat/prop_algorithm.py | 26 ++- .../versatile_thermostat/tests/const.py | 3 +- .../tests/test_config_flow.py | 1 + .../versatile_thermostat/tests/test_tpi.py | 28 ++- .../versatile_thermostat/underlyings.py | 2 +- 10 files changed, 206 insertions(+), 99 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c7f0ae3..1f06e7b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,7 +16,6 @@ "ms-python.vscode-pylance" ], "mounts": [ - "source=/Users/jmcollin/SugarSync/Projets/home-assistant/core,target=/home/vscode/core,type=bind,consistency=cached", "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached", "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached" ], diff --git a/.gitignore b/.gitignore index dd9d50a..f2bb3d8 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,6 @@ dist # TernJS port file .tern-port +# init file required for unittest +custom_components/__init__.py __pycache__ \ No newline at end of file diff --git a/container b/container index 2a5d7fe..4fc92ea 100755 --- a/container +++ b/container @@ -6,33 +6,45 @@ cd $HA -echo "arguments are: "$* -# Post installation of container -command=$1 -if [ "$command" == "install" ]; then - echo "Running container post installation" - script/setup -fi +function get_dev() { + cd /workspaces/versatile_thermostat/custom_components/versatile_thermostat/ + pip install pytest + pip install -r requirements_dev.txt + pip install -r requirements_test.txt + sudo chown -R vscode: /home/vscode/core + cd - +} -if [ "$command" == "start" ]; then - echo "Running container start" - hass -c ./config --debug -fi +echo "arguments are: "$1 -if [ "$command" == "translations" ]; then - echo "Running container start" - python3 -m script.translations develop -fi - -if [ "$command" == "hassfest" ]; then - echo "Running container start" - python3 -m script.hassfest - # python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/ -fi - -if [ "$command" == "restart" ]; then - echo "Killing existing container" - pkill hass - echo "Killing existing container" - hass -c ./config -fi +case $1 in + start) + echo "Running container start" + cd $HA + hass -c ./config --debug + ;; + dev-setup) + get_dev + ;; + install) + echo "Running container post installation" + script/setup + ;; + translations) + echo "Running container start" + cd $HA + python3 -m script.translations develop + ;; + hassfest) + echo "Running container start" + python3 -m script.hassfest + # python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/ + ;; + restart) + echo "Killing existing container" + pkill hass + echo "Killing existing container" + cd $HA + hass -c ./config + ;; +esac diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index 602e7bb..7e2eb39 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -301,7 +301,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): entry_infos, ) - self._ac_mode = entry_infos.get(CONF_AC_MODE) == True + self._ac_mode = entry_infos.get(CONF_AC_MODE) is True # convert entry_infos into usable attributes presets = {} items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items() @@ -339,7 +339,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): 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, CONF_CLIMATE_3, CONF_CLIMATE_4]: + for climate in [ + CONF_CLIMATE, + CONF_CLIMATE_2, + CONF_CLIMATE_3, + CONF_CLIMATE_4, + ]: if entry_infos.get(climate): self._underlyings.append( UnderlyingClimate( @@ -769,7 +774,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): self.async_write_ha_state() if self._prop_algorithm: self._prop_algorithm.calculate( - self._target_temp, self._cur_temp, self._cur_ext_temp + self._target_temp, + self._cur_temp, + self._cur_ext_temp, + self._hvac_mode == HVACMode.COOL, ) self.hass.create_task(self._check_switch_initial_state()) @@ -962,8 +970,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): # 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 one not OFF -> return it - # else OFF + # if one not OFF -> return it + # else OFF # for under in self._underlyings: # if (mode := under.hvac_mode) not in [HVACMode.OFF] # return mode @@ -983,7 +991,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): # else OFF one_idle = False for under in self._underlyings: - if (action := under.hvac_action) not in [HVACAction.IDLE, HVACAction.OFF]: + if (action := under.hvac_action) not in [ + HVACAction.IDLE, + HVACAction.OFF, + ]: return action if under.hvac_action == HVACAction.IDLE: one_idle = True @@ -995,6 +1006,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): return HVACAction.OFF if not self._is_device_active: return HVACAction.IDLE + if self._hvac_mode == HVACMode.COOL: + return HVACAction.COOLING return HVACAction.HEATING @property @@ -1066,7 +1079,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): @property def mean_cycle_power(self) -> float | None: - """Returns tne mean power consumption during the cycle""" + """Returns the mean power consumption during the cycle""" if not self._device_power or self._is_over_climate: return None @@ -1613,7 +1626,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): """Prevent the device from keep running if HVAC_MODE_OFF.""" _LOGGER.debug("%s - Calling _check_switch_initial_state", self) # We need to do the same check for over_climate underlyings - #if self.is_over_climate: + # if self.is_over_climate: # return for under in self._underlyings: await under.check_initial_state(self._hvac_mode) @@ -1632,16 +1645,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): @callback async def _async_climate_changed(self, event): """Handle unerdlying climate state changes. - This method takes the underlying values and update the VTherm with them. - To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received - less than 10 sec after the last command. What we want here is to take the values - from underlyings ONLY if someone have change directly on the underlying and not - as a return of the command. The only thing we take all the time is the HVACAction - which is important for feedaback and which cannot generates loops. + This method takes the underlying values and update the VTherm with them. + To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received + less than 10 sec after the last command. What we want here is to take the values + from underlyings ONLY if someone have change directly on the underlying and not + as a return of the command. The only thing we take all the time is the HVACAction + which is important for feedaback and which cannot generates loops. """ async def end_climate_changed(changes): - """ To end the event management""" + """To end the event management""" if changes: self.async_write_ha_state() self.update_custom_attributes() @@ -1667,15 +1680,25 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): else None ) - old_state_date_changed = old_state.last_changed if old_state and old_state.last_changed else None - old_state_date_updated = old_state.last_updated if old_state and old_state.last_updated else None - new_state_date_changed = new_state.last_changed if new_state and new_state.last_changed else None - new_state_date_updated = new_state.last_updated if new_state and new_state.last_updated else None + old_state_date_changed = ( + old_state.last_changed if old_state and old_state.last_changed else None + ) + old_state_date_updated = ( + old_state.last_updated if old_state and old_state.last_updated else None + ) + new_state_date_changed = ( + new_state.last_changed if new_state and new_state.last_changed else None + ) + new_state_date_updated = ( + new_state.last_updated if new_state and new_state.last_updated else None + ) # Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command # Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is - #if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE: - # _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF") + # if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE: + # _LOGGER.debug( + # "The underlying switch to idle instead of OFF. We will consider it as OFF" + # ) # new_hvac_mode = HVACMode.OFF _LOGGER.info( @@ -1687,7 +1710,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): old_hvac_action, ) - _LOGGER.debug("%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", self, self._last_change_time, old_state_date_changed, old_state_date_updated, new_state_date_changed, new_state_date_updated) + if new_hvac_mode in [ + HVACMode.OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.DRY, + HVACMode.AUTO, + HVACMode.FAN_ONLY, + None, + ]: + self._hvac_mode = new_hvac_mode # Interpretation of hvac action HVAC_ACTION_ON = [ # pylint: disable=invalid-name @@ -1733,21 +1766,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): if new_state_date_updated and self._last_change_time: delta = (new_state_date_updated - self._last_change_time).total_seconds() if delta < 10: - _LOGGER.info("%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", self + _LOGGER.info( + "%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", + self, ) await end_climate_changed(changes) return - if new_hvac_mode in [ - HVACMode.OFF, - HVACMode.HEAT, - HVACMode.COOL, - HVACMode.HEAT_COOL, - HVACMode.DRY, - HVACMode.AUTO, - HVACMode.FAN_ONLY, - None - ] and self._hvac_mode != new_hvac_mode: + if ( + new_hvac_mode + in [ + HVACMode.OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.HEAT_COOL, + HVACMode.DRY, + HVACMode.AUTO, + HVACMode.FAN_ONLY, + None, + ] + and self._hvac_mode != new_hvac_mode + ): changes = True self._hvac_mode = new_hvac_mode # Update all underlyings state @@ -1757,15 +1796,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): if not changes: # try to manage new target temperature set if state - _LOGGER.debug("Do temperature check. temperature is %s, new_state.attributes is %s", self.target_temperature, new_state.attributes) - if self._is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature: - _LOGGER.info("%s - Target temp in underlying have change to %s", self, new_target_temp) - await self.async_set_temperature(temperature = new_target_temp) + _LOGGER.debug( + "Do temperature check. temperature is %s, new_state.attributes is %s", + self.target_temperature, + new_state.attributes, + ) + if ( + self._is_over_climate + and new_state.attributes + and (new_target_temp := new_state.attributes.get("temperature")) + and new_target_temp != self.target_temperature + ): + _LOGGER.info( + "%s - Target temp in underlying have change to %s", + self, + new_target_temp, + ) + await self.async_set_temperature(temperature=new_target_temp) changes = True await end_climate_changed(changes) - @callback async def _async_update_temp(self, state: State): """Update thermostat with latest state from sensor.""" @@ -2208,13 +2259,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): ) # Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security ! - shouldClimateBeInSecurity = False # temp_cond and climate_cond + shouldClimateBeInSecurity = False # temp_cond and climate_cond shouldSwitchBeInSecurity = temp_cond and switch_cond shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity - shouldStartSecurity = mode_cond and not self._security_state and shouldBeInSecurity + shouldStartSecurity = ( + mode_cond and not self._security_state and shouldBeInSecurity + ) # attr_preset_mode is not necessary normaly. It is just here to be sure - shouldStopSecurity = self._security_state and not shouldBeInSecurity and self._attr_preset_mode == PRESET_SECURITY + shouldStopSecurity = ( + self._security_state + and not shouldBeInSecurity + and self._attr_preset_mode == PRESET_SECURITY + ) # Logging and event if shouldStartSecurity: @@ -2376,7 +2433,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): _LOGGER.debug("%s - recalculate all", self) if not self._is_over_climate: self._prop_algorithm.calculate( - self._target_temp, self._cur_temp, self._cur_ext_temp + self._target_temp, + self._cur_temp, + self._cur_ext_temp, + self._hvac_mode == HVACMode.COOL, ) self.update_custom_attributes() self.async_write_ha_state() @@ -2463,18 +2523,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): "max_power_sensor_entity_id": self._max_power_sensor_entity_id, } if self._is_over_climate: - self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[ - 0 - ].entity_id - self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[ - 1 - ].entity_id if len(self._underlyings) > 1 else None - self._attr_extra_state_attributes["underlying_climate_2"] = self._underlyings[ - 2 - ].entity_id if len(self._underlyings) > 2 else None - self._attr_extra_state_attributes["underlying_climate_3"] = self._underlyings[ - 3 - ].entity_id if len(self._underlyings) > 3 else None + self._attr_extra_state_attributes[ + "underlying_climate_0" + ] = self._underlyings[0].entity_id + self._attr_extra_state_attributes["underlying_climate_1"] = ( + self._underlyings[1].entity_id if len(self._underlyings) > 1 else None + ) + self._attr_extra_state_attributes["underlying_climate_2"] = ( + self._underlyings[2].entity_id if len(self._underlyings) > 2 else None + ) + self._attr_extra_state_attributes["underlying_climate_3"] = ( + self._underlyings[3].entity_id if len(self._underlyings) > 3 else None + ) self._attr_extra_state_attributes[ "start_hvac_action_date" @@ -2566,7 +2626,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): # If the changed preset is active, change the current temperature # Issue #119 - reload new preset temperature also in ac mode if preset.startswith(self._attr_preset_mode): - await self._async_set_preset_mode_internal(preset.rstrip(PRESET_AC_SUFFIX), force=True) + await self._async_set_preset_mode_internal( + preset.rstrip(PRESET_AC_SUFFIX), force=True + ) await self._async_control_heating(force=True) async def service_set_security(self, delay_min, min_on_percent, default_on_percent): diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 1c60c45..cf8c32f 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -227,6 +227,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): PROPORTIONAL_FUNCTION_TPI, ] ), + vol.Optional(CONF_AC_MODE, default=False): cv.boolean, } ) @@ -244,7 +245,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): vol.Optional(CONF_CLIMATE_4): selector.EntitySelector( selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN), ), - vol.Optional(CONF_AC_MODE, default=False): cv.boolean, } ) diff --git a/custom_components/versatile_thermostat/prop_algorithm.py b/custom_components/versatile_thermostat/prop_algorithm.py index 3be623f..f4f78df 100644 --- a/custom_components/versatile_thermostat/prop_algorithm.py +++ b/custom_components/versatile_thermostat/prop_algorithm.py @@ -45,19 +45,33 @@ class PropAlgorithm: self._default_on_percent = 0 def calculate( - self, target_temp: float, current_temp: float, ext_current_temp: float + self, + target_temp: float, + current_temp: float, + ext_current_temp: float, + cooling=False, ): """Do the calculation of the duration""" if target_temp is None or current_temp is None: _LOGGER.warning( - "Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long + "Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating/cooling will be disabled" # pylint: disable=line-too-long ) self._calculated_on_percent = 0 else: - delta_temp = target_temp - current_temp - delta_ext_temp = ( - target_temp - ext_current_temp if ext_current_temp is not None else 0 - ) + if cooling: + delta_temp = current_temp - target_temp + delta_ext_temp = ( + ext_current_temp + if ext_current_temp is not None + else 0 - target_temp + ) + else: + delta_temp = target_temp - current_temp + delta_ext_temp = ( + target_temp - ext_current_temp + if ext_current_temp is not None + else 0 + ) if self._function == PROPORTIONAL_FUNCTION_TPI: self._calculated_on_percent = ( diff --git a/custom_components/versatile_thermostat/tests/const.py b/custom_components/versatile_thermostat/tests/const.py index 3127e67..1eb3a15 100644 --- a/custom_components/versatile_thermostat/tests/const.py +++ b/custom_components/versatile_thermostat/tests/const.py @@ -97,6 +97,7 @@ 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, } MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { @@ -105,6 +106,7 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { CONF_HEATER_3: "switch.mock_4switch2", CONF_HEATER_4: "switch.mock_4switch3", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_AC_MODE: False, } MOCK_TH_OVER_SWITCH_TPI_CONFIG = { @@ -114,7 +116,6 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = { CONF_CLIMATE: "climate.mock_climate", - CONF_AC_MODE: False, } MOCK_PRESETS_CONFIG = { diff --git a/custom_components/versatile_thermostat/tests/test_config_flow.py b/custom_components/versatile_thermostat/tests/test_config_flow.py index d734f4b..a7545ec 100644 --- a/custom_components/versatile_thermostat/tests/test_config_flow.py +++ b/custom_components/versatile_thermostat/tests/test_config_flow.py @@ -378,6 +378,7 @@ async def test_user_config_flow_over_4_switches( CONF_HEATER_3: "switch.mock_switch3", CONF_HEATER_4: "switch.mock_switch4", CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, + CONF_AC_MODE: False, } result = await hass.config_entries.flow.async_init( diff --git a/custom_components/versatile_thermostat/tests/test_tpi.py b/custom_components/versatile_thermostat/tests/test_tpi.py index 4225190..7e73b5d 100644 --- a/custom_components/versatile_thermostat/tests/test_tpi.py +++ b/custom_components/versatile_thermostat/tests/test_tpi.py @@ -5,7 +5,7 @@ from .commons import * # pylint: disable=wildcard-import, unused-wildcard-impor @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) -async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state): +async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state: None): """Test the TPI calculation""" entry = MockConfigEntry( @@ -50,36 +50,52 @@ async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state): assert tpi_algo.off_time_sec == 0 assert entity.mean_cycle_power is None # no device power configured - tpi_algo.calculate(15, 14, 5) + tpi_algo.calculate(15, 14, 5, False) assert tpi_algo.on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.on_time_sec == 120 assert tpi_algo.off_time_sec == 180 tpi_algo.set_security(0.1) - tpi_algo.calculate(15, 14, 5) + tpi_algo.calculate(15, 14, 5, False) assert tpi_algo.on_percent == 0.1 assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.on_time_sec == 30 # >= minimal_activation_delay (=30) assert tpi_algo.off_time_sec == 270 tpi_algo.unset_security() - tpi_algo.calculate(15, 14, 5) + tpi_algo.calculate(15, 14, 5, False) assert tpi_algo.on_percent == 0.4 assert tpi_algo.calculated_on_percent == 0.4 assert tpi_algo.on_time_sec == 120 assert tpi_algo.off_time_sec == 180 # Test minimal activation delay - tpi_algo.calculate(15, 14.7, 15) + tpi_algo.calculate(15, 14.7, 15, False) assert tpi_algo.on_percent == 0.09 assert tpi_algo.calculated_on_percent == 0.09 assert tpi_algo.on_time_sec == 0 assert tpi_algo.off_time_sec == 300 tpi_algo.set_security(0.09) - tpi_algo.calculate(15, 14.7, 15) + tpi_algo.calculate(15, 14.7, 15, False) assert tpi_algo.on_percent == 0.09 assert tpi_algo.calculated_on_percent == 0.09 assert tpi_algo.on_time_sec == 0 assert tpi_algo.off_time_sec == 300 + + tpi_algo.unset_security() + tpi_algo.calculate(25, 30, 35, True) + assert tpi_algo.on_percent == 1 + assert tpi_algo.calculated_on_percent == 1 + assert tpi_algo.on_time_sec == 300 + assert tpi_algo.off_time_sec == 0 + assert entity.mean_cycle_power is None # no device power configured + + tpi_algo.set_security(0.09) + tpi_algo.calculate(25, 30, 35, True) + assert tpi_algo.on_percent == 0.09 + assert tpi_algo.calculated_on_percent == 1 + assert tpi_algo.on_time_sec == 0 + assert tpi_algo.off_time_sec == 300 + assert entity.mean_cycle_power is None # no device power configured diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index 613d9ae..3ac3f78 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -246,7 +246,7 @@ class UnderlyingSwitch(UnderlyingEntity): return # If we should heat, starts the cycle with delay - if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0: + if self._hvac_mode in [HVACMode.HEAT, HVACMode.COOL] and on_time_sec > 0: # Starts the cycle after the initial delay self._async_cancel_cycle = self.call_later( self._hass, self._initial_delay_sec, self._turn_on_later