async def test_update(hass, hass_ws_client, storage_setup): """Test updating timer entity.""" assert await storage_setup() timer_id = "from_storage" timer_entity_id = f"{DOMAIN}.{DOMAIN}_{timer_id}" ent_reg = await entity_registry.async_get_registry(hass) state = hass.states.get(timer_entity_id) assert state.attributes[ATTR_FRIENDLY_NAME] == "timer from storage" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) == timer_entity_id client = await hass_ws_client(hass) await client.send_json({ "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{timer_id}", CONF_DURATION: 33, }) resp = await client.receive_json() assert resp["success"] state = hass.states.get(timer_entity_id) assert state.attributes[ATTR_DURATION] == str(cv.time_period(33))
async def test_ws_create(hass, hass_ws_client, storage_setup): """Test create WS.""" assert await storage_setup(items=[]) timer_id = "new_timer" timer_entity_id = f"{DOMAIN}.{timer_id}" ent_reg = await entity_registry.async_get_registry(hass) state = hass.states.get(timer_entity_id) assert state is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) is None client = await hass_ws_client(hass) await client.send_json({ "id": 6, "type": f"{DOMAIN}/create", CONF_NAME: "New Timer", CONF_DURATION: 42, }) resp = await client.receive_json() assert resp["success"] state = hass.states.get(timer_entity_id) assert state.state == STATUS_IDLE assert state.attributes[ATTR_DURATION] == str(cv.time_period(42)) assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) == timer_entity_id
async def _async_delay(self, action, variables, context): """Handle delay.""" # Call ourselves in the future to continue work unsub = None @callback def async_script_delay(now): """Handle delay.""" with suppress(ValueError): self._async_listener.remove(unsub) self.hass.async_create_task(self.async_run(variables, context)) delay = action[CONF_DELAY] try: if isinstance(delay, template.Template): delay = vol.All(cv.time_period, cv.positive_timedelta)( delay.async_render(variables)) elif isinstance(delay, dict): delay_data = {} delay_data.update(template.render_complex(delay, variables)) delay = cv.time_period(delay_data) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' delay template: %s", self.name, ex) raise _StopScript self.last_action = action.get(CONF_ALIAS, "delay {}".format(delay)) self._log("Executing step %s" % self.last_action) unsub = async_track_point_in_utc_time(self.hass, async_script_delay, date_util.utcnow() + delay) self._async_listener.append(unsub) raise _SuspendScript
async def test_config_options(hass): """Test configuration options.""" count_start = len(hass.states.async_entity_ids()) _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) config = { DOMAIN: { "test_1": {}, "test_2": { CONF_NAME: "Hello World", CONF_ICON: "mdi:work", CONF_DURATION: 10, }, "test_3": None, } } assert await async_setup_component(hass, "timer", config) await hass.async_block_till_done() assert count_start + 3 == len(hass.states.async_entity_ids()) await hass.async_block_till_done() state_1 = hass.states.get("timer.test_1") state_2 = hass.states.get("timer.test_2") state_3 = hass.states.get("timer.test_3") assert state_1 is not None assert state_2 is not None assert state_3 is not None assert state_1.state == STATUS_IDLE assert ATTR_ICON not in state_1.attributes assert ATTR_FRIENDLY_NAME not in state_1.attributes assert state_2.state == STATUS_IDLE assert state_2.attributes.get(ATTR_FRIENDLY_NAME) == "Hello World" assert state_2.attributes.get(ATTR_ICON) == "mdi:work" assert state_2.attributes.get(ATTR_DURATION) == "0:00:10" assert state_3.state == STATUS_IDLE assert str(cv.time_period(DEFAULT_DURATION)) == state_3.attributes.get( CONF_DURATION )
class Timer(RestoreEntity): """Representation of a timer.""" def __init__(self, config: dict) -> None: """Initialize a timer.""" self._config: dict = config self.editable: bool = True self._state: str = STATUS_IDLE self._duration = cv.time_period_str(config[CONF_DURATION]) self._remaining: timedelta | None = None self._end: datetime | None = None self._listener: Callable[[], None] | None = None self._restore: bool = self._config.get(CONF_RESTORE, DEFAULT_RESTORE) self._attr_should_poll = False self._attr_force_update = True @classmethod def from_yaml(cls, config: dict) -> Timer: """Return entity instance initialized from yaml storage.""" timer = cls(config) timer.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) timer.editable = False return timer @property def name(self): """Return name of the timer.""" return self._config.get(CONF_NAME) @property def icon(self): """Return the icon to be used for this entity.""" return self._config.get(CONF_ICON) @property def state(self): """Return the current value of the timer.""" return self._state @property def extra_state_attributes(self): """Return the state attributes.""" attrs = { ATTR_DURATION: _format_timedelta(self._duration), ATTR_EDITABLE: self.editable, } if self._end is not None: attrs[ATTR_FINISHES_AT] = self._end.isoformat() if self._remaining is not None: attrs[ATTR_REMAINING] = _format_timedelta(self._remaining) if self._restore: attrs[ATTR_RESTORE] = self._restore return attrs @property def unique_id(self) -> str | None: """Return unique id for the entity.""" return self._config[CONF_ID] async def async_added_to_hass(self): """Call when entity is about to be added to Home Assistant.""" # If we don't need to restore a previous state or no previous state exists, # start at idle if not self._restore or (state := await self.async_get_last_state()) is None: self._state = STATUS_IDLE return # Begin restoring state self._state = state.state self._duration = cv.time_period(state.attributes[ATTR_DURATION]) # Nothing more to do if the timer is idle if self._state == STATUS_IDLE: return # If the timer was paused, we restore the remaining time if self._state == STATUS_PAUSED: self._remaining = cv.time_period(state.attributes[ATTR_REMAINING]) return # If we get here, the timer must have been active so we need to decide what # to do based on end time and the current time end = cv.datetime(state.attributes[ATTR_FINISHES_AT]) # If there is time remaining in the timer, restore the remaining time then # start the timer if (remaining := end - dt_util.utcnow().replace(microsecond=0)) > timedelta(0): self._remaining = remaining self._state = STATUS_PAUSED self.async_start()
async def async_run(self, variables: Optional[Sequence] = None, context: Optional[Context] = None) -> None: """Run script. This method is a coroutine. """ self.last_triggered = date_util.utcnow() if self._cur == -1: self._log('Running script') self._cur = 0 # Unregister callback if we were in a delay or wait but turn on is # called again. In that case we just continue execution. self._async_remove_listener() for cur, action in islice(enumerate(self.sequence), self._cur, None): if CONF_DELAY in action: # Call ourselves in the future to continue work unsub = None @callback def async_script_delay(now): """Handle delay.""" # pylint: disable=cell-var-from-loop self._async_listener.remove(unsub) self.hass.async_create_task( self.async_run(variables, context)) delay = action[CONF_DELAY] try: if isinstance(delay, template.Template): delay = vol.All(cv.time_period, cv.positive_timedelta)( delay.async_render(variables)) elif isinstance(delay, dict): delay_data = {} delay_data.update( template.render_complex(delay, variables)) delay = cv.time_period(delay_data) except (TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' delay template: %s", self.name, ex) break unsub = async_track_point_in_utc_time( self.hass, async_script_delay, date_util.utcnow() + delay) self._async_listener.append(unsub) self._cur = cur + 1 if self._change_listener: self.hass.async_add_job(self._change_listener) return if CONF_WAIT_TEMPLATE in action: # Call ourselves in the future to continue work wait_template = action[CONF_WAIT_TEMPLATE] wait_template.hass = self.hass # check if condition already okay if condition.async_template(self.hass, wait_template, variables): continue @callback def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" self._async_remove_listener() self.hass.async_create_task( self.async_run(variables, context)) self._async_listener.append( async_track_template(self.hass, wait_template, async_script_wait, variables)) self._cur = cur + 1 if self._change_listener: self.hass.async_add_job(self._change_listener) if CONF_TIMEOUT in action: self._async_set_timeout(action, variables, context, action.get(CONF_CONTINUE, True)) return if CONF_CONDITION in action: if not self._async_check_condition(action, variables): break elif CONF_EVENT in action: self._async_fire_event(action, variables, context) else: await self._async_call_service(action, variables, context) self._cur = -1 self.last_action = None if self._change_listener: self.hass.async_add_job(self._change_listener)
async def async_run(self, variables: Optional[Sequence] = None, context: Optional[Context] = None) -> None: """Run script. This method is a coroutine. """ self.last_triggered = date_util.utcnow() if self._cur == -1: self._log('Running script') self._cur = 0 # Unregister callback if we were in a delay or wait but turn on is # called again. In that case we just continue execution. self._async_remove_listener() for cur, action in islice(enumerate(self.sequence), self._cur, None): if CONF_DELAY in action: # Call ourselves in the future to continue work unsub = None @callback def async_script_delay(now): """Handle delay.""" # pylint: disable=cell-var-from-loop with suppress(ValueError): self._async_listener.remove(unsub) self.hass.async_create_task( self.async_run(variables, context)) delay = action[CONF_DELAY] try: if isinstance(delay, template.Template): delay = vol.All( cv.time_period, cv.positive_timedelta)( delay.async_render(variables)) elif isinstance(delay, dict): delay_data = {} delay_data.update( template.render_complex(delay, variables)) delay = cv.time_period(delay_data) except (TemplateError, vol.Invalid) as ex: _LOGGER.error("Error rendering '%s' delay template: %s", self.name, ex) break unsub = async_track_point_in_utc_time( self.hass, async_script_delay, date_util.utcnow() + delay ) self._async_listener.append(unsub) self._cur = cur + 1 if self._change_listener: self.hass.async_add_job(self._change_listener) return if CONF_WAIT_TEMPLATE in action: # Call ourselves in the future to continue work wait_template = action[CONF_WAIT_TEMPLATE] wait_template.hass = self.hass # check if condition already okay if condition.async_template( self.hass, wait_template, variables): continue @callback def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" self._async_remove_listener() self.hass.async_create_task( self.async_run(variables, context)) self._async_listener.append(async_track_template( self.hass, wait_template, async_script_wait, variables)) self._cur = cur + 1 if self._change_listener: self.hass.async_add_job(self._change_listener) if CONF_TIMEOUT in action: self._async_set_timeout( action, variables, context, action.get(CONF_CONTINUE, True)) return if CONF_CONDITION in action: if not self._async_check_condition(action, variables): break elif CONF_EVENT in action: self._async_fire_event(action, variables, context) else: await self._async_call_service(action, variables, context) self._cur = -1 self.last_action = None if self._change_listener: self.hass.async_add_job(self._change_listener)