11#!/usr/bin/env python3
22import logging
33import re
4+ import json
45from subprocess import Popen
56from pathlib import Path
7+ from time import sleep
8+ from typing import Optional
69import paho .mqtt .client as mqtt
710import platform
811
12+ API_VERSION = "1"
913BASE_PATH = Path (__file__ ).resolve ().parents [2 ]
1014RAMDISK_PATH = BASE_PATH / "ramdisk"
1115RUNS_PATH = BASE_PATH / "runs"
1216BASE_TOPIC = "openWB-remote/"
17+ API_TOPIC = BASE_TOPIC + "api_version"
18+ STATE_TOPIC = BASE_TOPIC + "connection_state"
1319REMOTE_SUPPORT_TOPIC = BASE_TOPIC + "support"
1420REMOTE_PARTNER_TOPIC = BASE_TOPIC + "partner"
21+ REMOTE_PARTNER_IDS_TOPIC = BASE_TOPIC + "valid_partner_ids"
1522CLOUD_TOPIC = BASE_TOPIC + "cloud"
23+
1624support_tunnel : Popen = None
1725partner_tunnel : Popen = None
1826cloud_tunnel : Popen = None
27+ valid_partner_ids : list [str ] = []
1928logging .basicConfig (
2029 filename = str (RAMDISK_PATH / "remote_support.log" ),
2130 level = logging .DEBUG , format = '%(asctime)s: %(message)s'
@@ -32,10 +41,43 @@ def get_serial():
3241 return "0000000000000000"
3342
3443
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+
3570def on_connect (client : mqtt .Client , userdata , flags : dict , rc : int ):
3671 """connect to broker and subscribe to set topics"""
3772 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 )
3981
4082
4183def on_message (client : mqtt .Client , userdata , msg : mqtt .MQTTMessage ):
@@ -56,6 +98,8 @@ def is_tunnel_closed(tunnel: Popen) -> bool:
5698 global support_tunnel
5799 global partner_tunnel
58100 global cloud_tunnel
101+ global valid_partner_ids
102+ clear_topic = False
59103 payload = msg .payload .decode ("utf-8" )
60104 if len (payload ) > 0 :
61105 log .debug ("Topic: %s, Message: %s" , msg .topic , payload )
@@ -81,6 +125,9 @@ def is_tunnel_closed(tunnel: Popen) -> bool:
81125 log .info (f"tunnel running with pid { support_tunnel .pid } " )
82126 else :
83127 log .info ("unknown message: " + payload )
128+ clear_topic = True
129+ elif msg .topic == REMOTE_PARTNER_IDS_TOPIC :
130+ valid_partner_ids = json .loads (payload )
84131 elif msg .topic == REMOTE_PARTNER_TOPIC :
85132 if payload == 'stop' :
86133 if partner_tunnel is None :
@@ -90,22 +137,39 @@ def is_tunnel_closed(tunnel: Popen) -> bool:
90137 partner_tunnel .terminate ()
91138 partner_tunnel .wait (timeout = 3 )
92139 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 ):
94141 if is_tunnel_closed (partner_tunnel ):
95142 splitted = payload .split (";" )
96143 if len (splitted ) != 3 :
97144 log .error ("invalid number of settings received!" )
98145 else :
99146 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 } " )
107170 else :
108171 log .info ("unknown message: " + payload )
172+ clear_topic = True
109173 elif msg .topic == CLOUD_TOPIC :
110174 if payload == 'stop' :
111175 if cloud_tunnel is None :
@@ -125,37 +189,44 @@ def is_tunnel_closed(tunnel: Popen) -> bool:
125189 cloud_node = splitted [1 ]
126190 user = splitted [2 ]
127191
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" ,
144197 "https://" + cloud_node + ".openwb.de/" , "-p" , "80" , "-s" , token ])
145198 log .info (f"cloud tunnel running with pid { cloud_tunnel .pid } " )
146- except FileNotFoundError :
147- log .exception (f"executable '{ lt_executable } ' does not exist!" )
148199 else :
149200 log .info ("unknown message: " + payload )
201+ clear_topic = True
150202 # 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 )
152205
153206
207+ lt_executable = get_lt_executable ()
154208mqtt_broker_host = "localhost"
155209client = mqtt .Client ("openWB-remote-" + get_serial ())
156210client .on_connect = on_connect
157211client .on_message = on_message
212+ client .will_set (STATE_TOPIC , json .dumps ("offline" ), qos = 2 , retain = True )
158213
214+ log .debug ("connecting to broker" )
159215client .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