def __init__(self, name, google_key, locale, units, timezone, time_limit, max_attempts, location, cache_type, geofence_file, debug): # Set the name of the Manager self.name = str(name).lower() self._log = self._create_logger(self.name) self._rule_log = self.get_child_logger('rules') self.__debug = debug # Get the Google Maps AP# TODO: Improve error checking self._google_key = None self._gmaps_service = None if str(google_key).lower() != 'none': self._google_key = google_key self._gmaps_service = GMaps(google_key) self._gmaps_reverse_geocode = False self._gmaps_distance_matrix = set() self._language = locale self.__locale = Locale(locale) # Setup the language-specific stuff self.__units = units # type of unit used for distances self.__timezone = timezone # timezone for time calculations self.__time_limit = time_limit # Minimum time remaining # Location should be [lat, lng] (or None for no location) self.__location = None if str(location).lower() != 'none': self.set_location(location) else: self._log.warning("NO LOCATION SET - this may cause issues " "with distance related DTS.") # Create cache self.__cache = cache_factory(self, cache_type) # Load and Setup the Pokemon Filters self._mons_enabled, self._mon_filters = False, OrderedDict() self._stops_enabled, self._stop_filters = False, OrderedDict() self._gyms_enabled, self._gym_filters = False, OrderedDict() self._ignore_neutral = False self._eggs_enabled, self._egg_filters = False, OrderedDict() self._raids_enabled, self._raid_filters = False, OrderedDict() self._weather_enabled, self._weather_filters = False, OrderedDict() # Create the Geofences to filter with from given file self.geofences = None if str(geofence_file).lower() != 'none': self.geofences = load_geofence_file(get_path(geofence_file)) # Create the alarms to send notifications out with self._alarms = {} self._max_attempts = int(max_attempts) # TODO: Move to alarm level # Initialize Rules self.__mon_rules = {} self.__stop_rules = {} self.__gym_rules = {} self.__egg_rules = {} self.__raid_rules = {} self.__weather_rules = {} # Initialize the queue and start the process self.__queue = Queue() self.__event = Event() self.__process = None
class Manager(object): def __init__(self, name, google_key, locale, units, timezone, time_limit, max_attempts, location, cache_type, geofence_file, debug): # Set the name of the Manager self.name = str(name).lower() self._log = self._create_logger(self.name) self._rule_log = self.get_child_logger('rules') self.__debug = debug # Get the Google Maps AP# TODO: Improve error checking self._google_key = None self._gmaps_service = None if str(google_key).lower() != 'none': self._google_key = google_key self._gmaps_service = GMaps(google_key) self._gmaps_reverse_geocode = False self._gmaps_distance_matrix = set() self._language = locale self.__locale = Locale(locale) # Setup the language-specific stuff self.__units = units # type of unit used for distances self.__timezone = timezone # timezone for time calculations self.__time_limit = time_limit # Minimum time remaining # Location should be [lat, lng] (or None for no location) self.__location = None if str(location).lower() != 'none': self.set_location(location) else: self._log.warning("NO LOCATION SET - this may cause issues " "with distance related DTS.") # Create cache self.__cache = cache_factory(self, cache_type) # Load and Setup the Pokemon Filters self._mons_enabled, self._mon_filters = False, OrderedDict() self._stops_enabled, self._stop_filters = False, OrderedDict() self._gyms_enabled, self._gym_filters = False, OrderedDict() self._ignore_neutral = False self._eggs_enabled, self._egg_filters = False, OrderedDict() self._raids_enabled, self._raid_filters = False, OrderedDict() self._weather_enabled, self._weather_filters = False, OrderedDict() # Create the Geofences to filter with from given file self.geofences = None if str(geofence_file).lower() != 'none': self.geofences = load_geofence_file(get_path(geofence_file)) # Create the alarms to send notifications out with self._alarms = {} self._max_attempts = int(max_attempts) # TODO: Move to alarm level # Initialize Rules self.__mon_rules = {} self.__stop_rules = {} self.__gym_rules = {} self.__egg_rules = {} self.__raid_rules = {} self.__weather_rules = {} # Initialize the queue and start the process self.__queue = Queue() self.__event = Event() self.__process = None # ~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROCESS CONTROL ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Update the object into the queue def update(self, obj): self.__queue.put(obj) # Get the name of this Manager def get_name(self): return self.name # Tell the process to finish up and go home def stop(self): self._log.info("Manager {} shutting down... {} items in queue." "".format(self.name, self.__queue.qsize())) self.__event.set() def join(self): self.__process.join(timeout=20) if not self.__process.ready(): self._log.warning("Manager {} could not be stopped in time! " "Forcing process to stop.".format(self.name)) self.__process.kill(timeout=2, block=True) # Force stop else: self._log.info("Manager {} successfully stopped!".format( self.name)) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GMAPS API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def enable_gmaps_reverse_geocoding(self): """Enable GMaps Reverse Geocoding DTS for triggered Events. """ if not self._gmaps_service: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No GMaps API key has been set.") self._gmaps_reverse_geocode = True def disable_gmaps_reverse_geocoding(self): """Disable GMaps Reverse Geocoding DTS for triggered Events. """ self._gmaps_reverse_geocode = False def enable_gmaps_distance_matrix(self, mode): """Enable 'mode' Distance Matrix DTS for triggered Events. """ if not self.__location: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No Manager location has been set.") elif not self._gmaps_service: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No GMaps API key has been provided.") elif mode not in GMaps.TRAVEL_MODES: raise ValueError("Unable to enable distance matrix mode: " "{} is not a valid mode.".format(mode)) self._gmaps_distance_matrix.add(mode) def disable_gmaps_dm_walking(self, mode): """Disable 'mode' Distance Matrix DTS for triggered Events. """ if mode not in GMaps.TRAVEL_MODES: raise ValueError("Unable to disable distance matrix mode: " "Invalid mode specified.") self._gmaps_distance_matrix.discard(mode) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGGING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @staticmethod def _create_logger(mgr_name): """ Internal method for initializing manager loggers. """ # Create a Filter to pass on manager name log = logging.getLogger('pokealarm.{}'.format(mgr_name)) return log def get_child_logger(self, name): """ Get a child logger of this manager. """ logger = self._log.getChild(name) logger.addFilter(ContextFilter()) return logger def set_log_level(self, log_level): if log_level == 1: self._log.setLevel(logging.WARNING) elif log_level == 2: self._log.setLevel(logging.INFO) self._log.getChild("cache").setLevel(logging.WARNING) self._log.getChild("filters").setLevel(logging.WARNING) self._log.getChild("alarms").setLevel(logging.WARNING) elif log_level == 3: self._log.setLevel(logging.INFO) self._log.getChild("cache").setLevel(logging.INFO) self._log.getChild("filters").setLevel(logging.WARNING) self._log.getChild("alarms").setLevel(logging.WARNING) elif log_level == 4: self._log.setLevel(logging.INFO) self._log.getChild("cache").setLevel(logging.INFO) self._log.getChild("filters").setLevel(logging.INFO) self._log.getChild("alarms").setLevel(logging.INFO) elif log_level == 5: self._log.setLevel(logging.DEBUG) self._log.getChild("cache").setLevel(logging.DEBUG) self._log.getChild("filters").setLevel(logging.DEBUG) self._log.getChild("alarms").setLevel(logging.DEBUG) else: raise ValueError("Unable to set verbosity, must be an " "integer between 1 and 5.") self._log.debug("Verbosity set to %s", log_level) def add_file_logger(self, path, max_size_mb, ct): setup_file_handler(self._log, path, max_size_mb, ct) self._log.debug("Added new file logger to %s", path) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FILTERS API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Enable/Disable Monster notifications def set_monsters_enabled(self, boolean): self._mons_enabled = parse_bool(boolean) self._log.debug("Monster notifications %s", "enabled" if self._mons_enabled else "disabled") # Add new Monster Filter def add_monster_filter(self, name, settings): if name in self._mon_filters: raise ValueError("Unable to add Monster Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.MonFilter(self, name, settings) self._mon_filters[name] = f self._log.debug("Monster filter '%s' set: %s", name, f) # Enable/Disable Stops notifications def set_stops_enabled(self, boolean): self._stops_enabled = parse_bool(boolean) self._log.debug("Stops notifications %s!", "enabled" if self._stops_enabled else "disabled") # Add new Stop Filter def add_stop_filter(self, name, settings): if name in self._stop_filters: raise ValueError("Unable to add Stop Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.StopFilter(self, name, settings) self._stop_filters[name] = f self._log.debug("Stop filter '%s' set: %s", name, f) # Enable/Disable Gym notifications def set_gyms_enabled(self, boolean): self._gyms_enabled = parse_bool(boolean) self._log.debug("Gyms notifications %s!", "enabled" if self._gyms_enabled else "disabled") # Enable/Disable Stops notifications def set_ignore_neutral(self, boolean): self._ignore_neutral = parse_bool(boolean) self._log.debug("Ignore neutral set to %s!", self._ignore_neutral) # Add new Gym Filter def add_gym_filter(self, name, settings): if name in self._gym_filters: raise ValueError("Unable to add Gym Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.GymFilter(self, name, settings) self._gym_filters[name] = f self._log.debug("Gym filter '%s' set: %s", name, f) # Enable/Disable Egg notifications def set_eggs_enabled(self, boolean): self._eggs_enabled = parse_bool(boolean) self._log.debug("Egg notifications %s!", "enabled" if self._eggs_enabled else "disabled") # Add new Egg Filter def add_egg_filter(self, name, settings): if name in self._egg_filters: raise ValueError("Unable to add Egg Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.EggFilter(self, name, settings) self._egg_filters[name] = f self._log.debug("Egg filter '%s' set: %s", name, f) # Enable/Disable Stops notifications def set_raids_enabled(self, boolean): self._raids_enabled = parse_bool(boolean) self._log.debug("Raid notifications %s!", "enabled" if self._raids_enabled else "disabled") # Add new Raid Filter def add_raid_filter(self, name, settings): if name in self._raid_filters: raise ValueError("Unable to add Raid Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.RaidFilter(self, name, settings) self._raid_filters[name] = f self._log.debug("Raid filter '%s' set: %s", name, f) # Enable/Disable Weather notifications def set_weather_enabled(self, boolean): self._weather_enabled = parse_bool(boolean) self._log.debug("Weather notifications %s!", "enabled" if self._weather_enabled else "disabled") # Add new Weather Filter def add_weather_filter(self, name, settings): if name in self._weather_filters: raise ValueError("Unable to add Weather Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.WeatherFilter(self, name, settings) self._weather_filters[name] = f self._log.debug("Weather filter '%s' set: %s", name, f) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ALARMS API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def add_alarm(self, name, settings): if name in self._alarms: raise ValueError("Unable to add new Alarm: Alarm with the name " "{} already exists!".format(name)) alarm = Alarms.alarm_factory(self, settings, self._max_attempts, self._google_key) self._alarms[name] = alarm # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RULES API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Add new Monster Rule def add_monster_rule(self, name, filters, alarms): if name in self.__mon_rules: raise ValueError("Unable to add Rule: Monster Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._mon_filters: raise ValueError("Unable to create Rule: No Monster Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__mon_rules[name] = Rule(filters, alarms) # Add new Stop Rule def add_stop_rule(self, name, filters, alarms): if name in self.__stop_rules: raise ValueError("Unable to add Rule: Stop Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._stop_filters: raise ValueError("Unable to create Rule: No Stop Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__stop_rules[name] = Rule(filters, alarms) # Add new Gym Rule def add_gym_rule(self, name, filters, alarms): if name in self.__gym_rules: raise ValueError("Unable to add Rule: Gym Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._gym_filters: raise ValueError("Unable to create Rule: No Gym Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__gym_rules[name] = Rule(filters, alarms) # Add new Egg Rule def add_egg_rule(self, name, filters, alarms): if name in self.__egg_rules: raise ValueError("Unable to add Rule: Egg Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._egg_filters: raise ValueError("Unable to create Rule: No Egg Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__egg_rules[name] = Rule(filters, alarms) # Add new Raid Rule def add_raid_rule(self, name, filters, alarms): if name in self.__raid_rules: raise ValueError("Unable to add Rule: Raid Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._raid_filters: raise ValueError("Unable to create Rule: No Raid Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__raid_rules[name] = Rule(filters, alarms) # Add new Weather Rule def add_weather_rule(self, name, filters, alarms): if name in self.__weather_rules: raise ValueError("Unable to add Rule: Weather Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._weather_filters: raise ValueError("Unable to create Rule: No Weather Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__weather_rules[name] = Rule(filters, alarms) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MANAGER LOADING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HANDLE EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Start it up def start(self): self.__process = gevent.spawn(self.run) def setup_in_process(self): # Update config config['DEBUG'] = self.__debug config['ROOT_PATH'] = os.path.abspath("{}/..".format( os.path.dirname(__file__))) # Hush some new loggers logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) if config['DEBUG'] is True: logging.getLogger().setLevel(logging.DEBUG) # Conect the alarms and send the start up message for alarm in self._alarms.values(): alarm.connect() alarm.startup_message() # Main event handler loop def run(self): self.setup_in_process() last_clean = datetime.utcnow() while True: # Run forever and ever # Clean out visited every 5 minutes if datetime.utcnow() - last_clean > timedelta(minutes=5): self._log.debug("Cleaning cache...") self.__cache.clean_and_save() last_clean = datetime.utcnow() try: # Get next object to process event = self.__queue.get(block=True, timeout=5) except gevent.queue.Empty: # Check if the process should exit process if self.__event.is_set(): break # Explict context yield gevent.sleep(0) continue try: kind = type(event) self._log.debug("Processing event: %s", event.id) if kind == Events.MonEvent: self.process_monster(event) elif kind == Events.StopEvent: self.process_stop(event) elif kind == Events.GymEvent: self.process_gym(event) elif kind == Events.EggEvent: self.process_egg(event) elif kind == Events.RaidEvent: self.process_raid(event) elif kind == Events.WeatherEvent: self.process_weather(event) else: self._log.error( "!!! Manager does not support {} events!".format(kind)) self._log.debug("Finished event: %s", event.id) except Exception as e: self._log.error("Encountered error during processing: " "{}: {}".format(type(e).__name__, e)) self._log.error("Stack trace: \n {}" "".format(traceback.format_exc())) # Explict context yield gevent.sleep(0) # Save cache and exit self.__cache.clean_and_save() raise gevent.GreenletExit() # Set the location of the Manager def set_location(self, location): # Regex for Lat,Lng coordinate prog = re.compile("^(-?\d+\.\d+)[,\s]\s*(-?\d+\.\d+?)$") res = prog.match(location) if res: # If location is in a Lat,Lng coordinate self.__location = [float(res.group(1)), float(res.group(2))] else: # Check if key was provided if self._gmaps_service is None: raise ValueError("Unable to find location coordinates by name" " - no Google API key was provided.") # Attempt to geocode location location = self._gmaps_service.geocode(location) if location is None: raise ValueError("Unable to geocode coordinates from {}. " "Location will not be set.".format(location)) self.__location = location self._log.info("Location successfully set to '{},{}'.".format( location[0], location[1])) def _check_filters(self, event, filter_set, filter_names): """ Function for checking if an event passes any filters. """ for name in filter_names: f = filter_set.get(name) # Filter should always exist, but sanity check anyway if f: # If the Event passes, return True if f.check_event(event) and self.check_geofences(f, event): event.custom_dts = f.custom_dts return True else: self._log.critical("ERROR: No filter named %s found!", name) return False def _notify_alarms(self, event, alarm_names, func_name): """ Function for triggering notifications to alarms. """ # Generate the DTS for the event dts = event.generate_dts(self.__locale, self.__timezone, self.__units) # Get GMaps Triggers if self._gmaps_reverse_geocode: dts.update( self._gmaps_service.reverse_geocode((event.lat, event.lng), self._language)) for mode in self._gmaps_distance_matrix: dts.update( self._gmaps_service.distance_matrix(mode, (event.lat, event.lng), self.__location, self._language, self.__units)) # Spawn notifications in threads so they can work asynchronously threads = [] for name in alarm_names: alarm = self._alarms.get(name) if not alarm: self._log.critical("ERROR: No alarm named %s found!", name) continue func = getattr(alarm, func_name) threads.append(gevent.spawn(func, dts)) for thread in threads: # Wait for all alarms to finish thread.join() # Process new Monster data and decide if a notification needs to be sent def process_monster(self, mon): # type: (Events.MonEvent) -> None """ Process a monster event and notify alarms if it passes. """ # Make sure that monsters are enabled if self._mons_enabled is False: self._log.debug("Monster ignored: monster notifications " "are disabled.") return # Set the name for this event so we can log rejects better mon.name = self.__locale.get_pokemon_name(mon.monster_id) # Check if previously processed and update expiration #if self.__cache.monster_expiration(mon.enc_id) is not None: # self._log.debug("{} monster was skipped because it was " # "previously processed.".format(mon.name)) # return # self.__cache.monster_expiration(mon.enc_id, mon.disappear_time) # Check the time remaining seconds_left = (mon.disappear_time - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: self._log.debug("{} monster was skipped because only {} seconds " "remained".format(mon.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: mon.distance = get_earth_dist([mon.lat, mon.lng], self.__location, self.__units) mon.direction = get_cardinal_dir([mon.lat, mon.lng], self.__location) # Check for Rules rules = self.__mon_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self._mon_filters.keys(), self._alarms.keys()) } rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters(mon, self._mon_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms(mon, rule.alarm_names, 'pokemon_alert') if rule_ct > 0: self._rule_log.info( 'Monster %s passed %s rule(s) and triggered %s alarm(s).', mon.name, rule_ct, alarm_ct) else: self._rule_log.info('Monster %s rejected by all rules.', mon.name) def process_stop(self, stop): # type: (Events.StopEvent) -> None """ Process a stop event and notify alarms if it passes. """ # Make sure that stops are enabled if self._stops_enabled is False: self._log.debug("Stop ignored: stop notifications are disabled.") return # Check for lured if stop.expiration is None: self._log.debug("Stop ignored: stop was not lured") return # Check if previously processed and update expiration if self.__cache.stop_expiration(stop.stop_id) is not None: self._log.debug("Stop {} was skipped because it was " "previously processed.".format(stop.name)) return self.__cache.stop_expiration(stop.stop_id, stop.expiration) # Check the time remaining seconds_left = (stop.expiration - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: self._log.debug("Stop {} was skipped because only {} seconds " "remained".format(stop.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: stop.distance = get_earth_dist([stop.lat, stop.lng], self.__location, self.__units) stop.direction = get_cardinal_dir([stop.lat, stop.lng], self.__location) # Check for Rules rules = self.__stop_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self._stop_filters.keys(), self._alarms.keys()) } rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters(stop, self._stop_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms(stop, rule.alarm_names, 'pokestop_alert') if rule_ct > 0: self._rule_log.info( 'Stop %s passed %s rule(s) and triggered %s alarm(s).', stop.name, rule_ct, alarm_ct) else: self._rule_log.info('Stop %s rejected by all rules.', stop.name) def process_gym(self, gym): # type: (Events.GymEvent) -> None """ Process a gym event and notify alarms if it passes. """ # Update Gym details (if they exist) gym.gym_name = self.__cache.gym_name(gym.gym_id, gym.gym_name) gym.gym_description = self.__cache.gym_desc(gym.gym_id, gym.gym_description) gym.gym_image = self.__cache.gym_image(gym.gym_id, gym.gym_image) # Ignore changes to neutral if self._ignore_neutral and gym.new_team_id == 0: self._log.debug("%s gym update skipped: new team was neutral") return # Update Team Information gym.old_team_id = self.__cache.gym_team(gym.gym_id) self.__cache.gym_team(gym.gym_id, gym.new_team_id) # Check if notifications are on if self._gyms_enabled is False: self._log.debug("Gym ignored: gym notifications are disabled.") return # Doesn't look like anything to me if gym.new_team_id == gym.old_team_id: self._log.debug("%s gym update skipped: no change detected", gym.gym_id) return # Calculate distance and direction if self.__location is not None: gym.distance = get_earth_dist([gym.lat, gym.lng], self.__location, self.__units) gym.direction = get_cardinal_dir([gym.lat, gym.lng], self.__location) # Check for Rules rules = self.__gym_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self._gym_filters.keys(), self._alarms.keys()) } rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters(gym, self._gym_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms(gym, rule.alarm_names, 'gym_alert') if rule_ct > 0: self._rule_log.info( 'Gym %s passed %s rule(s) and triggered %s alarm(s).', gym.name, rule_ct, alarm_ct) else: self._rule_log.info('Gym %s rejected by all rules.', gym.name) def process_egg(self, egg): # type: (Events.EggEvent) -> None """ Process a egg event and notify alarms if it passes. """ # Update Gym details (if they exist) egg.gym_name = self.__cache.gym_name(egg.gym_id, egg.gym_name) egg.gym_description = self.__cache.gym_desc(egg.gym_id, egg.gym_description) egg.gym_image = self.__cache.gym_image(egg.gym_id, egg.gym_image) # Update Team if Unknown if Unknown.is_(egg.current_team_id): egg.current_team_id = self.__cache.gym_team(egg.gym_id) # Make sure that eggs are enabled if self._eggs_enabled is False: self._log.debug("Egg ignored: egg notifications are disabled.") return # Skip if previously processed if self.__cache.egg_expiration(egg.gym_id) is not None: self._log.debug("Egg {} was skipped because it was " "previously processed.".format(egg.name)) return self.__cache.egg_expiration(egg.gym_id, egg.hatch_time) # Check the time remaining seconds_left = (egg.hatch_time - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: self._log.debug("Egg {} was skipped because only {} seconds " "remained".format(egg.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: egg.distance = get_earth_dist([egg.lat, egg.lng], self.__location, self.__units) egg.direction = get_cardinal_dir([egg.lat, egg.lng], self.__location) # Check for Rules rules = self.__egg_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self._egg_filters.keys(), self._alarms.keys()) } rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters(egg, self._egg_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms(egg, rule.alarm_names, 'raid_egg_alert') if rule_ct > 0: self._rule_log.info( 'Egg %s passed %s rule(s) and triggered %s alarm(s).', egg.name, rule_ct, alarm_ct) else: self._rule_log.info('Egg %s rejected by all rules.', egg.name) def process_raid(self, raid): # type: (Events.RaidEvent) -> None """ Process a raid event and notify alarms if it passes. """ # Update Gym details (if they exist) raid.gym_name = self.__cache.gym_name(raid.gym_id, raid.gym_name) raid.gym_description = self.__cache.gym_desc(raid.gym_id, raid.gym_description) raid.gym_image = self.__cache.gym_image(raid.gym_id, raid.gym_image) # Update Team if Unknown if Unknown.is_(raid.current_team_id): raid.current_team_id = self.__cache.gym_team(raid.gym_id) # Make sure that raids are enabled if self._raids_enabled is False: self._log.debug("Raid ignored: raid notifications are disabled.") return # Skip if previously processed if self.__cache.raid_expiration(raid.gym_id) is not None: self._log.debug("Raid {} was skipped because it was " "previously processed.".format(raid.name)) return self.__cache.raid_expiration(raid.gym_id, raid.raid_end) # Check the time remaining seconds_left = (raid.raid_end - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: self._log.debug("Raid {} was skipped because only {} seconds " "remained".format(raid.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: raid.distance = get_earth_dist([raid.lat, raid.lng], self.__location, self.__units) raid.direction = get_cardinal_dir([raid.lat, raid.lng], self.__location) # Check for Rules rules = self.__raid_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self._raid_filters.keys(), self._alarms.keys()) } rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters(raid, self._raid_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms(raid, rule.alarm_names, 'raid_alert') if rule_ct > 0: self._rule_log.info( 'Raid %s passed %s rule(s) and triggered %s alarm(s).', raid.name, rule_ct, alarm_ct) else: self._rule_log.info('Raid %s rejected by all rules.', raid.name) def process_weather(self, weather): # type: (Events.WeatherEvent) -> None """ Process a weather event and notify alarms if it passes. """ # Set the name for this event so we can log rejects better weather.name = self.__locale.get_weather_name(weather.s2_cell_id) # Make sure that weather changes are enabled if self._weather_enabled is False: self._log.debug("Weather ignored: weather change " "notifications are disabled.") return # Calculate distance and direction if self.__location is not None: weather.distance = get_earth_dist([weather.lat, weather.lng], self.__location, self.__units) weather.direction = get_cardinal_dir([weather.lat, weather.lng], self.__location) # Store copy of cache info cache_weather_id = self.__cache.cell_weather_id(weather.s2_cell_id) cache_day_or_night_id = self.__cache.day_or_night_id( weather.s2_cell_id) cache_severity_id = self.__cache.severity_id(weather.s2_cell_id) # Update cache info self.__cache.cell_weather_id(weather.s2_cell_id, weather.weather_id) self.__cache.day_or_night_id(weather.s2_cell_id, weather.day_or_night_id) self.__cache.severity_id(weather.s2_cell_id, weather.severity_id) # Check and see if the weather hasn't changed and ignore if weather.weather_id == cache_weather_id and \ weather.day_or_night_id == cache_day_or_night_id and \ weather.severity_id == cache_severity_id: self._log.debug( "weather of %s, alert of %s, and day or night of %s skipped: " "no change detected", weather.weather_id, weather.severity_id, weather.day_or_night_id) return # Check for Rules rules = self.__weather_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self._weather_filters.keys(), self._alarms.keys()) } rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters(weather, self._weather_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms(weather, rule.alarm_names, 'weather_alert') if rule_ct > 0: self._rule_log.info( 'Weather %s passed %s rule(s) and triggered %s alarm(s).', weather.name, rule_ct, alarm_ct) else: self._rule_log.info('Weather %s rejected by all rules.', weather.name) # Check to see if a notification is within the given range # TODO: Move this into filters and add unit tests def check_geofences(self, f, e): """ Returns true if the event passes the filter's geofences. """ if self.geofences is None or f.geofences is None: # No geofences set return True targets = f.geofences if len(targets) == 1 and "all" in targets: targets = self.geofences.iterkeys() for name in targets: gf = self.geofences.get(name) if not gf: # gf doesn't exist self._log.error("Cannot check geofence %s: " "does not exist!", name) elif gf.contains(e.lat, e.lng): # e in gf self._log.debug("{} is in geofence {}!".format( e.name, gf.get_name())) e.geofence = name # Set the geofence for dts return True else: # e not in gf self._log.debug("%s not in %s.", e.name, name) self._log.debug("%s rejected from filter by geofences.", e.name) return False
def __init__(self, name, google_key, locale, units, timezone, time_limit, max_attempts, location, quiet, cache_type, filter_file, geofence_file, alarm_file, debug): # Set the name of the Manager self.__name = str(name).lower() log.info("----------- Manager '{}' ".format(self.__name) + " is being created.") self.__debug = debug # Get the Google Maps API self._google_key = google_key self._gmaps_service = GMaps(google_key) self._gmaps_reverse_geocode = False self._gmaps_distance_matrix = set() self._language = locale self.__locale = Locale(locale) # Setup the language-specific stuff self.__units = units # type of unit used for distances self.__timezone = timezone # timezone for time calculations self.__time_limit = time_limit # Minimum time remaining # Location should be [lat, lng] (or None for no location) self.__location = None if str(location).lower() != 'none': self.set_location(location) else: log.warning("NO LOCATION SET - " + " this may cause issues with distance related DTS.") # Quiet mode self.__quiet = quiet # Create cache self.__cache = cache_factory(cache_type, self.__name) # Load and Setup the Pokemon Filters self.__mons_enabled, self.__mon_filters = False, OrderedDict() self.__stops_enabled, self.__stop_filters = False, OrderedDict() self.__gyms_enabled, self.__gym_filters = False, OrderedDict() self.__ignore_neutral = False self.__eggs_enabled, self.__egg_filters = False, OrderedDict() self.__raids_enabled, self.__raid_filters = False, OrderedDict() self.__weather_enabled, self.__weather_filters = False, OrderedDict() self.__quest_enabled, self.__quest_filters = False, OrderedDict() self.load_filter_file(get_path(filter_file)) # Create the Geofences to filter with from given file self.geofences = None if str(geofence_file).lower() != 'none': self.geofences = load_geofence_file(get_path(geofence_file)) # Create the alarms to send notifications out with self.__alarms = {} self.load_alarms_file(get_path(alarm_file), int(max_attempts)) # Initialize Rules self.__mon_rules = {} self.__stop_rules = {} self.__gym_rules = {} self.__egg_rules = {} self.__raid_rules = {} self.__weather_rules = {} self.__quest_rules = {} # Initialize the queue and start the process self.__queue = Queue() self.__event = Event() self.__process = None log.info("----------- Manager '{}' ".format(self.__name) + " successfully created.")
class Manager(object): def __init__(self, name, google_key, locale, units, timezone, time_limit, max_attempts, location, quiet, cache_type, filter_file, geofence_file, alarm_file, debug): # Set the name of the Manager self.__name = str(name).lower() log.info("----------- Manager '{}' ".format(self.__name) + " is being created.") self.__debug = debug # Get the Google Maps API self._google_key = google_key self._gmaps_service = GMaps(google_key) self._gmaps_reverse_geocode = False self._gmaps_distance_matrix = set() self._language = locale self.__locale = Locale(locale) # Setup the language-specific stuff self.__units = units # type of unit used for distances self.__timezone = timezone # timezone for time calculations self.__time_limit = time_limit # Minimum time remaining # Location should be [lat, lng] (or None for no location) self.__location = None if str(location).lower() != 'none': self.set_location(location) else: log.warning("NO LOCATION SET - " + " this may cause issues with distance related DTS.") # Quiet mode self.__quiet = quiet # Create cache self.__cache = cache_factory(cache_type, self.__name) # Load and Setup the Pokemon Filters self.__mons_enabled, self.__mon_filters = False, OrderedDict() self.__stops_enabled, self.__stop_filters = False, OrderedDict() self.__gyms_enabled, self.__gym_filters = False, OrderedDict() self.__ignore_neutral = False self.__eggs_enabled, self.__egg_filters = False, OrderedDict() self.__raids_enabled, self.__raid_filters = False, OrderedDict() self.__weather_enabled, self.__weather_filters = False, OrderedDict() self.__quest_enabled, self.__quest_filters = False, OrderedDict() self.load_filter_file(get_path(filter_file)) # Create the Geofences to filter with from given file self.geofences = None if str(geofence_file).lower() != 'none': self.geofences = load_geofence_file(get_path(geofence_file)) # Create the alarms to send notifications out with self.__alarms = {} self.load_alarms_file(get_path(alarm_file), int(max_attempts)) # Initialize Rules self.__mon_rules = {} self.__stop_rules = {} self.__gym_rules = {} self.__egg_rules = {} self.__raid_rules = {} self.__weather_rules = {} self.__quest_rules = {} # Initialize the queue and start the process self.__queue = Queue() self.__event = Event() self.__process = None log.info("----------- Manager '{}' ".format(self.__name) + " successfully created.") # ~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROCESS CONTROL ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Update the object into the queue def update(self, obj): self.__queue.put(obj) # Get the name of this Manager def get_name(self): return self.__name # Tell the process to finish up and go home def stop(self): log.info("Manager {} shutting down... ".format(self.__name) + "{} items in queue.".format(self.__queue.qsize())) self.__event.set() def join(self): self.__process.join(timeout=20) if not self.__process.ready(): log.warning("Manager {} could not be stopped in time!" " Forcing process to stop.".format(self.__name)) self.__process.kill(timeout=2, block=True) # Force stop else: log.info("Manager {} successfully stopped!".format(self.__name)) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GMAPS API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def enable_gmaps_reverse_geocoding(self): """Enable GMaps Reverse Geocoding DTS for triggered Events. """ if not self._gmaps_service: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No GMaps API key has been set.") self._gmaps_reverse_geocode = True def disable_gmaps_reverse_geocoding(self): """Disable GMaps Reverse Geocoding DTS for triggered Events. """ self._gmaps_reverse_geocode = False def enable_gmaps_distance_matrix(self, mode): """Enable 'mode' Distance Matrix DTS for triggered Events. """ if not self.__location: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No Manager location has been set.") elif not self._gmaps_service: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No GMaps API key has been provided.") elif mode not in GMaps.TRAVEL_MODES: raise ValueError("Unable to enable distance matrix mode: " "{} is not a valid mode.".format(mode)) self._gmaps_distance_matrix.add(mode) def disable_gmaps_dm_walking(self, mode): """Disable 'mode' Distance Matrix DTS for triggered Events. """ if mode not in GMaps.TRAVEL_MODES: raise ValueError("Unable to disable distance matrix mode: " "Invalid mode specified.") self._gmaps_distance_matrix.discard(mode) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RULES API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Add new Monster Rule def add_monster_rule(self, name, filters, alarms): if name in self.__mon_rules: raise ValueError("Unable to add Rule: Monster Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self.__mon_filters: raise ValueError("Unable to create Rule: No Monster Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self.__alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__mon_rules[name] = Rule(filters, alarms) # Add new Stop Rule def add_stop_rule(self, name, filters, alarms): if name in self.__stop_rules: raise ValueError("Unable to add Rule: Stop Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self.__stop_filters: raise ValueError("Unable to create Rule: No Stop Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self.__alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__stop_rules[name] = Rule(filters, alarms) # Add new Gym Rule def add_gym_rule(self, name, filters, alarms): if name in self.__gym_rules: raise ValueError("Unable to add Rule: Gym Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self.__gym_filters: raise ValueError("Unable to create Rule: No Gym Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self.__alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__gym_rules[name] = Rule(filters, alarms) # Add new Egg Rule def add_egg_rule(self, name, filters, alarms): if name in self.__egg_rules: raise ValueError("Unable to add Rule: Egg Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self.__egg_filters: raise ValueError("Unable to create Rule: No Egg Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self.__alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__egg_rules[name] = Rule(filters, alarms) # Add new Raid Rule def add_raid_rule(self, name, filters, alarms): if name in self.__raid_rules: raise ValueError("Unable to add Rule: Raid Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self.__raid_filters: raise ValueError("Unable to create Rule: No Raid Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self.__alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__raid_rules[name] = Rule(filters, alarms) # Add new Weather Rule def add_weather_rule(self, name, filters, alarms): if name in self.__weather_rules: raise ValueError("Unable to add Rule: Weather Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self.__weather_filters: raise ValueError("Unable to create Rule: No weather Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self.__alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__weather_rules[name] = Rule(filters, alarms) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Add new Quest Rule def add_quest_rule(self, name, filters, alarms): if name in self.__quest_rules: raise ValueError("Unable to add Rule: Quest Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self.__quest_filters: raise ValueError("Unable to create Rule: No quest Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self.__alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__quest_rules[name] = Rule(filters, alarms) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MANAGER LOADING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @staticmethod def load_filter_section(section, sect_name, filter_type): defaults = section.pop('defaults', {}) default_dts = defaults.pop('custom_dts', {}) filter_set = OrderedDict() for name, settings in section.pop('filters', {}).iteritems(): settings = dict(defaults.items() + settings.items()) try: local_dts = dict(default_dts.items() + settings.pop('custom_dts', {}).items()) if len(local_dts) > 0: settings['custom_dts'] = local_dts filter_set[name] = filter_type(name, settings) log.debug("Filter '%s' set as the following: %s", name, filter_set[name].to_dict()) except Exception as e: log.error("Encountered error inside filter named '%s'.", name) raise e # Pass the error up for key in section: # Reject leftover parameters raise ValueError("'{}' is not a recognized parameter for the " "'{}' section.".format(key, sect_name)) return filter_set # Load in a new filters file def load_filter_file(self, file_path): try: log.info("Loading Filters from file at {}".format(file_path)) with open(file_path, 'r') as f: filters = json.load(f, object_pairs_hook=OrderedDict) if type(filters) is not OrderedDict: log.critical("Filters files must be a JSON object:" " { \"monsters\":{...},... }") raise ValueError("Filter file did not contain a dict.") except ValueError as e: log.error("Encountered error while loading Filters:" " {}: {}".format(type(e).__name__, e)) log.error( "PokeAlarm has encountered a 'ValueError' while loading the " "Filters file. This typically means the file isn't in the " "correct json format. Try loading the file contents into a " "json validator.") log.debug("Stack trace: \n {}".format(traceback.format_exc())) sys.exit(1) except IOError as e: log.error("Encountered error while loading Filters: " "{}: {}".format(type(e).__name__, e)) log.error("PokeAlarm was unable to find a filters file " "at {}. Please check that this file exists " "and that PA has read permissions.".format(file_path)) log.debug("Stack trace: \n {}".format(traceback.format_exc())) sys.exit(1) try: # Load Monsters Section log.info("Parsing 'monsters' section.") section = filters.pop('monsters', {}) self.__mons_enabled = bool(section.pop('enabled', False)) self.__mon_filters = self.load_filter_section( section, 'monsters', Filters.MonFilter) # Load Stops Section log.info("Parsing 'stops' section.") section = filters.pop('stops', {}) self.__stops_enabled = bool(section.pop('enabled', False)) self.__stop_filters = self.load_filter_section( section, 'stops', Filters.StopFilter) # Load Gyms Section log.info("Parsing 'gyms' section.") section = filters.pop('gyms', {}) self.__gyms_enabled = bool(section.pop('enabled', False)) self.__ignore_neutral = bool(section.pop('ignore_neutral', False)) self.__gym_filters = self.load_filter_section( section, 'gyms', Filters.GymFilter) # Load Eggs Section log.info("Parsing 'eggs' section.") section = filters.pop('eggs', {}) self.__eggs_enabled = bool(section.pop('enabled', False)) self.__egg_filters = self.load_filter_section( section, 'eggs', Filters.EggFilter) # Load Raids Section log.info("Parsing 'raids' section.") section = filters.pop('raids', {}) self.__raids_enabled = bool(section.pop('enabled', False)) self.__raid_filters = self.load_filter_section( section, 'raids', Filters.RaidFilter) # Load Weather Section log.info("Parsing 'weather' section.") section = filters.pop('weather', {}) self.__weather_enabled = bool(section.pop('enabled', True)) self.__weather_filters = self.load_filter_section( section, 'weather', Filters.WeatherFilter) # Load Quest Section log.info("Parsing 'quest' section.") section = filters.pop('quest', {}) self.__quest_enabled = bool(section.pop('enabled', True)) self.__quest_filters = self.load_filter_section( section, 'quest', Filters.QuestFilter) return # exit function except Exception as e: log.error("Encountered error while parsing Filters. " "This is because of a mistake in your Filters file.") log.error("{}: {}".format(type(e).__name__, e)) log.debug("Stack trace: \n {}".format(traceback.format_exc())) sys.exit(1) def load_alarms_file(self, file_path, max_attempts): log.info("Loading Alarms from the file at {}".format(file_path)) try: with open(file_path, 'r') as f: alarm_settings = json.load(f) if type(alarm_settings) is not dict: log.critical( "Alarms file must be an object of Alarms objects " + "- { 'alarm1': {...}, ... 'alarm5': {...} }") sys.exit(1) self.__alarms = {} for name, alarm in alarm_settings.iteritems(): if parse_boolean( require_and_remove_key( 'active', alarm, "Alarm objects in file.")) is True: self.__alarms[name] = Alarms.alarm_factory( alarm, max_attempts, self._google_key) else: log.debug("Alarm not activated: {}".format(alarm['type']) + " because value not set to \"True\"") log.info("{} active alarms found.".format(len(self.__alarms))) return # all done except ValueError as e: log.error("Encountered error while loading Alarms file: " + "{}: {}".format(type(e).__name__, e)) log.error( "PokeAlarm has encountered a 'ValueError' while loading the " + " Alarms file. This typically means your file isn't in the " + "correct json format. Try loading your file contents into" + " a json validator.") except IOError as e: log.error("Encountered error while loading Alarms: " + "{}: {}".format(type(e).__name__, e)) log.error("PokeAlarm was unable to find a filters file " + "at {}. Please check that this file".format(file_path) + " exists and PA has read permissions.") except Exception as e: log.error("Encountered error while loading Alarms: " + "{}: {}".format(type(e).__name__, e)) log.debug("Stack trace: \n {}".format(traceback.format_exc())) sys.exit(1) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HANDLE EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Start it up def start(self): self.__process = gevent.spawn(self.run) def setup_in_process(self): # Update config config['DEBUG'] = self.__debug config['ROOT_PATH'] = os.path.abspath("{}/..".format( os.path.dirname(__file__))) # Hush some new loggers logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) if config['DEBUG'] is True: logging.getLogger().setLevel(logging.DEBUG) # Conect the alarms and send the start up message for alarm in self.__alarms.values(): alarm.connect() alarm.startup_message() # Main event handler loop def run(self): self.setup_in_process() last_clean = datetime.utcnow() while True: # Run forever and ever # Clean out visited every 5 minutes if datetime.utcnow() - last_clean > timedelta(minutes=5): log.debug("Cleaning cache...") self.__cache.clean_and_save() last_clean = datetime.utcnow() try: # Get next object to process event = self.__queue.get(block=True, timeout=5) except gevent.queue.Empty: # Check if the process should exit process if self.__event.is_set(): break # Explict context yield gevent.sleep(0) continue try: kind = type(event) log.debug("Processing event: %s", event.id) if kind == Events.MonEvent: self.process_monster(event) elif kind == Events.StopEvent: self.process_stop(event) elif kind == Events.GymEvent: self.process_gym(event) elif kind == Events.EggEvent: self.process_egg(event) elif kind == Events.RaidEvent: self.process_raid(event) elif kind == Events.WeatherEvent: self.process_weather(event) elif kind == Events.QuestEvent: self.process_quest(event) else: log.error("!!! Manager does not support " + "{} events!".format(kind)) log.debug("Finished event: %s", event.id) except Exception as e: log.error("Encountered error during processing: " + "{}: {}".format(type(e).__name__, e)) log.debug("Stack trace: \n {}".format(traceback.format_exc())) # Explict context yield gevent.sleep(0) # Save cache and exit self.__cache.clean_and_save() raise gevent.GreenletExit() # Set the location of the Manager def set_location(self, location): # Regex for Lat,Lng coordinate prog = re.compile("^(-?\d+\.\d+)[,\s]\s*(-?\d+\.\d+?)$") res = prog.match(location) if res: # If location is in a Lat,Lng coordinate self.__location = [float(res.group(1)), float(res.group(2))] else: # Check if key was provided if self._gmaps_service is None: raise ValueError("Unable to find location coordinates by name" " - no Google API key was provided.") # Attempt to geocode location location = self._gmaps_service.geocode(location) if location is None: raise ValueError("Unable to geocode coordinates from {}. " "Location will not be set.".format(location)) self.__location = location log.info("Location successfully set to '{},{}'.".format( location[0], location[1])) # Process new Monster data and decide if a notification needs to be sent def process_monster(self, mon): # type: (Events.MonEvent) -> None """ Process a monster event and notify alarms if it passes. """ # Make sure that monsters are enabled if self.__mons_enabled is False: log.debug("Monster ignored: monster notifications are disabled.") return # Set the name for this event so we can log rejects better mon.name = self.__locale.get_pokemon_name(mon.monster_id) # Check if previously processed and update expiration if self.__cache.monster_expiration(mon.enc_id) is not None: log.debug("{} monster was skipped because it was previously " "processed.".format(mon.name)) return self.__cache.monster_expiration(mon.enc_id, mon.disappear_time) # Check the time remaining seconds_left = (mon.disappear_time - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: log.debug("{} monster was skipped because only {} seconds remained" "".format(mon.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: mon.distance = get_earth_dist([mon.lat, mon.lng], self.__location, self.__units) mon.direction = get_cardinal_dir([mon.lat, mon.lng], self.__location) # Check for Rules rules = self.__mon_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self.__mon_filters.keys(), self.__alarms.keys()) } for r_name, rule in rules.iteritems(): # For all rules for f_name in rule.filter_names: # Check Filters in Rules f = self.__mon_filters.get(f_name) passed = f.check_event(mon) and self.check_geofences(f, mon) if not passed: continue # go to next filter mon.custom_dts = f.custom_dts if self.__quiet is False: log.info("{} monster notification" " has been triggered in rule '{}'!" "".format(mon.name, r_name)) self._trigger_mon(mon, rule.alarm_names) break # Next rule def _trigger_mon(self, mon, alarms): # Generate the DTS for the event dts = mon.generate_dts(self.__locale, self.__timezone, self.__units) # Get GMaps Triggers if self._gmaps_reverse_geocode: dts.update( self._gmaps_service.reverse_geocode((mon.lat, mon.lng), self._language)) for mode in self._gmaps_distance_matrix: dts.update( self._gmaps_service.distance_matrix(mode, (mon.lat, mon.lng), self.__location, self._language, self.__units)) threads = [] # Spawn notifications in threads so they can work in background for name in alarms: alarm = self.__alarms.get(name) if alarm: threads.append(gevent.spawn(alarm.pokemon_alert, dts)) else: log.critical("Alarm '{}' not found!".format(name)) for thread in threads: # Wait for all alarms to finish thread.join() def process_stop(self, stop): # type: (Events.StopEvent) -> None """ Process a stop event and notify alarms if it passes. """ # Make sure that stops are enabled if self.__stops_enabled is False: log.debug("Stop ignored: stop notifications are disabled.") return # Check for lured if stop.expiration is None: log.debug("Stop ignored: stop was not lured") return # Check if previously processed and update expiration if self.__cache.stop_expiration(stop.stop_id) is not None: log.debug("Stop {} was skipped because it was previously " "processed.".format(stop.name)) return self.__cache.stop_expiration(stop.stop_id, stop.expiration) # Check the time remaining seconds_left = (stop.expiration - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: log.debug("Stop {} was skipped because only {} seconds remained" "".format(stop.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: stop.distance = get_earth_dist([stop.lat, stop.lng], self.__location, self.__units) stop.direction = get_cardinal_dir([stop.lat, stop.lng], self.__location) # Check for Rules rules = self.__stop_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self.__stop_filters.keys(), self.__alarms.keys()) } for r_name, rule in rules.iteritems(): # For all rules for f_name in rule.filter_names: # Check Filters in Rules f = self.__stop_filters.get(f_name) passed = f.check_event(stop) and self.check_geofences(f, stop) if not passed: continue # go to next filter stop.custom_dts = f.custom_dts if self.__quiet is False: log.info("{} stop notification" " has been triggered in rule '{}'!" "".format(stop.name, r_name)) self._trigger_stop(stop, rule.alarm_names) break # Next rule def _trigger_stop(self, stop, alarms): # Generate the DTS for the event dts = stop.generate_dts(self.__locale, self.__timezone, self.__units) # Get GMaps Triggers if self._gmaps_reverse_geocode: dts.update( self._gmaps_service.reverse_geocode((stop.lat, stop.lng), self._language)) for mode in self._gmaps_distance_matrix: dts.update( self._gmaps_service.distance_matrix(mode, (stop.lat, stop.lng), self.__location, self._language, self.__units)) threads = [] # Spawn notifications in threads so they can work in background for name in alarms: alarm = self.__alarms.get(name) if alarm: threads.append(gevent.spawn(alarm.pokestop_alert, dts)) else: log.critical("Alarm '{}' not found!".format(name)) for thread in threads: thread.join() def process_gym(self, gym): # type: (Events.GymEvent) -> None """ Process a gym event and notify alarms if it passes. """ # Update Gym details (if they exist) gym.gym_name = self.__cache.gym_name(gym.gym_id, gym.gym_name) gym.gym_description = self.__cache.gym_desc(gym.gym_id, gym.gym_description) gym.gym_image = self.__cache.gym_image(gym.gym_id, gym.gym_image) # Ignore changes to neutral if self.__ignore_neutral and gym.new_team_id == 0: log.debug("%s gym update skipped: new team was neutral") return # Update Team Information gym.old_team_id = self.__cache.gym_team(gym.gym_id) self.__cache.gym_team(gym.gym_id, gym.new_team_id) # Check if notifications are on if self.__gyms_enabled is False: log.debug("Gym ignored: gym notifications are disabled.") return # Doesn't look like anything to me if gym.new_team_id == gym.old_team_id: log.debug("%s gym update skipped: no change detected", gym.gym_id) return # Calculate distance and direction if self.__location is not None: gym.distance = get_earth_dist([gym.lat, gym.lng], self.__location, self.__units) gym.direction = get_cardinal_dir([gym.lat, gym.lng], self.__location) # Check for Rules rules = self.__gym_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self.__gym_filters.keys(), self.__alarms.keys()) } for r_name, rule in rules.iteritems(): # For all rules for f_name in rule.filter_names: # Check Filters in Rules f = self.__gym_filters.get(f_name) passed = f.check_event(gym) and self.check_geofences(f, gym) if not passed: continue # go to next filter gym.custom_dts = f.custom_dts if self.__quiet is False: log.info("{} gym notification" " has been triggered in rule '{}'!" "".format(gym.name, r_name)) self._trigger_gym(gym, rule.alarm_names) break # Next rule def _trigger_gym(self, gym, alarms): # Generate the DTS for the event dts = gym.generate_dts(self.__locale, self.__timezone, self.__units) # Get GMaps Triggers if self._gmaps_reverse_geocode: dts.update( self._gmaps_service.reverse_geocode((gym.lat, gym.lng), self._language)) for mode in self._gmaps_distance_matrix: dts.update( self._gmaps_service.distance_matrix(mode, (gym.lat, gym.lng), self.__location, self._language, self.__units)) threads = [] # Spawn notifications in threads so they can work in background for name in alarms: alarm = self.__alarms.get(name) if alarm: threads.append(gevent.spawn(alarm.gym_alert, dts)) else: log.critical("Alarm '{}' not found!".format(name)) for thread in threads: # Wait for all alarms to finish thread.join() def process_egg(self, egg): # type: (Events.EggEvent) -> None """ Process a egg event and notify alarms if it passes. """ # Update Gym details (if they exist) egg.gym_name = self.__cache.gym_name(egg.gym_id, egg.gym_name) egg.gym_description = self.__cache.gym_desc(egg.gym_id, egg.gym_description) egg.gym_image = self.__cache.gym_image(egg.gym_id, egg.gym_image) # Update Team if Unknown if Unknown.is_(egg.current_team_id): egg.current_team_id = self.__cache.gym_team(egg.gym_id) # Make sure that eggs are enabled if self.__eggs_enabled is False: log.debug("Egg ignored: egg notifications are disabled.") return # Skip if previously processed if self.__cache.egg_expiration(egg.gym_id) is not None: log.debug("Egg {} was skipped because it was previously " "processed.".format(egg.name)) return self.__cache.egg_expiration(egg.gym_id, egg.hatch_time) # Check the time remaining seconds_left = (egg.hatch_time - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: log.debug("Egg {} was skipped because only {} seconds remained" "".format(egg.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: egg.distance = get_earth_dist([egg.lat, egg.lng], self.__location, self.__units) egg.direction = get_cardinal_dir([egg.lat, egg.lng], self.__location) # Check for Rules rules = self.__egg_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self.__egg_filters.keys(), self.__alarms.keys()) } for r_name, rule in rules.iteritems(): # For all rules for f_name in rule.filter_names: # Check Filters in Rules f = self.__egg_filters.get(f_name) passed = f.check_event(egg) and self.check_geofences(f, egg) if not passed: continue # go to next filter egg.custom_dts = f.custom_dts if self.__quiet is False: log.info("{} egg notification" " has been triggered in rule '{}'!" "".format(egg.name, r_name)) self._trigger_egg(egg, rule.alarm_names) break # Next rule def _trigger_egg(self, egg, alarms): # Generate the DTS for the event dts = egg.generate_dts(self.__locale, self.__timezone, self.__units) # Get GMaps Triggers if self._gmaps_reverse_geocode: dts.update( self._gmaps_service.reverse_geocode((egg.lat, egg.lng), self._language)) for mode in self._gmaps_distance_matrix: dts.update( self._gmaps_service.distance_matrix(mode, (egg.lat, egg.lng), self.__location, self._language, self.__units)) threads = [] # Spawn notifications in threads so they can work in background for name in alarms: alarm = self.__alarms.get(name) if alarm: threads.append(gevent.spawn(alarm.raid_egg_alert, dts)) else: log.critical("Alarm '{}' not found!".format(name)) for thread in threads: # Wait for all alarms to finish thread.join() def process_raid(self, raid): # type: (Events.RaidEvent) -> None """ Process a raid event and notify alarms if it passes. """ # Update Gym details (if they exist) raid.gym_name = self.__cache.gym_name(raid.gym_id, raid.gym_name) raid.gym_description = self.__cache.gym_desc(raid.gym_id, raid.gym_description) raid.gym_image = self.__cache.gym_image(raid.gym_id, raid.gym_image) # Update Team if Unknown if Unknown.is_(raid.current_team_id): raid.current_team_id = self.__cache.gym_team(raid.gym_id) # Make sure that raids are enabled if self.__raids_enabled is False: log.debug("Raid ignored: raid notifications are disabled.") return # Skip if previously processed if self.__cache.raid_expiration(raid.gym_id) is not None: log.debug("Raid {} was skipped because it was previously " "processed.".format(raid.name)) return self.__cache.raid_expiration(raid.gym_id, raid.raid_end) # Check the time remaining seconds_left = (raid.raid_end - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: log.debug("Raid {} was skipped because only {} seconds remained" "".format(raid.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: raid.distance = get_earth_dist([raid.lat, raid.lng], self.__location, self.__units) raid.direction = get_cardinal_dir([raid.lat, raid.lng], self.__location) # Check for Rules rules = self.__raid_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self.__raid_filters.keys(), self.__alarms.keys()) } for r_name, rule in rules.iteritems(): # For all rules for f_name in rule.filter_names: # Check Filters in Rules f = self.__raid_filters.get(f_name) passed = f.check_event(raid) and self.check_geofences(f, raid) if not passed: continue # go to next filter raid.custom_dts = f.custom_dts if self.__quiet is False: log.info("{} raid notification" " has been triggered in rule '{}'!" "".format(raid.name, r_name)) self._trigger_raid(raid, rule.alarm_names) break # Next rule def _trigger_raid(self, raid, alarms): # Generate the DTS for the event dts = raid.generate_dts(self.__locale, self.__timezone, self.__units) # Get GMaps Triggers if self._gmaps_reverse_geocode: dts.update( self._gmaps_service.reverse_geocode((raid.lat, raid.lng), self._language)) for mode in self._gmaps_distance_matrix: dts.update( self._gmaps_service.distance_matrix(mode, (raid.lat, raid.lng), self.__location, self._language, self.__units)) threads = [] # Spawn notifications in threads so they can work in background for name in alarms: alarm = self.__alarms.get(name) if alarm: threads.append(gevent.spawn(alarm.raid_alert, dts)) else: log.critical("Alarm '{}' not found!".format(name)) for thread in threads: # Wait for all alarms to finish thread.join() def process_weather(self, weather): # type: (Events.WeatherEvent) -> None """ Process a weather event and notify alarms if it passes. """ # Make sure that weather is enabled if self.__weather_enabled is False: log.debug("Weather ignored: weather notifications are disabled.") return # Skip if previously processed if self.__cache.get_cell_weather( weather.weather_cell_id) == weather.condition: log.debug("Weather alert for cell {} was skipped " "because it was already {} weather.".format( weather.weather_cell_id, weather.condition)) return self.__cache.update_cell_weather(weather.weather_cell_id, weather.condition) # Check for Rules rules = self.__weather_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self.__weather_filters.keys(), self.__alarms.keys()) } for r_name, rule in rules.iteritems(): # For all rules for f_name in rule.filter_names: # Check Filters in Rules f = self.__weather_filters.get(f_name) passed = f.check_event(weather) and \ self.check_weather_geofences(f, weather) if not passed: continue # go to next filter weather.custom_dts = f.custom_dts if self.__quiet is False: log.info("{} weather notification" " has been triggered in rule '{}'!" "".format(weather.weather_cell_id, r_name)) self._trigger_weather(weather, rule.alarm_names) break # Next rule def _trigger_weather(self, weather, alarms): dts = weather.generate_dts(self.__locale, self.__timezone, self.__units) threads = [] # Spawn notifications in threads so they can work in background for name in alarms: alarm = self.__alarms.get(name) if alarm: threads.append(gevent.spawn(alarm.weather_alert, dts)) else: log.critical("Alarm '{}' not found!".format(name)) for thread in threads: # Wait for all alarms to finish thread.join() def process_quest(self, quest): # type: (Events.QuestEvent) -> None """ Process a quest event and notify alarms if it passes. """ # Make sure that stops are enabled if self.__quest_enabled is False: log.debug("Quest ignored: quest notifications are disabled.") return # Check if previously processed and update expiration if self.__cache.quest_reward(quest.stop_id) is not None: log.debug("Quest {} was skipped because it was previously " "processed.".format(quest.stop_name)) return self.__cache.quest_reward(quest.stop_id, quest.reward) # Calculate distance and direction if self.__location is not None: quest.distance = get_earth_dist([quest.lat, quest.lng], self.__location, self.__units) quest.direction = get_cardinal_dir([quest.lat, quest.lng], self.__location) # Check for Rules rules = self.__quest_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self.__quest_filters.keys(), self.__alarms.keys()) } for r_name, rule in rules.iteritems(): # For all rules for f_name in rule.filter_names: # Check Filters in Rules f = self.__quest_filters.get(f_name) passed = f.check_event(quest) and self.check_geofences( f, quest) if not passed: continue # go to next filter quest.custom_dts = f.custom_dts if self.__quiet is False: log.info("{} quest notification" " has been triggered in rule '{}'!" "".format(quest.stop_name, r_name)) self._trigger_quest(quest, rule.alarm_names) break # Next rule def _trigger_quest(self, quest, alarms): # Generate the DTS for the event dts = quest.generate_dts(self.__locale, self.__timezone, self.__units) # Get GMaps Triggers if self._gmaps_reverse_geocode: dts.update( self._gmaps_service.reverse_geocode((quest.lat, quest.lng), self._language)) for mode in self._gmaps_distance_matrix: dts.update( self._gmaps_service.distance_matrix(mode, (quest.lat, quest.lng), self.__location, self._language, self.__units)) threads = [] # Spawn notifications in threads so they can work in background for name in alarms: alarm = self.__alarms.get(name) if alarm: threads.append(gevent.spawn(alarm.quest_alert, dts)) else: log.critical("Alarm '{}' not found!".format(name)) for thread in threads: thread.join() # Check to see if a notification is within the given range def check_geofences(self, f, e): """ Returns true if the event passes the filter's geofences. """ if self.geofences is None or f.geofences is None: # No geofences set return True targets = f.geofences if len(targets) == 1 and "all" in targets: targets = self.geofences.iterkeys() for name in targets: gf = self.geofences.get(name) if not gf: # gf doesn't exist log.error("Cannot check geofence %s: does not exist!", name) elif gf.contains(e.lat, e.lng): # e in gf log.debug("{} is in geofence {}!".format( e.name, gf.get_name())) e.geofence = name # Set the geofence for dts return True else: # e not in gf log.debug("%s not in %s.", e.name, name) f.reject(e, "not in geofences") return False # Check to see if a weather notification s2 cell # overlaps with a given range (geofence) def check_weather_geofences(self, f, weather): """ Returns true if the event passes the filter's geofences. """ if self.geofences is None or f.geofences is None: # No geofences set return True targets = f.geofences if len(targets) == 1 and "all" in targets: targets = self.geofences.iterkeys() for name in targets: gf = self.geofences.get(name) if not gf: # gf doesn't exist log.error("Cannot check geofence %s: does not exist!", name) elif gf.check_overlap(weather): # weather cell overlaps gf log.debug("{} is in geofence {}!".format( weather.weather_cell_id, gf.get_name())) weather.geofence = name # Set the geofence for dts return True else: # weather not in gf log.debug("%s not in %s.", weather.weather_cell_id, name) f.reject(weather, "not in geofences") return False
def __init__(self, name, google_key, locale, units, timezone, time_limit, max_attempts, location, cache_type, geofence_file, debug): # Set the name of the Manager self.name = str(name).lower() self._log = self._create_logger(self.name) self._rule_log = self.get_child_logger('rules') self.__debug = debug # Get the Google Maps AP# TODO: Improve error checking self._google_key = None self._gmaps_service = None if str(google_key).lower() != 'none': self._google_key = google_key self._gmaps_service = GMaps(google_key) self._gmaps_reverse_geocode = False self._gmaps_distance_matrix = set() self._language = locale self.__locale = Locale(locale) # Setup the language-specific stuff self.__units = units # type of unit used for distances self.__timezone = timezone # timezone for time calculations self.__time_limit = time_limit # Minimum time remaining # Location should be [lat, lng] (or None for no location) self.__location = None if str(location).lower() != 'none': self.set_location(location) else: self._log.warning( "NO LOCATION SET - this may cause issues " "with distance related DTS.") # Create cache self.__cache = cache_factory(self, cache_type) # Load and Setup the Pokemon Filters self._mons_enabled, self._mon_filters = False, OrderedDict() self._stops_enabled, self._stop_filters = False, OrderedDict() self._gyms_enabled, self._gym_filters = False, OrderedDict() self._ignore_neutral = False self._eggs_enabled, self._egg_filters = False, OrderedDict() self._raids_enabled, self._raid_filters = False, OrderedDict() self._weather_enabled, self._weather_filters = False, OrderedDict() # Create the Geofences to filter with from given file self.geofences = None if str(geofence_file).lower() != 'none': self.geofences = load_geofence_file(get_path(geofence_file)) # Create the alarms to send notifications out with self._alarms = {} self._max_attempts = int(max_attempts) # TODO: Move to alarm level # Initialize Rules self.__mon_rules = {} self.__stop_rules = {} self.__gym_rules = {} self.__egg_rules = {} self.__raid_rules = {} self.__weather_rules = {} # Initialize the queue and start the process self.__queue = Queue() self.__event = Event() self.__process = None
class Manager(object): def __init__(self, name, google_key, locale, units, timezone, time_limit, max_attempts, location, cache_type, geofence_file, debug): # Set the name of the Manager self.name = str(name).lower() self._log = self._create_logger(self.name) self._rule_log = self.get_child_logger('rules') self.__debug = debug # Get the Google Maps AP# TODO: Improve error checking self._google_key = None self._gmaps_service = None if str(google_key).lower() != 'none': self._google_key = google_key self._gmaps_service = GMaps(google_key) self._gmaps_reverse_geocode = False self._gmaps_distance_matrix = set() self._language = locale self.__locale = Locale(locale) # Setup the language-specific stuff self.__units = units # type of unit used for distances self.__timezone = timezone # timezone for time calculations self.__time_limit = time_limit # Minimum time remaining # Location should be [lat, lng] (or None for no location) self.__location = None if str(location).lower() != 'none': self.set_location(location) else: self._log.warning( "NO LOCATION SET - this may cause issues " "with distance related DTS.") # Create cache self.__cache = cache_factory(self, cache_type) # Load and Setup the Pokemon Filters self._mons_enabled, self._mon_filters = False, OrderedDict() self._stops_enabled, self._stop_filters = False, OrderedDict() self._gyms_enabled, self._gym_filters = False, OrderedDict() self._ignore_neutral = False self._eggs_enabled, self._egg_filters = False, OrderedDict() self._raids_enabled, self._raid_filters = False, OrderedDict() self._weather_enabled, self._weather_filters = False, OrderedDict() # Create the Geofences to filter with from given file self.geofences = None if str(geofence_file).lower() != 'none': self.geofences = load_geofence_file(get_path(geofence_file)) # Create the alarms to send notifications out with self._alarms = {} self._max_attempts = int(max_attempts) # TODO: Move to alarm level # Initialize Rules self.__mon_rules = {} self.__stop_rules = {} self.__gym_rules = {} self.__egg_rules = {} self.__raid_rules = {} self.__weather_rules = {} # Initialize the queue and start the process self.__queue = Queue() self.__event = Event() self.__process = None # ~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROCESS CONTROL ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Update the object into the queue def update(self, obj): self.__queue.put(obj) # Get the name of this Manager def get_name(self): return self.name # Tell the process to finish up and go home def stop(self): self._log.info( "Manager {} shutting down... {} items in queue." "".format(self.name, self.__queue.qsize())) self.__event.set() def join(self): self.__process.join(timeout=20) if not self.__process.ready(): self._log.warning("Manager {} could not be stopped in time! " "Forcing process to stop.".format(self.name)) self.__process.kill(timeout=2, block=True) # Force stop else: self._log.info( "Manager {} successfully stopped!".format(self.name)) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GMAPS API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def enable_gmaps_reverse_geocoding(self): """Enable GMaps Reverse Geocoding DTS for triggered Events. """ if not self._gmaps_service: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No GMaps API key has been set.") self._gmaps_reverse_geocode = True def disable_gmaps_reverse_geocoding(self): """Disable GMaps Reverse Geocoding DTS for triggered Events. """ self._gmaps_reverse_geocode = False def enable_gmaps_distance_matrix(self, mode): """Enable 'mode' Distance Matrix DTS for triggered Events. """ if not self.__location: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No Manager location has been set.") elif not self._gmaps_service: raise ValueError("Unable to enable Google Maps Reverse Geocoding." "No GMaps API key has been provided.") elif mode not in GMaps.TRAVEL_MODES: raise ValueError("Unable to enable distance matrix mode: " "{} is not a valid mode.".format(mode)) self._gmaps_distance_matrix.add(mode) def disable_gmaps_dm_walking(self, mode): """Disable 'mode' Distance Matrix DTS for triggered Events. """ if mode not in GMaps.TRAVEL_MODES: raise ValueError("Unable to disable distance matrix mode: " "Invalid mode specified.") self._gmaps_distance_matrix.discard(mode) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGGING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @staticmethod def _create_logger(mgr_name): """ Internal method for initializing manager loggers. """ # Create a Filter to pass on manager name log = logging.getLogger('pokealarm.{}'.format(mgr_name)) return log def get_child_logger(self, name): """ Get a child logger of this manager. """ logger = self._log.getChild(name) logger.addFilter(ContextFilter()) return logger def set_log_level(self, log_level): if log_level == 1: self._log.setLevel(logging.WARNING) elif log_level == 2: self._log.setLevel(logging.INFO) self._log.getChild("cache").setLevel(logging.WARNING) self._log.getChild("filters").setLevel(logging.WARNING) self._log.getChild("alarms").setLevel(logging.WARNING) elif log_level == 3: self._log.setLevel(logging.INFO) self._log.getChild("cache").setLevel(logging.INFO) self._log.getChild("filters").setLevel(logging.WARNING) self._log.getChild("alarms").setLevel(logging.WARNING) elif log_level == 4: self._log.setLevel(logging.INFO) self._log.getChild("cache").setLevel(logging.INFO) self._log.getChild("filters").setLevel(logging.INFO) self._log.getChild("alarms").setLevel(logging.INFO) elif log_level == 5: self._log.setLevel(logging.DEBUG) self._log.getChild("cache").setLevel(logging.DEBUG) self._log.getChild("filters").setLevel(logging.DEBUG) self._log.getChild("alarms").setLevel(logging.DEBUG) else: raise ValueError("Unable to set verbosity, must be an " "integer between 1 and 5.") self._log.debug("Verbosity set to %s", log_level) def add_file_logger(self, path, max_size_mb, ct): setup_file_handler(self._log, path, max_size_mb, ct) self._log.debug("Added new file logger to %s", path) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ FILTERS API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Enable/Disable Monster notifications def set_monsters_enabled(self, boolean): self._mons_enabled = parse_bool(boolean) self._log.debug("Monster notifications %s", "enabled" if self._mons_enabled else "disabled") # Add new Monster Filter def add_monster_filter(self, name, settings): if name in self._mon_filters: raise ValueError("Unable to add Monster Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.MonFilter(self, name, settings) self._mon_filters[name] = f self._log.debug("Monster filter '%s' set: %s", name, f) # Enable/Disable Stops notifications def set_stops_enabled(self, boolean): self._stops_enabled = parse_bool(boolean) self._log.debug("Stops notifications %s!", "enabled" if self._stops_enabled else "disabled") # Add new Stop Filter def add_stop_filter(self, name, settings): if name in self._stop_filters: raise ValueError("Unable to add Stop Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.StopFilter(self, name, settings) self._stop_filters[name] = f self._log.debug("Stop filter '%s' set: %s", name, f) # Enable/Disable Gym notifications def set_gyms_enabled(self, boolean): self._gyms_enabled = parse_bool(boolean) self._log.debug("Gyms notifications %s!", "enabled" if self._gyms_enabled else "disabled") # Enable/Disable Stops notifications def set_ignore_neutral(self, boolean): self._ignore_neutral = parse_bool(boolean) self._log.debug("Ignore neutral set to %s!", self._ignore_neutral) # Add new Gym Filter def add_gym_filter(self, name, settings): if name in self._gym_filters: raise ValueError("Unable to add Gym Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.GymFilter(self, name, settings) self._gym_filters[name] = f self._log.debug("Gym filter '%s' set: %s", name, f) # Enable/Disable Egg notifications def set_eggs_enabled(self, boolean): self._eggs_enabled = parse_bool(boolean) self._log.debug("Egg notifications %s!", "enabled" if self._eggs_enabled else "disabled") # Add new Egg Filter def add_egg_filter(self, name, settings): if name in self._egg_filters: raise ValueError("Unable to add Egg Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.EggFilter(self, name, settings) self._egg_filters[name] = f self._log.debug("Egg filter '%s' set: %s", name, f) # Enable/Disable Stops notifications def set_raids_enabled(self, boolean): self._raids_enabled = parse_bool(boolean) self._log.debug("Raid notifications %s!", "enabled" if self._raids_enabled else "disabled") # Add new Raid Filter def add_raid_filter(self, name, settings): if name in self._raid_filters: raise ValueError("Unable to add Raid Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.RaidFilter(self, name, settings) self._raid_filters[name] = f self._log.debug("Raid filter '%s' set: %s", name, f) # Enable/Disable Weather notifications def set_weather_enabled(self, boolean): self._weather_enabled = parse_bool(boolean) self._log.debug("Weather notifications %s!", "enabled" if self._weather_enabled else "disabled") # Add new Weather Filter def add_weather_filter(self, name, settings): if name in self._weather_filters: raise ValueError("Unable to add Weather Filter: Filter with the " "name {} already exists!".format(name)) f = Filters.WeatherFilter(self, name, settings) self._weather_filters[name] = f self._log.debug("Weather filter '%s' set: %s", name, f) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ALARMS API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def add_alarm(self, name, settings): if name in self._alarms: raise ValueError("Unable to add new Alarm: Alarm with the name " "{} already exists!".format(name)) alarm = Alarms.alarm_factory( self, settings, self._max_attempts, self._google_key) self._alarms[name] = alarm # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ RULES API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Add new Monster Rule def add_monster_rule(self, name, filters, alarms): if name in self.__mon_rules: raise ValueError("Unable to add Rule: Monster Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._mon_filters: raise ValueError("Unable to create Rule: No Monster Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__mon_rules[name] = Rule(filters, alarms) # Add new Stop Rule def add_stop_rule(self, name, filters, alarms): if name in self.__stop_rules: raise ValueError("Unable to add Rule: Stop Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._stop_filters: raise ValueError("Unable to create Rule: No Stop Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__stop_rules[name] = Rule(filters, alarms) # Add new Gym Rule def add_gym_rule(self, name, filters, alarms): if name in self.__gym_rules: raise ValueError("Unable to add Rule: Gym Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._gym_filters: raise ValueError("Unable to create Rule: No Gym Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__gym_rules[name] = Rule(filters, alarms) # Add new Egg Rule def add_egg_rule(self, name, filters, alarms): if name in self.__egg_rules: raise ValueError("Unable to add Rule: Egg Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._egg_filters: raise ValueError("Unable to create Rule: No Egg Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__egg_rules[name] = Rule(filters, alarms) # Add new Raid Rule def add_raid_rule(self, name, filters, alarms): if name in self.__raid_rules: raise ValueError("Unable to add Rule: Raid Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._raid_filters: raise ValueError("Unable to create Rule: No Raid Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__raid_rules[name] = Rule(filters, alarms) # Add new Weather Rule def add_weather_rule(self, name, filters, alarms): if name in self.__weather_rules: raise ValueError("Unable to add Rule: Weather Rule with the name " "{} already exists!".format(name)) for filt in filters: if filt not in self._weather_filters: raise ValueError("Unable to create Rule: No Weather Filter " "named {}!".format(filt)) for alarm in alarms: if alarm not in self._alarms: raise ValueError("Unable to create Rule: No Alarm " "named {}!".format(alarm)) self.__weather_rules[name] = Rule(filters, alarms) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MANAGER LOADING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HANDLE EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Start it up def start(self): self.__process = gevent.spawn(self.run) def setup_in_process(self): # Update config config['DEBUG'] = self.__debug config['ROOT_PATH'] = os.path.abspath( "{}/..".format(os.path.dirname(__file__))) # Hush some new loggers logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) if config['DEBUG'] is True: logging.getLogger().setLevel(logging.DEBUG) # Conect the alarms and send the start up message for alarm in self._alarms.values(): alarm.connect() alarm.startup_message() # Main event handler loop def run(self): self.setup_in_process() last_clean = datetime.utcnow() while True: # Run forever and ever # Clean out visited every 5 minutes if datetime.utcnow() - last_clean > timedelta(minutes=5): self._log.debug("Cleaning cache...") self.__cache.clean_and_save() last_clean = datetime.utcnow() try: # Get next object to process event = self.__queue.get(block=True, timeout=5) except gevent.queue.Empty: # Check if the process should exit process if self.__event.is_set(): break # Explict context yield gevent.sleep(0) continue try: kind = type(event) self._log.debug("Processing event: %s", event.id) if kind == Events.MonEvent: self.process_monster(event) elif kind == Events.StopEvent: self.process_stop(event) elif kind == Events.GymEvent: self.process_gym(event) elif kind == Events.EggEvent: self.process_egg(event) elif kind == Events.RaidEvent: self.process_raid(event) elif kind == Events.WeatherEvent: self.process_weather(event) else: self._log.error( "!!! Manager does not support {} events!".format(kind)) self._log.debug("Finished event: %s", event.id) except Exception as e: self._log.error("Encountered error during processing: " "{}: {}".format(type(e).__name__, e)) self._log.error("Stack trace: \n {}" "".format(traceback.format_exc())) # Explict context yield gevent.sleep(0) # Save cache and exit self.__cache.clean_and_save() raise gevent.GreenletExit() # Set the location of the Manager def set_location(self, location): # Regex for Lat,Lng coordinate prog = re.compile("^(-?\d+\.\d+)[,\s]\s*(-?\d+\.\d+?)$") res = prog.match(location) if res: # If location is in a Lat,Lng coordinate self.__location = [float(res.group(1)), float(res.group(2))] else: # Check if key was provided if self._gmaps_service is None: raise ValueError("Unable to find location coordinates by name" " - no Google API key was provided.") # Attempt to geocode location location = self._gmaps_service.geocode(location) if location is None: raise ValueError("Unable to geocode coordinates from {}. " "Location will not be set.".format(location)) self.__location = location self._log.info("Location successfully set to '{},{}'.".format( location[0], location[1])) def _check_filters(self, event, filter_set, filter_names): """ Function for checking if an event passes any filters. """ for name in filter_names: f = filter_set.get(name) # Filter should always exist, but sanity check anyway if f: # If the Event passes, return True if f.check_event(event) and self.check_geofences(f, event): event.custom_dts = f.custom_dts return True else: self._log.critical("ERROR: No filter named %s found!", name) return False def _notify_alarms(self, event, alarm_names, func_name): """ Function for triggering notifications to alarms. """ # Generate the DTS for the event dts = event.generate_dts(self.__locale, self.__timezone, self.__units) # Get GMaps Triggers if self._gmaps_reverse_geocode: dts.update(self._gmaps_service.reverse_geocode( (event.lat, event.lng), self._language)) for mode in self._gmaps_distance_matrix: dts.update(self._gmaps_service.distance_matrix( mode, (event.lat, event.lng), self.__location, self._language, self.__units)) # Spawn notifications in threads so they can work asynchronously threads = [] for name in alarm_names: alarm = self._alarms.get(name) if not alarm: self._log.critical("ERROR: No alarm named %s found!", name) continue func = getattr(alarm, func_name) threads.append(gevent.spawn(func, dts)) for thread in threads: # Wait for all alarms to finish thread.join() # Process new Monster data and decide if a notification needs to be sent def process_monster(self, mon): # type: (Events.MonEvent) -> None """ Process a monster event and notify alarms if it passes. """ # Make sure that monsters are enabled if self._mons_enabled is False: self._log.debug("Monster ignored: monster notifications " "are disabled.") return # Set the name for this event so we can log rejects better mon.name = self.__locale.get_pokemon_name(mon.monster_id) # Check if previously processed and update expiration if self.__cache.monster_expiration(mon.enc_id) is not None: self._log.debug("{} monster was skipped because it was " "previously processed.".format(mon.name)) return self.__cache.monster_expiration(mon.enc_id, mon.disappear_time) # Check the time remaining seconds_left = (mon.disappear_time - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: self._log.debug("{} monster was skipped because only {} seconds " "remained".format(mon.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: mon.distance = get_earth_dist( [mon.lat, mon.lng], self.__location, self.__units) mon.direction = get_cardinal_dir( [mon.lat, mon.lng], self.__location) # Check for Rules rules = self.__mon_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self._mon_filters.keys(), self._alarms.keys())} rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters( mon, self._mon_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms( mon, rule.alarm_names, 'pokemon_alert') if rule_ct > 0: self._rule_log.info( 'Monster %s passed %s rule(s) and triggered %s alarm(s).', mon.name, rule_ct, alarm_ct) else: self._rule_log.info('Monster %s rejected by all rules.', mon.name) def process_stop(self, stop): # type: (Events.StopEvent) -> None """ Process a stop event and notify alarms if it passes. """ # Make sure that stops are enabled if self._stops_enabled is False: self._log.debug("Stop ignored: stop notifications are disabled.") return # Check for lured if stop.expiration is None: self._log.debug("Stop ignored: stop was not lured") return # Check if previously processed and update expiration if self.__cache.stop_expiration(stop.stop_id) is not None: self._log.debug("Stop {} was skipped because it was " "previously processed.".format(stop.name)) return self.__cache.stop_expiration(stop.stop_id, stop.expiration) # Check the time remaining seconds_left = (stop.expiration - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: self._log.debug("Stop {} was skipped because only {} seconds " "remained".format(stop.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: stop.distance = get_earth_dist( [stop.lat, stop.lng], self.__location, self.__units) stop.direction = get_cardinal_dir( [stop.lat, stop.lng], self.__location) # Check for Rules rules = self.__stop_rules if len(rules) == 0: # If no rules, default to all rules = {"default": Rule( self._stop_filters.keys(), self._alarms.keys())} rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters( stop, self._stop_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms( stop, rule.alarm_names, 'pokestop_alert') if rule_ct > 0: self._rule_log.info( 'Stop %s passed %s rule(s) and triggered %s alarm(s).', stop.name, rule_ct, alarm_ct) else: self._rule_log.info('Stop %s rejected by all rules.', stop.name) def process_gym(self, gym): # type: (Events.GymEvent) -> None """ Process a gym event and notify alarms if it passes. """ # Update Gym details (if they exist) gym.gym_name = self.__cache.gym_name(gym.gym_id, gym.gym_name) gym.gym_description = self.__cache.gym_desc( gym.gym_id, gym.gym_description) gym.gym_image = self.__cache.gym_image(gym.gym_id, gym.gym_image) # Ignore changes to neutral if self._ignore_neutral and gym.new_team_id == 0: self._log.debug("%s gym update skipped: new team was neutral") return # Update Team Information gym.old_team_id = self.__cache.gym_team(gym.gym_id) self.__cache.gym_team(gym.gym_id, gym.new_team_id) # Check if notifications are on if self._gyms_enabled is False: self._log.debug("Gym ignored: gym notifications are disabled.") return # Doesn't look like anything to me if gym.new_team_id == gym.old_team_id: self._log.debug( "%s gym update skipped: no change detected", gym.gym_id) return # Calculate distance and direction if self.__location is not None: gym.distance = get_earth_dist( [gym.lat, gym.lng], self.__location, self.__units) gym.direction = get_cardinal_dir( [gym.lat, gym.lng], self.__location) # Check for Rules rules = self.__gym_rules if len(rules) == 0: # If no rules, default to all rules = {"default": Rule( self._gym_filters.keys(), self._alarms.keys())} rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters( gym, self._gym_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms( gym, rule.alarm_names, 'gym_alert') if rule_ct > 0: self._rule_log.info( 'Gym %s passed %s rule(s) and triggered %s alarm(s).', gym.name, rule_ct, alarm_ct) else: self._rule_log.info('Gym %s rejected by all rules.', gym.name) def process_egg(self, egg): # type: (Events.EggEvent) -> None """ Process a egg event and notify alarms if it passes. """ # Update Gym details (if they exist) egg.gym_name = self.__cache.gym_name(egg.gym_id, egg.gym_name) egg.gym_description = self.__cache.gym_desc( egg.gym_id, egg.gym_description) egg.gym_image = self.__cache.gym_image(egg.gym_id, egg.gym_image) # Update Team if Unknown if Unknown.is_(egg.current_team_id): egg.current_team_id = self.__cache.gym_team(egg.gym_id) # Make sure that eggs are enabled if self._eggs_enabled is False: self._log.debug("Egg ignored: egg notifications are disabled.") return # Skip if previously processed if self.__cache.egg_expiration(egg.gym_id) is not None: self._log.debug("Egg {} was skipped because it was " "previously processed.".format(egg.name)) return self.__cache.egg_expiration(egg.gym_id, egg.hatch_time) # Check the time remaining seconds_left = (egg.hatch_time - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: self._log.debug("Egg {} was skipped because only {} seconds " "remained".format(egg.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: egg.distance = get_earth_dist( [egg.lat, egg.lng], self.__location, self.__units) egg.direction = get_cardinal_dir( [egg.lat, egg.lng], self.__location) # Check for Rules rules = self.__egg_rules if len(rules) == 0: # If no rules, default to all rules = { "default": Rule(self._egg_filters.keys(), self._alarms.keys())} rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters( egg, self._egg_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms( egg, rule.alarm_names, 'raid_egg_alert') if rule_ct > 0: self._rule_log.info( 'Egg %s passed %s rule(s) and triggered %s alarm(s).', egg.name, rule_ct, alarm_ct) else: self._rule_log.info('Egg %s rejected by all rules.', egg.name) def process_raid(self, raid): # type: (Events.RaidEvent) -> None """ Process a raid event and notify alarms if it passes. """ # Update Gym details (if they exist) raid.gym_name = self.__cache.gym_name(raid.gym_id, raid.gym_name) raid.gym_description = self.__cache.gym_desc( raid.gym_id, raid.gym_description) raid.gym_image = self.__cache.gym_image(raid.gym_id, raid.gym_image) # Update Team if Unknown if Unknown.is_(raid.current_team_id): raid.current_team_id = self.__cache.gym_team(raid.gym_id) # Make sure that raids are enabled if self._raids_enabled is False: self._log.debug("Raid ignored: raid notifications are disabled.") return # Skip if previously processed if self.__cache.raid_expiration(raid.gym_id) is not None: self._log.debug("Raid {} was skipped because it was " "previously processed.".format(raid.name)) return self.__cache.raid_expiration(raid.gym_id, raid.raid_end) # Check the time remaining seconds_left = (raid.raid_end - datetime.utcnow()).total_seconds() if seconds_left < self.__time_limit: self._log.debug("Raid {} was skipped because only {} seconds " "remained".format(raid.name, seconds_left)) return # Calculate distance and direction if self.__location is not None: raid.distance = get_earth_dist( [raid.lat, raid.lng], self.__location, self.__units) raid.direction = get_cardinal_dir( [raid.lat, raid.lng], self.__location) # Check for Rules rules = self.__raid_rules if len(rules) == 0: # If no rules, default to all rules = {"default": Rule( self._raid_filters.keys(), self._alarms.keys())} rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters( raid, self._raid_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms( raid, rule.alarm_names, 'raid_alert') if rule_ct > 0: self._rule_log.info( 'Raid %s passed %s rule(s) and triggered %s alarm(s).', raid.name, rule_ct, alarm_ct) else: self._rule_log.info('Raid %s rejected by all rules.', raid.name) def process_weather(self, weather): # type: (Events.WeatherEvent) -> None """ Process a weather event and notify alarms if it passes. """ # Set the name for this event so we can log rejects better weather.name = self.__locale.get_weather_name(weather.s2_cell_id) # Make sure that weather changes are enabled if self._weather_enabled is False: self._log.debug("Weather ignored: weather change " "notifications are disabled.") return # Calculate distance and direction if self.__location is not None: weather.distance = get_earth_dist( [weather.lat, weather.lng], self.__location, self.__units) weather.direction = get_cardinal_dir( [weather.lat, weather.lng], self.__location) # Store copy of cache info cache_weather_id = self.__cache.cell_weather_id(weather.s2_cell_id) cache_day_or_night_id = self.__cache.day_or_night_id( weather.s2_cell_id) cache_severity_id = self.__cache.severity_id(weather.s2_cell_id) # Update cache info self.__cache.cell_weather_id(weather.s2_cell_id, weather.weather_id) self.__cache.day_or_night_id( weather.s2_cell_id, weather.day_or_night_id) self.__cache.severity_id(weather.s2_cell_id, weather.severity_id) # Check and see if the weather hasn't changed and ignore if weather.weather_id == cache_weather_id and \ weather.day_or_night_id == cache_day_or_night_id and \ weather.severity_id == cache_severity_id: self._log.debug( "weather of %s, alert of %s, and day or night of %s skipped: " "no change detected", weather.weather_id, weather.severity_id, weather.day_or_night_id) return # Check for Rules rules = self.__weather_rules if len(rules) == 0: # If no rules, default to all rules = {"default": Rule( self._weather_filters.keys(), self._alarms.keys())} rule_ct, alarm_ct = 0, 0 for r_name, rule in rules.iteritems(): # For all rules passed = self._check_filters( weather, self._weather_filters, rule.filter_names) if passed: rule_ct += 1 alarm_ct += len(rule.alarm_names) self._notify_alarms( weather, rule.alarm_names, 'weather_alert') if rule_ct > 0: self._rule_log.info( 'Weather %s passed %s rule(s) and triggered %s alarm(s).', weather.name, rule_ct, alarm_ct) else: self._rule_log.info( 'Weather %s rejected by all rules.', weather.name) # Check to see if a notification is within the given range # TODO: Move this into filters and add unit tests def check_geofences(self, f, e): """ Returns true if the event passes the filter's geofences. """ if self.geofences is None or f.geofences is None: # No geofences set return True targets = f.geofences if len(targets) == 1 and "all" in targets: targets = self.geofences.iterkeys() for name in targets: gf = self.geofences.get(name) if not gf: # gf doesn't exist self._log.error("Cannot check geofence %s: " "does not exist!", name) elif gf.contains(e.lat, e.lng): # e in gf self._log.debug("{} is in geofence {}!".format( e.name, gf.get_name())) e.geofence = name # Set the geofence for dts return True else: # e not in gf self._log.debug("%s not in %s.", e.name, name) self._log.debug("%s rejected from filter by geofences.", e.name) return False