#!/usr/bin/env python ########################################################################### ## Copyright (C) Wizardry and Steamworks 2019 - License: GNU GPLv3 ## ########################################################################### # Reads RetroArch logs looking for NetPlay events and publishes to MQTT # ########################################################################### # # wget https://bootstrap.pypa.io/get-pip.py # python get-pip.py # # pip install pyyaml # pip install watchdog # pip install paho-mqtt # pip install daemonize import yaml, subprocess, os, argparse, signal, sys, time, mmap, re, json import paho.mqtt.client as mqtt from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from os import path from daemonize import Daemonize import constants # Called when the file has been modified. class RetroArchLogUpdated(FileSystemEventHandler): def __init__(self, client): self.client = client # Called when the monitored file is modified. def on_modified(self, event): line = self.readlast(event.src_path) for event in logevents: match = logevents[event].match(line) if match: #print list(match.groups()) payload = {} payload['event'] = event if event == 'announcing': pass if event == 'waiting': pass if event == 'playerjoined': payload['nick'] = match.group(1) payload['controls'] = match.group(2) if event == 'playerdisconnected': payload['nick'] = match.groups(1) if event == 'connecting': pass if event == 'joined': payload['controls'] = match.groups(1) #print json.dumps(payload, ensure_ascii=False) # Publishes to MQTT. self.client.publish(cfg['mqtt']['topic'], json.dumps(payload, ensure_ascii=False)) # Gets the last line of a file. @staticmethod def readlast(filename): with open(filename) as f: mapping = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) i = mapping.rfind('\n') j = mapping.rfind('\n', 0, i) return mapping[j + 1:i] # The callback for when the client receives a CONNACK response from the server. def on_connect(client, userdata, flags, rc): client.subscribe(cfg['mqtt']['topic']) # Callback when a message is received on the MQTT bus. def on_message(client, userdata, msg): pass def main(): # Set up MQTT client. client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message client.connect(cfg['mqtt']['host'], cfg['mqtt']['port'], 60) # Set up log watcher. event_handler = RetroArchLogUpdated(client) observer = Observer() observer.schedule(event_handler, path=cfg['retroarch']['logdir'], recursive=False) observer.start() # Blocking call. client.loop_forever() def daemon(): try: if path.exists(cfg['pid']): with open(cfg['pid'], 'r') as pid_file: pid = pid_file.read() os.kill(int(pid), signal.SIGHUP) os.remove(cfg['pid']) except Exception, e: print 'Unable to kill previous process: ', str(e) return daemon = Daemonize(app='mqttSwitch', pid=cfg['pid'], action=main) daemon.start() with open(os.path.join(os.path.dirname(sys.argv[0]), 'config.yml'), 'r') as ymlfile: cfg = yaml.load(ymlfile, Loader=yaml.FullLoader) logevents = {} for event in constants.NETPLAY_PATTERNS: logevents[event] = re.compile(constants.NETPLAY_PATTERNS[event], re.IGNORECASE) parser = argparse.ArgumentParser() parser.add_argument('--daemon', action='store_true', help='run as a daemon') parser.add_argument('--foreground', action='store_true', help='run as a daemon') args = parser.parse_args() if(args.daemon): daemon() elif(args.foreground): main() parser.print_help()