mqtt: Added support for ColorSettingTemperature and ColorSettingHsv to the MQTT support. (#1317)

This commit is contained in:
Nick Berardi
2024-02-15 11:43:11 -05:00
committed by GitHub
parent b28eef9d10
commit c8b799f857
5 changed files with 728 additions and 97 deletions

View File

@@ -1,14 +1,13 @@
{
"name": "@scrypted/mqtt",
"version": "0.0.76",
"version": "0.0.77",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/mqtt",
"version": "0.0.76",
"version": "0.0.77",
"dependencies": {
"@types/node": "^16.6.1",
"aedes": "^0.46.1",
"axios": "^0.23.0",
"mqtt": "^4.2.8",
@@ -18,6 +17,7 @@
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.4.2",
"@types/nunjucks": "^3.2.0"
}
},
@@ -29,49 +29,50 @@
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"http-auth-utils": "^5.0.1",
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
"@types/node": "^20.11.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.0.206",
"version": "0.3.5",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.59.0"
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack-bundle-analyzer": "^4.5.0"
"typedoc": "^0.23.21"
}
},
"../sdk": {
@@ -86,9 +87,13 @@
"link": true
},
"node_modules/@types/node": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz",
"integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw=="
"version": "18.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.15.tgz",
"integrity": "sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/nunjucks": {
"version": "3.2.0",
@@ -359,9 +364,9 @@
"integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ=="
},
"node_modules/follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
@@ -502,9 +507,9 @@
}
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -574,9 +579,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nunjucks": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz",
"integrity": "sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
"integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
"dependencies": {
"a-sync-waterfall": "^1.0.0",
"asap": "^2.0.3",
@@ -717,6 +722,12 @@
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -836,39 +847,44 @@
"requires": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"@types/node": "^16.9.0",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"@types/node": "^20.11.0",
"http-auth-utils": "^5.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
},
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@babel/preset-typescript": "^7.18.6",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack": "^5.59.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@types/node": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz",
"integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw=="
"version": "18.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.15.tgz",
"integrity": "sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
}
},
"@types/nunjucks": {
"version": "3.2.0",
@@ -1087,9 +1103,9 @@
"integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ=="
},
"follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
},
"from2": {
"version": "2.3.0",
@@ -1195,9 +1211,9 @@
"integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -1253,9 +1269,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nunjucks": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz",
"integrity": "sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
"integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
"requires": {
"a-sync-waterfall": "^1.0.0",
"asap": "^2.0.3",
@@ -1355,6 +1371,12 @@
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -29,7 +29,6 @@
]
},
"dependencies": {
"@types/node": "^16.6.1",
"aedes": "^0.46.1",
"axios": "^0.23.0",
"mqtt": "^4.2.8",
@@ -37,9 +36,10 @@
"websocket-stream": "^5.5.2"
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.4.2",
"@types/nunjucks": "^3.2.0"
},
"version": "0.0.76"
"version": "0.0.77"
}

View File

