From e9ef32598d833c0c51b2f4762aa659777fd72eb5 Mon Sep 17 00:00:00 2001 From: Jonathan Harris Date: Sun, 17 Jul 2016 18:48:12 +0100 Subject: [PATCH] Option to automatically update on docking Fixes #83 --- EDMarketConnector.py | 10 ++++---- L10n/cs.strings | 3 +++ L10n/de.strings | 3 +++ L10n/en.template | 3 +++ L10n/es.strings | 3 +++ L10n/ja.strings | 3 +++ L10n/nl.strings | 3 +++ README.md | 4 +-- config.py | 1 + edproxy.py | 6 +++-- monitor.py | 59 ++++++++++++++++++++++++++++++++++++++------ prefs.py | 14 +++++------ 12 files changed, 87 insertions(+), 25 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 068f894b..30fcb3df 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -249,11 +249,11 @@ class AppWindow: hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring - monitor.set_callback(self.system_change) + monitor.set_callback('Dock', self.getandsend) + monitor.set_callback('Jump', self.system_change) + monitor.start(self.w) edproxy.set_callback(self.system_change) - if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_FILE|config.OUT_LOG_EDSM)): - monitor.start(self.w) - edproxy.start(self.w) + edproxy.start(self.w) # First run if not config.get('username') or not config.get('password'): @@ -497,7 +497,7 @@ class AppWindow: except: pass - def system_change(self, timestamp, system, coordinates): + def system_change(self, event, timestamp, system, coordinates): if self.system['text'] != system: self.system['text'] = system diff --git a/L10n/cs.strings b/L10n/cs.strings index e540ebed..4e7572ed 100644 --- a/L10n/cs.strings +++ b/L10n/cs.strings @@ -28,6 +28,9 @@ /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automaticky vytvořit záznam při vstoupení do systému"; +/* Output setting. [prefs.py] */ +"Automatically update on docking" = "Automaticky aktualizovat při zadokování"; + /* Cmdr stats. [stats.py] */ "Balance" = "Zůstatek"; diff --git a/L10n/de.strings b/L10n/de.strings index d742dd97..66b71224 100644 --- a/L10n/de.strings +++ b/L10n/de.strings @@ -28,6 +28,9 @@ /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automatisch Logbucheintrag bei Systemeintritt anlegen"; +/* Output setting. [prefs.py] */ +"Automatically update on docking" = "Automatisch beim Andocken aktualisieren"; + /* Cmdr stats. [stats.py] */ "Balance" = "Kontostand"; diff --git a/L10n/en.template b/L10n/en.template index 9f900d88..49a984ca 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -28,6 +28,9 @@ /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automatically make a log entry on entering a system"; +/* Output setting. [prefs.py] */ +"Automatically update on docking" = "Automatically update on docking"; + /* Cmdr stats. [stats.py] */ "Balance" = "Balance"; diff --git a/L10n/es.strings b/L10n/es.strings index 586272fc..d3e0cb55 100644 --- a/L10n/es.strings +++ b/L10n/es.strings @@ -28,6 +28,9 @@ /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Crear automáticamente una entrada en el registro al entrar en un sistema"; +/* Output setting. [prefs.py] */ +"Automatically update on docking" = "Actualizar automáticamente al atracar"; + /* Cmdr stats. [stats.py] */ "Balance" = "Saldo"; diff --git a/L10n/ja.strings b/L10n/ja.strings index 56c4fda0..749823b6 100644 --- a/L10n/ja.strings +++ b/L10n/ja.strings @@ -28,6 +28,9 @@ /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "別の星系に移動したら自動的にフライトログを記録する"; +/* Output setting. [prefs.py] */ +"Automatically update on docking" = "ドッキングした際に自動でデータを更新する"; + /* Cmdr stats. [stats.py] */ "Balance" = "Balance"; diff --git a/L10n/nl.strings b/L10n/nl.strings index fb66f833..ab316c70 100644 --- a/L10n/nl.strings +++ b/L10n/nl.strings @@ -28,6 +28,9 @@ /* Output setting. [prefs.py] */ "Automatically make a log entry on entering a system" = "Automatisch een log regel aanmaken bij het binnengaan van een stelsel"; +/* Output setting. [prefs.py] */ +"Automatically update on docking" = "Automatisch bijwerken na landing"; + /* Cmdr stats. [stats.py] */ "Balance" = "Balans"; diff --git a/README.md b/README.md index 2eee7114..70eb7505 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This app downloads commodity market and other station data from the game [Elite: Usage -------- -The user-interface is deliberately minimal - when you land at a station just switch to the app and press the “Update” button or press Enter to automatically download and transmit and/or save your choice of data. +The user-interface is deliberately minimal - when you land at a station just switch to the app and press the “Update” button or press Enter to download and transmit and/or save your choice of data. Click on the system name to go to its [Elite: Dangerous Star Map](http://www.edsm.net/) (“EDSM”) entry in your web broswer. @@ -44,7 +44,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 market 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 set up a hotkey so you don't have to switch to the app in order to “Update”, 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” 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. The first time that you hit “Update” 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 diff --git a/config.py b/config.py index 9ba85004..c780147a 100644 --- a/config.py +++ b/config.py @@ -80,6 +80,7 @@ class Config: OUT_SHIP_CORIOLIS = 128 OUT_LOG_EDSM = 256 OUT_LOG_AUTO = 512 + OUT_MANUAL = 1024 if platform=='darwin': diff --git a/edproxy.py b/edproxy.py index 4eb55be9..c5b41b6e 100644 --- a/edproxy.py +++ b/edproxy.py @@ -14,6 +14,8 @@ from calendar import timegm if __debug__: from traceback import print_exc +from config import config + class _EDProxy: @@ -73,7 +75,7 @@ class _EDProxy: def jump(self, event): # Called from Tkinter's main loop if self.callback and self.last_event: - self.callback(*self.last_event) + self.callback(event, *self.last_event) def close(): self.discover_sock.shutdown() @@ -131,7 +133,7 @@ class _EDProxy: s.settimeout(None) # was self.SERVICE_TIMEOUT, but heartbeat doesn't appear to work so wait indefinitely while True: msg = json.loads(s.recv(self.MESSAGE_MAX)) - if msg['Type'] == self.MESSAGE_SYSTEM: + if msg['Type'] == self.MESSAGE_SYSTEM and config.getint('output') & config.OUT_LOG_AUTO: if 'DateUtc' in msg: timestamp = timegm(datetime.strptime(msg['DateUtc'], '%Y-%m-%d %H:%M:%S').utctimetuple()) else: diff --git a/monitor.py b/monitor.py index 871628ff..5cd6e4e5 100644 --- a/monitor.py +++ b/monitor.py @@ -12,6 +12,9 @@ from datetime import datetime if __debug__: from traceback import print_exc +from config import config + + if platform=='darwin': from AppKit import NSWorkspace from Foundation import NSSearchPathForDirectoriesInDomains, NSApplicationSupportDirectory, NSUserDomainMask @@ -72,6 +75,8 @@ else: class EDLogs(FileSystemEventHandler): + _POLL = 5 # New system gets posted to log file before hyperspace ends, so don't need to poll too often + def __init__(self): FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog self.root = None @@ -79,11 +84,12 @@ class EDLogs(FileSystemEventHandler): self.logfile = None self.observer = None self.thread = None - self.callback = None + self.callbacks = { 'Jump': None, 'Dock': None } self.last_event = None # for communicating the Jump event - def set_callback(self, callback): - self.callback = callback + def set_callback(self, name, callback): + if name in self.callbacks: + self.callbacks[name] = callback def start(self, root): self.root = root @@ -94,6 +100,7 @@ class EDLogs(FileSystemEventHandler): return True 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. if not self.observer: @@ -131,6 +138,10 @@ class EDLogs(FileSystemEventHandler): self.logfile = event.src_path def worker(self): + # Tk isn't thread-safe in general. + # 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: @@ -140,6 +151,16 @@ class EDLogs(FileSystemEventHandler): # 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: @@ -149,6 +170,13 @@ class EDLogs(FileSystemEventHandler): loghandle = None while True: + + if docked and not updated and not config.getint('output') & config.OUT_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. newlogfile = self.logfile if logfile != newlogfile: @@ -168,8 +196,18 @@ class EDLogs(FileSystemEventHandler): 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: + if system and not docked and config.getint('output') & config.OUT_LOG_AUTO: # Convert local time string to UTC date and time visited_struct = strptime(visited, '%H:%M:%S') now = localtime() @@ -177,11 +215,10 @@ class EDLogs(FileSystemEventHandler): # 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 - # Tk on Windows doesn't like to be called outside of an event handler, so generate an event self.last_event = (mktime(time_struct), system, coordinates) self.root.event_generate('<>', when="tail") - sleep(10) # New system gets posted to log file before hyperspace ends, so don't need to poll too often + sleep(self._POLL) # Check whether we're still supposed to be running if threading.current_thread() != self.thread: @@ -189,8 +226,14 @@ class EDLogs(FileSystemEventHandler): def jump(self, event): # Called from Tkinter's main loop - if self.callback and self.last_event: - self.callback(*self.last_event) + if self.callbacks['Jump'] and self.last_event: + self.callbacks['Jump'](event, *self.last_event) + + def dock(self, event): + # Called from Tkinter's main loop + if self.callbacks['Dock']: + self.callbacks['Dock'](event) + if platform=='darwin': diff --git a/prefs.py b/prefs.py index 02fc4a5f..5b62219b 100644 --- a/prefs.py +++ b/prefs.py @@ -124,6 +124,9 @@ class PreferencesDialog(tk.Toplevel): nb.Checkbutton(outframe, text=_("Market data in Slopey's BPC format file"), variable=self.out_bpc, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_td = tk.IntVar(value = (output & config.OUT_TD ) and 1) nb.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) + self.out_auto = tk.IntVar(value = 0 if output & config.OUT_MANUAL else 1) # inverted + self.out_auto_button = nb.Checkbutton(outframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting + self.out_auto_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_ship_eds= tk.IntVar(value = (output & config.OUT_SHIP_EDS) and 1) nb.Checkbutton(outframe, text=_('Ship loadout in E:D Shipyard format file'), variable=self.out_ship_eds, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) self.out_ship_coriolis= tk.IntVar(value = (output & config.OUT_SHIP_CORIOLIS) and 1) @@ -418,9 +421,10 @@ class PreferencesDialog(tk.Toplevel): (self.out_bpc.get() and config.OUT_BPC) + (self.out_td.get() and config.OUT_TD) + (self.out_csv.get() and config.OUT_CSV) + + (config.OUT_MANUAL if not self.out_auto.get() else 0) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS) + - (self.out_log_file.get() and config.OUT_LOG_FILE) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) + + (self.out_log_file.get() and config.OUT_LOG_FILE) + (self.out_log_edsm.get() and config.OUT_LOG_EDSM) + (self.out_log_auto.get() and config.OUT_LOG_AUTO)) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) @@ -451,14 +455,8 @@ class PreferencesDialog(tk.Toplevel): self.callback() def _destroy(self): - # Re-enable hotkey and log monitoring before exit + # Re-enable hotkey monitoring before exit hotkeymgr.register(self.parent, config.getint('hotkey_code'), config.getint('hotkey_mods')) - if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_FILE|config.OUT_LOG_EDSM)): - monitor.start(self.parent) - edproxy.start(self.parent) - else: - monitor.stop() - edproxy.stop() self.parent.wm_attributes('-topmost', config.getint('always_ontop') and 1 or 0) self.destroy()