Skip to content

Commit db578d9

Browse files
authored
fix: Use dynamic import for optional extensions (#26735)
1 parent 8d2ef6d commit db578d9

25 files changed

+269
-144
lines changed

lib/controller.ts

+127-67
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
import type {IClientPublishOptions} from 'mqtt';
22

3+
import type Extension from './extension/extension';
34
import type {Zigbee2MQTTAPI} from './types/api';
45

5-
import assert from 'node:assert';
6-
76
import bind from 'bind-decorator';
87
import stringify from 'json-stable-stringify-without-jsonify';
98

109
import {setLogger as zhSetLogger} from 'zigbee-herdsman';
1110
import {setLogger as zhcSetLogger} from 'zigbee-herdsman-converters';
1211

1312
import EventBus from './eventBus';
13+
// Extensions
1414
import ExtensionAvailability from './extension/availability';
1515
import ExtensionBind from './extension/bind';
1616
import ExtensionBridge from './extension/bridge';
1717
import ExtensionConfigure from './extension/configure';
1818
import ExtensionExternalConverters from './extension/externalConverters';
1919
import ExtensionExternalExtensions from './extension/externalExtensions';
20-
// Extensions
21-
import ExtensionFrontend from './extension/frontend';
2220
import ExtensionGroups from './extension/groups';
23-
import ExtensionHomeAssistant from './extension/homeassistant';
2421
import ExtensionNetworkMap from './extension/networkMap';
2522
import ExtensionOnEvent from './extension/onEvent';
2623
import ExtensionOTAUpdate from './extension/otaUpdate';
@@ -34,43 +31,15 @@ import * as settings from './util/settings';
3431
import utils from './util/utils';
3532
import Zigbee from './zigbee';
3633

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-
6534
export class Controller {
6635
private eventBus: EventBus;
6736
private zigbee: Zigbee;
6837
private state: State;
6938
private mqtt: MQTT;
7039
private restartCallback: () => Promise<void>;
7140
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>;
7443
private sdNotify: Awaited<ReturnType<typeof initSdNotify>>;
7544

7645
constructor(restartCallback: () => Promise<void>, exitCallback: (code: number, restart: boolean) => Promise<void>) {
@@ -96,7 +65,7 @@ export class Controller {
9665
this.addExtension,
9766
];
9867

99-
this.extensions = [
68+
this.extensions = new Set([
10069
new ExtensionExternalConverters(...this.extensionArgs),
10170
new ExtensionOnEvent(...this.extensionArgs),
10271
new ExtensionBridge(...this.extensionArgs),
@@ -109,18 +78,22 @@ export class Controller {
10978
new ExtensionOTAUpdate(...this.extensionArgs),
11079
new ExtensionExternalExtensions(...this.extensionArgs),
11180
new ExtensionAvailability(...this.extensionArgs),
112-
];
81+
]);
82+
}
11383

84+
async start(): Promise<void> {
11485
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));
11689
}
11790

11891
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));
12095
}
121-
}
12296

123-
async start(): Promise<void> {
12497
this.state.start();
12598

12699
const info = await utils.getZigbee2MQTTVersion();
@@ -171,8 +144,9 @@ export class Controller {
171144
return await this.exit(1);
172145
}
173146

174-
// Call extensions
175-
await this.callExtensions('start', this.extensions);
147+
for (const extension of this.extensions) {
148+
await this.startExtension(extension);
149+
}
176150

177151
// Send all cached states.
178152
if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
@@ -191,31 +165,127 @@ export class Controller {
191165
}
192166

193167
@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+
}
199200
}
200201
} 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+
}
206246
}
207247
}
208248

209249
@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+
}
212280
}
213281

214282
async stop(restart = false): Promise<void> {
215283
this.sdNotify?.notifyStopping();
216284

217-
// Call extensions
218-
await this.callExtensions('stop', this.extensions);
285+
for (const extension of this.extensions) {
286+
await this.stopExtension(extension);
287+
}
288+
219289
this.eventBus.removeListeners(this);
220290

221291
// Wrap-up
@@ -346,14 +416,4 @@ export class Controller {
346416
}
347417
}
348418
}
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-
}
359419
}

