def initialize(self): """AppDaemon required method for app init.""" conf_data = dict(self.config['AppDaemon']) _lights_dim_on = self.args.get('lights_dim_on', '').split(',') _lights_dim_off = self.args.get('lights_dim_off', '').split(',') _lights_off = self.args.get('lights_off', '').split(',') _switch_dim_group = self.args.get('switch_dim_lights_use') self._lights = { "dim": { "on": _lights_dim_on, "off": _lights_dim_off }, "off": _lights_off, "state": self.get_state(_switch_dim_group) } # Listen for ambilight changes to change light dim group: self.listen_state(self.ch_dim_lights_group, _switch_dim_group) self._media_player = conf_data.get('media_player') self._ios_notifier = conf_data.get('notifier').replace('.', '/') self._target_sensor = conf_data.get('chatid_sensor') # Listen for Kodi changes: self._last_play = utils.get_now() self.listen_state(self.kodi_state, self._media_player) self.listen_event(self._receive_kodi_result, EVENT_KODI_CALL_METHOD_RESULT)
def run_daily(self, callback, start, **kwargs): name = self.name now = utils.get_now() today = now.date() event = datetime.datetime.combine(today, start) if event < now: event = event + datetime.timedelta(days=1) handle = self.run_every(callback, event, 24 * 60 * 60, **kwargs) return handle
def run_at(self, callback, start, **kwargs): name = self.name now = utils.get_now() if start < now: raise ValueError("{}: run_at() Start time must be " "in the future".format(self.name)) exec_time = start.timestamp() handle = utils.insert_schedule(name, exec_time, callback, False, None, **kwargs) return handle
def run_minutely(self, callback, start, **kwargs): name = self.name now = utils.get_now() if start is None: event = now + datetime.timedelta(minutes=1) else: event = now event = event.replace(second=start.second) if event < now: event = event + datetime.timedelta(minutes=1) handle = self.run_every(callback, event, 60, **kwargs) return handle
def run_once(self, callback, start, **kwargs): name = self.name now = utils.get_now() today = now.date() event = datetime.datetime.combine(today, start) if event < now: one_day = datetime.timedelta(days=1) event = event + one_day exec_time = event.timestamp() handle = utils.insert_schedule(name, exec_time, callback, False, None, **kwargs) return handle
def run_every(self, callback, start, interval, **kwargs): name = self.name now = utils.get_now() if start < now: raise ValueError("start cannot be in the past") utils.log( conf.logger, "DEBUG", "Registering run_every starting {} in {}s intervals for {}".format( start, interval, name ) ) exec_time = start.timestamp() handle = utils.insert_schedule(name, exec_time, callback, True, None, interval=interval, **kwargs) return handle
def _receive_kodi_result(self, event_id, payload_event, *args): result = payload_event['result'] method = payload_event['input']['method'] if event_id == EVENT_KODI_CALL_METHOD_RESULT \ and method == METHOD_GET_ITEM: if 'item' in result: item = result['item'] new_video = (self._item_playing is None or self._item_playing != item) self._is_playing_video = item['type'] in TYPE_ITEMS_NOTIFY self._item_playing = item delta = utils.get_now() - self._last_play if (self._is_playing_video and (new_video or delta > dt.timedelta(minutes=20))): self._last_play = utils.get_now() self._adjust_kodi_lights(play=True) # Notifications self._notify_ios_message(self._item_playing) self._notify_telegram_message(self._item_playing) else: self.log('RECEIVED BAD KODI RESULT: {}'.format(result), 'warn') elif event_id == EVENT_KODI_CALL_METHOD_RESULT \ and method == METHOD_GET_PLAYERS: self.log('KODI GET_PLAYERS RECEIVED: {}'.format(result))
def kodi_state(self, entity, attribute, old, new, kwargs): """Kodi state change main control.""" if new == 'playing': kodi_attrs = self.get_state(entity_id=self._media_player, attribute="attributes") self._is_playing_video = ('media_content_type' in kodi_attrs and kodi_attrs['media_content_type'] in TYPE_HA_ITEMS_NOTIFY) # self.log('KODI ATTRS: {}, is_playing_video={}' # .format(kodi_attrs, self._is_playing_video)) if self._is_playing_video: self._ask_for_playing_item() elif ((new == 'idle') and self._is_playing_video) or (new == 'off'): self._is_playing_video = False self._last_play = utils.get_now() self.log( 'KODI STOP. old:{}, new:{}, type_lp={}'.format( old, new, type(self._last_play)), LOG_LEVEL) # self._item_playing = None self._adjust_kodi_lights(play=False)
def update_sun(): # now = datetime.datetime.now(conf.tz) now = conf.tz.localize(utils.get_now()) mod = -1 while True: try: next_rising_dt = conf.location.sunrise( now + datetime.timedelta(days=mod), local=False ) if next_rising_dt > now: break except astral.AstralError: pass mod += 1 mod = -1 while True: try: next_setting_dt = conf.location.sunset( now + datetime.timedelta(days=mod), local=False ) if next_setting_dt > now: break except astral.AstralError: pass mod += 1 old_next_rising_dt = conf.sun.get("next_rising") old_next_setting_dt = conf.sun.get("next_setting") conf.sun["next_rising"] = next_rising_dt conf.sun["next_setting"] = next_setting_dt if old_next_rising_dt is not None and old_next_rising_dt != conf.sun["next_rising"]: # dump_schedule() process_sun("next_rising") # dump_schedule() if old_next_setting_dt is not None and old_next_setting_dt != conf.sun["next_setting"]: # dump_schedule() process_sun("next_setting")
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 today_is_constrained(days): day = utils.get_now().weekday() daylist = [utils.day_of_week(day) for day in days.split(",")] if day in daylist: return False return True
def run_ad(loop, tasks): conf.appq = asyncio.Queue(maxsize=0) conf.loop = loop first_time = True conf.stopping = False utils.log(conf.logger, "DEBUG", "Entering run()") # Load App Config conf.app_config = read_config() # Save start time conf.start_time = datetime.datetime.now() # Take a note of DST conf.was_dst = is_dst() # Setup sun update_sun() conf.executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) utils.log(conf.logger, "DEBUG", "Creating worker threads ...") # Create Worker Threads for i in range(conf.threads): t = threading.Thread(target=worker) t.daemon = True t.start() utils.log(conf.logger, "DEBUG", "Done") if conf.ha_url is not None: # Read apps and get HA State before we start the timer thread utils.log(conf.logger, "DEBUG", "Calling HA for initial state with key: {} and url: {}".format(conf.ha_key, conf.ha_url)) while conf.last_state is None: try: get_ha_state() conf.last_state = utils.get_now() except: 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) time.sleep(5) utils.log(conf.logger, "INFO", "Got initial state") # Initialize appdaemon loop tasks.append(asyncio.async(appdaemon_loop())) else: conf.last_state = utils.get_now() # Load apps # Let other parts know we are in business, appapi.reading_messages = True utils.log(conf.logger, "DEBUG", "Reading Apps") read_apps(True) utils.log(conf.logger, "INFO", "App initialization complete") # Create timer loop # First, update "now" for less chance of clock skew error if conf.realtime: conf.now = datetime.datetime.now().timestamp() utils.log(conf.logger, "DEBUG", "Starting timer loop") tasks.append(asyncio.async(appstate_loop())) tasks.append(asyncio.async(do_every(conf.tick, do_every_second))) appapi.reading_messages = True
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")