mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-14 16:27:13 +03:00
Add support for syncing with Inara
This commit is contained in:
parent
979162ea3d
commit
85cae73529
@ -157,6 +157,9 @@
|
||||
/* [edsm.py] */
|
||||
"Error: Can't connect to EDSM" = "Error: Can't connect to EDSM";
|
||||
|
||||
/* [inara.py] */
|
||||
"Error: Can't connect to Inara" = "Error: Can't connect to Inara";
|
||||
|
||||
/* [edsm.py] */
|
||||
"Error: EDSM {MSG}" = "Error: EDSM {MSG}";
|
||||
|
||||
@ -169,6 +172,9 @@
|
||||
/* Raised when the Companion API server thinks that the user has not purchased E:D. i.e. doesn't have the correct 'SKU'. [companion.py] */
|
||||
"Error: Frontier server SKU problem" = "Error: Frontier server SKU problem";
|
||||
|
||||
/* [inara.py] */
|
||||
"Error: Inara {MSG}" = "Error: Inara {MSG}";
|
||||
|
||||
/* [companion.py] */
|
||||
"Error: Invalid Credentials" = "Error: Invalid Credentials";
|
||||
|
||||
@ -235,6 +241,9 @@
|
||||
/* Tab heading in settings. [prefs.py] */
|
||||
"Identity" = "Identity";
|
||||
|
||||
/* Section heading in settings. [inara.py] */
|
||||
"Inara credentials" = "Inara credentials";
|
||||
|
||||
/* Hotkey/Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"Keyboard shortcut" = "Keyboard shortcut";
|
||||
|
||||
@ -431,7 +440,10 @@
|
||||
"Semi Professional" = "Semi Professional";
|
||||
|
||||
/* [edsm.py] */
|
||||
"Send flight log to Elite Dangerous Star Map" = "Send flight log to Elite Dangerous Star Map";
|
||||
"Send flight log and Cmdr status to EDSM" = "Send flight log and Cmdr status to EDSM";
|
||||
|
||||
/* [inara.py] */
|
||||
"Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara";
|
||||
|
||||
/* Output setting. [prefs.py] */
|
||||
"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network";
|
||||
|
@ -9,6 +9,7 @@ This app downloads your Cmdr's details and system, faction, scan and station dat
|
||||
* saves station commodity market prices to files on your computer that you can load into trading tools such as [Trade Dangerous](https://bitbucket.org/kfsone/tradedangerous/wiki/Home), [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 Cmdr's details, ship details, materials and flight log to [Elite: Dangerous Star Map](http://www.edsm.net/).
|
||||
* sends your Cmdr's details, ship details, materials and flight log to [Inara](https://inara.cz).
|
||||
|
||||
You can run the app on the same machine on which you're running Elite: Dangerous or on another machine connected via a network share.
|
||||
|
||||
@ -85,7 +86,11 @@ Some options work by reading the Elite: Dangerous game's log files. If you're ru
|
||||
|
||||
### EDSM
|
||||
|
||||
You can send a record of your Cmdr's details, ship details, materials and flight log to [Elite: Dangerous Star Map](http://www.edsm.net/). 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.
|
||||
You can send a record of your Cmdr's details, ship details, cargo, materials and flight log to [Elite: Dangerous Star Map](http://www.edsm.net/). 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.
|
||||
|
||||
### Inara
|
||||
|
||||
You can send a record of your Cmdr's details, ship details, cargo, materials and flight log to [Inara](https://inara.cz/). You will need to register for an account and then follow the “[Inara credentials](https://inara.cz/settings-api/)” link to obtain your API key.
|
||||
|
||||
|
||||
Uninstall
|
||||
|
@ -93,7 +93,7 @@ def plugin_prefs(parent, cmdr, is_beta):
|
||||
|
||||
HyperlinkLabel(frame, 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
|
||||
this.log = tk.IntVar(value = config.getint('edsm_out') and 1)
|
||||
this.log_button = nb.Checkbutton(frame, text=_('Send flight log to Elite Dangerous Star Map'), variable=this.log, command=prefsvarchanged)
|
||||
this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to EDSM'), variable=this.log, command=prefsvarchanged)
|
||||
this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
|
||||
|
||||
nb.Label(frame).grid(sticky=tk.W) # big spacer
|
||||
|
349
plugins/inara.py
Normal file
349
plugins/inara.py
Normal file
@ -0,0 +1,349 @@
|
||||
#
|
||||
# Inara sync
|
||||
#
|
||||
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
import urllib2
|
||||
from calendar import timegm
|
||||
from Queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
import Tkinter as tk
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
import myNotebook as nb
|
||||
|
||||
from config import appname, applongname, appversion, config
|
||||
import companion
|
||||
import coriolis
|
||||
import edshipyard
|
||||
import outfitting
|
||||
import plug
|
||||
|
||||
if __debug__:
|
||||
from traceback import print_exc
|
||||
|
||||
_TIMEOUT = 20
|
||||
FAKE = ['CQC', 'Training', 'Destination'] # Fake systems that shouldn't be sent to Inara
|
||||
|
||||
|
||||
this = sys.modules[__name__] # For holding module globals
|
||||
this.session = requests.Session()
|
||||
this.queue = Queue() # Items to be sent to Inara by worker thread
|
||||
|
||||
# Game state
|
||||
this.multicrew = False # don't send captain's ship info to Inara while on a crew
|
||||
|
||||
# Cached Cmdr state
|
||||
this.location = None
|
||||
this.cargo = None
|
||||
this.materials = None
|
||||
|
||||
def plugin_start():
|
||||
|
||||
# Migrate old settings
|
||||
if not config.get('inara_cmdrs'):
|
||||
if isinstance(config.get('cmdrs'), list) and config.get('inara_usernames') and config.get('inara_apikeys'):
|
||||
# Migrate <= 2.34 settings
|
||||
config.set('inara_cmdrs', config.get('cmdrs'))
|
||||
elif config.get('inara_cmdrname'):
|
||||
# Migrate <= 2.25 settings. inara_cmdrs is unknown at this time
|
||||
config.set('inara_usernames', [config.get('inara_cmdrname') or ''])
|
||||
config.set('inara_apikeys', [config.get('inara_apikey') or ''])
|
||||
config.delete('inara_cmdrname')
|
||||
config.delete('inara_apikey')
|
||||
if config.getint('output') & 256:
|
||||
# Migrate <= 2.34 setting
|
||||
config.set('inara_out', 1)
|
||||
config.delete('inara_autoopen')
|
||||
config.delete('inara_historical')
|
||||
|
||||
this.thread = Thread(target = worker, name = 'Inara worker')
|
||||
this.thread.daemon = True
|
||||
this.thread.start()
|
||||
|
||||
return 'Inara'
|
||||
|
||||
def plugin_close():
|
||||
# Signal thread to close and wait for it
|
||||
this.queue.put(None)
|
||||
this.thread.join()
|
||||
this.thread = None
|
||||
|
||||
def plugin_prefs(parent, cmdr, is_beta):
|
||||
|
||||
PADX = 10
|
||||
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
||||
PADY = 2 # close spacing
|
||||
|
||||
frame = nb.Frame(parent)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
HyperlinkLabel(frame, text='Inara', background=nb.Label().cget('background'), url='https://inara.cz/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate
|
||||
this.log = tk.IntVar(value = config.getint('inara_out') and 1)
|
||||
this.log_button = nb.Checkbutton(frame, text=_('Send flight log and Cmdr status to Inara'), variable=this.log, command=prefsvarchanged)
|
||||
this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
|
||||
|
||||
nb.Label(frame).grid(sticky=tk.W) # big spacer
|
||||
this.label = HyperlinkLabel(frame, text=_('Inara credentials'), background=nb.Label().cget('background'), url='https://inara.cz/settings-api', underline=True) # Section heading in settings
|
||||
this.label.grid(columnspan=2, padx=PADX, sticky=tk.W)
|
||||
|
||||
this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting
|
||||
this.apikey_label.grid(row=12, padx=PADX, sticky=tk.W)
|
||||
this.apikey = nb.Entry(frame)
|
||||
this.apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
|
||||
|
||||
prefs_cmdr_changed(cmdr, is_beta)
|
||||
|
||||
return frame
|
||||
|
||||
def prefs_cmdr_changed(cmdr, is_beta):
|
||||
this.log_button['state'] = cmdr and not is_beta and tk.NORMAL or tk.DISABLED
|
||||
this.apikey['state'] = tk.NORMAL
|
||||
this.apikey.delete(0, tk.END)
|
||||
if cmdr:
|
||||
cred = credentials(cmdr)
|
||||
if cred:
|
||||
this.apikey.insert(0, cred)
|
||||
this.label['state'] = this.apikey_label['state'] = this.apikey['state'] = cmdr and not is_beta and this.log.get() and tk.NORMAL or tk.DISABLED
|
||||
|
||||
def prefsvarchanged():
|
||||
this.label['state'] = this.apikey_label['state'] = this.apikey['state'] = this.log.get() and this.log_button['state'] or tk.DISABLED
|
||||
|
||||
def prefs_changed(cmdr, is_beta):
|
||||
config.set('inara_out', this.log.get())
|
||||
|
||||
print 'prefs_changed', cmdr, is_beta
|
||||
|
||||
if cmdr and not is_beta:
|
||||
cmdrs = config.get('inara_cmdrs') or []
|
||||
apikeys = config.get('inara_apikeys') or []
|
||||
if cmdr in cmdrs:
|
||||
idx = cmdrs.index(cmdr)
|
||||
apikeys.extend([''] * (1 + idx - len(apikeys)))
|
||||
apikeys[idx] = this.apikey.get().strip()
|
||||
else:
|
||||
config.set('inara_cmdrs', cmdrs + [cmdr])
|
||||
apikeys.append(this.apikey.get().strip())
|
||||
config.set('inara_apikeys', apikeys)
|
||||
# TODO: schedule a call with callback if changed
|
||||
|
||||
def credentials(cmdr):
|
||||
# Credentials for cmdr
|
||||
if not cmdr:
|
||||
return None
|
||||
|
||||
cmdrs = config.get('inara_cmdrs') or []
|
||||
if cmdr in cmdrs and config.get('inara_apikeys'):
|
||||
return config.get('inara_apikeys')[cmdrs.index(cmdr)]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||
|
||||
this.multicrew = bool(state['Role'])
|
||||
|
||||
if entry['event'] == 'LoadGame':
|
||||
# clear cached state
|
||||
this.location = None
|
||||
this.cargo = None
|
||||
this.materials = None
|
||||
|
||||
# Send location and status on new game or StartUp. Assumes Location is the last event on a new game (other than Docked).
|
||||
# Always send an update on Docked, Undocked, FSDJump, Promotion and EngineerProgress.
|
||||
# Also send material and cargo (if changed) whenever we send an update.
|
||||
|
||||
if config.getint('inara_out') and not is_beta and not multicrew and credentials(cmdr):
|
||||
try:
|
||||
events = []
|
||||
|
||||
# Send credits to Inara on new game (but not on startup - data might be old)
|
||||
if entry['event'] == 'Location':
|
||||
# TODO: 'commanderAssets'
|
||||
add_event(events, 'setCommanderCredits', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('commanderCredits', state['Credits']),
|
||||
('commanderLoan', state['Loan']),
|
||||
]))
|
||||
|
||||
# Send rank info to Inara on startup or change
|
||||
if entry['event'] in ['StartUp', 'Location'] and state['Rank']:
|
||||
for k,v in state['Rank'].iteritems():
|
||||
if v is not None:
|
||||
add_event(events, 'setCommanderRankPilot', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('rankName', k.lower()),
|
||||
('rankValue', v[0]),
|
||||
('rankProgress', v[1] / 100.0),
|
||||
]))
|
||||
elif entry['event'] == 'Promotion':
|
||||
for k,v in state['Rank'].iteritems():
|
||||
if k in entry:
|
||||
add_event(events, 'setCommanderRankPilot', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('rankName', k.lower()),
|
||||
('rankValue', v[0]),
|
||||
('rankProgress', 0),
|
||||
]))
|
||||
|
||||
# Send engineer status to Inara on change (not available on startup)
|
||||
if entry['event'] == 'EngineerProgress':
|
||||
if 'Rank' in entry:
|
||||
add_event(events, 'setCommanderRankEngineer', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('engineerName', entry['Engineer']),
|
||||
('rankValue', entry['Rank']),
|
||||
]))
|
||||
else:
|
||||
add_event(events, 'setCommanderRankEngineer', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('engineerName', entry['Engineer']),
|
||||
('rankStage', entry['Progress']),
|
||||
]))
|
||||
|
||||
# Send PowerPlay status to Inara on change (not available on startup, and promotion not available at all)
|
||||
if entry['event'] == 'PowerplayJoin':
|
||||
add_event(events, 'setCommanderRankPower', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('powerName', entry['Power']),
|
||||
('rankValue', 1),
|
||||
]))
|
||||
elif entry['event'] == 'PowerplayLeave':
|
||||
add_event(events, 'setCommanderRankPower', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('powerName', entry['Power']),
|
||||
('rankValue', 0),
|
||||
]))
|
||||
elif entry['event'] == 'PowerplayDefect':
|
||||
add_event(events, 'setCommanderRankPower', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('powerName', entry['ToPower']),
|
||||
('rankValue', 1),
|
||||
]))
|
||||
|
||||
# Update location
|
||||
if entry['event'] == 'Location':
|
||||
if entry.get('Docked'):
|
||||
add_event(events, 'setCommanderTravelLocation', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('starsystemName', entry['StarSystem']),
|
||||
('stationName', entry['StationName']),
|
||||
('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
|
||||
('shipGameID', state['ShipID']),
|
||||
]))
|
||||
this.location = (entry['StarSystem'], entry['StationName'])
|
||||
else:
|
||||
add_event(events, 'setCommanderTravelLocation', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('starsystemName', entry['StarSystem']),
|
||||
('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
|
||||
('shipGameID', state['ShipID']),
|
||||
]))
|
||||
this.location = (entry['StarSystem'], None)
|
||||
|
||||
elif entry['event'] == 'Docked' and this.location != (entry['StarSystem'], entry['StationName']):
|
||||
# Don't send docked event on new game - i.e. following 'Location' event
|
||||
add_event(events, 'addCommanderTravelDock', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('starsystemName', entry['StarSystem']),
|
||||
('stationName', entry['StationName']),
|
||||
('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
|
||||
('shipGameID', state['ShipID']),
|
||||
]))
|
||||
this.location = (entry['StarSystem'], entry['StationName'])
|
||||
|
||||
elif entry['event'] == 'Undocked' and this.location:
|
||||
add_event(events, 'setCommanderTravelLocation', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('starsystemName', this.location[0]),
|
||||
('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
|
||||
('shipGameID', state['ShipID']),
|
||||
]))
|
||||
this.location = (this.location[0], None)
|
||||
|
||||
elif entry['event'] == 'FSDJump':
|
||||
add_event(events, 'addCommanderTravelFSDJump', entry['timestamp'],
|
||||
OrderedDict([
|
||||
('starsystemName', entry['StarSystem']),
|
||||
('jumpDistance', entry['JumpDist']),
|
||||
('shipType', companion.ship_map.get(state['ShipType'], state['ShipType'])),
|
||||
('shipGameID', state['ShipID']),
|
||||
]))
|
||||
this.location = (entry['StarSystem'], None)
|
||||
|
||||
if events:
|
||||
# Send cargo and materials to Inara if changed and if we're sending any other kind of update
|
||||
cargo = [ OrderedDict([('itemName', k), ('itemCount', state['Cargo'][k])]) for k in sorted(state['Cargo']) ]
|
||||
if this.cargo != cargo:
|
||||
add_event(events, 'setCommanderInventoryCargo', entry['timestamp'], cargo)
|
||||
this.cargo = cargo
|
||||
materials = []
|
||||
for category in ['Raw', 'Manufactured', 'Encoded']:
|
||||
materials.extend([ OrderedDict([('itemName', k), ('itemCount', state[category][k])]) for k in sorted(state[category]) ])
|
||||
if this.materials != materials:
|
||||
add_event(events, 'setCommanderInventoryMaterials', entry['timestamp'], materials)
|
||||
this.materials = materials
|
||||
|
||||
# Queue a call to Inara
|
||||
call(cmdr, events)
|
||||
|
||||
except Exception as e:
|
||||
if __debug__: print_exc()
|
||||
return unicode(e)
|
||||
|
||||
def add_event(events, name, timestamp, data):
|
||||
events.append(OrderedDict([
|
||||
('eventName', name),
|
||||
('eventTimestamp', timestamp),
|
||||
('eventData', data),
|
||||
]))
|
||||
|
||||
|
||||
# Queue a call to Inara, handled in Worker thread
|
||||
def call(cmdr, events, callback=None):
|
||||
args = OrderedDict([
|
||||
('header', OrderedDict([
|
||||
('appName', applongname),
|
||||
('appVersion', appversion),
|
||||
('isDeveloped', True), # TODO: Remove before release
|
||||
('APIkey', credentials(cmdr)),
|
||||
('commanderName', cmdr.encode('utf-8')),
|
||||
])),
|
||||
('events', events),
|
||||
])
|
||||
this.queue.put(('https://inara.cz/inapi/v1/', json.dumps(args, separators = (',', ':')), None))
|
||||
|
||||
# Worker thread
|
||||
def worker():
|
||||
while True:
|
||||
item = this.queue.get()
|
||||
if not item:
|
||||
return # Closing
|
||||
else:
|
||||
(url, data, callback) = item
|
||||
|
||||
retrying = 0
|
||||
while retrying < 3:
|
||||
try:
|
||||
r = this.session.post(url, data=data, timeout=_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
reply = r.json()
|
||||
status = reply['header']['eventStatus']
|
||||
if callback:
|
||||
callback(reply)
|
||||
elif status // 100 != 2: # 2xx == OK (maybe with warnings)
|
||||
plug.show_error(_('Error: Inara {MSG}').format(MSG = reply['header'].get('eventStatusText', status)))
|
||||
if __debug__: print r.content
|
||||
break
|
||||
except:
|
||||
if __debug__: print_exc()
|
||||
retrying += 1
|
||||
else:
|
||||
if callback:
|
||||
callback(None)
|
||||
else:
|
||||
plug.show_error(_("Error: Can't connect to Inara"))
|
Loading…
x
Reference in New Issue
Block a user