library (
author: "Mike Bishop",
name: "color-tools",
namespace: "evequefou",
description: "Shared components between Holiday Lights and Palette Scenes"
)
import groovy.transform.Field
// put methods, etc. here
@Field final static String PICKER_JS = ''
private String maybeBold(String text, Boolean bold) {
if (bold) {
return "${text}"
} else {
return text
}
}
private deviceSelector() {
def key;
def displayIndex = 0
debug("Device indices are ${state.deviceIndices}")
def deviceIndices = state.deviceIndices.clone();
for( def index in deviceIndices ) {
key = "device${index}";
debug("settings[${key}] is ${settings[key]}")
if( settings[key] == null ) {
// User unselected this device -- drop the index
state.deviceIndices.removeElement(index);
app.removeSetting(key);
}
else {
displayIndex += 1;
input key, "capability.colorControl", title: "RGB light ${displayIndex}",
multiple: false, submitOnChange: true
}
}
def index = state.nextDeviceIndex;
displayIndex += 1;
key = "device${index}";
input key, "capability.colorControl", title: "RGB light ${displayIndex}",
multiple: false, submitOnChange: true
if( settings[key] != null ) {
// User selected device in new slot
state.deviceIndices.add(index);
state.nextDeviceIndex += 1;
index = state.nextDeviceIndex;
displayIndex += 1;
key = "device${index}";
input key, "capability.colorControl", title: "RGB light ${displayIndex}",
multiple: false, submitOnChange: true
}
}
private drawPicker(inputKey) {
def inputId = "settings[${inputKey}]";
def colorOptions = COLORS.
collect{ "" }.
join("\n");
// First, the actual ColorMap input for a literal selection
// Everything else transfers its value here.
input inputKey, "COLOR_MAP", title: "", required: true, defaultValue: COLORS["White"]
// Next, inject a color picker (and its scripts) to help with setting
// the map:
def pickerId = "colorPicker-${inputKey}"
paragraph "" +
"", width: 5
// Then the preset options
paragraph "",width: 4
}
private displayOptions(prefix = "") {
input "${prefix}${ALIGNMENT}", "bool",
title: "Different colors on different lights?",
width: 4, submitOnChange: true, defaultValue: true
input "${prefix}${ROTATION}", "enum", title: "How to rotate colors",
width: 4, options: [
(RANDOM): "Random",
(STATIC): "Static",
(SEQUENTIAL): "Sequential"
], submitOnChange: true
def staggerKey = prefix + STAGGER;
def stagger = settings[staggerKey];
input staggerKey, "bool",
title: "Change lights ${maybeBold("simultaneously", !stagger)} " +
"or ${maybeBold("separately", stagger)}?",
width: 4, submitOnChange: true
if( !settings["${prefix}${ALIGNMENT}"] && settings["${prefix}${ROTATION}"] == STATIC) {
paragraph "Note: With this combination, only the first color will ever be used!"
}
}
@Field final static String ALIGNMENT = "Alignment"
@Field final static String ROTATION = "Rotation"
private drawColorSection(prefix, color, index) {
def inputKey = "${prefix}Color${color}"
debug("Color ${index+1} is ${settings[inputKey]}")
// For each existing color slot, display four things:
section("Color ${index+1}") {
// Map, picker, and presets displayed here
drawPicker(inputKey);
// And finally a delete button.
def delete = ""
def capPrefix = prefix ? prefix[0].toUpperCase() + prefix[1..-1] : "";
input "delete${capPrefix}Color${color}", "button", title: "${delete} Delete", submitOnChange: true, width: 3
}
}
private scheduleHandler(handlerName, frequency, recurring = true) {
unschedule(handlerName);
unschedule("runHandler");
if( frequency && recurring ) {
debug("Scheduling ${handlerName} every ${frequency} minutes");
def dFreq = Double.parseDouble(frequency);
state.spread = dFreq * 60;
if (dFreq < 1 ) {
runEvery1Minute("runHandler", [data: [
handlerName: handlerName,
interval: (int) (state.spread)
]]);
}
else {
switch(Integer.parseInt(frequency)) {
case 1:
runEvery1Minute(handlerName);
break;
case 5:
runEvery5Minutes(handlerName);
break;
case 10:
runEvery10Minutes(handlerName);
break;
case 15:
runEvery15Minutes(handlerName);
break;
case 30:
runEvery30Minutes(handlerName);
break;
case 60:
runEvery1Hour(handlerName);
break;
case 180:
runEvery3Hours(handlerName);
break;
default:
log.error "Invalid frequency: ${frequency.inspect()}";
}
}
}
this."${handlerName}"()
}
private runHandler(data) {
def handlerName = data.handlerName;
def interval = data.interval;
debug "Running ${handlerName} on interval ${interval}";
def delays = 0.step(60, interval) {
debug("Running ${handlerName} in ${it} seconds");
runIn(it, handlerName, [overwrite: false]);
};
}
private getColors(colorIndices, desiredLength, prefix = "") {
debug("getColors(${colorIndices}, ${desiredLength}, ${prefix})")
if( desiredLength == 0 ) {
return [];
}
def colors = colorIndices.collect{
try {
evaluate(settings["${prefix}Color${it}"])
}
catch(Exception ex) {
error(ex);
null
}
};
debug("Colors${prefix ? " for " + prefix : ""}: ${colors.inspect()}");
colors = colors.findAll{it && it.containsKey("hue") && it.containsKey("saturation") && it.containsKey("level")};
if( colors.size() <= 0 ) {
warn("No valid colors found!");
return [];
}
def mode = settings["${prefix}Rotation"];
def additional = mode == SEQUENTIAL ? colors.size() : 0;
def result = [];
// If we don't have enough colors, we'll need to repeat the colors.
while( result.size() < desiredLength + additional ) result += colors;
if( mode == RANDOM ) {
Collections.shuffle(result);
}
debug("Selected colors: ${result.inspect()}");
def offset = 0;
if( mode == SEQUENTIAL ) {
offset = state.sequentialIndex ?: 0;
state.sequentialIndex = (offset + 1) % (additional ?: 1);
debug("Starting from offset ${offset} (next is ${state.sequentialIndex})");
}
def subList = result[offset..<(offset + desiredLength)];
def reshuffles = 0;
while( colors.size() > 1 && mode == RANDOM && subList == state.lastColors && reshuffles < 10) {
reshuffles++;
debug("Colors are the same as last time; shuffling and trying again");
Collections.shuffle(result);
subList = result[offset..<(offset + desiredLength)];
}
debug("Sublist: ${subList.inspect()}");
state.lastColors = subList;
return subList;
}
def doLightUpdate(devices, colorIndices, prefix = "") {
// Assemble the list of devices to use.
if( settings[prefix + ALIGNMENT] ) {
// Multiple colors displayed simultaneously.
devices = devices.collect{ [it] };
}
else {
// Single color displayed at a time.
devices = [devices];
}
// Assemble the list of colors to apply.
def colors = getColors(
colorIndices,
devices.size(),
prefix
);
// Apply the colors to the devices.
def delays = colorIndices.size() > 1 ?
generateDelays(prefix, devices.size()) :
(0.. 1 ?
generateDelays(prefix, it[0].size()) :
(0.. [deviceAndTime[0], it[1], deviceAndTime[1] + it[2]] }
}.groupBy{it[2]}.
each {
def delay = it.key;
def deviceColorPairs = it.value.collect{ [device: it[0], color: it[1]] };
debug("Setting ${deviceColorPairs.inspect()} in ${delay} seconds")
runInMillis((Long) (delay * 1000), "setColor", [
data: [
deviceColorPairs: deviceColorPairs
],
overwrite: false
]);
}
}
private setColor(data) {
data.deviceColorPairs.groupBy{it.color.inspect()}.each{
def color = it.value[0].color;
def devices = it.value.collect{ hydrateDevice(it.device) };
debug("Setting ${devices} to ${color}");
devices*.setColor(color);
}
}
private hydrateDevice(deviceID) {
// This requires knowledge of the parent app's structure.
switch(state.appType) {
case HOLIDAY:
return settings[deviceID];
case PALLETTE_INSTANCE:
return parent.getRgbDevices().find{it.deviceNetworkId == deviceID};
default:
log.error "Invalid app type: ${state.appType}";
return null;
}
}
private generateDelays(prefix, count) {
if( count == 1 ) {
return [0];
}
def stagger = settings[prefix + STAGGER];
def result = [];
if( stagger ) {
0.step(state.spread, state.spread / count) {
result << it;
}
Collections.shuffle(result);
}
else {
result = (0..