def update_rss(): # Grab RSS Feeds if conf.rss_feeds is not None and conf.rss_update is not None: while not conf.stopping: if conf.rss_last_update == None or ( conf.rss_last_update + conf.rss_update) <= time.time(): conf.rss_last_update = time.time() for feed_data in conf.rss_feeds: feed = yield from utils.run_in_executor( conf.loop, conf.executor, feedparser.parse, feed_data["feed"]) new_state = {"feed": feed} with conf.ha_state_lock: conf.ha_state[feed_data["target"]] = new_state data = { "event_type": "state_changed", "data": { "entity_id": feed_data["target"], "new_state": new_state } } ws_update(data) yield from asyncio.sleep(1)
def load_dash(request): name = request.match_info.get('name', "Anonymous") params = request.query skin = params.get("skin", "default") recompile = params.get("recompile", False) if recompile == '1': recompile = True response = yield from utils.run_in_executor( conf.loop, conf.executor, conf.dashboard_obj.get_dashboard, name, skin, recompile) return web.Response(text=response, content_type="text/html")
async def get_updates(self): already_initialized = False already_notified = False first_time = True first_time_service = True while not self.stopping: while not self.initialized or not already_initialized: #continue until initialization is successful if not already_initialized and not already_notified: #if it had connected before, it need not run this. Run if just trying for the first time try: await asyncio.wait_for(utils.run_in_executor(self.AD.loop, self.AD.executor, self.start_mqtt_service, first_time_service), 5.0, loop=self.loop) await asyncio.wait_for(self.mqtt_connect_event.wait(), 5.0, loop=self.loop) # wait for it to return true for 5 seconds in case still processing connect except asyncio.TimeoutError: self.AD.log( "CRITICAL", "{}: Could not Complete Connection to Broker, please Ensure Broker at URL {}:{} is correct or broker not down and restart Appdaemon".format(self.name, self.mqtt_client_host, self.mqtt_client_port)) self.mqtt_client.loop_stop() self.mqtt_client.disconnect() #disconnect so it won't attempt reconnection if the broker was to come up first_time_service = False if self.initialized : #meaning the plugin started as expected await self.AD.notify_plugin_started(self.namespace, first_time) already_notified = False already_initialized = True self.AD.log("INFO", "{}: MQTT Plugin initialization complete".format(self.name)) else: if not already_notified and already_initialized: self.AD.notify_plugin_stopped(self.namespace) self.AD.log("CRITICAL", "{}: MQTT Plugin Stopped Unexpectedly".format(self.name)) already_notified = True already_initialized = False first_time = False if not already_initialized and not already_notified: self.AD.log("CRITICAL", "{}: Could not complete MQTT Plugin initialization, trying again in 5 seconds".format(self.name)) else: self.AD.log("CRITICAL", "{}: Unable to reinitialize MQTT Plugin, will keep trying again until complete".format(self.name)) await asyncio.sleep(5) await asyncio.sleep(5)
async def get_updates(self): already_initialized = False already_notified = False first_time = True first_time_service = True self.mqtt_connect_event = asyncio.Event() while not self.stopping: while ( not self.initialized or not already_initialized ) and not self.stopping: # continue until initialization is successful if ( not already_initialized and not already_notified ): # if it had connected before, it need not run this. Run if just trying for the first time try: await asyncio.wait_for( utils.run_in_executor(self, self.start_mqtt_service, first_time_service), 5.0) await asyncio.wait_for( self.mqtt_connect_event.wait(), 5.0 ) # wait for it to return true for 5 seconds in case still processing connect except asyncio.TimeoutError: self.logger.critical( "Could not Complete Connection to Broker, please Ensure Broker at URL %s:%s is correct and broker is not down and restart Appdaemon", self.mqtt_client_host, self.mqtt_client_port, ) # meaning it should start anyway even if broker is down if self.mqtt_client_force_start: self.mqtt_connected = True else: self.mqtt_client.loop_stop() # disconnect so it won't attempt reconnection if the broker was to come up self.mqtt_client.disconnect() first_time_service = False state = await self.get_complete_state() meta = await self.get_metadata() # meaning the client has connected to the broker if self.mqtt_connected: await self.AD.plugins.notify_plugin_started( self.name, self.namespace, meta, state, first_time) already_notified = False already_initialized = True self.logger.info("MQTT Plugin initialization complete") self.initialized = True else: if not already_notified and already_initialized: await self.AD.plugins.notify_plugin_stopped( self.name, self.namespace) self.logger.critical( "MQTT Plugin Stopped Unexpectedly") already_notified = True already_initialized = False first_time = False if not already_initialized and not already_notified: self.logger.critical( "Could not complete MQTT Plugin initialization, trying again in 5 seconds" ) if self.stopping: break else: self.logger.critical( "Unable to reinitialize MQTT Plugin, will keep trying again until complete" ) await asyncio.sleep(5) await asyncio.sleep(5)
async def read_config(self): new_config = None if await utils.run_in_executor(self, os.path.isfile, self.app_config_file): self.logger.warning("apps.yaml in the Config directory is deprecated. Please move apps.yaml to the apps directory.") new_config = utils.run_in_executor(self.read_config_file, self.app_config_file) else: for root, subdirs, files in os.walk(self.AD.app_dir): subdirs[:] = [d for d in subdirs if d not in self.AD.exclude_dirs] if root[-11:] != "__pycache__": for file in files: if file[-5:] == ".yaml" and file[0] != ".": self.logger.debug("Reading %s", os.path.join(root, file)) config = await utils.run_in_executor(self, self.read_config_file, os.path.join(root, file)) valid_apps = {} if type(config).__name__ == "dict": for app in config: if config[app] is not None: if app == "global_modules": # # Check the parameter format for string or list # if isinstance(config[app], str): valid_apps[app] = [config[app]] elif isinstance(config[app], list): valid_apps[app] = config[app] else: if self.AD.invalid_yaml_warnings: self.logger.warning("global_modules should be a list or a string in File '%s' - ignoring", file) elif app == "sequence": # # We don't care what it looks like just pass it through # valid_apps[app] = config[app] elif "class" in config[app] and "module" in config[app]: valid_apps[app] = config[app] else: if self.AD.invalid_yaml_warnings: self.logger.warning("App '%s' missing 'class' or 'module' entry - ignoring", app) else: if self.AD.invalid_yaml_warnings: self.logger.warning("File '%s' invalid structure - ignoring", os.path.join(root, file)) if new_config is None: new_config = {} for app in valid_apps: if app == "global_modules": if app in new_config: new_config[app].extend(valid_apps[app]) continue if app == "sequence": if app in new_config: new_config[app] = {**new_config[app], **valid_apps[app]} continue if app in new_config: self.logger.warning("File '%s' duplicate app: %s - ignoring", os.path.join(root, file), app) else: new_config[app] = valid_apps[app] await self.AD.sequences.add_sequences(new_config.get("sequence", {})) return new_config
def do_every_second(utc): try: start_time = datetime.datetime.now().timestamp() now = datetime.datetime.fromtimestamp(utc) conf.now = utc # If we have reached endtime bail out if conf.endtime is not None and utils.get_now() >= conf.endtime: utils.log(conf.logger, "INFO", "End time reached, exiting") stopit() if conf.realtime: real_now = datetime.datetime.now().timestamp() delta = abs(utc - real_now) if delta > 1: utils.log(conf.logger, "WARNING", "Scheduler clock skew detected - delta = {} - resetting".format(delta)) return real_now # Update sunrise/sunset etc. update_sun() # Check if we have entered or exited DST - if so, reload apps # to ensure all time callbacks are recalculated now_dst = is_dst() if now_dst != conf.was_dst: utils.log( conf.logger, "INFO", "Detected change in DST from {} to {} -" " reloading all modules".format(conf.was_dst, now_dst) ) # dump_schedule() utils.log(conf.logger, "INFO", "-" * 40) yield from utils.run_in_executor(conf.loop, conf.executor, read_apps, True) # dump_schedule() conf.was_dst = now_dst # dump_schedule() # test code for clock skew #if random.randint(1, 10) == 5: # time.sleep(random.randint(1,20)) # Check to see if any apps have changed but only if we have valid state if conf.last_state is not None and appapi.reading_messages: yield from utils.run_in_executor(conf.loop, conf.executor, read_apps) # Check to see if config has changed if appapi.reading_messages: yield from utils.run_in_executor(conf.loop, conf.executor, check_config) # Call me suspicious, but lets update state form HA periodically # in case we miss events for whatever reason # Every 10 minutes seems like a good place to start if conf.last_state is not None and appapi.reading_messages and now - conf.last_state > datetime.timedelta(minutes=10) and conf.ha_url is not None: try: yield from utils.run_in_executor(conf.loop, conf.executor, get_ha_state) conf.last_state = now except: utils.log(conf.logger, "WARNING", "Unexpected error refreshing HA state - retrying in 10 minutes") # Check on Queue size qsize = q.qsize() if qsize > 0 and qsize % 10 == 0: conf.logger.warning("Queue size is {}, suspect thread starvation".format(q.qsize())) # Process callbacks # utils.log(conf.logger, "DEBUG", "Scheduler invoked at {}".format(now)) with conf.schedule_lock: for name in conf.schedule.keys(): for entry in sorted( conf.schedule[name].keys(), key=lambda uuid_: conf.schedule[name][uuid_]["timestamp"] ): if conf.schedule[name][entry]["timestamp"] <= utc: exec_schedule(name, entry, conf.schedule[name][entry]) else: break for k, v in list(conf.schedule.items()): if v == {}: del conf.schedule[k] end_time = datetime.datetime.now().timestamp() loop_duration = (int((end_time - start_time)*1000) / 1000) * 1000 utils.log(conf.logger, "DEBUG", "Main loop compute time: {}ms".format(loop_duration)) if loop_duration > 900: utils.log(conf.logger, "WARNING", "Excessive time spent in scheduler loop: {}ms".format(loop_duration)) return utc except: utils.log(conf.error, "WARNING", '-' * 60) utils.log(conf.error, "WARNING", "Unexpected error during do_every_second()") utils.log(conf.error, "WARNING", '-' * 60) utils.log(conf.error, "WARNING", traceback.format_exc()) utils.log(conf.error, "WARNING", '-' * 60) if conf.errorfile != "STDERR" and conf.logfile != "STDOUT": # When explicitly logging to stdout and stderr, suppress # log messages about writing an error (since they show up anyway) utils.log( conf.logger, "WARNING", "Logged an error to {}".format(conf.errorfile) )
def appdaemon_loop(): first_time = True disconnected_event = False global ws conf.stopping = False _id = 0 while not conf.stopping: _id += 1 try: if first_time is False: # Get initial state get_ha_state() conf.last_state = utils.get_now() utils.log(conf.logger, "INFO", "Got initial state") disconnected_event = False # Let other parts know we are in business, appapi.reading_messages = True # Load apps read_apps(True) utils.log(conf.logger, "INFO", "App initialization complete") # # Fire HA_STARTED and APPD_STARTED Events # if first_time is True: process_event({"event_type": "appd_started", "data": {}}) first_time = False elif conf.ha_url is not None: process_event({"event_type": "ha_started", "data": {}}) if conf.version < parse_version('0.34') or conf.commtype == "SSE": # # Older version of HA - connect using SSEClient # if conf.commtype == "SSE": utils.log(conf.logger, "INFO", "Using SSE") else: utils.log( conf.logger, "INFO", "Home Assistant version < 0.34.0 - " "falling back to SSE" ) headers = {'x-ha-access': conf.ha_key} if conf.timeout is None: messages = SSEClient( "{}/api/stream".format(conf.ha_url), verify=False, headers=headers, retry=3000 ) utils.log( conf.logger, "INFO", "Connected to Home Assistant".format(conf.timeout) ) else: messages = SSEClient( "{}/api/stream".format(conf.ha_url), verify=False, headers=headers, retry=3000, timeout=int(conf.timeout) ) utils.log( conf.logger, "INFO", "Connected to Home Assistant with timeout = {}".format( conf.timeout ) ) while True: msg = yield from utils.run_in_executor(conf.loop, conf.executor, messages.__next__) if msg.data != "ping": process_message(json.loads(msg.data)) else: # # Connect to websocket interface # url = conf.ha_url if url.startswith('https://'): url = url.replace('https', 'wss', 1) elif url.startswith('http://'): url = url.replace('http', 'ws', 1) sslopt = {} if conf.certpath: sslopt['ca_certs'] = conf.certpath ws = create_connection( "{}/api/websocket".format(url), sslopt=sslopt ) result = json.loads(ws.recv()) utils.log(conf.logger, "INFO", "Connected to Home Assistant {}".format( result["ha_version"])) # # Check if auth required, if so send password # if result["type"] == "auth_required": auth = json.dumps({ "type": "auth", "api_password": conf.ha_key }) ws.send(auth) result = json.loads(ws.recv()) if result["type"] != "auth_ok": utils.log(conf.logger, "WARNING", "Error in authentication") raise ValueError("Error in authentication") # # Subscribe to event stream # sub = json.dumps({ "id": _id, "type": "subscribe_events" }) ws.send(sub) result = json.loads(ws.recv()) if not (result["id"] == _id and result["type"] == "result" and result["success"] is True): utils.log( conf.logger, "WARNING", "Unable to subscribe to HA events, id = {}".format(_id) ) utils.log(conf.logger, "WARNING", result) raise ValueError("Error subscribing to HA Events") # # Loop forever consuming events # while not conf.stopping: ret = yield from utils.run_in_executor(conf.loop, conf.executor, ws.recv) result = json.loads(ret) result = json.loads(ret) if not (result["id"] == _id and result["type"] == "event"): utils.log( conf.logger, "WARNING", "Unexpected result from Home Assistant, " "id = {}".format(_id) ) utils.log(conf.logger, "WARNING", result) raise ValueError( "Unexpected result from Home Assistant" ) process_message(result["event"]) except: appapi.reading_messages = False if not conf.stopping: if disconnected_event == False: process_event({"event_type": "ha_disconnected", "data": {}}) disconnected_event = True utils.log( conf.logger, "WARNING", "Disconnected from Home Assistant, retrying in 5 seconds" ) if conf.loglevel == "DEBUG": utils.log(conf.logger, "WARNING", '-' * 60) utils.log(conf.logger, "WARNING", "Unexpected error:") utils.log(conf.logger, "WARNING", '-' * 60) utils.log(conf.logger, "WARNING", traceback.format_exc()) utils.log(conf.logger, "WARNING", '-' * 60) yield from asyncio.sleep(5) utils.log(conf.logger, "INFO", "Disconnecting from Home Assistant")
def _list_dash(request): response = yield from utils.run_in_executor( conf.loop, conf.executor, conf.dashboard_obj.get_dashboard_list) return web.Response(text=response, content_type="text/html")
async def get_updates(self): already_initialized = False already_notified = False first_time = True first_time_service = True while not self.stopping: while not self.initialized or not already_initialized: #continue until initialization is successful if not already_initialized and not already_notified: #if it had connected before, it need not run this. Run if just trying for the first time try: await asyncio.wait_for(utils.run_in_executor( self.AD.loop, self.AD.executor, self.start_mqtt_service, first_time_service), 5.0, loop=self.loop) await asyncio.wait_for( self.mqtt_connect_event.wait(), 5.0, loop=self.loop ) # wait for it to return true for 5 seconds in case still processing connect except asyncio.TimeoutError: self.AD.log( "CRITICAL", "{}: Could not Complete Connection to Broker, please Ensure Broker at URL {}:{} is correct or broker not down and restart Appdaemon" .format(self.name, self.mqtt_client_host, self.mqtt_client_port)) self.mqtt_client.loop_stop() self.mqtt_client.disconnect( ) #disconnect so it won't attempt reconnection if the broker was to come up first_time_service = False if self.initialized: #meaning the plugin started as expected await self.AD.notify_plugin_started( self.namespace, first_time) already_notified = False already_initialized = True self.AD.log( "INFO", "{}: MQTT Plugin initialization complete".format( self.name)) else: if not already_notified and already_initialized: self.AD.notify_plugin_stopped(self.namespace) self.AD.log( "CRITICAL", "{}: MQTT Plugin Stopped Unexpectedly".format( self.name)) already_notified = True already_initialized = False first_time = False if not already_initialized and not already_notified: self.AD.log( "CRITICAL", "{}: Could not complete MQTT Plugin initialization, trying again in 5 seconds" .format(self.name)) else: self.AD.log( "CRITICAL", "{}: Unable to reinitialize MQTT Plugin, will keep trying again until complete" .format(self.name)) await asyncio.sleep(5) await asyncio.sleep(5)
async def call_service(self, namespace: str, domain: str, service: str, data: dict) -> Any: self.logger.debug( "call_service: namespace=%s domain=%s service=%s data=%s", namespace, domain, service, data, ) with self.services_lock: name = data.pop("__name", None) if namespace not in self.services: raise NamespaceException( "Unknown namespace (%s) in call_service from %s", namespace, name) if domain not in self.services[namespace]: raise DomainException( "Unknown domain (%s/%s) in call_service from %s", namespace, domain, name) if service not in self.services[namespace][domain]: raise ServiceException( "Unknown service (%s/%s/%s) in call_service from %s", namespace, domain, service, name, ) # If we have namespace in data it's an override for the domain of the eventual service call, as distinct # from the namespace the call itself is executed from. e.g. set_state() is in the AppDaemon namespace but # needs to operate on a different namespace, e.g. "default" if "namespace" in data: ns = data["namespace"] del data["namespace"] else: ns = namespace funcref = self.services[namespace][domain][service]["callback"] # Decide whether or not to call this as async # Default to true isasync = True # if to wait for results, default to False return_result = data.pop("return_result", False) # if to return results via callback callback = data.pop("callback", None) if "__async" in self.services[namespace][domain][service]: # We have a kwarg to tell us what to do if self.services[namespace][domain][service][ "__async"] == "auto": # We decide based on introspection if not asyncio.iscoroutinefunction(funcref): isasync = False else: # We do what the kwarg tells us isasync = self.services[namespace][domain][service][ "__async"] if isasync is True: # it's a coroutine just await it. coro = funcref(ns, domain, service, data) else: # It's not a coroutine, , run it in an executor coro = utils.run_in_executor(self, funcref, ns, domain, service, data) if return_result is True: return await self.run_service(coro) elif callback is not None and name is not None: # results expected and it must belong to an app app_object = await self.AD.app_management.get_app(name) app_object.create_task(self.run_service(coro), callback=callback) else: asyncio.create_task(self.run_service(coro))