homebridge-tasmota
Version:
Homebridge plugin for Tasmota devices leveraging home assistant auto discovery.
266 lines • 8.27 kB
JavaScript
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function renameKeys(obj, mapShortToLong) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let result;
if (Array.isArray(obj)) {
result = obj.map((item) => renameKeys(item, mapShortToLong));
}
else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// Get the destination key
const destKey = mapShortToLong[key] || key;
// Get the value
const value = obj[key];
// Recursively handle nested objects or arrays
if (value && typeof value === 'object') {
renameKeys(value, mapShortToLong);
}
// Assign the value to the new key
result[destKey] = value;
}
}
}
return result;
}
export function replaceStringsInObject(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
obj, findStr, replaceStr,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cache = new Map()) {
// Check if the object has already been processed to avoid circular references
if (cache.has(obj)) {
return cache.get(obj);
}
// Initialize result object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = {};
cache.set(obj, result); // Cache the object reference
for (const [key, value] of Object.entries(obj)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let updatedValue;
if (typeof value === 'string') {
// Replace occurrences of findStr with replaceStr in strings
updatedValue = value.replace(new RegExp(findStr, 'gi'), replaceStr);
}
else if (Array.isArray(value)) {
// Recursively process arrays
updatedValue = value.map((item) => typeof item === 'object' && item !== null
? replaceStringsInObject(item, findStr, replaceStr, cache)
: item);
}
else if (typeof value === 'object' && value !== null) {
// Recursively process nested objects
updatedValue = replaceStringsInObject(value, findStr, replaceStr, cache);
}
else {
// Preserve other types (numbers, booleans, etc.)
updatedValue = value;
}
result[key] = updatedValue;
}
return result;
}
/* The various Tasmota firmware's have a slightly different flavors of the message. */
export function normalizeMessage(message) {
// Handle specific tasmotaType cases
if (message.tasmotaType === 'fanFixed') {
message = {
...message,
payload_high_speed: '3',
payload_medium_speed: '2',
payload_low_speed: '1',
pl_off: '0',
pl_on: '1',
val_tpl: '{% if value_json.FanSpeed == 0 -%}0{%- elif value_json.FanSpeed > 0 -%}1{%- endif %}',
bri_val_tpl: '{{value_json.FanSpeed*1/3*100}}',
speeds: ['off', 'low', 'medium', 'high'],
};
if (message.cmd_t) {
message.cmd_t = message.cmd_t.replace('POWER2', 'FanSpeed');
}
}
// Translation map for renaming keys
const translation = {
unique_id: 'uniq_id',
device_class: 'dev_cla',
payload_on: 'pl_on',
payload_off: 'pl_off',
payload_high_speed: 'pl_hi_spd',
payload_medium_speed: 'pl_med_spd',
payload_low_speed: 'pl_lo_spd',
speeds: 'spds',
device: 'dev',
model: 'mdl',
sw_version: 'sw',
manufacturer: 'mf',
identifiers: 'ids',
value_template: 'val_tpl',
state_value_template: 'stat_val_tpl',
unit_of_measurement: 'unit_of_meas',
state_topic: 'stat_t',
availability_topic: 'avty_t',
command_topic: 'cmd_t',
icon: 'ic',
};
// Rename keys in the message
message = renameKeys(message, translation);
// Replace placeholders in the message
if (message['~']) {
message = replaceStringsInObject(message, '~', message['~']);
}
// Validate MQTT topic uniqueness
if (['sonoff/tele/STATE', 'tasmota/tele/STATE'].includes(message.stat_t || '')) {
console.error('ERROR: %s has an incorrectly configured MQTT Topic, please make it unique.', message.name);
}
// Infer device class based on unique_id patterns
if (!message.dev_cla && message.uniq_id) {
if (/_CarbonDioxide|eCO2$/.test(message.uniq_id)) {
message.dev_cla = 'co2';
}
else if (/_AirQuality$/.test(message.uniq_id)) {
message.dev_cla = 'pm25';
}
}
// Set default payload values for ESPHome devices
if (typeof message.pl_on === 'undefined') {
message.pl_on = 'ON';
}
if (typeof message.pl_off === 'undefined') {
message.pl_off = 'OFF';
}
return message;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function findVal(object, key) {
let value;
Object.keys(object).some((k) => {
if (k === key) {
value = object[k];
return true; // Exit loop
}
if (object[k] && typeof object[k] === 'object') {
value = findVal(object[k], key);
return value !== undefined; // Exit loop if value is found
}
return false; // Continue searching
});
return value;
}
/*
* HSV to RGB conversion from https://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately
* accepts parameters
* h Object = {h:x, s:y, v:z}
* OR
* h, s, v
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function HSVtoRGB(h, s, v) {
let r = 0, g = 0, b = 0;
if (arguments.length === 1) {
s = h.s;
v = h.v;
h = h.h;
}
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
const rgb = [0, 0, 0];
rgb[0] = Math.round(r * 255);
rgb[1] = Math.round(g * 255);
rgb[2] = Math.round(b * 255);
return (rgb);
}
/* accepts parameters
* r Object = {r:x, g:y, b:z}
* OR
* r, g, b
*/
export function RGBtoHSV(r, g, b) {
// debug('from', r, g, b);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h = 0;
const s = (max === 0 ? 0 : d / max);
const v = max / 255;
// debug('max', )
switch (max) {
case min:
h = 0;
break;
case r:
h = (g - b) + d * (g < b ? 6 : 0);
h /= 6 * d;
break;
case g:
h = (b - r) + d * 2;
h /= 6 * d;
break;
case b:
h = (r - g) + d * 4;
h /= 6 * d;
break;
}
return {
h,
s,
v,
};
}
export function RGBtoScaledHSV(r, g, b) {
const hsv = RGBtoHSV(Number.parseFloat(r), Number.parseFloat(g), Number.parseFloat(b));
// debug('to', hsv);
return {
h: Math.round(hsv.h * 360),
s: Math.round(hsv.s * 100),
v: Math.round(hsv.v * 100),
};
}
export function ScaledHSVtoRGB(h, s, v) {
return HSVtoRGB(h / 360, s / 100, v / 100);
}
export function HSBtoTasmota(h, s, b) {
const hsb = [0, 0, 0];
hsb[0] = h;
hsb[1] = s;
hsb[2] = b;
return (hsb);
}
//# sourceMappingURL=utils.js.map