-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathload.py
312 lines (254 loc) · 10.8 KB
/
load.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# -*- coding: utf-8 -*-
"""Telemetry: An EDMC Plugin to relay dashboard status and journal entries via MQTT."""
# Written by Edward Wright (https://github.com/fasteddy516)
# Available at https://github.com/fasteddy516/EDMC-Telemetry
#
# Requires Elite Dangerous Market Connector: https://github.com/EDCD/EDMarketConnector
# Uses the Eclipse Paho MQTT Python Client (https://github.com/eclipse/paho.mqtt.python)
# for all MQTT protocol (http://mqtt.org/) interactions.
import json
import logging
import os
import time
import tkinter as tk
from typing import Any, Dict, Optional, Tuple
import myNotebook as nb # type: ignore (provided by EDMC)
import semantic_version # type: ignore (provided by EDMC)
from config import appname, appversion, config # type: ignore (provided by EDMC)
from monitor import monitor # type: ignore (provided by EDMC)
import paho.mqtt.client as mqtt_client
from settings import Settings
# plugin constants
TELEMETRY_VERSION = "0.3.9"
TELEMETRY_PIPS = ("sys", "eng", "wep")
GAME_STATE_EVENTS = ("startup", "loadgame", "shutdown")
# set up logging
logger = logging.getLogger(f"{appname}.{os.path.basename(os.path.dirname(__file__))}")
# Globals
class Globals:
"""Holds module globals."""
def __init__(self):
"""Create and initialize module globals."""
self.status: Optional[tk.Label] = None
self.status_message: str = "Initializing"
self.status_color: str = "grey"
self.modifying_preferences = False
self.mqtt_connected: bool = False
self.current_db = {}
self.current_location = {"system": "N/A", "station": "N/A"}
self.current_state = {}
self.settings = Settings(TELEMETRY_VERSION, logger)
self.mqtt = mqtt_client.Client()
this = Globals()
# Standard EDMC plugin functions
def plugin_start3(plugin_dir: str) -> str:
"""Start the telemetry plugin."""
if callable(appversion) and appversion() >= semantic_version.Version("5.0.0"):
connect_telemetry()
else:
logger.fatal("EDMC-Telemetry requires EDMC 5.0.0 or newer.")
status_message(message="ERROR: EDMC < 5.0.0", color="red", immediate=True)
return "Telemetry"
def plugin_stop() -> None:
"""Stop the telemetry plugin."""
disconnect_telemetry()
def plugin_app(parent: tk.Frame) -> Tuple[tk.Label, tk.Label]:
"""Show broker connection status on main UI."""
label = tk.Label(parent, text="Telemetry:")
this.status = tk.Label(parent, anchor=tk.W, text="Initializing", foreground="grey")
this.status.bind_all("<<TelemetryStatus>>", _update_status)
status_message(immediate=True)
return (label, this.status)
def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> Optional[tk.Frame]:
"""Allow configuration to be modified from UI."""
this.modifying_preferences = True
return this.settings.show_preferences(parent)
def prefs_changed(cmdr: str, is_beta: bool) -> None:
"""Update settings after they've been modified in UI."""
# update_preferences() returns True if a connection reset is required
if this.settings.update_preferences():
logger.info("MQTT broker settings modified, connection will now restart.")
disconnect_telemetry()
connect_telemetry()
this.modifying_preferences = False
status_message(immediate=True)
def status_message(message: str = "", color: str = "", immediate=False) -> None:
"""Update the status message and color to be displayed on the main UI."""
if len(message):
this.status_message = message
if len(color):
this.status_color = color
if immediate:
_update_status()
else:
if (
this.status is not None
and not config.shutting_down
and not this.modifying_preferences
):
this.status.event_generate("<<TelemetryStatus>>", when="tail")
def _update_status(event=None) -> None:
"""Post the status message to the main UI."""
if this.status is not None:
this.status["text"] = this.status_message
this.status["foreground"] = this.status_color
def dashboard_entry(cmdr: str, is_beta: bool, entry: Dict[str, Any]) -> None:
"""Publish dashboard status via MQTT."""
if not this.settings.dashboard:
return
if not this.mqtt_connected:
return
dashboard_topic = this.settings.topic("dashboard")
if this.settings.dashboard_format == "Raw":
publish(dashboard_topic, payload=json.dumps(entry))
else:
for key in entry:
# always ignore these keys
if key.lower() == "timestamp" or key.lower() == "event":
continue
# publish any updated dashboard data
if key not in this.current_db or this.current_db[key] != entry[key]:
topic = f"{dashboard_topic}/{this.settings.topic(key)}"
# additional processing for pip updates
if key.lower() == "pips":
for i, pips in enumerate(entry[key]):
publish(
f"{topic}/{this.settings.topic(TELEMETRY_PIPS[i])}",
payload=str(pips),
)
# additional processing for fuel updates
elif key.lower() == "fuel":
for tank in entry[key]:
publish(
f"{topic}/{this.settings.topic(tank)}",
payload=str(entry[key][tank]),
)
# standard processing for most status updates
else:
publish(topic, payload=str(entry[key]))
# update internal tracking variable (used to filter unnecessary updates)
this.current_db[key] = entry[key]
def journal_entry(
cmdr: str,
is_beta: bool,
system: str,
station: str,
entry: Dict[str, Any],
state: Dict[str, Any],
) -> None:
"""Process player journal entries."""
if not this.mqtt_connected:
return
if this.settings.location:
if this.current_location["system"] != system:
publish(
f"{this.settings.topic('location')}/{this.settings.topic('system')}",
payload="" if system is None else system,
)
this.current_location["system"] = system
if this.current_location["station"] != station:
publish(
f"{this.settings.topic('location')}/{this.settings.topic('station')}",
payload="" if station is None else station,
)
this.current_location["station"] = station
if this.settings.state:
if this.current_state != state:
new_state = state.copy()
if "Friends" in new_state and isinstance(new_state["Friends"], set):
new_state["Friends"] = list(new_state["Friends"])
publish(this.settings.topic("state"), payload=json.dumps(new_state))
this.current_state = state.copy()
if str(entry["event"]).lower() in GAME_STATE_EVENTS:
publish(
topic=this.settings.topic("gamerunning"),
payload=str(monitor.game_running()),
)
if not this.settings.journal:
return
topic = this.settings.topic("journal")
if this.settings.journal_format == "Raw":
data = entry
else:
topic = f"{topic}/{this.settings.topic(entry['event'])}"
data = entry.copy()
del data["event"]
del data["timestamp"]
publish(topic, payload=json.dumps(data))
def connect_telemetry() -> None:
"""Establish a connection with the MQTT broker."""
status_message(message="Connecting", color="steel blue")
this.mqtt.reinitialise(client_id=this.settings.client_id)
this.mqtt.on_connect = mqttCallback_on_connect
this.mqtt.on_disconnect = mqttCallback_on_disconnect
this.mqtt.username_pw_set(this.settings.username, this.settings.password)
this.mqtt.will_set(
topic=f"{this.settings.topic('root')}/{this.settings.topic('feedactive')}",
payload="False",
qos=0,
retain=True,
)
try:
if this.settings.encryption:
ca_certs_arg = (
this.settings.ca_certs if len(this.settings.ca_certs) else None
)
certfile_arg = (
this.settings.certfile if len(this.settings.certfile) else None
)
keyfile_arg = this.settings.keyfile if len(this.settings.keyfile) else None
this.mqtt.tls_set(
ca_certs=ca_certs_arg,
certfile=certfile_arg,
keyfile=keyfile_arg,
)
this.mqtt.tls_insecure_set(this.settings.tls_insecure)
this.mqtt.connect_async(
this.settings.broker,
this.settings.port,
this.settings.keepalive,
)
this.mqtt.loop_start()
except Exception as e:
status_message(message="CONFIG ERROR", color="red")
logger.error(f"MQTT configuration error - check your connection settings. {e}")
def disconnect_telemetry() -> None:
"""Break connection to the MQTT broker."""
status_message(message="Disconnecting", color="steel blue")
if this.mqtt_connected:
publish(topic=this.settings.topic("feedactive"), payload="False", retain=True)
time.sleep(0.5)
this.mqtt.disconnect()
start = time.monotonic()
while this.mqtt_connected:
time.sleep(0.1)
if (time.monotonic() - start) >= 5.0:
logger.error("Timeout waiting for MQTT to disconnect.")
break
this.mqtt.loop_stop()
def publish(topic: str, payload: str, retain: bool = False):
"""Publish the specified payload to the specified MQTT topic."""
topic = f"{this.settings.topic('root')}/{topic}"
if this.settings.lowercase_topics:
topic = topic.lower()
this.mqtt.publish(topic, payload=payload, qos=this.settings.qos, retain=retain)
def mqttCallback_on_connect(client, userdata, flags, rc):
"""Run this callback when connection to a broker is established."""
this.current_db = {}
this.current_location["system"] = "N/A"
this.current_location["station"] = "N/A"
this.current_state = {}
if this.mqtt_connected is False:
logger.info("Connected to MQTT Broker")
this.mqtt_connected = True
status_message(message="Online", color="dark green")
publish(topic=this.settings.topic("feedactive"), payload="True", retain=True)
publish(
topic=this.settings.topic("gamerunning"), payload=str(monitor.game_running())
)
def mqttCallback_on_disconnect(client, userdata, rc):
"""Run this callback when the connection to the broker is lost."""
if this.mqtt_connected is True:
logger.info("Disconnected from MQTT Broker")
this.mqtt_connected = False
status_message(message="Offline", color="orange red")