def __init__(self, input_device_index: int = None): """Initiates Porcupine object for hot word detection. Args: input_device_index: Index of Input Device to use. See Also: - Instantiates an instance of Porcupine object and monitors audio stream for occurrences of keywords. - A higher sensitivity results in fewer misses at the cost of increasing the false alarm rate. - sensitivity: Tolerance/Sensitivity level. Takes argument or env var ``sensitivity`` or defaults to ``0.5`` References: - `Audio Overflow <https://people.csail.mit.edu/hubert/pyaudio/docs/#pyaudio.Stream.read>`__ handling. """ logger.info(f"Initiating hot-word detector with sensitivity: {env.sensitivity}") keyword_paths = [pvporcupine.KEYWORD_PATHS[x] for x in [pathlib.PurePath(__file__).stem]] self.input_device_index = input_device_index self.recorded_frames = [] self.py_audio = PyAudio() self.detector = pvporcupine.create( library_path=pvporcupine.LIBRARY_PATH, model_path=pvporcupine.MODEL_PATH, keyword_paths=keyword_paths, sensitivities=[env.sensitivity] ) self.audio_stream = None
def speak(text: str = None, run: bool = False, block: bool = True) -> NoReturn: """Calls ``audio_driver.say`` to speak a statement from the received text. Args: text: Takes the text that has to be spoken as an argument. run: Takes a boolean flag to choose whether to run the ``audio_driver.say`` loop. block: Takes a boolean flag to wait other tasks while speaking. [Applies only for larynx running on docker] """ caller = sys._getframe(1).f_code.co_name # noqa if text: text = text.replace('\n', '\t').strip() shared.text_spoken = text if shared.called_by_offline: shared.offline_caller = caller return logger.info(f'Speaker called by: {caller}') logger.info(f'Response: {text}') sys.stdout.write(f"\r{text}") if env.speech_synthesis_timeout and \ speech_synthesizer(text=text) and \ os.path.isfile(fileio.speech_synthesis_wav): playsound(sound=fileio.speech_synthesis_wav, block=block) os.remove(fileio.speech_synthesis_wav) else: audio_driver.say(text=text) if run: audio_driver.runAndWait() Thread(target=frequently_used, kwargs={ "function_name": caller }).start() if caller in FUNCTIONS_TO_TRACK else None
def trigger_api() -> None: """Initiates the fast API in a dedicated process using uvicorn server. See Also: - Checks if the port is being used. If so, makes a ``GET`` request to the endpoint. - Attempts to kill the process listening to the port, if the endpoint doesn't respond. """ url = f'http://{env.offline_host}:{env.offline_port}' if is_port_in_use(port=env.offline_port): logger.info(f'{env.offline_port} is currently in use.') try: res = requests.get(url=url, timeout=1) if res.ok: logger.info(f'{url} is accessible.') return raise requests.exceptions.ConnectionError except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): logger.error('Unable to connect to existing uvicorn server.') if not kill_port_pid(port=env.offline_port): # This might terminate Jarvis logger.critical('Failed to kill existing PID. Attempting to re-create session.') argument_dict = { "app": "api.fast:app", "host": env.offline_host, "port": env.offline_port, "reload": True } if not env.macos: del argument_dict['reload'] config = uvicorn.Config(**argument_dict) APIServer(config=config).run_in_parallel()
def threat_notify(converted: str, date_extn: Union[str, None], gmail_user: str, gmail_pass: str, phone_number: str, recipient: str) -> NoReturn: """Sends an SMS and email notification in case of a threat. References: Uses `gmail-connector <https://pypi.org/project/gmail-connector/>`__ to send the SMS and email. Args: converted: Takes the voice recognized statement as argument. date_extn: Name of the attachment file which is the picture of the intruder. gmail_user: Email address for the gmail account. gmail_pass: Password of the gmail account. phone_number: Phone number to send SMS. recipient: Email address of the recipient. """ if converted: communicator.notify(user=gmail_user, password=gmail_pass, number=phone_number, subject="!!INTRUDER ALERT!!", body=f"{datetime.now().strftime('%B %d, %Y %I:%M %p')}\n{converted}") body_ = f"""<html><head></head><body><h2>Conversation of Intruder:</h2><br>{converted}<br><br> <h2>Attached is a photo of the intruder.</h2>""" else: communicator.notify(user=gmail_user, password=gmail_pass, number=phone_number, subject="!!INTRUDER ALERT!!", body=f"{datetime.now().strftime('%B %d, %Y %I:%M %p')}\n" "Check your email for more information.") body_ = """<html><head></head><body><h2>No conversation was recorded, but attached is a photo of the intruder.</h2>""" if date_extn: attachment_ = f'threat/{date_extn}.jpg' response_ = SendEmail(gmail_user=gmail_user, gmail_pass=gmail_pass, recipient=recipient, body=body_, attachment=attachment_, subject=f"Intruder Alert on {datetime.now().strftime('%B %d, %Y %I:%M %p')}").send_email() if response_.ok: logger.info('Email has been sent!') else: logger.error(f"Email dispatch failed with response: {response_.body}\n")
def sentry_mode() -> NoReturn: """Listens forever and invokes ``initiator()`` when recognized. Stops when ``restart`` table has an entry. See Also: - Gets invoked only when run from Mac-OS older than 10.14. - A regular listener is used to convert audio to text. - The text is then condition matched for wake-up words. - Additional wake words can be passed in a list as an env var ``LEGACY_KEYWORDS``. """ try: while True: sys.stdout.write("\rSentry Mode") if wake_word := listener.listen(timeout=10, phrase_limit=2.5, sound=False, stdout=False): support.flush_screen() if any(word in wake_word.lower() for word in env.legacy_keywords): playsound(sound=indicators.acknowledgement, block=False) if phrase := listener.listen(timeout=env.timeout, phrase_limit=env.phrase_limit, sound=False): initiator(phrase=phrase, should_return=True) speaker.speak(run=True) if flag := support.check_restart(): logger.info(f"Restart condition is set to {flag[0]} by {flag[1]}") if flag[1] == "OFFLINE": stop_processes() for _handler in logger.handlers: if isinstance(_handler, logging.FileHandler): logger.removeHandler(hdlr=_handler) handler = custom_handler() logger.info(f"Switching to {handler.baseFilename}") logger.addHandler(hdlr=handler) shared.processes = start_processes() else: stop_processes(func_name=flag[1]) shared.processes[flag[1]] = start_processes(flag[1])
def check_existing() -> bool: """Checks for existing connection. Returns: bool: A boolean flag whether a valid connection is present. """ if is_port_in_use(port=env.speech_synthesis_port): logger.info(f'{env.speech_synthesis_port} is currently in use.') try: res = requests.get( url= f"http://{env.speech_synthesis_host}:{env.speech_synthesis_port}", timeout=1) if res.ok: logger.info( f'http://{env.speech_synthesis_host}:{env.speech_synthesis_port} is accessible.' ) return True return False except (ConnectionError, TimeoutError, requests.exceptions.RequestException, requests.exceptions.Timeout) as \ error: logger.error(error) if not kill_port_pid(port=env.speech_synthesis_port): logger.critical( 'Failed to kill existing PID. Attempting to re-create session.' )
def start_processes( func_name: str = None) -> Union[Process, Dict[str, Process]]: """Initiates multiple background processes to achieve parallelization. Methods - poll_for_messages: Initiates polling for messages on the telegram bot. - trigger_api: Initiates Jarvis API using uvicorn server to receive offline commands. - automator: Initiates automator that executes offline commands and certain functions at said time. - initiate_tunneling: Initiates ngrok tunnel to host Jarvis API through a public endpoint. - write_current_location: Writes current location details into a yaml file. - speech_synthesis: Initiates larynx docker image. - playsound: Plays a start-up sound. """ processes = { "handler": Process(target=handler), "trigger_api": Process(target=trigger_api), "automator": Process(target=automator), "initiate_tunneling": Process(target=initiate_tunneling), "write_current_location": Process(target=write_current_location), "synthesizer": Process(target=docker_container.synthesizer) } if func_name: processes = {func_name: processes[func_name]} for func, process in processes.items(): process.start() logger.info( f"Started function: {func} {process.sentinel} with PID: {process.pid}" ) playsound(sound=indicators.initialize, block=False) if not func_name else None return processes[func_name] if func_name else processes
def listen(timeout: Union[int, float], phrase_limit: Union[int, float], sound: bool = True, stdout: bool = True) -> Union[str, None]: """Function to activate listener, this function will be called by most upcoming functions to listen to user input. Args: timeout: Time in seconds for the overall listener to be active. phrase_limit: Time in seconds for the listener to actively listen to a sound. sound: Flag whether to play the listener indicator sound. Defaults to True unless set to False. stdout: Flag whether to print the listener status on screen. Returns: str: - Returns recognized statement from the microphone. """ with Microphone() as source: try: playsound(sound=indicators.start, block=False) if sound else None sys.stdout.write("\rListener activated...") if stdout else None listened = recognizer.listen(source=source, timeout=timeout, phrase_time_limit=phrase_limit) playsound(sound=indicators.end, block=False) if sound else None support.flush_screen() recognized = recognizer.recognize_google(audio_data=listened) logger.info(recognized) return recognized except (UnknownValueError, RequestError, WaitTimeoutError): return except (ConnectionError, TimeoutError, requests.exceptions.RequestException, requests.exceptions.Timeout) as \ error: logger.error(error)
def wrapper(*args: Any, **kwargs: Any) -> Callable: """Executes the wrapped function in a loop for the number of attempts mentioned. Args: *args: Arguments. **kwargs: Keyword arguments. Returns: Callable: Return value of the function implemented. Raises: Raises the exception as received beyond the given number of attempts. """ return_exc = None for i in range(1, attempts + 1): try: return_val = func(*args, **kwargs) # Log messages only when the function did not return during the first attempt logger.info(msg=f"{func.__name__} returned at {engine().ordinal(num=i)} attempt") if i > 1 else None return return_val except exclude_exc or KeyboardInterrupt as excl_error: logger.error(msg=excl_error) except Exception as error: return_exc = error time.sleep(interval) logger.error(msg=f"{func.__name__} exceeded retry count::{attempts}") if return_exc and warn: warnings.warn( f"{type(return_exc).__name__}: {return_exc}" ) elif return_exc: raise return_exc
def text_to_audio( text: str, filename: Union[FilePath, str] = None) -> Union[FilePath, str]: """Converts text into an audio file. Args: filename: Name of the file that has to be generated. text: Text that has to be converted to audio. """ if not filename: if shared.offline_caller: filename = f"{shared.offline_caller}.wav" shared.offline_caller = None # Reset caller after using it else: filename = f"{int(time.time())}.wav" process = Process(target=_generate_audio_file, kwargs={ 'filename': filename, 'text': text }) process.start() while True: if os.path.isfile(filename) and os.stat(filename).st_size: time.sleep(0.5) break logger.info(f"Generated {filename}") data, samplerate = soundfile.read(file=filename) soundfile.write(file=filename, data=data, samplerate=samplerate) return filename
def create_reminder(hour, minute, am_pm, message, to_about, timer: str = None) -> NoReturn: """Creates the lock file necessary to set a reminder. Args: hour: Hour of reminder time. minute: Minute of reminder time. am_pm: AM/PM of reminder time. message: Message to be reminded for. to_about: remind to or remind about as said in phrase. timer: Number of minutes/hours to reminder. """ if not os.path.isdir('reminder'): os.mkdir('reminder') pathlib.Path( os.path.join( "reminder", f"{hour}_{minute}_{am_pm}|{message.replace(' ', '_')}.lock") ).touch() if timer: logger.info( f"Reminder created for '{message}' at {hour}:{minute} {am_pm}") speaker.speak( text=f"{random.choice(conversation.acknowledgement)}! " f"I will remind you {to_about} {message}, after {timer}.") else: speaker.speak( text=f"{random.choice(conversation.acknowledgement)}! " f"I will remind you {to_about} {message}, at {hour}:{minute} {am_pm}." )
def create_alarm(hour: str, minute: str, am_pm: str, phrase: str, timer: str = None) -> NoReturn: """Creates the lock file necessary to set an alarm/timer. Args: hour: Hour of alarm time. minute: Minute of alarm time. am_pm: AM/PM of alarm time. phrase: Phrase spoken. timer: Number of minutes/hours to alarm. """ if not os.path.isdir('alarm'): os.mkdir('alarm') pathlib.Path(f'alarm/{hour}_{minute}_{am_pm}.lock').touch() if 'wake' in phrase: speaker.speak(text=f"{random.choice(conversation.acknowledgement)}! " f"I will wake you up at {hour}:{minute} {am_pm}.") elif 'timer' in phrase and timer: logger.info(f"Timer set at {hour}:{minute} {am_pm}") response = [f"{random.choice(conversation.acknowledgement)}! I have set a timer for {timer}.", f"{timer}! Counting down.."] speaker.speak(text=random.choice(response)) else: if datetime.strptime(datetime.today().strftime("%I:%M %p"), "%I:%M %p") >= \ datetime.strptime(f"{hour}:{minute} {am_pm}", "%I:%M %p"): add = " tomorrow." else: add = "." response = [f"Alarm has been set for {hour}:{minute} {am_pm}{add}", f"Your alarm is set for {hour}:{minute} {am_pm}{add}"] speaker.speak(text=f"{random.choice(conversation.acknowledgement)}! {random.choice(response)}")
def delete_db() -> NoReturn: """Delete base db if exists. Called upon restart or shut down.""" if os.path.isfile(fileio.base_db): logger.info(f"Removing {fileio.base_db}") os.remove(fileio.base_db) if os.path.isfile(fileio.base_db): raise FileExistsError(f"{fileio.base_db} still exists!") return
def automator() -> NoReturn: """Place for long-running background tasks. See Also: - The automation file should be a dictionary within a dictionary that looks like the below: .. code-block:: yaml 6:00 AM: task: set my bedroom lights to 50% 9:00 PM: task: set my bedroom lights to 5% - Jarvis creates/swaps a ``status`` flag upon execution, so that it doesn't repeat execution within a minute. """ offline_list = compatibles.offline_compatible() + keywords.restart_control start_events = start_meetings = time.time() events.event_app_launcher() dry_run = True while True: if os.path.isfile(fileio.automation): if exec_task := auto_helper(offline_list=offline_list): offline_communicator(command=exec_task) if start_events + env.sync_events <= time.time() or dry_run: start_events = time.time() event_process = Process(target=events.events_writer) logger.info(f"Getting calendar events from {env.event_app}") if dry_run else None event_process.start() with db.connection: cursor = db.connection.cursor() cursor.execute("INSERT INTO children (events) VALUES (?);", (event_process.pid,)) db.connection.commit() if start_meetings + env.sync_meetings <= time.time() or dry_run: if dry_run and env.ics_url: try: if requests.get(url=env.ics_url).status_code == 503: env.sync_meetings = 21_600 # Set to 6 hours if unable to connect to the meetings URL except (ConnectionError, TimeoutError, requests.exceptions.RequestException, requests.exceptions.Timeout) as error: logger.error(error) env.sync_meetings = 99_999_999 # NEVER RUN, since env vars are loaded only once during start up start_meetings = time.time() meeting_process = Process(target=icalendar.meetings_writer) logger.info("Getting calendar schedule from ICS.") if dry_run else None meeting_process.start() with db.connection: cursor = db.connection.cursor() cursor.execute("INSERT INTO children (meetings) VALUES (?);", (meeting_process.pid,)) db.connection.commit() if alarm_state := support.lock_files(alarm_files=True): for each_alarm in alarm_state: if each_alarm == datetime.now().strftime("%I_%M_%p.lock"): Process(target=alarm_executor).start() os.remove(os.path.join("alarm", each_alarm))
def _generate_audio_file(filename: Union[FilePath, str], text: str) -> NoReturn: """Generates an audio file from text. Args: filename: Filename to be generated. text: Text that has to be converted into audio. """ logger.info(f"Generating audio into {filename} from the text: {text}") audio_driver.save_to_file(filename=filename, text=text) audio_driver.runAndWait()
def stop_processes(func_name: str = None) -> NoReturn: """Stops all background processes initiated during startup and removes database source file.""" stop_child_processes() if func_name in ["automator"] else None for func, process in shared.processes.items(): if func_name and func_name != func: continue if process.is_alive(): logger.info(f"Sending [SIGTERM] to {func} with PID: {process.pid}") process.terminate() if process.is_alive(): logger.info(f"Sending [SIGKILL] to {func} with PID: {process.pid}") process.kill()
def delay_condition(phrase: str, delay: Union[int, float]) -> None: """Delays the execution after sleeping for the said time, after which it is sent to ``offline_communicator``. Args: phrase: Takes the phrase spoken as an argument. delay: Sleeps for the number of seconds. """ logger.info( f"'{phrase}' will be executed after {support.time_converter(seconds=delay)}" ) time.sleep(delay) logger.info(f"Executing '{phrase}'") offline_communicator(command=phrase)
def restart_control(phrase: str = None, quiet: bool = False) -> NoReturn: """Controls the restart functions based on the user request. Args: phrase: Takes the phrase spoken as an argument. quiet: Take a boolean flag to restart without warning. """ if phrase and ('pc' in phrase.lower() or 'computer' in phrase.lower() or 'machine' in phrase.lower()): logger.info( f'JARVIS::Restart for {shared.hosted_device.get("device")} has been requested.' ) restart() else: caller = sys._getframe(1).f_code.co_name # noqa logger.info(f'Called by {caller}') if quiet: # restarted due internal errors logger.info(f"Restarting {caller}") elif shared.called_by_offline: # restarted via automator logger.info("Restarting all background processes!") caller = "OFFLINE" else: speaker.speak( text= "I didn't quite get that. Did you mean restart your computer?") return with db.connection: cursor = db.connection.cursor() cursor.execute("INSERT INTO restart (flag, caller) VALUES (?,?);", (True, caller)) cursor.connection.commit()
def initiate_tunneling() -> NoReturn: """Initiates Ngrok to tunnel requests from external sources if they aren't running already. Notes: - ``forever_ngrok.py`` is a simple script that triggers ngrok connection in the given offline port. - The connection is tunneled through a public facing URL used to make ``POST`` requests to Jarvis API. """ if not env.macos: return pid_check = subprocess.check_output("ps -ef | grep forever_ngrok.py", shell=True) pid_list = pid_check.decode('utf-8').split('\n') for id_ in pid_list: if id_ and 'grep' not in id_ and '/bin/sh' not in id_: logger.info('An instance of ngrok tunnel for offline communicator is running already.') return if os.path.exists(f"{env.home}/JarvisHelper/venv/bin/activate"): logger.info('Initiating ngrok connection for offline communicator.') initiate = f'cd {env.home}/JarvisHelper && ' \ f'source venv/bin/activate && export host={env.offline_host} ' \ f'export port={env.offline_port} && python forever_ngrok.py' os.system(f"""osascript -e 'tell application "Terminal" to do script "{initiate}"' > /dev/null""") else: logger.info(f'JarvisHelper is not available to trigger an ngrok tunneling through {env.offline_port}') endpoint = rf'http:\\{env.offline_host}:{env.offline_port}' logger.info('However offline communicator can still be accessed via ' f'{endpoint}\\offline-communicator for API calls and {endpoint}\\docs for docs.')
def terminator() -> NoReturn: """Exits the process with specified status without calling cleanup handlers, flushing stdio buffers, etc. Using this, eliminates the hassle of forcing multiple threads to stop. """ pid = os.getpid() proc = psutil.Process(pid=pid) logger.info(f"Terminating process: {pid}") try: proc.wait(timeout=5) except psutil.TimeoutExpired: logger.warning(f"Failed to terminate process in 5 seconds: {pid}") if proc.is_running(): logger.info(f"{pid} is still running. Killing it.") proc.kill()
def save_audio(frames: list, sample_rate: int) -> NoReturn: """Converts audio frames into a recording to store it in a wav file. Args: frames: List of frames. sample_rate: Sample rate. """ recordings_location = os.path.join(os.getcwd(), 'recordings') if not os.path.isdir(recordings_location): os.makedirs(recordings_location) filename = os.path.join(recordings_location, f"{datetime.now().strftime('%B_%d_%Y_%H%M')}.wav") logger.info(f"Saving {len(frames)} audio frames into {filename}") sys.stdout.write(f"\rSaving {len(frames)} audio frames into {filename}") recorded_audio = numpy.concatenate(frames, axis=0).astype(dtype=numpy.int16) soundfile.write(file=filename, data=recorded_audio, samplerate=sample_rate, subtype='PCM_16') support.flush_screen()
def start(self) -> NoReturn: """Runs ``audio_stream`` in a forever loop and calls ``initiator`` when the phrase ``Jarvis`` is heard.""" try: while True: sys.stdout.write("\rSentry Mode") if not self.audio_stream: self.open_stream() pcm = struct.unpack_from("h" * self.detector.frame_length, self.audio_stream.read(num_frames=self.detector.frame_length, exception_on_overflow=False)) self.recorded_frames.append(pcm) if self.detector.process(pcm=pcm) >= 0: playsound(sound=indicators.acknowledgement, block=False) self.close_stream() if phrase := listener.listen(timeout=env.timeout, phrase_limit=env.phrase_limit, sound=False): initiator(phrase=phrase, should_return=True) speaker.speak(run=True) if flag := support.check_restart(): logger.info(f"Restart condition is set to {flag[0]} by {flag[1]}") if flag[1] == "OFFLINE": stop_processes() for _handler in logger.handlers: if isinstance(_handler, logging.FileHandler): logger.removeHandler(hdlr=_handler) handler = custom_handler() logger.info(f"Switching to {handler.baseFilename}") logger.addHandler(hdlr=handler) shared.processes = start_processes() else: stop_processes(func_name=flag[1]) shared.processes[flag[1]] = start_processes(flag[1]) if flag := support.check_stop(): logger.info(f"Stopper condition is set to {flag[0]} by {flag[1]}") self.stop() terminator()
def stop_child_processes() -> NoReturn: """Stops sub processes (for meetings and events) triggered by child processes.""" with db.connection: cursor_ = db.connection.cursor() children = cursor_.execute( "SELECT meetings, events FROM children").fetchone() for pid in children: if not pid: continue try: proc = psutil.Process(pid) except psutil.NoSuchProcess as error: logger.error( error ) # Occurs commonly since child processes run only for a short time continue if proc.is_running(): logger.info(f"Sending [SIGTERM] to child process with PID: {pid}") proc.terminate() if proc.is_running(): logger.info(f"Sending [SIGKILL] to child process with PID: {pid}") proc.kill()
def write_current_location() -> NoReturn: """Extracts location information from public IP address and writes it to a yaml file.""" if os.path.isfile(fileio.location): try: with open(fileio.location) as file: data = yaml.load(stream=file, Loader=yaml.FullLoader) or {} except yaml.YAMLError as error: data = {} logger.error(error) address = data.get("address") if address and data.get("reserved") and data.get("latitude") and data.get("longitude") and \ address.get("city", address.get("hamlet")) and address.get("country") and \ address.get("state", address.get("county")): logger.info(f"{fileio.location} is reserved.") logger.warning("Automatic location detection has been disabled!") return current_lat, current_lon = get_coordinates_from_ip() location_info = get_location_from_coordinates(coordinates=(current_lat, current_lon)) current_tz = TimezoneFinder().timezone_at(lat=current_lat, lng=current_lon) logger.info(f"Writing location info in {fileio.location}") with open(fileio.location, 'w') as location_writer: yaml.dump(data={"timezone": current_tz, "latitude": current_lat, "longitude": current_lon, "address": location_info}, stream=location_writer, default_flow_style=False)
def speech_synthesizer( text: str, timeout: Union[int, float] = env.speech_synthesis_timeout, quality: str = "high", voice: str = "en-us_northern_english_male-glow_tts") -> bool: """Makes a post call to docker container for speech synthesis. Args: text: Takes the text that has to be spoken as an argument. timeout: Time to wait for the docker image to process text-to-speech request. quality: Quality at which the conversion is to be done. voice: Voice for speech synthesis. Returns: bool: A boolean flag to indicate whether speech synthesis has worked. """ logger.info(f"Request for speech synthesis: {text}") if time_in_str := re.findall(r'(\d+:\d+\s?(?:AM|PM|am|pm:?))', text): for t_12 in time_in_str: t_24 = datetime.strftime(datetime.strptime(t_12, "%I:%M %p"), "%H:%M") logger.info(f"Converted {t_12} -> {t_24}") text = text.replace(t_12, t_24)
def hostname_to_ip(hostname: str) -> list: """Uses ``socket.gethostbyname_ex`` to translate a host name to IPv4 address format, extended interface. See Also: - A host may have multiple interfaces. - | In case of true DNS being used or the host entry file is carefully handwritten, the system will look | there to find the translation. - | But depending on the configuration, the host name can be bound to all the available interfaces, including | the loopback ones. - ``gethostbyname`` returns the address of the first of those interfaces in its own order. - | To get the assigned IP, ``gethostbyname_ex`` is used, which returns a list of all the interfaces, including | the host spot connected, and loopback IPs. References: https://docs.python.org/3/library/socket.html#socket.gethostbyname_ex Args: hostname: Takes the hostname of a device as an argument. """ try: _hostname, _alias_list, _ipaddr_list = socket.gethostbyname_ex(hostname) except socket.error as error: logger.error(f"{error} on {hostname}") return [] logger.info({"Hostname": _hostname, "Alias": _alias_list, "Interfaces": _ipaddr_list}) if not _ipaddr_list: logger.critical(f"No interfaces found for {hostname}") elif len(_ipaddr_list) > 1: logger.warning(f"Host {hostname} has multiple interfaces. {_ipaddr_list}") return _ipaddr_list else: ip_addr = ip_address() if _ipaddr_list[0].split('.')[0] == ip_addr.split('.')[0]: return _ipaddr_list logger.error(f"NetworkID of the InterfaceIP of host {hostname} does not match the network id of DeviceIP.") return []
def kill_port_pid(port: int, protocol: str = 'tcp') -> Union[bool, None]: """Uses List all open files ``lsof`` to get the PID of the process that is listening on the given port and kills it. Args: port: Port number which the application is listening on. protocol: Protocol serving the port. Defaults to ``TCP`` Warnings: **Use only when the application listening to given port runs as a dedicated/child process with a different PID** - This function will kill the process that is using the given port. - If the PID is the same as that of ``MainProcess``, triggers a warning without terminating the process. Returns: bool: Flag to indicate whether the process was terminated successfully. """ try: active_sessions = subprocess.check_output( f"lsof -i {protocol}:{port}", shell=True).decode('utf-8').splitlines() for each in active_sessions: each_split = each.split() if each_split[0].strip() == 'Python': logger.info( f'Application hosted on {each_split[-2]} is listening to port: {port}' ) pid = int(each_split[1]) if pid == os.getpid(): called_function = sys._getframe(1).f_code.co_name # noqa called_file = sys._getframe(1).f_code.co_filename.replace( f'{os.getcwd()}/', '') # noqa logger.warning( f"{called_function} from {called_file} tried to kill the running process." ) warnings.warn( f'OPERATION DENIED: {called_function} from {called_file} tried to kill the running process.' ) return os.kill(pid, signal.SIGTERM) logger.info(f'Killed PID: {pid}') return True logger.info(f'No active process running on {port}') return False except (subprocess.SubprocessError, subprocess.CalledProcessError) as error: logger.error(error)
def system_vitals() -> None: """Reads system vitals on macOS. See Also: - Jarvis will suggest a reboot if the system uptime is more than 2 days. - If confirmed, invokes `restart <https://thevickypedia.github.io/Jarvis/#jarvis.restart>`__ function. """ output = "" if env.macos: if not env.root_password: speaker.speak( text= f"You haven't provided a root password for me to read system vitals {env.title}! " "Add the root password as an environment variable for me to read." ) return logger.info('Fetching system vitals') cpu_temp, gpu_temp, fan_speed, output = None, None, None, "" # Tested on 10.13, 10.14, 11.6 and 12.3 versions if not shared.hosted_device or not shared.hosted_device.get( 'os_version'): logger.warning( "hosted_device information was not loaded during startup. Reloading now." ) shared.hosted_device = hosted_device_info() if packaging.version.parse(shared.hosted_device.get( 'os_version')) > packaging.version.parse('10.14'): critical_info = [ each.strip() for each in (os.popen( f'echo {env.root_password} | sudo -S powermetrics --samplers smc -i1 -n1' )).read().split('\n') if each != '' ] support.flush_screen() for info in critical_info: if 'CPU die temperature' in info: cpu_temp = info.strip('CPU die temperature: ').replace( ' C', '').strip() if 'GPU die temperature' in info: gpu_temp = info.strip('GPU die temperature: ').replace( ' C', '').strip() if 'Fan' in info: fan_speed = info.strip('Fan: ').replace(' rpm', '').strip() else: fan_speed = subprocess.check_output( f'echo {env.root_password} | sudo -S spindump 1 1 -file /tmp/spindump.txt > /dev/null 2>&1;grep ' f'"Fan speed" /tmp/spindump.txt;sudo rm /tmp/spindump.txt', shell=True).decode('utf-8') if cpu_temp: cpu = f'Your current average CPU temperature is ' \ f'{support.format_nos(input_=temperature.c2f(arg=support.extract_nos(input_=cpu_temp)))}' \ f'\N{DEGREE SIGN}F. ' output += cpu speaker.speak(text=cpu) if gpu_temp: gpu = f'GPU temperature is {support.format_nos(temperature.c2f(support.extract_nos(gpu_temp)))}' \ f'\N{DEGREE SIGN}F. ' output += gpu speaker.speak(text=gpu) if fan_speed: fan = f'Current fan speed is {support.format_nos(support.extract_nos(fan_speed))} RPM. ' output += fan speaker.speak(text=fan) restart_time = datetime.fromtimestamp(psutil.boot_time()) second = (datetime.now() - restart_time).total_seconds() restart_time = datetime.strftime(restart_time, "%A, %B %d, at %I:%M %p") restart_duration = support.time_converter(seconds=second) output += f'Restarted on: {restart_time} - {restart_duration} ago from now.' if shared.called_by_offline: speaker.speak(text=output) return sys.stdout.write(f'\r{output}') speaker.speak( text= f"Your {shared.hosted_device.get('device')} was last booted on {restart_time}. " f"Current boot time is: {restart_duration}.") if second >= 259_200: # 3 days if boot_extreme := re.search('(.*) days', restart_duration): warn = int(boot_extreme.group().replace(' days', '').strip()) speaker.speak( text= f"{env.title}! your {shared.hosted_device.get('device')} has been running for more " f"than {warn} days. You must consider a reboot for better performance. Would you like " f"me to restart it for you {env.title}?", run=True) response = listener.listen(timeout=3, phrase_limit=3) if any(word in response.lower() for word in keywords.ok): logger.info( f'JARVIS::Restarting {shared.hosted_device.get("device")}') restart(ask=False)
def conditions(converted: str, should_return: bool = False) -> bool: """Conditions function is used to check the message processed. Uses the keywords to match pre-defined conditions and trigger the appropriate function which has dedicated task. Args: converted: Takes the voice recognized statement as argument. should_return: A boolean flag sent by ``Activator`` to indicate that the ``else`` part shouldn't be executed. Returns: bool: Boolean True only when asked to sleep for conditioned sleep message. """ converted_lower = converted.lower() todo_checks = ['to do', 'to-do', 'todo'] if any(word in converted_lower for word in keywords.lights): lights(converted) elif any(word in converted_lower for word in keywords.television): television(converted) elif any(word in converted_lower for word in keywords.volume): volume(converted) elif any(word in converted_lower for word in keywords.car): car(converted_lower) elif any(word in converted_lower for word in keywords.weather): weather(converted) # ORDER OF THE ABOVE SHOULD BE RETAINED elif any(word in converted_lower for word in keywords.meetings): meetings() elif any(word in converted_lower for word in keywords.current_date) and \ not any(word in converted_lower for word in keywords.avoid): current_date() elif any(word in converted_lower for word in keywords.current_time) and \ not any(word in converted_lower for word in keywords.avoid): current_time(converted) elif any(word in converted_lower for word in keywords.system_info): system_info() elif any(word in converted_lower for word in keywords.ip_info) or 'IP' in converted.split(): ip_info(converted) elif any(word in converted_lower for word in keywords.wikipedia_): wikipedia_() elif any(word in converted_lower for word in keywords.news): news() elif any(word in converted_lower for word in keywords.report): report() elif any(word in converted_lower for word in keywords.robinhood): robinhood() elif any(word in converted_lower for word in keywords.repeat): repeat() elif any(word in converted_lower for word in keywords.location): location() elif any(word in converted_lower for word in keywords.locate): locate(converted) elif any(word in converted_lower for word in keywords.read_gmail): read_gmail() elif any(word in converted_lower for word in keywords.meaning): meaning(converted) elif any(word in converted_lower for word in keywords.delete_todo) and 'items' in converted_lower and \ any(word in converted_lower for word in todo_checks): delete_todo_items() elif any(word in converted_lower for word in keywords.todo): todo() elif any(word in converted_lower for word in keywords.add_todo) and \ any(word in converted_lower for word in todo_checks): add_todo() elif any(word in converted_lower for word in keywords.delete_todo) and \ any(word in converted_lower for word in todo_checks): delete_todo() elif any(word in converted_lower for word in keywords.distance) and \ not any(word in converted_lower for word in keywords.avoid): distance(converted) elif any(word in converted_lower for word in conversation.form): speak(text="I am a program, I'm without form.") elif any(word in converted_lower for word in keywords.locate_places): locate_places(converted) elif any(word in converted_lower for word in keywords.directions): directions(converted) elif any(word in converted_lower for word in keywords.kill_alarm): kill_alarm(converted) elif any(word in converted_lower for word in keywords.set_alarm): set_alarm(converted) elif any(word in converted_lower for word in keywords.google_home): google_home() elif any(word in converted_lower for word in keywords.jokes): jokes() elif any(word in converted_lower for word in keywords.reminder): reminder(converted) elif any(word in converted_lower for word in keywords.notes): notes() elif any(word in converted_lower for word in keywords.github): github(converted) elif any(word in converted_lower for word in keywords.send_sms): send_sms(converted) elif any(word in converted_lower for word in keywords.google_search): google_search(converted) elif any(word in converted_lower for word in keywords.apps): apps(converted) elif any(word in converted_lower for word in keywords.music): music(converted) elif any(word in converted_lower for word in keywords.face_detection): face_detection() elif any(word in converted_lower for word in keywords.speed_test) and \ ('internet' in converted_lower or 'connection' in converted_lower or 'run' in converted_lower): speed_test() elif any(word in converted_lower for word in keywords.bluetooth): bluetooth(converted) elif any(word in converted_lower for word in keywords.brightness): brightness(converted) elif any(word in converted_lower for word in keywords.guard_enable): guard_enable() elif any(word in converted_lower for word in keywords.flip_a_coin): flip_a_coin() elif any(word in converted_lower for word in keywords.facts): facts() elif any(word in converted_lower for word in keywords.events): events() elif any(word in converted_lower for word in keywords.voice_changer): voice_changer(converted) elif any(word in converted_lower for word in keywords.system_vitals): system_vitals() elif any(word in converted_lower for word in keywords.vpn_server): vpn_server(converted) elif any(word in converted_lower for word in keywords.automation): automation_handler(converted_lower) elif any(word in converted_lower for word in keywords.sprint): sprint_name() elif any(word in converted_lower for word in conversation.greeting): response = [ 'I am spectacular. I hope you are doing fine too.', 'I am doing well. Thank you.', 'I am great. Thank you.' ] speak(text=random.choice(response)) elif any(word in converted_lower for word in conversation.capabilities): speak( text= 'There is a lot I can do. For example: I can get you the weather at any location, news around ' 'you, meanings of words, launch applications, create a to-do list, check your emails, get your ' 'system configuration, tell your investment details, locate your phone, find distance between ' 'places, set an alarm, play music on smart devices around you, control your TV, tell a joke, send' ' a message, set reminders, scan and clone your GitHub repositories, and much more. Time to ask,.' ) elif any(word in converted_lower for word in conversation.languages): speak( text= "Tricky question!. I'm configured in python, and I can speak English." ) elif any(word in converted_lower for word in conversation.whats_up): speak( text= "My listeners are up. There is nothing I cannot process. So ask me anything.." ) elif any(word in converted_lower for word in conversation.what): speak(text="I'm just a pre-programmed virtual assistant.") elif any(word in converted_lower for word in conversation.who): speak(text="I am Jarvis. A virtual assistant designed by Mr.Raauv.") elif any(word in converted_lower for word in conversation.about_me): speak( text="I am Jarvis. A virtual assistant designed by Mr.Raauv. " "I'm just a pre-programmed virtual assistant, trying to become a natural language UI. " "I can seamlessly take care of your daily tasks, and also help with most of your work!" ) elif any(word in converted_lower for word in keywords.sleep_control): return controls.sleep_control(converted) elif any(word in converted_lower for word in keywords.restart_control): controls.restart_control(converted) elif any(word in converted_lower for word in keywords.kill) and \ not any(word in converted_lower for word in keywords.avoid): raise StopSignal elif any(word in converted_lower for word in keywords.shutdown): controls.shutdown() elif should_return: Thread(target=support.unrecognized_dumper, args=[{ 'ACTIVATOR': converted }]).start() return False else: logger.info(f'Received unrecognized lookup parameter: {converted}') Thread(target=support.unrecognized_dumper, args=[{ 'CONDITIONS': converted }]).start() if not alpha(text=converted): if not google_maps(query=converted): if google(query=converted): # if none of the conditions above are met, opens a Google search on default browser search_query = str(converted).replace(' ', '+') unknown_url = f"https://www.google.com/search?q={search_query}" webbrowser.open(url=unknown_url)
raise StopSignal else: speaker.speak(text=f"Machine state is left intact {env.title}!") return def exit_process() -> NoReturn: """Function that holds the list of operations done upon exit.""" reminders = {} alarms = support.lock_files(alarm_files=True) if reminder_files := support.lock_files(reminder_files=True): for file in reminder_files: split_val = file.replace('.lock', '').split('|') reminders.update({split_val[0]: split_val[-1]}) if reminders: logger.info(f'JARVIS::Deleting Reminders - {reminders}') if len(reminders) == 1: speaker.speak(text=f'You have a pending reminder {env.title}!') else: speaker.speak( text=f'You have {len(reminders)} pending reminders {env.title}!' ) for key, value in reminders.items(): speaker.speak( text= f"{value.replace('_', ' ')} at {key.lstrip('0').replace('00', '').replace('_', ' ')}" ) if alarms: alarms = ', and '.join(alarms) if len(alarms) != 1 else ''.join(alarms) alarms = alarms.replace('.lock', '').replace('_', ':').replace( ':PM', ' PM').replace(':AM', ' AM')