1
1
#!/usr/bin/env python3
2
2
import logging
3
3
import re
4
+ import json
4
5
from subprocess import Popen
5
6
from pathlib import Path
7
+ from time import sleep
8
+ from typing import Optional
6
9
import paho .mqtt .client as mqtt
7
10
import platform
8
11
12
+ API_VERSION = "1"
9
13
BASE_PATH = Path (__file__ ).resolve ().parents [2 ]
10
14
RAMDISK_PATH = BASE_PATH / "ramdisk"
11
15
RUNS_PATH = BASE_PATH / "runs"
12
16
BASE_TOPIC = "openWB-remote/"
17
+ API_TOPIC = BASE_TOPIC + "api_version"
18
+ STATE_TOPIC = BASE_TOPIC + "connection_state"
13
19
REMOTE_SUPPORT_TOPIC = BASE_TOPIC + "support"
14
20
REMOTE_PARTNER_TOPIC = BASE_TOPIC + "partner"
21
+ REMOTE_PARTNER_IDS_TOPIC = BASE_TOPIC + "valid_partner_ids"
15
22
CLOUD_TOPIC = BASE_TOPIC + "cloud"
23
+
16
24
support_tunnel : Popen = None
17
25
partner_tunnel : Popen = None
18
26
cloud_tunnel : Popen = None
27
+ valid_partner_ids : list [str ] = []
19
28
logging .basicConfig (
20
29
filename = str (RAMDISK_PATH / "remote_support.log" ),
21
30
level = logging .DEBUG , format = '%(asctime)s: %(message)s'
@@ -32,10 +41,43 @@ def get_serial():
32
41
return "0000000000000000"
33
42
34
43
44
+ def publish_as_json (client : mqtt .Client , topic : str , str_payload : str , qos : int = 0 , retain : bool = False ,
45
+ properties : Optional [mqtt .Properties ] = None ) -> mqtt .MQTTMessageInfo :
46
+ return client .publish (topic , json .dumps (str_payload ), qos , retain , properties )
47
+
48
+
49
+ def get_lt_executable () -> Optional [Path ]:
50
+ machine = platform .machine ()
51
+ bits , linkage = platform .architecture ()
52
+ lt_executable = f"lt-{ machine } _{ linkage } "
53
+
54
+ log .debug ("System Info:" )
55
+ log .debug (f"Architecture: ({ (bits , linkage )} )" )
56
+ log .debug (f"Machine: { machine } " )
57
+ log .debug (f"Node: { platform .node ()} " )
58
+ log .debug (f"Platform: { platform .platform ()} " )
59
+ log .debug (f"System: { platform .system ()} " )
60
+ log .debug (f"Release: { platform .release ()} " )
61
+ log .debug (f"using binary: '{ lt_executable } '" )
62
+
63
+ lt_path = RUNS_PATH / lt_executable
64
+ if not lt_path .is_file ():
65
+ log .error (f"file '{ lt_executable } ' does not exist!" )
66
+ return None
67
+ return lt_path
68
+
69
+
35
70
def on_connect (client : mqtt .Client , userdata , flags : dict , rc : int ):
36
71
"""connect to broker and subscribe to set topics"""
37
72
log .info ("Connected" )
38
- client .subscribe (BASE_TOPIC + "#" , 2 )
73
+ client .subscribe ([
74
+ (REMOTE_SUPPORT_TOPIC , 2 ),
75
+ (CLOUD_TOPIC , 2 ),
76
+ (REMOTE_PARTNER_TOPIC , 2 ),
77
+ (REMOTE_PARTNER_IDS_TOPIC , 2 )
78
+ ])
79
+ publish_as_json (client , API_TOPIC , API_VERSION , qos = 2 , retain = True )
80
+ publish_as_json (client , STATE_TOPIC , "online" , qos = 2 , retain = True )
39
81
40
82
41
83
def on_message (client : mqtt .Client , userdata , msg : mqtt .MQTTMessage ):
@@ -56,6 +98,8 @@ def is_tunnel_closed(tunnel: Popen) -> bool:
56
98
global support_tunnel
57
99
global partner_tunnel
58
100
global cloud_tunnel
101
+ global valid_partner_ids
102
+ clear_topic = False
59
103
payload = msg .payload .decode ("utf-8" )
60
104
if len (payload ) > 0 :
61
105
log .debug ("Topic: %s, Message: %s" , msg .topic , payload )
@@ -81,6 +125,9 @@ def is_tunnel_closed(tunnel: Popen) -> bool:
81
125
log .info (f"tunnel running with pid { support_tunnel .pid } " )
82
126
else :
83
127
log .info ("unknown message: " + payload )
128
+ clear_topic = True
129
+ elif msg .topic == REMOTE_PARTNER_IDS_TOPIC :
130
+ valid_partner_ids = json .loads (payload )
84
131
elif msg .topic == REMOTE_PARTNER_TOPIC :
85
132
if payload == 'stop' :
86
133
if partner_tunnel is None :
@@ -90,22 +137,39 @@ def is_tunnel_closed(tunnel: Popen) -> bool:
90
137
partner_tunnel .terminate ()
91
138
partner_tunnel .wait (timeout = 3 )
92
139
partner_tunnel = None
93
- elif re .match (r'^([^;]+)(?:;([1-9][ 0-9]+)(?:;([a-zA-Z0-9 ]+))?)?$' , payload ):
140
+ elif re .match (r'^([^;]+)(?:;((?:cnode)?[ 0-9]+)(?:;([\wäöüÄÖÜ- ]+))?)?$' , payload ):
94
141
if is_tunnel_closed (partner_tunnel ):
95
142
splitted = payload .split (";" )
96
143
if len (splitted ) != 3 :
97
144
log .error ("invalid number of settings received!" )
98
145
else :
99
146
token = splitted [0 ]
100
- port = splitted [1 ]
101
- user = splitted [2 ]
102
- log .info ("start partner support" )
103
- partner_tunnel = Popen (["sshpass" , "-p" , token , "ssh" , "-N" , "-tt" , "-o" ,
104
- "StrictHostKeyChecking=no" , "-o" , "ServerAliveInterval 60" , "-R" ,
105
- f"{ port } :localhost:80" , f"{ user } @partner.openwb.de" ])
106
- log .info (f"tunnel running with pid { partner_tunnel .pid } " )
147
+ port_or_node = splitted [1 ]
148
+ user = splitted [2 ] # not used in v0, partner-id in v1
149
+ if port_or_node .isdecimal ():
150
+ # v0
151
+ log .info ("start partner support" )
152
+ partner_tunnel = Popen (["sshpass" , "-p" , token , "ssh" , "-N" , "-tt" , "-o" ,
153
+ "StrictHostKeyChecking=no" , "-o" , "ServerAliveInterval 60" , "-R" ,
154
+ f"{ port_or_node } :localhost:80" , f"{ user } @partner.openwb.de" ])
155
+ log .info (f"tunnel running with pid { partner_tunnel .pid } " )
156
+ else :
157
+ # v1
158
+ if lt_executable is None :
159
+ log .error ("start partner tunnel requested but lt executable not found!" )
160
+ else :
161
+ if user in valid_partner_ids :
162
+ log .info ("start partner support v1" )
163
+ if lt_executable is not None :
164
+ partner_tunnel = Popen ([f"{ lt_executable } " , "-h" ,
165
+ "https://" + port_or_node + ".openwb.de/" ,
166
+ "-p" , "80" , "-s" , token ])
167
+ log .info (f"tunnel running with pid { partner_tunnel .pid } " )
168
+ else :
169
+ log .error (f"invalid partner-id: { user } " )
107
170
else :
108
171
log .info ("unknown message: " + payload )
172
+ clear_topic = True
109
173
elif msg .topic == CLOUD_TOPIC :
110
174
if payload == 'stop' :
111
175
if cloud_tunnel is None :
@@ -125,37 +189,44 @@ def is_tunnel_closed(tunnel: Popen) -> bool:
125
189
cloud_node = splitted [1 ]
126
190
user = splitted [2 ]
127
191
128
- machine = platform .machine ()
129
- bits , linkage = platform .architecture ()
130
- lt_executable = f"lt-{ machine } _{ linkage } "
131
-
132
- log .debug ("System Info:" )
133
- log .debug (f"Architecture: ({ (bits , linkage )} )" )
134
- log .debug (f"Machine: { machine } " )
135
- log .debug (f"Node: { platform .node ()} " )
136
- log .debug (f"Platform: { platform .platform ()} " )
137
- log .debug (f"System: { platform .system ()} " )
138
- log .debug (f"Release: { platform .release ()} " )
139
- log .debug (f"using binary: '{ lt_executable } '" )
140
-
141
- log .info (f"start cloud tunnel '{ token [:4 ]} ...{ token [- 4 :]} ' on '{ cloud_node } '" )
142
- try :
143
- cloud_tunnel = Popen ([f"{ RUNS_PATH } /{ lt_executable } " , "-h" ,
192
+ if lt_executable is None :
193
+ log .error ("start cloud tunnel requested but lt executable not found!" )
194
+ else :
195
+ log .info (f"start cloud tunnel '{ token [:4 ]} ...{ token [- 4 :]} ' on '{ cloud_node } '" )
196
+ cloud_tunnel = Popen ([f"{ lt_executable } " , "-h" ,
144
197
"https://" + cloud_node + ".openwb.de/" , "-p" , "80" , "-s" , token ])
145
198
log .info (f"cloud tunnel running with pid { cloud_tunnel .pid } " )
146
- except FileNotFoundError :
147
- log .exception (f"executable '{ lt_executable } ' does not exist!" )
148
199
else :
149
200
log .info ("unknown message: " + payload )
201
+ clear_topic = True
150
202
# clear topic
151
- client .publish (msg .topic , "" , qos = 2 , retain = True )
203
+ if clear_topic and msg .retain :
204
+ client .publish (msg .topic , "" , qos = 2 , retain = True )
152
205
153
206
207
+ lt_executable = get_lt_executable ()
154
208
mqtt_broker_host = "localhost"
155
209
client = mqtt .Client ("openWB-remote-" + get_serial ())
156
210
client .on_connect = on_connect
157
211
client .on_message = on_message
212
+ client .will_set (STATE_TOPIC , json .dumps ("offline" ), qos = 2 , retain = True )
158
213
214
+ log .debug ("connecting to broker" )
159
215
client .connect (mqtt_broker_host , 1883 )
160
- client .loop_forever ()
161
- client .disconnect ()
216
+ log .debug ("starting loop" )
217
+ client .loop_start ()
218
+ try :
219
+ while True :
220
+ sleep (1 )
221
+ except (Exception , KeyboardInterrupt ) as e :
222
+ log .debug (e )
223
+ log .debug ("terminated" )
224
+ finally :
225
+ log .debug ("publishing state 'offline'" )
226
+ publish_as_json (client , STATE_TOPIC , "offline" , qos = 2 , retain = True )
227
+ sleep (0.5 )
228
+ log .debug ("stopping loop" )
229
+ client .loop_stop ()
230
+ client .disconnect ()
231
+ log .debug ("disconnected" )
232
+ log .debug ("exit" )
0 commit comments