Skip to content

Commit

Permalink
message interface between vm and host
Browse files Browse the repository at this point in the history
  • Loading branch information
grrowl committed Jan 25, 2024
1 parent 0c6fc91 commit 89daeac
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 43 deletions.
36 changes: 35 additions & 1 deletion 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

Expand Down
6 changes: 3 additions & 3 deletions config.schema.json
Expand Up @@ -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",
Expand Down
115 changes: 76 additions & 39 deletions src/platform.ts
Expand Up @@ -32,18 +32,35 @@ 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: {
serviceId,
iid,
value,
},
}));
});
}
}
`;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<void> {
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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 89daeac

Please sign in to comment.