From 050aada1a21f9e31fc8e411b90192f3b05e22ef6 Mon Sep 17 00:00:00 2001
From: Jonathan Harris <jonathan@marginal.org.uk>
Date: Mon, 22 Jun 2015 18:36:06 +0100
Subject: [PATCH] Add support for saving a simple flight log.

---
 EDMarketConnector.py | 55 +++++++++++++++++++++++++++++---------------
 README.md            | 30 +++++++++++++++++++-----
 config.py            |  1 +
 flightlog.py         | 55 ++++++++++++++++++++++++++++++++++++++++++++
 prefs.py             | 26 +++++++++++----------
 5 files changed, 130 insertions(+), 37 deletions(-)
 create mode 100644 flightlog.py

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()