1
1
import type { IClientPublishOptions } from 'mqtt' ;
2
2
3
+ import type Extension from './extension/extension' ;
3
4
import type { Zigbee2MQTTAPI } from './types/api' ;
4
5
5
- import assert from 'node:assert' ;
6
-
7
6
import bind from 'bind-decorator' ;
8
7
import stringify from 'json-stable-stringify-without-jsonify' ;
9
8
10
9
import { setLogger as zhSetLogger } from 'zigbee-herdsman' ;
11
10
import { setLogger as zhcSetLogger } from 'zigbee-herdsman-converters' ;
12
11
13
12
import EventBus from './eventBus' ;
13
+ // Extensions
14
14
import ExtensionAvailability from './extension/availability' ;
15
15
import ExtensionBind from './extension/bind' ;
16
16
import ExtensionBridge from './extension/bridge' ;
17
17
import ExtensionConfigure from './extension/configure' ;
18
18
import ExtensionExternalConverters from './extension/externalConverters' ;
19
19
import ExtensionExternalExtensions from './extension/externalExtensions' ;
20
- // Extensions
21
- import ExtensionFrontend from './extension/frontend' ;
22
20
import ExtensionGroups from './extension/groups' ;
23
- import ExtensionHomeAssistant from './extension/homeassistant' ;
24
21
import ExtensionNetworkMap from './extension/networkMap' ;
25
22
import ExtensionOnEvent from './extension/onEvent' ;
26
23
import ExtensionOTAUpdate from './extension/otaUpdate' ;
@@ -34,43 +31,15 @@ import * as settings from './util/settings';
34
31
import utils from './util/utils' ;
35
32
import Zigbee from './zigbee' ;
36
33
37
- const AllExtensions = [
38
- ExtensionPublish ,
39
- ExtensionReceive ,
40
- ExtensionNetworkMap ,
41
- ExtensionHomeAssistant ,
42
- ExtensionConfigure ,
43
- ExtensionBridge ,
44
- ExtensionGroups ,
45
- ExtensionBind ,
46
- ExtensionOnEvent ,
47
- ExtensionOTAUpdate ,
48
- ExtensionExternalConverters ,
49
- ExtensionFrontend ,
50
- ExtensionExternalExtensions ,
51
- ExtensionAvailability ,
52
- ] ;
53
-
54
- type ExtensionArgs = [
55
- Zigbee ,
56
- MQTT ,
57
- State ,
58
- PublishEntityState ,
59
- EventBus ,
60
- enableDisableExtension : ( enable : boolean , name : string ) => Promise < void > ,
61
- restartCallback : ( ) => Promise < void > ,
62
- addExtension : ( extension : Extension ) => Promise < void > ,
63
- ] ;
64
-
65
34
export class Controller {
66
35
private eventBus : EventBus ;
67
36
private zigbee : Zigbee ;
68
37
private state : State ;
69
38
private mqtt : MQTT ;
70
39
private restartCallback : ( ) => Promise < void > ;
71
40
private exitCallback : ( code : number , restart : boolean ) => Promise < void > ;
72
- private extensions : Extension [ ] ;
73
- private extensionArgs : ExtensionArgs ;
41
+ public readonly extensions : Set < Extension > ;
42
+ public readonly extensionArgs : ConstructorParameters < typeof Extension > ;
74
43
private sdNotify : Awaited < ReturnType < typeof initSdNotify > > ;
75
44
76
45
constructor ( restartCallback : ( ) => Promise < void > , exitCallback : ( code : number , restart : boolean ) => Promise < void > ) {
@@ -96,7 +65,7 @@ export class Controller {
96
65
this . addExtension ,
97
66
] ;
98
67
99
- this . extensions = [
68
+ this . extensions = new Set ( [
100
69
new ExtensionExternalConverters ( ...this . extensionArgs ) ,
101
70
new ExtensionOnEvent ( ...this . extensionArgs ) ,
102
71
new ExtensionBridge ( ...this . extensionArgs ) ,
@@ -109,18 +78,22 @@ export class Controller {
109
78
new ExtensionOTAUpdate ( ...this . extensionArgs ) ,
110
79
new ExtensionExternalExtensions ( ...this . extensionArgs ) ,
111
80
new ExtensionAvailability ( ...this . extensionArgs ) ,
112
- ] ;
81
+ ] ) ;
82
+ }
113
83
84
+ async start ( ) : Promise < void > {
114
85
if ( settings . get ( ) . frontend . enabled ) {
115
- this . extensions . push ( new ExtensionFrontend ( ...this . extensionArgs ) ) ;
86
+ const { Frontend} = await import ( './extension/frontend.js' ) ;
87
+
88
+ this . extensions . add ( new Frontend ( ...this . extensionArgs ) ) ;
116
89
}
117
90
118
91
if ( settings . get ( ) . homeassistant . enabled ) {
119
- this . extensions . push ( new ExtensionHomeAssistant ( ...this . extensionArgs ) ) ;
92
+ const { HomeAssistant} = await import ( './extension/homeassistant.js' ) ;
93
+
94
+ this . extensions . add ( new HomeAssistant ( ...this . extensionArgs ) ) ;
120
95
}
121
- }
122
96
123
- async start ( ) : Promise < void > {
124
97
this . state . start ( ) ;
125
98
126
99
const info = await utils . getZigbee2MQTTVersion ( ) ;
@@ -171,8 +144,9 @@ export class Controller {
171
144
return await this . exit ( 1 ) ;
172
145
}
173
146
174
- // Call extensions
175
- await this . callExtensions ( 'start' , this . extensions ) ;
147
+ for ( const extension of this . extensions ) {
148
+ await this . startExtension ( extension ) ;
149
+ }
176
150
177
151
// Send all cached states.
178
152
if ( settings . get ( ) . advanced . cache_state_send_on_startup && settings . get ( ) . advanced . cache_state ) {
@@ -191,31 +165,127 @@ export class Controller {
191
165
}
192
166
193
167
@bind async enableDisableExtension ( enable : boolean , name : string ) : Promise < void > {
194
- if ( ! enable ) {
195
- const extension = this . extensions . find ( ( e ) => e . constructor . name === name ) ;
196
- if ( extension ) {
197
- await this . callExtensions ( 'stop' , [ extension ] ) ;
198
- this . extensions . splice ( this . extensions . indexOf ( extension ) , 1 ) ;
168
+ if ( enable ) {
169
+ switch ( name ) {
170
+ case 'Frontend' : {
171
+ if ( ! settings . get ( ) . frontend . enabled ) {
172
+ throw new Error ( 'Tried to enable Frontend extension disabled in settings' ) ;
173
+ }
174
+
175
+ // this is not actually used, not tested either
176
+ /* v8 ignore start */
177
+ const { Frontend} = await import ( './extension/frontend.js' ) ;
178
+
179
+ await this . addExtension ( new Frontend ( ...this . extensionArgs ) ) ;
180
+
181
+ break ;
182
+ /* v8 ignore stop */
183
+ }
184
+ case 'HomeAssistant' : {
185
+ if ( ! settings . get ( ) . homeassistant . enabled ) {
186
+ throw new Error ( 'Tried to enable HomeAssistant extension disabled in settings' ) ;
187
+ }
188
+
189
+ const { HomeAssistant} = await import ( './extension/homeassistant.js' ) ;
190
+
191
+ await this . addExtension ( new HomeAssistant ( ...this . extensionArgs ) ) ;
192
+
193
+ break ;
194
+ }
195
+ default : {
196
+ throw new Error (
197
+ `Extension ${ name } does not exist (should be added with 'addExtension') or is built-in that cannot be enabled at runtime` ,
198
+ ) ;
199
+ }
199
200
}
200
201
} else {
201
- const Extension = AllExtensions . find ( ( e ) => e . name === name ) ;
202
- assert ( Extension , `Extension '${ name } ' does not exist` ) ;
203
- const extension = new Extension ( ...this . extensionArgs ) ;
204
- this . extensions . push ( extension ) ;
205
- await this . callExtensions ( 'start' , [ extension ] ) ;
202
+ switch ( name ) {
203
+ case 'Frontend' : {
204
+ if ( settings . get ( ) . frontend . enabled ) {
205
+ throw new Error ( 'Tried to disable Frontend extension enabled in settings' ) ;
206
+ }
207
+
208
+ break ;
209
+ }
210
+ case 'HomeAssistant' : {
211
+ if ( settings . get ( ) . homeassistant . enabled ) {
212
+ throw new Error ( 'Tried to disable HomeAssistant extension enabled in settings' ) ;
213
+ }
214
+
215
+ break ;
216
+ }
217
+ case 'Availability' :
218
+ case 'Bind' :
219
+ case 'Bridge' :
220
+ case 'Configure' :
221
+ case 'ExternalConverters' :
222
+ case 'ExternalExtensions' :
223
+ case 'Groups' :
224
+ case 'NetworkMap' :
225
+ case 'OnEvent' :
226
+ case 'OTAUpdate' :
227
+ case 'Publish' :
228
+ case 'Receive' : {
229
+ throw new Error ( `Built-in extension ${ name } cannot be disabled at runtime` ) ;
230
+ }
231
+ }
232
+
233
+ const extension = this . getExtension ( name ) ;
234
+
235
+ if ( extension ) {
236
+ await this . removeExtension ( extension ) ;
237
+ }
238
+ }
239
+ }
240
+
241
+ public getExtension ( name : string ) : Extension | undefined {
242
+ for ( const extension of this . extensions ) {
243
+ if ( extension . constructor . name === name ) {
244
+ return extension ;
245
+ }
206
246
}
207
247
}
208
248
209
249
@bind async addExtension ( extension : Extension ) : Promise < void > {
210
- this . extensions . push ( extension ) ;
211
- await this . callExtensions ( 'start' , [ extension ] ) ;
250
+ for ( const ext of this . extensions ) {
251
+ if ( ext . constructor . name === extension . constructor . name ) {
252
+ throw new Error ( `Extension with name ${ ext . constructor . name } already present` ) ;
253
+ }
254
+ }
255
+
256
+ this . extensions . add ( extension ) ;
257
+ await this . startExtension ( extension ) ;
258
+ }
259
+
260
+ async removeExtension ( extension : Extension ) : Promise < void > {
261
+ if ( this . extensions . delete ( extension ) ) {
262
+ await this . stopExtension ( extension ) ;
263
+ }
264
+ }
265
+
266
+ private async startExtension ( extension : Extension ) : Promise < void > {
267
+ try {
268
+ await extension . start ( ) ;
269
+ } catch ( error ) {
270
+ logger . error ( `Failed to start '${ extension . constructor . name } ' (${ ( error as Error ) . stack } )` ) ;
271
+ }
272
+ }
273
+
274
+ private async stopExtension ( extension : Extension ) : Promise < void > {
275
+ try {
276
+ await extension . stop ( ) ;
277
+ } catch ( error ) {
278
+ logger . error ( `Failed to stop '${ extension . constructor . name } ' (${ ( error as Error ) . stack } )` ) ;
279
+ }
212
280
}
213
281
214
282
async stop ( restart = false ) : Promise < void > {
215
283
this . sdNotify ?. notifyStopping ( ) ;
216
284
217
- // Call extensions
218
- await this . callExtensions ( 'stop' , this . extensions ) ;
285
+ for ( const extension of this . extensions ) {
286
+ await this . stopExtension ( extension ) ;
287
+ }
288
+
219
289
this . eventBus . removeListeners ( this ) ;
220
290
221
291
// Wrap-up
@@ -346,14 +416,4 @@ export class Controller {
346
416
}
347
417
}
348
418
}
349
-
350
- private async callExtensions ( method : 'start' | 'stop' , extensions : Extension [ ] ) : Promise < void > {
351
- for ( const extension of extensions ) {
352
- try {
353
- await extension [ method ] ?.( ) ;
354
- } catch ( error ) {
355
- logger . error ( `Failed to call '${ extension . constructor . name } ' '${ method } ' (${ ( error as Error ) . stack } )` ) ;
356
- }
357
- }
358
- }
359
419
}
0 commit comments