mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-14 16:27:13 +03:00
Merge branch 'develop' into cleanup/prefs
This commit is contained in:
commit
20464b80c7
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,18 +1,26 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
about: Report an issue with the program
|
||||
title: ''
|
||||
labels: bug, unconfirmed
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Please check the [Known Issues](https://github.com/EDCD/EDMarketConnector/issues/618) in case this has already been reported.**
|
||||
|
||||
**Please also check if the issue is covered in our [Troubleshooting Guide](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting).** It might be something with a known work around, or where a third party (such as EDSM) is causing logging that is harmless.
|
||||
|
||||
**Please complete the following information:**
|
||||
- Version [e.g. 4.0.6 - See 'Help > About E:D Market Connector']
|
||||
- OS: [e.g. Windows 10]
|
||||
- OS Locale: [e.g. English, French, Serbian...]
|
||||
- If applicable: Browser [e.g. chrome, safari]
|
||||
- Please attach the file `%TEMP%\EDMarketConnector.log` from *immediately* after the bug occurs (re-running the application overwrites this file). Use the Icon that looks like `_`, `|` and `^` all in one character towards the right of the toolbar above.
|
||||
- Please attach a log file:
|
||||
1. If running 4.1.0 (including betas) or later: `%TEMP%\EDMarketConnector\EDMarketConnector.log`. See [Debug Log File](https://github.com/EDCD/EDMarketConnector/wiki/Troubleshooting#debug-log-files).
|
||||
1. Else `%TEMP%\EDMarketConnector.log` from *immediately* after the bug occurs (re-running the application overwrites this file).
|
||||
|
||||
Use the Icon that looks like `_`, `|` and `^` all in one character towards the right of the toolbar above.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
45
ChangeLog.md
45
ChangeLog.md
@ -1,6 +1,51 @@
|
||||
This is the master changelog for Elite Dangerous Market Connector. Entries are in reverse chronological order (latest first).
|
||||
---
|
||||
|
||||
Pre-Release 4.1.0-beta6
|
||||
===
|
||||
|
||||
* Include a manifest in both EDMarketConnector.exe and EDMC.exe to specify
|
||||
they're Unicode applications so that they default to using the UTF-8
|
||||
codepage. Fixes [#711](https://github.com/EDCD/EDMarketConnector/issues/711).
|
||||
|
||||
* Add extra debug logging to EDSM plugin for `CarrierJump`, `FSDJump`,
|
||||
`Location` and `Docked` events. Hopefully this will help track down the
|
||||
cause of [#713](https://github.com/EDCD/EDMarketConnector/issues/713).
|
||||
|
||||
* Various code cleanups which shouldn't impact user experience.
|
||||
|
||||
* EDMC.exe will now log useful startup state information if run with the
|
||||
`--loglevel DEBUG` arguments.
|
||||
|
||||
Pre-Release 4.1.0-beta5
|
||||
===
|
||||
|
||||
* We are now explicitly setting a UTF8 encoding at startup. This *shouldn't*
|
||||
have any side effects and has allowed us to switch to the native tkinter
|
||||
file dialogues rather than some custom code.
|
||||
|
||||
If you do encounter errors that might be related to this then it would be
|
||||
useful to see the logging output that details the Locale settings at
|
||||
various points during startup. Examples might include incorrect text being
|
||||
rendered for your language when you have it set, or issues with filenames
|
||||
and their content, but any of these are unlikely.
|
||||
|
||||
* The error `'list' object has no attribute 'values'` should now be fixed.
|
||||
|
||||
* Code dealing with Frontier's CAPI was cleaned up, so please report any
|
||||
issues related to that (mostly when just docked or when you press the Update
|
||||
button).
|
||||
|
||||
* Extra logging added for when we process FSDJump and CarrierJump events for
|
||||
EDSM. This is aimed at checking if we do have a bug with CarrierJump events,
|
||||
but having FSDJump trigger the logging as well made it easier to test.
|
||||
|
||||
* Default `logging` level for plugins is now DEBUG. This won't change what's
|
||||
actually logged, it just ensures that everything gets through to the two
|
||||
channels that then decide what is output.
|
||||
|
||||
* Translations updated. Thanks again to all contributors!
|
||||
|
||||
Pre-Release 4.1.0-beta4
|
||||
===
|
||||
|
||||
|
38
EDMC.manifest
Normal file
38
EDMC.manifest
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
|
||||
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0' xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level='asInvoker' uiAccess='false' />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<!-- https://docs.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
|
||||
</application>
|
||||
</compatibility>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<!-- Declare that we want to use the UTF-8 code page -->
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<dependency>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
31
EDMC.py
31
EDMC.py
@ -6,6 +6,7 @@
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@ -14,6 +15,9 @@ from os.path import getmtime, join
|
||||
from time import sleep, time
|
||||
from typing import Any, Optional
|
||||
|
||||
# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
|
||||
os.environ["EDMC_NO_UI"] = "1"
|
||||
|
||||
import collate
|
||||
import commodity
|
||||
import companion
|
||||
@ -36,15 +40,22 @@ sys.path.append(config.internal_plugin_dir)
|
||||
import eddn # noqa: E402
|
||||
# isort: on
|
||||
|
||||
# workaround for https://github.com/EDCD/EDMarketConnector/issues/568
|
||||
os.environ["EDMC_NO_UI"] = "1"
|
||||
|
||||
l10n.Translations.install_dummy()
|
||||
|
||||
logger = EDMCLogging.Logger(appcmdname).get_logger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def log_locale(prefix: str) -> None:
|
||||
logger.debug(f'''Locale: {prefix}
|
||||
Locale LC_COLLATE: {locale.getlocale(locale.LC_COLLATE)}
|
||||
Locale LC_CTYPE: {locale.getlocale(locale.LC_CTYPE)}
|
||||
Locale LC_MONETARY: {locale.getlocale(locale.LC_MONETARY)}
|
||||
Locale LC_NUMERIC: {locale.getlocale(locale.LC_NUMERIC)}
|
||||
Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
|
||||
)
|
||||
|
||||
|
||||
l10n.Translations.install_dummy()
|
||||
|
||||
SERVER_RETRY = 5 # retry pause for Companion servers [s]
|
||||
EXIT_SUCCESS, EXIT_SERVER, EXIT_CREDENTIALS, EXIT_VERIFICATION, EXIT_LAGGING, EXIT_SYS_ERR, EXIT_ARGS = range(7)
|
||||
|
||||
@ -112,7 +123,15 @@ def main():
|
||||
sys.exit(EXIT_ARGS)
|
||||
logger.setLevel(args.loglevel)
|
||||
|
||||
logger.debug('Startup')
|
||||
logger.debug(f'Startup v{appversion} : Running on Python v{sys.version}')
|
||||
logger.debug(f'''Platform: {sys.platform}
|
||||
argv[0]: {sys.argv[0]}
|
||||
exec_prefix: {sys.exec_prefix}
|
||||
executable: {sys.executable}
|
||||
sys.path: {sys.path}'''
|
||||
)
|
||||
|
||||
log_locale('Initial Locale')
|
||||
|
||||
if args.j:
|
||||
logger.debug('Import and collate from JSON dump')
|
||||
|
@ -10,13 +10,14 @@ strings.
|
||||
import inspect
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
# So that any warning about accessing a protected member is only in one place.
|
||||
from sys import _getframe as getframe
|
||||
from typing import Tuple
|
||||
|
||||
from config import appname, config
|
||||
from config import appcmdname, appname, config
|
||||
|
||||
# TODO: Tests:
|
||||
#
|
||||
@ -41,7 +42,7 @@ from config import appname, config
|
||||
#
|
||||
# 14. Call from *package*
|
||||
|
||||
_default_loglevel = logging.INFO
|
||||
_default_loglevel = logging.DEBUG
|
||||
|
||||
|
||||
class Logger:
|
||||
@ -122,7 +123,7 @@ class Logger:
|
||||
return self.logger_channel
|
||||
|
||||
|
||||
def get_plugin_logger(name: str, loglevel: int = _default_loglevel) -> logging.Logger:
|
||||
def get_plugin_logger(plugin_name: str, loglevel: int = _default_loglevel) -> logging.Logger:
|
||||
"""
|
||||
Return a logger suitable for a plugin.
|
||||
|
||||
@ -143,7 +144,12 @@ def get_plugin_logger(name: str, loglevel: int = _default_loglevel) -> logging.L
|
||||
:param loglevel: Optional logLevel for this Logger.
|
||||
:return: logging.Logger instance, all set up.
|
||||
"""
|
||||
plugin_logger = logging.getLogger(name)
|
||||
if not os.getenv('EDMC_NO_UI'):
|
||||
base_logger_name = appname
|
||||
else:
|
||||
base_logger_name = appcmdname
|
||||
|
||||
plugin_logger = logging.getLogger(f'{base_logger_name}.{plugin_name}')
|
||||
plugin_logger.setLevel(loglevel)
|
||||
|
||||
plugin_logger.addFilter(EDMCContextFilter())
|
||||
@ -326,6 +332,18 @@ class EDMCContextFilter(logging.Filter):
|
||||
|
||||
return module_name
|
||||
|
||||
|
||||
def get_main_logger() -> logging.Logger:
|
||||
"""Return the correct logger for how the program is being run."""
|
||||
|
||||
if not os.getenv("EDMC_NO_UI"):
|
||||
# GUI app being run
|
||||
return logging.getLogger(appname)
|
||||
else:
|
||||
# Must be the CLI
|
||||
return logging.getLogger(appcmdname)
|
||||
|
||||
|
||||
# Singleton
|
||||
loglevel = config.get('loglevel')
|
||||
if not loglevel:
|
||||
|
@ -19,6 +19,8 @@
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAware>true</dpiAware>
|
||||
<!-- Declare that we want to use the UTF-8 code page -->
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<dependency>
|
||||
|
@ -23,6 +23,7 @@ if __name__ == "__main__":
|
||||
if getattr(sys, 'frozen', False):
|
||||
# By default py2exe tries to write log to dirname(sys.executable) which fails when installed
|
||||
import tempfile
|
||||
|
||||
# unbuffered not allowed for text in python3, so use `1 for line buffering
|
||||
sys.stdout = sys.stderr = open(join(tempfile.gettempdir(), f'{appname}.log'), mode='wt', buffering=1)
|
||||
|
||||
@ -45,6 +46,7 @@ if __debug__:
|
||||
if platform != 'win32':
|
||||
import pdb
|
||||
import signal
|
||||
|
||||
signal.signal(signal.SIGTERM, lambda sig, frame: pdb.Pdb().set_trace(frame))
|
||||
|
||||
import companion
|
||||
@ -61,7 +63,6 @@ from protocol import protocolhandler
|
||||
from dashboard import dashboard
|
||||
from theme import theme
|
||||
|
||||
|
||||
SERVER_RETRY = 5 # retry pause for Companion servers [s]
|
||||
|
||||
SHIPYARD_HTML_TEMPLATE = """
|
||||
@ -81,7 +82,6 @@ SHIPYARD_HTML_TEMPLATE = """
|
||||
|
||||
|
||||
class AppWindow(object):
|
||||
|
||||
# Tkinter Event types
|
||||
EVENT_KEYPRESS = 2
|
||||
EVENT_BUTTON = 4
|
||||
@ -104,10 +104,14 @@ class AppWindow(object):
|
||||
if platform == 'win32':
|
||||
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
|
||||
else:
|
||||
self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501
|
||||
self.theme_icon = tk.PhotoImage(data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501
|
||||
self.theme_minimize = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
|
||||
self.theme_close = tk.BitmapImage(data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
|
||||
self.w.tk.call('wm', 'iconphoto', self.w, '-default',
|
||||
tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501
|
||||
self.theme_icon = tk.PhotoImage(
|
||||
data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501
|
||||
self.theme_minimize = tk.BitmapImage(
|
||||
data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
|
||||
self.theme_close = tk.BitmapImage(
|
||||
data='#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') # noqa: E501
|
||||
|
||||
frame = tk.Frame(self.w, name=appname.lower())
|
||||
frame.grid(sticky=tk.NSEW)
|
||||
@ -152,7 +156,8 @@ class AppWindow(object):
|
||||
row = frame.grid_size()[1]
|
||||
self.button.grid(row=row, columnspan=2, sticky=tk.NSEW)
|
||||
self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW)
|
||||
theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501
|
||||
theme.register_alternate((self.button, self.theme_button, self.theme_button),
|
||||
{'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501
|
||||
self.status.grid(columnspan=2, sticky=tk.EW)
|
||||
self.button.bind('<Button-1>', self.getandsend)
|
||||
theme.button_bind(self.theme_button, self.getandsend)
|
||||
@ -291,6 +296,7 @@ class AppWindow(object):
|
||||
# Check that the titlebar will be at least partly on screen
|
||||
import ctypes
|
||||
from ctypes.wintypes import POINT
|
||||
|
||||
# https://msdn.microsoft.com/en-us/library/dd145064
|
||||
MONITOR_DEFAULTTONULL = 0 # noqa: N806
|
||||
if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16),
|
||||
@ -322,6 +328,7 @@ class AppWindow(object):
|
||||
|
||||
# Load updater after UI creation (for WinSparkle)
|
||||
import update
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running in frozen .exe, so use (Win)Sparkle
|
||||
self.updater = update.Updater(tkroot=self.w, provider='external')
|
||||
@ -453,7 +460,7 @@ class AppWindow(object):
|
||||
if not retrying:
|
||||
if time() < self.holdofftime: # Was invoked by key while in cooldown
|
||||
self.status['text'] = ''
|
||||
if play_sound and (self.holdofftime-time()) < companion.holdoff*0.75:
|
||||
if play_sound and (self.holdofftime - time()) < companion.holdoff * 0.75:
|
||||
hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats
|
||||
return
|
||||
elif play_sound:
|
||||
@ -493,7 +500,7 @@ class AppWindow(object):
|
||||
if isdir('dump'):
|
||||
with open('dump/{system}{station}.{timestamp}.json'.format(
|
||||
system=data['lastSystem']['name'],
|
||||
station=data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '',
|
||||
station=data['commander'].get('docked') and '.' + data['lastStarport']['name'] or '',
|
||||
timestamp=strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wb') as h:
|
||||
h.write(json.dumps(data,
|
||||
ensure_ascii=False,
|
||||
@ -525,7 +532,7 @@ class AppWindow(object):
|
||||
self.status['text'] = _("You're not docked at a station!")
|
||||
play_bad = True
|
||||
# Ignore possibly missing shipyard info
|
||||
elif (config.getint('output') & config.OUT_MKT_EDDN)\
|
||||
elif (config.getint('output') & config.OUT_MKT_EDDN) \
|
||||
and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')):
|
||||
if not self.status['text']:
|
||||
self.status['text'] = _("Station doesn't have anything!")
|
||||
@ -585,12 +592,12 @@ class AppWindow(object):
|
||||
if not data['commander'].get('docked'):
|
||||
# might have un-docked while we were waiting for retry in which case station data is unreliable
|
||||
pass
|
||||
elif (data.get('lastSystem', {}).get('name') == monitor.system and
|
||||
elif (data.get('lastSystem', {}).get('name') == monitor.system and
|
||||
data.get('lastStarport', {}).get('name') == monitor.station and
|
||||
data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')):
|
||||
self.eddn.export_shipyard(data, monitor.is_beta)
|
||||
elif tries > 1: # bogus data - retry
|
||||
self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries-1))
|
||||
self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries - 1))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -600,11 +607,11 @@ class AppWindow(object):
|
||||
def crewroletext(role):
|
||||
# Return translated crew role. Needs to be dynamic to allow for changing language.
|
||||
return {
|
||||
None: '',
|
||||
'Idle': '',
|
||||
None : '',
|
||||
'Idle' : '',
|
||||
'FighterCon': _('Fighter'), # Multicrew role
|
||||
'FireCon': _('Gunner'), # Multicrew role
|
||||
'FlightCon': _('Helm'), # Multicrew role
|
||||
'FireCon' : _('Gunner'), # Multicrew role
|
||||
'FlightCon' : _('Helm'), # Multicrew role
|
||||
}.get(role, role)
|
||||
|
||||
while True:
|
||||
@ -626,8 +633,8 @@ class AppWindow(object):
|
||||
self.ship_label['text'] = _('Ship') + ':' # Main window
|
||||
self.ship.configure(
|
||||
text=monitor.state['ShipName']
|
||||
or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType'])
|
||||
or '',
|
||||
or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType'])
|
||||
or '',
|
||||
url=self.shipyard_url)
|
||||
else:
|
||||
self.cmdr['text'] = ''
|
||||
@ -673,7 +680,7 @@ class AppWindow(object):
|
||||
logger.info("Can't start Status monitoring")
|
||||
|
||||
# Export loadout
|
||||
if entry['event'] == 'Loadout' and not monitor.state['Captain']\
|
||||
if entry['event'] == 'Loadout' and not monitor.state['Captain'] \
|
||||
and config.getint('output') & config.OUT_SHIP:
|
||||
monitor.export_ship()
|
||||
|
||||
@ -690,10 +697,10 @@ class AppWindow(object):
|
||||
hotkeymgr.play_bad()
|
||||
|
||||
# Auto-Update after docking, but not if auth callback is pending
|
||||
if entry['event'] in ('StartUp', 'Location', 'Docked')\
|
||||
and monitor.station\
|
||||
and not config.getint('output') & config.OUT_MKT_MANUAL\
|
||||
and config.getint('output') & config.OUT_STATION_ANY\
|
||||
if entry['event'] in ('StartUp', 'Location', 'Docked') \
|
||||
and monitor.station \
|
||||
and not config.getint('output') & config.OUT_MKT_MANUAL \
|
||||
and config.getint('output') & config.OUT_STATION_ANY \
|
||||
and companion.session.state != companion.Session.STATE_AUTH:
|
||||
self.w.after(int(SERVER_RETRY * 1000), self.getandsend)
|
||||
|
||||
@ -760,10 +767,10 @@ class AppWindow(object):
|
||||
return f'file://localhost/{file_name}'
|
||||
|
||||
def system_url(self, system):
|
||||
return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system)
|
||||
return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system)
|
||||
|
||||
def station_url(self, station):
|
||||
return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station)
|
||||
return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station)
|
||||
|
||||
def cooldown(self):
|
||||
if time() < self.holdofftime:
|
||||
@ -837,7 +844,7 @@ class AppWindow(object):
|
||||
|
||||
############################################################
|
||||
# version <link to changelog>
|
||||
ttk.Label(frame).grid(row=row, column=0) # spacer
|
||||
ttk.Label(frame).grid(row=row, column=0) # spacer
|
||||
row += 1
|
||||
self.appversion_label = tk.Label(frame, text=appversion)
|
||||
self.appversion_label.grid(row=row, column=0, sticky=tk.E)
|
||||
@ -855,7 +862,7 @@ class AppWindow(object):
|
||||
|
||||
############################################################
|
||||
# <copyright>
|
||||
ttk.Label(frame).grid(row=row, column=0) # spacer
|
||||
ttk.Label(frame).grid(row=row, column=0) # spacer
|
||||
row += 1
|
||||
self.copyright = tk.Label(frame, text=copyright)
|
||||
self.copyright.grid(row=row, columnspan=3, sticky=tk.EW)
|
||||
@ -864,7 +871,7 @@ class AppWindow(object):
|
||||
|
||||
############################################################
|
||||
# OK button to close the window
|
||||
ttk.Label(frame).grid(row=row, column=0) # spacer
|
||||
ttk.Label(frame).grid(row=row, column=0) # spacer
|
||||
row += 1
|
||||
button = ttk.Button(frame, text=_('OK'), command=self.apply)
|
||||
button.grid(row=row, column=2, sticky=tk.E)
|
||||
@ -895,7 +902,7 @@ class AppWindow(object):
|
||||
last_system: str = data.get("lastSystem", {}).get("name", "Unknown")
|
||||
last_starport: str = ''
|
||||
if data['commander'].get('docked'):
|
||||
last_starport = '.'+data.get('lastStarport', {}).get('name', 'Unknown')
|
||||
last_starport = '.' + data.get('lastStarport', {}).get('name', 'Unknown')
|
||||
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
|
||||
f = tkinter.filedialog.asksaveasfilename(parent=self.w,
|
||||
defaultextension=default_extension,
|
||||
@ -971,6 +978,7 @@ def enforce_single_instance() -> None:
|
||||
if platform == 'win32':
|
||||
import ctypes
|
||||
from ctypes.wintypes import HWND, LPWSTR, LPCWSTR, INT, BOOL, LPARAM
|
||||
|
||||
EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806
|
||||
GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806
|
||||
GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] # noqa: N806
|
||||
@ -1004,9 +1012,9 @@ def enforce_single_instance() -> None:
|
||||
def enumwindowsproc(window_handle, l_param):
|
||||
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
|
||||
cls = ctypes.create_unicode_buffer(257)
|
||||
if GetClassName(window_handle, cls, 257)\
|
||||
and cls.value == 'TkTopLevel'\
|
||||
and window_title(window_handle) == applongname\
|
||||
if GetClassName(window_handle, cls, 257) \
|
||||
and cls.value == 'TkTopLevel' \
|
||||
and window_title(window_handle) == applongname \
|
||||
and GetProcessHandleFromHwnd(window_handle):
|
||||
# If GetProcessHandleFromHwnd succeeds then the app is already running as this user
|
||||
if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler.redirect):
|
||||
@ -1028,38 +1036,53 @@ def test_logging():
|
||||
logger.debug('Test from EDMarketConnector.py top-level test_logging()')
|
||||
|
||||
|
||||
# Run the app
|
||||
if __name__ == "__main__":
|
||||
enforce_single_instance()
|
||||
|
||||
from EDMCLogging import logger
|
||||
logger.info(f'Startup v{appversion} : Running on Python v{sys.version}')
|
||||
logger.debug(f'''Platform: {sys.platform}
|
||||
argv[0]: {sys.argv[0]}
|
||||
exec_prefix: {sys.exec_prefix}
|
||||
executable: {sys.executable}
|
||||
sys.path: {sys.path}
|
||||
def log_locale(prefix: str) -> None:
|
||||
logger.debug(f'''Locale: {prefix}
|
||||
Locale LC_COLLATE: {locale.getlocale(locale.LC_COLLATE)}
|
||||
Locale LC_CTYPE: {locale.getlocale(locale.LC_CTYPE)}
|
||||
Locale LC_MONETARY: {locale.getlocale(locale.LC_MONETARY)}
|
||||
Locale LC_NUMERIC: {locale.getlocale(locale.LC_NUMERIC)}
|
||||
Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Run the app
|
||||
if __name__ == "__main__":
|
||||
enforce_single_instance()
|
||||
|
||||
from EDMCLogging import logger
|
||||
|
||||
logger.info(f'Startup v{appversion} : Running on Python v{sys.version}')
|
||||
logger.debug(f'''Platform: {sys.platform}
|
||||
argv[0]: {sys.argv[0]}
|
||||
exec_prefix: {sys.exec_prefix}
|
||||
executable: {sys.executable}
|
||||
sys.path: {sys.path}'''
|
||||
)
|
||||
|
||||
# Change locale to a utf8 one
|
||||
# First make sure the local is actually set as per locale's idea of defaults
|
||||
# Log what we have at startup
|
||||
log_locale('Initial Locale')
|
||||
# Make sure the local is actually set as per locale's idea of defaults
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
log_locale('After LC_ALL defaults set')
|
||||
# Now find out the current locale, mostly the language
|
||||
locale_startup = locale.getlocale(locale.LC_ALL)
|
||||
logger.debug(f'Locale LC_ALL: {locale_startup}')
|
||||
# Now set that same language, but utf8 encoding (it was probably cp1252
|
||||
# or equivalent for other languages).
|
||||
locale.setlocale(locale.LC_ALL, (locale_startup[0], 'utf8'))
|
||||
# UTF-8, not utf8: <https://en.wikipedia.org/wiki/UTF-8#Naming>
|
||||
locale.setlocale(locale.LC_ALL, (locale_startup[0], 'UTF-8'))
|
||||
log_locale('After switching to UTF-8 encoding (same language)')
|
||||
|
||||
# TODO: unittests in place of these
|
||||
# logger.debug('Test from __main__')
|
||||
# test_logging()
|
||||
|
||||
class A(object):
|
||||
|
||||
class B(object):
|
||||
|
||||
def __init__(self):
|
||||
logger.debug('A call from A.B.__init__')
|
||||
|
||||
@ -1102,7 +1125,8 @@ Locale LC_TIME: {locale.getlocale(locale.LC_TIME)}'''
|
||||
# Now the string should match, so try translation
|
||||
popup_text = _(popup_text)
|
||||
# And substitute in the other words.
|
||||
popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled')
|
||||
popup_text = popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'),
|
||||
DISABLED='.disabled')
|
||||
# And now we do need these to be actual \r\n
|
||||
popup_text = popup_text.replace('\\n', '\n')
|
||||
popup_text = popup_text.replace('\\r', '\r')
|
||||
|
@ -1,9 +1,6 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "Procenta UI škálování";
|
||||
|
||||
|
||||
/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
|
||||
"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 je výchozí hodnota{CR}Je potřeba restartovat{CR}aby se změna projevila!";
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 je výchozí hodnota{CR}je potřeba restartovat,{CR}aby se změna projevila!";
|
||||
|
||||
@ -535,8 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} vyžaduje oprávnění pro použití klávesových zkratek";
|
||||
|
||||
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scaling" = "UI škálování";
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Míra logování";
|
||||
|
@ -1,9 +1,6 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "UI-Skalierung in Prozent";
|
||||
|
||||
|
||||
/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
|
||||
"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 bedeutet Standardwert{CR}Neustart erforderlich, um{CR}Änderungen zu übernehmen!";
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 bedeutet Standardwert{CR}Neustart erforderlich, um{CR}Änderungen zu übernehmen!";
|
||||
|
||||
@ -535,8 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} benötigt für Tastenkürzel Systemrechte (Bedienungshilfen)";
|
||||
|
||||
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scaling" = "UI-Skalierung";
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Log-Level";
|
||||
|
@ -1,3 +1,9 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "UI Scale Percentage";
|
||||
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "English";
|
||||
|
||||
@ -526,11 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts";
|
||||
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "UI Scale Percentage";
|
||||
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!";
|
||||
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Log Level";
|
||||
|
@ -1,3 +1,9 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "Käyttöliittymän skaalausprosentti";
|
||||
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 tarkoittaa oletusta{CR}Muutosten käyttöönotto{CR}edellyttää uudelleenkäynnistystä!";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Suomi";
|
||||
|
||||
@ -526,3 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} tarvitsee lupasi pikakuvakkeiden käyttämiseen";
|
||||
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Lokin syvyys";
|
||||
|
@ -526,8 +526,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} ha bisogno dei permessi per usare le scorciatoie";
|
||||
|
||||
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scaling" = "Adattamento UI";
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Livello di Log";
|
||||
|
@ -1,9 +1,6 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "UIスケール比率";
|
||||
|
||||
|
||||
/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
|
||||
"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0はデフォルトを意味します{CR}変更した値を反映するには{CR}再起動が必要です!";
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100はデフォルトを意味します{CR}変更した値を反映するには{CR}再起動が必要です!";
|
||||
|
||||
@ -535,8 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} がショートカットを利用するには許可が必要です";
|
||||
|
||||
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scaling" = "UIスケーリング";
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "ログレベル";
|
||||
|
@ -1,9 +1,6 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "Porcentagem de escala da IU";
|
||||
|
||||
|
||||
/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
|
||||
"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 significa Padrão{CR}Necessário Reiniciar para as{CR}alterações entrarem em vigor!";
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 significa Padrão{CR}Necessário Reiniciar para as{CR}alterações entrarem em vigor!";
|
||||
|
||||
@ -535,8 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} precisa de permissão para usar atalhos";
|
||||
|
||||
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scaling" = "Escala da UI";
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Nível de Log";
|
||||
|
@ -1,3 +1,9 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "Percentagem da escala de UI";
|
||||
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 significa Predefinido{CR}Requer reinício para{CR}que as mudanças façam efeito!";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Português (Portugal)";
|
||||
|
||||
@ -526,3 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "A {APP} precisa de autorização para usar atalhos";
|
||||
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Nível de Log";
|
||||
|
@ -1,3 +1,9 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "Процент масштабирования интерфейса";
|
||||
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 - значение по умолчанию{CR}требуется перезапуск для вступления в силу изменений{CR}!";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Русский";
|
||||
|
||||
@ -56,7 +62,7 @@
|
||||
"Chief Petty Officer" = "Главный старшина";
|
||||
|
||||
/* Main window. [EDMarketConnector.py] */
|
||||
"Cmdr" = "Командир";
|
||||
"Cmdr" = "КМДР";
|
||||
|
||||
/* Ranking. [stats.py] */
|
||||
"Combat" = "Боевой";
|
||||
@ -86,7 +92,7 @@
|
||||
"Dangerous" = "Опасный";
|
||||
|
||||
/* Appearance theme setting. [prefs.py] */
|
||||
"Dark" = "Темная";
|
||||
"Dark" = "Тёмная";
|
||||
|
||||
/* Combat rank. [stats.py] */
|
||||
"Deadly" = "Смертоносный";
|
||||
@ -101,7 +107,7 @@
|
||||
"Delay sending until docked" = "Отложить отправку данных до завершения стыковки";
|
||||
|
||||
/* Option to disabled Automatic Check For Updates whilst in-game [prefs.py] */
|
||||
"Disable Automatic Application Updates Check when in-game" = "Отключить автоматическое обновление приложения во время нахождения в игре.";
|
||||
"Disable Automatic Application Updates Check when in-game" = "Отключить автоматическое обновление приложения во время нахождения в игре";
|
||||
|
||||
/* List of plugins in settings. [prefs.py] */
|
||||
"Disabled Plugins" = "Отключенные плагины";
|
||||
@ -125,13 +131,13 @@
|
||||
"Elite" = "Элита";
|
||||
|
||||
/* Section heading in settings. [edsm.py] */
|
||||
"Elite Dangerous Star Map credentials" = "Учетные данные Elite Dangerous Star Map";
|
||||
"Elite Dangerous Star Map credentials" = "Учётные данные Elite Dangerous Star Map";
|
||||
|
||||
/* Ranking. [stats.py] */
|
||||
"Empire" = "Империя";
|
||||
|
||||
/* List of plugins in settings. [prefs.py] */
|
||||
"Enabled Plugins" = "Включенные плагины";
|
||||
"Enabled Plugins" = "Включённые плагины";
|
||||
|
||||
/* Federation rank. [stats.py] */
|
||||
"Ensign" = "Энсин";
|
||||
@ -221,7 +227,7 @@
|
||||
"Hotkey" = "Горячая клавиша";
|
||||
|
||||
/* Section heading in settings. [inara.py] */
|
||||
"Inara credentials" = "Учетные данные Inara";
|
||||
"Inara credentials" = "Учётные данные Inara";
|
||||
|
||||
/* Hotkey/Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"Keyboard shortcut" = "Сочетание клавиш";
|
||||
@ -302,7 +308,7 @@
|
||||
"Open" = "Открыть";
|
||||
|
||||
/* Shortcut settings button on OSX. [prefs.py] */
|
||||
"Open System Preferences" = "Открыты «Системные настройки»";
|
||||
"Open System Preferences" = "Открыты «Настройки системы»";
|
||||
|
||||
/* Tab heading in settings. [prefs.py] */
|
||||
"Output" = "Данные";
|
||||
@ -356,7 +362,7 @@
|
||||
"Post Commander" = "Начальник гарнизона";
|
||||
|
||||
/* Ranking. [stats.py] */
|
||||
"Powerplay" = "Ранг в Игре Сил";
|
||||
"Powerplay" = "Ранг у галактических держав";
|
||||
|
||||
/* [prefs.py] */
|
||||
"Preferences" = "Настройки";
|
||||
@ -416,13 +422,13 @@
|
||||
"Semi Professional" = "Полупрофессионал";
|
||||
|
||||
/* [edsm.py] */
|
||||
"Send flight log and Cmdr status to EDSM" = "Отправлять данные полетного журнала и информацию статусе командира в EDSM";
|
||||
"Send flight log and Cmdr status to EDSM" = "Отправлять данные журнала полёта и информацию статусе командира в EDSM";
|
||||
|
||||
/* [inara.py] */
|
||||
"Send flight log and Cmdr status to Inara" = "Отправлять журнал полёта и состояние командира в Inara";
|
||||
"Send flight log and Cmdr status to Inara" = "Отправлять данные журнала полёта и информацию статусе командира в Inara";
|
||||
|
||||
/* Output setting. [eddn.py] */
|
||||
"Send station data to the Elite Dangerous Data Network" = "Отправлять станционную информацию на Elite Dangerous Data Network";
|
||||
"Send station data to the Elite Dangerous Data Network" = "Отправлять информацию о станциях на Elite Dangerous Data Network";
|
||||
|
||||
/* Output setting new in E:D 2.2. [eddn.py] */
|
||||
"Send system and scan data to the Elite Dangerous Data Network" = "Отправлять данные о системе и данные сканирования в Elite Dangerous Data Network";
|
||||
@ -437,13 +443,13 @@
|
||||
"Settings" = "Настройки";
|
||||
|
||||
/* Main window. [EDMarketConnector.py] */
|
||||
"Ship" = "Судно";
|
||||
"Ship" = "Корабль";
|
||||
|
||||
/* Output setting. [prefs.py] */
|
||||
"Ship loadout" = "Оборудование судна";
|
||||
"Ship loadout" = "Оборудование корабля";
|
||||
|
||||
/* Status dialog title. [stats.py] */
|
||||
"Ships" = "Суда";
|
||||
"Ships" = "Корабли";
|
||||
|
||||
/* Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis. [prefs.py] */
|
||||
"Shipyard" = "Верфи";
|
||||
@ -491,7 +497,7 @@
|
||||
"Update" = "Обновить данные";
|
||||
|
||||
/* Option to use alternate URL method on shipyard links [prefs.py] */
|
||||
"Use alternate URL method" = "Альтернативный URL(адрес)";
|
||||
"Use alternate URL method" = "Альтернативный URL (адрес)";
|
||||
|
||||
/* Status dialog subtitle - CR value of ship. [stats.py] */
|
||||
"Value" = "Стоимость";
|
||||
@ -509,7 +515,7 @@
|
||||
"Warrant Officer" = "Уорент-офицер";
|
||||
|
||||
/* Shouldn't happen. [EDMarketConnector.py] */
|
||||
"What are you flying?!" = "Невозможно определить тип судна!";
|
||||
"What are you flying?!" = "Невозможно определить тип корабля!";
|
||||
|
||||
/* Shouldn't happen. [EDMarketConnector.py] */
|
||||
"Where are you?!" = "Невозможно определить местоположение!";
|
||||
@ -521,13 +527,10 @@
|
||||
"Window" = "Окно";
|
||||
|
||||
/* [EDMarketConnector.py] */
|
||||
"You're not docked at a station!" = "Ваше судно не пристыковано к станции!";
|
||||
"You're not docked at a station!" = "Ваш корабль не пристыкован к станции!";
|
||||
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} требуется разрешение на использование глобальных сочетаний клавиш";
|
||||
|
||||
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scaling" = "Масштабирование интерфейса";
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Уровень лога";
|
||||
|
@ -1,9 +1,6 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "Procenat UI skaliranja";
|
||||
|
||||
|
||||
/* Text describing that value '0' means 'default', and changes require a restart [prefs.py] */
|
||||
"0 means Default{CR}Restart Required for{CR}changes to take effect!" = "0 je standardna vrijednost{CR}Potreban je restart{CR}za primjenu podešavanja!";
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 je standardna vrijednost{CR}Potreban je restart{CR}za primjenu podešavanja!";
|
||||
|
||||
@ -535,8 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} traži dozvolu da koristi prečice";
|
||||
|
||||
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scaling" = "UI skaliranje";
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Log nivo";
|
||||
|
@ -1,3 +1,9 @@
|
||||
/* Label for 'UI Scaling' option [prefs.py] */
|
||||
"UI Scale Percentage" = "Procenat UI skaliranja";
|
||||
|
||||
/* Text describing that value '100' means 'default', and changes require a restart [prefs.py] */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 znači podrazumevano{CR}Restart je potreban{CR}da bi se promene primenile!";
|
||||
|
||||
/* Language name */
|
||||
"!Language" = "Srpski (Latinica)";
|
||||
|
||||
@ -526,3 +532,5 @@
|
||||
/* Shortcut settings prompt on OSX. [prefs.py] */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} traži dozvolu da koristi skraćenice";
|
||||
|
||||
/* Label for user configured level of logging [prefs.py] */
|
||||
"Log Level" = "Log Nivo";
|
||||
|
360
PLUGINS.md
360
PLUGINS.md
@ -3,6 +3,7 @@
|
||||
Plugins allow you to customise and extend the behavior of EDMC.
|
||||
|
||||
## Installing a Plugin
|
||||
|
||||
See [Plugins](https://github.com/EDCD/EDMarketConnector/wiki/Plugins) on the
|
||||
wiki.
|
||||
|
||||
@ -12,16 +13,22 @@ Plugins are loaded when EDMC starts up.
|
||||
|
||||
Each plugin has it's own folder in the `plugins` directory:
|
||||
|
||||
* Windows: `%LOCALAPPDATA%\EDMarketConnector\plugins`
|
||||
* Mac: `~/Library/Application Support/EDMarketConnector/plugins`
|
||||
* Linux: `$XDG_DATA_HOME/EDMarketConnector/plugins`, or
|
||||
`~/.local/share/EDMarketConnector/plugins` if `$XDG_DATA_HOME` is unset.
|
||||
- Windows: `%LOCALAPPDATA%\EDMarketConnector\plugins`
|
||||
- Mac: `~/Library/Application Support/EDMarketConnector/plugins`
|
||||
- Linux: `$XDG_DATA_HOME/EDMarketConnector/plugins`, or `~/.local/share/EDMarketConnector/plugins` if `$XDG_DATA_HOME` is unset.
|
||||
|
||||
Plugins are python files. The plugin folder must have a file named `load.py`
|
||||
that must provide one module level function and optionally provide a few
|
||||
others.
|
||||
|
||||
---
|
||||
|
||||
### Examples
|
||||
|
||||
We have some example plugins available in the docs/examples directory. See the readme in each folder for more info.
|
||||
|
||||
---
|
||||
|
||||
### Available imports
|
||||
|
||||
**`import`ing anything from the core EDMarketConnector code that is not
|
||||
@ -32,34 +39,36 @@ breaking with future code changes.**
|
||||
|
||||
`from theme import theme` - So plugins can theme their own UI elements to
|
||||
match the main UI.
|
||||
|
||||
|
||||
`from config import appname, applongname, appcmdname, appversion
|
||||
, copyright, config` - to access config.
|
||||
|
||||
`from prefs import prefsVersion` - to allow for versioned preferences.
|
||||
|
||||
`from companion import category_map` - Or any of the other static date
|
||||
contained therein. NB: There's a plan to move such to a `data` module.
|
||||
contained therein. NB: There's a plan to move such to a `data` module.
|
||||
|
||||
`import plug` - Mostly for using `plug.show_error()`. Also the flags
|
||||
for `dashboard_entry()` to be useful (see example below). Relying on anything
|
||||
else isn't supported.
|
||||
|
||||
for `dashboard_entry()` to be useful (see example below). Relying on anything
|
||||
else isn't supported.
|
||||
|
||||
`from monitor import gamerunning` - in case a plugin needs to know if we
|
||||
think the game is running.
|
||||
|
||||
`import timeout_session` - provides a method called `new_session` that creates a requests.session with a default timeout
|
||||
on all requests. Recommended to reduce noise in HTTP requests
|
||||
|
||||
|
||||
```python
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
import myNotebook as nb
|
||||
```
|
||||
|
||||
For creating UI elements.
|
||||
|
||||
---
|
||||
|
||||
### Logging
|
||||
|
||||
In the past the only way to provide any logged output from a
|
||||
plugin was to use `print(...)` statements. When running the application from
|
||||
the packaged executeable all output is redirected to a log file. See
|
||||
@ -70,8 +79,8 @@ EDMC now implements proper logging using the Python `logging` module. Plugin
|
||||
developers should now use the following code instead of simple `print(...)`
|
||||
statements.
|
||||
|
||||
Insert this at the top-level of your load.py file (so not inside
|
||||
`plugin_start3()` ):
|
||||
Insert this at the top-level of your load.py file (so not inside `plugin_start3()` ):
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
@ -82,6 +91,8 @@ plugin_name = os.path.basename(os.path.dirname(__file__))
|
||||
|
||||
# A Logger is used per 'found' plugin to make it easy to include the plugin's
|
||||
# folder name in the logging output format.
|
||||
# NB: plugin_name here *must* be the plugin's folder name as per the preceding
|
||||
# code, else the logger won't be properly set up.
|
||||
logger = logging.getLogger(f'{appname}.{plugin_name}')
|
||||
|
||||
# If the Logger has handlers then it was already set up by the core code, else
|
||||
@ -98,6 +109,13 @@ if not logger.hasHandlers():
|
||||
logger.addHandler(logger_channel)
|
||||
```
|
||||
|
||||
Note the admonishment about `plugin_name` being the folder name of your plugin.
|
||||
It can't be anything else (such as a different string returned from
|
||||
`plugin_start3()`) because the code in plug.py that sets up the logger uses
|
||||
exactly the folder name. Our custom `qualname` and `class` formatters won't
|
||||
work with a 'bare' logger, and will cause your code to throw exceptions if
|
||||
you're not using our supplied logger.
|
||||
|
||||
If running with 4.1.0-beta1 or later of EDMC the logging setup happens in
|
||||
the core code and will include the extra logfile destinations. If your
|
||||
plugin is run under a pre-4.1.0 version of EDMC then the above will set up
|
||||
@ -132,7 +150,7 @@ Replace all `print(...)` statements with one of the following:
|
||||
logger.debug('Exception we only note in debug output', exc_info=e)
|
||||
```
|
||||
|
||||
Remember you can use fstrings to include variables, and even the returns of
|
||||
Remember you can use [fstrings](https://www.python.org/dev/peps/pep-0498/) to include variables, and even the returns of
|
||||
functions, in the output.
|
||||
|
||||
```python
|
||||
@ -140,40 +158,50 @@ functions, in the output.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Startup
|
||||
|
||||
EDMC will import the `load.py` file as a module and then call the
|
||||
`plugin_start3()` function.
|
||||
|
||||
```python
|
||||
def plugin_start3(plugin_dir):
|
||||
def plugin_start3(plugin_dir: str) -> str:
|
||||
"""
|
||||
Load this plugin into EDMC
|
||||
"""
|
||||
print("I am loaded! My plugin folder is {}".format(plugin_dir))
|
||||
print(f"I am loaded! My plugin folder is {plugin_dir}")
|
||||
return "Test"
|
||||
```
|
||||
|
||||
The string you return is used as the internal name of the plugin.
|
||||
|
||||
Any errors or print statements from your plugin will appear in
|
||||
`%TMP%\EDMarketConnector.log` on Windows, `$TMPDIR/EDMarketConnector.log` on
|
||||
Mac, and `$TMP/EDMarketConnector.log` on Linux.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :----------- | :---: | :------------------------------------------------------ |
|
||||
| `plugin_dir` | `str` | The directory that your plugin is located in. |
|
||||
| `RETURN` | `str` | The name you want to be used for your plugin internally |
|
||||
|
||||
### Shutdown
|
||||
|
||||
This gets called when the user closes the program:
|
||||
|
||||
```python
|
||||
def plugin_stop():
|
||||
def plugin_stop() -> None:
|
||||
"""
|
||||
EDMC is closing
|
||||
"""
|
||||
print("Farewell cruel world!")
|
||||
```
|
||||
|
||||
If your plugin uses one or more threads to handle Events then stop and join()
|
||||
the threads before returning from this function.
|
||||
If your plugin uses one or more threads to handle Events then `stop()` and `join()`
|
||||
(to wait for their exit -- Recommended, not required) the threads before returning from this function.
|
||||
|
||||
## Plugin Hooks
|
||||
### Configuration
|
||||
|
||||
### Configuration
|
||||
|
||||
If you want your plugin to be configurable via the GUI you can define a frame
|
||||
(panel) to be displayed on its own tab in EDMC's settings dialog. The tab
|
||||
@ -192,39 +220,53 @@ numbers in a locale-independent way.
|
||||
|
||||
```python
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import myNotebook as nb
|
||||
from config import config
|
||||
from typing import Optional
|
||||
|
||||
this = sys.modules[__name__] # For holding module globals
|
||||
my_setting: Optional[tk.IntVar] = None
|
||||
|
||||
def plugin_prefs(parent, cmdr, is_beta):
|
||||
def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]:
|
||||
"""
|
||||
Return a TK Frame for adding to the EDMC settings dialog.
|
||||
"""
|
||||
this.mysetting = tk.IntVar(value=config.getint("MyPluginSetting")) # Retrieve saved value from config
|
||||
global my_setting
|
||||
my_setting = tk.IntVar(value=config.getint("MyPluginSetting")) # Retrieve saved value from config
|
||||
frame = nb.Frame(parent)
|
||||
nb.Label(frame, text="Hello").grid()
|
||||
nb.Label(frame, text="Commander").grid()
|
||||
nb.Checkbutton(frame, text="My Setting", variable=this.mysetting).grid()
|
||||
nb.Checkbutton(frame, text="My Setting", variable=my_setting).grid()
|
||||
|
||||
return frame
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :-----------: | :----------------------------------------------- |
|
||||
| `parent` | `nb.Notebook` | Root Notebook object the preferences window uses |
|
||||
| `cmdr` | `str` | The current commander |
|
||||
| `is_beta` | `bool` | If the game is currently a beta version |
|
||||
|
||||
This gets called when the user dismisses the settings dialog:
|
||||
|
||||
```python
|
||||
def prefs_changed(cmdr, is_beta):
|
||||
def prefs_changed(cmdr: str, is_beta: bool) -> None:
|
||||
"""
|
||||
Save settings.
|
||||
"""
|
||||
config.set('MyPluginSetting', this.mysetting.getint()) # Store new value in config
|
||||
config.set('MyPluginSetting', my_setting.get()) # Store new value in config
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----: | :-------------------------------------- |
|
||||
| `cmdr` | `str` | The current commander |
|
||||
| `is_beta` | `bool` | If the game is currently a beta version |
|
||||
|
||||
### Display
|
||||
|
||||
You can also have your plugin add an item to the EDMC main window and update
|
||||
from your event hooks. This works in the same way as `plugin_prefs()`. For a
|
||||
simple one-line item return a tk.Label widget or a pair of widgets as a tuple.
|
||||
simple one-line item return a `tk.Label` widget or a 2 tuple of widgets.
|
||||
For a more complicated item create a tk.Frame widget and populate it with other
|
||||
ttk widgets. Return `None` if you just want to use this as a callback after the
|
||||
main window and all other plugins are initialised.
|
||||
@ -233,42 +275,62 @@ You can use `stringFromNumber()` from EDMC's `l10n.Locale` object to format
|
||||
numbers in your widgets in a locale-independent way.
|
||||
|
||||
```python
|
||||
this = sys.modules[__name__] # For holding module globals
|
||||
from typing import Optional, Tuple
|
||||
import tkinter as tk
|
||||
|
||||
def plugin_app(parent):
|
||||
status: Optional[tk.Label]
|
||||
|
||||
|
||||
def plugin_app(parent: tk.Frame) -> Tuple[tk.Label, tk.Label]:
|
||||
"""
|
||||
Create a pair of TK widgets for the EDMC main window
|
||||
"""
|
||||
global status
|
||||
label = tk.Label(parent, text="Status:") # By default widgets inherit the current theme's colors
|
||||
this.status = tk.Label(parent, text="", foreground="yellow") # Override theme's foreground color
|
||||
return (label, this.status)
|
||||
|
||||
status = tk.Label(parent, text="", foreground="yellow") # Override theme's foreground color
|
||||
return (label, status)
|
||||
|
||||
# later on your event functions can update the contents of these widgets
|
||||
this.status["text"] = "Happy!"
|
||||
this.status["foreground"] = "green"
|
||||
def some_other_function() -> None:
|
||||
global status
|
||||
status["text"] = "Happy!"
|
||||
status["foreground"] = "green"
|
||||
```
|
||||
|
||||
You can dynamically add and remove widgets on the main window by returning a
|
||||
tk.Frame from `plugin_app()` and later creating and destroying child widgets
|
||||
of that frame.
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :---------------------------------------------: | :---------------------------------------------------------- |
|
||||
| `parent` | `tk.Frame` | The root EDMC window |
|
||||
| `RETURN` | `Union[tk.Widget, Tuple[tk.Widget, tk.Widget]]` | A widget to add to the main window. See below for more info |
|
||||
|
||||
The return from plugin_app can either be any widget (Frame, Label, Notebook, etc.), or a 2 tuple of widgets. In the case of
|
||||
a 2 tuple, indices 0 and 1 are placed automatically in the outer grid on column indices 0 and 1. Otherwise, the only thing done
|
||||
to your return widget is it is set to use a columnspan of 2, and placed on the grid.
|
||||
|
||||
You can dynamically add and remove widgets on the main window by returning a tk.Frame from `plugin_app()` and later
|
||||
creating and destroying child widgets of that frame.
|
||||
|
||||
```python
|
||||
from typing import Option
|
||||
import tkinter as tk
|
||||
|
||||
from theme import theme
|
||||
|
||||
this = sys.modules[__name__] # For holding module globals
|
||||
frame: Optional[tk.Frame] = None
|
||||
|
||||
def plugin_app(parent):
|
||||
def plugin_app(parent: tk.Frame) -> tk.Frame:
|
||||
"""
|
||||
Create a frame for the EDMC main window
|
||||
"""
|
||||
this.frame = tk.Frame(parent)
|
||||
return this.frame
|
||||
global frame
|
||||
frame = tk.Frame(parent)
|
||||
return frame
|
||||
|
||||
def some_other_function_called_later() -> None:
|
||||
# later on your event functions can add or remove widgets
|
||||
row = this.frame.grid_size()[1]
|
||||
new_widget_1 = tk.Label(this.frame, text="Status:")
|
||||
row = frame.grid_size()[1]
|
||||
new_widget_1 = tk.Label(frame, text="Status:")
|
||||
new_widget_1.grid(row=row, column=0, sticky=tk.W)
|
||||
new_widget_2 = tk.Label(this.frame, text="Unhappy!", foreground="red") # Override theme's foreground color
|
||||
new_widget_2 = tk.Label(frame, text="Unhappy!", foreground="red") # Override theme's foreground color
|
||||
new_widget_2.grid(row=row, column=1, sticky=tk.W)
|
||||
theme.update(this.frame) # Apply theme colours to the frame and its children, including the new widgets
|
||||
```
|
||||
@ -295,58 +357,54 @@ for an example of these techniques.
|
||||
#### Journal Entry
|
||||
|
||||
```python
|
||||
def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||
def journal_entry(
|
||||
cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any]
|
||||
) -> None:
|
||||
if entry['event'] == 'FSDJump':
|
||||
# We arrived at a new system!
|
||||
if 'StarPos' in entry:
|
||||
sys.stderr.write("Arrived at {} ({},{},{})\n".format(entry['StarSystem'], *tuple(entry['StarPos'])))
|
||||
logger.info(f'Arrived at {entry["StarSystem"]} {entry["StarPos"')
|
||||
|
||||
else:
|
||||
sys.stderr.write("Arrived at {}\n".format(entry['StarSystem']))
|
||||
logger.info(f'Arrived at {entry["StarSystem"]}')
|
||||
```
|
||||
|
||||
This gets called when EDMC sees a new entry in the game's journal.
|
||||
|
||||
- `cmdr` is a `str` denoting the current Commander Name.
|
||||
- `is_beta` is a `bool` denoting if data came from a beta version of the game.
|
||||
- `system` is a `str` holding the name of the current system, or `None` if not
|
||||
yet known.
|
||||
- `station` is a `str` holding the name of the current station, or `None` if
|
||||
not yet known or appropriate.
|
||||
- `entry` is an `OrderedDict` holding the Journal event.
|
||||
- `state` is a `dictionary` containing information about the Cmdr and their
|
||||
ship and cargo (including the effect of the current journal entry).
|
||||
- `Captain` - `str` of name of Commander's crew you joined in multi-crew,
|
||||
else `None`
|
||||
- `Cargo` - `dict` with details of current cargo.
|
||||
- `Credits` - Current credit balance.
|
||||
- `FID` - Frontier Cmdr ID
|
||||
- `Horizons` - `bool` denoting if Horizons expansion active.
|
||||
- `Loan` - Current loan amount, else None.
|
||||
- `Raw` - `dict` with details of "Raw" materials held.
|
||||
- `Manufactured` - `dict` with details of "Manufactured" materials held.
|
||||
- `Encoded` - `dict` with details of "Encoded" materials held.
|
||||
- `Engineers` - `dict` with details of Rank Progress for Engineers.
|
||||
- `Rank` - `dict` of current Ranks. Each entry is a `tuple` of
|
||||
(<rank `int`>, <progress %age `int`>)
|
||||
- `Reputation` - `dict` of Major Faction reputations, scale is -100 to +100
|
||||
See Frontier's Journal Manual for detail of bands.
|
||||
- `Statistics` - `dict` of a Journal "Statistics" event, i.e. data shown
|
||||
in the statistics panel on the right side of the cockpit. See Frontier's
|
||||
Journal Manual for details.
|
||||
- `Role` - Crew role if multi-crewing in another Commander's ship:
|
||||
- `None`
|
||||
- "Idle"
|
||||
- "FireCon"
|
||||
- "FighterCon"
|
||||
- `Friends` -`set` of online friends.
|
||||
- `ShipID` - `int` that denotes Frontier internal ID for your current ship.
|
||||
- `ShipIdent` - `str` of your current ship's textual ID (which you set).
|
||||
- `ShipName` - `str` of your current ship's textual Name (which you set).
|
||||
- `ShipType` - `str` of your current ship's model, e.g. "CobraMkIII".
|
||||
- `HullValue` - `int` of current ship's credits value, excluding modules.
|
||||
- `ModulesValue` - `int` of current ship's module's total credits value.
|
||||
- `Rebuy` - `int` of current ship's rebuy cost in credits.
|
||||
- `Modules` - `dict` with data on currently fitted modules.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :--------------: | :--------------------------------------------------------------------- |
|
||||
| `cmdr` | `str` | Current commander name |
|
||||
| `is_beta` | `bool` | Is the game currently in beta |
|
||||
| `system` | `Optional[str]` | Current system, if known |
|
||||
| `station` | `Optional[str]` | Current station, if any |
|
||||
| `entry` | `Dict[str, Any]` | The journal event |
|
||||
| `state` | `Dict[str, Any]` | More info about the commander, their ship, and their cargo (see below) |
|
||||
|
||||
Content of `state` (updated to the current journal entry):
|
||||
|
||||
| Field | Type | Description |
|
||||
| :------------- | :-------------------------: | :-------------------------------------------------------------------------------------------------------------- |
|
||||
| `Captian` | `Optional[str]` | Name of the commander who's crew you're on, if any |
|
||||
| `Cargo` | `dict` | Current cargo |
|
||||
| `Credits` | `int` | Current credits balance |
|
||||
| `FID` | `str` | Frontier commander ID |
|
||||
| `Loan` | `Optional[int]` | Current loan amount, if any |
|
||||
| `Raw` | `dict` | Current raw engineering materials |
|
||||
| `Manufactured` | `dict` | Current manufactured engineering materials |
|
||||
| `Encoded` | `dict` | Current encoded engineering materials |
|
||||
| `Engineers` | `dict` | Current Raw engineering materials |
|
||||
| `Rank` | `Dict[str, Tuple[int, int]` | Current ranks, each entry is a tuple of the current rank, and age |
|
||||
| `Statistics` | `dict` | Contents of a Journal Statistics event, ie, data shown in the stats panel. See the Journal manual for more info |
|
||||
| `Role` | `Optional[str]` | Current role if in multi-crew, one of `Idle`, `FireCon`, `FighterCon` |
|
||||
| `Friends` | `set` | Currently online friend |
|
||||
| `ShipID` | `int` | Frontier ID of current ship |
|
||||
| `ShipIdent` | `str` | Current user-set ship ID |
|
||||
| `ShipName` | `str` | Current user-set ship name |
|
||||
| `ShipType` | `str` | Internal name for the current ship type |
|
||||
| `HullValue` | `int` | Current ship value, excluding modules |
|
||||
| `ModulesValue` | `int` | Value of the current ship's modules |
|
||||
| `Rebuy` | `int` | Current ship's rebuy cost |
|
||||
| `Modules` | `dict` | Currently fitted modules |
|
||||
|
||||
A special "StartUp" entry is sent if EDMC is started while the game is already
|
||||
running. In this case you won't receive initial events such as "LoadGame",
|
||||
@ -357,29 +415,27 @@ Similarly, a special "ShutDown" entry is sent when the game is quitted while
|
||||
EDMC is running. This event is not sent when EDMC is running on a different
|
||||
machine so you should not *rely* on receiving this event.
|
||||
|
||||
|
||||
#### Player Dashboard
|
||||
|
||||
```python
|
||||
import plug
|
||||
|
||||
def dashboard_entry(cmdr, is_beta, entry):
|
||||
def dashboard_entry(cmdr: str, is_beta: bool, entry: Dict[str, Any]):
|
||||
is_deployed = entry['Flags'] & plug.FlagsHardpointsDeployed
|
||||
sys.stderr.write("Hardpoints {}\n".format(is_deployed and "deployed" or "stowed"))
|
||||
```
|
||||
|
||||
This gets called when something on the player's cockpit display changes -
|
||||
typically about once a second when in orbital flight.
|
||||
|
||||
|
||||
- `cmdr` is a `str` denoting the current Commander Name.
|
||||
- `is_beta` is a `bool` denoting if data came from a beta version of the game.
|
||||
- `entry` is a `dict` loaded from the Status.json file the game writes.
|
||||
See the "Status File" section in the Frontier [Journal documentation](https://forums.frontier.co.uk/showthread.php/401661)
|
||||
for the available `entry` properties and for the list of available `"Flags"`.
|
||||
Ask on the EDCD Discord server to be sure you have the latest version.
|
||||
Refer to the source code of [plug.py](./plug.py) for the list of available
|
||||
constants.
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :----: | :-------------------------------- |
|
||||
| `cmdr` | `str` | Current command name |
|
||||
| `is_beta` | `bool` | if the game is currently in beta |
|
||||
| `entry` | `dict` | Data from status.json (see below) |
|
||||
|
||||
For more info on `status.json`, See the "Status File" section in the Frontier [Journal documentation](https://forums.frontier.co.uk/showthread.php/401661) for the available `entry` properties and for the list of available `"Flags"`. Refer to the source code of [plug.py](./plug.py) for the list of available constants.
|
||||
|
||||
#### Getting Commander Data
|
||||
|
||||
```python
|
||||
@ -387,19 +443,19 @@ def cmdr_data(data, is_beta):
|
||||
"""
|
||||
We have new data on our commander
|
||||
"""
|
||||
sys.stderr.write(data.get('commander') and data.get('commander').get('name') or '')
|
||||
if data.get('commander') is None or data['commander'].get('name') is None:
|
||||
raise ValueError("this isn't possible")
|
||||
|
||||
logger.info(data['commander']['name'])
|
||||
```
|
||||
|
||||
This gets called when EDMC has just fetched fresh Cmdr and station data from
|
||||
Frontier's servers.
|
||||
|
||||
- `data` is a dictionary containing the response from Frontier to a CAPI
|
||||
`/profile` request, augmented with two extra keys:
|
||||
- `marketdata` - contains the CAPI data from the `/market` endpoint, if
|
||||
docked and the station has the commodites service.
|
||||
- `shipdata` - contains the CAPI data from the `/shipyard` endpoint, if
|
||||
docked and the station has the shipyard service.
|
||||
- `is_beta` is a `bool` denoting if data came from a beta version of the game.
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :--------------: | :------------------------------------------------------------------------------------------------------- |
|
||||
| `data` | `Dict[str, Any]` | `/profile` API response, with `/market` and `/shipyard` added under the keys `marketdata` and `shipdata` |
|
||||
| `is_beta` | `bool` | If the game is currently in beta |
|
||||
|
||||
#### Plugin-specific events
|
||||
|
||||
@ -409,37 +465,41 @@ def edsm_notify_system(reply):
|
||||
`reply` holds the response from a call to https://www.edsm.net/en/api-journal-v1
|
||||
"""
|
||||
if not reply:
|
||||
sys.stderr.write("Error: Can't connect to EDSM\n")
|
||||
logger.info("Error: Can't connect to EDSM")
|
||||
|
||||
elif reply['msgnum'] // 100 not in (1,4):
|
||||
sys.stderr.write('Error: EDSM {MSG}\n').format(MSG=reply['msg'])
|
||||
logger.info(f'Error: EDSM {reply["msg"]}')
|
||||
|
||||
elif reply.get('systemCreated'):
|
||||
sys.stderr.write('New EDSM system!\n')
|
||||
logger.info('New EDSM system!')
|
||||
|
||||
else:
|
||||
sys.stderr.write('Known EDSM system\n')
|
||||
logger.info('Known EDSM system')
|
||||
```
|
||||
|
||||
If the player has chosen to "Send flight log and Cmdr status to EDSM" this gets
|
||||
called when the player starts the game or enters a new system. It is called
|
||||
some time after the corresponding `journal_entry()` event.
|
||||
|
||||
---
|
||||
| Parameter | Type | Description |
|
||||
| :-------- | :--------------: | :--------------------------------------------------------------------------------------------- |
|
||||
| `reply` | `Dict[str, Any]` | Response to an API call to [EDSM's journal API target](https://www.edsm.net/en/api-journal-v1) |
|
||||
|
||||
```python
|
||||
def inara_notify_location(eventData):
|
||||
def inara_notify_location(event_data):
|
||||
"""
|
||||
`eventData` holds the response to one of the "Commander's Flight Log" events https://inara.cz/inara-api-docs/#event-29
|
||||
`event_data` holds the response to one of the "Commander's Flight Log" events https://inara.cz/inara-api-docs/#event-29
|
||||
"""
|
||||
if eventData.get('starsystemInaraID'):
|
||||
sys.stderr.write('Now in Inara system {ID} at {URL}\n'.format(ID=eventData['starsystemInaraID'],
|
||||
URL=eventData['starsystemInaraURL'])
|
||||
)
|
||||
if event_data.get('starsystemInaraID'):
|
||||
logging.info(f'Now in Inara system {event_data["starsystemInaraID"]} at {event_data["starsystemInaraURL"]}')
|
||||
else:
|
||||
sys.stderr.write('System not known to Inara\n')
|
||||
if eventData.get('stationInaraID'):
|
||||
sys.stderr.write('Docked at Inara station {ID} at {URL}\n'.format(ID=eventData['stationInaraID'],
|
||||
URL=eventData['stationInaraURL'])
|
||||
)
|
||||
logger.info('System not known to Inara')
|
||||
|
||||
if event_data.get('stationInaraID'):
|
||||
logger.info(f'Docked at Inara station {event_data["stationInaraID"]} at {event_data["stationInaraURL"]}')
|
||||
|
||||
else:
|
||||
sys.stderr.write('Undocked or station unknown to Inara\n')
|
||||
logger.info('Undocked or station unknown to Inara')
|
||||
```
|
||||
|
||||
If the player has chosen to "Send flight log and Cmdr status to Inara" this
|
||||
@ -447,15 +507,20 @@ gets called when the player starts the game, enters a new system, docks or
|
||||
undocks. It is called some time after the corresponding `journal_entry()`
|
||||
event.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :----------- | :--------------: | :----------------------------------------------------------------------------------------------------------- |
|
||||
| `event_data` | `Dict[str, Any]` | Response to an API call to [INARA's `Commander Flight Log` event](https://inara.cz/inara-api-docs/#event-29) |
|
||||
|
||||
---
|
||||
|
||||
```python
|
||||
def inara_notify_ship(eventData):
|
||||
def inara_notify_ship(event_data):
|
||||
"""
|
||||
`eventData` holds the response to an addCommanderShip or setCommanderShip event https://inara.cz/inara-api-docs/#event-11
|
||||
`event_data` holds the response to an addCommanderShip or setCommanderShip event https://inara.cz/inara-api-docs/#event-11
|
||||
"""
|
||||
if eventData.get('shipInaraID'):
|
||||
sys.stderr.write('Now in Inara ship {ID} at {URL}\n'.format(ID=eventData['shipInaraID'],
|
||||
URL=eventData['shipInaraURL'])
|
||||
if event_data.get('shipInaraID'):
|
||||
logger.info(
|
||||
f'Now in Inara ship {event_data['shipInaraID'],} at {event_data['shipInaraURL']}
|
||||
)
|
||||
```
|
||||
|
||||
@ -463,6 +528,10 @@ If the player has chosen to "Send flight log and Cmdr status to Inara" this
|
||||
gets called when the player starts the game or switches ship. It is called some
|
||||
time after the corresponding `journal_entry()` event.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| :----------- | :--------------: | :----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `event_data` | `Dict[str, Any]` | Response to an API call to [INARA's `addCommanderShip` or `setCommanderShip` event](https://inara.cz/inara-api-docs/#event-11) |
|
||||
|
||||
## Error messages
|
||||
|
||||
You can display an error in EDMC's status area by returning a string from your
|
||||
@ -490,7 +559,7 @@ _ = functools.partial(l10n.Translations.translate, context=__file__)
|
||||
Wrap each string that needs translating with the `_()` function, e.g.:
|
||||
|
||||
```python
|
||||
this.status["text"] = _('Happy!') # Main window status
|
||||
status["text"] = _('Happy!') # Main window status
|
||||
```
|
||||
|
||||
If you display localized strings in EDMC's main window you should refresh them
|
||||
@ -508,7 +577,6 @@ See EDMC's own [`L10n`](https://github.com/EDCD/EDMarketConnector/tree/master/L1
|
||||
folder for the list of supported language codes and for example translation
|
||||
files.
|
||||
|
||||
|
||||
## Python Package Plugins
|
||||
|
||||
A _Package Plugin_ is both a standard Python package (i.e. contains an
|
||||
@ -519,20 +587,19 @@ before any non-Package plugins.
|
||||
Other plugins can access features in a Package Plugin by `import`ing the
|
||||
package by name in the usual way.
|
||||
|
||||
|
||||
## Distributing a Plugin
|
||||
|
||||
To package your plugin for distribution simply create a `.zip` archive of your
|
||||
plugin's folder:
|
||||
|
||||
* Windows: In Explorer right click on your plugin's folder and choose Send to
|
||||
→ Compressed (zipped) folder.
|
||||
* Mac: In Finder right click on your plugin's folder and choose Compress.
|
||||
- Windows: In Explorer right click on your plugin's folder and choose Send to
|
||||
→ Compressed (zipped) folder.
|
||||
- Mac: In Finder right click on your plugin's folder and choose Compress.
|
||||
|
||||
If there are any external dependencies then include them in the plugin's
|
||||
folder.
|
||||
|
||||
Optionally, for tidiness delete any `.pyc` and `.pyo` files in the archive.
|
||||
Optionally, for tidiness delete any `.pyc` and `.pyo` files in the archive, as well as the `__pycache__` directory.
|
||||
|
||||
## Disable a plugin
|
||||
|
||||
@ -545,23 +612,26 @@ Disabled and enabled plugins are listed on the "Plugins" Settings tab
|
||||
## Migration
|
||||
|
||||
Starting with pre-release 3.5 EDMC uses Python **3.7**. The first full
|
||||
release under Python 3.7 will be 4.0.0.0. This is a brief outline of the steps
|
||||
release under Python 3.7 was 4.0.0.0. This is a brief outline of the steps
|
||||
required to migrate a plugin from earlier versions of EDMC:
|
||||
|
||||
- Rename the function `plugin_start` to `plugin_start3(plugin_dir)`.
|
||||
Plugins without a `plugin_start3` function are listed as disabled on EDMC's
|
||||
"Plugins" tab and a message like "plugin SuperSpaceHelper needs migrating"
|
||||
appears in the log. Such plugins are also listed in a section "Plugins Without
|
||||
Python 3.x Support:" on the Settings > Plugins tab.
|
||||
Plugins without a `plugin_start3` function are listed as disabled on EDMC's
|
||||
"Plugins" tab and a message like "plugin SuperSpaceHelper needs migrating"
|
||||
appears in the log. Such plugins are also listed in a section "Plugins Without
|
||||
Python 3.x Support:" on the Settings > Plugins tab.
|
||||
|
||||
- Check that callback functions `plugin_prefs`, `prefs_changed`,
|
||||
`journal_entry`, `dashboard_entry` and `cmdr_data` if used are declared with
|
||||
the correct number of arguments. Older versions of this app were tolerant
|
||||
of missing arguments in these function declarations.
|
||||
`journal_entry`, `dashboard_entry` and `cmdr_data` if used are declared with
|
||||
the correct number of arguments. Older versions of this app were tolerant
|
||||
of missing arguments in these function declarations.
|
||||
|
||||
- Port the code to Python 3.7. The [2to3](https://docs.python.org/3/library/2to3.html)
|
||||
tool can automate much of this work.
|
||||
tool can automate much of this work.
|
||||
|
||||
Depending on the complexity of the plugin it may be feasible to make it
|
||||
compatible with both EDMC 3.4 + Python 2.7 and EDMC 3.5 + Python 3.7.
|
||||
|
||||
[Here's](https://python-future.org/compatible_idioms.html) a guide on writing
|
||||
Python 2/3 compatible code and [here's](https://github.com/Marginal/HabZone/commit/3c41cd41d5ad81ef36aab40e967e3baf77b4bd06)
|
||||
an example of the changes required for a simple plugin.
|
||||
|
385
companion.py
385
companion.py
@ -1,33 +1,42 @@
|
||||
from builtins import str
|
||||
from builtins import range
|
||||
from builtins import object
|
||||
"""
|
||||
Handle use of Frontier's Companion API (CAPI) service.
|
||||
|
||||
Deals with initiating authentication for, and use of, CAPI.
|
||||
Some associated code is in protocol.py which creates and handles the edmc://
|
||||
protocol used for the callback.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import csv
|
||||
import requests
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# TODO: see https://github.com/EDCD/EDMarketConnector/issues/569
|
||||
from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but retained in case plugins use it
|
||||
from email.utils import parsedate
|
||||
import hashlib
|
||||
import numbers
|
||||
import os
|
||||
from os.path import join
|
||||
import random
|
||||
import time
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
from builtins import object, range, str
|
||||
from email.utils import parsedate
|
||||
# TODO: see https://github.com/EDCD/EDMarketConnector/issues/569
|
||||
from http.cookiejar import LWPCookieJar # noqa: F401 - No longer needed but retained in case plugins use it
|
||||
from os.path import join
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, NewType, Union
|
||||
|
||||
import requests
|
||||
|
||||
from config import appname, appversion, config
|
||||
from EDMCLogging import get_main_logger
|
||||
from protocol import protocolhandler
|
||||
import logging
|
||||
logger = logging.getLogger(appname)
|
||||
|
||||
logger = get_main_logger()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_ = lambda x: x # noqa # to make flake8 stop complaining that the hacked in _ method doesnt exist
|
||||
_ = lambda x: x # noqa: E731 # to make flake8 stop complaining that the hacked in _ method doesnt exist
|
||||
|
||||
|
||||
# Define custom type for the dicts that hold CAPI data
|
||||
CAPIData = NewType('CAPIData', Dict)
|
||||
|
||||
holdoff = 60 # be nice
|
||||
timeout = 10 # requests timeout
|
||||
auth_timeout = 30 # timeout for initial auth
|
||||
@ -35,83 +44,89 @@ auth_timeout = 30 # timeout for initial auth
|
||||
# Currently the "Elite Dangerous Market Connector (EDCD/Athanasius)" one in
|
||||
# Athanasius' Frontier account
|
||||
# Obtain from https://auth.frontierstore.net/client/signup
|
||||
CLIENT_ID = os.getenv('CLIENT_ID') or 'fb88d428-9110-475f-a3d2-dc151c2b9c7a'
|
||||
CLIENT_ID = os.getenv('CLIENT_ID') or 'fb88d428-9110-475f-a3d2-dc151c2b9c7a'
|
||||
SERVER_AUTH = 'https://auth.frontierstore.net'
|
||||
URL_AUTH = '/auth'
|
||||
URL_AUTH = '/auth'
|
||||
URL_TOKEN = '/token'
|
||||
|
||||
USER_AGENT = 'EDCD-{}-{}'.format(appname, appversion)
|
||||
USER_AGENT = f'EDCD-{appname}-{appversion}'
|
||||
|
||||
SERVER_LIVE = 'https://companion.orerve.net'
|
||||
SERVER_BETA = 'https://pts-companion.orerve.net'
|
||||
URL_QUERY = '/profile'
|
||||
URL_MARKET = '/market'
|
||||
URL_SHIPYARD= '/shipyard'
|
||||
URL_QUERY = '/profile'
|
||||
URL_MARKET = '/market'
|
||||
URL_SHIPYARD = '/shipyard'
|
||||
|
||||
|
||||
# Map values reported by the Companion interface to names displayed in-game
|
||||
# May be imported by plugins
|
||||
category_map = {
|
||||
'Narcotics' : 'Legal Drugs',
|
||||
'Slaves' : 'Slavery',
|
||||
'Waste ' : 'Waste',
|
||||
'NonMarketable' : False, # Don't appear in the in-game market so don't report
|
||||
'Narcotics': 'Legal Drugs',
|
||||
'Slaves': 'Slavery',
|
||||
'Waste ': 'Waste',
|
||||
'NonMarketable': False, # Don't appear in the in-game market so don't report
|
||||
}
|
||||
|
||||
commodity_map = {}
|
||||
commodity_map: Dict = {}
|
||||
|
||||
ship_map = {
|
||||
'adder' : 'Adder',
|
||||
'anaconda' : 'Anaconda',
|
||||
'asp' : 'Asp Explorer',
|
||||
'asp_scout' : 'Asp Scout',
|
||||
'belugaliner' : 'Beluga Liner',
|
||||
'cobramkiii' : 'Cobra MkIII',
|
||||
'cobramkiv' : 'Cobra MkIV',
|
||||
'clipper' : 'Panther Clipper',
|
||||
'cutter' : 'Imperial Cutter',
|
||||
'diamondback' : 'Diamondback Scout',
|
||||
'diamondbackxl' : 'Diamondback Explorer',
|
||||
'dolphin' : 'Dolphin',
|
||||
'eagle' : 'Eagle',
|
||||
'empire_courier' : 'Imperial Courier',
|
||||
'empire_eagle' : 'Imperial Eagle',
|
||||
'empire_fighter' : 'Imperial Fighter',
|
||||
'empire_trader' : 'Imperial Clipper',
|
||||
'federation_corvette' : 'Federal Corvette',
|
||||
'federation_dropship' : 'Federal Dropship',
|
||||
'federation_dropship_mkii' : 'Federal Assault Ship',
|
||||
'federation_gunship' : 'Federal Gunship',
|
||||
'federation_fighter' : 'F63 Condor',
|
||||
'ferdelance' : 'Fer-de-Lance',
|
||||
'hauler' : 'Hauler',
|
||||
'independant_trader' : 'Keelback',
|
||||
'independent_fighter' : 'Taipan Fighter',
|
||||
'krait_mkii' : 'Krait MkII',
|
||||
'krait_light' : 'Krait Phantom',
|
||||
'mamba' : 'Mamba',
|
||||
'orca' : 'Orca',
|
||||
'python' : 'Python',
|
||||
'scout' : 'Taipan Fighter',
|
||||
'sidewinder' : 'Sidewinder',
|
||||
'testbuggy' : 'Scarab',
|
||||
'type6' : 'Type-6 Transporter',
|
||||
'type7' : 'Type-7 Transporter',
|
||||
'type9' : 'Type-9 Heavy',
|
||||
'type9_military' : 'Type-10 Defender',
|
||||
'typex' : 'Alliance Chieftain',
|
||||
'typex_2' : 'Alliance Crusader',
|
||||
'typex_3' : 'Alliance Challenger',
|
||||
'viper' : 'Viper MkIII',
|
||||
'viper_mkiv' : 'Viper MkIV',
|
||||
'vulture' : 'Vulture',
|
||||
'adder': 'Adder',
|
||||
'anaconda': 'Anaconda',
|
||||
'asp': 'Asp Explorer',
|
||||
'asp_scout': 'Asp Scout',
|
||||
'belugaliner': 'Beluga Liner',
|
||||
'cobramkiii': 'Cobra MkIII',
|
||||
'cobramkiv': 'Cobra MkIV',
|
||||
'clipper': 'Panther Clipper',
|
||||
'cutter': 'Imperial Cutter',
|
||||
'diamondback': 'Diamondback Scout',
|
||||
'diamondbackxl': 'Diamondback Explorer',
|
||||
'dolphin': 'Dolphin',
|
||||
'eagle': 'Eagle',
|
||||
'empire_courier': 'Imperial Courier',
|
||||
'empire_eagle': 'Imperial Eagle',
|
||||
'empire_fighter': 'Imperial Fighter',
|
||||
'empire_trader': 'Imperial Clipper',
|
||||
'federation_corvette': 'Federal Corvette',
|
||||
'federation_dropship': 'Federal Dropship',
|
||||
'federation_dropship_mkii': 'Federal Assault Ship',
|
||||
'federation_gunship': 'Federal Gunship',
|
||||
'federation_fighter': 'F63 Condor',
|
||||
'ferdelance': 'Fer-de-Lance',
|
||||
'hauler': 'Hauler',
|
||||
'independant_trader': 'Keelback',
|
||||
'independent_fighter': 'Taipan Fighter',
|
||||
'krait_mkii': 'Krait MkII',
|
||||
'krait_light': 'Krait Phantom',
|
||||
'mamba': 'Mamba',
|
||||
'orca': 'Orca',
|
||||
'python': 'Python',
|
||||
'scout': 'Taipan Fighter',
|
||||
'sidewinder': 'Sidewinder',
|
||||
'testbuggy': 'Scarab',
|
||||
'type6': 'Type-6 Transporter',
|
||||
'type7': 'Type-7 Transporter',
|
||||
'type9': 'Type-9 Heavy',
|
||||
'type9_military': 'Type-10 Defender',
|
||||
'typex': 'Alliance Chieftain',
|
||||
'typex_2': 'Alliance Crusader',
|
||||
'typex_3': 'Alliance Challenger',
|
||||
'viper': 'Viper MkIII',
|
||||
'viper_mkiv': 'Viper MkIV',
|
||||
'vulture': 'Vulture',
|
||||
}
|
||||
|
||||
|
||||
# Companion API sometimes returns an array as a json array, sometimes as a json object indexed by "int".
|
||||
# This seems to depend on whether the there are 'gaps' in the Cmdr's data - i.e. whether the array is sparse.
|
||||
# In practice these arrays aren't very sparse so just convert them to lists with any 'gaps' holding None.
|
||||
def listify(thing):
|
||||
def listify(thing: Union[List, Dict]) -> List:
|
||||
"""
|
||||
Convert actual JSON array or int-indexed dict into a Python list.
|
||||
|
||||
Companion API sometimes returns an array as a json array, sometimes as
|
||||
a json object indexed by "int". This seems to depend on whether the
|
||||
there are 'gaps' in the Cmdr's data - i.e. whether the array is sparse.
|
||||
In practice these arrays aren't very sparse so just convert them to
|
||||
lists with any 'gaps' holding None.
|
||||
"""
|
||||
if thing is None:
|
||||
return [] # data is not present
|
||||
|
||||
@ -119,7 +134,7 @@ def listify(thing):
|
||||
return list(thing) # array is not sparse
|
||||
|
||||
elif isinstance(thing, dict):
|
||||
retval = []
|
||||
retval: List[Any] = []
|
||||
for k, v in thing.items():
|
||||
idx = int(k)
|
||||
|
||||
@ -132,10 +147,12 @@ def listify(thing):
|
||||
return retval
|
||||
|
||||
else:
|
||||
raise ValueError("expected an array or sparse array, got {!r}".format(thing))
|
||||
raise ValueError(f"expected an array or sparse array, got {thing!r}")
|
||||
|
||||
|
||||
class ServerError(Exception):
|
||||
"""Exception Class for CAPI ServerErrors."""
|
||||
|
||||
def __init__(self, *args):
|
||||
# Raised when cannot contact the Companion API server
|
||||
self.args = args
|
||||
@ -144,23 +161,34 @@ class ServerError(Exception):
|
||||
|
||||
|
||||
class ServerLagging(Exception):
|
||||
"""Exception Class for CAPI Server lagging.
|
||||
|
||||
Raised when Companion API server is returning old data, e.g. when the
|
||||
servers are too busy.
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
# Raised when Companion API server is returning old data, e.g. when the servers are too busy
|
||||
self.args = args
|
||||
if not args:
|
||||
self.args = (_('Error: Frontier server is lagging'),)
|
||||
|
||||
|
||||
class SKUError(Exception):
|
||||
"""Exception Class for CAPI SKU error.
|
||||
|
||||
Raised when the Companion API server thinks that the user has not
|
||||
purchased E:D i.e. doesn't have the correct 'SKU'.
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
# Raised when the Companion API server thinks that the user has not purchased E:D
|
||||
# i.e. doesn't have the correct 'SKU'
|
||||
self.args = args
|
||||
if not args:
|
||||
self.args = (_('Error: Frontier server SKU problem'),)
|
||||
|
||||
|
||||
class CredentialsError(Exception):
|
||||
"""Exception Class for CAPI Credentials error."""
|
||||
|
||||
def __init__(self, *args):
|
||||
self.args = args
|
||||
if not args:
|
||||
@ -168,24 +196,38 @@ class CredentialsError(Exception):
|
||||
|
||||
|
||||
class CmdrError(Exception):
|
||||
"""Exception Class for CAPI Commander error.
|
||||
|
||||
Raised when the user has multiple accounts and the username/password
|
||||
setting is not for the account they're currently playing OR the user has
|
||||
reset their Cmdr and the Companion API server is still returning data
|
||||
for the old Cmdr.
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
# Raised when the user has multiple accounts and the username/password setting is not for
|
||||
# the account they're currently playing OR the user has reset their Cmdr and the Companion API
|
||||
# server is still returning data for the old Cmdr
|
||||
self.args = args
|
||||
if not args:
|
||||
self.args = (_('Error: Wrong Cmdr'),)
|
||||
|
||||
|
||||
class Auth(object):
|
||||
def __init__(self, cmdr):
|
||||
self.cmdr = cmdr
|
||||
"""Handles authentication with the Frontier CAPI service via oAuth2."""
|
||||
|
||||
def __init__(self, cmdr: str):
|
||||
self.cmdr: str = cmdr
|
||||
self.session = requests.Session()
|
||||
self.session.headers['User-Agent'] = USER_AGENT
|
||||
self.verifier = self.state = None
|
||||
self.verifier: Union[bytes, None] = None
|
||||
self.state: Union[str, None] = None
|
||||
|
||||
def refresh(self):
|
||||
# Try refresh token. Returns new refresh token if successful, otherwise makes new authorization request.
|
||||
def refresh(self) -> Union[str, None]:
|
||||
"""
|
||||
Attempt use of Refresh Token to get a valid Access Token.
|
||||
|
||||
If the Refresh Token doesn't work, make a new authorization request.
|
||||
|
||||
:return: Access Token if retrieved, else None.
|
||||
"""
|
||||
logger.debug(f'Trying for "{self.cmdr}"')
|
||||
|
||||
self.verifier = None
|
||||
@ -199,14 +241,14 @@ class Auth(object):
|
||||
tokens = tokens + [''] * (len(cmdrs) - len(tokens))
|
||||
if tokens[idx]:
|
||||
logger.debug('We have a refresh token for that idx')
|
||||
try:
|
||||
data = {
|
||||
'grant_type': 'refresh_token',
|
||||
'client_id': CLIENT_ID,
|
||||
'refresh_token': tokens[idx],
|
||||
}
|
||||
data = {
|
||||
'grant_type': 'refresh_token',
|
||||
'client_id': CLIENT_ID,
|
||||
'refresh_token': tokens[idx],
|
||||
}
|
||||
|
||||
logger.debug('Attempting refresh with Frontier...')
|
||||
logger.debug('Attempting refresh with Frontier...')
|
||||
try:
|
||||
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=auth_timeout)
|
||||
if r.status_code == requests.codes.ok:
|
||||
data = r.json()
|
||||
@ -219,7 +261,7 @@ class Auth(object):
|
||||
logger.error(f"Frontier CAPI Auth: Can't refresh token for \"{self.cmdr}\"")
|
||||
self.dump(r)
|
||||
|
||||
except Exception as e:
|
||||
except (ValueError, requests.RequestException, ):
|
||||
logger.exception(f"Frontier CAPI Auth: Can't refresh token for \"{self.cmdr}\"")
|
||||
|
||||
else:
|
||||
@ -233,21 +275,19 @@ class Auth(object):
|
||||
self.state = self.base64_url_encode(s.to_bytes(32, byteorder='big'))
|
||||
# Won't work under IE: https://blogs.msdn.microsoft.com/ieinternals/2011/07/13/understanding-protocols/
|
||||
logger.debug(f'Trying auth from scratch for Commander "{self.cmdr}"')
|
||||
challenge = self.base64_url_encode(hashlib.sha256(self.verifier).digest())
|
||||
webbrowser.open(
|
||||
'{server_auth}{url_auth}?response_type=code&audience=frontier&scope=capi&client_id={client_id}&code_challenge={challenge}&code_challenge_method=S256&state={state}&redirect_uri={redirect}'.format( # noqa: E501 # I cant make this any shorter
|
||||
server_auth=SERVER_AUTH,
|
||||
url_auth=URL_AUTH,
|
||||
client_id=CLIENT_ID,
|
||||
challenge=self.base64_url_encode(hashlib.sha256(self.verifier).digest()),
|
||||
state=self.state,
|
||||
redirect=protocolhandler.redirect
|
||||
)
|
||||
f'{SERVER_AUTH}{URL_AUTH}?response_type=code&audience=frontier&scope=capi&client_id={CLIENT_ID}&code_challenge={challenge}&code_challenge_method=S256&state={self.state}&redirect_uri={protocolhandler.redirect}' # noqa: E501 # I cant make this any shorter
|
||||
)
|
||||
|
||||
def authorize(self, payload):
|
||||
return None
|
||||
|
||||
def authorize(self, payload: str) -> str:
|
||||
"""Handle oAuth authorization callback.
|
||||
|
||||
:return: access token if successful, otherwise raises CredentialsError.
|
||||
"""
|
||||
logger.debug('Checking oAuth authorization callback')
|
||||
# Handle OAuth authorization code callback.
|
||||
# Returns access token if successful, otherwise raises CredentialsError
|
||||
if '?' not in payload:
|
||||
logger.error(f'Frontier CAPI Auth: Malformed response (no "?" in payload)\n{payload}\n')
|
||||
raise CredentialsError('malformed payload') # Not well formed
|
||||
@ -255,19 +295,20 @@ class Auth(object):
|
||||
data = urllib.parse.parse_qs(payload[(payload.index('?') + 1):])
|
||||
if not self.state or not data.get('state') or data['state'][0] != self.state:
|
||||
logger.error(f'Frontier CAPI Auth: Unexpected response\n{payload}\n')
|
||||
raise CredentialsError('Unexpected response from authorization {!r}'.format(payload)) # Unexpected reply
|
||||
raise CredentialsError(f'Unexpected response from authorization {payload!r}')
|
||||
|
||||
if not data.get('code'):
|
||||
logger.error(f'Frontier CAPI Auth: Negative response (no "code" in returned data)\n{payload}\n')
|
||||
error = next(
|
||||
(data[k] for k in ('error_description', 'error', 'message') if k in data), ('<unknown error>',)
|
||||
(data[k] for k in ('error_description', 'error', 'message') if k in data),
|
||||
'<unknown error>'
|
||||
)
|
||||
raise CredentialsError('Error: {!r}'.format(error)[0])
|
||||
raise CredentialsError(f'Error: {error!r}')
|
||||
|
||||
r = None
|
||||
try:
|
||||
logger.debug('Got code, posting it back...')
|
||||
r = None
|
||||
data = {
|
||||
request_data = {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': CLIENT_ID,
|
||||
'code_verifier': self.verifier,
|
||||
@ -275,7 +316,7 @@ class Auth(object):
|
||||
'redirect_uri': protocolhandler.redirect,
|
||||
}
|
||||
|
||||
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=auth_timeout)
|
||||
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=request_data, timeout=auth_timeout)
|
||||
data = r.json()
|
||||
if r.status_code == requests.codes.ok:
|
||||
logger.info(f'Frontier CAPI Auth: New token for \"{self.cmdr}\"')
|
||||
@ -287,7 +328,7 @@ class Auth(object):
|
||||
config.set('fdev_apikeys', tokens)
|
||||
config.save() # Save settings now for use by command-line app
|
||||
|
||||
return data.get('access_token')
|
||||
return str(data.get('access_token'))
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"")
|
||||
@ -298,11 +339,15 @@ class Auth(object):
|
||||
|
||||
logger.error(f"Frontier CAPI Auth: Can't get token for \"{self.cmdr}\"")
|
||||
self.dump(r)
|
||||
error = next((data[k] for k in ('error_description', 'error', 'message') if k in data), ('<unknown error>',))
|
||||
raise CredentialsError('Error: {!r}'.format(error)[0])
|
||||
error = next(
|
||||
(data[k] for k in ('error_description', 'error', 'message') if k in data),
|
||||
'<unknown error>'
|
||||
)
|
||||
raise CredentialsError(f'Error: {error!r}')
|
||||
|
||||
@staticmethod
|
||||
def invalidate(cmdr):
|
||||
def invalidate(cmdr: str) -> None:
|
||||
"""Invalidate Refresh Token for specified Commander."""
|
||||
logger.info(f'Frontier CAPI Auth: Invalidated token for "{cmdr}"')
|
||||
cmdrs = config.get('cmdrs')
|
||||
idx = cmdrs.index(cmdr)
|
||||
@ -312,25 +357,36 @@ class Auth(object):
|
||||
config.set('fdev_apikeys', tokens)
|
||||
config.save() # Save settings now for use by command-line app
|
||||
|
||||
def dump(self, r):
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def dump(self, r: requests.Response) -> None:
|
||||
"""Dump details of HTTP failure from oAuth attempt."""
|
||||
logger.debug(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason if r.reason else "None"} {r.text}')
|
||||
|
||||
def base64_url_encode(self, text):
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def base64_url_encode(self, text: bytes) -> str:
|
||||
"""Base64 encode text for URL."""
|
||||
return base64.urlsafe_b64encode(text).decode().replace('=', '')
|
||||
|
||||
|
||||
class Session(object):
|
||||
"""Methods for handling an oAuth2 session."""
|
||||
|
||||
STATE_INIT, STATE_AUTH, STATE_OK = list(range(3))
|
||||
|
||||
def __init__(self):
|
||||
self.state = Session.STATE_INIT
|
||||
self.server = None
|
||||
self.credentials = None
|
||||
self.session = None
|
||||
self.auth = None
|
||||
self.retrying = False # Avoid infinite loop when successful auth / unsuccessful query
|
||||
|
||||
def login(self, cmdr=None, is_beta=None):
|
||||
# Returns True if login succeeded, False if re-authorization initiated.
|
||||
def login(self, cmdr: str = None, is_beta: Union[None, bool] = None) -> bool:
|
||||
"""
|
||||
Attempt oAuth2 login.
|
||||
|
||||
:return: True if login succeeded, False if re-authorization initiated.
|
||||
"""
|
||||
if not CLIENT_ID:
|
||||
logger.error('CLIENT_ID is None')
|
||||
raise CredentialsError('cannot login without a valid Client ID')
|
||||
@ -376,7 +432,8 @@ class Session(object):
|
||||
# Wait for callback
|
||||
|
||||
# Callback from protocol handler
|
||||
def auth_callback(self):
|
||||
def auth_callback(self) -> None:
|
||||
"""Handle callback from edmc:// handler."""
|
||||
logger.debug('Handling callback from edmc:// handler')
|
||||
if self.state != Session.STATE_AUTH:
|
||||
# Shouldn't be getting a callback
|
||||
@ -394,14 +451,16 @@ class Session(object):
|
||||
self.auth = None
|
||||
raise # Bad thing happened
|
||||
|
||||
def start(self, access_token):
|
||||
def start(self, access_token: str) -> None:
|
||||
"""Start an oAuth2 session."""
|
||||
logger.debug('Starting session')
|
||||
self.session = requests.Session()
|
||||
self.session.headers['Authorization'] = 'Bearer {}'.format(access_token)
|
||||
self.session.headers['Authorization'] = f'Bearer {access_token}'
|
||||
self.session.headers['User-Agent'] = USER_AGENT
|
||||
self.state = Session.STATE_OK
|
||||
|
||||
def query(self, endpoint):
|
||||
def query(self, endpoint: str) -> CAPIData:
|
||||
"""Perform a query against the specified CAPI endpoint."""
|
||||
logger.debug(f'Performing query for endpoint "{endpoint}"')
|
||||
if self.state == Session.STATE_INIT:
|
||||
if self.login():
|
||||
@ -432,7 +491,7 @@ class Session(object):
|
||||
# Server error. Typically 500 "Internal Server Error" if server is down
|
||||
logger.debug('500 status back from CAPI')
|
||||
self.dump(r)
|
||||
raise ServerError('Received error {} from server'.format(r.status_code))
|
||||
raise ServerError(f'Received error {r.status_code} from server')
|
||||
|
||||
try:
|
||||
r.raise_for_status() # Typically 403 "Forbidden" on token expiry
|
||||
@ -463,40 +522,45 @@ class Session(object):
|
||||
self.retrying = False
|
||||
if 'timestamp' not in data:
|
||||
logger.debug('timestamp not in data, adding from response headers')
|
||||
data['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', parsedate(r.headers['Date']))
|
||||
data['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', parsedate(r.headers['Date'])) # type: ignore
|
||||
|
||||
return data
|
||||
|
||||
def profile(self):
|
||||
def profile(self) -> CAPIData:
|
||||
"""Perform general CAPI /profile endpoint query."""
|
||||
return self.query(URL_QUERY)
|
||||
|
||||
def station(self):
|
||||
def station(self) -> CAPIData:
|
||||
"""Perform CAPI /profile endpoint query for station data."""
|
||||
data = self.query(URL_QUERY)
|
||||
if data['commander'].get('docked'):
|
||||
services = data['lastStarport'].get('services', {})
|
||||
if not data['commander'].get('docked'):
|
||||
return data
|
||||
|
||||
last_starport_name = data['lastStarport']['name']
|
||||
last_starport_id = int(data['lastStarport']['id'])
|
||||
services = data['lastStarport'].get('services', {})
|
||||
|
||||
if services.get('commodities'):
|
||||
marketdata = self.query(URL_MARKET)
|
||||
if (last_starport_name != marketdata['name'] or last_starport_id != int(marketdata['id'])):
|
||||
raise ServerLagging()
|
||||
last_starport_name = data['lastStarport']['name']
|
||||
last_starport_id = int(data['lastStarport']['id'])
|
||||
|
||||
else:
|
||||
data['lastStarport'].update(marketdata)
|
||||
if services.get('commodities'):
|
||||
marketdata = self.query(URL_MARKET)
|
||||
if last_starport_name != marketdata['name'] or last_starport_id != int(marketdata['id']):
|
||||
raise ServerLagging()
|
||||
|
||||
if services.get('outfitting') or services.get('shipyard'):
|
||||
shipdata = self.query(URL_SHIPYARD)
|
||||
if (last_starport_name != shipdata['name'] or last_starport_id != int(shipdata['id'])):
|
||||
raise ServerLagging()
|
||||
else:
|
||||
data['lastStarport'].update(marketdata)
|
||||
|
||||
else:
|
||||
data['lastStarport'].update(shipdata)
|
||||
if services.get('outfitting') or services.get('shipyard'):
|
||||
shipdata = self.query(URL_SHIPYARD)
|
||||
if last_starport_name != shipdata['name'] or last_starport_id != int(shipdata['id']):
|
||||
raise ServerLagging()
|
||||
|
||||
else:
|
||||
data['lastStarport'].update(shipdata)
|
||||
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Close CAPI authorization session."""
|
||||
self.state = Session.STATE_INIT
|
||||
if self.session:
|
||||
try:
|
||||
@ -507,18 +571,26 @@ class Session(object):
|
||||
|
||||
self.session = None
|
||||
|
||||
def invalidate(self):
|
||||
def invalidate(self) -> None:
|
||||
"""Invalidate oAuth2 credentials."""
|
||||
logger.debug('Forcing a full re-authentication')
|
||||
# Force a full re-authentication
|
||||
self.close()
|
||||
Auth.invalidate(self.credentials['cmdr'])
|
||||
|
||||
def dump(self, r):
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def dump(self, r: requests.Response) -> None:
|
||||
"""Log, as error, status of requests.Response from CAPI request."""
|
||||
logger.error(f'Frontier CAPI Auth: {r.url} {r.status_code} {r.reason and r.reason or "None"} {r.text}')
|
||||
|
||||
# Returns a shallow copy of the received data suitable for export to older tools
|
||||
# English commodity names and anomalies fixed up
|
||||
def fixup(data):
|
||||
|
||||
def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully simplified
|
||||
"""
|
||||
Fix up commodity names to English & miscellaneous anomalies fixes.
|
||||
|
||||
:return: a shallow copy of the received data suitable for export to
|
||||
older tools.
|
||||
"""
|
||||
if not commodity_map:
|
||||
# Lazily populate
|
||||
for f in ('commodity.csv', 'rare_commodity.csv'):
|
||||
@ -586,15 +658,16 @@ def fixup(data):
|
||||
datacopy = data.copy()
|
||||
datacopy['lastStarport'] = data['lastStarport'].copy()
|
||||
datacopy['lastStarport']['commodities'] = commodities
|
||||
return datacopy
|
||||
return CAPIData(datacopy)
|
||||
|
||||
|
||||
# Return a subset of the received data describing the current ship
|
||||
def ship(data):
|
||||
def filter_ship(d):
|
||||
filtered = {}
|
||||
def ship(data: CAPIData) -> CAPIData:
|
||||
"""Construct a subset of the received data describing the current ship."""
|
||||
def filter_ship(d: CAPIData) -> CAPIData:
|
||||
"""Filter provided ship data."""
|
||||
filtered: CAPIData = CAPIData({})
|
||||
for k, v in d.items():
|
||||
if v == []:
|
||||
if not v:
|
||||
pass # just skip empty fields for brevity
|
||||
|
||||
elif k in ('alive', 'cargo', 'cockpitBreached', 'health', 'oxygenRemaining',
|
||||
@ -619,8 +692,8 @@ def ship(data):
|
||||
return filter_ship(data['ship'])
|
||||
|
||||
|
||||
# Ship name suitable for writing to a file
|
||||
def ship_file_name(ship_name, ship_type):
|
||||
def ship_file_name(ship_name: str, ship_type: str) -> str:
|
||||
"""Return a ship name suitable for a filename."""
|
||||
name = str(ship_name or ship_map.get(ship_type.lower(), ship_type)).strip()
|
||||
if name.endswith('.'):
|
||||
name = name[:-1]
|
||||
|
@ -13,7 +13,7 @@ appcmdname = 'EDMC'
|
||||
# appversion **MUST** follow Semantic Versioning rules:
|
||||
# <https://semver.org/#semantic-versioning-specification-semver>
|
||||
# Major.Minor.Patch(-prerelease)(+buildmetadata)
|
||||
appversion = '4.1.0-beta4' #-rc1+a872b5f'
|
||||
appversion = '4.1.0-beta6' #-rc1+a872b5f'
|
||||
# For some things we want appversion without (possible) +build metadata
|
||||
appversion_nobuild = str(semantic_version.Version(appversion).truncate('prerelease'))
|
||||
copyright = u'© 2015-2019 Jonathan Harris, 2020 EDCD'
|
||||
|
126
docs/examples/click_counter/load.py
Normal file
126
docs/examples/click_counter/load.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""
|
||||
Example EDMC plugin.
|
||||
|
||||
It adds a single button to the EDMC interface that displays the number of times it has been clicked.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from typing import Optional
|
||||
|
||||
import myNotebook as nb
|
||||
from config import appname, config
|
||||
|
||||
PLUGIN_NAME = "ClickCounter"
|
||||
|
||||
logger = logging.getLogger(f"{appname}.{PLUGIN_NAME}")
|
||||
|
||||
|
||||
class ClickCounter:
|
||||
"""
|
||||
ClickCounter implements the EDMC plugin interface.
|
||||
|
||||
It adds a button to the EDMC UI that displays the number of times it has been clicked, and a preference to set
|
||||
the number directly.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Be sure to use names that wont collide in our config variables
|
||||
self.click_count: Optional[tk.StringVar] = tk.StringVar(value=str(config.getint('click_counter_count')))
|
||||
logger.info("ClickCounter instantiated")
|
||||
|
||||
def on_load(self) -> str:
|
||||
"""
|
||||
on_load is called by plugin_start3 below.
|
||||
|
||||
It is the first point EDMC interacts with our code after loading our module.
|
||||
|
||||
:return: The name of the plugin, which will be used by EDMC for logging and for the settings window
|
||||
"""
|
||||
return PLUGIN_NAME
|
||||
|
||||
def on_unload(self) -> None:
|
||||
"""
|
||||
on_unload is called by plugin_stop below.
|
||||
|
||||
It is the last thing called before EDMC shuts down. Note that blocking code here will hold the shutdown process.
|
||||
"""
|
||||
self.on_preferences_closed("", False) # Save our prefs
|
||||
|
||||
def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]:
|
||||
"""
|
||||
setup_preferences is called by plugin_prefs below.
|
||||
|
||||
It is where we can setup our own settings page in EDMC's settings window. Our tab is defined for us.
|
||||
|
||||
:param parent: the tkinter parent that our returned Frame will want to inherit from
|
||||
:param cmdr: The current ED Commander
|
||||
:param is_beta: Whether or not EDMC is currently marked as in beta mode
|
||||
:return: The frame to add to the settings window
|
||||
"""
|
||||
current_row = 0
|
||||
frame = nb.Frame(parent)
|
||||
|
||||
# setup our config in a "Click Count: number"
|
||||
nb.Label(frame, text='Click Count').grid(row=current_row)
|
||||
nb.Entry(frame, textvariable=self.click_count).grid(row=current_row, column=1)
|
||||
current_row += 1 # Always increment our row counter, makes for far easier tkinter design.
|
||||
return frame
|
||||
|
||||
def on_preferences_closed(self, cmdr: str, is_beta: bool) -> None:
|
||||
"""
|
||||
on_preferences_closed is called by prefs_changed below.
|
||||
|
||||
It is called when the preferences dialog is dismissed by the user.
|
||||
|
||||
:param cmdr: The current ED Commander
|
||||
:param is_beta: Whether or not EDMC is currently marked as in beta mode
|
||||
"""
|
||||
config.set('click_counter_count', self.click_count.get())
|
||||
|
||||
def setup_main_ui(self, parent: tk.Frame) -> tk.Frame:
|
||||
"""
|
||||
Create our entry on the main EDMC UI.
|
||||
|
||||
This is called by plugin_app below.
|
||||
|
||||
:param parent: EDMC main window Tk
|
||||
:return: Our frame
|
||||
"""
|
||||
current_row = 0
|
||||
frame = tk.Frame(parent)
|
||||
button = tk.Button(
|
||||
frame,
|
||||
text="Count me",
|
||||
command=lambda: self.click_count.set(str(int(self.click_count.get()) + 1))
|
||||
)
|
||||
button.grid(row=current_row)
|
||||
current_row += 1
|
||||
nb.Label(frame, text="Count:").grid(row=current_row, sticky=tk.W)
|
||||
nb.Label(frame, textvariable=self.click_count).grid(row=current_row, column=1)
|
||||
return frame
|
||||
|
||||
|
||||
cc = ClickCounter()
|
||||
|
||||
|
||||
# Note that all of these could be simply replaced with something like:
|
||||
# plugin_start3 = cc.on_load
|
||||
def plugin_start3(plugin_dir: str) -> str:
|
||||
return cc.on_load()
|
||||
|
||||
|
||||
def plugin_stop() -> None:
|
||||
return cc.on_unload()
|
||||
|
||||
|
||||
def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]:
|
||||
return cc.setup_preferences(parent, cmdr, is_beta)
|
||||
|
||||
|
||||
def prefs_changed(cmdr: str, is_beta: bool) -> None:
|
||||
return cc.on_preferences_closed(cmdr, is_beta)
|
||||
|
||||
|
||||
def plugin_app(parent: tk.Frame) -> Optional[tk.Frame]:
|
||||
return cc.setup_main_ui(parent)
|
18
docs/examples/plugintest/SubA/__init__.py
Normal file
18
docs/examples/plugintest/SubA/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""Test class logging."""
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class SubA:
|
||||
"""Simple class to test logging."""
|
||||
|
||||
def __init__(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
|
||||
def ping(self) -> None:
|
||||
"""
|
||||
Log a ping to demonstrate correct logging.
|
||||
|
||||
:return:
|
||||
"""
|
||||
self.logger.info('ping!')
|
121
docs/examples/plugintest/load.py
Normal file
121
docs/examples/plugintest/load.py
Normal file
@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: textwidth=0 wrapmargin=0 tabstop=4 shiftwidth=4 softtabstop=4 smartindent smarttab
|
||||
"""Plugin that tests that modules we bundle for plugins are present and working."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
from SubA import SubA
|
||||
|
||||
from config import appname
|
||||
|
||||
# This could also be returned from plugin_start3()
|
||||
plugin_name = os.path.basename(os.path.dirname(__file__))
|
||||
|
||||
# Logger per found plugin, so the folder name is included in
|
||||
# the logging format.
|
||||
logger = logging.getLogger(f'{appname}.{plugin_name}')
|
||||
if not logger.hasHandlers():
|
||||
level = logging.INFO # So logger.info(...) is equivalent to print()
|
||||
|
||||
logger.setLevel(level)
|
||||
logger_channel = logging.StreamHandler()
|
||||
logger_channel.setLevel(level)
|
||||
logger_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d:%(funcName)s: %(message)s') # noqa: E501
|
||||
logger_formatter.default_time_format = '%Y-%m-%d %H:%M:%S'
|
||||
logger_formatter.default_msec_format = '%s.%03d'
|
||||
logger_channel.setFormatter(logger_formatter)
|
||||
logger.addHandler(logger_channel)
|
||||
|
||||
|
||||
this = sys.modules[__name__] # For holding module globals
|
||||
this.DBFILE = 'plugintest.db'
|
||||
this.mt = None
|
||||
|
||||
|
||||
class PluginTest(object):
|
||||
"""Class that performs actual tests on bundled modules."""
|
||||
|
||||
def __init__(self, directory: str):
|
||||
logger.debug(f'directory = "{directory}')
|
||||
dbfile = os.path.join(directory, this.DBFILE)
|
||||
|
||||
# Test 'import zipfile'
|
||||
with zipfile.ZipFile(dbfile + '.zip', 'w') as zip:
|
||||
if os.path.exists(dbfile):
|
||||
zip.write(dbfile)
|
||||
zip.close()
|
||||
|
||||
# Testing 'import shutil'
|
||||
if os.path.exists(dbfile):
|
||||
shutil.copyfile(dbfile, dbfile + '.bak')
|
||||
|
||||
# Testing 'import sqlite3'
|
||||
self.sqlconn = sqlite3.connect(dbfile)
|
||||
self.sqlc = self.sqlconn.cursor()
|
||||
try:
|
||||
self.sqlc.execute('CREATE TABLE entries (timestamp TEXT, cmdrname TEXT, system TEXT, station TEXT, eventtype TEXT)') # noqa: E501
|
||||
except sqlite3.OperationalError:
|
||||
logger.exception('sqlite3.OperationalError when CREATE TABLE entries:')
|
||||
|
||||
def store(self, timestamp: str, cmdrname: str, system: str, station: str, event: str) -> None:
|
||||
"""
|
||||
Store the provided data in sqlite database.
|
||||
|
||||
:param timestamp:
|
||||
:param cmdrname:
|
||||
:param system:
|
||||
:param station:
|
||||
:param event:
|
||||
:return: None
|
||||
"""
|
||||
logger.debug(f'timestamp = "{timestamp}", cmdr = "{cmdrname}", system = "{system}", station = "{station}", event = "{event}"') # noqa: E501
|
||||
self.sqlc.execute('INSERT INTO entries VALUES(?, ?, ?, ?, ?)', (timestamp, cmdrname, system, station, event))
|
||||
self.sqlconn.commit()
|
||||
return None
|
||||
|
||||
|
||||
def plugin_start3(plugin_dir: str) -> str:
|
||||
"""
|
||||
Plugin startup method.
|
||||
|
||||
:param plugin_dir:
|
||||
:return: 'Pretty' name of this plugin.
|
||||
"""
|
||||
logger.info(f'Folder is {plugin_dir}')
|
||||
this.mt = PluginTest(plugin_dir)
|
||||
|
||||
this.suba = SubA(logger)
|
||||
|
||||
this.suba.ping()
|
||||
|
||||
return plugin_name
|
||||
|
||||
|
||||
def plugin_stop() -> None:
|
||||
"""
|
||||
Plugin stop method.
|
||||
|
||||
:return:
|
||||
"""
|
||||
logger.info('Stopping')
|
||||
|
||||
|
||||
def journal_entry(cmdrname: str, is_beta: bool, system: str, station: str, entry: dict, state: dict) -> None:
|
||||
"""
|
||||
Handle the given journal entry.
|
||||
|
||||
:param cmdrname:
|
||||
:param is_beta:
|
||||
:param system:
|
||||
:param station:
|
||||
:param entry:
|
||||
:param state:
|
||||
:return: None
|
||||
"""
|
||||
logger.debug(f'cmdr = "{cmdrname}", is_beta = "{is_beta}", system = "{system}", station = "{station}"')
|
||||
this.mt.store(entry['timestamp'], cmdrname, system, station, entry['event'])
|
8
plug.py
8
plug.py
@ -9,15 +9,15 @@ import sys
|
||||
import operator
|
||||
import threading # noqa: F401 - We don't use it, but plugins might
|
||||
from typing import Optional
|
||||
import logging
|
||||
import tkinter as tk
|
||||
|
||||
import myNotebook as nb # noqa: N813
|
||||
|
||||
from config import config, appname
|
||||
from config import appcmdname, appname, config
|
||||
from EDMCLogging import get_main_logger
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(appname)
|
||||
logger = get_main_logger()
|
||||
|
||||
# Dashboard Flags constants
|
||||
FlagsDocked = 1 << 0 # on a landing pad
|
||||
@ -203,8 +203,8 @@ def load_plugins(master):
|
||||
# Create a logger for this 'found' plugin. Must be before the
|
||||
# load.py is loaded.
|
||||
import EDMCLogging
|
||||
plugin_logger = EDMCLogging.get_plugin_logger(f'{appname}.{name}')
|
||||
|
||||
plugin_logger = EDMCLogging.get_plugin_logger(name)
|
||||
found.append(Plugin(name, os.path.join(config.plugin_dir, name, 'load.py'), plugin_logger))
|
||||
except Exception as e:
|
||||
logger.exception(f'Failure loading found Plugin "{name}"')
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
@ -11,15 +10,16 @@ from collections import OrderedDict
|
||||
from os import SEEK_SET
|
||||
from os.path import join
|
||||
from platform import system
|
||||
from typing import TYPE_CHECKING, Any, AnyStr, Dict, Iterator, List, Mapping, MutableMapping, Optional
|
||||
from typing import TYPE_CHECKING, Any, AnyStr, Dict, Iterator, List, Mapping, MutableMapping, Optional, Tuple
|
||||
from typing import OrderedDict as OrderedDictT
|
||||
from typing import Sequence, TextIO, Tuple
|
||||
from typing import Sequence, TextIO
|
||||
|
||||
import requests
|
||||
|
||||
import myNotebook as nb # noqa: N813
|
||||
from companion import category_map
|
||||
from config import applongname, appname, appversion, config
|
||||
from config import applongname, appversion, config
|
||||
from EDMCLogging import get_main_logger
|
||||
from myNotebook import Frame
|
||||
from prefs import prefsVersion
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
@ -32,7 +32,7 @@ if TYPE_CHECKING:
|
||||
def _(x: str) -> str:
|
||||
return x
|
||||
|
||||
logger = logging.getLogger(appname)
|
||||
logger = get_main_logger()
|
||||
|
||||
this: Any = sys.modules[__name__] # For holding module globals
|
||||
|
||||
@ -262,6 +262,32 @@ Msg:\n{msg}''')
|
||||
|
||||
this.commodities = commodities
|
||||
|
||||
def safe_modules_and_ships(self, data: Mapping[str, Any]) -> Tuple[Dict, Dict]:
|
||||
modules: Dict[str, Any] = data['lastStarport'].get('modules')
|
||||
if modules is None or not isinstance(modules, dict):
|
||||
if modules is None:
|
||||
logger.debug('modules was None. FC or Damaged Station?')
|
||||
elif isinstance(modules, list):
|
||||
if len(modules) == 0:
|
||||
logger.debug('modules is empty list. Damaged Station?')
|
||||
else:
|
||||
logger.error(f'modules is non-empty list: {modules!r}')
|
||||
else:
|
||||
logger.error(f'modules was not None, a list, or a dict! type = {type(modules)}')
|
||||
# Set a safe value
|
||||
modules = {}
|
||||
|
||||
ships: Dict[str, Any] = data['lastStarport'].get('ships')
|
||||
if ships is None or not isinstance(ships, dict):
|
||||
if ships is None:
|
||||
logger.debug('ships was None')
|
||||
else:
|
||||
logger.error(f'ships was neither None nor a Dict! Type = {type(ships)}')
|
||||
# Set a safe value
|
||||
ships = {'shipyard_list': {}, 'unavailable_list': []}
|
||||
|
||||
return modules, ships
|
||||
|
||||
def export_outfitting(self, data: Mapping[str, Any], is_beta: bool) -> None:
|
||||
"""
|
||||
export_outfitting updates EDDN with the current (lastStarport) station's outfitting options, if any.
|
||||
@ -270,15 +296,7 @@ Msg:\n{msg}''')
|
||||
:param data: dict containing the outfitting data
|
||||
:param is_beta: whether or not we're currently in beta mode
|
||||
"""
|
||||
modules: Dict[str, Any] = data['lastStarport'].get('modules')
|
||||
if modules is None:
|
||||
logger.debug('modules was None')
|
||||
modules = {}
|
||||
|
||||
ships: Dict[str, Any] = data['lastStarport'].get('ships')
|
||||
if ships is None:
|
||||
logger.debug('ships was None')
|
||||
ships = {'shipyard_list': {}, 'unavailable_list': []}
|
||||
modules, ships = self.safe_modules_and_ships(data)
|
||||
|
||||
# Horizons flag - will hit at least Int_PlanetApproachSuite other than at engineer bases ("Colony"),
|
||||
# prison or rescue Megaships, or under Pirate Attack etc
|
||||
@ -321,15 +339,7 @@ Msg:\n{msg}''')
|
||||
:param data: dict containing the shipyard data
|
||||
:param is_beta: whether or not we are in beta mode
|
||||
"""
|
||||
modules: Dict[str, Any] = data['lastStarport'].get('modules')
|
||||
if modules is None:
|
||||
logger.debug('modules was None')
|
||||
modules = {}
|
||||
|
||||
ships: Dict[str, Any] = data['lastStarport'].get('ships')
|
||||
if ships is None:
|
||||
logger.debug('ships was None')
|
||||
ships = {'shipyard_list': {}, 'unavailable_list': []}
|
||||
modules, ships = self.safe_modules_and_ships(data)
|
||||
|
||||
horizons: bool = is_horizons(
|
||||
data['lastStarport'].get('economies', {}),
|
||||
|
432
plugins/edsm.py
432
plugins/edsm.py
@ -1,6 +1,4 @@
|
||||
#
|
||||
# System display and EDSM lookup
|
||||
#
|
||||
"""System display and EDSM lookup."""
|
||||
|
||||
# TODO:
|
||||
# 1) Re-factor EDSM API calls out of journal_entry() into own function.
|
||||
@ -12,51 +10,80 @@
|
||||
# text is always fired. i.e. CAPI cmdr_data() processing.
|
||||
|
||||
import json
|
||||
import requests
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, List, Mapping, MutableMapping, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
import tkinter as tk
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
import myNotebook as nb # noqa: N813
|
||||
|
||||
from config import appname, applongname, appversion, config
|
||||
import plug
|
||||
from config import applongname, appversion, config
|
||||
from EDMCLogging import get_main_logger
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
|
||||
logger = logging.getLogger(appname)
|
||||
if TYPE_CHECKING:
|
||||
def _(x: str) -> str:
|
||||
return x
|
||||
|
||||
logger = get_main_logger()
|
||||
|
||||
EDSM_POLL = 0.1
|
||||
_TIMEOUT = 20
|
||||
|
||||
|
||||
this = sys.modules[__name__] # For holding module globals
|
||||
this.session = requests.Session()
|
||||
this.queue = Queue() # Items to be sent to EDSM by worker thread
|
||||
this.discardedEvents = [] # List discarded events from EDSM
|
||||
this.lastlookup = False # whether the last lookup succeeded
|
||||
this: Any = sys.modules[__name__] # For holding module globals
|
||||
this.session: requests.Session = requests.Session()
|
||||
this.queue: Queue = Queue() # Items to be sent to EDSM by worker thread
|
||||
this.discardedEvents: List[str] = [] # List discarded events from EDSM
|
||||
this.lastlookup: bool = False # whether the last lookup succeeded
|
||||
|
||||
# Game state
|
||||
this.multicrew = False # don't send captain's ship info to EDSM while on a crew
|
||||
this.coordinates = None
|
||||
this.newgame = False # starting up - batch initial burst of events
|
||||
this.newgame_docked = False # starting up while docked
|
||||
this.navbeaconscan = 0 # batch up burst of Scan events after NavBeaconScan
|
||||
this.system_link = None
|
||||
this.system = None
|
||||
this.system_address = None # Frontier SystemAddress
|
||||
this.system_population = None
|
||||
this.station_link = None
|
||||
this.station = None
|
||||
this.station_marketid = None # Frontier MarketID
|
||||
this.multicrew: bool = False # don't send captain's ship info to EDSM while on a crew
|
||||
this.coordinates: Optional[Tuple[int, int, int]] = None
|
||||
this.newgame: bool = False # starting up - batch initial burst of events
|
||||
this.newgame_docked: bool = False # starting up while docked
|
||||
this.navbeaconscan: int = 0 # batch up burst of Scan events after NavBeaconScan
|
||||
this.system_link: tk.Tk = None
|
||||
this.system: tk.Tk = None
|
||||
this.system_address: Optional[int] = None # Frontier SystemAddress
|
||||
this.system_population: Optional[int] = None
|
||||
this.station_link: tk.Tk = None
|
||||
this.station: Optional[str] = None
|
||||
this.station_marketid: Optional[int] = None # Frontier MarketID
|
||||
STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00D7
|
||||
__cleanup = str.maketrans({' ': None, '\n': None})
|
||||
IMG_KNOWN_B64 = """
|
||||
R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7r
|
||||
MpvNV4q932VSuRiPjQQAOw==
|
||||
""".translate(__cleanup)
|
||||
|
||||
IMG_UNKNOWN_B64 = """
|
||||
R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kC
|
||||
ADs=
|
||||
""".translate(__cleanup)
|
||||
|
||||
IMG_NEW_B64 = """
|
||||
R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXO
|
||||
PvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//j
|
||||
d//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/s
|
||||
lv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP//////////////
|
||||
/////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xH
|
||||
Jk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+G
|
||||
BAcUCIIBBDSYYGiAAUMALFR6FAgAOw==
|
||||
""".translate(__cleanup)
|
||||
|
||||
IMG_ERR_B64 = """
|
||||
R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2L
|
||||
plEAADs=
|
||||
""".translate(__cleanup)
|
||||
|
||||
|
||||
# Main window clicks
|
||||
def system_url(system_name):
|
||||
def system_url(system_name: str) -> str:
|
||||
"""Get a URL for the current system."""
|
||||
if this.system_address:
|
||||
return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemID64={this.system_address}')
|
||||
|
||||
@ -65,140 +92,224 @@ def system_url(system_name):
|
||||
|
||||
return ''
|
||||
|
||||
def station_url(system_name, station_name):
|
||||
|
||||
def station_url(system_name: str, station_name: str) -> str:
|
||||
"""Get a URL for the current station."""
|
||||
if system_name and station_name:
|
||||
return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemName={system_name}&stationName={station_name}')
|
||||
return requests.utils.requote_uri(
|
||||
f'https://www.edsm.net/en/system?systemName={system_name}&stationName={station_name}'
|
||||
)
|
||||
|
||||
# monitor state might think these are gone, but we don't yet
|
||||
if this.system and this.station:
|
||||
return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemName={this.system}&stationName={this.station}')
|
||||
return requests.utils.requote_uri(
|
||||
f'https://www.edsm.net/en/system?systemName={this.system}&stationName={this.station}'
|
||||
)
|
||||
|
||||
if system_name:
|
||||
return requests.utils.requote_uri(f'https://www.edsm.net/en/system?systemName={system_name}&stationName=ALL')
|
||||
return requests.utils.requote_uri(
|
||||
f'https://www.edsm.net/en/system?systemName={system_name}&stationName=ALL'
|
||||
)
|
||||
|
||||
return ''
|
||||
|
||||
def plugin_start3(plugin_dir):
|
||||
|
||||
def plugin_start3(plugin_dir: str) -> str:
|
||||
"""Plugin setup hook."""
|
||||
# Can't be earlier since can only call PhotoImage after window is created
|
||||
this._IMG_KNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAMIEAFWjVVWkVWS/ZGfFZ////////////////yH5BAEKAAQALAAAAAAQABAAAAMvSLrc/lAFIUIkYOgNXt5g14Dk0AQlaC1CuglM6w7wgs7rMpvNV4q932VSuRiPjQQAOw==') # green circle
|
||||
this._IMG_UNKNOWN = tk.PhotoImage(data = 'R0lGODlhEAAQAKEDAGVLJ+ddWO5fW////yH5BAEKAAMALAAAAAAQABAAAAItnI+pywYRQBtA2CtVvTwjDgrJFlreEJRXgKSqwB5keQ6vOKq1E+7IE5kIh4kCADs=') # red circle
|
||||
this._IMG_NEW = tk.PhotoImage(data = 'R0lGODlhEAAQAMZwANKVHtWcIteiHuiqLPCuHOS1MN22ZeW7ROG6Zuu9MOy+K/i8Kf/DAuvCVf/FAP3BNf/JCf/KAPHHSv7ESObHdv/MBv/GRv/LGP/QBPXOPvjPQfjQSvbRSP/UGPLSae7Sfv/YNvLXgPbZhP7dU//iI//mAP/jH//kFv7fU//fV//ebv/iTf/iUv/kTf/iZ/vgiP/hc/vgjv/jbfriiPriiv7ka//if//jd//sJP/oT//tHv/mZv/sLf/rRP/oYv/rUv/paP/mhv/sS//oc//lkf/mif/sUf/uPv/qcv/uTv/uUv/vUP/qhP/xP//pm//ua//sf//ubf/wXv/thv/tif/slv/tjf/smf/yYP/ulf/2R//2Sv/xkP/2av/0gP/ylf/2df/0i//0j//0lP/5cP/7a//1p//5gf/7ev/3o//2sf/5mP/6kv/2vP/3y//+jP///////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAQABAAAAePgH+Cg4SFhoJKPIeHYT+LhVppUTiPg2hrUkKPXWdlb2xHJk9jXoNJQDk9TVtkYCUkOy4wNjdGfy1UXGJYOksnPiwgFwwYg0NubWpmX1ArHREOFYUyWVNIVkxXQSoQhyMoNVUpRU5EixkcMzQaGy8xhwsKHiEfBQkSIg+GBAcUCIIBBDSYYGiAAUMALFR6FAgAOw==')
|
||||
this._IMG_ERROR = tk.PhotoImage(data = 'R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAIwlBWpeR0AIwwNPRmZuVNJinyWuClhBlZjpm5fqnIAHJPtOd3Hou9mL6NVgj2LplEAADs=') # BBC Mode 5 '?'
|
||||
this._IMG_KNOWN = tk.PhotoImage(data=IMG_KNOWN_B64) # green circle
|
||||
this._IMG_UNKNOWN = tk.PhotoImage(data=IMG_UNKNOWN_B64) # red circle
|
||||
this._IMG_NEW = tk.PhotoImage(data=IMG_NEW_B64)
|
||||
this._IMG_ERROR = tk.PhotoImage(data=IMG_ERR_B64) # BBC Mode 5 '?'
|
||||
|
||||
# Migrate old settings
|
||||
if not config.get('edsm_cmdrs'):
|
||||
if isinstance(config.get('cmdrs'), list) and config.get('edsm_usernames') and config.get('edsm_apikeys'):
|
||||
# Migrate <= 2.34 settings
|
||||
config.set('edsm_cmdrs', config.get('cmdrs'))
|
||||
|
||||
elif config.get('edsm_cmdrname'):
|
||||
# Migrate <= 2.25 settings. edsm_cmdrs is unknown at this time
|
||||
config.set('edsm_usernames', [config.get('edsm_cmdrname') or ''])
|
||||
config.set('edsm_apikeys', [config.get('edsm_apikey') or ''])
|
||||
|
||||
config.delete('edsm_cmdrname')
|
||||
config.delete('edsm_apikey')
|
||||
|
||||
if config.getint('output') & 256:
|
||||
# Migrate <= 2.34 setting
|
||||
config.set('edsm_out', 1)
|
||||
|
||||
config.delete('edsm_autoopen')
|
||||
config.delete('edsm_historical')
|
||||
|
||||
this.thread = Thread(target = worker, name = 'EDSM worker')
|
||||
this.thread = Thread(target=worker, name='EDSM worker')
|
||||
this.thread.daemon = True
|
||||
this.thread.start()
|
||||
|
||||
return 'EDSM'
|
||||
|
||||
def plugin_app(parent):
|
||||
|
||||
def plugin_app(parent: tk.Tk) -> None:
|
||||
"""Plugin UI setup."""
|
||||
this.system_link = parent.children['system'] # system label in main window
|
||||
this.system_link.bind_all('<<EDSMStatus>>', update_status)
|
||||
this.station_link = parent.children['station'] # station label in main window
|
||||
|
||||
def plugin_stop():
|
||||
|
||||
def plugin_stop() -> None:
|
||||
"""Plugin exit hook."""
|
||||
# Signal thread to close and wait for it
|
||||
this.queue.put(None)
|
||||
this.thread.join()
|
||||
this.thread = None
|
||||
# Suppress 'Exception ignored in: <function Image.__del__ at ...>' errors
|
||||
# Suppress 'Exception ignored in: <function Image.__del__ at ...>' errors # TODO: this is bad.
|
||||
this._IMG_KNOWN = this._IMG_UNKNOWN = this._IMG_NEW = this._IMG_ERROR = None
|
||||
|
||||
def plugin_prefs(parent, cmdr, is_beta):
|
||||
|
||||
PADX = 10
|
||||
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
||||
PADY = 2 # close spacing
|
||||
def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame:
|
||||
"""Plugin preferences setup hook."""
|
||||
PADX = 10 # noqa: N806
|
||||
BUTTONX = 12 # indent Checkbuttons and Radiobuttons # noqa: N806
|
||||
PADY = 2 # close spacing # noqa: N806
|
||||
|
||||
frame = nb.Frame(parent)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
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 and Cmdr status to EDSM'), variable=this.log, command=prefsvarchanged)
|
||||
this.log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
|
||||
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 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
|
||||
# Section heading in settings
|
||||
this.label = HyperlinkLabel(
|
||||
frame,
|
||||
text=_('Elite Dangerous Star Map credentials'),
|
||||
background=nb.Label().cget('background'),
|
||||
url='https://www.edsm.net/settings/api',
|
||||
underline=True
|
||||
)
|
||||
|
||||
cur_row = 10
|
||||
|
||||
nb.Label(frame).grid(sticky=tk.W) # big spacer
|
||||
this.label = HyperlinkLabel(frame, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings
|
||||
this.label.grid(columnspan=2, padx=PADX, sticky=tk.W)
|
||||
|
||||
this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window
|
||||
this.cmdr_label.grid(row=10, padx=PADX, sticky=tk.W)
|
||||
this.cmdr_label = nb.Label(frame, text=_('Cmdr')) # Main window
|
||||
this.cmdr_label.grid(row=cur_row, padx=PADX, sticky=tk.W)
|
||||
this.cmdr_text = nb.Label(frame)
|
||||
this.cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W)
|
||||
this.cmdr_text.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.W)
|
||||
|
||||
this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting
|
||||
this.user_label.grid(row=11, padx=PADX, sticky=tk.W)
|
||||
cur_row += 1
|
||||
|
||||
this.user_label = nb.Label(frame, text=_('Commander Name')) # EDSM setting
|
||||
this.user_label.grid(row=cur_row, padx=PADX, sticky=tk.W)
|
||||
this.user = nb.Entry(frame)
|
||||
this.user.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
|
||||
this.user.grid(row=cur_row, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
|
||||
|
||||
this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting
|
||||
this.apikey_label.grid(row=12, padx=PADX, sticky=tk.W)
|
||||
cur_row += 1
|
||||
|
||||
this.apikey_label = nb.Label(frame, text=_('API Key')) # EDSM setting
|
||||
this.apikey_label.grid(row=cur_row, padx=PADX, sticky=tk.W)
|
||||
this.apikey = nb.Entry(frame)
|
||||
this.apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
|
||||
this.apikey.grid(row=cur_row, 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
|
||||
|
||||
def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None:
|
||||
"""Commanders changed hook."""
|
||||
this.log_button['state'] = tk.NORMAL if cmdr and not is_beta else tk.DISABLED
|
||||
this.user['state'] = tk.NORMAL
|
||||
this.user.delete(0, tk.END)
|
||||
this.apikey['state'] = tk.NORMAL
|
||||
this.apikey.delete(0, tk.END)
|
||||
if cmdr:
|
||||
this.cmdr_text['text'] = cmdr + (is_beta and ' [Beta]' or '')
|
||||
this.cmdr_text['text'] = f'{cmdr}{" [Beta]" if is_beta else ""}'
|
||||
cred = credentials(cmdr)
|
||||
|
||||
if cred:
|
||||
this.user.insert(0, cred[0])
|
||||
this.apikey.insert(0, cred[1])
|
||||
|
||||
else:
|
||||
this.cmdr_text['text'] = _('None') # No hotkey/shortcut currently defined
|
||||
this.label['state'] = this.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['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.cmdr_label['state'] = this.cmdr_text['state'] = this.user_label['state'] = this.user['state'] = this.apikey_label['state'] = this.apikey['state'] = this.log.get() and this.log_button['state'] or tk.DISABLED
|
||||
to_set = tk.DISABLED
|
||||
if cmdr and not is_beta and this.log.get():
|
||||
to_set = tk.NORMAL
|
||||
|
||||
def prefs_changed(cmdr, is_beta):
|
||||
set_prefs_ui_states(to_set)
|
||||
|
||||
|
||||
def prefsvarchanged() -> None:
|
||||
"""Preferences screen closed hook."""
|
||||
to_set = tk.DISABLED
|
||||
if this.log.get():
|
||||
to_set = this.log_button['state']
|
||||
|
||||
set_prefs_ui_states(to_set)
|
||||
|
||||
|
||||
def set_prefs_ui_states(state: str) -> None:
|
||||
"""
|
||||
Set the state of various config UI entries.
|
||||
|
||||
:param state: the state to set each entry to
|
||||
"""
|
||||
this.label['state'] = state
|
||||
this.cmdr_label['state'] = state
|
||||
this.cmdr_text['state'] = state
|
||||
this.user_label['state'] = state
|
||||
this.user['state'] = state
|
||||
this.apikey_label['state'] = state
|
||||
this.apikey['state'] = state
|
||||
|
||||
|
||||
def prefs_changed(cmdr: str, is_beta: bool) -> None:
|
||||
"""Preferences changed hook."""
|
||||
config.set('edsm_out', this.log.get())
|
||||
|
||||
if cmdr and not is_beta:
|
||||
cmdrs = config.get('edsm_cmdrs')
|
||||
usernames = config.get('edsm_usernames') or []
|
||||
apikeys = config.get('edsm_apikeys') or []
|
||||
# TODO: remove this when config is rewritten.
|
||||
cmdrs: List[str] = list(config.get('edsm_cmdrs') or [])
|
||||
usernames: List[str] = list(config.get('edsm_usernames') or [])
|
||||
apikeys: List[str] = list(config.get('edsm_apikeys') or [])
|
||||
if cmdr in cmdrs:
|
||||
idx = cmdrs.index(cmdr)
|
||||
usernames.extend([''] * (1 + idx - len(usernames)))
|
||||
usernames[idx] = this.user.get().strip()
|
||||
apikeys.extend([''] * (1 + idx - len(apikeys)))
|
||||
apikeys[idx] = this.apikey.get().strip()
|
||||
|
||||
else:
|
||||
config.set('edsm_cmdrs', cmdrs + [cmdr])
|
||||
usernames.append(this.user.get().strip())
|
||||
apikeys.append(this.apikey.get().strip())
|
||||
|
||||
config.set('edsm_usernames', usernames)
|
||||
config.set('edsm_apikeys', apikeys)
|
||||
|
||||
|
||||
def credentials(cmdr):
|
||||
def credentials(cmdr: str) -> Optional[Tuple[str, str]]:
|
||||
"""
|
||||
Get credentials for the given commander, if they exist.
|
||||
|
||||
:param cmdr: The commander to get credentials for
|
||||
:return: The credentials, or None
|
||||
"""
|
||||
# Credentials for cmdr
|
||||
if not cmdr:
|
||||
return None
|
||||
@ -212,16 +323,28 @@ def credentials(cmdr):
|
||||
if cmdr in cmdrs and config.get('edsm_usernames') and config.get('edsm_apikeys'):
|
||||
idx = cmdrs.index(cmdr)
|
||||
return (config.get('edsm_usernames')[idx], config.get('edsm_apikeys')[idx])
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||
def journal_entry(
|
||||
cmdr: str, is_beta: bool, system: str, station: str, entry: MutableMapping[str, Any], state: Mapping[str, Any]
|
||||
) -> None:
|
||||
"""Journal Entry hook."""
|
||||
if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'):
|
||||
logger.debug(f'''{entry["event"]}
|
||||
Commander: {cmdr}
|
||||
System: {system}
|
||||
Station: {station}
|
||||
state: {state!r}
|
||||
entry: {entry!r}'''
|
||||
)
|
||||
# Always update our system address even if we're not currently the provider for system or station, but dont update
|
||||
# on events that contain "future" data, such as FSDTarget
|
||||
if entry['event'] in ('Location', 'Docked', 'CarrierJump', 'FSDJump'):
|
||||
this.system_address = entry.get('SystemAddress') or this.system_address
|
||||
this.system = entry.get('StarSystem') or this.system
|
||||
this.system_address = entry.get('SystemAddress', this.system_address)
|
||||
this.system = entry.get('StarSystem', this.system)
|
||||
|
||||
# We need pop == 0 to set the value so as to clear 'x' in systems with
|
||||
# no stations.
|
||||
@ -229,47 +352,62 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||
if pop is not None:
|
||||
this.system_population = pop
|
||||
|
||||
this.station = entry.get('StationName') or this.station
|
||||
this.station_marketid = entry.get('MarketID') or this.station_marketid
|
||||
this.station = entry.get('StationName', this.station)
|
||||
this.station_marketid = entry.get('MarketID', this.station)
|
||||
# We might pick up StationName in DockingRequested, make sure we clear it if leaving
|
||||
if entry['event'] in ('Undocked', 'FSDJump', 'SupercruiseEntry'):
|
||||
this.station = None
|
||||
this.station_marketid = None
|
||||
|
||||
if config.get('station_provider') == 'EDSM':
|
||||
this.station_link['text'] = this.station or (this.system_population and this.system_population > 0 and STATION_UNDOCKED or '')
|
||||
this.station_link['url'] = station_url(this.system, this.station)
|
||||
to_set = this.station
|
||||
if not this.station:
|
||||
if this.system_population and this.system_population > 0:
|
||||
to_set = STATION_UNDOCKED
|
||||
|
||||
else:
|
||||
to_set = ''
|
||||
|
||||
this.station_link['text'] = to_set
|
||||
this.station_link['url'] = station_url(this.system, str(this.station))
|
||||
this.station_link.update_idletasks()
|
||||
|
||||
# Update display of 'EDSM Status' image
|
||||
if this.system_link['text'] != system:
|
||||
this.system_link['text'] = system or ''
|
||||
this.system_link['text'] = system if system else ''
|
||||
this.system_link['image'] = ''
|
||||
this.system_link.update_idletasks()
|
||||
|
||||
this.multicrew = bool(state['Role'])
|
||||
if 'StarPos' in entry:
|
||||
this.coordinates = entry['StarPos']
|
||||
|
||||
elif entry['event'] == 'LoadGame':
|
||||
this.coordinates = None
|
||||
|
||||
if entry['event'] in ['LoadGame', 'Commander', 'NewCommander']:
|
||||
if entry['event'] in ('LoadGame', 'Commander', 'NewCommander'):
|
||||
this.newgame = True
|
||||
this.newgame_docked = False
|
||||
this.navbeaconscan = 0
|
||||
|
||||
elif entry['event'] == 'StartUp':
|
||||
this.newgame = False
|
||||
this.newgame_docked = False
|
||||
this.navbeaconscan = 0
|
||||
|
||||
elif entry['event'] == 'Location':
|
||||
this.newgame = True
|
||||
this.newgame_docked = entry.get('Docked', False)
|
||||
this.navbeaconscan = 0
|
||||
|
||||
elif entry['event'] == 'NavBeaconScan':
|
||||
this.navbeaconscan = entry['NumBodies']
|
||||
|
||||
# Send interesting events to EDSM
|
||||
if config.getint('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr) and entry['event'] not in this.discardedEvents:
|
||||
if (
|
||||
config.getint('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr) and
|
||||
entry['event'] not in this.discardedEvents
|
||||
):
|
||||
# Introduce transient states into the event
|
||||
transient = {
|
||||
'_systemName': system,
|
||||
@ -277,6 +415,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||
'_stationName': station,
|
||||
'_shipId': state['ShipID'],
|
||||
}
|
||||
|
||||
entry.update(transient)
|
||||
|
||||
if entry['event'] == 'LoadGame':
|
||||
@ -284,26 +423,36 @@ def journal_entry(cmdr, is_beta, system, station, entry, state):
|
||||
materials = {
|
||||
'timestamp': entry['timestamp'],
|
||||
'event': 'Materials',
|
||||
'Raw': [ { 'Name': k, 'Count': v } for k,v in state['Raw'].items() ],
|
||||
'Manufactured': [ { 'Name': k, 'Count': v } for k,v in state['Manufactured'].items() ],
|
||||
'Encoded': [ { 'Name': k, 'Count': v } for k,v in state['Encoded'].items() ],
|
||||
'Raw': [{'Name': k, 'Count': v} for k, v in state['Raw'].items()],
|
||||
'Manufactured': [{'Name': k, 'Count': v} for k, v in state['Manufactured'].items()],
|
||||
'Encoded': [{'Name': k, 'Count': v} for k, v in state['Encoded'].items()],
|
||||
}
|
||||
materials.update(transient)
|
||||
this.queue.put((cmdr, materials))
|
||||
|
||||
if entry['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked'):
|
||||
logger.debug(f'''{entry["event"]}
|
||||
Queueing: {entry!r}'''
|
||||
)
|
||||
this.queue.put((cmdr, entry))
|
||||
|
||||
|
||||
# Update system data
|
||||
def cmdr_data(data, is_beta):
|
||||
def cmdr_data(data: Mapping[str, Any], is_beta: bool) -> None:
|
||||
"""CAPI Entry Hook."""
|
||||
system = data['lastSystem']['name']
|
||||
|
||||
# Always store initially, even if we're not the *current* system provider.
|
||||
if not this.station_marketid:
|
||||
this.station_marketid = data['commander']['docked'] and data['lastStarport']['id']
|
||||
if not this.station_marketid and data['commander']['docked']:
|
||||
this.station_marketid = data['lastStarport']['id']
|
||||
|
||||
# Only trust CAPI if these aren't yet set
|
||||
this.system = this.system or data['lastSystem']['name']
|
||||
this.station = this.station or data['commander']['docked'] and data['lastStarport']['name']
|
||||
if not this.system:
|
||||
this.system = data['lastSystem']['name']
|
||||
|
||||
if not this.station and data['commander']['docked']:
|
||||
this.station = data['lastStarport']['name']
|
||||
|
||||
# TODO: Fire off the EDSM API call to trigger the callback for the icons
|
||||
|
||||
if config.get('system_provider') == 'EDSM':
|
||||
@ -311,11 +460,14 @@ def cmdr_data(data, is_beta):
|
||||
# Do *NOT* set 'url' here, as it's set to a function that will call
|
||||
# through correctly. We don't want a static string.
|
||||
this.system_link.update_idletasks()
|
||||
|
||||
if config.get('station_provider') == 'EDSM':
|
||||
if data['commander']['docked']:
|
||||
this.station_link['text'] = this.station
|
||||
|
||||
elif data['lastStarport']['name'] and data['lastStarport']['name'] != "":
|
||||
this.station_link['text'] = STATION_UNDOCKED
|
||||
|
||||
else:
|
||||
this.station_link['text'] = ''
|
||||
|
||||
@ -331,22 +483,33 @@ def cmdr_data(data, is_beta):
|
||||
|
||||
|
||||
# Worker thread
|
||||
def worker():
|
||||
def worker() -> None:
|
||||
"""
|
||||
Upload worker.
|
||||
|
||||
pending = [] # Unsent events
|
||||
Processes `this.queue` until the queued item is None.
|
||||
"""
|
||||
pending = [] # Unsent events
|
||||
closing = False
|
||||
|
||||
while True:
|
||||
item = this.queue.get()
|
||||
item: Optional[Tuple[str, Mapping[str, Any]]] = this.queue.get()
|
||||
if item:
|
||||
(cmdr, entry) = item
|
||||
else:
|
||||
closing = True # Try to send any unsent events before we close
|
||||
closing = True # Try to send any unsent events before we close
|
||||
|
||||
retrying = 0
|
||||
while retrying < 3:
|
||||
try:
|
||||
if item and entry['event'] not in this.discardedEvents:
|
||||
if TYPE_CHECKING:
|
||||
# Tell the type checker that these two are bound.
|
||||
# TODO: While this works because of the item check below, these names are still technically unbound
|
||||
# TODO: in some cases, therefore this should be refactored.
|
||||
cmdr: str = ""
|
||||
entry: Mapping[str, Any] = {}
|
||||
|
||||
if item and entry['event'] not in this.discardedEvents: # TODO: Technically entry can be unbound here.
|
||||
pending.append(entry)
|
||||
|
||||
# Get list of events to discard
|
||||
@ -354,12 +517,26 @@ def worker():
|
||||
r = this.session.get('https://www.edsm.net/api-journal-v1/discard', timeout=_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
this.discardedEvents = set(r.json())
|
||||
this.discardedEvents.discard('Docked') # should_send() assumes that we send 'Docked' events
|
||||
assert this.discardedEvents # wouldn't expect this to be empty
|
||||
pending = [x for x in pending if x['event'] not in this.discardedEvents] # Filter out unwanted events
|
||||
this.discardedEvents.discard('Docked') # should_send() assumes that we send 'Docked' events
|
||||
if not this.discardedEvents:
|
||||
logger.error(
|
||||
'Unexpected empty discarded events list from EDSM. Bailing out of send: '
|
||||
f'{type(this.discardedEvents)} -- {this.discardedEvents}'
|
||||
)
|
||||
continue
|
||||
|
||||
# Filter out unwanted events
|
||||
pending = list(filter(lambda x: x['event'] not in this.discardedEvents, pending))
|
||||
|
||||
if should_send(pending):
|
||||
(username, apikey) = credentials(cmdr)
|
||||
if any(p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')):
|
||||
logger.debug('CarrierJump (or FSDJump) in pending and it passed should_send()')
|
||||
|
||||
creds = credentials(cmdr) # TODO: possibly unbound
|
||||
if creds is None:
|
||||
raise ValueError("Unexpected lack of credentials")
|
||||
|
||||
username, apikey = creds
|
||||
data = {
|
||||
'commanderName': username.encode('utf-8'),
|
||||
'apiKey': apikey,
|
||||
@ -367,33 +544,44 @@ def worker():
|
||||
'fromSoftwareVersion': appversion,
|
||||
'message': json.dumps(pending, ensure_ascii=False).encode('utf-8'),
|
||||
}
|
||||
|
||||
if any(p for p in pending if p['event'] in ('CarrierJump', 'FSDJump', 'Location', 'Docked')):
|
||||
data_elided = data.copy()
|
||||
data_elided['apiKey'] = '<elided>'
|
||||
logger.debug(f'CarrierJump (or FSDJump): Attempting API call\ndata: {data_elided!r}')
|
||||
|
||||
r = this.session.post('https://www.edsm.net/api-journal-v1', data=data, timeout=_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
reply = r.json()
|
||||
(msgnum, msg) = reply['msgnum'], reply['msg']
|
||||
msg_num = reply['msgnum']
|
||||
msg = reply['msg']
|
||||
# 1xx = OK
|
||||
# 2xx = fatal error
|
||||
# 3&4xx not generated at top-level
|
||||
# 5xx = error but events saved for later processing
|
||||
if msgnum // 100 == 2:
|
||||
logger.warning(f'EDSM\t{msgnum} {msg}\t{json.dumps(pending, separators = (",", ": "))}')
|
||||
if msg_num // 100 == 2:
|
||||
logger.warning(f'EDSM\t{msg_num} {msg}\t{json.dumps(pending, separators=(",", ": "))}')
|
||||
plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg))
|
||||
|
||||
else:
|
||||
for e, r in zip(pending, reply['events']):
|
||||
if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump', 'CarrierJump']:
|
||||
if not closing and e['event'] in ('StartUp', 'Location', 'FSDJump', 'CarrierJump'):
|
||||
# Update main window's system status
|
||||
this.lastlookup = r
|
||||
# calls update_status in main thread
|
||||
this.system_link.event_generate('<<EDSMStatus>>', when="tail")
|
||||
|
||||
elif r['msgnum'] // 100 != 1:
|
||||
logger.warning(f'EDSM\t{r["msgnum"]} {r["msg"]}\t'
|
||||
f'{json.dumps(e, separators = (",", ": "))}')
|
||||
|
||||
pending = []
|
||||
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug('Sending API events', exc_info=e)
|
||||
retrying += 1
|
||||
|
||||
else:
|
||||
plug.show_error(_("Error: Can't connect to EDSM"))
|
||||
|
||||
@ -401,17 +589,25 @@ def worker():
|
||||
return
|
||||
|
||||
|
||||
# Whether any of the entries should be sent immediately
|
||||
def should_send(entries):
|
||||
def should_send(entries: List[Mapping[str, Any]]) -> bool:
|
||||
"""
|
||||
Whether or not any of the given entries should be sent to EDSM.
|
||||
|
||||
:param entries: The entries to check
|
||||
:return: bool indicating whether or not to send said entries
|
||||
"""
|
||||
# batch up burst of Scan events after NavBeaconScan
|
||||
if this.navbeaconscan:
|
||||
if entries and entries[-1]['event'] == 'Scan':
|
||||
this.navbeaconscan -= 1
|
||||
if this.navbeaconscan:
|
||||
return False
|
||||
|
||||
else:
|
||||
assert(False)
|
||||
logger.error(
|
||||
'Invalid state NavBeaconScan exists, but passed entries either '
|
||||
"doesn't exist or doesn't have the expected content"
|
||||
)
|
||||
this.navbeaconscan = 0
|
||||
|
||||
for entry in entries:
|
||||
@ -420,32 +616,40 @@ def should_send(entries):
|
||||
this.newgame = False
|
||||
this.newgame_docked = False
|
||||
return True
|
||||
|
||||
elif this.newgame:
|
||||
pass
|
||||
elif entry['event'] not in ['CommunityGoal', # Spammed periodically
|
||||
'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout"
|
||||
'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # "
|
||||
|
||||
elif entry['event'] not in (
|
||||
'CommunityGoal', # Spammed periodically
|
||||
'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout"
|
||||
'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap'): # "
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Call edsm_notify_system() in this and other interested plugins with EDSM's response to a 'StartUp', 'Location', 'FSDJump' or 'CarrierJump' event
|
||||
def update_status(event=None):
|
||||
def update_status(event=None) -> None:
|
||||
"""Update listening plugins with our response to StartUp, Location, FSDJump, or CarrierJump."""
|
||||
for plugin in plug.provides('edsm_notify_system'):
|
||||
plug.invoke(plugin, None, 'edsm_notify_system', this.lastlookup)
|
||||
|
||||
|
||||
# Called with EDSM's response to a 'StartUp', 'Location', 'FSDJump' or 'CarrierJump' event. https://www.edsm.net/en/api-journal-v1
|
||||
# Called with EDSM's response to a 'StartUp', 'Location', 'FSDJump' or 'CarrierJump' event.
|
||||
# https://www.edsm.net/en/api-journal-v1
|
||||
# msgnum: 1xx = OK, 2xx = fatal error, 3xx = error, 4xx = ignorable errors.
|
||||
def edsm_notify_system(reply):
|
||||
def edsm_notify_system(reply: Mapping[str, Any]) -> None:
|
||||
"""Update the image next to the system link."""
|
||||
if not reply:
|
||||
this.system_link['image'] = this._IMG_ERROR
|
||||
plug.show_error(_("Error: Can't connect to EDSM"))
|
||||
elif reply['msgnum'] // 100 not in (1,4):
|
||||
|
||||
elif reply['msgnum'] // 100 not in (1, 4):
|
||||
this.system_link['image'] = this._IMG_ERROR
|
||||
plug.show_error(_('Error: EDSM {MSG}').format(MSG=reply['msg']))
|
||||
|
||||
elif reply.get('systemCreated'):
|
||||
this.system_link['image'] = this._IMG_NEW
|
||||
|
||||
else:
|
||||
this.system_link['image'] = this._IMG_KNOWN
|
||||
|
||||
|
155
plugins/inara.py
155
plugins/inara.py
@ -1,10 +1,7 @@
|
||||
#
|
||||
# Inara sync
|
||||
#
|
||||
"""Inara Sync."""
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import tkinter as tk
|
||||
@ -13,22 +10,25 @@ from collections import OrderedDict, defaultdict, deque
|
||||
from operator import itemgetter
|
||||
from queue import Queue
|
||||
from threading import Lock, Thread
|
||||
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Deque, Dict, List, Mapping, NamedTuple, Optional
|
||||
from typing import (
|
||||
TYPE_CHECKING, Any, AnyStr, Callable, Deque, Dict, List, Mapping, MutableMapping, NamedTuple, Optional
|
||||
)
|
||||
from typing import OrderedDict as OrderedDictT
|
||||
from typing import Sequence, Union
|
||||
from typing import Sequence, Union, cast
|
||||
|
||||
import requests
|
||||
|
||||
import myNotebook as nb # noqa: N813
|
||||
import plug
|
||||
import timeout_session
|
||||
from config import applongname, appname, appversion, config
|
||||
from config import applongname, appversion, config
|
||||
from EDMCLogging import get_main_logger
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
|
||||
logger = logging.getLogger(appname)
|
||||
logger = get_main_logger()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
def _(x):
|
||||
def _(x: str) -> str:
|
||||
return x
|
||||
|
||||
|
||||
@ -81,9 +81,8 @@ STATION_UNDOCKED: str = '×' # "Station" name to display when not docked = U+00
|
||||
|
||||
|
||||
class Credentials(NamedTuple):
|
||||
"""
|
||||
Credentials holds the set of credentials required to identify an inara API payload to inara
|
||||
"""
|
||||
"""Credentials holds the set of credentials required to identify an inara API payload to inara."""
|
||||
|
||||
cmdr: str
|
||||
fid: str
|
||||
api_key: str
|
||||
@ -93,9 +92,8 @@ EVENT_DATA = Union[Mapping[AnyStr, Any], Sequence[Mapping[AnyStr, Any]]]
|
||||
|
||||
|
||||
class Event(NamedTuple):
|
||||
"""
|
||||
Event represents an event for the Inara API
|
||||
"""
|
||||
"""Event represents an event for the Inara API."""
|
||||
|
||||
name: str
|
||||
timestamp: str
|
||||
data: EVENT_DATA
|
||||
@ -103,12 +101,19 @@ class Event(NamedTuple):
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NewThis:
|
||||
"""
|
||||
NewThis is where the plugin stores all of its data.
|
||||
|
||||
It is named NewThis as it is currently being migrated to. Once migration is complete it will be renamed to This.
|
||||
"""
|
||||
|
||||
events: Dict[Credentials, Deque[Event]] = dataclasses.field(default_factory=lambda: defaultdict(deque))
|
||||
event_lock: Lock = dataclasses.field(default_factory=Lock) # protects events, for use when rewriting events
|
||||
|
||||
def filter_events(self, key: Credentials, predicate: Callable[[Event], bool]):
|
||||
def filter_events(self, key: Credentials, predicate: Callable[[Event], bool]) -> None:
|
||||
"""
|
||||
filter_events is the equivalent of running filter() on any event list in the events dict.
|
||||
|
||||
it will automatically handle locking, and replacing the event list with the filtered version.
|
||||
|
||||
:param key: the key to filter
|
||||
@ -124,7 +129,8 @@ new_this = NewThis()
|
||||
TARGET_URL = 'https://inara.cz/inapi/v1/'
|
||||
|
||||
|
||||
def system_url(system_name: str):
|
||||
def system_url(system_name: str) -> str:
|
||||
"""Get a URL for the current system."""
|
||||
if this.system_address:
|
||||
return requests.utils.requote_uri(f'https://inara.cz/galaxy-starsystem/?search={this.system_address}')
|
||||
|
||||
@ -133,7 +139,17 @@ def system_url(system_name: str):
|
||||
|
||||
return ''
|
||||
|
||||
def station_url(system_name, station_name):
|
||||
|
||||
def station_url(system_name: str, station_name: str) -> str:
|
||||
"""
|
||||
Get a URL for the current station.
|
||||
|
||||
If there is no station, the system URL is returned.
|
||||
|
||||
:param system_name: The name of the current system
|
||||
:param station_name: The name of the current station, if any
|
||||
:return: A URL to inara for the given system and station
|
||||
"""
|
||||
if system_name and station_name:
|
||||
return requests.utils.requote_uri(f'https://inara.cz/galaxy-station/?search={system_name}%20[{station_name}]')
|
||||
|
||||
@ -146,7 +162,9 @@ def station_url(system_name, station_name):
|
||||
|
||||
return ''
|
||||
|
||||
def plugin_start3(plugin_dir):
|
||||
|
||||
def plugin_start3(plugin_dir: str) -> str:
|
||||
"""Plugin start Hook."""
|
||||
this.thread = Thread(target=new_worker, name='Inara worker')
|
||||
this.thread.daemon = True
|
||||
this.thread.start()
|
||||
@ -154,14 +172,16 @@ def plugin_start3(plugin_dir):
|
||||
return 'Inara'
|
||||
|
||||
|
||||
def plugin_app(parent: tk.Tk):
|
||||
def plugin_app(parent: tk.Tk) -> None:
|
||||
"""Plugin UI setup Hook."""
|
||||
this.system_link = parent.children['system'] # system label in main window
|
||||
this.station_link = parent.children['station'] # station label in main window
|
||||
this.system_link.bind_all('<<InaraLocation>>', update_location)
|
||||
this.system_link.bind_all('<<InaraShip>>', update_ship)
|
||||
|
||||
|
||||
def plugin_stop():
|
||||
def plugin_stop() -> None:
|
||||
"""Plugin shutdown hook."""
|
||||
# Signal thread to close and wait for it
|
||||
this.queue.put(None)
|
||||
# this.thread.join()
|
||||
@ -170,7 +190,8 @@ def plugin_stop():
|
||||
this.timer_run = False
|
||||
|
||||
|
||||
def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool):
|
||||
def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool) -> tk.Frame:
|
||||
"""Plugin Preferences UI hook."""
|
||||
PADX = 10
|
||||
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
|
||||
PADY = 2 # close spacing
|
||||
@ -212,7 +233,8 @@ def plugin_prefs(parent: tk.Tk, cmdr: str, is_beta: bool):
|
||||
return frame
|
||||
|
||||
|
||||
def prefs_cmdr_changed(cmdr: str, is_beta: bool):
|
||||
def prefs_cmdr_changed(cmdr: str, is_beta: bool) -> None:
|
||||
"""Plugin commander change hook."""
|
||||
this.log_button['state'] = tk.NORMAL if cmdr and not is_beta else tk.DISABLED
|
||||
this.apikey['state'] = tk.NORMAL
|
||||
this.apikey.delete(0, tk.END)
|
||||
@ -231,6 +253,7 @@ def prefs_cmdr_changed(cmdr: str, is_beta: bool):
|
||||
|
||||
|
||||
def prefsvarchanged():
|
||||
"""Preferences window change hook."""
|
||||
state = tk.DISABLED
|
||||
if this.log.get():
|
||||
state = this.log_button['state']
|
||||
@ -240,7 +263,8 @@ def prefsvarchanged():
|
||||
this.apikey['state'] = state
|
||||
|
||||
|
||||
def prefs_changed(cmdr: str, is_beta: bool):
|
||||
def prefs_changed(cmdr: str, is_beta: bool) -> None:
|
||||
"""Preferences window closed hook."""
|
||||
changed = config.getint('inara_out') != this.log.get()
|
||||
config.set('inara_out', this.log.get())
|
||||
|
||||
@ -264,14 +288,14 @@ def prefs_changed(cmdr: str, is_beta: bool):
|
||||
|
||||
if this.log.get() and changed:
|
||||
this.newuser = True # Send basic info at next Journal event
|
||||
new_add_event('getCommanderProfile', time.strftime(
|
||||
'%Y-%m-%dT%H:%M:%SZ', time.gmtime()), {'searchName': cmdr})
|
||||
# call()
|
||||
new_add_event(
|
||||
'getCommanderProfile', time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), {'searchName': cmdr}
|
||||
)
|
||||
|
||||
|
||||
def credentials(cmdr: str) -> Optional[str]:
|
||||
"""
|
||||
credentials fetches the credentials for the given commander
|
||||
Get the credentials for the current commander.
|
||||
|
||||
:param cmdr: Commander name to search for credentials
|
||||
:return: Credentials for the given commander or None
|
||||
@ -287,12 +311,11 @@ def credentials(cmdr: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any]):
|
||||
# Send any unsent events when switching accounts
|
||||
# if cmdr and cmdr != this.cmdr:
|
||||
# call(force=True)
|
||||
|
||||
event_name = entry['event']
|
||||
def journal_entry(
|
||||
cmdr: str, is_beta: bool, system: str, station: str, entry: Dict[str, Any], state: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Journal entry hook."""
|
||||
event_name: str = entry['event']
|
||||
this.cmdr = cmdr
|
||||
this.FID = state['FID']
|
||||
this.multicrew = bool(state['Role'])
|
||||
@ -337,7 +360,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
|
||||
# We need pop == 0 to set the value so as to clear 'x' in systems with
|
||||
# no stations.
|
||||
pop = entry.get('Population')
|
||||
pop: Optional[int] = entry.get('Population')
|
||||
if pop is not None:
|
||||
this.system_population = pop
|
||||
|
||||
@ -352,10 +375,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
current_creds = Credentials(this.cmdr, this.FID, str(credentials(this.cmdr)))
|
||||
try:
|
||||
# Dump starting state to Inara
|
||||
if (this.newuser or
|
||||
event_name == 'StartUp' or
|
||||
(this.newsession and event_name == 'Cargo')):
|
||||
|
||||
if (this.newuser or event_name == 'StartUp' or (this.newsession and event_name == 'Cargo')):
|
||||
this.newuser = False
|
||||
this.newsession = False
|
||||
|
||||
@ -379,7 +399,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
)
|
||||
|
||||
if state['Engineers']: # Not populated < 3.3
|
||||
to_send = []
|
||||
to_send: List[Mapping[str, Any]] = []
|
||||
for k, v in state['Engineers'].items():
|
||||
e = {'engineerName': k}
|
||||
if isinstance(v, tuple):
|
||||
@ -440,6 +460,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
)
|
||||
|
||||
elif event_name == 'EngineerProgress' and 'Engineer' in entry:
|
||||
# TODO: due to this var name being used above, the types are weird
|
||||
to_send = {'engineerName': entry['Engineer']}
|
||||
if 'Rank' in entry:
|
||||
to_send['rankValue'] = entry['Rank']
|
||||
@ -477,7 +498,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
|
||||
# Ship change
|
||||
if event_name == 'Loadout' and this.shipswap:
|
||||
cur_ship = {
|
||||
cur_ship: Dict[str, Any] = {
|
||||
'shipType': state['ShipType'],
|
||||
'shipGameID': state['ShipID'],
|
||||
'shipName': state['ShipName'], # Can be None
|
||||
@ -595,7 +616,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
new_add_event('setCommanderInventoryCargo', entry['timestamp'], cargo)
|
||||
this.cargo = cargo
|
||||
|
||||
materials = []
|
||||
materials: List[Mapping[str, Any]] = []
|
||||
for category in ('Raw', 'Manufactured', 'Encoded'):
|
||||
materials.extend(
|
||||
[OrderedDict([('itemName', k), ('itemCount', state[category][k])]) for k in sorted(state[category])]
|
||||
@ -929,7 +950,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
# ))
|
||||
|
||||
for goal in entry['CurrentGoals']:
|
||||
data = OrderedDict([
|
||||
data: MutableMapping[str, Any] = OrderedDict([
|
||||
('communitygoalGameID', goal['CGID']),
|
||||
('communitygoalName', goal['Title']),
|
||||
('starsystemName', goal['SystemName']),
|
||||
@ -952,7 +973,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
|
||||
new_add_event('setCommunityGoal', entry['timestamp'], data)
|
||||
|
||||
data = OrderedDict([
|
||||
data: MutableMapping[str, Any] = OrderedDict([
|
||||
('communitygoalGameID', goal['CGID']),
|
||||
('contribution', goal['PlayerContribution']),
|
||||
('percentileBand', goal['PlayerPercentileBand']),
|
||||
@ -998,7 +1019,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
this.system_link.update_idletasks()
|
||||
|
||||
if config.get('station_provider') == 'Inara':
|
||||
to_set = this.station
|
||||
to_set: str = cast(str, this.station)
|
||||
if not to_set:
|
||||
if this.system_population is not None and this.system_population > 0:
|
||||
to_set = STATION_UNDOCKED
|
||||
@ -1012,6 +1033,7 @@ def journal_entry(cmdr: str, is_beta: bool, system: str, station: str, entry: Di
|
||||
|
||||
|
||||
def cmdr_data(data, is_beta):
|
||||
"""CAPI event hook."""
|
||||
this.cmdr = data['commander']['name']
|
||||
|
||||
# Always store initially, even if we're not the *current* system provider.
|
||||
@ -1049,7 +1071,8 @@ def cmdr_data(data, is_beta):
|
||||
if not (CREDIT_RATIO > this.lastcredits / data['commander']['credits'] > 1/CREDIT_RATIO):
|
||||
new_this.filter_events(
|
||||
Credentials(this.cmdr, this.FID, str(credentials(this.cmdr))),
|
||||
lambda e: e.name != 'setCommanderCredits')
|
||||
lambda e: e.name != 'setCommanderCredits'
|
||||
)
|
||||
|
||||
# this.events = [x for x in this.events if x['eventName'] != 'setCommanderCredits'] # Remove any unsent
|
||||
new_add_event(
|
||||
@ -1065,6 +1088,12 @@ def cmdr_data(data, is_beta):
|
||||
|
||||
|
||||
def make_loadout(state: Dict[str, Any]) -> OrderedDictT[str, Any]:
|
||||
"""
|
||||
Construct an inara loadout from an event.
|
||||
|
||||
:param state: The event / state to construct the event from
|
||||
:return: The constructed loadout
|
||||
"""
|
||||
modules = []
|
||||
for m in state['Modules'].values():
|
||||
module: OrderedDictT[str, Any] = OrderedDict([
|
||||
@ -1132,8 +1161,9 @@ def new_add_event(
|
||||
fid: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
add a journal event to the queue, to be sent to inara at the next opportunity. If provided, use the given cmdr
|
||||
name over the current one
|
||||
Add a journal event to the queue, to be sent to inara at the next opportunity.
|
||||
|
||||
If provided, use the given cmdr name over the current one
|
||||
|
||||
:param name: name of the event
|
||||
:param timestamp: timestamp of the event
|
||||
@ -1148,7 +1178,7 @@ def new_add_event(
|
||||
|
||||
api_key = credentials(this.cmdr)
|
||||
if api_key is None:
|
||||
logger.warn(f"cannot find an API key for cmdr {this.cmdr!r}")
|
||||
logger.warning(f"cannot find an API key for cmdr {this.cmdr!r}")
|
||||
return
|
||||
|
||||
key = Credentials(str(cmdr), str(fid), api_key) # this fails type checking due to `this` weirdness, hence str()
|
||||
@ -1158,6 +1188,11 @@ def new_add_event(
|
||||
|
||||
|
||||
def new_worker():
|
||||
"""
|
||||
Queue worker.
|
||||
|
||||
Will only ever send one message per WORKER_WAIT_TIME, regardless of status.
|
||||
"""
|
||||
while True:
|
||||
events = get_events()
|
||||
for creds, event_list in events.items():
|
||||
@ -1182,9 +1217,9 @@ def new_worker():
|
||||
time.sleep(WORKER_WAIT_TIME)
|
||||
|
||||
|
||||
def get_events(clear=True) -> Dict[Credentials, List[Event]]:
|
||||
def get_events(clear: bool = True) -> Dict[Credentials, List[Event]]:
|
||||
"""
|
||||
get_events fetches all events from the current queue and returns a frozen version of them
|
||||
Fetch a frozen copy of all events from the current queue.
|
||||
|
||||
:param clear: whether or not to clear the queues as we go, defaults to True
|
||||
:return: the frozen event list
|
||||
@ -1199,9 +1234,9 @@ def get_events(clear=True) -> Dict[Credentials, List[Event]]:
|
||||
return out
|
||||
|
||||
|
||||
def try_send_data(url: str, data: Mapping[str, Any]):
|
||||
def try_send_data(url: str, data: Mapping[str, Any]) -> None:
|
||||
"""
|
||||
attempt repeatedly to send the payload forward
|
||||
Attempt repeatedly to send the payload forward.
|
||||
|
||||
:param url: target URL for the payload
|
||||
:param data: the payload
|
||||
@ -1219,7 +1254,7 @@ def try_send_data(url: str, data: Mapping[str, Any]):
|
||||
|
||||
def send_data(url: str, data: Mapping[str, Any]) -> bool:
|
||||
"""
|
||||
write a set of events to the inara API
|
||||
Write a set of events to the inara API.
|
||||
|
||||
:param url: the target URL to post to
|
||||
:param data: the data to POST
|
||||
@ -1266,10 +1301,9 @@ def send_data(url: str, data: Mapping[str, Any]) -> bool:
|
||||
return True # regardless of errors above, we DID manage to send it, therefore inform our caller as such
|
||||
|
||||
|
||||
def update_location(event=None):
|
||||
def update_location(event: Dict[str, Any] = None) -> None:
|
||||
"""
|
||||
Call inara_notify_location in this and other interested plugins with Inara's response when changing system
|
||||
or station
|
||||
Update other plugins with our response to system and station changes.
|
||||
|
||||
:param event: Unused and ignored, defaults to None
|
||||
"""
|
||||
@ -1278,13 +1312,14 @@ def update_location(event=None):
|
||||
plug.invoke(plugin, None, 'inara_notify_location', this.lastlocation)
|
||||
|
||||
|
||||
def inara_notify_location(eventData):
|
||||
def inara_notify_location(eventData: Dict[str, Any]) -> None:
|
||||
"""Unused."""
|
||||
pass
|
||||
|
||||
|
||||
def update_ship(event=None):
|
||||
def update_ship(event: Dict[str, Any] = None) -> None:
|
||||
"""
|
||||
Call inara_notify_ship() in interested plugins with Inara's response when changing ship
|
||||
Update other plugins with our response to changing.
|
||||
|
||||
:param event: Unused and ignored, defaults to None
|
||||
"""
|
||||
|
4
prefs.py
4
prefs.py
@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Callable, Optional, Type, Union
|
||||
import myNotebook as nb # noqa: N813
|
||||
import plug
|
||||
from config import applongname, appname, appversion, config
|
||||
from EDMCLogging import edmclogger
|
||||
from EDMCLogging import edmclogger, get_main_logger
|
||||
from hotkey import hotkeymgr
|
||||
from l10n import Translations
|
||||
from monitor import monitor
|
||||
@ -23,7 +23,7 @@ from myNotebook import Notebook
|
||||
from theme import theme
|
||||
from ttkHyperlinkLabel import HyperlinkLabel
|
||||
|
||||
logger = logging.getLogger(appname)
|
||||
logger = get_main_logger()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
def _(x: str) -> str:
|
||||
|
Loading…
x
Reference in New Issue
Block a user