async def async_update_callback(self, device_id=None): """Let OPP know there has been an update from the controller.""" # Track changes in connection state if not self._controller.is_connected and self._connected: # Connection has dropped self._connected = False reconnect_minutes = 1 + randrange(10) _LOGGER.error( "Connection to %s API was lost. Reconnecting in %i minutes", self._device_type, reconnect_minutes, ) # Schedule reconnection async def try_connect(_now): await self._controller.connect() async_call_later(self.opp, reconnect_minutes * 60, try_connect) if self._controller.is_connected and not self._connected: # Connection has been restored self._connected = True _LOGGER.debug("Connection to %s API was restored", self._device_type) if not device_id or self._device_id == device_id: # Update all devices if no device_id was specified _LOGGER.debug( "%s API sent a status update for device %s", self._device_type, device_id, ) self.async_schedule_update_op_state(True)
async def fetching_data(self, *_): """Get the latest data from yr.no.""" def try_again(err: str): """Retry in 15 to 20 minutes.""" minutes = 15 + randrange(6) _LOGGER.error("Retrying in %i minutes: %s", minutes, err) async_call_later(self.opp, minutes * 60, self.fetching_data) try: websession = async_get_clientsession(self.opp) with async_timeout.timeout(10): resp = await websession.get(self._url, params=self._urlparams) if resp.status != 200: try_again(f"{resp.url} returned {resp.status}") return text = await resp.text() except (asyncio.TimeoutError, aiohttp.ClientError) as err: try_again(err) return try: self.data = xmltodict.parse(text)["weatherdata"] except (ExpatError, IndexError) as err: try_again(err) return await self.updating_devices() async_call_later(self.opp, 60 * 60, self.fetching_data)
async def async_update(self): """Update the sensor.""" await self._data.async_update() if not self.available: # Entity cannot be removed while its being added async_call_later(self.opp, 1, self._remove) return package = self._data.packages.get(self._tracking_number, None) # If the user has elected to not see delivered packages and one gets # delivered, post a notification: if package.status == VALUE_DELIVERED and not self._data.show_delivered: self._notify_delivered() # Entity cannot be removed while its being added async_call_later(self.opp, 1, self._remove) return self._attrs.update({ ATTR_INFO_TEXT: package.info_text, ATTR_TIMESTAMP: package.timestamp, ATTR_LOCATION: package.location, }) self._state = package.status self._friendly_name = package.friendly_name
async def async_update_prices(self, now): """Update electricity prices from the ESIOS API.""" prices = await self._pvpc_data.async_update_prices(now) if not prices and self._pvpc_data.source_available: self._num_retries += 1 if self._num_retries > 2: _LOGGER.warning( "%s: repeated bad data update, mark component as unavailable source", self.entity_id, ) self._pvpc_data.source_available = False return retry_delay = 2 * self._num_retries * self._pvpc_data.timeout _LOGGER.debug( "%s: Bad update[retry:%d], will try again in %d s", self.entity_id, self._num_retries, retry_delay, ) async_call_later(self.opp, retry_delay, self.async_update_prices) return if not prices: _LOGGER.debug("%s: data source is not yet available", self.entity_id) return self._num_retries = 0 if not self._pvpc_data.source_available: self._pvpc_data.source_available = True _LOGGER.warning("%s: component has recovered data access", self.entity_id) self.update_current_price(now)
def _adapter_watchdog(now=None): _LOGGER.debug("Reached _adapter_watchdog") event.async_call_later(opp, WATCHDOG_INTERVAL, _adapter_watchdog) if not adapter.initialized: _LOGGER.info("Adapter not initialized; Trying to restart") opp.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) adapter.init()
async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" try: with async_timeout.timeout(10): self._forecasts = await self.get_weather_forecast() self._fail_count = 0 except (asyncio.TimeoutError, SmhiForecastException): _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") self._fail_count += 1 if self._fail_count < 3: async_call_later(self.opp, RETRY_TIMEOUT, self.retry_update)
def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) if self.activate_only and not value: _LOGGER.debug("%s: Ignoring turn_off call", self.entity_id) return params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.async_call_service(self._domain, service, params) if self.activate_only: async_call_later(self.opp, 1, self.reset_switch)
async def _attempt_connect(self): """Attempt to connect to the socket (retrying later on fail).""" async def connect(timestamp=None): """Connect.""" await self.client.websocket.connect() try: await connect() except WebsocketError as err: LOGGER.error("Error with the websocket connection: %s", err) self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480) async_call_later(self._opp, self._ws_reconnect_delay, connect)
async def enable_alexa(_): """Enable Alexa.""" aconf = await self.get_alexa_config() try: await aconf.async_enable_proactive_mode() except aiohttp.ClientError as err: # If no internet available yet if self._opp.is_running: logging.getLogger(__package__).warning( "Unable to activate Alexa Report State: %s. Retrying in 30 seconds", err, ) async_call_later(self._opp, 30, enable_alexa) except alexa_errors.NoTokenAvailable: pass
async def _async_stop_scripts_at_shutdown(opp, event): """Stop running Script objects started before shutdown.""" async_call_later(opp, _SHUTDOWN_MAX_WAIT, partial(_async_stop_scripts_after_shutdown, opp)) running_scripts = [ script for script in opp.data[DATA_SCRIPTS] if script["instance"].is_running and script["started_before_shutdown"] ] if running_scripts: names = ", ".join( [script["instance"].name for script in running_scripts]) _LOGGER.debug("Stopping scripts running at shutdown: %s", names) await asyncio.gather( *[script["instance"].async_stop() for script in running_scripts])
async def async_send(self, _): """Write preprocessed events to eventhub, with retry.""" client = self._get_client() async with client: while not self.queue.empty(): data_batch, dequeue_count = await self.fill_batch(client) _LOGGER.debug( "Sending %d event(s), out of %d events in the queue", len(data_batch), dequeue_count, ) if data_batch: try: await client.send_batch(data_batch) except EventHubError as exc: _LOGGER.error( "Error in sending events to Event Hub: %s", exc) finally: for _ in range(dequeue_count): self.queue.task_done() await client.close() if not self.shutdown: self._next_send_remover = async_call_later(self.opp, self._send_interval, self.async_send)
def _handle_event(self, event, device_id): """Check if event applies to me and update.""" if device_id != self._device_id: return _LOGGER.debug( "Binary sensor update (Device ID: %s Class: %s Sub: %s)", event.device.id_string, event.device.__class__.__name__, event.device.subtype, ) self._apply_event(event) self.async_write_op_state() if self._delay_listener: self._delay_listener() self._delay_listener = None if self.is_on and self._off_delay is not None: @callback def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None self._state = False self.async_write_op_state() self._delay_listener = evt.async_call_later( self.opp, self._off_delay, off_delay_listener )
async def async_restart(self, _now: dt = None) -> None: """Restart the subscription assuming the camera rebooted.""" if not self.started: return if self._subscription: # Suppressed. The subscription may no longer exist. with suppress(*SUBSCRIPTION_ERRORS): await self._subscription.Unsubscribe() self._subscription = None try: restarted = await self.async_start() except SUBSCRIPTION_ERRORS: restarted = False if not restarted: LOGGER.warning( "Failed to restart ONVIF PullPoint subscription for '%s'. Retrying", self.unique_id, ) # Try again in a minute self._unsub_refresh = async_call_later(self.opp, 60, self.async_restart) elif self._listeners: LOGGER.debug( "Restarted ONVIF PullPoint subscription for '%s'", self.unique_id ) self.async_schedule_pull()
async def async_update_data(_now): await self.request_update() self._cancel_periodic_update = async_call_later( self.opp, self.config_entry.options[CONF_SCAN_INTERVAL], async_update_data, )
async def _async_prefs_updated(self, prefs): """Handle updated preferences.""" if ALEXA_DOMAIN not in self.opp.config.components and self.enabled: await async_setup_component(self.opp, ALEXA_DOMAIN, {}) if self.should_report_state != self.is_reporting_states: if self.should_report_state: await self.async_enable_proactive_mode() else: await self.async_disable_proactive_mode() # State reporting is reported as a property on entities. # So when we change it, we need to sync all entities. await self.async_sync_entities() return # If user has filter in config.yaml, don't sync. if not self._config[CONF_FILTER].empty_filter: return # If entity prefs are the same, don't sync. if (self._cur_entity_prefs is prefs.alexa_entity_configs and self._cur_default_expose is prefs.alexa_default_expose): return if self._alexa_sync_unsub: self._alexa_sync_unsub() self._alexa_sync_unsub = None if self._cur_default_expose is not prefs.alexa_default_expose: await self.async_sync_entities() return self._alexa_sync_unsub = async_call_later(self.opp, SYNC_DELAY, self._sync_prefs)
async def register_webhook(event): if CONF_WEBHOOK_ID not in entry.data: data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()} opp.config_entries.async_update_entry(entry, data=data) if opp.components.cloud.async_active_subscription(): if CONF_CLOUDHOOK_URL not in entry.data: webhook_url = await opp.components.cloud.async_create_cloudhook( entry.data[CONF_WEBHOOK_ID]) data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url} opp.config_entries.async_update_entry(entry, data=data) else: webhook_url = entry.data[CONF_CLOUDHOOK_URL] else: webhook_url = opp.components.webhook.async_generate_url( entry.data[CONF_WEBHOOK_ID]) if entry.data[ "auth_implementation"] == cloud.DOMAIN and not webhook_url.startswith( "https://"): _LOGGER.warning( "Webhook not registered - " "https and port 443 is required to register the webhook") return try: webhook_register( opp, DOMAIN, "Netatmo", entry.data[CONF_WEBHOOK_ID], async_handle_webhook, ) async def handle_event(event): """Handle webhook events.""" if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: if activation_listener is not None: activation_listener() if activation_timeout is not None: activation_timeout() activation_listener = async_dispatcher_connect( opp, f"signal-{DOMAIN}-webhook-None", handle_event, ) activation_timeout = async_call_later(opp, 30, unregister_webhook) await opp.data[DOMAIN][entry.entry_id ][AUTH].async_addwebhook(webhook_url) _LOGGER.info("Register Netatmo webhook: %s", webhook_url) except pyatmo.ApiError as err: _LOGGER.error("Error during webhook registration - %s", err) entry.async_on_unload( opp.bus.async_listen_once(EVENT_OPENPEERPOWER_STOP, unregister_webhook))
def cluster_command(self, tsn, command_id, args): """Handle commands received to this cluster.""" cmd = parse_and_log_command(self, tsn, command_id, args) if cmd in ("off", "off_with_effect"): self.attribute_updated(self.ON_OFF, False) elif cmd in ("on", "on_with_recall_global_scene"): self.attribute_updated(self.ON_OFF, True) elif cmd == "on_with_timed_off": should_accept = args[0] on_time = args[1] # 0 is always accept 1 is only accept when already on if should_accept == 0 or (should_accept == 1 and self._state): if self._off_listener is not None: self._off_listener() self._off_listener = None self.attribute_updated(self.ON_OFF, True) if on_time > 0: self._off_listener = async_call_later( self._ch_pool.opp, (on_time / 10), # value is in 10ths of a second self.set_to_off, ) elif cmd == "toggle": self.attribute_updated(self.ON_OFF, not bool(self._state))
def _set_state(self, state, _=None): """Set up auto off.""" self._state = state self.async_set_context(self.coordinator.data["context"]) self.async_write_op_state() if not state: return auto_off_time = self._rendered.get(CONF_AUTO_OFF) or self._config.get( CONF_AUTO_OFF) if auto_off_time is None: return if not isinstance(auto_off_time, timedelta): try: auto_off_time = cv.positive_time_period(auto_off_time) except vol.Invalid as err: logging.getLogger(__name__).warning( "Error rendering %s template: %s", CONF_AUTO_OFF, err) return @callback def _auto_off(_): """Set state of template binary sensor.""" self._state = False self.async_write_op_state() self._auto_off_cancel = async_call_later(self.opp, auto_off_time.total_seconds(), _auto_off)
def _send_message(self, message: str | dict[str, Any]) -> None: """Send a message to the client. Closes connection if the client is not reading the messages. Async friendly. """ if not isinstance(message, str): message = message_to_json(message) try: self._to_write.put_nowait(message) except asyncio.QueueFull: self._logger.error("Client exceeded max pending messages [2]: %s", MAX_PENDING_MSG) self._cancel() if self._to_write.qsize() < PENDING_MSG_PEAK: if self._peak_checker_unsub: self._peak_checker_unsub() self._peak_checker_unsub = None return if self._peak_checker_unsub is None: self._peak_checker_unsub = async_call_later( self.opp, PENDING_MSG_PEAK_TIME, self._check_write_peak)
def _update_state(self, result): super()._update_state(result) if self._delay_cancel: self._delay_cancel() self._delay_cancel = None state = (None if isinstance(result, TemplateError) else template.result_as_boolean(result)) if state == self._state: return # state without delay if (state is None or (state and not self._delay_on) or (not state and not self._delay_off)): self._state = state return @callback def _set_state(_): """Set state of template binary sensor.""" self._state = state self.async_write_op_state() delay = (self._delay_on if state else self._delay_off).total_seconds() # state with delay. Cancelled if template result changes. self._delay_cancel = async_call_later(self.opp, delay, _set_state)
async def async_got_disconnected(self, _=None): """Notification that we're disconnected from the HUB.""" _LOGGER.debug("%s: disconnected from the HUB", self._name) # We're going to wait for 10 seconds before announcing we're # unavailable, this to allow a reconnection to happen. self._unsub_mark_disconnected = async_call_later( self.opp, TIME_MARK_DISCONNECTED, self._mark_disconnected_if_unavailable )
async def _fetch_data(self, *_): """Get the latest data from met.no.""" if not await self._weather_data.fetching_data(): # Retry in 15 to 20 minutes. minutes = 15 + randrange(6) _LOGGER.error("Retrying in %i minutes", minutes) self._unsub_fetch_data = async_call_later(self.opp, minutes * 60, self._fetch_data) return # Wait between 55-65 minutes. If people update HA on the hour, this # will make sure it will spread it out. self._unsub_fetch_data = async_call_later(self.opp, randrange(55, 65) * 60, self._fetch_data) self._update()
def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(VERIFIED_WRONG_KEY) if value is not None: self._verified_wrong_times = int(value) return True for key in (FINGER_KEY, PASSWORD_KEY, CARD_KEY): value = data.get(key) if value is not None: self._changed_by = int(value) self._verified_wrong_times = 0 self._state = STATE_UNLOCKED async_call_later(self.opp, UNLOCK_MAINTAIN_TIME, self.clear_unlock_state) return True return False
def parse_data(self, data, raw_data): """Parse data sent by gateway. Polling (proto v1, firmware version 1.4.1_159.0143) >> { "cmd":"read","sid":"158..."} << {'model': 'motion', 'sid': '158...', 'short_id': 26331, 'cmd': 'read_ack', 'data': '{"voltage":3005}'} Multicast messages (proto v1, firmware version 1.4.1_159.0143) << {'model': 'motion', 'sid': '158...', 'short_id': 26331, 'cmd': 'report', 'data': '{"status":"motion"}'} << {'model': 'motion', 'sid': '158...', 'short_id': 26331, 'cmd': 'report', 'data': '{"no_motion":"120"}'} << {'model': 'motion', 'sid': '158...', 'short_id': 26331, 'cmd': 'report', 'data': '{"no_motion":"180"}'} << {'model': 'motion', 'sid': '158...', 'short_id': 26331, 'cmd': 'report', 'data': '{"no_motion":"300"}'} << {'model': 'motion', 'sid': '158...', 'short_id': 26331, 'cmd': 'heartbeat', 'data': '{"voltage":3005}'} """ if raw_data["cmd"] == "heartbeat": _LOGGER.debug( "Skipping heartbeat of the motion sensor. " "It can introduce an incorrect state because of a firmware " "bug (https://github.com/openpeerpower/core/pull/" "11631#issuecomment-357507744)" ) return if NO_MOTION in data: self._no_motion_since = data[NO_MOTION] self._state = False return True value = data.get(self._data_key) if value is None: return False if value == MOTION: if self._data_key == "motion_status": if self._unsub_set_no_motion: self._unsub_set_no_motion() self._unsub_set_no_motion = async_call_later( self._opp, 120, self._async_set_no_motion ) if self.entity_id is not None: self._opp.bus.fire("xiaomi_aqara.motion", {"entity_id": self.entity_id}) self._no_motion_since = 0 if self._state: return False self._state = True return True
async def interval_listener(now=None): """Handle elapsed interval with backoff.""" nonlocal interval, remove try: if await action(): interval = MIN_INTERVAL else: interval = min(interval * 2, MAX_INTERVAL) finally: remove = async_call_later(opp, interval, interval_listener)
async def _get_services(opp): """Get the available services.""" services = opp.data.get(DATA_SERVICES) if services is not None: return services services = await account_link.async_fetch_available_services(opp.data[DOMAIN]) opp.data[DATA_SERVICES] = services @callback def clear_services(_now): """Clear services cache.""" opp.data.pop(DATA_SERVICES, None) event.async_call_later(opp, CACHE_TIMEOUT, clear_services) return services
def async_schedule_google_sync(self, agent_user_id: str): """Schedule a sync.""" async def _schedule_callback(_now): """Handle a scheduled sync callback.""" self._google_sync_unsub.pop(agent_user_id, None) await self.async_sync_entities(agent_user_id) self._google_sync_unsub.pop(agent_user_id, lambda: None)() self._google_sync_unsub[agent_user_id] = async_call_later( self.opp, SYNC_DELAY, _schedule_callback)
async def async_turn(self, command): """Evaluate switch result.""" result = await self._hub.async_pymodbus_call( self._slave, self._address, command, self._write_type ) if result is None: self._available = False self.async_write_op_state() return self._available = True if not self._verify_active: self._is_on = command == self.command_on self.async_write_op_state() return if self._verify_delay: async_call_later(self.opp, self._verify_delay, self.async_update) else: await self.async_update()
async def async_dump_service(call: ServiceCall): """Handle MQTT dump service calls.""" messages = [] @callback def collect_msg(msg): messages.append((msg.topic, msg.payload.replace("\n", ""))) unsub = await async_subscribe(opp, call.data["topic"], collect_msg) def write_dump(): with open(opp.config.path("mqtt_dump.txt"), "wt") as fp: for msg in messages: fp.write(",".join(msg) + "\n") async def finish_dump(_): """Write dump to file.""" unsub() await opp.async_add_executor_job(write_dump) event.async_call_later(opp, call.data["duration"], finish_dump)
async def interval_listener(now): """Handle elapsed intervals with backoff.""" nonlocal failed, remove try: failed += 1 if await action(now): failed = 0 finally: delay = intervals[failed] if failed < len( intervals) else intervals[-1] remove = async_call_later(opp, delay.total_seconds(), interval_listener)