fix github installs, ship dist output, add install-time build hook

This commit is contained in:
2026-02-23 11:35:53 +01:00
parent 54b75088e9
commit b207a7ed90
8 changed files with 347 additions and 7 deletions

7
.gitignore vendored
View File

@@ -1,9 +1,6 @@
# Dependencies # Dependencies
node_modules/ node_modules/
# Build output
dist/
# Logs # Logs
*.log *.log
npm-debug.log* npm-debug.log*
@@ -20,3 +17,7 @@ Thumbs.db
# TypeScript # TypeScript
*.tsbuildinfo *.tsbuildinfo
# Agent notes
AI_AGENT_REPORT.md
LEARNINGS.md

View File

@@ -13,10 +13,34 @@ A Homebridge plugin for ESPHome RGBWW lights with native HSV support and automat
## Installation ## Installation
### Install from GitHub (Homebridge Docker/Unraid, persistent)
Run these commands inside the Homebridge container terminal:
```bash ```bash
npm install -g homebridge-esphome-rgbww cd /homebridge
npm uninstall homebridge-esphome-rgbww
npm install --save git+https://github.com/felixfoertsch/homebridge-esphome-rgbww.git
``` ```
Then restart Homebridge:
```bash
hb-service restart
```
If `hb-service` is not available, restart the container from your Docker/Unraid UI.
Verify the runtime entrypoint exists:
```bash
ls -l /homebridge/node_modules/homebridge-esphome-rgbww/dist/index.js
```
Important:
- Install from `/homebridge` with `--save` so dependency metadata is persisted.
- Do not use `npm -g install` inside the container.
## Configuration ## Configuration
Add this to your Homebridge `config.json`: Add this to your Homebridge `config.json`:

247
dist/accessory.js vendored Normal file
View File

@@ -0,0 +1,247 @@
"use strict";
const mqtt = require("mqtt");
class ESPHomeRGBWWAccessory {
static MAX_BRIGHTNESS = 255;
constructor(platform, accessory, config) {
this.platform = platform;
this.accessory = accessory;
this.config = config;
this.currentState = {
on: false,
brightness: 100,
hue: 0,
saturation: 0,
colorTemperature: 300,
};
this.accessory
.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(this.platform.Characteristic.Manufacturer, config.manufacturer || "ESPHome")
.setCharacteristic(this.platform.Characteristic.Model, config.model || "RGBWW Light")
.setCharacteristic(this.platform.Characteristic.SerialNumber, config.id);
this.service =
this.accessory.getService(this.platform.Service.Lightbulb) ||
this.accessory.addService(this.platform.Service.Lightbulb);
this.service.setCharacteristic(this.platform.Characteristic.Name, config.name);
this.service
.getCharacteristic(this.platform.Characteristic.On)
.onSet(this.setOn.bind(this))
.onGet(this.getOn.bind(this));
this.service
.getCharacteristic(this.platform.Characteristic.Brightness)
.onSet(this.setBrightness.bind(this))
.onGet(this.getBrightness.bind(this));
this.service
.getCharacteristic(this.platform.Characteristic.Hue)
.onSet(this.setHue.bind(this))
.onGet(this.getHue.bind(this));
this.service
.getCharacteristic(this.platform.Characteristic.Saturation)
.onSet(this.setSaturation.bind(this))
.onGet(this.getSaturation.bind(this));
this.service
.getCharacteristic(this.platform.Characteristic.ColorTemperature)
.onSet(this.setColorTemperature.bind(this))
.onGet(this.getColorTemperature.bind(this));
this.mqttClient = mqtt.connect(config.mqtt_broker || "mqtt://localhost:1883");
this.mqttClient.on("connect", () => {
this.platform.log.info("Connected to MQTT broker");
this.mqttClient.subscribe(config.state_topic);
});
this.mqttClient.on("message", (topic, message) => {
if (topic === config.state_topic) {
this.handleStateUpdate(message.toString());
}
});
this.mqttClient.on("error", (error) => {
this.platform.log.error("MQTT error:", error);
});
}
rgbToHsv(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let h = 0;
let s = 0;
const v = max;
if (delta > 0) {
s = delta / max;
if (max === r) {
h = ((g - b) / delta) % 6;
} else if (max === g) {
h = (b - r) / delta + 2;
} else {
h = (r - g) / delta + 4;
}
h *= 60;
if (h < 0) {
h += 360;
}
}
return { h, s: s * 100, v: v * 100 };
}
hsvToRgb(h, s, v) {
s /= 100;
v /= 100;
const c = v * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - c;
let r = 0;
let g = 0;
let b = 0;
if (h >= 0 && h < 60) {
r = c;
g = x;
b = 0;
} else if (h < 120) {
r = x;
g = c;
b = 0;
} else if (h < 180) {
r = 0;
g = c;
b = x;
} else if (h < 240) {
r = 0;
g = x;
b = c;
} else if (h < 300) {
r = x;
g = 0;
b = c;
} else {
r = c;
g = 0;
b = x;
}
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255),
};
}
handleStateUpdate(message) {
try {
const state = JSON.parse(message);
this.currentState.on = state.state === "ON";
this.currentState.brightness = Math.round((state.brightness / ESPHomeRGBWWAccessory.MAX_BRIGHTNESS) * 100);
if (state.color && state.color.r !== undefined) {
const hsv = this.rgbToHsv(state.color.r, state.color.g, state.color.b);
this.currentState.hue = hsv.h;
this.currentState.saturation = hsv.s;
}
if (state.color_temp) {
this.currentState.colorTemperature = state.color_temp;
}
this.service.updateCharacteristic(this.platform.Characteristic.On, this.currentState.on);
this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.currentState.brightness);
this.service.updateCharacteristic(this.platform.Characteristic.Hue, this.currentState.hue);
this.service.updateCharacteristic(this.platform.Characteristic.Saturation, this.currentState.saturation);
this.service.updateCharacteristic(this.platform.Characteristic.ColorTemperature, this.currentState.colorTemperature);
} catch (error) {
this.platform.log.error("Failed to parse state update:", error);
}
}
publishCommand(payload) {
this.mqttClient.publish(this.config.command_topic, JSON.stringify(payload));
}
async setOn(value) {
this.currentState.on = value;
this.publishCommand({ state: value ? "ON" : "OFF" });
this.platform.log.debug("Set On ->", value);
}
async getOn() {
return this.currentState.on;
}
async setBrightness(value) {
this.currentState.brightness = value;
const brightness = Math.round(((value) / 100) * ESPHomeRGBWWAccessory.MAX_BRIGHTNESS);
this.publishCommand({ brightness });
this.platform.log.debug("Set Brightness ->", value);
}
async getBrightness() {
return this.currentState.brightness;
}
async setHue(value) {
this.currentState.hue = value;
this.updateRGBColor();
this.platform.log.debug("Set Hue ->", value);
}
async getHue() {
return this.currentState.hue;
}
async setSaturation(value) {
this.currentState.saturation = value;
this.updateRGBColor();
this.platform.log.debug("Set Saturation ->", value);
}
async getSaturation() {
return this.currentState.saturation;
}
async setColorTemperature(value) {
this.currentState.colorTemperature = value;
this.publishCommand({ color_temp: value });
this.platform.log.debug("Set ColorTemperature ->", value);
}
async getColorTemperature() {
return this.currentState.colorTemperature;
}
updateRGBColor() {
const rgb = this.hsvToRgb(this.currentState.hue, this.currentState.saturation, this.currentState.brightness);
this.publishCommand({
color: {
r: rgb.r,
g: rgb.g,
b: rgb.b,
},
});
}
}
module.exports = { ESPHomeRGBWWAccessory };

