commit 52f9b6bd6ae4477db6826b290016b67c6a56b3dd Author: Kuba Orlik Date: Sat Dec 30 13:47:05 2023 +0100 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d4cd95 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Use any Home Assistant lights with Hyperion + +## Setup: + +1. Clone this respository; + +2. Edit `config.js` to your liking. There are two light types: `rgb` and + `dim`. The names of the lights must represent light entity id that are + present in Home Assistant; + +3. Generate a Home Assistant Token using the /profile view (scroll to the bottom + of the view); +4. Setup a new LED instance in Hyperion using the `udpraw` controller. Set the + number of lights to the amount of lights you've specified in the `config.js` + file; +5. Run `HA_TOKEN=your_token12341234123432 node .` diff --git a/config.js b/config.js new file mode 100644 index 0000000..b4a4434 --- /dev/null +++ b/config.js @@ -0,0 +1,9 @@ +module.exports = { + lights: [ + { id: "lampa_na_parapecie_rgb_light", type: "rgb" }, + { + id: "ikea_of_sweden_tradfribulbe27wsglobeopal1055lm_light" /* lampa stojÄ…ca */, + type: "dim", + }, + ], +}; diff --git a/env.js b/env.js new file mode 100644 index 0000000..340cf00 --- /dev/null +++ b/env.js @@ -0,0 +1,5 @@ +const TOKEN = process.env.HA_TOKEN; +const HA_URL = process.env.HA_URL || "http://127.0.0.1:8123"; +const PORT = parseInt(process.env.HA_BRIDGE_PORT || "41234"); + +module.exports = { TOKEN, HA_URL, PORT }; diff --git a/index.js b/index.js new file mode 100644 index 0000000..295220d --- /dev/null +++ b/index.js @@ -0,0 +1,42 @@ +const dgram = require("node:dgram"); +const server = dgram.createSocket("udp4"); + +const { lights } = require("./config.js"); +const light_loop = require("./light-loop.js"); +const latest_color = require("./latest_color.js"); +const { TOKEN, PORT } = require("./env.js"); + +server.on("error", (err) => { + console.error(`server error:\n${err.stack}`); + server.close(); +}); + +const debug = false; +const max_brightness = 0.8; + +if (!TOKEN) { + throw new Error( + "Provide the Home Assistant Long Lived Token as a HA_TOKEN environment variable. Go to /profile in Home Assistant and scroll down.", + ); +} + +console.log( + `Remember to set the Hyperion output controller type to UDPRAW and set it to output ${lights.length} lights`, +); + +server.on("message", (msg, rinfo) => { + latest_color.set(Array.from(msg).map((e) => parseInt(e))); + if (debug) { + console.log("Received colors:", latest_color.get()); + } +}); + +server.on("listening", async () => { + const address = server.address(); + console.log(`server listening ${address.address}:${address.port}`); + for (let i in lights) { + light_loop(i, max_brightness, debug); + } +}); + +server.bind(PORT); diff --git a/latest_color.js b/latest_color.js new file mode 100644 index 0000000..972447a --- /dev/null +++ b/latest_color.js @@ -0,0 +1,20 @@ +const { lights } = require("./config.js"); + +let latest_color = []; + +// initialize +for (let i in lights) { + for (let j = 1; j <= 3; j++) { + latest_color.push(1); + } +} + +module.exports = { + get: () => { + return latest_color; + }, + + set: (new_color) => { + latest_color = new_color; + }, +}; diff --git a/light-loop.js b/light-loop.js new file mode 100644 index 0000000..44d6b6c --- /dev/null +++ b/light-loop.js @@ -0,0 +1,59 @@ +const { HA_URL, TOKEN } = require("./env.js"); + +const { sleep } = require("./util.js"); +const latest_color = require("./latest_color.js"); +const { lights } = require("./config.js"); + +async function send_color(light_data, color, max_brightness, debug) { + const brightness = (color[0] + color[1] + color[2]) / 3 / 255; + let body = { entity_id: `light.${light_data.id}`, transition: 0.18 }; + + if (light_data.type == "rgb") { + body.rgb_color = color; + body.brightness = Math.floor( + Math.max(...latest_color.get()) * max_brightness, + ); + } else { + // body.brightness = Math.max(1, Math.round(255 * brightness)); // 0 seems to turn it off and make it slower to react + body.brightness = Math.floor( + Math.max(...latest_color.get()) * max_brightness, + ); + } + + return fetch(`${HA_URL}/api/services/light/turn_on`, { + method: "POST", + headers: { + Authorization: `Bearer ${TOKEN}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body: JSON.stringify(body), + }).then(async (response) => { + if (debug) { + console.log(await response.text()); + } + }); +} + +module.exports = async function light_loop(light_index, max_brightness, debug) { + let last_time = 0; + let last_color = [0, 0, 0]; + while (true) { + last_time = Date.now(); + const from = 3 * light_index; + const to = 3 * (light_index + 1); + const current_color = latest_color.get().slice(from, to); + let is_changed = false; + for (i in last_color) { + if (last_color[i] != current_color[i]) { + is_changed = true; + break; + } + } + if (is_changed) { + await send_color(lights[light_index], current_color); + last_color = current_color; + } else { + await sleep(10); + } + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..07d1f7d --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "hyperion-ha", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/util.js b/util.js new file mode 100644 index 0000000..491b531 --- /dev/null +++ b/util.js @@ -0,0 +1,4 @@ +const { promisify } = require("node:util"); +const sleep = promisify((time, callback) => setTimeout(callback, time)); + +module.exports = { sleep };