fix github installs, ship dist output, add install-time build hook
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -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
247
dist/accessory.js
vendored
Normal 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
6
dist/index.js
vendored
Normal 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
45
dist/platform.js
vendored
Normal 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
4
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -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
7
test/smoke.test.mjs
Normal 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");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user