diff --git a/README.md b/README.md index ba89ba5..15c927e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,40 @@ # homebridge-automation -Control your homebridge instance with Javascript functions. +Control your homebridge instance with Javascript. + +## Examples + +(sorry this is so gnarly, we can improve the interface in `PLATFORM_SCRIPT`) + +```js +function onMessage(event) { + if (event.type === "Lightbulb") { + } + if (event.name === "Book Globe") { + } + + if (event.name === "Motion Sensor") { + if ( + event.serviceCharacteristics.find( + (c) => c.name === "Active" && c.value === true, + ) + ) { + const light = automation.services.find((s) => s.name === "Book Globe"); + if (light) { + const on = light.serviceCharacteristics.find((s) => s.type === "On"); + if (on) { + automation.set(light.uniqueId, on.iid, 1); + } + } + automation.set(); + } + } +} +``` + +## API + +See `schemas/Service.ts` and `schemas/Characteristic.ts` ## Local testing diff --git a/config.schema.json b/config.schema.json index 59af389..05ae7a7 100644 --- a/config.schema.json +++ b/config.schema.json @@ -11,9 +11,9 @@ "title": "Automation JS", "type": "string", "required": false, - "placeholder": "function (event, act) {}", - "default": "function (event, act) {}", - "description": "Function to run on every change" + "placeholder": "function onMessage(event) {}", + "default": "function onMessage(event) {}", + "description": "Function to run on every change, must be called onEvent" }, "pin": { "title": "Homebridge PIN", diff --git a/package.json b/package.json index be488ce..54ce251 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "displayName": "Homebridge Automation", "name": "homebridge-automation", "version": "0.1.0", - "description": "Command and automate your home using natural language from anywhere.", + "description": "Command and automate your home using javascript.", "license": "Apache-2.0", "homepage": "https://tommckenzie.dev/homebridge-automation", "repository": { diff --git a/src/platform.ts b/src/platform.ts index 431117e..63ee606 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -32,10 +32,27 @@ const METRIC_DEBOUNCE = 1_500; // only reset reconnection backoff once the connection is open for 5 seconds const CONNECTION_RESET_DELAY = 5_000; +const VM_MEMORY_LIMIT_MB = 128; + const PLATFORM_SCRIPT = ` const automation = { + services: []; + + handleMessage(message) { + if (message.type === "deviceStatusChange") { + onMessage(message.data); + const service = automation.services.find(s => s.uniqueId === message.data.uniqueId); + if (service) { + Object.assign(service, message.data); + } + } + if (message.type === "deviceList") { + automation.services = message.data; + } + } + set(serviceId, iid, value) { - isolate.postMessage(JSON.stringify({ + __host({ version: 1, type: "SetCharacteristic", data: { @@ -43,7 +60,7 @@ const automation = { iid, value, }, - })); + }); } } `; @@ -80,38 +97,9 @@ export class HomebridgeAutomation implements DynamicPlatformPlugin { } if (this.config.automationJs) { - this.isolate = new ivm.Isolate({ memoryLimit: 128 }); - this.context = this.isolate.createContextSync({}); - const contextGlobal = this.context.global; - // contextGlobal.setSync("global", contextGlobal.derefInto()); - - const platformScript = this.isolate.compileScriptSync(PLATFORM_SCRIPT); - platformScript.runSync(this.context); - - const script = this.isolate.compileScriptSync(this.config.automationJs); - script.runSync(this.context); - - // contextGlobal.getSync("global").on("message", (msg) => {}); - - // this.context.evalClosureSync - // this.isolate.setMicrotask(async () => { - // const message = await this.isolate.receiveMessage(); - // console.log(message); // Output: "Hello from the isolated environment!" - // }); - - // Compile and run a script in the context of the isolate - const eventScript = this.isolate.compileScriptSync(` - function myFunction() { - return 'Hello from isolated environment!'; - } - myFunction(); - `); - script.runSync(this.context); - - const result = contextGlobal.getSync("foo"); - if (result) { - this.log.debug(`Automation result:\n${JSON.stringify(result)}`); - } + this.initVm().catch((error) => { + this.log.error("Error initializing Automation VM", error); + }); } // Connect to Homebridge in insecure mode @@ -160,6 +148,57 @@ export class HomebridgeAutomation implements DynamicPlatformPlugin { }, SEND_STATE_DELAY); } + // do this async so we don't hold up Homebridge + async initVm(): Promise { + this.isolate = new ivm.Isolate({ memoryLimit: VM_MEMORY_LIMIT_MB }); + this.context = await this.isolate.createContext({}); + const jail = this.context.global; + + await jail.set("global", jail.derefInto()); + await jail.set( + "__host", + new ivm.Reference((data) => { + this.handleVm(data); + }), + ); + + const platformScript = await this.isolate.compileScript(PLATFORM_SCRIPT); + await platformScript.run(this.context); + + const script = await this.isolate.compileScript(this.config.automationJs); + await script.run(this.context); + + const result = await jail.get("foo"); + if (result) { + this.log.debug(`Automation result:\n${JSON.stringify(result)}`); + } + } + + handleVm(data: unknown) { + const messageParse = ServerMessageSchema.safeParse(data); + if (!messageParse.success) { + this.log.error( + `Invalid message from VM`, + // messageParse.error, + ); + this.incrementMetric("invalidServerMessages"); + return; + } + this.handleMessage(messageParse.data); + } + + async invokeVm(message: ClientMessage) { + if (!this.context || !this.isolate) { + return; + } + + const result = await this.context.evalClosure( + `automation.handleMessage($0);`, + [message], + ); + this.log.debug(`Automation result:\n${JSON.stringify(result)}`); + } + connectSocket(): void { const wsAddress = new URL( UPSTREAM_API ? String(UPSTREAM_API) : this.config.remoteHost, @@ -202,12 +241,9 @@ export class HomebridgeAutomation implements DynamicPlatformPlugin { const messageParse = ServerMessageSchema.safeParse(raw); if (!messageParse.success) { this.log.error( - `Invalid ServerMessage -- please update homebridge-ai to the latest version`, + `Invalid ServerMessage`, // messageParse.error, ); - this.log.warn( - `→ https://homebridgeai.com/help/invalid-servermessage`, - ); this.incrementMetric("invalidServerMessages"); return; } @@ -329,11 +365,12 @@ export class HomebridgeAutomation implements DynamicPlatformPlugin { version: 1, ...message, }; - const json = JSON.stringify(versionedMessage); if (this.config.automationJs) { + this.invokeVm(versionedMessage as any); } if (this.config.remoteEnabled) { + const json = JSON.stringify(versionedMessage); if (this.socketReady) { this.socket?.send(json); } else {