6
dist/index.js vendored Normal file
View File

@@ -0,0 +1,6 @@
"use strict";
const { ESPHomeRGBWWPlatform } = require("./platform");
module.exports = (api) => {
api.registerPlatform("homebridge-esphome-rgbww", "ESPHomeRGBWW", ESPHomeRGBWWPlatform);
};

45
dist/platform.js vendored Normal file
View File

@@ -0,0 +1,45 @@
"use strict";
const { ESPHomeRGBWWAccessory } = require("./accessory");
class ESPHomeRGBWWPlatform {
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
this.accessories = [];
this.log.debug("Finished initializing platform:", this.config.name);
this.api.on("didFinishLaunching", () => {
this.discoverDevices();
});
}
configureAccessory(accessory) {
this.log.info("Loading accessory from cache:", accessory.displayName);
this.accessories.push(accessory);
}
discoverDevices() {
const devices = this.config.lights || [];
for (const device of devices) {
const uuid = this.api.hap.uuid.generate(device.id);
const existingAccessory = this.accessories.find((accessory) => accessory.UUID === uuid);
if (existingAccessory) {
this.log.info("Restoring existing accessory from cache:", existingAccessory.displayName);
new ESPHomeRGBWWAccessory(this, existingAccessory, device);
} else {
this.log.info("Adding new accessory:", device.name);
const accessory = new this.api.platformAccessory(device.name, uuid);
accessory.context.device = device;
new ESPHomeRGBWWAccessory(this, accessory, device);
this.api.registerPlatformAccessories("homebridge-esphome-rgbww", "ESPHomeRGBWW", [accessory]);
}
}
}
}
module.exports = { ESPHomeRGBWWPlatform };

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "homebridge-esphome-rgbww", "name": "homebridge-esphome-rgbww",
"version": "1.0.0", "version": "2026.02.23",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "homebridge-esphome-rgbww", "name": "homebridge-esphome-rgbww",
"version": "1.0.0", "version": "2026.02.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mqtt": "^5.3.5" "mqtt": "^5.3.5"

View File

@@ -1,11 +1,21 @@
{ {
"name": "homebridge-esphome-rgbww", "name": "homebridge-esphome-rgbww",
"version": "1.0.0", "version": "2026.02.23",
"description": "Homebridge plugin for ESPHome RGBWW lights with HSV support", "description": "Homebridge plugin for ESPHome RGBWW lights with HSV support",
"main": "dist/index.js", "main": "dist/index.js",
"files": [
"dist/",
"config.schema.json",
"README.md",
"LICENSE"
],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"clean": "rm -rf dist",
"pretest": "npm run build",
"test": "node --test test/smoke.test.mjs",
"watch": "tsc -w", "watch": "tsc -w",
"prepare": "npm run build",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"keywords": [ "keywords": [

7
test/smoke.test.mjs Normal file
View File

@@ -0,0 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
import pluginRegister from "../dist/index.js";
test("exports a register function", () => {
assert.equal(typeof pluginRegister, "function");
});