lib/extension/availability.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Zigbee2MQTTAPI} from 'lib/types/api';
1+
import type {Zigbee2MQTTAPI} from '../types/api';
22

33
import assert from 'node:assert';
44

lib/extension/bind.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from 'lib/types/api';
1+
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from '../types/api';
22

33
import assert from 'node:assert';
44

lib/extension/bridge.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from 'lib/types/api';
1+
import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from '../types/api';
22

33
import fs from 'node:fs';
44

lib/extension/configure.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Zigbee2MQTTAPI} from 'lib/types/api';
1+
import type {Zigbee2MQTTAPI} from '../types/api';
22

33
import bind from 'bind-decorator';
44
import stringify from 'json-stable-stringify-without-jsonify';

lib/extension/externalJS.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from 'lib/types/api';
1+
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from '../types/api';
22

33
import fs from 'node:fs';
44
import path from 'node:path';

lib/extension/frontend.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Extension from './extension';
2727
/**
2828
* This extension servers the frontend
2929
*/
30-
export default class Frontend extends Extension {
30+
export class Frontend extends Extension {
3131
private mqttBaseTopic: string;
3232
private host: string | undefined;
3333
private port: number;
@@ -225,3 +225,5 @@ export default class Frontend extends Extension {
225225
}
226226
}
227227
}
228+
229+
export default Frontend;

lib/extension/groups.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from 'lib/types/api';
1+
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from '../types/api';
22

33
import assert from 'node:assert';
44

lib/extension/homeassistant.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ class Bridge {
383383
/**
384384
* This extensions handles integration with HomeAssistant
385385
*/
386-
export default class HomeAssistant extends Extension {
386+
export class HomeAssistant extends Extension {
387387
private discovered: {[s: string]: Discovered} = {};
388388
private discoveryTopic: string;
389389
private discoveryRegex: RegExp;
@@ -2150,3 +2150,5 @@ export default class HomeAssistant extends Extension {
21502150
return value_template;
21512151
}
21522152
}
2153+
2154+
export default HomeAssistant;

lib/extension/networkMap.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from 'lib/types/api';
1+
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from '../types/api';
22

33
import bind from 'bind-decorator';
44
import stringify from 'json-stable-stringify-without-jsonify';

lib/extension/otaUpdate.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type {Zigbee2MQTTAPI} from 'lib/types/api';
21
import type {Ota} from 'zigbee-herdsman-converters';
32

3+
import type {Zigbee2MQTTAPI} from '../types/api';
4+
45
import assert from 'node:assert';
56
import path from 'node:path';
67

lib/types/types.d.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import type TypeEventBus from 'lib/eventBus';
2-
import type TypeExtension from 'lib/extension/extension';
3-
import type TypeDevice from 'lib/model/device';
4-
import type TypeGroup from 'lib/model/group';
5-
import type TypeMQTT from 'lib/mqtt';
6-
import type TypeState from 'lib/state';
7-
import type TypeZigbee from 'lib/zigbee';
81
import type {AdapterTypes as ZHAdapterTypes, Events as ZHEvents, Models as ZHModels} from 'zigbee-herdsman';
92
import type {Cluster as ZHCluster, FrameControl as ZHFrameControl} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
103

11-
import {LogLevel} from 'lib/util/settings';
4+
import type TypeEventBus from '../eventBus';
5+
import type TypeExtension from '../extension/extension';
6+
import type TypeDevice from '../model/device';
7+
import type TypeGroup from '../model/group';
8+
import type TypeMQTT from '../mqtt';
9+
import type TypeState from '../state';
10+
import type TypeZigbee from '../zigbee';
11+
12+
import {LogLevel} from '../util/settings';
1213

1314
type OptionalProps<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
1415

0 commit comments

Comments
 (0)