From 4583f0e316cedad1928509ac383e2bf2b31d196d Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Mon, 22 Aug 2016 18:40:40 +0100 Subject: [PATCH] Monitor the journal file and send events to EDDN Sends 'FSDJump', 'Docked' and 'Scan' events in draft http://schemas.elite-markets.net/eddn/journal/1 format. --- EDMarketConnector.py | 94 +++++++++++++++++++--------- PLUGINS.md | 18 +++--- README.md | 18 ++++-- config.py | 1 + eddn.py | 9 ++- monitor.py | 140 +++++++++++++++++++----------------------- plug.py | 21 +++++++ plugins/About/load.py | 21 ++++--- prefs.py | 31 +++++++--- 9 files changed, 215 insertions(+), 138 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 59b6e7bd..7a5ce618 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -9,7 +9,8 @@ from os import mkdir from os.path import expanduser, isdir, join import re import requests -from time import time, localtime, strftime +from time import time, localtime, strftime, strptime +from calendar import timegm import Tkinter as tk import ttk @@ -75,9 +76,6 @@ class AppWindow: self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) - # Special handling for overrideredict - self.w.bind("", self.onmap) - plug.load_plugins() if platform != 'darwin': @@ -242,6 +240,9 @@ class AppWindow: theme.register_highlight(self.station) theme.apply(self.w) + # Special handling for overrideredict + self.w.bind("", self.onmap) + # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) @@ -252,8 +253,7 @@ class AppWindow: hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring - monitor.set_callback('Dock', self.getandsend) - monitor.set_callback('Jump', self.system_change) + self.w.bind_all('<>', self.journal_event) # user-generated monitor.start(self.w) # First run @@ -394,9 +394,14 @@ class AppWindow: pass elif not data['commander'].get('docked'): - # signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up - if not self.status['text']: - self.status['text'] = _("You're not docked at a station!") + if not event and not retrying: + # Silently retry if we got here by 'Automatically update on docking' and the server hasn't caught up + self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True)) + return # early exit to avoid starting cooldown count + else: + # Signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up + if not self.status['text']: + self.status['text'] = _("You're not docked at a station!") else: # Finally - the data looks sane and we're docked at a station @@ -479,31 +484,62 @@ class AppWindow: except: pass - def system_change(self, event, timestamp, system, coordinates): + # Handle event(s) from the journal + def journal_event(self, event): + while True: + entry = monitor.get_entry() + if entry is None: + return + system_changed = monitor.system and self.system['text'] != monitor.system + station_changed = monitor.station and self.station['text'] != monitor.station - if self.system['text'] != system: - self.system['text'] = system + # Update main window + self.cmdr['text'] = monitor.cmdr or '' + self.system['text'] = monitor.system or '' + self.station['text'] = monitor.station or (EDDB.system(monitor.system) and self.STATION_UNDOCKED or '') - self.system['image'] = '' - self.station['text'] = EDDB.system(system) and self.STATION_UNDOCKED or '' + plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry) - plug.notify_system_changed(timestamp, system, coordinates) + if system_changed: + self.system['image'] = '' + timestamp = timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) - if config.getint('output') & config.OUT_SYS_EDSM: - try: - self.status['text'] = _('Sending data to EDSM...') - self.w.update_idletasks() - self.edsm.writelog(timestamp, system, coordinates) # Do EDSM lookup during EDSM export + # Backwards compatibility + plug.notify_system_changed(timestamp, monitor.system, monitor.coordinates) + + # Update EDSM if we have coordinates - i.e. Location or FSDJump events + if config.getint('output') & config.OUT_SYS_EDSM and monitor.coordinates: + try: + self.status['text'] = _('Sending data to EDSM...') + self.w.update_idletasks() + self.edsm.writelog(timestamp, monitor.system, monitor.coordinates) + self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') + except Exception as e: + if __debug__: print_exc() + self.status['text'] = unicode(e) + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() + else: + self.edsm.link(monitor.system) self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') - except Exception as e: - if __debug__: print_exc() - self.status['text'] = unicode(e) - if not config.getint('hotkey_mute'): - hotkeymgr.play_bad() - else: - self.edsm.link(system) - self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') - self.edsmpoll() + self.edsmpoll() + + # Send interesting events to EDDN + if (config.getint('output') & config.OUT_SYS_EDDN and monitor.cmdr and + (entry['event'] == 'FSDJump' and system_changed or + entry['event'] == 'Docked' and station_changed or + entry['event'] == 'Scan')): + # strip out properties disallowed by the schema + for thing in ['CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist']: + entry.pop(thing, None) + for thing in entry.keys(): + if thing.endswith('_Localised'): + entry.pop(thing, None) + eddn.export_journal_entry(monitor.cmdr, monitor.is_beta, entry) + + # Auto-Update after docking + if station_changed and config.getint('output') & (config.OUT_MKT_EDDN|config.OUT_MKT_MANUAL) == config.OUT_MKT_EDDN and entry['event'] == 'Docked': + self.w.after(int(SERVER_RETRY * 1000), self.getandsend) def edsmpoll(self): result = self.edsm.result diff --git a/PLUGINS.md b/PLUGINS.md index fe59cf4b..2a26117b 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -68,21 +68,23 @@ Once you have created your plugin and EDMC has loaded it there are two other fun Your events all get called on the main tkinter loop so be sure not to block for very long or the EDMC will appear to freeze. If you have a long running operation then you should take a look at how to do background updates in tkinter - http://effbot.org/zone/tkinter-threads.htm -### Arriving in a System +### Journal Entry -This gets called when EDMC uses the netlog to notice that you have arrived at a new star system. +This gets called when EDMC sees a new entry in the game's journal. ``` -def system_changed(timestamp, system, coordinates): - """ - We arrived at a new system! - """ - sys.stderr.write("{} {}".format(timestamp, system)) +def journal_entry(cmdr, system, station, entry): + if entry['event'] == 'FSDJump': + # We arrived at a new system! + if 'StarPos' in entry: + sys.stderr.write("Arrived at {} ({},{},{})\n".format(entry['StarSystem'], *tuple(entry['StarPos']))) + else: + sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) ``` ### Getting Commander Data -This gets called when EDMC has just fetched fresh data from Frontier's servers. +This gets called when EDMC has just fetched fresh Cmdr and station data from Frontier's servers. ``` def cmdr_data(data): diff --git a/README.md b/README.md index 49430c04..c517dbd7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ Elite: Dangerous Market Connector (EDMC) ======== -This app downloads your data and station data from the game [Elite: Dangerous](https://www.elitedangerous.com/) and, at your choice, either: +This app downloads your Cmdr's data, system, scan and station data from the game [Elite: Dangerous](https://www.elitedangerous.com/) and, at your choice, either: -* sends the station commodity market prices and other station data to the [Elite Dangerous Data Network](http://eddn-gateway.elite-markets.net/) (“EDDN”) from where you and others can use it via online trading and shopping tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. -* saves the station market prices to files on your computer that you can load into trading tools such as [Slopey's BPC Market Tool](https://forums.frontier.co.uk/showthread.php?t=76081), [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Inara](http://inara.cz), [mEDI's Elite Tools](https://github.com/mEDI-S/mEDI_s-Elite-Tools), etc. +* sends the station commodity market prices, other station data and system and scan data to the [Elite Dangerous Data Network](http://eddn-gateway.elite-markets.net/) (“EDDN”) from where you and others can use it via online trading, prospecting and shopping tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. +* saves the station commodity market prices to files on your computer that you can load into trading tools such as [Slopey's BPC Market Tool](https://forums.frontier.co.uk/showthread.php?t=76081), [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Inara](http://inara.cz), [mEDI's Elite Tools](https://github.com/mEDI-S/mEDI_s-Elite-Tools), etc. * saves a record of your ship loadout to files on your computer that you can load into outfitting tools such as [E:D Shipyard](http://www.edshipyard.com), [Coriolis](http://coriolis.io) or [Elite Trade Net](http://etn.io/). * sends your flight log to [Elite: Dangerous Star Map](http://www.edsm.net/). @@ -46,7 +46,7 @@ Setup The first time that you run the app you are prompted for your username and password. This is the same username and password combination that you use to log into the Elite: Dangerous launcher, and is required so that the Frontier servers can send the app *your* data and the data for the station that *you* are docked at. -You can also choose here what data to save (refer to the next section for details), whether to “Update” automatically on docking and/or with a hotkey, and whether to attach your Cmdr name or a [pseudo-anonymized](http://en.wikipedia.org/wiki/Pseudonymity) ID to the data. +You can also choose here what data to save (refer to the next section for details), whether to “Update” Cmdr and station data automatically on docking and/or with a hotkey, and whether to attach your Cmdr name or a [pseudo-anonymized](http://en.wikipedia.org/wiki/Pseudonymity) ID to the data. You will be prompted to authenticate with a “verification code”, which you will shortly receive by email from Frontier. Note that each “verification code” is one-time only - if you enter the code incorrectly or quit the app before @@ -59,8 +59,7 @@ option EDMarketConnector → Preferences (Mac) or File → Settings (Windows) an This app can save a variety of data in a variety of formats: -* Station data - * Elite Dangerous Data Network - sends station commodity market, outfitting and shipyard data to “[EDDN](http://eddn-gateway.elite-markets.net/)” from where you and others can use it via online trading tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. +* Market data * Slopey's BPC format file - saves commodity market data as files that you can load into [Slopey's BPC Market Tool](https://forums.frontier.co.uk/showthread.php?t=76081). * Trade Dangerous format file - saves commodity market data as files that you can load into [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home). * CSV format file - saves commodity market data as files that you can upload to [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Inara](http://inara.cz) or [mEDI's Elite Tools](https://github.com/mEDI-S/mEDI_s-Elite-Tools). @@ -72,6 +71,13 @@ By default these files will be placed in your Documents folder. Since this app w Some options work by reading the Elite: Dangerous game's log files. Normally this app will find the log files but if you find some options greyed-out then adjust the “E:D log file location” setting described [below](#doesnt-track-systems-visited). +### EDDN + +* Station data + * Sends station commodity market, outfitting and shipyard data to “[EDDN](http://eddn-gateway.elite-markets.net/)” from where you and others can use it via online trading tools such as [eddb](http://eddb.io/), [Elite Trade Net](http://etn.io/), [Inara](http://inara.cz), [ED-TD](http://ed-td.space/), [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/), [Roguey's](http://roguey.co.uk/elite-dangerous/), etc. +* System and scan data + * Sends general system information and the results of your detailed planet scans to “[EDDN](http://eddn-gateway.elite-markets.net/)” from where you and others can use it via online prospecting tools such as [eddb](http://eddb.io/), [Inara](http://inara.cz), etc. + ### EDSM You can send a record of your location to [Elite: Dangerous Star Map](http://www.edsm.net/) where you can view your flight log under My account → Exploration Logs and optionally add private comments about a system. You will need to register for an account and then follow the “[Elite Dangerous Star Map credentials](http://www.edsm.net/settings/api)” link to obtain your API key. diff --git a/config.py b/config.py index d8e43ef8..84b8950a 100644 --- a/config.py +++ b/config.py @@ -86,6 +86,7 @@ class Config: OUT_SYS_EDSM = 256 # OUT_SYS_AUTO = 512 # Now always automatic OUT_MKT_MANUAL = 1024 + OUT_SYS_EDDN = 2048 if platform=='darwin': diff --git a/eddn.py b/eddn.py index 48e59889..a986d1d4 100644 --- a/eddn.py +++ b/eddn.py @@ -32,7 +32,8 @@ class _EDDN: 'softwareVersion' : appversion, 'uploaderID' : config.getint('anonymous') and hashlib.md5(cmdr.encode('utf-8')).hexdigest() or cmdr.encode('utf-8'), } - msg['message']['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(config.getint('querytime') or int(time.time()))) + if not msg['message'].get('timestamp'): # already present in journal messages + msg['message']['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(config.getint('querytime') or int(time.time()))) r = self.session.post(self.UPLOAD, data=json.dumps(msg), timeout=timeout) if __debug__ and r.status_code != requests.codes.ok: @@ -94,5 +95,11 @@ class _EDDN: } }) + def export_journal_entry(self, cmdr, is_beta, entry): + self.send(cmdr, { + '$schemaRef' : 'http://schemas.elite-markets.net/eddn/journal/1' + (is_beta and '/test' or ''), + 'message' : entry + }) + # singleton eddn = _EDDN() diff --git a/monitor.py b/monitor.py index daa8022f..5645b660 100644 --- a/monitor.py +++ b/monitor.py @@ -1,4 +1,6 @@ import atexit +from collections import OrderedDict +import json import re import threading from os import listdir, pardir, rename, unlink, SEEK_SET, SEEK_CUR, SEEK_END @@ -6,8 +8,7 @@ from os.path import basename, exists, isdir, isfile, join from platform import machine import sys from sys import platform -from time import strptime, localtime, mktime, sleep, time -from datetime import datetime +from time import sleep if __debug__: from traceback import print_exc @@ -68,11 +69,18 @@ class EDLogs(FileSystemEventHandler): self.currentdir = None # The actual logdir that we're monitoring self.logfile = None self.observer = None - self.observed = None + self.observed = None # a watchdog ObservedWatch, or None if polling self.thread = None - self.callbacks = { 'Jump': None, 'Dock': None } - self.last_event = None # for communicating the Jump event + self.event_queue = [] # For communicating journal entries back to main thread + # Context for journal handling + self.version = None + self.is_beta = False + self.mode = None + self.cmdr = None + self.system = None + self.station = None + self.coordinates = None def set_callback(self, name, callback): if name in self.callbacks: @@ -89,9 +97,6 @@ class EDLogs(FileSystemEventHandler): self.stop() self.currentdir = logdir - self.root.bind_all('<>', self.jump) # user-generated - self.root.bind_all('<>', self.dock) # user-generated - # Set up a watchog observer. This is low overhead so is left running irrespective of whether monitoring is desired. # File system events are unreliable/non-existent over network drives on Linux. # We can't easily tell whether a path points to a network drive, so assume @@ -108,17 +113,17 @@ class EDLogs(FileSystemEventHandler): # Latest pre-existing logfile - e.g. if E:D is already running. Assumes logs sort alphabetically. try: - logfiles = sorted([x for x in listdir(logdir) if x.startswith('netLog.')]) - self.logfile = logfiles and join(logdir, logfiles[-1]) or None + logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.')]) + self.logfile = logfiles and join(self.currentdir, logfiles[-1]) or None except: self.logfile = None if __debug__: - print '%s "%s"' % (polling and 'Polling' or 'Monitoring', logdir) + print '%s "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir) print 'Start logfile "%s"' % self.logfile if not self.running(): - self.thread = threading.Thread(target = self.worker, name = 'netLog worker') + self.thread = threading.Thread(target = self.worker, name = 'Journal worker') self.thread.daemon = True self.thread.start() @@ -128,18 +133,19 @@ class EDLogs(FileSystemEventHandler): if __debug__: print 'Stopping monitoring' self.currentdir = None + self.version = self.mode = self.cmdr = self.system = self.station = self.coordinates = None + self.is_beta = False if self.observed: self.observed = None self.observer.unschedule_all() self.thread = None # Orphan the worker thread - will terminate at next poll - self.last_event = None def running(self): return self.thread and self.thread.is_alive() def on_created(self, event): # watchdog callback, e.g. client (re)started. - if not event.is_directory and basename(event.src_path).startswith('netLog.'): + if not event.is_directory and basename(event.src_path).startswith('Journal.'): self.logfile = event.src_path def worker(self): @@ -147,30 +153,16 @@ class EDLogs(FileSystemEventHandler): # event_generate() is the only safe way to poke the main thread from this thread: # https://mail.python.org/pipermail/tkinter-discuss/2013-November/003522.html - # e.g.: - # "{18:00:41} System:"Shinrarta Dezhra" StarPos:(55.719,17.594,27.156)ly NormalFlight\r\n" - # or with verboseLogging: - # "{17:20:18} System:"Shinrarta Dezhra" StarPos:(55.719,17.594,27.156)ly Body:69 RelPos:(0.334918,1.20754,1.23625)km NormalFlight\r\n" - # or: - # "... Supercruise\r\n" - # Note that system name may contain parantheses, e.g. "Pipe (stem) Sector PI-T c3-5". - regexp = re.compile(r'\{(.+)\} System:"(.+)" StarPos:\((.+),(.+),(.+)\)ly.* (\S+)') # (localtime, system, x, y, z, context) - - # e.g.: - # "{14:42:11} GetSafeUniversalAddress Station Count 1 moved 0 Docked Not Landed\r\n" - # or: - # "... Undocked Landed\r\n" - # Don't use the simpler "Commander Put ..." message since its more likely to be delayed. - dockre = re.compile(r'\{(.+)\} GetSafeUniversalAddress Station Count \d+ moved \d+ (\S+) ([^\r\n]+)') # (localtime, docked_status, landed_status) - - docked = False # Whether we're docked - updated = False # Whether we've sent an update since we docked - # Seek to the end of the latest log file logfile = self.logfile if logfile: loghandle = open(logfile, 'r') - loghandle.seek(0, SEEK_END) # seek to EOF + for line in loghandle: + try: + self.parse_entry(line) # Some events are of interest even in the past + except: + if __debug__: + print 'Invalid journal entry "%s"' % repr(line) else: loghandle = None @@ -179,19 +171,13 @@ class EDLogs(FileSystemEventHandler): while True: - if docked and not updated and not config.getint('output') & config.OUT_MKT_MANUAL: - self.root.event_generate('<>', when="tail") - updated = True - if __debug__: - print "%s :\t%s %s" % ('Updated', docked and " docked" or "!docked", updated and " updated" or "!updated") - # Check whether new log file started, e.g. client (re)started. if emitter and emitter.is_alive(): newlogfile = self.logfile # updated by on_created watchdog callback else: # Poll try: - logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('netLog.')]) + logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.')]) newlogfile = logfiles and join(self.currentdir, logfiles[-1]) or None except: if __debug__: print_exc() @@ -207,37 +193,11 @@ class EDLogs(FileSystemEventHandler): print 'New logfile "%s"' % logfile if logfile: - system = visited = coordinates = None loghandle.seek(0, SEEK_CUR) # reset EOF flag - for line in loghandle: - match = regexp.match(line) - if match: - (visited, system, x, y, z, context) = match.groups() - if system == 'ProvingGround': - system = 'CQC' - coordinates = (float(x), float(y), float(z)) - else: - match = dockre.match(line) - if match: - if match.group(2) == 'Undocked': - docked = updated = False - elif match.group(2) == 'Docked': - docked = True - # do nothing now in case the API server is lagging, but update on next poll - if __debug__: - print "%s :\t%s %s" % (match.group(2), docked and " docked" or "!docked", updated and " updated" or "!updated") - - if system and not docked and config.getint('output'): - # Convert local time string to UTC date and time - visited_struct = strptime(visited, '%H:%M:%S') - now = localtime() - if now.tm_hour == 0 and visited_struct.tm_hour == 23: - # Crossed midnight between timestamp and poll - now = localtime(time()-12*60*60) # yesterday - time_struct = datetime(now.tm_year, now.tm_mon, now.tm_mday, visited_struct.tm_hour, visited_struct.tm_min, visited_struct.tm_sec).timetuple() # still local time - self.last_event = (mktime(time_struct), system, coordinates) - self.root.event_generate('<>', when="tail") + self.event_queue.append(line) + if self.event_queue: + self.root.event_generate('<>', when="tail") sleep(self._POLL) @@ -245,15 +205,39 @@ class EDLogs(FileSystemEventHandler): if threading.current_thread() != self.thread: return # Terminate - def jump(self, event): - # Called from Tkinter's main loop - if self.callbacks['Jump'] and self.last_event: - self.callbacks['Jump'](event, *self.last_event) + def parse_entry(self, line): + try: + entry = json.loads(line, object_pairs_hook=OrderedDict) # Preserve property order because why not? + entry['timestamp'] # we expect this to exist + if entry['event'] == 'Fileheader': # XXX or 'fileheader' ? + self.version = entry['gameversion'] + self.is_beta = 'beta' in entry['gameversion'].lower() + elif entry['event'] == 'LoadGame': + self.cmdr = entry['Commander'] + self.mode = entry['GameMode'] + elif entry['event'] == 'NewCommander': + self.cmdr = entry['Name'] + elif entry['event'] in ['Undocked']: + self.station = None + self.coordinates = None + elif entry['event'] in ['Location', 'FSDJump', 'Docked']: + if 'StarPos' in entry: + self.coordinates = tuple(entry['StarPos']) + elif self.system != entry['StarSystem']: + self.coordinates = None # Docked event doesn't include coordinates + self.system = entry['StarSystem'] == 'ProvingGround' and 'CQC' or entry['StarSystem'] + self.station = entry.get('StationName') # May be None + return entry + except: + if __debug__: + print 'Invalid journal entry "%s"' % repr(line) + return { 'event': None } - def dock(self, event): - # Called from Tkinter's main loop - if self.callbacks['Dock']: - self.callbacks['Dock']() + def get_entry(self): + if not self.event_queue: + return None + else: + return self.parse_entry(self.event_queue.pop(0)) def is_valid_logdir(self, path): return self._is_valid_logdir(path) diff --git a/plug.py b/plug.py index 6920b90f..7f96c354 100644 --- a/plug.py +++ b/plug.py @@ -87,12 +87,33 @@ def get_plugin_pref(plugname, parent): return None +def notify_journal_entry(cmdr, system, station, entry): + """ + Send a journal entry to each plugin. + :param cmdr: The Cmdr name, or None if not yet known + :param system: The current system, or None if not yet known + :param station: The current station, or None if not docked or not yet known + :param entry: The journal entry as a dictionary + :return: + """ + for plugname in PLUGINS: + journal_entry = _get_plugin_func(plugname, "journal_entry") + if journal_entry: + try: + # Pass a copy of the journal entry in case the callee modifies it + journal_entry(cmdr, system, station, dict(entry)) + except Exception as plugerr: + print plugerr + + def notify_system_changed(timestamp, system, coordinates): """ Send notification data to each plugin when we arrive at a new system. :param timestamp: :param system: :return: + deprecated:: 2.2 + Use :func:`journal_entry` with the 'FSDJump' event. """ for plugname in PLUGINS: system_changed = _get_plugin_func(plugname, "system_changed") diff --git a/plugins/About/load.py b/plugins/About/load.py index 5441f83f..1dbc2e02 100644 --- a/plugins/About/load.py +++ b/plugins/About/load.py @@ -45,18 +45,21 @@ def plugin_app(parent): return plugin_app.status -def system_changed(timestamp, system, coordinates): +def journal_entry(cmdr, system, station, entry): """ - Arrived in a new System - :param timestamp: when we arrived - :param system: the name of the system - :param coordinates: tuple of (x,y,z) ly relative to Sol, or None if unknown + E:D client made a journal entry + :param cmdr: The Cmdr name, or None if not yet known + :param system: The current system, or None if not yet known + :param station: The current station, or None if not docked or not yet known + :param entry: The journal entry as a dictionary :return: """ - if coordinates: - sys.stderr.write("Arrived at {} ({},{},{})\n".format(system, *coordinates)) - else: - sys.stderr.write("Arrived at {}\n".format(system)) + if entry['event'] == 'FSDJump': + # We arrived at a new system! + if 'StarPos' in entry: + sys.stderr.write("Arrived at {} ({},{},{})\n".format(entry['StarSystem'], *tuple(entry['StarPos']))) + else: + sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) def cmdr_data(data): diff --git a/prefs.py b/prefs.py index 90c34e7f..7f460efb 100644 --- a/prefs.py +++ b/prefs.py @@ -113,11 +113,9 @@ class PreferencesDialog(tk.Toplevel): outframe = nb.Frame(notebook) outframe.columnconfigure(0, weight=1) - output = config.getint('output') or (config.OUT_MKT_EDDN | config.OUT_SHIP_EDS) # default settings + output = config.getint('output') or (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN | config.OUT_SHIP_EDS) # default settings nb.Label(outframe, text=_('Please choose what data to save')).grid(columnspan=2, padx=PADX, sticky=tk.W) - self.out_eddn= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1) - nb.Checkbutton(outframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.out_eddn, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_csv = tk.IntVar(value = (output & config.OUT_MKT_CSV ) and 1) nb.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_bpc = tk.IntVar(value = (output & config.OUT_MKT_BPC ) and 1) @@ -149,13 +147,27 @@ class PreferencesDialog(tk.Toplevel): notebook.add(outframe, text=_('Output')) # Tab heading in settings + eddnframe = nb.Frame(notebook) + + HyperlinkLabel(eddnframe, text='Elite Dangerous Data Network', background=nb.Label().cget('background'), url='https://github.com/jamesremuscat/EDDN/wiki', underline=True).grid(padx=PADX, sticky=tk.W) # Don't translate + self.eddn_station= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1) + nb.Checkbutton(eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.eddn_station, command=self.outvarchanged).grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) # Output setting + self.eddn_auto_button = nb.Checkbutton(eddnframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting + self.eddn_auto_button.grid(padx=BUTTONX, sticky=tk.W) + self.eddn_system = tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1) + self.eddn_system_button = nb.Checkbutton(eddnframe, text=_('Send system and scan data to the Elite Dangerous Data Network'), variable=self.eddn_system, command=self.outvarchanged) # Output setting new in E:D 2.2 + self.eddn_system_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) + + notebook.add(eddnframe, text='EDDN') # Not translated + + edsmframe = nb.Frame(notebook) edsmframe.columnconfigure(1, weight=1) HyperlinkLabel(edsmframe, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate self.edsm_log = tk.IntVar(value = (output & config.OUT_SYS_EDSM) and 1) self.edsm_log_button = nb.Checkbutton(edsmframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.edsm_log, command=self.outvarchanged) - self.edsm_log_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) + self.edsm_log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) nb.Label(edsmframe).grid(sticky=tk.W) # big spacer self.edsm_label = HyperlinkLabel(edsmframe, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings @@ -196,7 +208,8 @@ class PreferencesDialog(tk.Toplevel): _('Browse...')), # Folder selection button on Windows command = lambda:self.filebrowse(_('E:D log file location'), self.logdir)) self.logbutton.grid(row=10, column=2, padx=PADX, sticky=tk.EW) - nb.Button(configframe, text=_('Default'), command=self.logdir_reset, state = monitor.logdir and tk.NORMAL or tk.DISABLED).grid(column=2, padx=PADX, pady=(5,0), sticky=tk.EW) # Appearance theme and language setting + if monitor.logdir: + nb.Button(configframe, text=_('Default'), command=self.logdir_reset, state = monitor.logdir and tk.NORMAL or tk.DISABLED).grid(column=2, padx=PADX, pady=(5,0), sticky=tk.EW) # Appearance theme and language setting if platform == 'win32': ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) @@ -298,11 +311,14 @@ class PreferencesDialog(tk.Toplevel): logvalid = monitor.is_valid_logdir(logdir) local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship_eds.get() or self.out_ship_coriolis.get() - self.out_auto_button['state'] = (local or self.out_eddn.get()) and logvalid and tk.NORMAL or tk.DISABLED + self.out_auto_button['state'] = local and logvalid and tk.NORMAL or tk.DISABLED self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED + self.eddn_auto_button['state'] = self.eddn_station.get() and logvalid and tk.NORMAL or tk.DISABLED + self.eddn_system_button['state']= logvalid and tk.NORMAL or tk.DISABLED + self.edsm_log_button['state'] = logvalid and tk.NORMAL or tk.DISABLED edsm_state = logvalid and self.edsm_log.get() and tk.NORMAL or tk.DISABLED self.edsm_label['state'] = edsm_state @@ -425,13 +441,14 @@ class PreferencesDialog(tk.Toplevel): config.set('password', self.password.get().strip()) config.set('output', - (self.out_eddn.get() and config.OUT_MKT_EDDN) + (self.out_bpc.get() and config.OUT_MKT_BPC) + (self.out_td.get() and config.OUT_MKT_TD) + (self.out_csv.get() and config.OUT_MKT_CSV) + (config.OUT_MKT_MANUAL if not self.out_auto.get() else 0) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) + + (self.eddn_station.get() and config.OUT_MKT_EDDN) + + (self.eddn_system.get() and config.OUT_SYS_EDDN) + (self.edsm_log.get() and config.OUT_SYS_EDSM)) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get())