diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 575213c7..a9552282 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -18,6 +18,7 @@ import bpc import td import eddn import loadout +import flightlog import stats import prefs from config import appname, applongname, config @@ -136,6 +137,15 @@ class AppWindow: try: self.session.login(config.get('username'), config.get('password')) self.status['text'] = '' + + # Try to obtain exclusive lock on flight log ASAP + if config.getint('output') & config.OUT_LOG: + try: + flightlog.openlog() + except Exception as e: + if __debug__: print_exc() + self.status['text'] = str(e) + except companion.VerificationRequired: # don't worry about authentication now - prompt on query self.status['text'] = '' @@ -178,31 +188,38 @@ class AppWindow: # Validation if not data.get('commander') or not data['commander'].get('name','').strip(): - self.status['text'] = "Who are you?!" # Shouldn't happen + self.status['text'] = "Who are you?!" # Shouldn't happen elif not data.get('lastSystem') or not data['lastSystem'].get('name','').strip() or not data.get('lastStarport') or not data['lastStarport'].get('name','').strip(): - self.status['text'] = "Where are you?!" # Shouldn't happen + self.status['text'] = "Where are you?!" # Shouldn't happen elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name','').strip(): self.status['text'] = "What are you flying?!" # Shouldn't happen - elif not data['commander'].get('docked'): - if config.getint('output') & config.OUT_SHIP: - loadout.export(data) # do loadout even if not docked - self.status['text'] = "You're not docked at a station!" - elif not data['lastStarport'].get('commodities'): - self.status['text'] = "Station doesn't have a market!" else: + # stuff we can do when not docked + if config.getint('output') & config.OUT_LOG: + flightlog.export(data) if config.getint('output') & config.OUT_SHIP: loadout.export(data) - if config.getint('output') & config.OUT_CSV: - bpc.export(data, True) - if config.getint('output') & config.OUT_TD: - td.export(data) - if config.getint('output') & config.OUT_BPC: - bpc.export(data, False) - if config.getint('output') & config.OUT_EDDN: - self.status['text'] = 'Sending data to EDDN...' - self.w.update_idletasks() - eddn.export(data) - self.status['text'] = strftime('Last updated at %H:%M:%S', localtime(querytime)) + + if not (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)): + # no further output requested + self.status['text'] = strftime('Last updated at %H:%M:%S', localtime(querytime)) + + elif not data['commander'].get('docked'): + self.status['text'] = "You're not docked at a station!" + elif not data['lastStarport'].get('commodities'): + self.status['text'] = "Station doesn't have a market!" + else: + if config.getint('output') & config.OUT_CSV: + bpc.export(data, True) + if config.getint('output') & config.OUT_TD: + td.export(data) + if config.getint('output') & config.OUT_BPC: + bpc.export(data, False) + if config.getint('output') & config.OUT_EDDN: + self.status['text'] = 'Sending data to EDDN...' + self.w.update_idletasks() + eddn.export(data) + self.status['text'] = strftime('Last updated at %H:%M:%S', localtime(querytime)) except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, self.verify) diff --git a/README.md b/README.md index c6f31f7b..4a4b7016 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,11 @@ Elite: Dangerous Market Connector This app downloads commodity market data from the game [Elite: Dangerous](https://www.elitedangerous.com/) and, at your choice, either: -* transmits the data to the [Elite Dangerous Data Network](http://eddn.ed-td.space/) ("EDDN") from where you and others can use it via online trading tools such as [eddb](http://eddb.io/). +* sends the data to the [Elite Dangerous Data Network](http://eddn.ed-td.space/) ("EDDN") from where you and others can use it via online trading tools such as [eddb](http://eddb.io/) or [Elite Trade Net](http://etn.io/). * saves the data 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) and [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/). +* saves a record of your ship loadout and/or flight log. -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 the station's commodity market 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 automatically download and transmit and/or save your choice of data. ![Windows screenshot](img/win.png) ![Mac screenshot](img/mac.png) @@ -32,11 +33,9 @@ Windows: 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 the market data for the station that *you* are docked at. +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 whether to send the market data that you download to EDDN or to save it locally, -whether to attach your Cmdr name or a [pseudo-anonymized](http://en.wikipedia.org/wiki/Pseudonymity) ID to the data, -and whether to save a record of your ship loadout in a form that you can import into [E:D Shipyard](http://www.edshipyard.com) after every outfitting change. +You can also choose here what data to save (refer to the next section for details) and whether to attach your Cmdr name or a [pseudo-anonymized](http://en.wikipedia.org/wiki/Pseudonymity) ID to the data. You are next 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 @@ -45,6 +44,24 @@ authenticating you will need to wait for Frontier to send you a new code. If you are not prompted to authenticate, but instead see the message "Error: Invalid Credentials" then choose the menu option EDMarketConnector → Preferences (Mac) or File → Settings (Windows) and double-check your username and password. +Output +-------- +This app can save a variety of data in a variety of formats: + +* Market data + * Elite Dangerous Data Network - sends the market data to the "[EDDN](http://eddn.ed-td.space/)" from where you and others can use it via online trading tools such as [eddb](http://eddb.io/) or [Elite Trade Net](http://etn.io/). + * Slopey's BPC format - saves the 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 - saves the market data as files that you can load into [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home). + * CSV format - saves the market data as files that you can upload to [Thrudd's Trading Tools](http://www.elitetradingtool.co.uk/). + +* Ship loadout + * After every outfitting change saves a record of your ship loadout as a file that you can open in a text editor and that you can import into [E:D Shipyard](http://www.edshipyard.com). + +* Flight log + * Adds a record of your location, ship and cargo to a file that you can open in a text editor or a spreadsheet program such as Excel. + +By default these files will be placed in your Documents folder. Since this app will create a lot of files if you use it for a while you may wish to create a separate folder for the files and tell the app to place them there. + Statistics -------- Choose the "Statistics" item from the menu to view your Cmdrs's statistics. The statistics shown are largely the same @@ -61,6 +78,7 @@ Windows: * Uninstall **Elite Dangerous Market Connector** from Control Panel → Programs. +Note: Uninstalling the app does not delete any output files that it has previously written. Running from source -------- diff --git a/config.py b/config.py index 740d26a5..dfe93a81 100644 --- a/config.py +++ b/config.py @@ -70,6 +70,7 @@ class Config: OUT_TD = 4 OUT_CSV = 8 OUT_SHIP = 16 + OUT_LOG = 32 if platform=='darwin': diff --git a/flightlog.py b/flightlog.py new file mode 100644 index 00000000..0a62667a --- /dev/null +++ b/flightlog.py @@ -0,0 +1,55 @@ +# Export poor man's flight log + +import errno +import os +from os.path import join +from sys import platform +import time + +from config import config +from companion import ship_map, commodity_map + + +logfile = None + +def openlog(): + + global logfile + if logfile: return + + try: + logfile = open(join(config.get('outdir'), 'Flight Log.csv'), 'a+') + if platform != 'win32': # open for writing is automatically exclusive on Windows + from fcntl import lockf, LOCK_SH, LOCK_NB + lockf(logfile, LOCK_SH|LOCK_NB) + logfile.seek(0, os.SEEK_END) + if not logfile.tell(): + logfile.write('Date,Time,System,Station,Ship,Cargo\r\n') + except EnvironmentError as e: + logfile = None + if e.errno in [errno.EACCES, errno.EAGAIN]: + raise Exception('Can\'t write "Flight Log.csv". Are you editing it in another app?') + else: + raise + except: + logfile = None + raise + + +def export(data): + + def elapsed(game_time): + return '%3d:%02d:%02d' % ((game_time // 3600) % 3600, (game_time // 60) % 60, game_time % 60) + + querytime = config.getint('querytime') or int(time.time()) + + openlog() + + logfile.write('%s,%s,%s,%s,%s,%s\r\n' % ( + time.strftime('%Y-%m-%d', time.localtime(querytime)), + time.strftime('%H:%M:%S', time.localtime(querytime)), + data['lastSystem']['name'], + data['commander']['docked'] and data['lastStarport']['name'] or '', + data['ship']['name'], + ','.join([('%d %s' % (x['qty'], commodity_map.get(x['commodity'],x['commodity']))) for x in data['ship']['cargo']['items'] if x['commodity']!='drones']))) + logfile.flush() diff --git a/prefs.py b/prefs.py index 915502e2..10a5fd38 100644 --- a/prefs.py +++ b/prefs.py @@ -57,7 +57,7 @@ class PreferencesDialog(tk.Toplevel): credframe.grid(padx=10, pady=10, sticky=tk.NSEW) credframe.columnconfigure(1, weight=1) - ttk.Label(credframe, text="Please log in with your Elite:Dangerous account details").grid(row=0, columnspan=2, sticky=tk.W) + ttk.Label(credframe, text="Please log in with your Elite: Dangerous account details").grid(row=0, columnspan=2, sticky=tk.W) ttk.Label(credframe, text="Username (Email)").grid(row=1, sticky=tk.W) ttk.Label(credframe, text="Password").grid(row=2, sticky=tk.W) @@ -77,23 +77,25 @@ class PreferencesDialog(tk.Toplevel): outframe.columnconfigure(0, weight=1) output = config.getint('output') or (config.OUT_EDDN | config.OUT_SHIP) - ttk.Label(outframe, text="Please choose how you want the data saved").grid(row=0, columnspan=2, padx=5, pady=3, sticky=tk.W) + ttk.Label(outframe, text="Please choose what data to save").grid(row=0, columnspan=2, padx=5, pady=3, sticky=tk.W) self.out_eddn= tk.IntVar(value = (output & config.OUT_EDDN) and 1 or 0) - ttk.Checkbutton(outframe, text="Online to the Elite Dangerous Data Network (EDDN)", variable=self.out_eddn).grid(row=1, columnspan=2, padx=5, sticky=tk.W) + ttk.Checkbutton(outframe, text="Send market data to the Elite Dangerous Data Network", variable=self.out_eddn).grid(row=1, columnspan=2, padx=5, sticky=tk.W) self.out_bpc = tk.IntVar(value = (output & config.OUT_BPC ) and 1 or 0) - ttk.Checkbutton(outframe, text="Offline in Slopey's BPC format", variable=self.out_bpc, command=self.outvarchanged).grid(row=2, columnspan=2, padx=5, sticky=tk.W) + ttk.Checkbutton(outframe, text="Market data in Slopey's BPC format", variable=self.out_bpc, command=self.outvarchanged).grid(row=2, columnspan=2, padx=5, sticky=tk.W) self.out_td = tk.IntVar(value = (output & config.OUT_TD ) and 1 or 0) - ttk.Checkbutton(outframe, text="Offline in Trade Dangerous format", variable=self.out_td, command=self.outvarchanged).grid(row=3, columnspan=2, padx=5, sticky=tk.W) + ttk.Checkbutton(outframe, text="Market data in Trade Dangerous format", variable=self.out_td, command=self.outvarchanged).grid(row=3, columnspan=2, padx=5, sticky=tk.W) self.out_csv = tk.IntVar(value = (output & config.OUT_CSV ) and 1 or 0) - ttk.Checkbutton(outframe, text="Offline in CSV format", variable=self.out_csv, command=self.outvarchanged).grid(row=4, columnspan=2, padx=5, sticky=tk.W) + ttk.Checkbutton(outframe, text="Market data in CSV format", variable=self.out_csv, command=self.outvarchanged).grid(row=4, columnspan=2, padx=5, sticky=tk.W) self.out_ship= tk.IntVar(value = (output & config.OUT_SHIP) and 1 or 0) - ttk.Checkbutton(outframe, text="Offline ship loadout in E:D Shipyard format", variable=self.out_ship, command=self.outvarchanged).grid(row=5, columnspan=2, padx=5, sticky=tk.W) - ttk.Label(outframe, text=(platform=='darwin' and 'Where:' or 'File location:')).grid(row=6, padx=5, pady=(5,0), sticky=tk.NSEW) + ttk.Checkbutton(outframe, text="Ship loadout in E:D Shipyard format", variable=self.out_ship, command=self.outvarchanged).grid(row=5, columnspan=2, padx=5, sticky=tk.W) + self.out_log = tk.IntVar(value = (output & config.OUT_LOG) and 1 or 0) + ttk.Checkbutton(outframe, text="Flight log", variable=self.out_log, command=self.outvarchanged).grid(row=6, columnspan=2, padx=5, sticky=tk.W) + ttk.Label(outframe, text=(platform=='darwin' and 'Where:' or 'File location:')).grid(row=7, padx=5, pady=(5,0), sticky=tk.NSEW) self.outbutton = ttk.Button(outframe, text=(platform=='darwin' and 'Change...' or 'Browse...'), command=self.outbrowse) - self.outbutton.grid(row=6, column=1, padx=5, pady=(5,0), sticky=tk.NSEW) + self.outbutton.grid(row=7, column=1, padx=5, pady=(5,0), sticky=tk.NSEW) self.outdir = ttk.Entry(outframe) self.outdir.insert(0, config.get('outdir')) - self.outdir.grid(row=7, columnspan=2, padx=5, pady=5, sticky=tk.EW) + self.outdir.grid(row=8, columnspan=2, padx=5, pady=5, sticky=tk.EW) self.outvarchanged() privacyframe = ttk.LabelFrame(frame, text='Privacy') @@ -120,7 +122,7 @@ class PreferencesDialog(tk.Toplevel): #self.wait_window(self) # causes duplicate events on OSX def outvarchanged(self): - local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship.get() + local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship.get() or self.out_log.get() self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED @@ -159,7 +161,7 @@ class PreferencesDialog(tk.Toplevel): credentials = (config.get('username'), config.get('password')) config.set('username', self.username.get().strip()) config.set('password', self.password.get().strip()) - config.set('output', (self.out_eddn.get() and config.OUT_EDDN or 0) + (self.out_bpc.get() and config.OUT_BPC or 0) + (self.out_td.get() and config.OUT_TD or 0) + (self.out_csv.get() and config.OUT_CSV or 0) + (self.out_ship.get() and config.OUT_SHIP or 0)) + config.set('output', (self.out_eddn.get() and config.OUT_EDDN or 0) + (self.out_bpc.get() and config.OUT_BPC or 0) + (self.out_td.get() and config.OUT_TD or 0) + (self.out_csv.get() and config.OUT_CSV or 0) + (self.out_ship.get() and config.OUT_SHIP or 0) + (self.out_log.get() and config.OUT_LOG or 0)) config.set('outdir', self.outdir.get().strip()) config.set('anonymous', self.out_anon.get()) self.destroy()