iobroker.lightcontrol
Version:
Light control of lamps from various manufacturers
660 lines (577 loc) • 22.3 kB
JavaScript
/* eslint-disable no-mixed-spaces-and-tabs */
/* eslint-disable indent */
;
const SunCalc = require("suncalc2");
const getAstroDate = require("./helper").getAstroDate;
const compareTime = require("./helper").compareTime;
const { getDateObject } = require("./helper");
const colorConv = require("./colorCoversation");
/**
* AdaptiveBri
* @param {object} adapter Adapter-Class
* @param {string} Group Group of Lightgroups eg. LivingRoom, Children1,...
*/
async function AdaptiveBri(adapter, Group) {
try {
const LightGroups = adapter.LightGroups;
adapter.writeLog(
`[ AdaptiveBri ] Reaching for Group="${Group}" actual Lux="${LightGroups[Group].actualLux}" generic lux="${adapter.ActualGenericLux}`,
);
let TempBri = 0;
if (LightGroups[Group].adaptiveBri) {
if (LightGroups[Group].actualLux === 0) {
TempBri = parseInt(adapter.Settings.minBri);
} else if (LightGroups[Group].actualLux >= 10000) {
TempBri = 100;
} else if (LightGroups[Group].actualLux > 0 && LightGroups[Group].actualLux < 10000) {
TempBri = LightGroups[Group].actualLux / 100;
if (TempBri < adapter.Settings.minBri) TempBri = parseInt(adapter.Settings.minBri);
}
}
return Math.round(TempBri);
} catch (error) {
adapter.errorHandling(error, "AdaptiveBri");
}
}
/**
* SetBrightness
* @param {object} adapter Adapter-Class
* @param {string} Group Group of Lightgroups eg. LivingRoom, Children1,...
* @param {number | undefined} Brightness Value 0 to 100
* @param {string} [caller="default"] - Quelle des Funktionsaufrufs. Standardmäßig "default"
*/
async function SetBrightness(adapter, Group, Brightness, caller = "default") {
const LightGroups = adapter.LightGroups;
adapter.writeLog(
`[ SetBrightness ] Reaching for Group="${Group}", Brightness="${Brightness}, PowerState="${LightGroups[Group].power}"`,
);
try {
if (!LightGroups[Group].lights.length) {
adapter.writeLog(
`[ SetBrightness ] Not able to set Brighness for Group = "${Group}". No lights are defined!!`,
"warn",
);
return;
}
//Set Brightness only if Group Power on
if (LightGroups[Group].power) {
const promises = LightGroups[Group].lights
.filter((Light) => Light?.bri?.oid)
.map((Light) => setDeviceBri(adapter, Light, Brightness));
await Promise.all(promises);
}
if (caller === "default") await adapter.setStateAsync(Group + "." + "bri", Brightness, true);
return true;
} catch (error) {
adapter.errorHandling(error, "SetBrightness");
}
}
/**
* Sets the brightness of a device based on the given `Brightness` parameter and the `minVal` and `maxVal` values from the `Light` object.
* @param {object} adapter - The adapter object used for logging and error handling.
* @param {object} Light - The Light object containing the device information, including the `minVal` and `maxVal` values for the brightness.
* @param {number | undefined} brightness - The brightness value to be set on the device.
* @returns {Promise<boolean>} - Returns a Promise that resolves to `true` if the brightness was successfully set, or `false` if there was an error.
*/
async function setDeviceBri(adapter, Light, brightness) {
try {
const { bri } = Light ?? {};
if (!bri?.oid) {
return false;
}
const log = !bri?.useBri
? `[ setDeviceBri ] Switching with Power State is activated. Min. Brightness is defined with 2%. Actual Brightness = "${brightness}"`
: `[ setDeviceBri ] Switching with Brightness is activated. No min. Brightness needed. Actual Brightness = "${brightness}"`;
adapter.writeLog(log);
const minBrightness = bri?.useBri ? 0 : 2;
brightness = Math.round(Math.min(Math.max(brightness || 0, minBrightness), 100));
const minVal = bri?.minVal || 0;
const maxVal = bri?.maxVal || 100;
const defaultBri = bri?.defaultBri || 100;
const value = Math.round((brightness / 100) * (maxVal - minVal) + minVal);
await adapter.setForeignStateAsync(Light.bri.oid, Math.round((value / maxVal) * defaultBri), false);
return true;
} catch (error) {
adapter.errorHandling(error, "setDeviceBri");
return false;
}
}
/**
* setCt
* @param {object} adapter Adapter-Class
* @param {string} Group Group of Lightgroups eg. LivingRoom, Children1,...
* @param {number} ct
*/
async function SetCt(adapter, Group, ct = adapter.LightGroups[Group].ct) {
const LightGroups = adapter.LightGroups;
try {
if (!LightGroups[Group].lights?.length) {
adapter.writeLog(
`[ SetCt ] Not able to set Color-Temperature for Group = "${Group}". No lights are defined!!`,
"warn",
);
return;
}
const ctValue = ct || LightGroups[Group].ct;
adapter.writeLog(`Reaching SetCt, Group="${Group}" Ct="${ctValue}"`);
await Promise.all(
LightGroups[Group].lights.map(async (Light) => {
const { ct } = Light ?? {};
if ((LightGroups[Group].power || ct?.sendCt) && ct?.oid) {
const oid = ct?.oid;
const outMinVal = ct?.minVal || 0;
const outMaxVal = ct?.maxVal || 100;
const minKelvin = ct?.minKelvin || 2700;
const maxKelvin = ct?.maxKelvin || 6500;
const ctConversion = ct?.ctConversion ?? "default";
const value = await KelvinToRange(
adapter,
outMinVal,
outMaxVal,
minKelvin,
maxKelvin,
ctValue,
ctConversion,
);
if (value < 0) {
adapter.writeLog(
`[ SetCt ] Not able to set Color-Temperature for Light="${oid}" in Group ="${Group}". No conversion defined!!`,
"warn",
);
return;
} else if (value >= 0) {
await adapter.setForeignStateAsync(oid, value, false);
}
}
}),
);
await adapter.setStateAsync(Group + ".ct", ctValue, true);
return true;
} catch (error) {
adapter.errorHandling(error, "SetCt");
}
}
/**
* KelvinToRange
* @param {object} adapter Adapter-Class
* @param {number} outMinVal minimum Ct-Value of target state
* @param {number} outMaxVal maximum Ct-Value of target state
* @param {number} minKelvin minimum kelvin value
* @param {number} maxKelvin maximum kelvin value
* @param {number} kelvin kelvin value of group (e.g. 2700)
* @param {string} ctConversion switch if lower ct-value is cold
* @returns {Promise<number>} return the Ct-Value of the target state
*/
async function KelvinToRange(adapter, outMinVal, outMaxVal, minKelvin, maxKelvin, kelvin, ctConversion) {
try {
adapter.writeLog(
`[ KelvinToRange ] outMinVal: ${outMinVal}, outMaxVal: ${outMaxVal}, minKelvin: ${minKelvin}, maxKelvin: ${maxKelvin}, kelvin: ${kelvin}, ctConversion: ${ctConversion}`,
);
let rangeValue;
let limitedKelvin;
switch (ctConversion) {
case "default":
return -2;
case "warmToCool":
limitedKelvin = Math.max(Math.min(kelvin, maxKelvin), minKelvin);
rangeValue =
((limitedKelvin - minKelvin) / (maxKelvin - minKelvin)) * (outMaxVal - outMinVal) + outMinVal;
adapter.writeLog(`[ KelvinToRange ] Calculated ct-value (warmToCool): ${rangeValue}`);
return rangeValue;
case "coolToWarm":
limitedKelvin = Math.max(Math.min(kelvin, minKelvin), maxKelvin);
rangeValue =
((limitedKelvin - minKelvin) / (maxKelvin - minKelvin)) * (outMaxVal - outMinVal) + outMinVal;
adapter.writeLog(`[ KelvinToRange ] Calculated ct-value (coolToWarm): ${rangeValue}`);
return rangeValue;
case "mired":
rangeValue = Math.floor(1000000 / kelvin);
rangeValue = Math.max(Math.min(rangeValue, outMaxVal), outMinVal);
adapter.writeLog(`[ KelvinToRange ] Calculated ct-value (mired): ${rangeValue}`);
return rangeValue;
default:
return -3;
}
} catch (error) {
adapter.errorHandling(
error,
"KelvinToRange",
`kelvin: ${kelvin}, outMaxCt: ${outMaxVal}, outMinCt: ${outMinVal}`,
);
return -1;
}
}
/**
* AdaptiveCt
* @param {object} adapter Adapter-Class
*/
async function AdaptiveCt(adapter) {
try {
const LightGroups = adapter.LightGroups;
const now = new Date();
const ActualTime = now.getTime();
const minCt = adapter.Settings.minCt || 2700;
const maxCt = adapter.Settings.maxCt || 6500;
const CtRange = maxCt - minCt;
adapter.writeLog(`[ AdaptiveCt ] minCT="${minCt}", maxCt="${maxCt}", CtRange="${CtRange}"`);
let adaptiveCtLinear = 0;
let adaptiveCtSolar = 0;
let adaptiveCtSolarInterpolated = 0;
let adaptiveCtTimed = 0;
let adaptiveCtTimedInterpolated = 0;
let sunset = 0;
let sunrise = 0;
let solarNoon = 0;
const [sunsetDate, sunriseDate, solarNoonDate] = await Promise.all([
getAstroDate(adapter, "sunset", undefined),
getAstroDate(adapter, "sunrise", undefined),
getAstroDate(adapter, "solarNoon", undefined),
]);
adapter.writeLog(
`[ AdaptiveCt // getAstroDate] sunsetDate="${sunsetDate}", sunriseDate="${sunriseDate}", solarNoonDate="${solarNoonDate}"`,
);
if (sunsetDate instanceof Date && sunriseDate instanceof Date && solarNoonDate instanceof Date) {
sunset = sunsetDate.getTime(); //Sonnenuntergang
sunrise = sunriseDate.getTime(); //Sonnenaufgang
solarNoon = solarNoonDate.getTime(); //Höchster Sonnenstand (Mittag)
} else {
adapter.writeLog(`[ AdaptiveCt ] sunsetDate, sunriseDate or solarNoonDate are no Date Objects"`, "warn");
return;
}
adapter.writeLog(
`[ AdaptiveCt ] minCT="${minCt}", maxCt="${maxCt}", sunset="${sunset}", sunrise="${sunrise}", solarNoon="${solarNoon}"`,
);
let morningTime = 0;
const sunMinutesDay = (sunset - sunrise) / 1000 / 60;
const RangePerMinute = CtRange / sunMinutesDay;
const sunpos = SunCalc.getPosition(now, adapter.lat, adapter.lng);
const sunposNoon = SunCalc.getPosition(solarNoon, adapter.lat, adapter.lng);
if (await compareTime(adapter, sunrise, solarNoon, "between", ActualTime)) {
// log("Aufsteigend")
adaptiveCtLinear = Math.round(minCt + ((ActualTime - sunrise) / 1000 / 60) * RangePerMinute * 2); // Linear = ansteigende Rampe von Sonnenaufgang bis Sonnenmittag, danach abfallend bis Sonnenuntergang
} else if (await compareTime(adapter, solarNoon, sunset, "between", ActualTime)) {
// log("Absteigend")
adaptiveCtLinear = Math.round(maxCt - ((ActualTime - solarNoon) / 1000 / 60) * RangePerMinute * 2);
}
if (await compareTime(adapter, sunrise, sunset, "between", ActualTime)) {
adaptiveCtSolar = Math.round(minCt + sunMinutesDay * RangePerMinute * sunpos.altitude); // Solar = Sinusrampe entsprechend direkter Elevation, max Ct differiert nach Jahreszeiten
adaptiveCtSolarInterpolated = Math.round(
minCt + sunMinutesDay * RangePerMinute * sunpos.altitude * (1 / sunposNoon.altitude),
); // SolarInterpolated = Wie Solar, jedoch wird der Wert so hochgerechnet dass immer zum Sonnenmittag maxCt gesetzt wird, unabhängig der Jahreszeit
}
adapter.writeLog(`[ AdaptiveCt ] adaptiveCtLinear="${adaptiveCtLinear}" adaptiveCtSolar="${adaptiveCtSolar}"`);
for (const Group in LightGroups) {
if (Group === "All") continue;
switch (LightGroups[Group].adaptiveCtMode) {
case "linear":
if (LightGroups[Group].adaptiveCt && LightGroups[Group].ct !== adaptiveCtLinear) {
await adapter.setStateAsync(Group + ".ct", adaptiveCtLinear, false);
}
break;
case "solar":
if (LightGroups[Group].adaptiveCt && LightGroups[Group].ct !== adaptiveCtSolar) {
await adapter.setStateAsync(Group + ".ct", adaptiveCtSolar, false);
}
break;
case "solarInterpolated":
if (LightGroups[Group].adaptiveCt && LightGroups[Group].ct !== adaptiveCtSolarInterpolated) {
await adapter.setStateAsync(Group + ".ct", adaptiveCtSolarInterpolated, false);
}
break;
case "timed":
if (LightGroups[Group].adaptiveCt && LightGroups[Group].ct !== adaptiveCtTimed) {
morningTime = (await getDateObject(LightGroups[Group].adaptiveCtTime)).getTime();
if (ActualTime >= morningTime && ActualTime <= sunset) {
adaptiveCtTimed = Math.round(
maxCt + ((minCt - maxCt) * (ActualTime - morningTime)) / (sunset - morningTime),
);
} else {
adaptiveCtTimed = minCt;
}
adapter.writeLog(
`[ AdaptiveCt // timed ] morningTime="${LightGroups[Group].adaptiveCtTime}" => "${morningTime}", ActualTime="${ActualTime}", sunset="${sunset}", adativeCtTimed="${adaptiveCtTimed}"`,
);
await adapter.setStateAsync(Group + ".ct", adaptiveCtTimed, false);
}
break;
case "timedInterpolated":
if (LightGroups[Group].adaptiveCt && LightGroups[Group].ct !== adaptiveCtTimedInterpolated) {
morningTime = (await getDateObject(LightGroups[Group].adaptiveCtTime)).getTime();
if (ActualTime >= morningTime && ActualTime <= sunset) {
const base = 2;
const timeFraction = (ActualTime - morningTime) / (sunset - morningTime);
const exponentialValue = Math.pow(base, timeFraction);
adaptiveCtTimedInterpolated = Math.round(
await map(exponentialValue, 1, base, maxCt, minCt),
);
} else {
adaptiveCtTimedInterpolated = minCt;
}
adapter.writeLog(
`[ AdaptiveCt // timedInterpolated ] morningTime="${LightGroups[Group].adaptiveCtTime}" => "${morningTime}", ActualTime="${ActualTime}", sunset="${sunset}", adativeCtTimed="${adaptiveCtTimedInterpolated}"`,
);
await adapter.setStateAsync(Group + ".ct", adaptiveCtTimedInterpolated, false);
}
break;
}
}
//Timeout 60s to restart function
if (adapter.TickerIntervall) adapter.clearTimeout(adapter.TickerIntervall);
adapter.TickerIntervall = setTimeout(async () => {
await AdaptiveCt(adapter);
}, 60000);
} catch (error) {
adapter.errorHandling(error, "AdaptiveCt");
}
}
/**
* Maps a value from one range to another range.
* @param {number} value - The value to map to the new range.
* @param {number} minInput - The minimum value of the input range.
* @param {number} maxInput - The maximum value of the input range.
* @param {number} minOutput - The minimum value of the output range.
* @param {number} maxOutput - The maximum value of the output range.
* @returns {Promise<number>} - The mapped value.
*/
async function map(value, minInput, maxInput, minOutput, maxOutput) {
return ((value - minInput) * (maxOutput - minOutput)) / (maxInput - minInput) + minOutput;
}
/**
* SetWhiteSubstituteColor
* @param {object} adapter Adapter-Class
* @param {string} Group Group of Lightgroups eg. LivingRoom, Children1,...
*/
async function SetWhiteSubstituteColor(adapter, Group) {
const LightGroups = adapter.LightGroups;
try {
if (!LightGroups[Group] || !LightGroups[Group].lights || !LightGroups[Group].lights.length) {
adapter.writeLog(
`[ SetWhiteSubstituteColor ] Not able to set white substitute color for Group = "${Group}". No lights are defined!!`,
"warn",
);
return;
}
const minCt = adapter.Settings.minCt || 2700;
const maxCt = adapter.GlobalSettings.maxCt || 6500;
adapter.writeLog(
`[ SetWhiteSubstituteColor ] Reaching for Group="${Group}" = "${LightGroups[Group].description}" LightGroups[Group].power="${LightGroups[Group].power}" LightGroups[Group].color="${LightGroups[Group].color}`,
"info",
);
//Nur ausführen bei anschalten und Farbe weiß
const promises = LightGroups[Group].lights
.filter(
(Light) =>
Light.ct &&
Light.color &&
LightGroups[Group].color.toUpperCase() == "#FFFFFF" &&
(LightGroups[Group].power || Light?.color?.sendColor),
)
.map(async (Light) => {
if (
!Light?.ct?.oid &&
Light?.color?.oid &&
Light?.color?.warmWhiteColor &&
Light?.color?.dayLightColor
) {
const colorValue =
LightGroups[Group].ct < (maxCt - minCt) / 4 + minCt
? Light.color.warmWhiteColor
: Light.color.dayLightColor;
await adapter.setForeignStateAsync(Light.color.oid, colorValue, false);
}
});
await Promise.all(promises);
} catch (error) {
adapter.errorHandling(error, "SetWhiteSubstituteColor");
}
}
/**
* SetColorMode
* @param {object} adapter Adapter-Class
* @param {string} Group Group of Lightgroups eg. LivingRoom, Children1,...
*/
async function SetColorMode(adapter, Group) {
const LightGroups = adapter.LightGroups;
try {
if (!LightGroups[Group] || !LightGroups[Group].lights || !LightGroups[Group].lights.length) {
adapter.writeLog(
`[ SetWhiteSubstituteColor ] Not able to set color mode for Group = "${Group}". No lights are defined!!`,
"warn",
);
return;
}
adapter.writeLog(`[ SetColorMode ] Reaching for Group="${Group}"`, "info");
const promises = LightGroups[Group].lights
.filter(
(Light) =>
Light.modeswitch &&
(LightGroups[Group].power || Light?.modeswitch?.sendModeswitch) &&
Light?.modeswitch?.oid,
) // Prüfen, ob der Datenpunkt vorhanden ist und die Bedingungen erfüllt sind
.map(async (Light) => {
if (LightGroups[Group].color.toUpperCase() == "#FFFFFF") {
// bei Farbe weiss
await Promise.all([
adapter.setForeignStateAsync(Light.modeswitch.oid, Light.modeswitch.whiteModeVal, false),
adapter.writeLog(`[ SetColorMode ] Device="${Light.modeswitch.oid}" to whiteMode`, "info"),
]);
} else {
// bei allen anderen Farben
await Promise.all([
adapter.setForeignStateAsync(Light.modeswitch.oid, Light.modeswitch.colorModeVal, false),
adapter.writeLog(`[ SetColorMode ] Device="${Light.modeswitch.oid}" to colorMode`, "info"),
]);
}
});
await Promise.all(promises);
return true;
} catch (error) {
adapter.errorHandling(error, "SetColorMode");
}
}
/**
* SetColor
* @param {object} adapter Adapter-Class
* @param {string} Group Group of Lightgroups eg. LivingRoom, Children1,...
* @param {any} Color
*/
async function SetColor(adapter, Group, Color) {
const LightGroups = adapter.LightGroups;
try {
if (!LightGroups[Group] || !LightGroups[Group].lights || !LightGroups[Group].lights.length) {
adapter.writeLog(
`[ SetWhiteSubstituteColor ] Not able to set color for Group = "${Group}". No lights are defined!!`,
"warn",
);
return;
}
adapter.writeLog(
`[ SetColor ] Reaching for Group="${Group}" power="${LightGroups[Group].power}" Color="${Color}"`,
"info",
);
const rgbTemp = await colorConv.ConvertHexToRgb(Color);
const promises = LightGroups[Group].lights
.filter((Light) => Light.color && (LightGroups[Group].power || Light?.color?.sendColor))
.map(async (Light) => {
if (Light?.color?.oid) {
// Prüfen ob Datenpunkt für Color vorhanden
switch (Light.color.colorType) {
case "hex":
await adapter.setForeignStateAsync(Light.color.oid, Color, false);
break;
case "rgb":
await adapter.setForeignStateAsync(Light.color.oid, rgbTemp, false);
break;
case "xy":
await adapter.setForeignStateAsync(
Light.color.oid,
await colorConv.ConvertRgbToXy(rgbTemp),
false,
);
break;
default:
await adapter.writeLog(
`[ SetColor ] Unknown colorType = "${Light.color.colorType}" in Group="${Group}", please specify!`,
"warn",
);
}
}
});
await Promise.all(promises);
await adapter.setStateAsync(Group + ".color", LightGroups[Group].color, true);
return true;
} catch (error) {
adapter.errorHandling(error, "SetColor", JSON.stringify(LightGroups[Group].color));
}
}
/**
* SetTt
* @description Set transmission time to lights
* @async
* @function
* @param {object} adapter Adapter-Class
* @param {object} Group Group of Lightgroups eg. LivingRoom, Children1,...
* @param {number} RampTime Information about the RampTime
* @param {string} prop rampUp, rampDown or standard
*/
/*
async function SetTt(adapter, Group, RampTime, prop) {
const LightGroups = adapter.LightGroups;
try {
if (!LightGroups[Group] || !LightGroups[Group].lights || !LightGroups[Group].lights.length) {
adapter.writeLog(
`[ SetWhiteSubstituteColor ] Not able to set transmission time for Group = "${Group}". No lights are defined!!`,
"warn",
);
return;
}
adapter.writeLog(`[ SetTt ] Reaching for Group="${Group}", RampTime="${RampTime}"`);
const promises = LightGroups[Group].lights
.filter((Light) => Light.tt?.oid)
.map(async (Light) => {
await Promise.all([
adapter.setForeignStateAsync(Light.tt.oid, RampTime),
adapter.writeLog(`[ SetTt ] Set ${Light.description} (${Light.tt.oid}) to: ${RampTime}`),
]);
});
await Promise.all(promises);
await adapter.setStateAsync(Group + "." + prop, RampTime, true);
return true;
} catch (error) {
adapter.errorHandling(error, "SetTt");
}
}
*/
/**
* PowerOnAftercare
* @param {object} adapter Adapter-Class
* @param {string} Group Group of Lightgroups eg. LivingRoom, Children1,...
* @param {number} bri Brighness 0 - 100 %
* @param {number} ct Color-Temperatur Kelvin
* @param {string} color Color HEX value
*/
async function PowerOnAftercare(
adapter,
Group,
bri = adapter.LightGroups[Group].bri,
ct = adapter.LightGroups[Group].ct,
color = adapter.LightGroups[Group].color,
) {
try {
const LightGroups = adapter.LightGroups;
adapter.writeLog(
`[ PowerOnAfterCare ] Reaching for Group="${Group}" bri="${bri}" ct="${ct}" color="${color}"`,
"info",
);
if (LightGroups[Group].power) {
//Nur bei anschalten ausführen
if (!LightGroups[Group].rampOn.enabled) {
//Wenn kein RampOn Helligkeit direkt setzen
if (LightGroups[Group].adaptiveBri) {
//Bei aktiviertem AdaptiveBri
await SetBrightness(adapter, Group, await AdaptiveBri(adapter, Group));
} else {
adapter.writeLog(`[ PowerOnAfterCare ] Now setting bri to ${bri}% for Group="${Group}"`, "info");
await SetBrightness(adapter, Group, bri);
}
}
await SetColor(adapter, Group, color); //Nach anschalten Color setzen
if (color == "#FFFFFF") await SetWhiteSubstituteColor(adapter, Group);
await SetColorMode(adapter, Group); //Nach anschalten Colormode setzen
if (color == "#FFFFFF") await SetCt(adapter, Group, ct); //Nach anschalten Ct setzen
}
} catch (error) {
adapter.errorHandling(error, "PowerOnAftercare");
}
}
module.exports = {
SetBrightness,
SetColor,
SetColorMode,
SetCt,
SetWhiteSubstituteColor,
AdaptiveCt,
AdaptiveBri,
setDeviceBri,
PowerOnAftercare,
};