Best Python code snippet using slash
motion_alarm_push_email.py
Source:motion_alarm_push_email.py
1# -*- coding: utf-8 -*-2"""3# Alarma de activación por detección de movimiento.4Activada con estÃmulos en sensores binarios (PIR's, de sonido, vibración, inclinación, movimiento en cámaras),5zonificación con cámaras y sensores asociados a cada zona, para la captura de eventos que se envÃan como html por6email, además de emitir notificaciones push para los eventos tÃpicos de activación, alarma, desactivación e inicio.7Ante estÃmulos de los sensores de movimiento, genera "eventos" con el estado de los sensores y capturas jpg de8las cámaras asociadas. Estos eventos, tipificados, forman los informes generados, que se guardan en disco para estar9disponibles por el servidor web de HomeAssistant como ficheros locales.10En el disparo de alarma, activa 2 relés (sirena y opcional), si la alarma no está definida como "silenciosa", en cuyo11caso opera igualmente, excepto por el encendido de los relés asociados.12Los tiempos de espera a armado, periodo de pre-alarma, âT min entre eventos, âT para la captura periódica de eventos13en estado de alarma, y el # máximo de eventos (con imágenes) en los informes (para reducir el peso de los emails),14son editables en la configuración de la AppDaemon app:15```16 [Alarm]17 module = motion_alarm_push_email18 class = MotionAlarm19 # Hora de envÃo del informe diario de eventos detectados (si los ha habido). Comentar con # para desactivar20 hora_informe = 07:3021 # Parámetro de tiempo de espera desde conexión a armado de alarma:22 espera_a_armado_sec = 2023 # Parámetro de tiempo de espera en pre-alarma para conectar la alarma si ocurre un nuevo evento:24 reset_prealarm_time_sec = 1525 # Segundos entre captura de eventos con la alarma conectada26 min_delta_sec_events = 327 delta_secs_trigger = 15028 # Número de eventos máx. a incluir por informe en correo electrónico. Se limita eliminando eventos de baja prioridad29 num_max_eventos_por_informe = 1030```31"""32import appdaemon.appapi as appapi33import appdaemon.conf as conf34# import asyncio35# from base64 import b64encode36from collections import OrderedDict37import datetime as dt38from dateutil.parser import parse39from functools import reduce40from itertools import cycle41from jinja2 import Environment, FileSystemLoader42import json43from math import ceil44import os45import re46import requests47from time import time, sleep48import yaml49# LOG_LEVEL = 'DEBUG'50LOG_LEVEL = 'INFO'51NUM_RETRIES_MAX_GET_JPG_FROM_CAM = 1052BYTES_MIN_FOR_JPG = 10.53MIN_TIME_BETWEEN_MOTION = 1 # secs54DEFAULT_RAWBS_SECS_OFF = 555DEFAULT_ESPERA_A_ARMADO_SEC = 1056DEFAULT_RESET_PREALARM_TIME_SEC = 1557DEFAULT_MIN_DELTA_SEC_EVENTS = 658DEFAULT_DELTA_SECS_TRIGGER = 6059DEFAULT_NUM_MAX_EVENTOS_POR_INFORME = 1560DIR_INFORMES = 'alarm_reports'61DIR_CAPTURAS = 'eventos'62# jinja2 template environment63basedir = os.path.dirname(os.path.abspath(__file__))64PATH_TEMPLATES = os.path.join(basedir, 'templates')65JINJA2_ENV = Environment(loader=FileSystemLoader(PATH_TEMPLATES), trim_blocks=True)66# Leyenda de eventos:67EVENT_INICIO = "INICIO"68EVENT_ACTIVACION = "ACTIVACION"69EVENT_DESCONEXION = "DESCONEXION"70EVENT_PREALARMA = "PRE-ALARMA"71EVENT_ALARMA = "ALARMA"72EVENT_EN_ALARMA = "EN ALARMA (ACTIVACION)"73EVENT_ALARMA_ENCENDIDA = "ALARMA ENCENDIDA"74AVISO_RETRY_ALARMA_ENCENDIDA_TITLE = "ALARMA ENCENDIDA"75AVISO_RETRY_ALARMA_ENCENDIDA_MSG = "La alarma sigue encendida, desde las {:%H:%M:%S}. {}"76HASS_COLOR = '#58C1F0'77DEFAULT_ALARM_COLORS = [(255, 0, 0), (50, 0, 255)] # para cycle en luces RGB (simulación de sirena)78# TÃtulo, color, es_prioritario, subject_report79EVENT_TYPES = OrderedDict(zip([EVENT_INICIO, EVENT_ACTIVACION, EVENT_DESCONEXION, EVENT_PREALARMA,80 EVENT_ALARMA, EVENT_EN_ALARMA, EVENT_ALARMA_ENCENDIDA],81 [('Inicio del sistema', "#1393f0", 1, 'Informe de eventos'),82 ('Activación de sistema', "#1393f0", 3, 'Informe de eventos'),83 ('Desconexión de sistema', "#1393f0", 3, 'Informe de desconexión de alarma'),84 ('PRE-ALARMA', "#f0aa28", 1, 'Informe de eventos'),85 ('ALARMA!', "#f00a2d", 10, 'ALARMA ACTIVADA'),86 ('en ALARMA', "#f0426a", 5, 'ALARMA ACTIVADA'),87 ('Alarma encendida', "#f040aa", 0, 'ALARMA ACTIVADA')]))88SOUND_MOTION = "US-EN-Morgan-Freeman-Motion-Detected.wav"89def _read_hass_secret_conf(path_ha_conf):90 """Read config values from secrets.yaml file & get also the known_devices.yaml path"""91 path_secrets = os.path.join(path_ha_conf, 'secrets.yaml')92 path_known_dev = os.path.join(path_ha_conf, 'known_devices.yaml')93 with open(path_secrets) as _file:94 secrets = yaml.load(_file.read())95 return dict(secrets=secrets, hass_base_url=secrets['base_url'], path_known_dev=path_known_dev,96 email_target=secrets['email_target'], pb_target=secrets['pb_target'])97def _get_events_path(path_base_data):98 path_reports = os.path.join(path_base_data, DIR_INFORMES)99 path_captures = os.path.join(path_base_data, DIR_CAPTURAS)100 if not os.path.exists(path_reports):101 os.mkdir(path_reports)102 if not os.path.exists(path_captures):103 os.mkdir(path_captures)104 return path_captures, path_reports105# noinspection PyClassHasNoInit106class MotionAlarm(appapi.AppDaemon):107 """App for handle the main intrusion alarm."""108 _lock = None109 _path_captures = None110 _path_reports = None111 _secrets = None112 _pirs = None113 _use_pirs = None114 _camera_movs = None115 _use_cams_movs = None116 _extra_sensors = None117 _use_extra_sensors = None118 _dict_asign_switchs_inputs = None119 _videostreams = {}120 _cameras_jpg_ip = None121 _cameras_jpg_params = None122 _main_switch = None123 _rele_sirena = None124 _rele_secundario = None125 _led_act = None126 _use_push_notifier_switch = None127 _email_notifier = None128 _push_notifier = None129 _silent_mode_switch = None130 _tz = None131 _time_report = None132 _espera_a_armado_sec = None133 _reset_prealarm_time_sec = None134 _min_delta_sec_events = None135 _delta_secs_trigger = None136 _use_push_notifier = False137 _retry_push_alarm = None138 _max_time_sirena_on = None139 _max_report_events = None140 _alarm_lights = None141 _cycle_colors = None142 _alarm_on = False143 _silent_mode = False144 _alarm_state = False145 _alarm_state_ts_trigger = None146 _alarm_state_entity_trigger = None147 _pre_alarm_on = False148 _pre_alarm_ts_trigger = None149 _pre_alarms = []150 _post_alarms = []151 _events_data = None152 _in_capture_mode = False153 _ts_lastcap = None154 _dict_use_inputs = None155 _dict_friendly_names = None156 _dict_sensor_classes = None157 _handler_periodic_trigger = None158 _handler_retry_alert = None159 _handler_armado_alarma = None160 _ts_lastbeat = None161 _known_devices = None162 _raw_sensors = None163 _raw_sensors_sufix = None164 _raw_sensors_seconds_to_off = None165 _raw_sensors_last_states = {}166 _raw_sensors_attributes = {}167 def initialize(self):168 """AppDaemon required method for app init."""169 self._lock = conf.callbacks_lock170 self._tz = conf.tz171 # self.log('INIT w/conf_data: {}'.format(conf_data))172 # Paths173 _path_base_data = self.args.get('path_base_data')174 _path_hass_conf = self.args.get('path_ha_conf')175 self._path_captures, self._path_reports = _get_events_path(_path_base_data)176 self._secrets = _read_hass_secret_conf(_path_hass_conf)177 # Interruptor principal178 self._main_switch = self.args.get('main_switch')179 # Sensores de movimiento (PIR's, cam_movs, extra)180 self._raw_sensors = self.args.get('raw_binary_sensors', None)181 self._pirs = self._listconf_param(self.args, 'pirs')182 self._camera_movs = self._listconf_param(self.args, 'camera_movs')183 self._extra_sensors = self._listconf_param(self.args, 'extra_sensors')184 self._use_pirs = self._listconf_param(self.args, 'use_pirs', min_len=len(self._pirs), default=True)185 self._use_cams_movs = self._listconf_param(self.args, 'use_cam_movs',186 min_len=len(self._camera_movs), default=True)187 self._use_extra_sensors = self._listconf_param(self.args, 'use_extra_sensors',188 min_len=len(self._extra_sensors), default=True)189 # self.log('_use_pirs: {}'.format(self._use_pirs))190 # self.log('_use_cams_movs: {}'.format(self._use_cams_movs))191 # self.log('use_extra_sensors: {}'.format(self._use_extra_sensors))192 # Video streams asociados a sensores para notif193 _streams = self._listconf_param(self.args, 'videostreams')194 if _streams:195 self._videostreams = {sensor: cam196 for cam, list_triggers in _streams[0].items()197 for sensor in list_triggers}198 # Streams de vÃdeo (HA entities, URLs + PAYLOADS for request jpg images)199 self._cameras_jpg_ip = self._listconf_param(self.args, 'cameras_jpg_ip_secret', is_secret=True)200 self._cameras_jpg_params = self._listconf_param(self.args, 'cameras_jpg_params_secret',201 is_secret=True, is_json=True, min_len=len(self._cameras_jpg_ip))202 # Actuadores en caso de alarma (relays, LED's, ...)203 self._rele_sirena = self.args.get('rele_sirena', None)204 self._rele_secundario = self.args.get('rele_secundario', None)205 self._led_act = self.args.get('led_act', None)206 # Switch de modo silencioso (sin relays)207 self._silent_mode_switch = self.args.get('silent_mode', 'False')208 # Configuración de notificaciones209 self._email_notifier = self.args.get('email_notifier')210 self._push_notifier = self.args.get('push_notifier')211 self._use_push_notifier_switch = self.args.get('usar_push_notifier', 'True')212 # Hora de envÃo del informe diario de eventos detectados (si los ha habido)213 self._time_report = self.args.get('hora_informe', None)214 # Parámetro de tiempo de espera desde conexión a armado de alarma:215 self._espera_a_armado_sec = int(self.args.get('espera_a_armado_sec', DEFAULT_ESPERA_A_ARMADO_SEC))216 # Parámetro de tiempo de espera en pre-alarma para conectar la alarma si ocurre un nuevo evento:217 self._reset_prealarm_time_sec = int(self.args.get('reset_prealarm_time_sec', DEFAULT_RESET_PREALARM_TIME_SEC))218 # Segundos entre captura de eventos con la alarma conectada219 self._min_delta_sec_events = int(self.args.get('min_delta_sec_events', DEFAULT_MIN_DELTA_SEC_EVENTS))220 self._delta_secs_trigger = int(self.args.get('delta_secs_trigger', DEFAULT_DELTA_SECS_TRIGGER))221 # Número de eventos máximo a incluir por informe en email. Se limita eliminando eventos de baja prioridad222 self._max_report_events = int(self.args.get('num_max_eventos_por_informe', DEFAULT_NUM_MAX_EVENTOS_POR_INFORME))223 self._alarm_lights = self.args.get('alarm_rgb_lights', None)224 # Insistencia de notificación de alarma encendida225 self._retry_push_alarm = self.args.get('retry_push_alarm', None)226 if self._retry_push_alarm is not None:227 self._retry_push_alarm = int(self._retry_push_alarm)228 # Persistencia de alarma encendida229 self._max_time_sirena_on = self.args.get('max_time_alarm_on', None)230 if self._max_time_sirena_on is not None:231 self._max_time_sirena_on = int(self._max_time_sirena_on)232 # self.log('Insistencia de notificación de alarma: {};'233 # ' Persistencia de sirena: {}'234 # .format(self._retry_push_alarm, self._max_time_sirena_on))235 # RAW SENSORS:236 if self._raw_sensors is not None:237 self._raw_sensors = self._raw_sensors.split(',')238 self._raw_sensors_sufix = self.args.get('raw_binary_sensors_sufijo', '_raw')239 # Persistencia en segundos de último valor hasta considerarlos 'off'240 self._raw_sensors_seconds_to_off = int(self.args.get('raw_binary_sensors_time_off', DEFAULT_RAWBS_SECS_OFF))241 # Handlers de cambio en raw binary_sensors:242 l1, l2 = 'attributes', 'last_changed'243 for s in self._raw_sensors:244 self._raw_sensors_attributes[s] = (s.replace(self._raw_sensors_sufix, ''), self.get_state(s, l1))245 # self._raw_sensors_last_states[s] = [parse(self.get_state(s, l2)).replace(tzinfo=None), False]246 self._raw_sensors_last_states[s] = [self.datetime(), False]247 self.listen_state(self._turn_on_raw_sensor_on_change, s)248 # self.log('seconds_to_off: {}'.format(self._raw_sensors_seconds_to_off))249 # self.log('attributes_sensors: {}'.format(self._raw_sensors_attributes))250 # self.log('last_changes: {}'.format(self._raw_sensors_last_states))251 [self.set_state(dev, state='off', attributes=attrs) for dev, attrs in self._raw_sensors_attributes.values()]252 next_run = self.datetime() + dt.timedelta(seconds=self._raw_sensors_seconds_to_off)253 self.run_every(self._turn_off_raw_sensor_if_not_updated, next_run, self._raw_sensors_seconds_to_off)254 self._events_data = []255 # Main switches:256 self._alarm_on = self._listen_to_switch('main_switch', self._main_switch, self._main_switch_ch)257 # set_global(self, GLOBAL_ALARM_STATE, self._alarm_on)258 self._use_push_notifier = self._listen_to_switch('push_n', self._use_push_notifier_switch, self._main_switch_ch)259 self._silent_mode = self._listen_to_switch('silent_mode', self._silent_mode_switch, self._main_switch_ch)260 # Sensors states & input usage:261 all_sensors = self._pirs + self._camera_movs + self._extra_sensors262 all_sensors_use = self._use_pirs + self._use_cams_movs + self._use_extra_sensors263 self._dict_asign_switchs_inputs = {s_use: s_input for s_input, s_use in zip(all_sensors, all_sensors_use)264 if type(s_use) is not bool}265 self._dict_use_inputs = {s_input: self._listen_to_switch(s_input, s_use, self._switch_usar_input)266 for s_input, s_use in zip(all_sensors, all_sensors_use)}267 self._dict_friendly_names = {s: self.get_state(s, attribute='friendly_name') for s in all_sensors}268 # self._dict_friendly_names.update({c: self.get_state(c, attribute='friendly_name') for c in self._videostreams})269 self._dict_sensor_classes = {s: self.get_state(s, attribute='device_class') for s in all_sensors}270 # Movement detection271 for s_mov in all_sensors:272 self.listen_state(self._motion_detected, s_mov, new="on", duration=1)273 # Programación de informe de actividad274 if self._time_report is not None:275 time_alarm = reduce(lambda x, y: x.replace(**{y[1]: int(y[0])}),276 zip(self._time_report.split(':'), ['hour', 'minute', 'second']),277 self.datetime().replace(second=0, microsecond=0))278 self.run_daily(self.email_events_data, time_alarm.time())279 self.log('Creado timer para informe diario de eventos a las {} de cada dÃa'.format(time_alarm.time()))280 # Simulación de alarma visual con luces RBG (opcional)281 if self._alarm_lights is not None:282 self._cycle_colors = cycle(DEFAULT_ALARM_COLORS)283 self.log('Alarma visual con luces RGB: {}; colores: {}'.format(self._alarm_lights, self._cycle_colors))284 # Listen to main events:285 self.listen_event(self.receive_init_event, 'ha_started')286 self.listen_event(self.device_tracker_new_device, 'device_tracker_new_device')287 self.listen_event(self._reset_alarm_state, 'reset_alarm_state')288 self.listen_event(self._turn_off_sirena_in_alarm_state, 'silent_alarm_state')289 def _listconf_param(self, conf_args, param_name, is_secret=False, is_json=False, min_len=None, default=None):290 """Carga de configuración de lista de entidades de HA"""291 p_config = conf_args.get(param_name, default)292 # self.log('DEBUG listconf_param: {}, min_l={} --> {}'.format(param_name, min_len, p_config))293 if (type(p_config) is str) and ',' in p_config:294 p_config = p_config.split(',')295 if is_json and is_secret:296 return [json.loads(self._secrets['secrets'][p]) for p in p_config]297 if is_json:298 return [json.loads(p) for p in p_config]299 elif is_secret:300 return [self._secrets['secrets'][p] for p in p_config]301 else:302 return p_config303 elif p_config is not None:304 if is_secret:305 p_config = self._secrets['secrets'][p_config]306 if is_json:307 p_config = json.loads(p_config)308 if min_len is not None:309 return [p_config] * min_len310 return [p_config]311 if min_len is not None:312 return [default] * min_len313 return []314 # noinspection PyUnusedLocal315 def _turn_on_raw_sensor_on_change(self, entity, attribute, old, new, kwargs):316 _, last_st = self._raw_sensors_last_states[entity]317 self._raw_sensors_last_states[entity] = [self.datetime(), True]318 if not last_st:319 name, attrs = self._raw_sensors_attributes[entity]320 self.set_state(name, state='on', attributes=attrs)321 # self.log('TURN ON "{}" (de {} a {} --> {})'.format(entity, old, new, name))322 # noinspection PyUnusedLocal323 def _turn_off_raw_sensor_if_not_updated(self, *kwargs):324 now = self.datetime()325 for s, (ts, st) in self._raw_sensors_last_states.copy().items():326 if st and ceil((now - ts).total_seconds()) >= self._raw_sensors_seconds_to_off:327 # self.log('TURN OFF "{}" (last ch: {})'.format(s, ts))328 name, attrs = self._raw_sensors_attributes[s]329 self._raw_sensors_last_states[s] = [now, False]330 self.set_state(name, state='off', attributes=attrs)331 def _listen_to_switch(self, identif, entity_switch, func_listen_change):332 if type(entity_switch) is bool:333 # self.log('FIXED BOOL: {} -> {}'334 # .format(identif, entity_switch), LOG_LEVEL)335 return entity_switch336 if entity_switch.lower() in ['true', 'false', 'on', 'off', '1', '0']:337 fixed_bool = entity_switch.lower() in ['true', 'on', '1']338 # self.log('FIXED SWITCH: {} -> "{}": {}'339 # .format(identif, entity_switch, fixed_bool), LOG_LEVEL)340 return fixed_bool341 else:342 state = self.get_state(entity_switch) == 'on'343 self.listen_state(func_listen_change, entity_switch)344 # self.log('LISTEN TO CHANGES IN SWITCH: {} -> {}, ST={}'345 # .format(identif, entity_switch, state), LOG_LEVEL)346 return state347 def _is_too_old(self, ts, delta_secs):348 if ts is None:349 return True350 else:351 now = dt.datetime.now(tz=self._tz)352 return (now - ts).total_seconds() > delta_secs353 # noinspection PyUnusedLocal354 def track_device_in_zone(self, entity, attribute, old, new, kwargs):355 if self._alarm_on:356 self.log('* DEVICE: "{}", from "{}" to "{}"'.format(entity, kwargs['codename'], old, new))357 # noinspection PyUnusedLocal358 def _reload_known_devices(self, *args):359 # Reload known_devices from yaml file:360 with open(self._secrets['path_known_dev']) as f:361 new_known_devices = yaml.load(f.read())362 if self._known_devices is None:363 self.log('KNOWN_DEVICES: {}'.format(['{name} [{mac}]'.format(**v) for v in new_known_devices.values()]))364 else:365 if any([dev not in self._known_devices.keys() for dev in new_known_devices.keys()]):366 for dev, dev_data in new_known_devices.items():367 if dev not in new_known_devices.keys():368 new_dev = '{name} [{mac}]'.format(**dev_data)369 self.listen_state(self.track_device_in_zone, dev, old="home", codename=new_dev)370 self.log('NEW KNOWN_DEV: {}'.format(new_dev))371 self._known_devices = new_known_devices372 # noinspection PyUnusedLocal373 def device_tracker_new_device(self, event_id, payload_event, *args):374 """Event listener."""375 dev = payload_event['entity_id']376 self.log('* DEVICE_TRACKER_NEW_DEVICE RECEIVED * --> {}: {}'.format(dev, payload_event))377 self.run_in(self._reload_known_devices, 5)378 # noinspection PyUnusedLocal379 def receive_init_event(self, event_id, payload_event, *args):380 """Event listener."""381 self.log('* INIT_EVENT * RECEIVED: "{}", payload={}'.format(event_id, payload_event))382 self.append_event_data(dict(event_type=EVENT_INICIO))383 self.text_notification()384 self._reload_known_devices()385 def _make_event_path(self, event_type, id_cam):386 now = dt.datetime.now(tz=self._tz)387 ev_clean = re.sub('\(|\)', '', re.sub(':|-|\+| |\.', '_', event_type))388 name = 'evento_{}_cam{}_ts{:%Y%m%d_%H%M%S}.jpg'.format(ev_clean, id_cam, now)389 sub_dir = 'ts_{:%Y_%m_%d}'.format(now.date())390 base_path = os.path.join(self._path_captures, sub_dir)391 if not os.path.exists(base_path):392 os.mkdir(base_path)393 url = '{}/{}/{}/{}/{}'.format(self._secrets['hass_base_url'], 'local', DIR_CAPTURAS, sub_dir, name)394 return name, os.path.join(base_path, name), url395 def _append_pic_to_data(self, data, event_type, index, url, params=None):396 # Get PIC from IP cams or from MotionEye in LocalHost:397 pic, ok, retries = None, False, 0398 name_pic, path_pic, url_pic = 'NONAME', None, None399 while not ok and (retries < NUM_RETRIES_MAX_GET_JPG_FROM_CAM):400 try:401 r = requests.get(url, params=params, timeout=5)402 length = float(r.headers['Content-Length'])403 if r.ok and (r.headers['Content-type'] == 'image/jpeg') and (length > BYTES_MIN_FOR_JPG):404 pic = r.content405 ok = True406 if retries > 5:407 self.log('CGI PIC OK CON {} INTENTOS: {}, length={}'408 .format(retries + 1, url, length), 'WARNING')409 break410 elif not r.ok:411 self.log('ERROR {} EN CGI PIC: {}, length={}'.format(r.status_code, url, length), 'WARNING')412 except requests.ConnectionError:413 if retries > 0:414 self.log('ConnectionError EN CGI PIC en {}?{}'.format(url, params), 'ERROR')415 break416 except requests.Timeout:417 if retries > 0:418 self.log('Timeout EN CGI PIC en {}?{}'.format(url, params), 'ERROR')419 break420 retries += 1421 # TODO ASYNC!!422 # asyncio.sleep(.2)423 sleep(.2)424 # Save PIC & (opc) b64 encode:425 if ok:426 name_pic, path_pic, url_pic = self._make_event_path(event_type, index + 1)427 with open(path_pic, 'wb') as f:428 f.write(pic)429 # pic_b64 = b64encode(pic).decode()430 data['ok_img{}'.format(index + 1)] = True431 else:432 # pic_b64 = 'NOIMG'433 data['ok_img{}'.format(index + 1)] = False434 # data['incluir'] = False435 self.log('ERROR EN CAPTURE PIC con event_type: "{}", cam #{}'.format(event_type, index + 1))436 data['path_img{}'.format(index + 1)] = path_pic437 data['url_img{}'.format(index + 1)] = url_pic438 data['name_img{}'.format(index + 1)] = name_pic439 # data['base64_img{}'.format(index + 1)] = pic_b64440 def _append_state_to_data(self, data, entity, prefix):441 st = self.get_state(entity)442 ts = self.get_state(entity, attribute='last_changed')443 if ts:444 ts = '{:%-H:%M:%S %-d/%-m}'.format(parse(ts).astimezone(self._tz))445 data[prefix + '_st'] = st446 data[prefix + '_ts'] = ts447 data[prefix + '_fn'] = self._dict_friendly_names[entity]448 # noinspection PyUnusedLocal449 def append_event_data(self, kwargs, *args):450 """Creación de eventos.451 params = dict(pir_1_st='ON', pir_2_st='OFF', cam_mov_1_st='OFF', cam_mov_2_st='ON',452 pir_1_ts='ON', pir_2_ts='OFF', cam_mov_1_ts='OFF', cam_mov_2_ts='ON',453 base64_img1=b64encode(bytes_img1).decode(),454 base64_img2=b64encode(bytes_img2).decode())455 """456 event_type = kwargs.get('event_type')457 entity_trigger = kwargs.get('entity_trigger', None)458 prioridad = EVENT_TYPES[event_type][2]459 proceed = False460 with self._lock:461 tic = time()462 if not self._in_capture_mode:463 proceed = (prioridad > 1) or self._is_too_old(self._ts_lastcap, self._min_delta_sec_events)464 self._in_capture_mode = proceed465 if proceed:466 now = dt.datetime.now(tz=self._tz)467 params = dict(ts=now, ts_event='{:%H:%M:%S}'.format(now), incluir=True, prioridad=prioridad,468 event_type=event_type, event_color=EVENT_TYPES[event_type][1], entity_trigger=entity_trigger)469 # Binary sensors: PIR's, camera_movs, extra_sensors:470 for i, p in enumerate(self._pirs):471 mask_pirs = 'pir_{}'472 self._append_state_to_data(params, p, 'pir_{}'.format(i + 1))473 for i, cm in enumerate(self._camera_movs):474 self._append_state_to_data(params, cm, 'cam_mov_{}'.format(i + 1))475 for extra_s in self._extra_sensors:476 # extra_sensor_usar = extra_s.replace('_raw', '')477 self._append_state_to_data(params, extra_s, self._dict_sensor_classes[extra_s])478 # image captures:479 if self._cameras_jpg_ip:480 if self._cameras_jpg_params is not None:481 for i, (url, params_req) in enumerate(zip(self._cameras_jpg_ip, self._cameras_jpg_params)):482 self._append_pic_to_data(params, event_type, i, url, params_req)483 else:484 for i, url in enumerate(self._cameras_jpg_ip):485 self._append_pic_to_data(params, event_type, i, url)486 params['took'] = time() - tic487 self.log('Nuevo evento "{}" adquirido en {:.2f}s, con ts={}'488 .format(event_type, params['took'], params['ts']))489 self._events_data.append(params)490 with self._lock:491 self._in_capture_mode = False492 # self._ts_lastcap = now + dt.timedelta(seconds=params['took'])493 self._ts_lastcap = now494 else:495 if prioridad > 1:496 self.log('SOLAPAMIENTO DE LLAMADAS A APPEND_EVENT. POSPUESTO. "{}"; ts_lastcap={}'497 .format(event_type, self._ts_lastcap), 'WARNING')498 self.run_in(self.append_event_data, 1, **kwargs)499 else:500 self.log('SOLAPAMIENTO DE LLAMADAS A APPEND_EVENT. DESECHADO. "{}"; ts_lastcap={}'501 .format(event_type, self._ts_lastcap))502 def _reset_session_data(self):503 with self._lock:504 self._in_capture_mode = False505 self._alarm_state = False506 self._alarm_state_ts_trigger = None507 self._alarm_state_entity_trigger = None508 self._pre_alarm_on = False509 self._pre_alarm_ts_trigger = None510 self._pre_alarms = []511 self._post_alarms = []512 self._handler_periodic_trigger = None513 self._handler_retry_alert = None514 # noinspection PyUnusedLocal515 def _armado_sistema(self, *args):516 with self._lock:517 self._handler_armado_alarma = None518 self._alarm_on = True519 # set_global(self, GLOBAL_ALARM_STATE, True)520 self._reset_session_data()521 self.append_event_data(dict(event_type=EVENT_ACTIVACION))522 self.text_notification()523 # noinspection PyUnusedLocal524 def _main_switch_ch(self, entity, attribute, old, new, kwargs):525 if entity == self._main_switch:526 alarm_on = new == 'on'527 if alarm_on and (old == 'off'): # turn_on_alarm with delay528 self._handler_armado_alarma = self.run_in(self._armado_sistema, self._espera_a_armado_sec)529 self.log('--> ALARMA CONECTADA DENTRO DE {} SEGUNDOS'.format(self._espera_a_armado_sec))530 elif not alarm_on and (old == 'on'): # turn_off_alarm531 if self._handler_armado_alarma is not None:532 self.cancel_timer(self._handler_armado_alarma)533 self._handler_armado_alarma = None534 with self._lock:535 self._alarm_on = False536 # set_global(self, GLOBAL_ALARM_STATE, False)537 self._alarm_state = False538 # Operación con relés en apagado de alarma:539 [self.call_service('{}/turn_off'.format(ent.split('.')[0]), entity_id=ent)540 for ent in [self._rele_sirena, self._rele_secundario, self._led_act] if ent is not None]541 # send & reset events542 if self._events_data:543 self.append_event_data(dict(event_type=EVENT_DESCONEXION))544 self.text_notification()545 self.email_events_data()546 # reset ts alarm & pre-alarm547 self._reset_session_data()548 if self._alarm_lights is not None:549 self.call_service("light/turn_off", entity_id=self._alarm_lights, transition=1)550 self.log('--> ALARMA DESCONECTADA')551 elif entity == self._use_push_notifier_switch:552 self._use_push_notifier = new == 'on'553 self.log('SWITCH USAR PUSH NOTIFS: de "{}" a "{}" --> {}'.format(old, new, self._use_push_notifier))554 elif entity == self._silent_mode_switch:555 self._silent_mode = new == 'on'556 self.log('SILENT MODE: {}'.format(self._silent_mode))557 if self._alarm_state and self._silent_mode and (self._rele_sirena is not None):558 self.call_service('{}/turn_off'.format(self._rele_sirena.split('.')[0]), entity_id=self._rele_sirena)559 else:560 self.log('Entity unknown in _main_switch_ch: {} (from {} to {}, attrs={}'561 .format(entity, old, new, attribute), 'ERROR')562 # noinspection PyUnusedLocal563 def _switch_usar_input(self, entity, attribute, old, new, kwargs):564 k = self._dict_asign_switchs_inputs[entity]565 if (new == 'on') and (old == 'off'):566 # Turn ON input567 self._dict_use_inputs[k] = True568 elif (new == 'off') and (old == 'on'):569 # Turn OFF input570 self._dict_use_inputs[k] = False571 self.log('SWITCH USAR INPUT "{}" from {} to {}'.format(entity, old, new))572 def _validate_input(self, entity):573 # DEBUGGING NEW SENSORS574 # if entity in self._extra_sensors:575 # self.log('EXTRA SENSOR "{}": {}->{}'.format(entity, old, new))576 if self._alarm_on:577 if (entity in self._dict_use_inputs) and (self._dict_use_inputs[entity]):578 return True579 return False580 # noinspection PyUnusedLocal581 def _reset_alarm_state(self, *args):582 """Reset del estado de alarma ON. La alarma sigue encendida, pero se pasa a estado inactivo en espera"""583 process = False584 with self._lock:585 if self._alarm_on and self._alarm_state:586 self._alarm_state = False587 self._alarm_state_ts_trigger = None588 self._alarm_state_entity_trigger = None589 # self._events_data = []590 self._pre_alarms = []591 self._post_alarms = []592 self._pre_alarm_on = False593 self._pre_alarm_ts_trigger = None594 self._handler_periodic_trigger = None595 self._handler_retry_alert = None596 process = True597 if process:598 self.log('** RESET OF ALARM STATE')599 # apagado de relés de alarma:600 [self.call_service('{}/turn_off'.format(ent.split('.')[0]), entity_id=ent)601 for ent in [self._rele_sirena, self._rele_secundario, self._led_act] if ent is not None]602 if self._alarm_lights is not None:603 self.call_service("light/turn_off", entity_id=self._alarm_lights, transition=1)604 # noinspection PyUnusedLocal605 def _turn_off_sirena_in_alarm_state(self, *args):606 """Apaga el relé asociado a la sirena.607 La alarma sigue encendida y grabando eventos en activaciones de sensor y periódicamente."""608 process = False609 with self._lock:610 if self._alarm_on and self._alarm_state and not self._silent_mode:611 # self._silent_mode = True612 process = True613 if process:614 # apagado de relés de alarma:615 self.log('** Apagado del relé de la sirena de alarma')616 if self._rele_sirena is not None:617 self.call_service('{}/turn_off'.format(self._rele_sirena.split('.')[0]), entity_id=self._rele_sirena)618 if self._alarm_lights is not None:619 self.call_service("light/turn_off", entity_id=self._alarm_lights, transition=1)620 # noinspection PyUnusedLocal621 def _turn_off_prealarm(self, *args):622 proceed = False623 with self._lock:624 if self._pre_alarm_on and not self._alarm_state:625 self._pre_alarm_ts_trigger = None626 self._pre_alarm_on = False627 proceed = True628 if proceed:629 if self._led_act is not None:630 self.call_service('switch/turn_off', entity_id=self._led_act)631 self.log('*PREALARMA DESACTIVADA*')632 # noinspection PyUnusedLocal633 def _motion_detected(self, entity, attribute, old, new, kwargs):634 """Lógica de activación de alarma por detección de movimiento.635 - El 1º evento pone al sistema en 'pre-alerta', durante un tiempo determinado. Se genera un evento.636 - Si se produce un 2º evento en estado de pre-alerta, comienza el estado de alerta, se genera un evento,637 se disparan los relés asociados, se notifica al usuario con push_notif + email, y se inician los actuadores638 periódicos.639 - Las siguientes detecciones generan nuevos eventos, que se acumulan hasta que se desconecte la alarma y se640 notifique al usuario por email.641 """642 # self.log('DEBUG MOTION: {}, {}->{}'.format(entity, old, new))643 if self._validate_input(entity):644 # Actualiza persistent_notification de entity en cualquier caso645 # self._persistent_notification(entity)646 now = dt.datetime.now(tz=self._tz)647 delta_beat = 100648 # LOCK649 priority = 0650 with self._lock:651 if self._ts_lastbeat is not None:652 delta_beat = (now - self._ts_lastbeat).total_seconds()653 if delta_beat > MIN_TIME_BETWEEN_MOTION:654 if self._alarm_state:655 priority = 1656 elif self._pre_alarm_on:657 priority = 3658 self._alarm_state = True659 else:660 priority = 2661 self._pre_alarm_on = True662 self._ts_lastbeat = now663 # self.log('DEBUG MOTION "{}": "{}"->"{}" at {:%H:%M:%S.%f}. A={}, ST_A={}, ST_PRE-A={}'664 # .format(entity, old, new, now, self._alarm_on, self._alarm_state, self._pre_alarm_on))665 # Nuevo evento, con alarma conectada. Se ignora por ahora666 # if self._alarm_state:667 if priority == 1:668 self.log('(IN ALARM MODE) motion_detected in {}, âTbeat={:.6f}s'.format(entity, delta_beat))669 if self._led_act is not None:670 self.call_service('switch/toggle', entity_id=self._led_act)671 if self._is_too_old(self._ts_lastcap, self._min_delta_sec_events):672 self.append_event_data(dict(event_type=EVENT_EN_ALARMA, entity_trigger=entity))673 self.alarm_persistent_notification(entity, now)674 # Trigger ALARMA después de pre-alarma675 # elif self._pre_alarm_on:676 elif priority == 3:677 self.log('**** ALARMA!! **** activada por "{}", âTbeat={:.6f}s'.format(entity, delta_beat))678 # self.turn_on_alarm()679 self._alarm_state_ts_trigger = now680 self._alarm_state_entity_trigger = entity681 self._alarm_state = True682 if self._handler_periodic_trigger is None: # Sólo 1ª vez!683 if not self._silent_mode and (self._rele_sirena is not None):684 self.call_service('{}/turn_on'.format(self._rele_sirena.split('.')[0]),685 entity_id=self._rele_sirena)686 if self._rele_secundario is not None:687 self.call_service('{}/turn_on'.format(self._rele_secundario.split('.')[0]),688 entity_id=self._rele_secundario)689 self.append_event_data(dict(event_type=EVENT_ALARMA, entity_trigger=entity))690 self.text_notification(append_extra_data=True)691 self.alarm_persistent_notification()692 self.email_events_data()693 # Empieza a grabar eventos periódicos cada DELTA_SECS_TRIGGER:694 self._handler_periodic_trigger = self.run_in(self.periodic_capture_mode, self._delta_secs_trigger)695 if self._max_time_sirena_on is not None:696 # Programa el apagado automático de la sirena pasado cierto tiempo desde la activación.697 self.run_in(self._turn_off_sirena_in_alarm_state, self._max_time_sirena_on)698 if (self._handler_retry_alert is None) and (self._retry_push_alarm is not None): # Sólo 1ª vez!699 # Empieza a notificar la alarma conectada cada X minutos700 self._handler_retry_alert = self.run_in(self.periodic_alert, self._retry_push_alarm)701 # Sirena visual con RGB lights:702 if self._alarm_lights is not None:703 self.run_in(self._flash_alarm_lights, 2)704 # Dispara estado pre-alarma705 elif priority == 2:706 self.log('** PRE-ALARMA ** activada por "{}"'.format(entity), LOG_LEVEL)707 self._pre_alarm_ts_trigger = now708 self._pre_alarm_on = True709 self.run_in(self._turn_off_prealarm, self._reset_prealarm_time_sec)710 self.prealarm_persistent_notification(entity, now)711 self.append_event_data(dict(event_type=EVENT_PREALARMA, entity_trigger=entity))712 if self._led_act is not None:713 self.call_service('switch/turn_on', entity_id=self._led_act)714 else:715 self.log('** MOVIMIENTO DESECHADO ** activado por "{}", âTbeat={:.6f}s'.format(entity, delta_beat))716 # noinspection PyUnusedLocal717 def periodic_capture_mode(self, *args):718 """Ejecución periódica con la alarma encendida para capturar eventos cada cierto tiempo."""719 # self.log('EN PERIODIC_CAPTURE_MODE con âT={} s'.format(self._delta_secs_trigger))720 proceed = append_event = False721 with self._lock:722 if self._alarm_state:723 proceed = True724 append_event = self._is_too_old(self._ts_lastbeat, self._min_delta_sec_events)725 if proceed:726 if append_event:727 self.append_event_data(dict(event_type=EVENT_ALARMA_ENCENDIDA))728 self.run_in(self.periodic_capture_mode, self._delta_secs_trigger)729 else:730 # self.log('STOP PERIODIC CAPTURE MODE')731 self._handler_periodic_trigger = None732 # noinspection PyUnusedLocal733 def periodic_alert(self, *args):734 """Ejecución periódica con la alarma encendida para enviar una notificación recordando dicho estado."""735 self.log('EN PERIODIC_ALERT con âT={} s'.format(self._retry_push_alarm))736 proceed = False737 with self._lock:738 if self._alarm_state:739 proceed = True740 if proceed:741 self.periodic_alert_notification()742 self.run_in(self.periodic_alert, self._retry_push_alarm)743 else:744 self.log('STOP PERIODIC ALERT')745 self._handler_retry_alert = None746 # def _persistent_notification(self, trigger_entity, ts, title=None, unique_id=True):747 # f_name = notif_id = self._dict_friendly_names[trigger_entity]748 # if not unique_id:749 # notif_id += '_{:%y%m%d%H%M%S}'.format(ts)750 # message = 'Activación a las {:%H:%M:%S de %d/%m/%Y} por "{}"'.format(ts, f_name)751 # params = dict(message=message, title=title if title is not None else f_name, id=notif_id)752 # self._post_alarms.append((self._dict_friendly_names[trigger_entity], '{:%H:%M:%S}'.format(ts)))753 # self.persistent_notification(**params)754 # # self.log('PERSISTENT NOTIFICATION: {}'.format(params))755 def alarm_persistent_notification(self, trigger_entity=None, ts=None):756 """Notificación en el frontend de alarma activada."""757 if trigger_entity is not None:758 self._post_alarms.append((self._dict_friendly_names[trigger_entity], '{:%H:%M:%S}'.format(ts)))759 params_templ = dict(ts='{:%H:%M:%S}'.format(self._alarm_state_ts_trigger),760 entity=self._dict_friendly_names[self._alarm_state_entity_trigger],761 postalarms=self._post_alarms[::-1])762 message = JINJA2_ENV.get_template('persistent_notif_alarm.html').render(**params_templ)763 params = dict(message=message, title="ALARMA!!", id='alarm')764 # self.log('DEBUG ALARM PERSISTENT NOTIFICATION: {}'.format(params))765 self.persistent_notification(**params)766 def prealarm_persistent_notification(self, trigger_entity, ts):767 """Notificación en el frontend de pre-alarma activada."""768 self._pre_alarms.append((self._dict_friendly_names[trigger_entity], '{:%H:%M:%S}'.format(ts)))769 message = JINJA2_ENV.get_template('persistent_notif_prealarm.html').render(prealarms=self._pre_alarms[::-1])770 params = dict(message=message, title="PRE-ALARMA", id='prealarm')771 # self.log('DEBUG PRE-ALARM PERSISTENT NOTIFICATION: {}'.format(params))772 self.persistent_notification(**params)773 def _update_ios_notify_params(self, params, url_usar):774 if ((self._alarm_state_entity_trigger is not None) and775 (self._videostreams.get(self._alarm_state_entity_trigger))):776 # Get the camera video stream as function of trigger777 cam_entity = self._videostreams.get(778 self._alarm_state_entity_trigger)779 params.update(780 data=dict(781 push=dict(badge=10, sound=SOUND_MOTION,782 category="camera"),783 entity_id=cam_entity,784 attachment=dict(url=url_usar)))785 else:786 params.update(787 data=dict(788 push=dict(badge=10, sound=SOUND_MOTION,789 category="alarmsounded"),790 attachment=dict(url=url_usar)))791 return params792 def periodic_alert_notification(self):793 """Notificación de recordatorio de alarma encendida."""794 if self._alarm_state:795 extra = ''796 if self._rele_sirena is not None:797 extra += 'Sirena: {}. '.format(self.get_state(self._rele_sirena))798 msg = AVISO_RETRY_ALARMA_ENCENDIDA_MSG.format(self._alarm_state_ts_trigger, extra)799 params = dict(title=AVISO_RETRY_ALARMA_ENCENDIDA_TITLE, message=msg)800 if self._use_push_notifier:801 service = self._push_notifier802 if self._events_data and ('url_img1' in self._events_data[-1]):803 url_usar = self._events_data[-1]['url_img1']804 else:805 url_usar = self._secrets['hass_base_url']806 if 'ios' in service:807 params = self._update_ios_notify_params(params, url_usar)808 elif 'pushbullet' in service:809 # params.update(data=dict(url=url_usar))810 params.update(target=self._secrets['pb_target'], data=dict(url=url_usar))811 self.log('PUSH TEXT NOTIFICATION "{title}": {message}, {data}'.format(**params))812 else:813 params.update(target=self._secrets['email_target'])814 params['message'] += '\n\nURL del sistema de vigilancia: {}'.format(self._secrets['hass_base_url'])815 service = self._email_notifier816 self.log('EMAIL RAW TEXT NOTIFICATION: {title}: {message}'.format(**params))817 self.call_service(service, **params)818 def text_notification(self, append_extra_data=False):819 """EnvÃa una notificación de texto plano con el status del último evento añadido."""820 if self._events_data:821 last_event = self._events_data[-1]822 event_type = last_event['event_type']823 pre_alarm_ts = '{:%H:%M:%S}'.format(self._pre_alarm_ts_trigger) if self._pre_alarm_ts_trigger else None824 alarm_ts = '{:%H:%M:%S}'.format(self._alarm_state_ts_trigger) if self._alarm_state_ts_trigger else None825 params_templ = dict(pre_alarm_ts=pre_alarm_ts, alarm_ts=alarm_ts,826 alarm_entity=self._alarm_state_entity_trigger, evento=last_event,827 pirs=self._pirs, cam_movs=self._camera_movs,828 extra_sensors=[(s, self._dict_sensor_classes[s]) for s in self._extra_sensors],829 friendly_names=self._dict_friendly_names)830 msg = JINJA2_ENV.get_template('raw_text_pbnotif.html').render(**params_templ)831 msg_text = msg.replace('</pre>', '').replace('<pre>', '')832 params = dict(title=EVENT_TYPES[event_type][0], message=msg_text)833 if self._use_push_notifier:834 service = self._push_notifier835 if 'pushbullet' in service:836 params.update(target=self._secrets['pb_target'])837 if append_extra_data:838 if 'url_img1' in last_event:839 url_usar = last_event['url_img1']840 else:841 url_usar = self._secrets['hass_base_url']842 if 'ios' in service:843 params = self._update_ios_notify_params(844 params, url_usar)845 elif 'pushbullet' in service:846 params.update(data=dict(url=url_usar))847 # self.log('PUSH TEXT NOTIFICATION "{title}: {message}"'.format(**params))848 self.log('PUSH TEXT NOTIFICATION "{title}"'.format(**params))849 else:850 params.update(target=self._secrets['email_target'])851 service = self._email_notifier852 self.log('EMAIL RAW TEXT NOTIFICATION: {title}: {message}'.format(**params))853 self.call_service(service, **params)854 def get_events_for_email(self):855 """Devuelve los eventos acumulados filtrados y ordenados, junto a los paths de las imágenes adjuntadas."""856 def _count_included_events(evs):857 """Cuenta los eventos marcados para inclusión."""858 return len(list(filter(lambda x: x['incluir'], evs)))859 def _ok_num_events(evs, num_max, prioridad_filtro, logger):860 """Marca 'incluir' = False para eventos de prioridad < X, hasta reducir a num_max."""861 n_included = n_included_init = _count_included_events(evs)862 if n_included > num_max:863 # Filtrado eliminando eventos periódicos, después prealarmas864 idx = len(evs) - 1865 while (idx >= 0) and (n_included > num_max):866 if evs[idx]['incluir'] and (evs[idx]['prioridad'] < prioridad_filtro):867 evs[idx]['incluir'] = False868 n_included -= 1869 idx -= 1870 logger('Filtrado de eventos con P < {} por exceso. De {}, quedan {} eventos.'871 .format(prioridad_filtro, n_included_init, n_included))872 return n_included <= num_max873 eventos = self._events_data.copy()874 self._events_data = []875 # Filtrado de eventos de baja prioridad si hay demasiados876 ok_filter, prioridad_min = False, 1877 while (not _ok_num_events(eventos, self._max_report_events, prioridad_min, self.log)878 and (prioridad_min <= 5)):879 # self.log('Filtrado de eventos con P < {} por exceso. De {}, quedan {} eventos.'880 # .format(prioridad_min, len(self._events_data), _count_included_events(self._events_data)))881 prioridad_min += 1882 # Eventos e imágenes para email attachments (cid:#):883 num_included_events = _count_included_events(eventos)884 eventos = eventos[::-1]885 counter_imgs, paths_imgs = 0, []886 for event in filter(lambda x: x['incluir'], eventos):887 for i in range(len(self._cameras_jpg_ip)):888 if event['ok_img{}'.format(i + 1)]:889 event['id_img{}'.format(i + 1)] = event['name_img{}'.format(i + 1)]890 paths_imgs.append(event['path_img{}'.format(i + 1)])891 counter_imgs += 1892 return eventos, paths_imgs, num_included_events893 # noinspection PyUnusedLocal894 def email_events_data(self, *args):895 """EnvÃa por email los eventos acumulados."""896 tic = time()897 if self._events_data:898 now = dt.datetime.now(tz=self._tz)899 eventos, paths_imgs, num_included_events = self.get_events_for_email()900 # Informe901 r_name = 'report_{:%Y%m%d_%H%M%S}.html'.format(now)902 last_event = eventos[0]903 color_title = last_event['event_color'] if EVENT_TYPES[last_event['event_type']][2] else HASS_COLOR904 url_local_path_report = '{}/{}/{}/{}'.format(self._secrets['hass_base_url'], 'local', DIR_INFORMES, r_name)905 title = EVENT_TYPES[last_event['event_type']][3]906 ts_title = '{:%-d-%-m-%Y}'.format(now.date())907 # Render html reports for email & static server908 report_templ = JINJA2_ENV.get_template('report_template.html')909 params_templ = dict(title=title, ts_title=ts_title, color_title=color_title,910 eventos=eventos, include_images_base64=False,911 num_cameras=len(self._cameras_jpg_ip),912 pirs=self._pirs, cam_movs=self._camera_movs,913 extra_sensors=[(s, self._dict_sensor_classes[s]) for s in self._extra_sensors],914 friendly_names=self._dict_friendly_names)915 html_email = report_templ.render(is_email=True, url_local_report=url_local_path_report, **params_templ)916 html_static = report_templ.render(is_email=False, **params_templ)917 path_disk_report = os.path.join(self._path_reports, r_name)918 try:919 with open(path_disk_report, 'w') as f:920 f.write(html_static)921 self.log('INFORME POR EMAIL con {} eventos ({} con imágenes [{}]) generado y guardado en {} en {:.2f} s'922 .format(len(eventos), num_included_events, len(paths_imgs), path_disk_report, time() - tic))923 except Exception as e:924 self.log('ERROR EN SAVE REPORT TO DISK: {} [{}]'.format(e, e.__class__))925 self._events_data = []926 params = dict(title="{} - {}".format(title, ts_title), target=self._secrets['email_target'],927 message='No text!', data=dict(html=html_email, images=paths_imgs))928 self.call_service(self._email_notifier, **params)929 else:930 self.log('Se solicita enviar eventos, pero no hay ninguno! --> {}'.format(self._events_data), 'ERROR')931 # noinspection PyUnusedLocal932 def _flash_alarm_lights(self, *args):933 """Recursive-like method for flashing lights with cycling colors."""934 if self._alarm_lights is not None:935 if self._alarm_state:936 self.call_service("light/turn_on", entity_id=self._alarm_lights,937 rgb_color=next(self._cycle_colors), brightness=255, transition=1)...
application.py
Source:application.py
1import os2import logging3import asyncio4from typing import Iterator, List, Optional56import attr7import psutil89from . import __prog__, __version__10from .flv.operators import MetaData, StreamProfile11from .disk_space import SpaceMonitor, SpaceReclaimer12from .bili.helpers import ensure_room_id13from .task import (14 RecordTaskManager,15 TaskData,16 TaskParam,17 VideoFileDetail,18 DanmakuFileDetail,19)20from .exception import ExistsError, ExceptionHandler, exception_callback21from .event.event_submitters import SpaceEventSubmitter22from .setting import (23 SettingsManager,24 Settings,25 SettingsIn,26 SettingsOut,27 TaskOptions,28)29from .setting.typing import KeySetOfSettings30from .notification import (31 EmailNotifier,32 ServerchanNotifier,33 PushdeerNotifier,34 PushplusNotifier,35 TelegramNotifier,36)37from .webhook import WebHookEmitter383940logger = logging.getLogger(__name__)414243@attr.s(auto_attribs=True, slots=True, frozen=True)44class AppInfo:45 name: str46 version: str47 pid: int48 ppid: int49 create_time: float50 cwd: str51 exe: str52 cmdline: List[str]535455@attr.s(auto_attribs=True, slots=True, frozen=True)56class AppStatus:57 cpu_percent: float58 memory_percent: float59 num_threads: int606162class Application:63 def __init__(self, settings: Settings) -> None:64 self._out_dir = settings.output.out_dir65 self._settings_manager = SettingsManager(self, settings)66 self._task_manager = RecordTaskManager(self._settings_manager)6768 @property69 def info(self) -> AppInfo:70 p = psutil.Process(os.getpid())71 with p.oneshot():72 return AppInfo(73 name=__prog__,74 version=__version__,75 pid=p.pid,76 ppid=p.ppid(),77 create_time=p.create_time(),78 cwd=p.cwd(),79 exe=p.exe(),80 cmdline=p.cmdline(),81 )8283 @property84 def status(self) -> AppStatus:85 p = psutil.Process(os.getpid())86 with p.oneshot():87 return AppStatus(88 cpu_percent=p.cpu_percent(),89 memory_percent=p.memory_percent(),90 num_threads=p.num_threads(),91 )9293 def run(self) -> None:94 asyncio.run(self._run())9596 async def _run(self) -> None:97 self._loop = asyncio.get_running_loop()9899 await self.launch()100 try:101 self._interrupt_event = asyncio.Event()102 await self._interrupt_event.wait()103 finally:104 await self.exit()105106 async def launch(self) -> None:107 self._setup()108 logger.debug(f'Default umask {os.umask(000)}')109 logger.info(f'Launched Application v{__version__}')110 task = asyncio.create_task(self._task_manager.load_all_tasks())111 task.add_done_callback(exception_callback)112113 async def exit(self) -> None:114 await self._exit()115 logger.info('Exited Application')116117 async def abort(self) -> None:118 await self._exit(force=True)119 logger.info('Aborted Application')120121 async def _exit(self, force: bool = False) -> None:122 await self._task_manager.stop_all_tasks(force=force)123 await self._task_manager.destroy_all_tasks()124 self._destroy()125126 async def restart(self) -> None:127 logger.info('Restarting Application...')128 await self.exit()129 await self.launch()130131 def has_task(self, room_id: int) -> bool:132 return self._task_manager.has_task(room_id)133134 async def add_task(self, room_id: int) -> int:135 room_id = await ensure_room_id(room_id)136137 if self._task_manager.has_task(room_id):138 raise ExistsError(139 f'a task for the room {room_id} is already existed'140 )141142 settings = self._settings_manager.find_task_settings(room_id)143 if not settings:144 settings = await self._settings_manager.add_task_settings(room_id)145146 await self._task_manager.add_task(settings)147148 return room_id149150 async def remove_task(self, room_id: int) -> None:151 logger.info(f'Removing task {room_id}...')152 await self._task_manager.remove_task(room_id)153 await self._settings_manager.remove_task_settings(room_id)154 logger.info(f'Successfully removed task {room_id}')155156 async def remove_all_tasks(self) -> None:157 logger.info('Removing all tasks...')158 await self._task_manager.remove_all_tasks()159 await self._settings_manager.remove_all_task_settings()160 logger.info('Successfully removed all tasks')161162 async def start_task(self, room_id: int) -> None:163 logger.info(f'Starting task {room_id}...')164 await self._task_manager.start_task(room_id)165 await self._settings_manager.mark_task_enabled(room_id)166 logger.info(f'Successfully started task {room_id}')167168 async def stop_task(self, room_id: int, force: bool = False) -> None:169 logger.info(f'Stopping task {room_id}...')170 await self._task_manager.stop_task(room_id, force)171 await self._settings_manager.mark_task_disabled(room_id)172 logger.info(f'Successfully stopped task {room_id}')173174 async def start_all_tasks(self) -> None:175 logger.info('Starting all tasks...')176 await self._task_manager.start_all_tasks()177 await self._settings_manager.mark_all_tasks_enabled()178 logger.info('Successfully started all tasks')179180 async def stop_all_tasks(self, force: bool = False) -> None:181 logger.info('Stopping all tasks...')182 await self._task_manager.stop_all_tasks(force)183 await self._settings_manager.mark_all_tasks_disabled()184 logger.info('Successfully stopped all tasks')185186 async def enable_task_monitor(self, room_id: int) -> None:187 logger.info(f'Enabling monitor for task {room_id}...')188 await self._task_manager.enable_task_monitor(room_id)189 await self._settings_manager.mark_task_monitor_enabled(room_id)190 logger.info(f'Successfully enabled monitor for task {room_id}')191192 async def disable_task_monitor(self, room_id: int) -> None:193 logger.info(f'Disabling monitor for task {room_id}...')194 await self._task_manager.disable_task_monitor(room_id)195 await self._settings_manager.mark_task_monitor_disabled(room_id)196 logger.info(f'Successfully disabled monitor for task {room_id}')197198 async def enable_all_task_monitors(self) -> None:199 logger.info('Enabling monitors for all tasks...')200 await self._task_manager.enable_all_task_monitors()201 await self._settings_manager.mark_all_task_monitors_enabled()202 logger.info('Successfully enabled monitors for all tasks')203204 async def disable_all_task_monitors(self) -> None:205 logger.info('Disabling monitors for all tasks...')206 await self._task_manager.disable_all_task_monitors()207 await self._settings_manager.mark_all_task_monitors_disabled()208 logger.info('Successfully disabled monitors for all tasks')209210 async def enable_task_recorder(self, room_id: int) -> None:211 logger.info(f'Enabling recorder for task {room_id}...')212 await self._task_manager.enable_task_recorder(room_id)213 await self._settings_manager.mark_task_recorder_enabled(room_id)214 logger.info(f'Successfully enabled recorder for task {room_id}')215216 async def disable_task_recorder(217 self, room_id: int, force: bool = False218 ) -> None:219 logger.info(f'Disabling recorder for task {room_id}...')220 await self._task_manager.disable_task_recorder(room_id, force)221 await self._settings_manager.mark_task_recorder_disabled(room_id)222 logger.info(f'Successfully disabled recorder for task {room_id}')223224 async def enable_all_task_recorders(self) -> None:225 logger.info('Enabling recorders for all tasks...')226 await self._task_manager.enable_all_task_recorders()227 await self._settings_manager.mark_all_task_recorders_enabled()228 logger.info('Successfully enabled recorders for all tasks')229230 async def disable_all_task_recorders(self, force: bool = False) -> None:231 logger.info('Disabling recorders for all tasks...')232 await self._task_manager.disable_all_task_recorders(force)233 await self._settings_manager.mark_all_task_recorders_disabled()234 logger.info('Successfully disabled recorders for all tasks')235236 def get_task_data(self, room_id: int) -> TaskData:237 return self._task_manager.get_task_data(room_id)238239 def get_all_task_data(self) -> Iterator[TaskData]:240 yield from self._task_manager.get_all_task_data()241242 def get_task_param(self, room_id: int) -> TaskParam:243 return self._task_manager.get_task_param(room_id)244245 def get_task_metadata(self, room_id: int) -> Optional[MetaData]:246 return self._task_manager.get_task_metadata(room_id)247248 def get_task_stream_profile(self, room_id: int) -> StreamProfile:249 return self._task_manager.get_task_stream_profile(room_id)250251 def get_task_video_file_details(252 self, room_id: int253 ) -> Iterator[VideoFileDetail]:254 yield from self._task_manager.get_task_video_file_details(room_id)255256 def get_task_danmaku_file_details(257 self, room_id: int258 ) -> Iterator[DanmakuFileDetail]:259 yield from self._task_manager.get_task_danmaku_file_details(room_id)260261 def can_cut_stream(self, room_id: int) -> bool:262 return self._task_manager.can_cut_stream(room_id)263264 def cut_stream(self, room_id: int) -> bool:265 return self._task_manager.cut_stream(room_id)266267 async def update_task_info(self, room_id: int) -> None:268 logger.info(f'Updating info for task {room_id}...')269 await self._task_manager.update_task_info(room_id)270 logger.info(f'Successfully updated info for task {room_id}')271272 async def update_all_task_infos(self) -> None:273 logger.info('Updating info for all tasks...')274 await self._task_manager.update_all_task_infos()275 logger.info('Successfully updated info for all tasks')276277 def get_settings(278 self,279 include: Optional[KeySetOfSettings] = None,280 exclude: Optional[KeySetOfSettings] = None,281 ) -> SettingsOut:282 return self._settings_manager.get_settings(include, exclude)283284 async def change_settings(self, settings: SettingsIn) -> SettingsOut:285 return await self._settings_manager.change_settings(settings)286287 def get_task_options(self, room_id: int) -> TaskOptions:288 return self._settings_manager.get_task_options(room_id)289290 async def change_task_options(291 self, room_id: int, options: TaskOptions292 ) -> TaskOptions:293 return await self._settings_manager.change_task_options(294 room_id, options295 )296297 def _setup(self) -> None:298 self._setup_logger()299 self._setup_exception_handler()300 self._setup_space_monitor()301 self._setup_space_event_submitter()302 self._setup_space_reclaimer()303 self._setup_notifiers()304 self._setup_webhooks()305306 def _setup_logger(self) -> None:307 self._settings_manager.apply_logging_settings()308309 def _setup_exception_handler(self) -> None:310 self._exception_handler = ExceptionHandler()311 self._exception_handler.enable()312313 def _setup_space_monitor(self) -> None:314 self._space_monitor = SpaceMonitor(self._out_dir)315 self._settings_manager.apply_space_monitor_settings()316 self._space_monitor.enable()317318 def _setup_space_event_submitter(self) -> None:319 self._space_event_submitter = SpaceEventSubmitter(self._space_monitor)320321 def _setup_space_reclaimer(self) -> None:322 self._space_reclaimer = SpaceReclaimer(323 self._space_monitor, self._out_dir,324 )325 self._settings_manager.apply_space_reclaimer_settings()326 self._space_reclaimer.enable()327328 def _setup_notifiers(self) -> None:329 self._email_notifier = EmailNotifier()330 self._serverchan_notifier = ServerchanNotifier()331 self._pushdeer_notifier = PushdeerNotifier()332 self._pushplus_notifier = PushplusNotifier()333 self._telegram_notifier = TelegramNotifier()334 self._settings_manager.apply_email_notification_settings()335 self._settings_manager.apply_serverchan_notification_settings()336 self._settings_manager.apply_pushdeer_notification_settings()337 self._settings_manager.apply_pushplus_notification_settings()338 self._settings_manager.apply_telegram_notification_settings()339340 def _setup_webhooks(self) -> None:341 self._webhook_emitter = WebHookEmitter()342 self._settings_manager.apply_webhooks_settings()343 self._webhook_emitter.enable()344345 def _destroy(self) -> None:346 self._destroy_space_reclaimer()347 self._destroy_space_event_submitter()348 self._destroy_space_monitor()349 self._destroy_notifiers()350 self._destroy_webhooks()351 self._destroy_exception_handler()352353 def _destroy_space_monitor(self) -> None:354 self._space_monitor.disable()355 del self._space_monitor356357 def _destroy_space_event_submitter(self) -> None:358 del self._space_event_submitter359360 def _destroy_space_reclaimer(self) -> None:361 self._space_reclaimer.disable()362 del self._space_reclaimer363364 def _destroy_notifiers(self) -> None:365 self._email_notifier.disable()366 self._serverchan_notifier.disable()367 self._pushdeer_notifier.disable()368 self._pushplus_notifier.disable()369 self._telegram_notifier.disable()370 del self._email_notifier371 del self._serverchan_notifier372 del self._pushdeer_notifier373 del self._pushplus_notifier374 del self._telegram_notifier375376 def _destroy_webhooks(self) -> None:377 self._webhook_emitter.disable()378 del self._webhook_emitter379380 def _destroy_exception_handler(self) -> None:381 self._exception_handler.disable()
...
notifications.py
Source:notifications.py
...126 api_key = self._get_from_config_with_legacy('pushbullet', 'pushbullet_api_key', 'api_key')127 if api_key:128 data = {"type": "note", "title": message.get_title(), "body": message.get_short_message()}129 _post_request("https://api.pushbullet.com/api/pushes", data=data, auth=(api_key, ""))130 def _email_notifier(self, message):131 email_config = config.root.plugin_config.notifications.email132 email_kwargs = {133 'from_email': email_config.from_email,134 'subject': message.get_title(),135 'body': message.get_html_message(),136 'smtp_server': email_config.smtp_server,137 'to_list': email_config.to_list or None,138 'cc_list': email_config.cc_list,139 }140 if all(value is not None for value in email_kwargs.values()):141 _send_email(**email_kwargs)142 def _slack_notifier(self, message):143 slack_config = config.root.plugin_config.notifications.slack144 if (slack_config.url is None) or (slack_config.channel is None):...
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!