Skip to content

Commit

Permalink
Merge pull request #4 from grrowl/monaco-editor
Browse files Browse the repository at this point in the history
Add Monaco editor
  • Loading branch information
grrowl authored May 13, 2024
2 parents e5c01b8 + 4995774 commit d041a29
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 35 deletions.
39 changes: 7 additions & 32 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"pluginAlias": "HomebridgeAutomation",
"pluginType": "platform",
"singular": true,
"customUi": true,
"headerDisplay": "Homebridge Automation settings",
"footerDisplay": "",
"schema": {
Expand All @@ -10,9 +11,12 @@
"automationJs": {
"title": "Automation script",
"type": "string",
"condition": {
"functionBody": "return false;"
},
"required": false,
"placeholder": "automation.listen(function (event) { return; })",
"default": "automation.listen(function (event) { return; })",
"default": "automation.listen(function (event) {\n\treturn;\n})",
"description": "Javascript function. Use automation.listen() to define your callback to run on every device status change. Do not enter untrusted code, as any code entered here will run on your Homebridge instance and is not isolated or sandboxed."
},
"pin": {
Expand All @@ -30,47 +34,18 @@
"default": false
},
"remoteHost": {
"title": "Remote Control host",
"title": "Remote Control Host",
"type": "string",
"required": false,
"default": "",
"description": "Can be overridden by env var UPSTREAM_API (does not apply if enabled is not set)"
"description": "Can be overridden by env var UPSTREAM_API (does not apply if remoteEnabled is not set)"
},
"apiKey": {
"title": "Remote Control API Key",
"type": "string",
"required": false,
"default": "",
"description": ""
},
"disabled_modes": {
"condition": {
"functionBody": "false"
},
"description": "(Not yet in use - filter updates by accessory type)",
"title": "Disabled Accessory Types",
"type": "array",
"uniqueItems": true,
"items": {
"title": "Disabled Types",
"type": "string",
"enum": [
"BatteryService",
"InputSource",
"Lightbulb",
"LightSensor",
"MotionSensor",
"Outlet",
"ProtocolInformation",
"ServiceLabel",
"Speaker",
"StatelessProgrammableSwitch",
"Switch",
"Television",
"TemperatureSensor",
"Thermostat"
]
}
}
}
}
Expand Down
192 changes: 192 additions & 0 deletions homebridge-ui/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/codicon.min.css"
rel="stylesheet"
/>

<div>
<h1>Homebridge Automation</h1>

<div id="automationJsEditor" style="min-height: 100px">
Loading script editor...
</div>
</div>

<script type="module">
// Lazy loaded in initMonaco()
// import * as monaco from "https://cdn.jsdelivr.net/npm/[email protected]/+esm";

// FIXME: Not working due to CORS issues
self.MonacoEnvironment = {
getWorkerUrl: function (moduleUrl) {
return `https://cdn.jsdelivr.net/npm/${moduleUrl}+esm`;
},
getWorker: function (_, label) {
const getWorkerModule = (moduleUrl, label) => {
return new Worker(self.MonacoEnvironment.getWorkerUrl(moduleUrl), {
name: label,
type: "module",
});
};

switch (label) {
case "json":
return getWorkerModule(
"[email protected]/esm/vs/language/json/json.worker?worker",
label,
);
case "css":
case "scss":
case "less":
return getWorkerModule(
"[email protected]/esm/vs/language/css/css.worker?worker",
label,
);
case "html":
case "handlebars":
case "razor":
return getWorkerModule(
"[email protected]/esm/vs/language/html/html.worker?worker",
label,
);
case "typescript":
case "javascript":
return getWorkerModule(
"[email protected]/esm/vs/language/typescript/ts.worker?worker",
label,
);
default:
return getWorkerModule(
"[email protected]/esm/vs/editor/editor.worker?worker",
label,
);
}
},
};

(async () => {
const pluginConfig = await homebridge.getPluginConfig();
const pluginSchema = await homebridge.getPluginConfigSchema();

if (pluginConfig.length === 0) {
pluginConfig[0] = getDefaults(pluginSchema);
}

// note to pass the whole pluginConfig array to maintain the reference

initForm(pluginConfig, pluginSchema);
initMonaco(pluginConfig, pluginSchema);

// for some reason this is necessary when no config yet
homebridge.updatePluginConfig(pluginConfig);
})();

function getDefaults(pluginConfig) {
const defaults = {};

function extractDefaults(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === "object" && obj[key] !== null) {
extractDefaults(obj[key]);
} else if (key === "default") {
defaults[obj.title] = obj[key];
}
}
}
}

extractDefaults(pluginConfig.schema.properties);

return defaults;
}

function initForm(pluginConfig, _pluginSchema) {
homebridge.showSchemaForm();

// listen for built-in form schema changes
window.homebridge.addEventListener("configChanged", (event) => {
console.log("Updated config:", event.data);

// pluginConfig[0].automationJs = event.data.automationJs
pluginConfig[0].pin = event.data.pin;
pluginConfig[0].remoteEnabled = event.data.remoteEnabled;
pluginConfig[0].remoteHost = event.data.remoteHost;
pluginConfig[0].apiKey = event.data.apiKey;

homebridge.updatePluginConfig(pluginConfig);
});
}

async function initMonaco(pluginConfig, pluginSchema) {
const monaco = await import(
"https://cdn.jsdelivr.net/npm/[email protected]/+esm"
);

const automationJs =
pluginConfig[0].automationJs ||
pluginSchema.schema.properties.automationJs.default;

// validation settings
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: false,
});

// compiler options
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2015,
allowNonTsExtensions: true,
});

// add automation environment types
// FIXME: Not working due to CORS / can't load Typescript Worker
monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource);

const container = document.getElementById("automationJsEditor");
container.innerHTML = "";

// https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.IStandaloneEditorConstructionOptions.html
const editor = monaco.editor.create(container, {
value: automationJs,
language: "javascript",
minimap: {
enabled: false,
},
stickyScroll: {
enabled: false,
},
});

// autolayout or content sizing is buggy in iframe
editor.layout({
width: 760,
height: 300,
});

editor.onDidChangeModelContent((event) => {
pluginConfig[0].automationJs = editor.getValue();
homebridge.updatePluginConfig(pluginConfig);
});
}

const libSource = `
interface ServiceCharacteristic {
iid: number;
serviceName: string;
value: string | number | boolean;
}
interface ListenEvent {
serviceName: string;
serviceCharacteristics: ServiceCharacteristic[];
uniqueId: string;
}
interface Automation {
listen(callback: (event: ListenEvent) => void | string | number | boolean | null): void;
set(uniqueId: string, iid: number, value: unknown): void;
}
declare const automation: Automation;
`;
</script>
15 changes: 13 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Homebridge Automation",
"name": "homebridge-automation",
"version": "0.2.0",
"version": "1.0.0-dev.0",
"description": "Command and automate your home using Javascript.",
"license": "Apache-2.0",
"homepage": "https://tommckenzie.dev/homebridge-automation",
Expand Down Expand Up @@ -32,6 +32,7 @@
"automation"
],
"dependencies": {
"@homebridge/plugin-ui-utils": "^1.0.3",
"@oznu/hap-client": "^1.9.0",
"ws": "^8.13.0",
"zod": "^3.21.4"
Expand Down

0 comments on commit d041a29

Please sign in to comment.