@@ -1,10 +1,11 @@
import crypto from 'crypto';
import { Brightness, DeviceProvider, Lock, LockState, MixinDeviceBase, OnOff, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceProperty, Setting, Settings } from "@scrypted/sdk";
import { Online, Brightness, ColorSettingHsv, ColorSettingTemperature, DeviceProvider, Lock, LockState, MixinDeviceBase, OnOff, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceProperty } from "@scrypted/sdk";
import { Client, MqttClient, connect } from "mqtt";
import { MqttDeviceBase } from "./api/mqtt-device-base";
import nunjucks from 'nunjucks';
import sdk from "@scrypted/sdk";
import type { MqttProvider } from './main';
import { getHsvFromXyColor, getXyYFromHsvColor } from './color-util';
const { deviceManager } = sdk;
@@ -20,7 +21,20 @@ typeMap.set('switch', {
type: ScryptedDeviceType.Switch,
});
typeMap.set('light', {
interfaces: [ScryptedInterface.OnOff, ScryptedInterface.Brightness],
getInterfaces(config: any) {
const interfaces = [ScryptedInterface.OnOff, ScryptedInterface.Brightness];
if (config.color_mode) {
config.supported_color_modes.forEach(color_mode => {
if (color_mode === 'xy')
interfaces.push(ScryptedInterface.ColorSettingHsv);
else if (color_mode === 'hs')
interfaces.push(ScryptedInterface.ColorSettingHsv);
else if (color_mode === 'color_temp')
interfaces.push(ScryptedInterface.ColorSettingTemperature);
});
}
return interfaces;
},
type: ScryptedDeviceType.Light,
});
typeMap.set('lock', {
@@ -102,7 +116,7 @@ export class MqttAutoDiscoveryProvider extends MqttDeviceBase implements DeviceP
const nativeId = 'autodiscovered:' + this.nativeId + ':' + nativeIdSuffix;
let deviceInterfaces: string[];
let deviceInterfaces: string[]
if (type.interfaces)
deviceInterfaces = type.interfaces;
else
@@ -111,8 +125,10 @@ export class MqttAutoDiscoveryProvider extends MqttDeviceBase implements DeviceP
if (!deviceInterfaces)
return;
deviceInterfaces.push(ScryptedInterface.Online);
let interfaces = [
'@scrypted/mqtt',
'@scrypted/mqtt'
];
interfaces.push(...deviceInterfaces);
// try combine into existing device if this mqtt device presents
@@ -196,13 +212,26 @@ function scaleBrightness(scryptedBrightness: number, brightnessScale: number) {
return Math.round(scryptedBrightness * brightnessScale / 100);
}
function getMiredFromKelvin(kelvin: number) {
return Math.round(1000000 / kelvin);
}
function getKelvinFromMired(mired: number) {
return Math.round(1000000 / mired);
}
function unscaleBrightness(mqttBrightness: number, brightnessScale: number) {
brightnessScale = brightnessScale || 255;
return Math.round(mqttBrightness * 100 / brightnessScale);
}
export class MqttAutoDiscoveryDevice extends ScryptedDeviceBase implements OnOff, Brightness, Lock {
export class MqttAutoDiscoveryDevice extends ScryptedDeviceBase implements Online, OnOff, Brightness, Lock, ColorSettingTemperature, ColorSettingHsv {
messageListeners: ((topic: string, payload: Buffer) => void)[] = [];
debounceCallbacks: Map<string, Set<(payload: Buffer) => void>>;
modelId: any;
xyY: { x: number; y: number; brightness: number; };
colorMode: string;
constructor(nativeId: string, public provider: MqttAutoDiscoveryProvider, noBind?: boolean) {
super(nativeId);
@@ -216,6 +245,13 @@ export class MqttAutoDiscoveryDevice extends ScryptedDeviceBase implements OnOff
this.console.warn('delayed bind')
return;
}
this.debounceCallbacks = new Map<string, Set<(payload: Buffer) => void>>();
const { client } = provider;
client.on('message', this.listener.bind(this));
this.messageListeners.push(this.listener.bind(this));
this.bind();
}
@@ -227,25 +263,135 @@ export class MqttAutoDiscoveryDevice extends ScryptedDeviceBase implements OnOff
}
bindMessage(topic: string, cb: (payload: Buffer) => void) {
this.console.log('subscribing', topic);
const listener = (messageTopic: string, payload: Buffer) => {
if (topic !== messageTopic)
return;
this.console.log('message', topic, payload?.toString());
try {
cb(payload);
}
catch (e) {
this.console.error('callback error', e);
}
};
this.provider.client.on('message', listener);
this.messageListeners.push(listener);
let set: Set<(payload: Buffer) => void> = this.debounceCallbacks.get(topic);
if (set) {
this.console.log('subscribing', topic);
set.add(cb);
} else {
this.console.log('subscribing to new topic', topic);
set = new Set([cb]);
this.debounceCallbacks.set(topic, set);
}
}
listener(topic: string, payload: Buffer) {
let set = this.debounceCallbacks?.get(topic);
if (!set)
return;
this.console.log('message', topic, payload?.toString());
try {
set.forEach(callback => {
callback(payload);
});
}
catch (e) {
this.console.error('callback error', e);
}
}
bind() {
this.console.log('binding...');
const { client } = this.provider;
this.debounceCallbacks = new Map<string, Set<(payload: Buffer) => void>>();
if (this.providedInterfaces.includes(ScryptedInterface.Online)) {
const config = this.loadComponentConfig(ScryptedInterface.Online);
if (config.availability && config.availability.length > 0) {
const availabilityTopic = config.availability[0].topic;
client.subscribe(availabilityTopic);
this.bindMessage(availabilityTopic,
payload => this.online =
(config.payload_on || 'online') === this.eval(config.availability[0].value_template || '{{ value_json.state }}', payload));
}
}
if (this.providedInterfaces.includes(ScryptedInterface.ColorSettingHsv)) {
const config = this.loadComponentConfig(ScryptedInterface.ColorSettingHsv);
const colorStateTopic = config.hs_state_topic || config.state_topic;
client.subscribe(colorStateTopic);
this.bindMessage(colorStateTopic,
payload => {
let obj = JSON.parse(payload.toString());
// exit updating the below because the user set the color_temp
if (obj.color_mode !== "xy" && obj.color_mode !== "hs") {
this.hsv = undefined;
return;
}
// handle hs_value_template if present
if (config.hs_value_template) {
this.hsv = this.eval(config.hs_value_template, payload);
return;
}
// handle xy_value_template if present
if (config.xy_value_template) {
var xy = this.eval(config.xy_value_template, payload);
this.hsv = getHsvFromXyColor(xy.x, xy.y, this.xyY?.brightness ?? 1);
return;
}
let color = obj.color;
this.modelId = obj.device?.model;
// handle color_mode hs if present
if (color.h !== undefined && color.s !== undefined) {
this.colorMode = "hs";
// skip update if the colors match
if (color.h === this.hsv.h && color.s === this.hsv.s)
return;
const brightness = unscaleBrightness(obj.brightness, config.brightness_scale);
this.hsv = {
h: color.h,
s: color.s,
v: brightness
};
return;
}
// handle color_mode xy if present
if (color.x !== undefined && color.y !== undefined) {
this.colorMode = "xy";
const hsv = getHsvFromXyColor(color.x, color.y, this.xyY?.brightness ?? 100);
this.hsv = {
h: hsv.h,
s: hsv.s,
v: hsv.v
};
return;
}
});
}
if (this.providedInterfaces.includes(ScryptedInterface.ColorSettingTemperature)) {
const config = this.loadComponentConfig(ScryptedInterface.ColorSettingTemperature);
const colorTempStateTopic = config.color_temp_command_topic || config.state_topic;
client.subscribe(colorTempStateTopic);
this.bindMessage(colorTempStateTopic,
payload => {
let obj = JSON.parse(payload.toString());
// exit updating the below because the user set the color_temp
if (obj.color_mode !== "color_temp") {
this.colorTemperature = undefined;
return;
}
if (config.color_temp_value_template) {
this.colorTemperature = this.eval(config.color_temp_value_template, payload);
return;
}
this.colorTemperature = getKelvinFromMired(obj.color_temp);
});
}
if (this.providedInterfaces.includes(ScryptedInterface.Brightness)) {
const config = this.loadComponentConfig(ScryptedInterface.Brightness);
const brightnessStateTopic = config.brightness_state_topic || config.state_topic;
@@ -292,11 +438,13 @@ export class MqttAutoDiscoveryDevice extends ScryptedDeviceBase implements OnOff
publishValue(command_topic: string, template: string, value: any, defaultValue: any) {
if (value == null)
value = defaultValue;
const payload = template ? nunjucks.renderString(template, {
value_json: {
value,
}
}) : value.toString();
}) : JSON.stringify(value);
this.provider.client.publish(command_topic, Buffer.from(payload), {
qos: 1,
retain: true,
@@ -305,32 +453,148 @@ export class MqttAutoDiscoveryDevice extends ScryptedDeviceBase implements OnOff
async turnOff(): Promise<void> {
const config = this.loadComponentConfig(ScryptedInterface.OnOff);
if (config.on_command_type === 'brightness')
return this.publishValue(config.brightness_command_topic,
config.brightness_value_template, 0, 0);
return this.publishValue(config.command_topic,
config.brightness_value_template,
config.payload_off, 'OFF');
}
if (config.on_command_type === 'brightness') {
await this.setBrightnessInternal(0, config);
return;
}
let command = {
state: "OFF"
};
if (config.command_off_template) {
this.publishValue(config.command_topic,
config.command_off_template,
command, "ON");
} else {
this.publishValue(config.command_topic,
undefined, command, command);
}
}
async turnOn(): Promise<void> {
const config = this.loadComponentConfig(ScryptedInterface.OnOff);
if (config.on_command_type === 'brightness')
return this.publishValue(config.brightness_command_topic,
config.brightness_value_template,
config.brightness_scale || 255,
config.brightness_scale || 255);
return this.publishValue(config.command_topic,
config.brightness_value_template,
config.payload_on, 'ON');
if (config.on_command_type === 'brightness') {
await this.setBrightnessInternal(config.brightness_scale || 255, config);
return;
}
let command = {
state: "ON"
};
if (config.command_on_template) {
this.publishValue(config.command_topic,
config.command_on_template,
command, "ON");
} else {
this.publishValue(config.command_topic,
undefined, command, command);
}
}
async setBrightness(brightness: number): Promise<void> {
const config = this.loadComponentConfig(ScryptedInterface.Brightness);
const scaledBrightness = scaleBrightness(brightness, config.brightness_scale);
this.publishValue(config.brightness_command_topic,
config.brightness_value_template,
scaledBrightness, scaledBrightness);
await this.setBrightnessInternal(brightness, config);
}
async setBrightnessInternal(brightness: number, config: any): Promise<void> {
const scaledBrightness = scaleBrightness(brightness, config.brightness_scale);
// use brightness_command_topic and fallback to JSON if not provided
if (config.brightness_value_template) {
this.publishValue(config.brightness_command_topic,
config.brightness_value_template,
scaledBrightness, scaledBrightness);
} else {
this.publishValue(config.command_topic,
`{ "state": "${ scaledBrightness === 0 ? 'OFF' : 'ON'}", "brightness": ${scaledBrightness} }`,
scaledBrightness, 255);
}
}
async getTemperatureMaxK(): Promise<number> {
const config = this.loadComponentConfig(ScryptedInterface.ColorSettingTemperature);
return getKelvinFromMired(Math.min(config.min_mireds, config.max_mireds));
}
async getTemperatureMinK(): Promise<number> {
const config = this.loadComponentConfig(ScryptedInterface.ColorSettingTemperature);
return getKelvinFromMired(Math.max(config.min_mireds, config.max_mireds));
}
async setColorTemperature(kelvin: number): Promise<void> {
const config = this.loadComponentConfig(ScryptedInterface.ColorSettingTemperature);
if (kelvin >= 0 || kelvin <= 100) {
const min = await this.getTemperatureMinK();
const max = await this.getTemperatureMaxK();
const diff = (max - min) * (kelvin/100);
kelvin = Math.round(min + diff);
}
const mired = getMiredFromKelvin(kelvin);
const color = {
state: "ON",
//color_mode: "color_temp",
color_temp: mired ?? 370
};
// use color_temp_command_topic and fallback to JSON if not provided
if (config.color_temp_command_template) {
this.publishValue(config.color_temp_command_topic,
config.color_temp_command_template,
color, color);
} else {
this.publishValue(config.command_topic,
undefined, color, color);
}
}
async setHsv(hue: number, saturation: number, value: number): Promise<void> {
const config = this.loadComponentConfig(ScryptedInterface.ColorSettingHsv);
this.colorMode = this.colorMode ?? (config.supported_color_modes.includes("hs") ? "hs" : "xy");
if (this.colorMode === "hs") {
const color = {
state: "ON",
//color_mode: "hs",
color: {
h: hue ?? 0,
s: (saturation ?? 1) * 100
}
};
// use hs_command_topic and fallback to JSON if not provided
if (config.hs_command_template) {
this.publishValue(config.hs_command_topic,
config.hs_command_template,
color, color);
} else {
this.publishValue(config.command_topic,
undefined, color, color);
}
} else if (this.colorMode === "xy") {
const xy = getXyYFromHsvColor(hue, saturation, value, this.modelId);
const color = {
state: "ON",
//color_mode: "xy",
color: {
x: xy.x,
y: xy.y
}
};
this.xyY = xy;
// use xy_command_template and fallback to JSON if not provided
if (config.xy_command_template) {
this.publishValue(config.xy_command_topic,
config.xy_command_template,
color, color);
} else {
this.publishValue(config.command_topic,
undefined, color, color);
}
}
}
async lock(): Promise<void> {
const config = this.loadComponentConfig(ScryptedInterface.Lock);
return this.publishValue(config.command_topic,

View File

@@ -0,0 +1,345 @@
export function getXyYFromHsvColor(h: number, s: number, v: number, hueModelId: string = null) {
if (s > 1 || v > 1 || h > 360)
throw new Error('invalid hsv color, h must not be greater than 360, and s and v must not be greater than 1');
const rgb = hsvToRgb(h, s, v);
const xyz = rgbToXyz(rgb.r, rgb.g, rgb.b);
const { x, y, z } = xyz;
let xyY = {
x: x / (x + y + z),
y: y / (x + y + z),
brightness: y
};
if (!xyIsInGamutRange(xyY, hueModelId)) {
xyY = getClosestColor(xyY, hueModelId);
}
return xyY;
}
export function getHsvFromXyColor(x: number, y: number, brightness: number) {
if (x > 1 || y > 1 || brightness > 1)
throw new Error('invalid xy color, x, y, and brightness must not be greater than 1');
const Y = brightness;
const z = 1 - x - Y;
const X = (Y / y) * x;
const Z = (Y / y) * z;
const rgb = xyzToRgb(X, Y, Z);
const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
const h: number = hsv[0];
const s: number = hsv[1];
const v: number = hsv[2];
return {
h, s, v
};
}
export function getRgbFromXyColor(x: number, y: number, brightness: number) {
if (x > 1 || y > 1 || brightness > 1)
throw new Error('invalid xy color, x, y, and brightness must not be greater than 1');
const Y = brightness;
const z = 1 - x - Y;
const X = (Y / y) * x;
const Z = (Y / y) * z;
const rgb = xyzToRgb(X, Y, Z);
return rgb;
}
export function getXyFromRgbColor(r: number, g: number, b: number, hueModelId: string = null) {
if (r > 255 || g > 255 || b > 255)
throw new Error('invalid rgb color, r, g, and b must not be greater than 255');
const xyz = rgbToXyz(r, g, b);
const { x, y, z } = xyz;
let xyY = {
x: x / (x + y + z),
y: y / (x + y + z),
brightness: y
};
if (!xyIsInGamutRange(xyY, hueModelId)) {
xyY = getClosestColor(xyY, hueModelId);
}
return xyY;
}
function xyzToRgb (x: number, y: number, z: number) {
let r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986);
let g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415);
let b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570);
// Assume sRGB
r = r > 0.0031308
? ((1.055 * (r ** (1.0 / 2.4))) - 0.055)
: r * 12.92;
g = g > 0.0031308
? ((1.055 * (g ** (1.0 / 2.4))) - 0.055)
: g * 12.92;
b = b > 0.0031308
? ((1.055 * (b ** (1.0 / 2.4))) - 0.055)
: b * 12.92;
r = Math.min(Math.max(0, r), 1);
g = Math.min(Math.max(0, g), 1);
b = Math.min(Math.max(0, b), 1);
return {
r: r * 255,
g: g * 255,
b: b * 255
};
}
function hsvToRgb (h: number, s: number, v: number) {
h /= 60;
const hi = Math.floor(h) % 6;
const f = h - Math.floor(h);
const p = 255 * v * (1 - s);
const q = 255 * v * (1 - (s * f));
const t = 255 * v * (1 - (s * (1 - f)));
v *= 255;
switch (hi) {
case 0:
return { r:v, g:t, b:p };
case 1:
return { r:q, g:v, b:p };
case 2:
return { r:p, g:v, b:t };
case 3:
return { r:p, g:q, b:v };
case 4:
return { r:t, g:p, b:v };
case 5:
return { r:v, g:p, b:q };
}
}
function rgbToXyz(r: number, g: number, b: number) {
r /= 255;
g /= 255;
b /= 255;
r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
const x = r * 0.4124 + g * 0.3576 + b * 0.1805;
const y = r * 0.2126 + g * 0.7152 + b * 0.0722;
const z = r * 0.0193 + g * 0.1192 + b * 0.9505;
return {
x, y, z
};
}
function rgbToHsv (r: number, g: number, b: number) {
r /= 255;
g /= 255;
b /= 255;
const v = Math.max(r, g, b);
const diff = v - Math.min(r, g, b);
const diffc = function (c) {
return (v - c) / 6 / diff + 1 / 2;
};
let rdif: number;
let gdif: number;
let bdif: number;
let h: number;
let s: number;
if (diff === 0) {
h = 0;
s = 0;
} else {
s = diff / v;
rdif = diffc(r);
gdif = diffc(g);
bdif = diffc(b);
if (r === v) {
h = bdif - gdif;
} else if (g === v) {
h = (1 / 3) + rdif - bdif;
} else if (b === v) {
h = (2 / 3) + gdif - rdif;
}
if (h < 0) {
h += 1;
} else if (h > 1) {
h -= 1;
}
}
return [
h * 360, s, v
];
}
export function xyIsInGamutRange(xy: any, hueModelId: string = null) {
let gamut = getLightColorGamutRange(hueModelId);
if (Array.isArray(xy)) {
xy = {
x: xy[0],
y: xy[1]
};
}
let v0 = [gamut.blue[0] - gamut.red[0], gamut.blue[1] - gamut.red[1]];
let v1 = [gamut.green[0] - gamut.red[0], gamut.green[1] - gamut.red[1]];
let v2 = [xy.x - gamut.red[0], xy.y - gamut.red[1]];
let dot00 = (v0[0] * v0[0]) + (v0[1] * v0[1]);
let dot01 = (v0[0] * v1[0]) + (v0[1] * v1[1]);
let dot02 = (v0[0] * v2[0]) + (v0[1] * v2[1]);
let dot11 = (v1[0] * v1[0]) + (v1[1] * v1[1]);
let dot12 = (v1[0] * v2[0]) + (v1[1] * v2[1]);
let invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
let u = (dot11 * dot02 - dot01 * dot12) * invDenom;
let v = (dot00 * dot12 - dot01 * dot02) * invDenom;
return ((u >= 0) && (v >= 0) && (u + v < 1));
}
export function getLightColorGamutRange(hueModelId: string = null): any {
// legacy LivingColors Bloom, Aura, Light Strips and Iris (Gamut A)
let gamutA = {
red: [0.704, 0.296],
green: [0.2151, 0.7106],
blue: [0.138, 0.08]
};
// older model hue bulb (Gamut B)
let gamutB = {
red: [0.675, 0.322],
green: [0.409, 0.518],
blue: [0.167, 0.04]
};
// newer model Hue lights (Gamut C)
let gamutC = {
red: [0.692, 0.308],
green: [0.17, 0.7],
blue: [0.153, 0.048]
};
let defaultGamut ={
red: [1.0, 0],
green: [0.0, 1.0],
blue: [0.0, 0.0]
};
let philipsModels = {
"9290012573A": gamutB
};
if(!!philipsModels[hueModelId]){
return philipsModels[hueModelId];
}
return defaultGamut;
}
export function getClosestColor(xy: any, hueModelId: string = null) {
function getLineDistance(pointA, pointB){
return Math.hypot(pointB.x - pointA.x, pointB.y - pointA.y);
}
function getClosestPoint(xy, pointA, pointB) {
let xy2a = [xy.x - pointA.x, xy.y - pointA.y];
let a2b = [pointB.x - pointA.x, pointB.y - pointA.y];
let a2bSqr = Math.pow(a2b[0],2) + Math.pow(a2b[1],2);
let xy2a_dot_a2b = xy2a[0] * a2b[0] + xy2a[1] * a2b[1];
let t = xy2a_dot_a2b /a2bSqr;
return {
x: pointA.x + a2b[0] * t,
y: pointA.y + a2b[1] * t,
brightness: xy.brightness
}
}
let gamut = getLightColorGamutRange(hueModelId);
let greenBlue = {
a: {
x: gamut.green[0],
y: gamut.green[1]
},
b: {
x: gamut.blue[0],
y: gamut.blue[1]
}
};
let greenRed = {
a: {
x: gamut.green[0],
y: gamut.green[1]
},
b: {
x: gamut.red[0],
y: gamut.red[1]
}
};
let blueRed = {
a: {
x: gamut.red[0],
y: gamut.red[1]
},
b: {
x: gamut.blue[0],
y: gamut.blue[1]
}
};
let closestColorPoints = {
greenBlue : getClosestPoint(xy,greenBlue.a,greenBlue.b),
greenRed : getClosestPoint(xy,greenRed.a,greenRed.b),
blueRed : getClosestPoint(xy,blueRed.a,blueRed.b)
};
let distance = {
greenBlue : getLineDistance(xy,closestColorPoints.greenBlue),
greenRed : getLineDistance(xy,closestColorPoints.greenRed),
blueRed : getLineDistance(xy,closestColorPoints.blueRed)
};
let closestDistance;
let closestColor;
for (let i in distance){
if(distance.hasOwnProperty(i)){
if(!closestDistance){
closestDistance = distance[i];
closestColor = i;
}
if(closestDistance > distance[i]){
closestDistance = distance[i];
closestColor = i;
}
}
}
return closestColorPoints[closestColor];
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",