1
+ /************************************************
2
+ * CONFIGURATION
3
+ ************************************************/
4
+ var CONFIG = {
5
+ FILTERED_BLE_ID : '' , // default BLE ID
6
+ ALLTERCO_MFD_ID : 'a90b' ,
7
+
8
+ MQTT_SERVER : '' ,
9
+ MQTT_CLIENT_ID : '' ,
10
+ MQTT_PREFIX : '' ,
11
+
12
+ URL_CA_BUNDLE : '' ,
13
+ URL_CLIENT_CERT : '' ,
14
+ URL_CLIENT_KEY : '' ,
15
+
16
+ SYS_BTN : 'pair' ,
17
+ VIRTUAL_BTN : 'button:202' , // MQTT deploy button
18
+
19
+ // --- IDs of your virtual UI components ---
20
+ UI_FILTERED_BLE_ID : 'text:200' , // BLE ID
21
+ UI_MQTT_SERVER : 'text:205' , // MQTT Server
22
+ UI_MQTT_CLIENT_ID : 'text:206' , // MQTT Client ID
23
+ UI_MQTT_PREFIX : 'text:207' , // MQTT Prefix
24
+ UI_URL_CA_BUNDLE : 'text:208' , // CA Bundle URL
25
+ UI_URL_CLIENT_CERT : 'text:209' , // Client Cert URL
26
+ UI_URL_CLIENT_KEY : 'text:210' , // Client Key URL
27
+ }
28
+
29
+ var BLE_SCAN = { active : false , duration_ms : 750 , window_ms : 95 , interval_ms : 100 , rssi_thr : - 60 }
30
+
31
+ /************************************************
32
+ * OVERRIDE CONFIG WITH UI FIELDS
33
+ ************************************************/
34
+ var uiBleId = Virtual . getHandle ( CONFIG . UI_FILTERED_BLE_ID )
35
+ var uiServer = Virtual . getHandle ( CONFIG . UI_MQTT_SERVER )
36
+ var uiClientId = Virtual . getHandle ( CONFIG . UI_MQTT_CLIENT_ID )
37
+ var uiPrefix = Virtual . getHandle ( CONFIG . UI_MQTT_PREFIX )
38
+ var uiCaBundle = Virtual . getHandle ( CONFIG . UI_URL_CA_BUNDLE )
39
+ var uiCert = Virtual . getHandle ( CONFIG . UI_URL_CLIENT_CERT )
40
+ var uiKey = Virtual . getHandle ( CONFIG . UI_URL_CLIENT_KEY )
41
+ var uiDeployBtn = Virtual . getHandle ( CONFIG . VIRTUAL_BTN )
42
+
43
+ function refreshConfig ( ) {
44
+ var v
45
+ if ( uiBleId ) {
46
+ v = uiBleId . getValue ( )
47
+ // parse hex or decimal input
48
+ CONFIG . FILTERED_BLE_ID = ( v . indexOf ( '0x' ) === 0 ? parseInt ( v , 16 ) : parseInt ( v , 10 ) ) || CONFIG . FILTERED_BLE_ID
49
+ }
50
+ if ( uiServer && ( v = uiServer . getValue ( ) ) ) CONFIG . MQTT_SERVER = v
51
+ if ( uiClientId && ( v = uiClientId . getValue ( ) ) ) CONFIG . MQTT_CLIENT_ID = v
52
+ if ( uiPrefix && ( v = uiPrefix . getValue ( ) ) ) CONFIG . MQTT_PREFIX = v
53
+ if ( uiCaBundle && ( v = uiCaBundle . getValue ( ) ) ) CONFIG . URL_CA_BUNDLE = v
54
+ if ( uiCert && ( v = uiCert . getValue ( ) ) ) CONFIG . URL_CLIENT_CERT = v
55
+ if ( uiKey && ( v = uiKey . getValue ( ) ) ) CONFIG . URL_CLIENT_KEY = v
56
+
57
+ console . log (
58
+ 'CONFIG ← UI:' ,
59
+ JSON . stringify ( {
60
+ FILTERED_BLE_ID : CONFIG . FILTERED_BLE_ID ,
61
+ MQTT_SERVER : CONFIG . MQTT_SERVER ,
62
+ MQTT_CLIENT_ID : CONFIG . MQTT_CLIENT_ID ,
63
+ MQTT_PREFIX : CONFIG . MQTT_PREFIX ,
64
+ URL_CA_BUNDLE : CONFIG . URL_CA_BUNDLE ,
65
+ URL_CLIENT_CERT : CONFIG . URL_CLIENT_CERT ,
66
+ URL_CLIENT_KEY : CONFIG . URL_CLIENT_KEY ,
67
+ } )
68
+ )
69
+ }
70
+
71
+ /************************************************
72
+ * HTTP GET helper – accepts body / body_b64 / body_base64
73
+ ************************************************/
74
+ function fetch ( url , cb ) {
75
+ console . log ( 'HTTP GET' , url )
76
+ Shelly . call ( 'HTTP.GET' , { url : url , binary : true } , function ( res , ec , em ) {
77
+ if ( ec || ! res || res . code !== 200 ) {
78
+ console . log ( 'HTTP GET failed' , ec , em )
79
+ cb ( null )
80
+ return
81
+ }
82
+
83
+ var txt = null
84
+
85
+ if ( typeof res . body === 'string' ) {
86
+ /* plain text */
87
+ txt = res . body
88
+ console . log ( 'HTTP GET -> body' , txt . length , 'bytes' )
89
+ } else if ( typeof res . body_b64 === 'string' ) {
90
+ txt = atob ( res . body_b64 )
91
+ console . log ( 'HTTP GET -> body_b64' , txt . length , 'bytes' )
92
+ } else if ( typeof res . body_base64 === 'string' ) {
93
+ txt = atob ( res . body_base64 )
94
+ console . log ( 'HTTP GET -> body_base64' , txt . length , 'bytes' )
95
+ } else {
96
+ console . log ( 'HTTP GET unknown payload keys:' , JSON . stringify ( res ) )
97
+ }
98
+
99
+ cb ( txt )
100
+ } )
101
+ }
102
+
103
+ /************************************************
104
+ * upload PEM with progress
105
+ ************************************************/
106
+ function putPem ( addr , method , text , cb ) {
107
+ if ( ! text ) {
108
+ console . log ( method , 'input NULL – abort' )
109
+ cb ( false )
110
+ return
111
+ }
112
+ var lines = text . split ( '\n' ) , i = 0 , bytes = 0
113
+ function next ( app ) {
114
+ var chunk = lines [ i ++ ] + '\n'
115
+ bytes += chunk . length
116
+ if ( i % 10 === 0 || i === lines . length ) console . log ( method , '…' , i , '/' , lines . length )
117
+ Shelly . call (
118
+ 'GATTC.call' ,
119
+ {
120
+ addr : addr ,
121
+ method : method ,
122
+ params : { data : chunk , append : app } ,
123
+ } ,
124
+ function ( r , e , m ) {
125
+ if ( e ) {
126
+ console . log ( method , 'error' , m )
127
+ cb ( false )
128
+ } else if ( i < lines . length ) next ( true )
129
+ else {
130
+ console . log ( method , 'DONE' , bytes , 'bytes' )
131
+ cb ( true )
132
+ }
133
+ }
134
+ )
135
+ }
136
+ next ( false )
137
+ }
138
+
139
+ /************************************************
140
+ * push MQTT config
141
+ ************************************************/
142
+ function mqttConfig ( addr , cb ) {
143
+ var cfg = {
144
+ enable : true ,
145
+ server : CONFIG . MQTT_SERVER ,
146
+ client_id : CONFIG . MQTT_CLIENT_ID ,
147
+ topic_prefix : CONFIG . MQTT_PREFIX ,
148
+ ssl_ca : 'user_ca.pem' ,
149
+ rpc_ntf : true ,
150
+ status_ntf : true ,
151
+ enable_control : true ,
152
+ enable_rpc : true ,
153
+ use_client_cert : true ,
154
+ }
155
+ console . log ( 'MQTT.SetConfig sending…' )
156
+ Shelly . call (
157
+ 'GATTC.call' ,
158
+ {
159
+ addr : addr ,
160
+ method : 'MQTT.SetConfig' ,
161
+ params : { config : cfg } ,
162
+ } ,
163
+ function ( r , e , m ) {
164
+ if ( e ) {
165
+ console . log ( 'MQTT.SetConfig ERROR' , m )
166
+ cb ( false )
167
+ } else {
168
+ console . log ( 'MQTT.SetConfig OK – restart required' )
169
+ cb ( true )
170
+ }
171
+ }
172
+ )
173
+ }
174
+
175
+ /************************************************
176
+ * full deployment chain
177
+ ************************************************/
178
+ function fetchCa ( context ) {
179
+ fetch ( CONFIG . URL_CA_BUNDLE , function ( ca ) {
180
+ if ( ! ca ) {
181
+ console . log ( 'CA download NULL – abort' )
182
+ return
183
+ }
184
+ context . ca = ca
185
+ fetchClientCert ( context )
186
+ } )
187
+ }
188
+
189
+ function fetchClientCert ( context ) {
190
+ fetch ( CONFIG . URL_CLIENT_CERT , function ( cc ) {
191
+ if ( ! cc ) {
192
+ console . log ( 'Client-cert download NULL – abort' )
193
+ return
194
+ }
195
+ context . cc = cc
196
+ fetchClientKey ( context )
197
+ } )
198
+ }
199
+
200
+ function fetchClientKey ( context ) {
201
+ fetch ( CONFIG . URL_CLIENT_KEY , function ( ck ) {
202
+ if ( ! ck ) {
203
+ console . log ( 'Client-key download NULL – abort' )
204
+ return
205
+ }
206
+ context . ck = ck
207
+ putUserCa ( context )
208
+ } )
209
+ }
210
+
211
+ function putUserCa ( context ) {
212
+ putPem ( context . addr , 'Shelly.PutUserCA' , context . ca , function ( ok1 ) {
213
+ if ( ! ok1 ) {
214
+ console . log ( 'CA upload failed – abort' )
215
+ return
216
+ }
217
+ putClientCert ( context )
218
+ } )
219
+ }
220
+
221
+ function putClientCert ( context ) {
222
+ putPem ( context . addr , 'Shelly.PutTLSClientCert' , context . cc , function ( ok2 ) {
223
+ if ( ! ok2 ) {
224
+ console . log ( 'Cert upload failed – abort' )
225
+ return
226
+ }
227
+ putClientKey ( context )
228
+ } )
229
+ }
230
+
231
+ function putClientKey ( context ) {
232
+ putPem ( context . addr , 'Shelly.PutTLSClientKey' , context . ck , function ( ok3 ) {
233
+ if ( ! ok3 ) {
234
+ console . log ( 'Key upload failed – abort' )
235
+ return
236
+ }
237
+ mqttConfigProcess ( context )
238
+ } )
239
+ }
240
+
241
+ function mqttConfigProcess ( context ) {
242
+ console . log ( 'All certificates uploaded OK' )
243
+ mqttConfig ( context . addr , function ( ok4 ) {
244
+ if ( ! ok4 ) {
245
+ console . log ( 'MQTT config failed – abort' )
246
+ return
247
+ }
248
+ reboot ( context )
249
+ } )
250
+ }
251
+
252
+ function reboot ( context ) {
253
+ Shelly . call ( 'GATTC.call' , { addr : context . addr , method : 'Shelly.Reboot' , params : { } } , function ( r , e , m ) {
254
+ if ( e ) {
255
+ console . log ( 'Reboot RPC error' , m )
256
+ return
257
+ }
258
+ console . log ( 'Rebooting … wait 10 s' )
259
+ Timer . set ( 10000 , false , function ( ) {
260
+ Shelly . call ( 'GATTC.call' , { addr : context . addr , method : 'MQTT.GetStatus' , params : { } } , function ( res , ec , em ) {
261
+ if ( ec ) console . log ( 'MQTT.GetStatus error' , em )
262
+ else console . log ( 'MQTT connected?' , res && res . connected )
263
+ } )
264
+ } )
265
+ } )
266
+ }
267
+
268
+ function deploy ( addr ) {
269
+ console . log ( '=== provisioning' , addr , '===' )
270
+ var context = { addr : addr }
271
+ fetchCa ( context )
272
+ }
273
+
274
+ /************************************************
275
+ * BLE scan selecting strongest RSSI
276
+ ************************************************/
277
+ function idFromAdv ( a ) {
278
+ return parseInt ( a . substr ( 22 , 2 ) , 16 ) + ( parseInt ( a . substr ( 24 , 2 ) , 16 ) << 8 )
279
+ }
280
+
281
+ function sortRSSI ( devices ) {
282
+ for ( var i = 0 ; i < devices . length ; i ++ ) {
283
+ for ( var j = i + 1 ; j < devices . length ; j ++ ) {
284
+ if ( devices [ i ] . rssi < devices [ j ] . rssi ) {
285
+ var temp = devices [ i ]
286
+ devices [ i ] = devices [ j ]
287
+ devices [ j ] = temp
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ function scanCb ( res ) {
294
+ if ( ! res || ! Array . isArray ( res . results ) ) {
295
+ console . log ( 'BLE scan invalid' )
296
+ return
297
+ }
298
+ var matchedDevices = res . results . filter ( function ( dev ) {
299
+ return (
300
+ typeof dev . adv_data === 'string' &&
301
+ dev . adv_data . indexOf ( CONFIG . ALLTERCO_MFD_ID ) === 10 &&
302
+ idFromAdv ( dev . adv_data ) === CONFIG . FILTERED_BLE_ID
303
+ )
304
+ } )
305
+ if ( ! matchedDevices . length ) {
306
+ console . log ( 'No matching devices' )
307
+ return
308
+ }
309
+ sortRSSI ( matchedDevices )
310
+ var target = matchedDevices [ 0 ]
311
+ console . log ( 'Target' , target . addr , 'RSSI' , target . rssi )
312
+ deploy ( target . addr )
313
+ }
314
+
315
+ function scan ( ) {
316
+ console . log ( 'BLE scan…' )
317
+ Shelly . call ( 'GATTC.Scan' , BLE_SCAN , scanCb )
318
+ }
319
+
320
+ /************************************************
321
+ * triggers
322
+ ************************************************/
323
+ Shelly . addEventHandler ( function ( ev ) {
324
+ if ( ev . info && ev . info . component === 'sys' && ev . info . event === 'brief_btn_down' && ev . info . name === CONFIG . SYS_BTN ) {
325
+ console . log ( 'System button -> scan' )
326
+ scan ( )
327
+ }
328
+ } )
329
+
330
+ function init ( ) {
331
+ // apply overrides at startup
332
+ refreshConfig ( )
333
+
334
+ // re-apply on any text-field change
335
+ var uiHandles = [ uiBleId , uiServer , uiClientId , uiPrefix , uiCaBundle , uiCert , uiKey ]
336
+ uiHandles . forEach ( function ( h ) {
337
+ if ( h && h . on ) h . on ( 'change' , refreshConfig )
338
+ } )
339
+
340
+ // hook the virtual “MQTT” button
341
+ if ( uiDeployBtn ) {
342
+ uiDeployBtn . on ( 'single_push' , function ( ) {
343
+ console . log ( 'Virtual deploy button pressed → scan' )
344
+ scan ( )
345
+ } )
346
+ }
347
+
348
+ console . log ( 'Ready – press the physical or virtual button to provision MQTT' )
349
+ }
350
+
351
+ init ( )
0 commit comments