I wanted to try to replicate the functionality of the Philips Hue Tap Dial but for all lights in a zone,
and I especially wanted the functionality where the dimmer only affects the lights that are already turned on.
So to try to replicate this I made a HomeyScript where I could send in the rotation from my Aqara Cube T1 Pro to do the dimming.
This seems to kind of work, but there are some issues with the lights going up and down again making me think that there might be some kind of race condition taking place, maybe the script is triggered again by the cube before the last flow finished?
Any suggestions on how to improve this would be highly appreciated
const minDimValue = 0.01;
// Parsing argument as json (Is there a better way to pass multiple arguments?)
const arg = args[0] ?? '{"relativeDim": -0.15, "zones": ["Stue", "Spisestue", "Kjøkken"]}';
var { relativeDim, zones } = JSON.parse(arg);
// Filtering out the zones where we want to dim the lights
const allZones = await Homey.zones.getZones();
var zoneIdsToDim = [];
for (var currentZoneId in allZones){
if(zones.includes(allZones[currentZoneId].name)) {
zoneIdsToDim .push(currentZoneId);
}
}
// Filtering out the lights that have dim control and is in the chosen zones
var allDevices = await Homey.devices.getDevices();
var lightsToControl = [];
for (key in allDevices) {
if(allDevices[key].class === 'light' && zoneIdsToDim.includes(allDevices[key].zone) && allDevices[key].capabilitiesObj?.dim){
lightsToControl.push(allDevices[key]);
}
}
var offDevices = []; // needed so we wont affect lights that are already off unless we are turning all on
var toBeTurnedOff = []; // We wont turn off any lights unless all lights turn off
var allOff = true; // keeping track if all lights are off or about to be turned off
var promises = []; // All promises so we can await them before exiting
for (light of lightsToControl) {
if (light.capabilitiesObj?.onoff.value === true) {
var newDimValue = light.capabilitiesObj.dim.value + relativeDim;
if(newDimValue <= minDimValue) {
toBeTurnedOff.push(light);
} else {
promises.push(light.setCapabilityValue('dim', newDimValue));
allOff = false
}
} else {
offDevices.push(light);
}
}
if(allOff && relativeDim > 0) { // If all lights are off and dim is increasing, turn on all lights
promises = offDevices.map(device => device.setCapabilityValue('dim', relativeDim));
} else if (allOff) { // If all lights are off or about to be turned off, turn off the rest
promises.push(toBeTurnedOff.map(device => device.setCapabilityValue('dim', 0)));
} else { // else set the ones that where about to be turned off to min dim level until all lights are turned off
promises.push(toBeTurnedOff.map(device => device.setCapabilityValue('dim', minDimValue)));
}
await Promise.all(promises); // Await all promises before exiting
Thanks, I added a “Write to timeline” at the end of my script and that confirmed that the scrips where ending out of order.
So I rewrote my script using a global variable to keep track of a queue of events and this seems to have helped, here is my my new script if anyone want to use or improve it:
const minDimValue = 0.01;
// Parsing argument as json (Is there a better way to pass multiple arguments?)
const arg = args[0] ?? '{"relativeDim": -0.05, "zones": ["Stue", "Spisestue", "Kjøkken"]}';
var { relativeDim, zones } = JSON.parse(arg);
const scriptStateTemplate = {
isRunning: false,
lastRun: new Date(),
dimQueue: [],
};
var scriptState = global.get('DimZonesRelative') ?? scriptStateTemplate;
scriptState.dimQueue.push({ relativeDim, zones });
const timeDiff = (new Date - new Date(scriptState.lastRun)) / 60000; // Minutes since last loop
// If script is already running and has been running for more than a minute, just add to queue and exit
// the minute check is to prevent the script for getting stuck in the running state if something went wrong
if (scriptState.isRunning && timeDiff < 1) {
global.set('DimZonesRelative', scriptState);
return;
} else {
scriptState.isRunning = true;
scriptState.lastRun = new Date();
global.set('DimZonesRelative', scriptState);
}
// Function to filter out the dimmable lights in selected zones
const getLights = (homeyZones, homeyDevices, zoneNames) => {
var selectedZones = [];
var lightsToControl = [];
for (var currentZoneId in homeyZones){
if(zoneNames.includes(homeyZones[currentZoneId].name)) {
selectedZones.push(currentZoneId);
}
}
for (key in homeyDevices) {
if(homeyDevices[key].class === 'light' && selectedZones.includes(homeyDevices[key].zone) && homeyDevices[key].capabilitiesObj?.dim){
lightsToControl.push(homeyDevices[key]);
}
}
return lightsToControl;
}
const allZones = await Homey.zones.getZones();
var allDevices = await Homey.devices.getDevices();
var tmpLightDimLevelsDictionary = {};
var firstRun = true;
while(scriptState.isRunning) {
scriptState = global.get('DimZonesRelative');
if (scriptState.dimQueue.length === 0) {
scriptState.isRunning = false;
global.set('DimZonesRelative', scriptState);
return;
}
var currentDim = scriptState.dimQueue.shift();
scriptState.lastRun = new Date(),
global.set('DimZonesRelative', scriptState);
lightsToControl = getLights(allZones, allDevices, currentDim.zones);
var offDevices = []; // needed so we wont affect lights that are already off unless all lights are off
var toBeTurnedOff = []; // We wont turn off lights unless all lights turn off
var allOff = true; // keeping track if all lights are off ot about to be turned off
var promises = []; // All promises so we can await them before exiting
for (light of lightsToControl) {
if (light.capabilitiesObj?.onoff.value === true) {
if (!tmpLightDimLevelsDictionary[light.id]) {
tmpLightDimLevelsDictionary[light.id] = light.capabilitiesObj.dim.value;
}
tmpLightDimLevelsDictionary[light.id] = tmpLightDimLevelsDictionary[light.id] + relativeDim;
var newDimValue = light.capabilitiesObj.dim.value + relativeDim;
if(tmpLightDimLevelsDictionary[light.id] <= minDimValue) {
toBeTurnedOff.push(light);
} else {
promises.push(light.setCapabilityValue('dim', tmpLightDimLevelsDictionary[light.id]));
allOff = false
}
} else {
offDevices.push(light);
}
}
if(allOff && relativeDim > 0) { // If all lights are off and dm is increasing, turn on all lights
if (firstRun) { // Due to possibility of a long queue I dont want all lights to turn on unless there has been a pause
promises = offDevices.map(device => {
tmpLightDimLevelsDictionary[device.id] = relativeDim;
return device.setCapabilityValue('dim', relativeDim);
});
}
} else if (allOff) { // If all lights are off or about to be turned off, turn off the rest
promises = toBeTurnedOff.map(device => {
tmpLightDimLevelsDictionary[device.id] = 0;
return device.setCapabilityValue('dim', 0);
});
} else { // else set the ones that where about to be turned off to min dim level untill all are ready to be turned off
promises.push(toBeTurnedOff.map(device => {
tmpLightDimLevelsDictionary[device.id] = minDimValue;
return device.setCapabilityValue('dim', minDimValue);
}));
}
await Promise.all(promises); // Await all promises before exiting
firstRun = false;
}