# # Inara sync # from collections import OrderedDict import json import requests import sys import time from operator import itemgetter 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 plug if __debug__: from traceback import print_exc _TIMEOUT = 20 FAKE = ['CQC', 'Training', 'Destination'] # Fake systems that shouldn't be sent to Inara CREDIT_RATIO = 1.05 # Update credits if they change by 5% over the course of a session this = sys.modules[__name__] # For holding module globals this.session = requests.Session() this.queue = Queue() # Items to be sent to Inara by worker thread # Cached Cmdr state this.events = [] # Unsent events this.cmdr = None this.multicrew = False # don't send captain's ship info to Inara while on a crew this.newuser = False # just entered API Key this.undocked = False # just undocked this.suppress_docked = False # Skip initial Docked event if started docked this.cargo = None this.materials = None this.lastcredits = 0 # Send credit update soon after Startup / new game this.storedmodules = None this.loadout = None this.fleet = None this.shipswap = False # just swapped ship # Main window clicks this.system = None this.station = None def system_url(system_name): return this.system def station_url(system_name, station_name): return this.station def plugin_start(): this.thread = Thread(target = worker, name = 'Inara worker') this.thread.daemon = True this.thread.start() return 'Inara' def plugin_stop(): # Send any unsent events call() # 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): changed = config.getint('inara_out') != this.log.get() config.set('inara_out', this.log.get()) if cmdr and not is_beta: this.cmdr = cmdr 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))) changed |= (apikeys[idx] != this.apikey.get().strip()) apikeys[idx] = this.apikey.get().strip() else: config.set('inara_cmdrs', cmdrs + [cmdr]) changed = True apikeys.append(this.apikey.get().strip()) config.set('inara_apikeys', apikeys) if this.log.get() and changed: this.newuser = True # Send basic info at next Journal event add_event('getCommanderProfile', time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), { 'searchName': cmdr }) call() 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): # Send any unsent events when switching accounts if cmdr and cmdr != this.cmdr: call() this.cmdr = cmdr this.multicrew = bool(state['Role']) if entry['event'] == 'LoadGame': # clear cached state this.undocked = False this.suppress_docked = False this.cargo = None this.materials = None this.lastcredits = 0 this.storedmodules = None this.loadout = None this.fleet = None this.shipswap = False elif entry['event'] in ['Resurrect', 'ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy']: # Events that mean a significant change in credits so we should send credits after next "Update" this.lastcredits = 0 elif entry['event'] in ['ShipyardNew', 'ShipyardSwap'] or (entry['event'] == 'Location' and entry['Docked']): this.suppress_docked = True # Send location and status on new game or StartUp. Assumes Cargo is the last event on a new game (other than Docked). # Always send an update on Docked, FSDJump, Undocked+SuperCruise, 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 this.multicrew and credentials(cmdr): try: old_events = len(this.events) # Will only send existing events if we add a new event below # Send rank info to Inara on startup or change if (entry['event'] in ['StartUp', 'Cargo'] or this.newuser): for k,v in state['Rank'].iteritems(): if v is not None: add_event('setCommanderRankPilot', entry['timestamp'], OrderedDict([ ('rankName', k.lower()), ('rankValue', v[0]), ('rankProgress', v[1] / 100.0), ])) for k,v in state['Reputation'].iteritems(): if v is not None: add_event('setCommanderReputationMajorFaction', entry['timestamp'], OrderedDict([ ('majorfactionName', k.lower()), ('majorfactionReputation', v / 100.0), ])) elif entry['event'] == 'Promotion': for k,v in state['Rank'].iteritems(): if k in entry: add_event('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('setCommanderRankEngineer', entry['timestamp'], OrderedDict([ ('engineerName', entry['Engineer']), ('rankValue', entry['Rank']), ])) else: add_event('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('setCommanderRankPower', entry['timestamp'], OrderedDict([ ('powerName', entry['Power']), ('rankValue', 1), ])) elif entry['event'] == 'PowerplayLeave': add_event('setCommanderRankPower', entry['timestamp'], OrderedDict([ ('powerName', entry['Power']), ('rankValue', 0), ])) elif entry['event'] == 'PowerplayDefect': add_event('setCommanderRankPower', entry['timestamp'], OrderedDict([ ('powerName', entry['ToPower']), ('rankValue', 1), ])) # Update ship if (state['ShipID'] and # Unknown if started in Fighter or SRV (entry['event'] in ['StartUp', 'Cargo'] or (entry['event'] == 'Loadout' and this.shipswap) or this.newuser)): data = OrderedDict([ ('shipType', state['ShipType']), ('shipGameID', state['ShipID']), ('shipName', state['ShipName']), # Can be None ('shipIdent', state['ShipIdent']), # Can be None ('isCurrentShip', True), ]) if state['HullValue']: data['shipHullValue'] = state['HullValue'] if state['ModulesValue']: data['shipModulesValue'] = state['ModulesValue'] data['shipRebuyCost'] = state['Rebuy'] add_event('setCommanderShip', entry['timestamp'], data) this.loadout = make_loadout(state) add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout) this.shipswap = False # Update location if (entry['event'] in ['StartUp', 'Cargo'] or this.newuser) and system: this.undocked = False add_event('setCommanderTravelLocation', entry['timestamp'], OrderedDict([ ('starsystemName', system), ('stationName', station), # Can be None ])) elif entry['event'] == 'Docked': if this.undocked: # Undocked and now docking again. Don't send. this.undocked = False elif this.suppress_docked: # Don't send initial Docked event on new game this.suppress_docked = False else: add_event('addCommanderTravelDock', entry['timestamp'], OrderedDict([ ('starsystemName', system), ('stationName', station), ('shipType', state['ShipType']), ('shipGameID', state['ShipID']), ])) elif entry['event'] == 'Undocked': this.undocked = True elif entry['event'] == 'SupercruiseEntry': if this.undocked: # Staying in system after undocking - send any pending events from in-station action add_event('setCommanderTravelLocation', entry['timestamp'], OrderedDict([ ('starsystemName', system), ('shipType', state['ShipType']), ('shipGameID', state['ShipID']), ])) this.undocked = False elif entry['event'] == 'FSDJump': this.undocked = False add_event('addCommanderTravelFSDJump', entry['timestamp'], OrderedDict([ ('starsystemName', entry['StarSystem']), ('jumpDistance', entry['JumpDist']), ('shipType', state['ShipType']), ('shipGameID', state['ShipID']), ])) if entry['event'] == 'ShutDown' or len(this.events) > old_events: # We have new event(s) so send to Inara # Send cargo and materials if changed cargo = [ OrderedDict([('itemName', k), ('itemCount', state['Cargo'][k])]) for k in sorted(state['Cargo']) ] if this.cargo != cargo: add_event('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('setCommanderInventoryMaterials', entry['timestamp'], materials) this.materials = materials # Queue a call to Inara call() except Exception as e: if __debug__: print_exc() return unicode(e) # # Events that don't need to be sent immediately but will be sent on the next mandatory event # # Send credits and stats to Inara on startup only - otherwise may be out of date if entry['event'] == 'LoadGame': add_event('setCommanderCredits', entry['timestamp'], OrderedDict([ ('commanderCredits', state['Credits']), ('commanderLoan', state['Loan']), ])) this.lastcredits = state['Credits'] elif entry['event'] == 'Statistics': add_event('setCommanderGameStatistics', entry['timestamp'], state['Statistics']) # may be out of date # Selling / swapping ships if entry['event'] == 'ShipyardNew': add_event('addCommanderShip', entry['timestamp'], OrderedDict([ ('shipType', entry['ShipType']), ('shipGameID', entry['NewShipID']), ])) this.shipswap = True # Want subsequent Loadout event to be sent immediately elif entry['event'] in ['ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy', 'ShipyardSwap']: if entry['event'] == 'ShipyardSwap': this.shipswap = True # Don't know new ship name and ident 'til the following Loadout event if 'StoreShipID' in entry: add_event('setCommanderShip', entry['timestamp'], OrderedDict([ ('shipType', entry['StoreOldShip']), ('shipGameID', entry['StoreShipID']), ('starsystemName', system), ('stationName', station), ])) elif 'SellShipID' in entry: add_event('delCommanderShip', entry['timestamp'], OrderedDict([ ('shipType', entry.get('SellOldShip', entry['ShipType'])), ('shipGameID', entry['SellShipID']), ])) elif entry['event'] == 'SetUserShipName': add_event('setCommanderShip', entry['timestamp'], OrderedDict([ ('shipType', state['ShipType']), ('shipGameID', state['ShipID']), ('shipName', state['ShipName']), # Can be None ('shipIdent', state['ShipIdent']), # Can be None ('isCurrentShip', True), ])) elif entry['event'] == 'ShipyardTransfer': add_event('setCommanderShipTransfer', entry['timestamp'], OrderedDict([ ('shipType', entry['ShipType']), ('shipGameID', entry['ShipID']), ('starsystemName', system), ('stationName', station), ('transferTime', entry['TransferTime']), ])) # Fleet if entry['event'] == 'StoredShips': fleet = sorted( [{ 'shipType': x['ShipType'], 'shipGameID': x['ShipID'], 'shipName': x.get('Name'), 'isHot': x['Hot'], 'starsystemName': entry['StarSystem'], 'stationName': entry['StationName'], 'marketID': entry['MarketID'], } for x in entry['ShipsHere']] + [{ 'shipType': x['ShipType'], 'shipGameID': x['ShipID'], 'shipName': x.get('Name'), 'isHot': x['Hot'], 'starsystemName': x.get('StarSystem'), # Not present for ships in transit 'marketID': x.get('ShipMarketID'), # " } for x in entry['ShipsRemote']], key = itemgetter('shipGameID') ) if this.fleet != fleet: this.fleet = fleet this.events = [x for x in this.events if x['eventName'] != 'setCommanderShip'] # Remove any unsent for ship in this.fleet: add_event('setCommanderShip', entry['timestamp'], ship) # Loadout if entry['event'] == 'Loadout': loadout = make_loadout(state) if this.loadout != loadout: this.loadout = loadout this.events = [x for x in this.events if x['eventName'] != 'setCommanderShipLoadout' or x['shipGameID'] != this.loadout['shipGameID']] # Remove any unsent for this ship add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout) # Stored modules if entry['event'] == 'StoredModules': items = dict([(x['StorageSlot'], x) for x in entry['Items']]) # Impose an order modules = [] for slot in sorted(items): item = items[slot] module = OrderedDict([ ('itemName', item['Name']), ('itemValue', item['BuyPrice']), ('isHot', item['Hot']), ]) # Location can be absent if in transit if 'StarSystem' in item: module['starsystemName'] = item['StarSystem'] if 'MarketID' in item: module['marketID'] = item['MarketID'] if 'EngineerModifications' in item: module['engineering'] = OrderedDict([('blueprintName', item['EngineerModifications'])]) if 'Level' in item: module['engineering']['blueprintLevel'] = item['Level'] if 'Quality' in item: module['engineering']['blueprintQuality'] = item['Quality'] modules.append(module) if this.storedmodules != modules: # Only send on change this.storedmodules = modules this.events = [x for x in this.events if x['eventName'] != 'setCommanderStorageModules'] # Remove any unsent add_event('setCommanderStorageModules', entry['timestamp'], this.storedmodules) # Missions if entry['event'] == 'MissionAccepted': data = OrderedDict([ ('missionName', entry['Name']), ('missionGameID', entry['MissionID']), ('influenceGain', entry['Influence']), ('reputationGain', entry['Reputation']), ('starsystemNameOrigin', system), ('stationNameOrigin', station), ('minorfactionNameOrigin', entry['Faction']), ]) # optional mission-specific properties for (iprop, prop) in [ ('missionExpiry', 'Expiry'), # Listed as optional in the docs, but always seems to be present ('starsystemNameTarget', 'DestinationSystem'), ('stationNameTarget', 'DestinationStation'), ('minorfactionNameTarget', 'TargetFaction'), ('commodityName', 'Commodity'), ('commodityCount', 'Count'), ('targetName', 'Target'), ('targetType', 'TargetType'), ('killCount', 'KillCount'), ('passengerType', 'PassengerType'), ('passengerCount', 'PassengerCount'), ('passengerIsVIP', 'PassengerVIPs'), ('passengerIsWanted', 'PassengerWanted'), ]: if prop in entry: data[iprop] = entry[prop] add_event('addCommanderMission', entry['timestamp'], data) elif entry['event'] == 'MissionAbandoned': add_event('setCommanderMissionAbandoned', entry['timestamp'], { 'missionGameID': entry['MissionID'] }) elif entry['event'] == 'MissionCompleted': for x in entry.get('PermitsAwarded', []): add_event('addCommanderPermit', entry['timestamp'], { 'starsystemName': x }) data = OrderedDict([ ('missionGameID', entry['MissionID']) ]) if 'Donation' in entry: data['donationCredits'] = entry['Donation'] if 'Reward' in entry: data['rewardCredits'] = entry['Reward'] if 'PermitsAwarded' in entry: data['rewardPermits'] = [{ 'starsystemName': x } for x in entry['PermitsAwarded']] if 'CommodityReward' in entry: data['rewardCommodities'] = [{ 'itemName': x['Name'], 'itemCount': x['Count'] } for x in entry['CommodityReward']] if 'MaterialsReward' in entry: data['rewardMaterials'] = [{ 'itemName': x['Name'], 'itemCount': x['Count'] } for x in entry['MaterialsReward']] add_event('setCommanderMissionCompleted', entry['timestamp'], data) elif entry['event'] == 'MissionFailed': add_event('setCommanderMissionFailed', entry['timestamp'], { 'missionGameID': entry['MissionID'] }) # Combat if entry['event'] == 'Died': data = OrderedDict([ ('starsystemName', system) ]) if 'Killers' in entry: data['wingOpponentNames'] = [x['Name'] for x in entry['Killers']] elif 'KillerName' in entry: data['opponentName'] = entry['KillerName'] add_event('addCommanderCombatDeath', entry['timestamp'], data) elif entry['event'] == 'Interdicted': add_event('addCommanderCombatInterdicted', entry['timestamp'], OrderedDict([('starsystemName', system), ('opponentName', entry['Interdictor']), ('isPlayer', entry['IsPlayer']), ('isSubmit', entry['Submitted']), ])) elif entry['event'] == 'Interdiction': data = OrderedDict([('starsystemName', system), ('isPlayer', entry['IsPlayer']), ('isSuccess', entry['Success']), ]) if 'Interdictor' in entry: data['opponentName'] = entry['Interdictor'] elif 'Faction' in entry: data['opponentName'] = entry['Faction'] elif 'Power' in entry: data['opponentName'] = entry['Power'] add_event('addCommanderCombatInterdiction', entry['timestamp'], data) elif entry['event'] == 'EscapeInterdiction': add_event('addCommanderCombatInterdictionEscape', entry['timestamp'], OrderedDict([('starsystemName', system), ('opponentName', entry['Interdictor']), ('isPlayer', entry['IsPlayer']), ])) elif entry['event'] == 'PVPKill': add_event('addCommanderCombatKill', entry['timestamp'], OrderedDict([('starsystemName', system), ('opponentName', entry['Victim']), ])) # Community Goals if entry['event'] == 'CommunityGoal': this.events = [x for x in this.events if x['eventName'] not in ['setCommunityGoal', 'setCommanderCommunityGoalProgress']] # Remove any unsent for goal in entry['CurrentGoals']: data = OrderedDict([ ('communitygoalGameID', goal['CGID']), ('communitygoalName', goal['Title']), ('starsystemName', goal['SystemName']), ('stationName', goal['MarketName']), ('goalExpiry', goal['Expiry']), ('isCompleted', goal['IsComplete']), ('contributorsNum', goal['NumContributors']), ('contributionsTotal', goal['CurrentTotal']), ]) if 'TierReached' in goal: data['tierReached'] = int(goal['TierReached'].split()[-1]) if 'TopRankSize' in goal: data['topRankSize'] = goal['TopRankSize'] if 'TopTier' in goal: data['tierMax'] = int(goal['TopTier']['Name'].split()[-1]) data['completionBonus'] = goal['TopTier']['Bonus'] add_event('setCommunityGoal', entry['timestamp'], data) data = OrderedDict([ ('communitygoalGameID', goal['CGID']), ('contribution', goal['PlayerContribution']), ('percentileBand', goal['PlayerPercentileBand']), ]) if 'Bonus' in goal: data['percentileBandReward'] = goal['Bonus'] if 'PlayerInTopRank' in goal: data['isTopRank'] = goal['PlayerInTopRank'] add_event('setCommanderCommunityGoalProgress', entry['timestamp'], data) this.newuser = False def cmdr_data(data, is_beta): this.cmdr = data['commander']['name'] if config.getint('inara_out') and not is_beta and not this.multicrew and credentials(this.cmdr): if not (CREDIT_RATIO > this.lastcredits / data['commander']['credits'] > 1/CREDIT_RATIO): this.events = [x for x in this.events if x['eventName'] != 'setCommanderCredits'] # Remove any unsent add_event('setCommanderCredits', time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), OrderedDict([ ('commanderCredits', data['commander']['credits']), ('commanderLoan', data['commander'].get('debt', 0)), ])) this.lastcredits = float(data['commander']['credits']) def make_loadout(state): modules = [] for m in state['Modules'].itervalues(): module = OrderedDict([ ('slotName', m['Slot']), ('itemName', m['Item']), ('itemHealth', m['Health']), ('isOn', m['On']), ('itemPriority', m['Priority']), ]) if 'AmmoInClip' in m: module['itemAmmoClip'] = m['AmmoInClip'] if 'AmmoInHopper' in m: module['itemAmmoHopper'] = m['AmmoInHopper'] if 'Value' in m: module['itemValue'] = m['Value'] if 'Hot' in m: module['isHot'] = m['Hot'] if 'Engineering' in m: engineering = OrderedDict([ ('blueprintName', m['Engineering']['BlueprintName']), ('blueprintLevel', m['Engineering']['Level']), ('blueprintQuality', m['Engineering']['Quality']), ]) if 'ExperimentalEffect' in m['Engineering']: engineering['experimentalEffect'] = m['Engineering']['ExperimentalEffect'] engineering['modifiers'] = [] for mod in m['Engineering']['Modifiers']: modifier = OrderedDict([ ('name', mod['Label']), ('value', mod['Value']), ]) if 'OriginalValue' in mod: modifier['originalValue'] = mod['OriginalValue'] modifier['lessIsGood'] = mod['LessIsGood'] engineering['modifiers'].append(modifier) module['engineering'] = engineering modules.append(module) return OrderedDict([ ('shipType', state['ShipType']), ('shipGameID', state['ShipID']), ('shipLoadout', modules), ]) def add_event(name, timestamp, data): this.events.append(OrderedDict([ ('eventName', name), ('eventTimestamp', timestamp), ('eventData', data), ])) # Queue a call to Inara, handled in Worker thread def call(callback=None): if not this.events: return data = OrderedDict([ ('header', OrderedDict([ ('appName', applongname), ('appVersion', appversion), ('APIkey', credentials(this.cmdr)), ('commanderName', this.cmdr.encode('utf-8')), ])), ('events', list(this.events)), # shallow copy ]) this.events = [] this.queue.put(('https://inara.cz/inapi/v1/', data, 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=json.dumps(data, separators = (',', ':')), 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) # Log fatal errors print 'Inara\t%s %s' % (reply['header']['eventStatus'], reply['header'].get('eventStatusText', '')) print json.dumps(data, indent=2, separators = (',', ': ')) plug.show_error(_('Error: Inara {MSG}').format(MSG = reply['header'].get('eventStatusText', status))) else: # Log individual errors and warnings for data_event, reply_event in zip(data['events'], reply['events']): if reply_event['eventStatus'] != 200: print 'Inara\t%s %s\t%s' % (reply_event['eventStatus'], reply_event.get('eventStatusText', ''), json.dumps(data_event)) if reply_event['eventStatus'] // 100 != 2: plug.show_error(_('Error: Inara {MSG}').format(MSG = '%s, %s' % (data_event['eventName'], reply_event.get('eventStatusText', reply_event['eventStatus'])))) elif data_event['eventName'] in ['addCommanderTravelDock', 'addCommanderTravelFSDJump', 'setCommanderTravelLocation']: eventData = reply_event.get('eventData', {}) this.system = eventData.get('starsystemInaraURL') this.station = eventData.get('stationInaraURL') break except: if __debug__: print_exc() retrying += 1 else: if callback: callback(None) else: plug.show_error(_("Error: Can't connect to Inara"))