def calculate_offset(event, offset): """Calculate event offset. Return the updated event with the offset_time included. """ summary = event.get('summary', '') # check if we have an offset tag in the message # time is HH:MM or MM reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(offset) search = re.search(reg, summary) if search and search.group(1): time = search.group(1) if ':' not in time: if time[0] == '+' or time[0] == '-': time = '{}0:{}'.format(time[0], time[1:]) else: time = '0:{}'.format(time) offset_time = time_period_str(time) summary = (summary[:search.start()] + summary[search.end():]).strip() event['summary'] = summary else: offset_time = dt.dt.timedelta() # default it event['offset_time'] = offset_time return event
async def async_step_misc(self, user_input=None): from . import get_update_interval errors = {} if user_input is not None: self.options[KEY_LISTEN_TO_BROADCASTS] = user_input[ KEY_LISTEN_TO_BROADCASTS] try: self.options["update_interval"] = cv.time_period_str( user_input["update_interval"]).total_seconds() except vol.Error: errors["update_interval"] = "invalid_time_period" else: return await self.async_step_units() return self.async_show_form( step_id="misc", data_schema=vol.Schema({ vol.Required( "update_interval", default=str(get_update_interval(self.config_entry)), ): str, vol.Required( KEY_LISTEN_TO_BROADCASTS, default=get_listen_to_broadcasts(self.config_entry), ): bool, }), errors=errors, )
def parse_timedelta_or_default(input_str: str, default: datetime.timedelta = None) -> datetime.timedelta: if not input_str: return default try: return cv.time_period_str(input_str) except vol.Invalid: return default
async def test_set_auto_off_service_fail(hass, mock_bridge, mock_api, caplog): """Test set auto off service failed.""" await init_integration(hass) assert mock_bridge device = DUMMY_WATER_HEATER_DEVICE entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" with patch( "homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown", return_value=None, ) as mock_set_auto_shutdown: await hass.services.async_call( DOMAIN, SERVICE_SET_AUTO_OFF_NAME, { ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET }, blocking=True, ) assert mock_api.call_count == 2 mock_set_auto_shutdown.assert_called_once_with( time_period_str(DUMMY_AUTO_OFF_SET)) assert (f"Call api for {device.name} failed, api: 'set_auto_shutdown'" in caplog.text) state = hass.states.get(entity_id) assert state.state == STATE_UNAVAILABLE
def calculate_offset(event, offset): """Calculate event offset. Return the updated event with the offset_time included. """ summary = event.get("summary", "") # check if we have an offset tag in the message # time is HH:MM or MM reg = f"{offset}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)" search = re.search(reg, summary) if search and search.group(1): time = search.group(1) if ":" not in time: if time[0] == "+" or time[0] == "-": time = f"{time[0]}0:{time[1:]}" else: time = f"0:{time}" offset_time = time_period_str(time) summary = (summary[:search.start()] + summary[search.end():]).strip() event["summary"] = summary else: offset_time = dt.dt.timedelta() # default it event["offset_time"] = offset_time return event
def __init__(self, config: Dict): """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 = self._duration self._restore = config.get(CONF_RESTORE, DEFAULT_RESTORE) if self._restore: self._restore_grace_period: Optional[timedelta] \ = cv.time_period_str(config.get(CONF_RESTORE_GRACE_PERIOD, DEFAULT_RESTORE_GRACE_PERIOD)) else: self._restore_grace_period: Optional[timedelta] = None self._end: Optional[datetime] = None self._listener = None
async def async_update_config(self, config: Dict) -> None: """Handle when the config is updated.""" self._config = config self._duration = cv.time_period_str(config[CONF_DURATION]) self._restore = config.get(CONF_RESTORE, DEFAULT_RESTORE) if self._restore: self._restore_grace_period: Optional[timedelta] \ = cv.time_period_str(config.get(CONF_RESTORE_GRACE_PERIOD, DEFAULT_RESTORE_GRACE_PERIOD)) self._restore_state(self._state, self.state_attributes) else: self._listener = None self._state = STATUS_IDLE self._end = None self._remaining = timedelta() self._restore_grace_period = timedelta() self.async_write_ha_state()
def __init__(self, config: dict): """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 = None
def handle_task(self, entity_id, operation, **kwargs): """ handle task when time arrive. if handler take long time, use hass.async_add_job(func) to exec in another thread. """ _LOGGER.debug("handle_task(%s): operation = %s.", entity_id, operation) task = self._get_task(entity_id) task['handle'] = None task['remaining'] = '0:00:00' if operation == 'temporary_on': ratio = self._ratio if task['operation'] == operation else 1 delay = int( time_period_str(task['duration']).total_seconds() * ratio) task['handle'] = self._queue.insert(entity_id, str(timedelta(seconds=delay)), self.handle_task, 'temporary_off') task['next_operation'] = 'off' task['exec_time'] = datetime.now( ) + self._queue.get_remaining_time(task['handle']) operation = 'on' elif operation == 'temporary_off': ratio = self._ratio if task['operation'] == operation else 1 delay = int( time_period_str(task['duration']).total_seconds() * ratio) task['handle'] = self._queue.insert(entity_id, str(timedelta(seconds=delay)), self.handle_task, 'temporary_on') task['next_operation'] = 'on' task['exec_time'] = datetime.now( ) + self._queue.get_remaining_time(task['handle']) operation = 'off' elif operation == 'custom': pass service = 'turn_' + operation state = operation self.set_state(entity_id, state=state, service=service, force_update=True) _LOGGER.debug("handle_task finish:{}({})".format(service, entity_id))
async def async_update_config(self, config: Dict) -> None: """Handle when the config is updated.""" self._config = config self._duration = cv.time_period_str(config[CONF_DURATION]) self._restore = config.get(CONF_RESTORE, DEFAULT_RESTORE) if self._restore: self._restore_grace_period: Optional[timedelta] \ = config.get(CONF_RESTORE_GRACE_PERIOD, DEFAULT_RESTORE_GRACE_PERIOD) else: self._restore_grace_period: Optional[timedelta] = None self.async_write_ha_state()
async def test_set_auto_off_service( hass: HomeAssistantType, mock_bridge: Generator[None, Any, None], mock_api: Generator[None, Any, None], hass_owner_user: MockUser, ) -> None: """Test the set_auto_off service.""" assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) await hass.async_block_till_done() assert hass.services.has_service(DOMAIN, SERVICE_SET_AUTO_OFF_NAME) await hass.services.async_call( DOMAIN, SERVICE_SET_AUTO_OFF_NAME, { CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET }, blocking=True, context=Context(user_id=hass_owner_user.id), ) with raises(UnknownUser) as unknown_user_exc: await hass.services.async_call( DOMAIN, SERVICE_SET_AUTO_OFF_NAME, { CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET }, blocking=True, context=Context(user_id="not_real_user"), ) assert unknown_user_exc.type is UnknownUser with patch( "homeassistant.components.switcher_kis.switch.SwitcherV2Api.set_auto_shutdown" ) as mock_set_auto_shutdown: await hass.services.async_call( DOMAIN, SERVICE_SET_AUTO_OFF_NAME, { CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET }, ) await hass.async_block_till_done() mock_set_auto_shutdown.assert_called_once_with( time_period_str(DUMMY_AUTO_OFF_SET))
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._attr_should_poll = False self._attr_force_update = True
def update(self): """Search for the next event.""" if not self.data or not self.data.update(): # update cached, don't do anything return if not self.data.event: # we have no event to work on, make sure we're clean self.cleanup() return def _get_date(date): """Get the dateTime from date or dateTime as a local.""" if 'date' in date: return dt.as_utc( dt.dt.datetime.combine(dt.parse_date(date['date']), dt.dt.time())) else: return dt.parse_datetime(date['dateTime']) start = _get_date(self.data.event['start']) end = _get_date(self.data.event['end']) summary = self.data.event['summary'] # check if we have an offset tag in the message # time is HH:MM or MM reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset) search = re.search(reg, summary) if search and search.group(1): time = search.group(1) if ':' not in time: if time[0] == '+' or time[0] == '-': time = '{}0:{}'.format(time[0], time[1:]) else: time = '0:{}'.format(time) offset_time = time_period_str(time) summary = (summary[:search.start()] + summary[search.end():]) \ .strip() else: offset_time = dt.dt.timedelta() # default it # cleanup the string so we don't have a bunch of double+ spaces self._cal_data['message'] = re.sub(' +', '', summary).strip() self._cal_data['offset_time'] = offset_time self._cal_data['location'] = self.data.event.get('location', '') self._cal_data['description'] = self.data.event.get('description', '') self._cal_data['start'] = start self._cal_data['end'] = end self._cal_data['all_day'] = 'date' in self.data.event['start']
def update(self): """Search for the next event.""" if not self.data or not self.data.update(): # update cached, don't do anything return if not self.data.event: # we have no event to work on, make sure we're clean self.cleanup() return def _get_date(date): """Get the dateTime from date or dateTime as a local.""" if 'date' in date: return dt.start_of_local_day(dt.dt.datetime.combine( dt.parse_date(date['date']), dt.dt.time.min)) else: return dt.as_local(dt.parse_datetime(date['dateTime'])) start = _get_date(self.data.event['start']) end = _get_date(self.data.event['end']) summary = self.data.event.get('summary', '') # check if we have an offset tag in the message # time is HH:MM or MM reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset) search = re.search(reg, summary) if search and search.group(1): time = search.group(1) if ':' not in time: if time[0] == '+' or time[0] == '-': time = '{}0:{}'.format(time[0], time[1:]) else: time = '0:{}'.format(time) offset_time = time_period_str(time) summary = (summary[:search.start()] + summary[search.end():]) \ .strip() else: offset_time = dt.dt.timedelta() # default it # cleanup the string so we don't have a bunch of double+ spaces self._cal_data['message'] = re.sub(' +', '', summary).strip() self._cal_data['offset_time'] = offset_time self._cal_data['location'] = self.data.event.get('location', '') self._cal_data['description'] = self.data.event.get('description', '') self._cal_data['start'] = start self._cal_data['end'] = end self._cal_data['all_day'] = 'date' in self.data.event['start']
def insert(self, task_id, duration, callback, operation='turn_off', **kwargs): """ add new task into queue """ if duration == "0:00:00": return None second = time_period_str(duration).total_seconds() loop = second / len(self.__queue) slot = (second + self.__current_slot - 1) % len(self.__queue) delayQueueTask = DelayQueueTask(task_id, operation, int(slot), loop, callback, kwargs=kwargs) self.__queue[delayQueueTask.slot].append(delayQueueTask) _LOGGER.debug("create task:{}/{}".format(delayQueueTask.slot, delayQueueTask.loop)) return delayQueueTask
def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.timedelta]: """Extract the offset from the event summary. Return a tuple with the updated event summary and offset time. """ # check if we have an offset tag in the message # time is HH:MM or MM reg = f"{offset_prefix}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)" search = re.search(reg, summary) if search and search.group(1): time = search.group(1) if ":" not in time: if time[0] == "+" or time[0] == "-": time = f"{time[0]}0:{time[1:]}" else: time = f"0:{time}" offset_time = time_period_str(time) summary = (summary[:search.start()] + summary[search.end():]).strip() return (summary, offset_time) return (summary, datetime.timedelta())
async def test_set_auto_off_service(hass, mock_bridge, mock_api): """Test the set auto off service.""" await init_integration(hass) assert mock_bridge device = DUMMY_WATER_HEATER_DEVICE entity_id = f"{SWITCH_DOMAIN}.{slugify(device.name)}" with patch( "homeassistant.components.switcher_kis.switch.SwitcherApi.set_auto_shutdown" ) as mock_set_auto_shutdown: await hass.services.async_call( DOMAIN, SERVICE_SET_AUTO_OFF_NAME, { ATTR_ENTITY_ID: entity_id, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET }, blocking=True, ) assert mock_api.call_count == 2 mock_set_auto_shutdown.assert_called_once_with( time_period_str(DUMMY_AUTO_OFF_SET))
def _restore_state(self, restored_state, state_attributes) -> None: if restored_state not in VIABLE_STATUSES: self._state = STATUS_IDLE self._state = restored_state # restore last duration if config doesn't have a default if not self._config[CONF_DURATION] \ and not state_attributes.get(ATTR_DURATION) == "None": try: duration_data = list( map(int, str(state_attributes.get(ATTR_DURATION)).split(":"))) self._config[CONF_DURATION] = timedelta( hours=duration_data[0], minutes=duration_data[1], seconds=duration_data[2]) except ValueError: self._config[CONF_DURATION] = timedelta(DEFAULT_DURATION) self._duration = cv.time_period_str(config[CONF_DURATION]) # restore remaining (needed for paused state) if self._state == STATUS_PAUSED \ and not state_attributes.get(ATTR_REMAINING) == "None" \ and not state_attributes.get(ATTR_REMAINING) == str(timedelta()): try: remaining_dt = list( map(int, str(state_attributes.get(ATTR_REMAINING)).split(":"))) self._remaining = timedelta(hours=remaining_dt[0], minutes=remaining_dt[1], seconds=remaining_dt[2]) except ValueError: self._remaining = self._duration else: self._remaining = timedelta() # restore end time try: if state_attributes.get(ATTR_FINISHES_AT) is not None: self._end = datetime.strptime( state_attributes.get(ATTR_FINISHES_AT), "%Y-%m-%d %H:%M:%S%z") else: self._end = None except ValueError: self._end = None # timer was active if self._state == STATUS_ACTIVE: try: # account for lost time if self._end: self._remaining = self._end - dt_util.utcnow().replace( microsecond=0) else: self._remaining = timedelta() _LOGGER.debug("%s : Restored remaining: %s", self._config.get(CONF_NAME), _format_timedelta(self._remaining)) # only restore if restore_grace_period not exceeded if self._remaining + self._restore_grace_period >= timedelta(): self._state = STATUS_PAUSED self._end = None self.async_start(None) else: self._state = STATUS_IDLE except ValueError: self._remaining = timedelta() self._end = None self._state = STATUS_IDLE
def _timespan_secs(timespan: str | None) -> None | float: """Parse a time-span into number of seconds.""" if timespan in UNAVAILABLE_VALUES: return None return time_period_str(timespan).total_seconds() # type: ignore[arg-type]
async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" self._config = config self._duration = cv.time_period_str(config[CONF_DURATION]) self.async_write_ha_state()