Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Live loading of external JS (converters/extensions) #24764

Merged
merged 29 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b0c527a
feat: Live loading of external JS (converters/extensions)
Nerivec Nov 12, 2024
13e7da4
Fix imports
Nerivec Nov 13, 2024
a0ba986
Improve error message on MQTT save
Nerivec Nov 13, 2024
05d21d0
Handle non-existing base path
Nerivec Nov 14, 2024
b585a05
Throw on bad converter
Nerivec Nov 14, 2024
fa94991
Add tests
Nerivec Nov 14, 2024
2d49547
Fix use of ext conv in network map tests.
Nerivec Nov 14, 2024
5a2c69f
More coverage.
Nerivec Nov 14, 2024
57aac29
Dont mock zhc for basics, tests actual live loading
Nerivec Nov 14, 2024
0d5a8fd
Update
Nerivec Nov 17, 2024
fa39976
Merge branch 'feat/2.0.0' of https://github.com/Nerivec/zigbee2mqtt i…
Nerivec Nov 19, 2024
0914261
feat: Live loading of external JS (converters/extensions)
Nerivec Nov 12, 2024
8d515f6
Fix imports
Nerivec Nov 13, 2024
20b7f6f
Improve error message on MQTT save
Nerivec Nov 13, 2024
f62e0ab
Handle non-existing base path
Nerivec Nov 14, 2024
f13789b
Throw on bad converter
Nerivec Nov 14, 2024
e1ea2e9
Add tests
Nerivec Nov 14, 2024
2f8d45f
Fix use of ext conv in network map tests.
Nerivec Nov 14, 2024
68272ad
More coverage.
Nerivec Nov 14, 2024
6e02520
Dont mock zhc for basics, tests actual live loading
Nerivec Nov 14, 2024
1f483a4
Update
Nerivec Nov 17, 2024
13f0b2c
Fix rebase
Nerivec Nov 19, 2024
0589d40
Merge branch 'rework-external-js' of https://github.com/Nerivec/zigbe…
Nerivec Nov 19, 2024
3e4b327
Fix
Nerivec Nov 19, 2024
2827eac
Bump zhc
Koenkk Nov 21, 2024
690ef48
pretty
Koenkk Nov 21, 2024
d47724e
Merge branch 'feat/2.0.0' into pr/Nerivec/24764-1
Koenkk Nov 22, 2024
77a5f11
fix typing
Koenkk Nov 22, 2024
d7a8cfb
Cleanup `external_converters` setting remnants.
Nerivec Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions lib/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import ExtensionBind from './extension/bind';
import ExtensionBridge from './extension/bridge';
import ExtensionConfigure from './extension/configure';
import ExtensionExternalConverters from './extension/externalConverters';
import ExtensionExternalExtension from './extension/externalExtension';
import ExtensionExternalExtensions from './extension/externalExtensions';
// Extensions
import ExtensionFrontend from './extension/frontend';
import ExtensionGroups from './extension/groups';
Expand Down Expand Up @@ -47,7 +47,7 @@ const AllExtensions = [
ExtensionOTAUpdate,
ExtensionExternalConverters,
ExtensionFrontend,
ExtensionExternalExtension,
ExtensionExternalExtensions,
ExtensionAvailability,
];

Expand Down Expand Up @@ -106,18 +106,15 @@ export class Controller {
new ExtensionGroups(...this.extensionArgs),
new ExtensionBind(...this.extensionArgs),
new ExtensionOTAUpdate(...this.extensionArgs),
new ExtensionExternalExtension(...this.extensionArgs),
new ExtensionExternalExtensions(...this.extensionArgs),
new ExtensionExternalConverters(...this.extensionArgs),
new ExtensionAvailability(...this.extensionArgs),
];

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

if (settings.get().external_converters.length) {
this.extensions.push(new ExtensionExternalConverters(...this.extensionArgs));
}

if (settings.get().homeassistant) {
this.extensions.push(new ExtensionHomeAssistant(...this.extensionArgs));
}
Expand Down
74 changes: 51 additions & 23 deletions lib/extension/externalConverters.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as zhc from 'zigbee-herdsman-converters';
import type * as zhc from 'zigbee-herdsman-converters';

