Skip to content

Commit e3be0a4

Browse files
NerivecKoenkk
andauthored
feat: Live loading of external JS (converters/extensions) (#24764)
* feat: Live loading of external JS (converters/extensions) * Fix imports * Improve error message on MQTT save * Handle non-existing base path * Throw on bad converter * Add tests * Fix use of ext conv in network map tests. * More coverage. * Dont mock zhc for basics, tests actual live loading * Update * feat: Live loading of external JS (converters/extensions) * Fix imports * Improve error message on MQTT save * Handle non-existing base path * Throw on bad converter * Add tests * Fix use of ext conv in network map tests. * More coverage. * Dont mock zhc for basics, tests actual live loading * Update * Fix rebase * Fix * Bump zhc * pretty * fix typing * Cleanup `external_converters` setting remnants. --------- Co-authored-by: Koen Kanters <[email protected]>
1 parent 9be124f commit e3be0a4

22 files changed

+750
-477
lines changed

lib/controller.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import ExtensionBind from './extension/bind';
1515
import ExtensionBridge from './extension/bridge';
1616
import ExtensionConfigure from './extension/configure';
1717
import ExtensionExternalConverters from './extension/externalConverters';
18-
import ExtensionExternalExtension from './extension/externalExtension';
18+
import ExtensionExternalExtensions from './extension/externalExtensions';
1919
// Extensions
2020
import ExtensionFrontend from './extension/frontend';
2121
import ExtensionGroups from './extension/groups';
@@ -47,7 +47,7 @@ const AllExtensions = [
4747
ExtensionOTAUpdate,
4848
ExtensionExternalConverters,
4949
ExtensionFrontend,
50-
ExtensionExternalExtension,
50+
ExtensionExternalExtensions,
5151
ExtensionAvailability,
5252
];
5353

@@ -106,18 +106,15 @@ export class Controller {
106106
new ExtensionGroups(...this.extensionArgs),
107107
new ExtensionBind(...this.extensionArgs),
108108
new ExtensionOTAUpdate(...this.extensionArgs),
109-
new ExtensionExternalExtension(...this.extensionArgs),
109+
new ExtensionExternalExtensions(...this.extensionArgs),
110+
new ExtensionExternalConverters(...this.extensionArgs),
110111
new ExtensionAvailability(...this.extensionArgs),
111112
];
112113

113114
if (settings.get().frontend) {
114115
this.extensions.push(new ExtensionFrontend(...this.extensionArgs));
115116
}
116117

117-
if (settings.get().external_converters.length) {
118-
this.extensions.push(new ExtensionExternalConverters(...this.extensionArgs));
119-
}
120-
121118
if (settings.get().homeassistant) {
122119
this.extensions.push(new ExtensionHomeAssistant(...this.extensionArgs));
123120
}

lib/extension/externalConverters.ts

+51-23
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import * as zhc from 'zigbee-herdsman-converters';
1+
import type * as zhc from 'zigbee-herdsman-converters';
2+
3+
import {addDefinition, removeExternalDefinitions} from 'zigbee-herdsman-converters';
24

35
import logger from '../util/logger';
4-
import * as settings from '../util/settings';
5-
import {loadExternalConverter} from '../util/utils';
6-
import Extension from './extension';
6+
import ExternalJSExtension from './externalJS';
7+
8+
type ModuleExports = zhc.Definition | zhc.Definition[];
79

8-
export default class ExternalConverters extends Extension {
10+
export default class ExternalConverters extends ExternalJSExtension<ModuleExports> {
911
constructor(
1012
zigbee: Zigbee,
1113
mqtt: MQTT,
@@ -16,25 +18,51 @@ export default class ExternalConverters extends Extension {
1618
restartCallback: () => Promise<void>,
1719
addExtension: (extension: Extension) => Promise<void>,
1820
) {
19-
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
20-
21-
for (const file of settings.get().external_converters) {
22-
try {
23-
for (const definition of loadExternalConverter(file)) {
24-
zhc.addDefinition(definition);
25-
}
26-
logger.info(`Loaded external converter '${file}'`);
27-
} catch (error) {
28-
logger.error(`Failed to load external converter file '${file}' (${(error as Error).message})`);
29-
logger.error(
30-
`Probably there is a syntax error in the file or the external converter is not ` +
31-
`compatible with the current Zigbee2MQTT version`,
32-
);
33-
logger.error(
34-
`Note that external converters are not meant for long term usage, it's meant for local ` +
35-
`testing after which a pull request should be created to add out-of-the-box support for the device`,
36-
);
21+
super(
22+
zigbee,
23+
mqtt,
24+
state,
25+
publishEntityState,
26+
eventBus,
27+
enableDisableExtension,
28+
restartCallback,
29+
addExtension,
30+
'converter',
31+
'external_converters',
32+
);
33+
}
34+
35+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
36+
protected async removeJS(name: string, module: ModuleExports): Promise<void> {
37+
removeExternalDefinitions(name);
38+
39+
await this.zigbee.resolveDevicesDefinitions(true);
40+
}
41+
42+
protected async loadJS(name: string, module: ModuleExports): Promise<void> {
43+
try {
44+
removeExternalDefinitions(name);
45+
46+
for (const definition of this.getDefinitions(module)) {
47+
definition.externalConverterName = name;
48+
49+
addDefinition(definition);
50+
logger.info(`Loaded external converter '${name}'.`);
3751
}
52+
53+
await this.zigbee.resolveDevicesDefinitions(true);
54+
} catch (error) {
55+
logger.error(`Failed to load external converter '${name}'`);
56+
logger.error(`Check the code for syntax error and make sure it is up to date with the current Zigbee2MQTT version.`);
57+
logger.error(
58+
`External converters are not meant for long term usage, but for local testing after which a pull request should be created to add out-of-the-box support for the device`,
59+
);
60+
61+
throw error;
3862
}
3963
}
64+
65+
private getDefinitions(module: ModuleExports): zhc.Definition[] {
66+
return Array.isArray(module) ? module : [module];
67+
}
4068
}

lib/extension/externalExtension.ts

-120
This file was deleted.

lib/extension/externalExtensions.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type Extension from './extension';
2+
3+
import logger from '../util/logger';
4+
import * as settings from '../util/settings';
5+
import ExternalJSExtension from './externalJS';
6+
7+
type ModuleExports = typeof Extension;
8+
9+
export default class ExternalExtensions extends ExternalJSExtension<ModuleExports> {
10+
constructor(
11+
zigbee: Zigbee,
12+
mqtt: MQTT,
13+
state: State,
14+
publishEntityState: PublishEntityState,
15+
eventBus: EventBus,
16+
enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
17+
restartCallback: () => Promise<void>,
18+
addExtension: (extension: Extension) => Promise<void>,
19+
) {
20+
super(
21+
zigbee,
22+
mqtt,
23+
state,
24+
publishEntityState,
25+
eventBus,
26+
enableDisableExtension,
27+
restartCallback,
28+
addExtension,
29+
'extension',
30+
'external_extensions',
31+
);
32+
}
33+
34+
protected async removeJS(name: string, module: ModuleExports): Promise<void> {
35+
await this.enableDisableExtension(false, module.name);
36+
}
37+
38+
protected async loadJS(name: string, module: ModuleExports): Promise<void> {
39+
// stop if already started
40+
await this.enableDisableExtension(false, module.name);
41+
await this.addExtension(
42+
// @ts-expect-error `module` is the interface, not the actual passed class
43+
new module(
44+
this.zigbee,
45+
this.mqtt,
46+
this.state,
47+
this.publishEntityState,
48+
this.eventBus,
49+
this.enableDisableExtension,
50+
this.restartCallback,
51+
this.addExtension,
52+
settings,
53+
logger,
54+
),
55+
);
56+
57+
logger.info(`Loaded external extension '${name}'.`);
58+
}
59+
}

0 commit comments

Comments
 (0)