From cc4390de4907537ceb1409706d8b10461d374a82 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sat, 17 Nov 2018 18:19:20 +0000 Subject: [PATCH] Switch EDDN integration to a plugin --- EDMarketConnector.py | 163 +++--------------- EDMarketConnector.wxs | 4 + L10n/en.template | 10 +- eddn.py | 231 ------------------------- plugins/eddn.py | 382 ++++++++++++++++++++++++++++++++++++++++++ prefs.py | 36 +--- setup.py | 2 +- 7 files changed, 423 insertions(+), 405 deletions(-) delete mode 100644 eddn.py create mode 100644 plugins/eddn.py diff --git a/EDMarketConnector.py b/EDMarketConnector.py index f0ab0606..9350fd4c 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -48,7 +48,6 @@ import companion import commodity from commodity import COMMODITY_CSV import td -import eddn import stats import prefs import plug @@ -60,11 +59,6 @@ from theme import theme SERVER_RETRY = 5 # retry pause for Companion servers [s] -# Limits on local clock drift from EDDN gateway -DRIFT_THRESHOLD = 3 * 60 -TZ_THRESHOLD = 30 * 60 -CLOCK_THRESHOLD = 11 * 60 * 60 + TZ_THRESHOLD - class AppWindow: @@ -76,7 +70,6 @@ class AppWindow: def __init__(self, master): self.holdofftime = config.getint('querytime') + companion.holdoff - self.eddn = eddn.EDDN(self) self.w = master self.w.title(applongname) @@ -312,10 +305,6 @@ class AppWindow: if keyring.get_keyring().priority < 1: self.status['text'] = 'Warning: Storing passwords as text' # Shouldn't happen unless no secure storage on Linux - # Try to obtain exclusive lock on journal cache, even if we don't need it yet - if not self.eddn.load(): - self.status['text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing - # callback after the Preferences dialog is applied def postprefs(self, dologin=True): self.prefsdialog = None @@ -471,57 +460,26 @@ class AppWindow: if err: play_bad = True - # Export ship for backwards compatibility even 'though it's no longer derived from cAPI - if config.getint('output') & config.OUT_SHIP: - monitor.export_ship() - - if not (config.getint('output') & ~config.OUT_SHIP & config.OUT_STATION_ANY): - # no station data requested - we're done - pass - - elif not data['commander'].get('docked'): - if not self.status['text']: - # Signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up - self.status['text'] = _("You're not docked at a station!") - play_bad = True - - else: - # Finally - the data looks sane and we're docked at a station - - # No EDDN output? - if (config.getint('output') & config.OUT_MKT_EDDN) and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): # Ignore possibly missing shipyard info + # Export market data + if config.getint('output') & (config.OUT_STATION_ANY): + if not data['commander'].get('docked'): + if not self.status['text']: + # Signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up + self.status['text'] = _("You're not docked at a station!") + play_bad = True + elif (config.getint('output') & config.OUT_MKT_EDDN) and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): # Ignore possibly missing shipyard info if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") - - # No market output? - elif not (config.getint('output') & config.OUT_MKT_EDDN) and not data['lastStarport'].get('commodities'): + elif not data['lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _("Station doesn't have a market!") - - else: - if data['lastStarport'].get('commodities') and config.getint('output') & (config.OUT_MKT_CSV|config.OUT_MKT_TD): - # Fixup anomalies in the commodity data - fixed = companion.fixup(data) - - if config.getint('output') & config.OUT_MKT_CSV: - commodity.export(fixed, COMMODITY_CSV) - if config.getint('output') & config.OUT_MKT_TD: - td.export(fixed) - - if config.getint('output') & config.OUT_MKT_EDDN: - old_status = self.status['text'] - if not old_status: - self.status['text'] = _('Sending data to EDDN...') - self.w.update_idletasks() - self.eddn.export_commodities(data, monitor.is_beta) - self.eddn.export_outfitting(data, monitor.is_beta) - if data['lastStarport'].get('ships', {}).get('shipyard_list'): - self.eddn.export_shipyard(data, monitor.is_beta) - elif data['lastStarport'].get('services', {}).get('shipyard'): - # API is flakey about shipyard info - silently retry if missing (<1s is usually sufficient - 5s for margin). - self.w.after(int(SERVER_RETRY * 1000), lambda:self.retry_for_shipyard(2)) - if not old_status: - self.status['text'] = '' + elif config.getint('output') & (config.OUT_MKT_CSV|config.OUT_MKT_TD): + # Fixup anomalies in the commodity data + fixed = companion.fixup(data) + if config.getint('output') & config.OUT_MKT_CSV: + commodity.export(fixed, COMMODITY_CSV) + if config.getint('output') & config.OUT_MKT_TD: + td.export(fixed) except companion.VerificationRequired: if not self.authdialog: @@ -537,11 +495,6 @@ class AppWindow: self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True)) return # early exit to avoid starting cooldown count - except requests.RequestException as e: - if __debug__: print_exc() - self.status['text'] = _("Error: Can't connect to EDDN") - play_bad = True - except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) @@ -630,6 +583,15 @@ class AppWindow: if not entry['event'] or not monitor.mode: return # Startup or in CQC + if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: + # Can start dashboard monitoring + if not dashboard.start(self.w, monitor.started): + print "Can't start Status monitoring" + + # Export loadout + if entry['event'] == 'Loadout' and not monitor.state['Captain'] and config.getint('output') & config.OUT_SHIP: + monitor.export_ship() + # Plugins err = plug.notify_journal_entry(monitor.cmdr, monitor.is_beta, monitor.system, monitor.station, entry, monitor.state) if err: @@ -637,67 +599,10 @@ class AppWindow: if not config.getint('hotkey_mute'): hotkeymgr.play_bad() - if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: - # Can start dashboard monitoring - if not dashboard.start(self.w, monitor.started): - print "Can't start Status monitoring" - - # Don't send to EDDN while on crew - if monitor.state['Captain']: - return - - # Plugin backwards compatibility - if monitor.mode and entry['event'] in ['StartUp', 'Location', 'FSDJump']: - plug.notify_system_changed(timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), monitor.system, monitor.coordinates) - - # Export loadout - if monitor.mode and entry['event'] == 'Loadout' and config.getint('output') & config.OUT_SHIP: - monitor.export_ship() - # Auto-Update after docking - if monitor.mode and monitor.station and entry['event'] in ['StartUp', 'Location', 'Docked'] and not config.getint('output') & config.OUT_MKT_MANUAL and config.getint('output') & config.OUT_STATION_ANY: + if entry['event'] in ['StartUp', 'Location', 'Docked'] and monitor.station and not config.getint('output') & config.OUT_MKT_MANUAL and config.getint('output') & config.OUT_STATION_ANY: self.w.after(int(SERVER_RETRY * 1000), self.getandsend) - # Send interesting events to EDDN - try: - if (config.getint('output') & config.OUT_SYS_EDDN and monitor.cmdr and monitor.mode and - (entry['event'] == 'Location' or - entry['event'] == 'FSDJump' or - entry['event'] == 'Docked' or - entry['event'] == 'Scan' and monitor.system and monitor.coordinates)): - # strip out properties disallowed by the schema - for thing in ['ActiveFine', 'CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist', 'Latitude', 'Longitude', 'Wanted']: - entry.pop(thing, None) - for faction in entry.get('Factions', []): - faction.pop('MyReputation', None) - - # add planet to Docked event for planetary stations if known - if entry['event'] == 'Docked' and monitor.planet: - entry['Body'] = monitor.planet - entry['BodyType'] = 'Planet' - - # add mandatory StarSystem, StarPos and SystemAddress properties to Scan events - if 'StarSystem' not in entry: - entry['StarSystem'] = monitor.system - if 'StarPos' not in entry: - entry['StarPos'] = list(monitor.coordinates) - if 'SystemAddress' not in entry and monitor.systemaddress: - entry['SystemAddress'] = monitor.systemaddress - - self.eddn.export_journal_entry(monitor.cmdr, monitor.is_beta, self.filter_localised(entry)) - - except requests.exceptions.RequestException as e: - if __debug__: print_exc() - self.status['text'] = _("Error: Can't connect to EDDN") - if not config.getint('hotkey_mute'): - hotkeymgr.play_bad() - - except Exception as e: - if __debug__: print_exc() - self.status['text'] = unicode(e) - if not config.getint('hotkey_mute'): - hotkeymgr.play_bad() - # Handle Status event def dashboard_event(self, event): entry = dashboard.status @@ -726,21 +631,6 @@ class AppWindow: def station_url(self, station): return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station) - - # Recursively filter '*_Localised' keys from dict - def filter_localised(self, d): - filtered = OrderedDict() - for k, v in d.iteritems(): - if k.endswith('_Localised'): - pass - elif hasattr(v, 'iteritems'): # dict -> recurse - filtered[k] = self.filter_localised(v) - elif isinstance(v, list): # list of dicts -> recurse - filtered[k] = [self.filter_localised(x) if hasattr(x, 'iteritems') else x for x in v] - else: - filtered[k] = v - return filtered - def cooldown(self): if time() < self.holdofftime: self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window @@ -800,7 +690,6 @@ class AppWindow: dashboard.close() monitor.close() plug.notify_stop() - self.eddn.close() self.updater.close() companion.session.close() config.close() diff --git a/EDMarketConnector.wxs b/EDMarketConnector.wxs index 79674cf5..da1d3d9a 100644 --- a/EDMarketConnector.wxs +++ b/EDMarketConnector.wxs @@ -246,6 +246,9 @@ + + + @@ -490,6 +493,7 @@ + diff --git a/L10n/en.template b/L10n/en.template index 4a8e05b5..3f42fe75 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -103,7 +103,7 @@ /* Appearance theme and language setting. [l10n.py] */ "Default" = "Default"; -/* Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2. [prefs.py] */ +/* Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2. [eddn.py] */ "Delay sending until docked" = "Delay sending until docked"; /* List of plugins in settings. [prefs.py] */ @@ -142,7 +142,7 @@ /* Trade rank. [stats.py] */ "Entrepreneur" = "Entrepreneur"; -/* [EDMarketConnector.py] */ +/* [eddn.py] */ "Error: Can't connect to EDDN" = "Error: Can't connect to EDDN"; /* [edsm.py] */ @@ -436,13 +436,13 @@ /* [inara.py] */ "Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara"; -/* Output setting. [prefs.py] */ +/* Output setting. [eddn.py] */ "Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network"; -/* Output setting new in E:D 2.2. [prefs.py] */ +/* Output setting new in E:D 2.2. [eddn.py] */ "Send system and scan data to the Elite Dangerous Data Network" = "Send system and scan data to the Elite Dangerous Data Network"; -/* [EDMarketConnector.py] */ +/* [eddn.py] */ "Sending data to EDDN..." = "Sending data to EDDN..."; /* Empire rank. [stats.py] */ diff --git a/eddn.py b/eddn.py deleted file mode 100644 index 2f4eb66a..00000000 --- a/eddn.py +++ /dev/null @@ -1,231 +0,0 @@ -# Export to EDDN - -from collections import OrderedDict -import json -import numbers -from os import SEEK_SET, SEEK_CUR, SEEK_END -from os.path import exists, join -from platform import system -import re -import requests -from sys import platform -import time -import uuid - -if platform != 'win32': - from fcntl import lockf, LOCK_EX, LOCK_NB - -if __debug__: - from traceback import print_exc - -from config import applongname, appversion, config -from companion import category_map - - -timeout= 10 # requests timeout -module_re = re.compile('^Hpt_|^Int_|_Armour_') - -replayfile = None # For delayed messages - -class EDDN: - - ### SERVER = 'http://localhost:8081' # testing - SERVER = 'https://eddn.edcd.io:4430' - UPLOAD = '%s/upload/' % SERVER - REPLAYPERIOD = 400 # Roughly two messages per second, accounting for send delays [ms] - REPLAYFLUSH = 20 # Update log on disk roughly every 10 seconds - - def __init__(self, parent): - self.parent = parent - self.session = requests.Session() - self.replaylog = [] - - def load(self): - # Try to obtain exclusive access to the journal cache - global replayfile - filename = join(config.app_dir, 'replay.jsonl') - try: - try: - # Try to open existing file - replayfile = open(filename, 'r+') - except: - if exists(filename): - raise # Couldn't open existing file - else: - replayfile = open(filename, 'w+') # Create file - if platform != 'win32': # open for writing is automatically exclusive on Windows - lockf(replayfile, LOCK_EX|LOCK_NB) - except: - if __debug__: print_exc() - if replayfile: - replayfile.close() - replayfile = None - return False - self.replaylog = [line.strip() for line in replayfile] - return True - - def flush(self): - replayfile.seek(0, SEEK_SET) - replayfile.truncate() - for line in self.replaylog: - replayfile.write('%s\n' % line) - replayfile.flush() - - def close(self): - global replayfile - if replayfile: - replayfile.close() - replayfile = None - - def send(self, cmdr, msg): - if config.getint('anonymous'): - uploaderID = config.get('uploaderID') - if not uploaderID: - uploaderID = uuid.uuid4().hex - config.set('uploaderID', uploaderID) - else: - uploaderID = cmdr.encode('utf-8') - - msg = OrderedDict([ - ('$schemaRef', msg['$schemaRef']), - ('header', OrderedDict([ - ('softwareName', '%s [%s]' % (applongname, platform=='darwin' and "Mac OS" or system())), - ('softwareVersion', appversion), - ('uploaderID', uploaderID), - ])), - ('message', msg['message']), - ]) - - r = self.session.post(self.UPLOAD, data=json.dumps(msg), timeout=timeout) - if __debug__ and r.status_code != requests.codes.ok: - print 'Status\t%s' % r.status_code - print 'URL\t%s' % r.url - print 'Headers\t%s' % r.headers - print ('Content:\n%s' % r.text).encode('utf-8') - r.raise_for_status() - - def sendreplay(self): - if not replayfile: - return # Probably closing app - - if not self.replaylog: - self.parent.status['text'] = '' - return - - if len(self.replaylog) == 1: - self.parent.status['text'] = _('Sending data to EDDN...') - else: - self.parent.status['text'] = '%s [%d]' % (_('Sending data to EDDN...').replace('...',''), len(self.replaylog)) - self.parent.w.update_idletasks() - try: - cmdr, msg = json.loads(self.replaylog[0], object_pairs_hook=OrderedDict) - except: - # Couldn't decode - shouldn't happen! - if __debug__: - print self.replaylog[0] - print_exc() - self.replaylog.pop(0) # Discard and continue - else: - # Rewrite old schema name - if msg['$schemaRef'].startswith('http://schemas.elite-markets.net/eddn/'): - msg['$schemaRef'] = 'https://eddn.edcd.io/schemas/' + msg['$schemaRef'][38:] - try: - self.send(cmdr, msg) - self.replaylog.pop(0) - if not len(self.replaylog) % self.REPLAYFLUSH: - self.flush() - except requests.exceptions.RequestException as e: - if __debug__: print_exc() - self.parent.status['text'] = _("Error: Can't connect to EDDN") - return # stop sending - except Exception as e: - if __debug__: print_exc() - self.parent.status['text'] = unicode(e) - return # stop sending - - self.parent.w.after(self.REPLAYPERIOD, self.sendreplay) - - def export_commodities(self, data, is_beta): - commodities = [] - for commodity in data['lastStarport'].get('commodities') or []: - if (category_map.get(commodity['categoryname'], True) and # Check marketable - not commodity.get('legality')): # check not prohibited - commodities.append(OrderedDict([ - ('name', commodity['name']), - ('meanPrice', int(commodity['meanPrice'])), - ('buyPrice', int(commodity['buyPrice'])), - ('stock', int(commodity['stock'])), - ('stockBracket', commodity['stockBracket']), - ('sellPrice', int(commodity['sellPrice'])), - ('demand', int(commodity['demand'])), - ('demandBracket', commodity['demandBracket']), - ])) - if commodity['statusFlags']: - commodities[-1]['statusFlags'] = commodity['statusFlags'] - - # Don't send empty commodities list - schema won't allow it - if commodities: - message = OrderedDict([ - ('timestamp', data['timestamp']), - ('systemName', data['lastSystem']['name']), - ('stationName', data['lastStarport']['name']), - ('marketId', data['lastStarport']['id']), - ('commodities', commodities), - ]) - if 'economies' in data['lastStarport']: - message['economies'] = sorted([x for x in (data['lastStarport']['economies'] or {}).itervalues()]) - if 'prohibited' in data['lastStarport']: - message['prohibited'] = sorted([x for x in (data['lastStarport']['prohibited'] or {}).itervalues()]) - self.send(data['commander']['name'], { - '$schemaRef' : 'https://eddn.edcd.io/schemas/commodity/3' + (is_beta and '/test' or ''), - 'message' : message, - }) - - def export_outfitting(self, data, is_beta): - # Don't send empty modules list - schema won't allow it - if data['lastStarport'].get('modules'): - self.send(data['commander']['name'], { - '$schemaRef' : 'https://eddn.edcd.io/schemas/outfitting/2' + (is_beta and '/test' or ''), - 'message' : OrderedDict([ - ('timestamp', data['timestamp']), - ('systemName', data['lastSystem']['name']), - ('stationName', data['lastStarport']['name']), - ('marketId', data['lastStarport']['id']), - ('modules', sorted([module['name'] for module in data['lastStarport']['modules'].itervalues() if module_re.search(module['name']) and module.get('sku') in [None, 'ELITE_HORIZONS_V_PLANETARY_LANDINGS'] and module['name'] != 'Int_PlanetApproachSuite'])), - ]), - }) - - def export_shipyard(self, data, is_beta): - # Don't send empty ships list - shipyard data is only guaranteed present if user has visited the shipyard. - if data['lastStarport'].get('ships', {}).get('shipyard_list'): - self.send(data['commander']['name'], { - '$schemaRef' : 'https://eddn.edcd.io/schemas/shipyard/2' + (is_beta and '/test' or ''), - 'message' : OrderedDict([ - ('timestamp', data['timestamp']), - ('systemName', data['lastSystem']['name']), - ('stationName', data['lastStarport']['name']), - ('marketId', data['lastStarport']['id']), - ('ships', sorted([ship['name'] for ship in data['lastStarport']['ships']['shipyard_list'].values() + data['lastStarport']['ships']['unavailable_list']])), - ]), - }) - - def export_journal_entry(self, cmdr, is_beta, entry): - msg = { - '$schemaRef' : 'https://eddn.edcd.io/schemas/journal/1' + (is_beta and '/test' or ''), - 'message' : entry - } - if replayfile or self.load(): - # Store the entry - self.replaylog.append(json.dumps([cmdr.encode('utf-8'), msg])) - replayfile.write('%s\n' % self.replaylog[-1]) - - if (entry['event'] == 'Docked' or - (entry['event'] == 'Location' and entry['Docked']) or - not (config.getint('output') & config.OUT_SYS_DELAY)): - self.parent.w.after(self.REPLAYPERIOD, self.sendreplay) # Try to send this and previous entries - else: - # Can't access replay file! Send immediately. - self.parent.status['text'] = _('Sending data to EDDN...') - self.parent.w.update_idletasks() - self.send(cmdr, msg) - self.parent.status['text'] = '' diff --git a/plugins/eddn.py b/plugins/eddn.py new file mode 100644 index 00000000..340cb572 --- /dev/null +++ b/plugins/eddn.py @@ -0,0 +1,382 @@ +# Export to EDDN + +from collections import OrderedDict +import json +import numbers +from os import SEEK_SET, SEEK_CUR, SEEK_END +from os.path import exists, join +from platform import system +import re +import requests +import sys +import time +import uuid + +import Tkinter as tk +from ttkHyperlinkLabel import HyperlinkLabel +import myNotebook as nb + +if sys.platform != 'win32': + from fcntl import lockf, LOCK_EX, LOCK_NB + +if __debug__: + from traceback import print_exc + +from config import applongname, appversion, config +from companion import category_map + + +this = sys.modules[__name__] # For holding module globals + +# Track location to add to Journal events +this.systemaddress = None +this.coordinates = None +this.planet = None + + +class EDDN: + + ### SERVER = 'http://localhost:8081' # testing + SERVER = 'https://eddn.edcd.io:4430' + UPLOAD = '%s/upload/' % SERVER + REPLAYPERIOD = 400 # Roughly two messages per second, accounting for send delays [ms] + REPLAYFLUSH = 20 # Update log on disk roughly every 10 seconds + TIMEOUT= 10 # requests timeout + MODULE_RE = re.compile('^Hpt_|^Int_|_Armour_') + + def __init__(self, parent): + self.parent = parent + self.session = requests.Session() + self.replayfile = None # For delayed messages + self.replaylog = [] + + def load(self): + # Try to obtain exclusive access to the journal cache + filename = join(config.app_dir, 'replay.jsonl') + try: + try: + # Try to open existing file + self.replayfile = open(filename, 'r+') + except: + if exists(filename): + raise # Couldn't open existing file + else: + self.replayfile = open(filename, 'w+') # Create file + if sys.platform != 'win32': # open for writing is automatically exclusive on Windows + lockf(self.replayfile, LOCK_EX|LOCK_NB) + except: + if __debug__: print_exc() + if self.replayfile: + self.replayfile.close() + self.replayfile = None + return False + self.replaylog = [line.strip() for line in self.replayfile] + return True + + def flush(self): + self.replayfile.seek(0, SEEK_SET) + self.replayfile.truncate() + for line in self.replaylog: + self.replayfile.write('%s\n' % line) + self.replayfile.flush() + + def close(self): + if self.replayfile: + self.replayfile.close() + self.replayfile = None + + def send(self, cmdr, msg): + if config.getint('anonymous'): + uploaderID = config.get('uploaderID') + if not uploaderID: + uploaderID = uuid.uuid4().hex + config.set('uploaderID', uploaderID) + else: + uploaderID = cmdr.encode('utf-8') + + msg = OrderedDict([ + ('$schemaRef', msg['$schemaRef']), + ('header', OrderedDict([ + ('softwareName', '%s [%s]' % (applongname, sys.platform=='darwin' and "Mac OS" or system())), + ('softwareVersion', appversion), + ('uploaderID', uploaderID), + ])), + ('message', msg['message']), + ]) + + r = self.session.post(self.UPLOAD, data=json.dumps(msg), timeout=self.TIMEOUT) + if __debug__ and r.status_code != requests.codes.ok: + print 'Status\t%s' % r.status_code + print 'URL\t%s' % r.url + print 'Headers\t%s' % r.headers + print ('Content:\n%s' % r.text).encode('utf-8') + r.raise_for_status() + + def sendreplay(self): + if not self.replayfile: + return # Probably closing app + + status = self.parent.children['status'] + + if not self.replaylog: + status['text'] = '' + return + + if len(self.replaylog) == 1: + status['text'] = _('Sending data to EDDN...') + else: + status['text'] = '%s [%d]' % (_('Sending data to EDDN...').replace('...',''), len(self.replaylog)) + self.parent.update_idletasks() + try: + cmdr, msg = json.loads(self.replaylog[0], object_pairs_hook=OrderedDict) + except: + # Couldn't decode - shouldn't happen! + if __debug__: + print self.replaylog[0] + print_exc() + self.replaylog.pop(0) # Discard and continue + else: + # Rewrite old schema name + if msg['$schemaRef'].startswith('http://schemas.elite-markets.net/eddn/'): + msg['$schemaRef'] = 'https://eddn.edcd.io/schemas/' + msg['$schemaRef'][38:] + try: + self.send(cmdr, msg) + self.replaylog.pop(0) + if not len(self.replaylog) % self.REPLAYFLUSH: + self.flush() + except requests.exceptions.RequestException as e: + if __debug__: print_exc() + status['text'] = _("Error: Can't connect to EDDN") + return # stop sending + except Exception as e: + if __debug__: print_exc() + status['text'] = unicode(e) + return # stop sending + + self.parent.after(self.REPLAYPERIOD, self.sendreplay) + + def export_commodities(self, data, is_beta): + commodities = [] + for commodity in data['lastStarport'].get('commodities') or []: + if (category_map.get(commodity['categoryname'], True) and # Check marketable + not commodity.get('legality')): # check not prohibited + commodities.append(OrderedDict([ + ('name', commodity['name']), + ('meanPrice', int(commodity['meanPrice'])), + ('buyPrice', int(commodity['buyPrice'])), + ('stock', int(commodity['stock'])), + ('stockBracket', commodity['stockBracket']), + ('sellPrice', int(commodity['sellPrice'])), + ('demand', int(commodity['demand'])), + ('demandBracket', commodity['demandBracket']), + ])) + if commodity['statusFlags']: + commodities[-1]['statusFlags'] = commodity['statusFlags'] + + # Don't send empty commodities list - schema won't allow it + if commodities: + message = OrderedDict([ + ('timestamp', data['timestamp']), + ('systemName', data['lastSystem']['name']), + ('stationName', data['lastStarport']['name']), + ('marketId', data['lastStarport']['id']), + ('commodities', commodities), + ]) + if 'economies' in data['lastStarport']: + message['economies'] = sorted([x for x in (data['lastStarport']['economies'] or {}).itervalues()]) + if 'prohibited' in data['lastStarport']: + message['prohibited'] = sorted([x for x in (data['lastStarport']['prohibited'] or {}).itervalues()]) + self.send(data['commander']['name'], { + '$schemaRef' : 'https://eddn.edcd.io/schemas/commodity/3' + (is_beta and '/test' or ''), + 'message' : message, + }) + + def export_outfitting(self, data, is_beta): + # Don't send empty modules list - schema won't allow it + if data['lastStarport'].get('modules'): + self.send(data['commander']['name'], { + '$schemaRef' : 'https://eddn.edcd.io/schemas/outfitting/2' + (is_beta and '/test' or ''), + 'message' : OrderedDict([ + ('timestamp', data['timestamp']), + ('systemName', data['lastSystem']['name']), + ('stationName', data['lastStarport']['name']), + ('marketId', data['lastStarport']['id']), + ('modules', sorted([module['name'] for module in data['lastStarport']['modules'].itervalues() if self.MODULE_RE.search(module['name']) and module.get('sku') in [None, 'ELITE_HORIZONS_V_PLANETARY_LANDINGS'] and module['name'] != 'Int_PlanetApproachSuite'])), + ]), + }) + + def export_shipyard(self, data, is_beta): + # Don't send empty ships list - shipyard data is only guaranteed present if user has visited the shipyard. + if data['lastStarport'].get('ships', {}).get('shipyard_list'): + self.send(data['commander']['name'], { + '$schemaRef' : 'https://eddn.edcd.io/schemas/shipyard/2' + (is_beta and '/test' or ''), + 'message' : OrderedDict([ + ('timestamp', data['timestamp']), + ('systemName', data['lastSystem']['name']), + ('stationName', data['lastStarport']['name']), + ('marketId', data['lastStarport']['id']), + ('ships', sorted([ship['name'] for ship in data['lastStarport']['ships']['shipyard_list'].values() + data['lastStarport']['ships']['unavailable_list']])), + ]), + }) + + def export_journal_entry(self, cmdr, is_beta, entry): + msg = { + '$schemaRef' : 'https://eddn.edcd.io/schemas/journal/1' + (is_beta and '/test' or ''), + 'message' : entry + } + if self.replayfile or self.load(): + # Store the entry + self.replaylog.append(json.dumps([cmdr.encode('utf-8'), msg])) + self.replayfile.write('%s\n' % self.replaylog[-1]) + + if (entry['event'] == 'Docked' or + (entry['event'] == 'Location' and entry['Docked']) or + not (config.getint('output') & config.OUT_SYS_DELAY)): + self.parent.after(self.REPLAYPERIOD, self.sendreplay) # Try to send this and previous entries + else: + # Can't access replay file! Send immediately. + status = self.parent.children['status'] + status['text'] = _('Sending data to EDDN...') + self.parent.update_idletasks() + self.send(cmdr, msg) + status['text'] = '' + + +# Plugin callbacks + +def plugin_start(): + return 'EDDN' + +def plugin_app(parent): + this.parent = parent + this.eddn = EDDN(parent) + # Try to obtain exclusive lock on journal cache, even if we don't need it yet + if not this.eddn.load(): + this.status['text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing + +def plugin_prefs(parent, cmdr, is_beta): + + PADX = 10 + BUTTONX = 12 # indent Checkbuttons and Radiobuttons + PADY = 2 # close spacing + + output = config.getint('output') or (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN) # default settings + + eddnframe = nb.Frame(parent) + + HyperlinkLabel(eddnframe, text='Elite Dangerous Data Network', background=nb.Label().cget('background'), url='https://github.com/EDSM-NET/EDDN/wiki', underline=True).grid(padx=PADX, sticky=tk.W) # Don't translate + this.eddn_station= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1) + this.eddn_station_button = nb.Checkbutton(eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=this.eddn_station, command=prefsvarchanged) # Output setting + this.eddn_station_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) + this.eddn_system = tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1) + this.eddn_system_button = nb.Checkbutton(eddnframe, text=_('Send system and scan data to the Elite Dangerous Data Network'), variable=this.eddn_system, command=prefsvarchanged) # Output setting new in E:D 2.2 + this.eddn_system_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) + this.eddn_delay= tk.IntVar(value = (output & config.OUT_SYS_DELAY) and 1) + this.eddn_delay_button = nb.Checkbutton(eddnframe, text=_('Delay sending until docked'), variable=this.eddn_delay) # Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2 + this.eddn_delay_button.grid(padx=BUTTONX, sticky=tk.W) + + return eddnframe + +def prefsvarchanged(event=None): + this.eddn_station_button['state'] = tk.NORMAL + this.eddn_system_button['state']= tk.NORMAL + this.eddn_delay_button['state'] = this.eddn.replayfile and this.eddn_system.get() and tk.NORMAL or tk.DISABLED + +def prefs_changed(cmdr, is_beta): + config.set('output', + (config.getint('output') & (config.OUT_MKT_TD | config.OUT_MKT_CSV | config.OUT_SHIP)) + + (this.eddn_station.get() and config.OUT_MKT_EDDN) + + (this.eddn_system.get() and config.OUT_SYS_EDDN) + + (this.eddn_delay.get() and config.OUT_SYS_DELAY)) + +def plugin_stop(): + this.eddn.close() + +def journal_entry(cmdr, is_beta, system, station, entry, state): + + # Recursively filter '*_Localised' keys from dict + def filter_localised(d): + filtered = OrderedDict() + for k, v in d.iteritems(): + if k.endswith('_Localised'): + pass + elif hasattr(v, 'iteritems'): # dict -> recurse + filtered[k] = filter_localised(v) + elif isinstance(v, list): # list of dicts -> recurse + filtered[k] = [filter_localised(x) if hasattr(x, 'iteritems') else x for x in v] + else: + filtered[k] = v + return filtered + + # Track location + if entry['event'] in ['Location', 'FSDJump', 'Docked']: + if entry['event'] == 'Location': + this.planet = entry.get('Body') if entry.get('BodyType') == 'Planet' else None + elif entry['event'] == 'FSDJump': + this.planet = None + if 'StarPos' in entry: + this.coordinates = tuple(entry['StarPos']) + elif this.systemaddress != entry.get('SystemAddress'): + this.coordinates = None # Docked event doesn't include coordinates + this.systemaddress = entry.get('SystemAddress') + elif entry['event'] == 'ApproachBody': + this.planet = entry['Body'] + elif entry['event'] in ['LeaveBody', 'SupercruiseEntry']: + this.planet = None + + # Send interesting events to EDDN, but not when on a crew + if (config.getint('output') & config.OUT_SYS_EDDN and not state['Captain'] and + (entry['event'] == 'Location' or + entry['event'] == 'FSDJump' or + entry['event'] == 'Docked' or + entry['event'] == 'Scan' and this.coordinates)): + # strip out properties disallowed by the schema + for thing in ['ActiveFine', 'CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist', 'Latitude', 'Longitude', 'Wanted']: + entry.pop(thing, None) + for faction in entry.get('Factions', []): + faction.pop('MyReputation', None) + + # add planet to Docked event for planetary stations if known + if entry['event'] == 'Docked' and this.planet: + entry['Body'] = this.planet + entry['BodyType'] = 'Planet' + + # add mandatory StarSystem, StarPos and SystemAddress properties to Scan events + if 'StarSystem' not in entry: + entry['StarSystem'] = system + if 'StarPos' not in entry: + entry['StarPos'] = list(this.coordinates) + if 'SystemAddress' not in entry and this.systemaddress: + entry['SystemAddress'] = this.systemaddress + + try: + this.eddn.export_journal_entry(cmdr, is_beta, filter_localised(entry)) + except requests.exceptions.RequestException as e: + if __debug__: print_exc() + return _("Error: Can't connect to EDDN") + except Exception as e: + if __debug__: print_exc() + return unicode(e) + +def cmdr_data(data, is_beta): + if data['commander'].get('docked') & config.OUT_MKT_EDDN: + try: + status = this.parent.children['status'] + old_status = status['text'] + if not old_status: + status['text'] = _('Sending data to EDDN...') + status.update_idletasks() + this.eddn.export_commodities(data, is_beta) + this.eddn.export_outfitting(data, is_beta) + this.eddn.export_shipyard(data, is_beta) + if not old_status: + status['text'] = '' + status.update_idletasks() + + except requests.RequestException as e: + if __debug__: print_exc() + return _("Error: Can't connect to EDDN") + + except Exception as e: + if __debug__: print_exc() + return unicode(e) diff --git a/prefs.py b/prefs.py index 7f7cacc0..14deb259 100644 --- a/prefs.py +++ b/prefs.py @@ -12,7 +12,6 @@ from ttkHyperlinkLabel import HyperlinkLabel import myNotebook as nb from config import applongname, config -import eddn from hotkey import hotkeymgr from l10n import Translations from monitor import monitor @@ -137,7 +136,7 @@ class PreferencesDialog(tk.Toplevel): outframe = nb.Frame(notebook) outframe.columnconfigure(0, weight=1) - output = config.getint('output') or (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN | config.OUT_SHIP) # default settings + output = config.getint('output') or config.OUT_SHIP # default settings self.out_label = nb.Label(outframe, text=_('Please choose what data to save')) self.out_label.grid(columnspan=2, padx=PADX, sticky=tk.W) @@ -168,24 +167,6 @@ 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/EDSM-NET/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) - self.eddn_station_button = nb.Checkbutton(eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.eddn_station, command=self.outvarchanged) # Output setting - self.eddn_station_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) - 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) - self.eddn_delay= tk.IntVar(value = (output & config.OUT_SYS_DELAY) and 1) - self.eddn_delay_button = nb.Checkbutton(eddnframe, text=_('Delay sending until docked'), variable=self.eddn_delay) # Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2 - self.eddn_delay_button.grid(padx=BUTTONX, sticky=tk.W) - - notebook.add(eddnframe, text='EDDN') # Not translated - # build plugin prefs tabs for plugin in plug.PLUGINS: plugframe = plugin.get_prefs(notebook, monitor.cmdr, monitor.is_beta) @@ -413,11 +394,6 @@ class PreferencesDialog(tk.Toplevel): self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir_entry['state'] = local and 'readonly' or tk.DISABLED - self.eddn_station_button['state'] = tk.NORMAL 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.eddn_delay_button['state'] = logvalid and eddn.replayfile and self.eddn_system.get() and tk.NORMAL or tk.DISABLED - def filebrowse(self, title, pathvar): if platform != 'win32': import tkFileDialog @@ -560,13 +536,11 @@ class PreferencesDialog(tk.Toplevel): _putfirst('fdev_usernames', idx, self.username.get().strip()) config.set('output', - (self.out_td.get() and config.OUT_MKT_TD) + - (self.out_csv.get() and config.OUT_MKT_CSV) + + (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.get() and config.OUT_SHIP) + - (self.eddn_station.get() and config.OUT_MKT_EDDN) + - (self.eddn_system.get() and config.OUT_SYS_EDDN) + - (self.eddn_delay.get() and config.OUT_SYS_DELAY)) + (self.out_ship.get() and config.OUT_SHIP) + + (config.getint('output') & (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN | config.OUT_SYS_DELAY))) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) logdir = self.logdir.get() diff --git a/setup.py b/setup.py index 60025d51..742a5a38 100755 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ if sys.platform=='darwin': APP = 'EDMarketConnector.py' APPCMD = 'EDMC.py' SHORTVERSION = ''.join(VERSION.split('.')[:3]) -PLUGINS = [ 'plugins/coriolis.py', 'plugins/eddb.py', 'plugins/edsm.py', 'plugins/edsy.py', 'plugins/inara.py' ] +PLUGINS = [ 'plugins/coriolis.py', 'plugins/eddb.py', 'plugins/eddn.py', 'plugins/edsm.py', 'plugins/edsy.py', 'plugins/inara.py' ] if sys.platform=='darwin': OPTIONS = { 'py2app':