import {addDefinition, removeExternalDefinitions} from 'zigbee-herdsman-converters';

import logger from '../util/logger';
import * as settings from '../util/settings';
import {loadExternalConverter} from '../util/utils';
import Extension from './extension';
import ExternalJSExtension from './externalJS';

type ModuleExports = zhc.Definition | zhc.Definition[];

export default class ExternalConverters extends Extension {
export default class ExternalConverters extends ExternalJSExtension<ModuleExports> {
constructor(
zigbee: Zigbee,
mqtt: MQTT,
Expand All @@ -16,25 +18,51 @@ export default class ExternalConverters extends Extension {
restartCallback: () => Promise<void>,
addExtension: (extension: Extension) => Promise<void>,
) {
super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);

for (const file of settings.get().external_converters) {
try {
for (const definition of loadExternalConverter(file)) {
zhc.addDefinition(definition);
}
logger.info(`Loaded external converter '${file}'`);
} catch (error) {
logger.error(`Failed to load external converter file '${file}' (${(error as Error).message})`);
logger.error(
`Probably there is a syntax error in the file or the external converter is not ` +
`compatible with the current Zigbee2MQTT version`,
);
logger.error(
`Note that external converters are not meant for long term usage, it's meant for local ` +
`testing after which a pull request should be created to add out-of-the-box support for the device`,
);
super(
zigbee,
mqtt,
state,
publishEntityState,
eventBus,
enableDisableExtension,
restartCallback,
addExtension,
'converter',
'external_converters',
);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected async removeJS(name: string, module: ModuleExports): Promise<void> {
removeExternalDefinitions(name);

await this.zigbee.resolveDevicesDefinitions(true);
}

protected async loadJS(name: string, module: ModuleExports): Promise<void> {
try {
removeExternalDefinitions(name);

for (const definition of this.getDefinitions(module)) {
definition.externalConverterName = name;

addDefinition(definition);
logger.info(`Loaded external converter '${name}'.`);
}

await this.zigbee.resolveDevicesDefinitions(true);
} catch (error) {
logger.error(`Failed to load external converter '${name}'`);
logger.error(`Check the code for syntax error and make sure it is up to date with the current Zigbee2MQTT version.`);
logger.error(
`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`,
);

throw error;
}
}

private getDefinitions(module: ModuleExports): zhc.Definition[] {
return Array.isArray(module) ? module : [module];
}
}
120 changes: 0 additions & 120 deletions lib/extension/externalExtension.ts

This file was deleted.

59 changes: 59 additions & 0 deletions lib/extension/externalExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type Extension from './extension';

import logger from '../util/logger';
import * as settings from '../util/settings';
import ExternalJSExtension from './externalJS';

type ModuleExports = typeof Extension;

export default class ExternalExtensions extends ExternalJSExtension<ModuleExports> {
constructor(
zigbee: Zigbee,
mqtt: MQTT,
state: State,
publishEntityState: PublishEntityState,
eventBus: EventBus,
enableDisableExtension: (enable: boolean, name: string) => Promise<void>,
restartCallback: () => Promise<void>,
addExtension: (extension: Extension) => Promise<void>,
) {
super(
zigbee,
mqtt,
state,
publishEntityState,
eventBus,
enableDisableExtension,
restartCallback,
addExtension,
'extension',
'external_extensions',
);
}

protected async removeJS(name: string, module: ModuleExports): Promise<void> {
await this.enableDisableExtension(false, module.name);
}

protected async loadJS(name: string, module: ModuleExports): Promise<void> {
// stop if already started
await this.enableDisableExtension(false, module.name);
await this.addExtension(
// @ts-expect-error `module` is the interface, not the actual passed class
new module(
this.zigbee,
this.mqtt,
this.state,
this.publishEntityState,
this.eventBus,
this.enableDisableExtension,
this.restartCallback,
this.addExtension,
settings,
logger,
),
);

logger.info(`Loaded external extension '${name}'.`);
}
}
Loading