================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/brightnessPI.js
================================================
/**
@file brightnessPI.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function BrightnessPI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion, isEncoder) {
// Init BrightnessPI
let instance = this;
// Inherit from PI
PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion);
// Before overwriting parent method, save a copy of it
let piLocalize = this.localize;
// Localize the UI
this.localize = () => {
// Call PIs localize method
piLocalize.call(instance);
// Localize the brightness label
document.getElementById('brightness-label').innerHTML = instance.localization['Brightness'];
if(isEncoder) {
document.getElementById('scaleticks-label').innerHTML = instance.localization['Scale Ticks'] || 'Scale Ticks';
}
};
const values = [1,2,3,4,5,10,20];
const selectedIndex = values.indexOf(Number(settings.scaleTicks));
// Add brightness slider
document.getElementById('placeholder').innerHTML = `
${this.getEncoderOptions(settings.scaleTicks, isEncoder)}
`;
console.log("value:", selectedIndex, settings.scaleTicks, typeof settings.scaleTicks, document.getElementById('placeholder').innerHTML);
// Initialize the tooltips
initToolTips();
// Add event listener
document.getElementById('brightness-input').addEventListener('change', brightnessChanged);
if(isEncoder) {
document.getElementById('scaleticks-input').addEventListener('change', scaleticksChanged);
}
// Brightness changed
function brightnessChanged(inEvent) {
// Save the new brightness settings
settings.brightness = inEvent.target.value;
instance.saveSettings();
// Inform the plugin that a new brightness is set
instance.sendToPlugin({
piEvent: 'valueChanged',
});
}
function scaleticksChanged(inEvent) {
settings.scaleTicks = inEvent.target.value;
instance.saveSettings();
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/brightnessRelPI.js
================================================
/**
@file brightnessRelPI.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function BrightnessRelPI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) {
// Init BrightnessPI
let instance = this;
// Inherit from PI
PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion);
// Before overwriting parent method, save a copy of it
let piLocalize = this.localize;
// Localize the UI
this.localize = function() {
// Call PIs localize method
piLocalize.call(instance);
// Localize the brightness label
document.getElementById('brightness-rel-label').innerHTML = instance.localization['Steps'];
};
// Add steps slider
document.getElementById('placeholder').innerHTML = `
`;
// Initialize the tooltips
initToolTips();
// Add event listener
document.getElementById('brightness-rel-input').addEventListener('change', brightnessRelChanged);
// Brightness changed
function brightnessRelChanged(inEvent) {
// Save the new brightness settings
settings.brightnessRel = inEvent.target.value;
instance.saveSettings();
// Inform the plugin that a new brightness is set
instance.sendToPlugin({ 'piEvent': 'valueChanged' });
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/colorPI.js
================================================
/**
@file colorPI.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function ColorPI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) {
// Init ColorPI
let instance = this;
// Inherit from PI
PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion);
// Add event listener
document.getElementById('light-select').addEventListener('change', lightChanged);
// Color changed
function colorChanged(inEvent) {
// Get the selected color
let color = inEvent.target.value;
// If the color is hex
if (color.charAt(0) === '#') {
// Convert the color to HSV
let hsv = Bridge.hex2hsv(color);
// Check if the color is valid
if (hsv.v !== 1) {
// Remove brightness component
hsv.v = 1;
// Set the color to the corrected color
color = Bridge.hsv2hex(hsv);
}
}
// Save the new color
settings.color = color;
instance.saveSettings();
// Inform the plugin that a new color is set
instance.sendToPlugin({
piEvent: 'valueChanged',
});
}
// Light changed
function lightChanged() {
// Get the light value manually
// Because it is not set if this function was triggered via a CustomEvent
let lightID = document.getElementById('light-select').value;
// Don't show any color picker if no light or group is set
if (lightID === 'no-lights' || lightID === 'no-groups') {
return;
}
// Check if any bridge is configured
if (!('bridge' in settings)) {
return;
}
// Check if the configured bridge is in the cache
if (!(settings.bridge in cache)) {
return;
}
// Find the configured bridge
let bridgeCache = cache[settings.bridge];
// Check if the selected light or group is in the cache
if (!(lightID in bridgeCache.lights || lightID in bridgeCache.groups)) {
return;
}
// Get light or group cache
let lightCache;
if (lightID.indexOf('l') !== -1) {
lightCache = bridgeCache.lights[lightID];
}
else {
lightCache = bridgeCache.groups[lightID];
}
// Add full color picker or only temperature slider
let colorPicker;
if (lightCache.xy !== null) {
colorPicker = `
${instance.localization['Color']}
`;
}
else {
colorPicker = `
${instance.localization['Temperature']}
`;
}
// Add color picker
document.getElementById('placeholder').innerHTML = colorPicker;
// Initialize the tooltips
initToolTips();
// Add event listener
document.getElementById('color-input').addEventListener('change', colorChanged);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/cyclePI.js
================================================
/**
@file cyclePI.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function CyclePI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) {
// Init CyclePI
let instance = this;
// Maximum amount of Colors
let maxColors = 10;
// Current amount of Colors
let curColors = settings?.colors?.length || 0;
// Default color for new pickers
let defaultColor = "#ffffff";
// Default temperature for new pickers
let defaultTemperature = 2000;
// Inherit from PI
PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion);
// Add event listener
document.getElementById('light-select').addEventListener('change', lightChanged);
// Color changed
function colorChanged(inEvent) {
// Get the selected index and color
let index = inEvent.target.dataset.id;
let color = inEvent.target.value;
// If the color is hex
if (color.charAt(0) === '#') {
// Convert the color to HSV
let hsv = Bridge.hex2hsv(color);
// Check if the color is valid
if (hsv.v !== 1) {
// Remove brightness component
hsv.v = 1;
// Set the color to the corrected color
color = Bridge.hsv2hex(hsv);
}
}
// Save the new color
settings.colors[index] = color;
instance.saveSettings();
// Inform the plugin that a new color is set
instance.sendToPlugin({
piEvent: 'valueChanged',
});
}
// Light changed
function lightChanged() {
// Get the light value manually
// Because it is not set if this function was triggered via a CustomEvent
let lightID = document.getElementById('light-select').value;
// Don't show any color picker if no light or group is set
if (lightID === 'no-lights' || lightID === 'no-groups') {
return;
}
// Check if any bridge is configured
if (!('bridge' in settings)) {
return;
}
// Check if the configured bridge is in the cache
if (!(settings.bridge in cache)) {
return;
}
// Find the configured bridge
let bridgeCache = cache[settings.bridge];
// Check if the selected light or group is in the cache
if (!(lightID in bridgeCache.lights || lightID in bridgeCache.groups)) {
return;
}
// Get light or group cache
let lightCache;
if (lightID.indexOf('l') !== -1) {
lightCache = bridgeCache.lights[lightID];
}
else {
lightCache = bridgeCache.groups[lightID];
}
// Get html of color picker or temperature slider
let getColorPicker = i => {
let colorIndex = i - 1;
if (lightCache.xy != null) {
if (i === 0) {
return `
${instance.localization['Colors']}
`;
}
else {
return `
`;
}
}
else if (i > 0) {
return `
${instance.localization['Temperature']} ${i}
`;
}
return '';
};
let placeholder = document.getElementById('placeholder');
// Add a new color picker to document
let addColorPicker = i => {
let picker = document.createElement('div');
const cphtml = getColorPicker(i).trim();
picker.innerHTML = cphtml;
if (lightCache.xy != null) {
document.querySelector('#color-input-container .sdpi-item-value').append(picker.firstChild);
}
else {
placeholder.insertBefore(picker.firstChild, document.getElementById('cycle-buttons'));
}
document.getElementById('color-input-' + (i - 1)).addEventListener('change', colorChanged);
};
// Add first color pickers container and buttons
placeholder.innerHTML = getColorPicker(0) + `
`;
// Initial create color pickers from settings
for (let n = 1; n <= settings.colors.length; n++) {
addColorPicker(n);
}
// Get buttons for later usage
let addButton = document.getElementById('add-color');
let removeButton = document.getElementById('remove-color');
let checkButtonStates = () => {
// Hide add button when reached max color pickers
addButton.style.display = curColors >= maxColors ? 'none' : 'inline-block';
// Hide remove button when only two color pickers left
removeButton.style.display = curColors <= 2 ? 'none' : 'inline-block';
};
// Event listener for add color
addButton.addEventListener('click', () => {
addColorPicker((++curColors));
// Add new picker value to settings
let colorIndex = curColors - 1;
if (!settings.colors[colorIndex]) {
if (lightCache.xy != null) {
settings.colors[colorIndex] = defaultColor;
}
else {
settings.colors[colorIndex] = defaultTemperature;
}
instance.saveSettings();
}
checkButtonStates();
});
// Event listener for remove last color
removeButton.addEventListener('click', () => {
document.getElementById('color-input-container-' + (--curColors)).remove();
// Remove color from settings
settings.colors = settings.colors.splice(0, settings.colors.length - 1);
instance.saveSettings();
checkButtonStates();
});
// Initial button states
checkButtonStates();
// Initialize the tooltips
initToolTips();
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/main.js
================================================
/**
@file main.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Global web socket
var websocket = null;
// Global plugin settings
var globalSettings = {};
// Global settings
var settings = {};
// Global cache
var cache = {};
// Setup the websocket and handle communication
function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
// Parse parameter from string to object
let actionInfo = JSON.parse(inActionInfo);
let info = JSON.parse(inInfo);
let isEncoder = actionInfo?.payload?.controller == 'Encoder';
let streamDeckVersion = info['application']['version'];
let pluginVersion = info['plugin']['version'];
// Save global settings
settings = actionInfo['payload']['settings'];
// Retrieve language
let language = info['application']['language'];
// Retrieve action identifier
let action = actionInfo['action'];
// Open the web socket to Stream Deck
// Use 127.0.0.1 because Windows needs 300ms to resolve localhost
websocket = new WebSocket(`ws://127.0.0.1:${inPort}`);
// WebSocket is connected, send message
websocket.onopen = () => {
// Register property inspector to Stream Deck
registerPluginOrPI(inRegisterEvent, inUUID);
// Request the global settings of the plugin
requestGlobalSettings(inUUID);
};
// Create actions
let pi;
if (action === 'com.elgato.philips-hue.power') {
pi = new PowerPI(inUUID, language, streamDeckVersion, pluginVersion);
}
else if (action === 'com.elgato.philips-hue.color') {
pi = new ColorPI(inUUID, language, streamDeckVersion, pluginVersion);
}
else if (action === 'com.elgato.philips-hue.cycle') {
pi = new CyclePI(inUUID, language, streamDeckVersion, pluginVersion);
}
else if (action === 'com.elgato.philips-hue.brightness') {
pi = new BrightnessPI(inUUID, language, streamDeckVersion, pluginVersion, isEncoder);
}
else if (action === 'com.elgato.philips-hue.temperature') {
pi = new TemperaturePI(inUUID, language, streamDeckVersion, pluginVersion, isEncoder);
}
else if (action === 'com.elgato.philips-hue.scene') {
pi = new ScenePI(inUUID, language, streamDeckVersion, pluginVersion);
}
else if (action === 'com.elgato.philips-hue.brightness-rel') {
pi = new BrightnessRelPI(inUUID, language, streamDeckVersion, pluginVersion);
}
websocket.onmessage = msg => {
// Received message from Stream Deck
let jsonObj = JSON.parse(msg.data);
let event = jsonObj['event'];
let jsonPayload = jsonObj['payload'];
if (event === 'didReceiveGlobalSettings') {
// Set global plugin settings
globalSettings = jsonPayload['settings'];
}
else if (event === 'didReceiveSettings') {
// Save global settings after default was set
settings = jsonPayload['settings'];
}
else if (event === 'sendToPropertyInspector') {
// Save global cache
cache = jsonPayload;
// Load bridges and lights
pi.loadBridges();
}
};
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/pi.js
================================================
/**
@file pi.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function PI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) {
// Init PI
let instance = this;
const values = [1,2,3,4,5,10,20];
this.getEncoderOptions = (settingsValue, forEncoder) => {
const selectedIndex = values.indexOf(Number(settingsValue));
return forEncoder === true ? `
`: ''
};
// Public localizations for the UI
this.localization = {};
// Add event listener
document.getElementById('bridge-select').addEventListener('change', bridgeChanged);
document.getElementById('light-select').addEventListener('change', lightsChanged);
document.addEventListener('saveBridge', setupCallback);
// Load the localizations
getLocalization(inLanguage, (inStatus, inLocalization) => {
if (inStatus) {
// Save public localization
instance.localization = inLocalization['PI'];
// Localize the PI
instance.localize();
}
else {
log(inLocalization);
}
});
// Localize the UI
this.localize = () => {
// Check if localizations were loaded
if (instance.localization == null) {
return;
}
// Localize the bridge select
document.getElementById('bridge-label').innerHTML = instance.localization['Bridge'];
document.getElementById('no-bridges').innerHTML = instance.localization['NoBridges'];
document.getElementById('add-bridge').innerHTML = instance.localization['AddBridge'];
// Localize the light and group select
document.getElementById('lights-label').innerHTML = instance.localization['Lights'];
document.getElementById('lights').label = instance.localization['LightsTitle'];
document.getElementById('no-lights').innerHTML = instance.localization['NoLights'];
document.getElementById('no-groups').innerHTML = instance.localization['NoGroups'];
// Groups label is removed for scenes PI
if (document.getElementById('groups') != null) {
document.getElementById('groups').label = instance.localization['GroupsTitle'];
}
};
// Show all paired bridges
this.loadBridges = () => {
// Remove previously shown bridges
let bridges = document.getElementsByClassName('bridges');
while (bridges.length > 0) {
bridges[0].parentNode.removeChild(bridges[0]);
}
// Check if any bridge is paired
if (Object.keys(cache).length > 0) {
// Hide the 'No Bridges' option
document.getElementById('no-bridges').style.display = 'none';
// Sort the bridges alphabetically
let bridgeIDsSorted = Object.keys(cache).sort((a, b) => {
return cache[a].name.localeCompare(cache[b].name);
});
// Add the bridges
bridgeIDsSorted.forEach(inBridgeID => {
// Add the group
let option = `
`;
document.getElementById('no-bridges').insertAdjacentHTML('beforebegin', option);
});
// Check if the bridge is already configured
if (settings.bridge !== undefined) {
// Select the currently configured bridge
document.getElementById('bridge-select').value = settings.bridge;
}
// Load the lights
loadLights();
}
else {
// Show the 'No Bridges' option
document.getElementById('no-bridges').style.display = 'block';
}
// Show PI
document.getElementById('pi').style.display = 'block';
}
// Show all lights
function loadLights() {
// Check if any bridge is configured
if (!('bridge' in settings)) {
return;
}
// Check if the configured bridge is in the cache
if (!(settings.bridge in cache)) {
return;
}
// Find the configured bridge
let bridgeCache = cache[settings.bridge];
// Remove previously shown lights
let lights = document.getElementsByClassName('lights');
while (lights.length > 0) {
lights[0].parentNode.removeChild(lights[0]);
}
let requireTemperature = instance instanceof ColorPI || instance instanceof TemperaturePI;
// Check if the bridge has at least one light
if (Object.keys(bridgeCache.lights).length > 0) {
// Hide the 'No Light' option
document.getElementById('no-lights').style.display = 'none';
// Sort the lights alphabetically
let lightIDsSorted = Object.keys(bridgeCache.lights).sort((a, b) => {
return bridgeCache.lights[a].name.localeCompare(bridgeCache.lights[b].name);
});
// Add the lights
lightIDsSorted.forEach(inLightID => {
let light = bridgeCache.lights[inLightID];
// Check if this is a color action and the lights supports colors
if (!(requireTemperature && light.temperature == null && light.xy == null)) {
// Add the light
let option = `
`;
document.getElementById('no-lights').insertAdjacentHTML('beforebegin', option);
}
});
}
else {
// Show the 'No Light' option
document.getElementById('no-lights').style.display = 'block';
}
// Remove previously shown groups
let groups = document.getElementsByClassName('groups');
while (groups.length > 0) {
groups[0].parentNode.removeChild(groups[0]);
}
// Check if the bridge has at least one group
if (Object.keys(bridgeCache.groups).length > 0) {
// Hide the 'No Group' option
document.getElementById('no-groups').style.display = 'none';
// Sort the groups alphabetically
let groupIDsSorted = Object.keys(bridgeCache.groups).sort((a, b) => {
return bridgeCache.groups[a].name.localeCompare(bridgeCache.groups[b].name);
});
// Add the groups
groupIDsSorted.forEach(inGroupID => {
let group = bridgeCache.groups[inGroupID];
// Check if this is a color action and the lights supports colors
if (!(instance instanceof ColorPI && group.temperature == null && group.xy == null)) {
// Add the group
let option = `
`;
document.getElementById('no-groups').insertAdjacentHTML('beforebegin', option);
}
});
}
else {
// Show the 'No Group' option
document.getElementById('no-groups').style.display = 'block';
}
// Check if a light is already setup
if (settings.light !== undefined) {
// Check if the configured light or group is part of the bridge cache
if (!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) {
return;
}
// Select the currently configured light or group
document.getElementById('light-select').value = settings.light;
// Dispatch light change event manually
// So that the colorPI can set the correct color picker at initialization
document.getElementById('light-select').dispatchEvent(new CustomEvent('change', {'detail': {'manual': true}} ));
}
// If this is a scene PI
if (instance instanceof ScenePI) {
//Load the scenes
instance.loadScenes();
}
}
// Function called on successful bridge pairing
function setupCallback(inEvent) {
// Set bridge to the newly added bridge
settings.bridge = inEvent.detail.id;
instance.saveSettings();
// Check if global settings need to be initialized
if (globalSettings.bridges === undefined) {
globalSettings.bridges = {};
}
// Add new bridge to the global settings
globalSettings.bridges[inEvent.detail.id] = {
ip: inEvent.detail.ip,
id: inEvent.detail.id,
username: inEvent.detail.username,
};
saveGlobalSettings(inContext);
}
// Bridge select changed
function bridgeChanged(inEvent) {
if (inEvent.target.value === 'add') {
// Open setup window
window.open(`../setup/index.html?language=${inLanguage}&streamDeckVersion=${inStreamDeckVersion}&pluginVersion=${inPluginVersion}`);
// Select the first in case user cancels the setup
document.getElementById('bridge-select').selectedIndex = 0;
}
else if (inEvent.target.value === 'no-bridges') {
// If no bridge was selected, do nothing
}
else {
settings.bridge = inEvent.target.value;
instance.saveSettings();
instance.loadBridges();
}
}
// Light select changed
function lightsChanged(inEvent) {
if (inEvent.target.value === 'no-lights' || inEvent.target.value === 'no-groups') {
// If no light or group was selected, do nothing
}
else if (inEvent.detail !== undefined) {
// If the light was changed via code
if (inEvent.detail.manual === true) {
// do nothing
}
}
else {
settings.light = inEvent.target.value;
instance.saveSettings();
// If this is a scene PI
if (instance instanceof ScenePI) {
//Load the scenes
instance.loadScenes();
}
instance.sendToPlugin({
piEvent: 'lightsChanged',
});
}
}
// Private function to return the action identifier
function getAction() {
let action
// Find out type of action
if (instance instanceof PowerPI) {
action = 'com.elgato.philips-hue.power';
}
else if (instance instanceof ColorPI) {
action = 'com.elgato.philips-hue.color';
}
else if (instance instanceof CyclePI) {
action = 'com.elgato.philips-hue.cycle';
}
else if (instance instanceof BrightnessPI) {
action = 'com.elgato.philips-hue.brightness';
}
else if (instance instanceof TemperaturePI) {
action = 'com.elgato.philips-hue.temperature';
}
else if (instance instanceof BrightnessRelPI) {
action = 'com.elgato.philips-hue.brightness-rel';
}
else if (instance instanceof ScenePI) {
action = 'com.elgato.philips-hue.scene';
}
return action;
}
// Public function to save the settings
this.saveSettings = () => {
saveSettings(getAction(), inContext, settings);
}
// Public function to send data to the plugin
this.sendToPlugin = inData => {
sendToPlugin(getAction(), inContext, inData);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/powerPI.js
================================================
/**
@file powerPI.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function PowerPI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) {
// Inherit from PI
PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion);
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/scenePI.js
================================================
/**
@file scenePI.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function ScenePI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) {
// Init ScenePI
let instance = this;
// Inherit from PI
PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion);
// Hide lights from light select
document.getElementById('lights').style.display = 'none';
// Remove groups label from lights select
let groups = document.getElementById('groups');
let groupsChildren = document.getElementById('groups').children;
let lightSelect = document.getElementById('light-select');
lightSelect.removeChild(groups);
lightSelect.appendChild(groupsChildren[0]);
// Before overwriting parent method, save a copy of it
let piLocalize = this.localize;
// Localize the UI
this.localize = () => {
// Call PIs localize method
piLocalize.call(instance);
// Localize the scene select
document.getElementById('lights-label').innerHTML = instance.localization['Group'];
document.getElementById('scene-label').innerHTML = instance.localization['Scene'];
document.getElementById('no-scenes').innerHTML = instance.localization['NoScenes'];
};
// Add scene select
document.getElementById('placeholder').innerHTML = `
`;
// Add event listener
document.getElementById('scene-select').addEventListener('change', sceneChanged);
// Scenes changed
function sceneChanged(inEvent) {
if (inEvent.target.value === 'no-scenes') {
// do nothing
}
else {
// Save the new scene settings
settings.scene = inEvent.target.value;
instance.saveSettings();
// Inform the plugin that a new scene is set
instance.sendToPlugin({
piEvent: 'valueChanged',
});
}
}
// Show all scenes
this.loadScenes = () => {
// Check if any bridge is configured
if (!('bridge' in settings)) {
return;
}
// Check if the configured bridge is in the cache
if (!(settings.bridge in cache)) {
return;
}
// Find the configured bridge
let bridgeCache = cache[settings.bridge];
// Check if any light is configured
if (!('light' in settings)) {
return;
}
// Check if the light was set to a group
if (!(settings.light.indexOf('g-') !== -1)) {
return;
}
// Check if the configured group is in the cache
if (!(settings.light in bridgeCache.groups)) {
return;
}
// Find the configured group
let groupCache = bridgeCache.groups[settings.light];
// Remove previously shown scenes
let scenes = document.getElementsByClassName('scenes');
while (scenes.length > 0) {
scenes[0].parentNode.removeChild(scenes[0]);
}
// Check if the group has at least one scene
if (Object.keys(groupCache.scenes).length > 0) {
// Hide the 'No Scenes' option
document.getElementById('no-scenes').style.display = 'none';
// Sort the scenes alphabetically
let sceneIDsSorted = Object.keys(groupCache.scenes).sort((a, b) => {
return groupCache.scenes[a].name.localeCompare(groupCache.scenes[b].name);
});
// Add the scenes
sceneIDsSorted.forEach((inSceneID) => {
// Add the scene
let scene = groupCache.scenes[inSceneID];
let option = ``;
document.getElementById('no-scenes').insertAdjacentHTML('beforebegin', option);
});
}
else {
// Show the 'No Scenes' option
document.getElementById('no-scenes').style.display = 'block';
}
// Check if scene is already setup
if (settings.scene !== undefined) {
// Check if the configured scene is in this group
if (!(settings.scene in groupCache.scenes)) {
return;
}
// Select the currently configured scene
document.getElementById('scene-select').value = settings.scene;
}
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/temperaturePI.js
================================================
/**
@file temperaturePI.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function TemperaturePI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion, isEncoder) {
// Init TemperaturePI
let instance = this;
// Inherit from PI
PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion);
// Before overwriting parent method, save a copy of it
let piLocalize = this.localize;
// Localize the UI
this.localize = () => {
// Call PIs localize method
piLocalize.call(instance);
// Localize the brightness label
document.getElementById('temperature-label').innerHTML = instance.localization['Temperature'];
if(isEncoder) {
document.getElementById('scaleticks-label').innerHTML = instance.localization['Scale Ticks'] || 'Scale Ticks';
}
};
// Add brightness slider
document.getElementById('placeholder').innerHTML = `
0%100%
${this.getEncoderOptions(settings.scaleTicks, isEncoder)}
`;
// Initialize the tooltips
initToolTips();
// Add event listener
document.getElementById('temperature-input').addEventListener('input', temperatureChanged);
if(isEncoder) {
document.getElementById('scaleticks-input').addEventListener('change', scaleticksChanged);
}
// Brightness changed
function temperatureChanged(inEvent) {
// Save the new brightness settings
settings.temperature = inEvent.target.value;
instance.saveSettings();
// Inform the plugin that a new brightness is set
instance.sendToPlugin({
piEvent: 'valueChanged',
});
}
function scaleticksChanged(inEvent) {
settings.scaleTicks = inEvent.target.value;
instance.saveSettings();
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/tooltips.js
================================================
//==============================================================================
/**
@file tooltips.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
This source code is licensed under the MIT-style license found in the LICENSE file.
**/
//==============================================================================
function rangeToPercent(value, min, max) {
return ((value - min) / (max - min));
}
function initToolTips() {
const tooltip = document.querySelector('.sdpi-info-label');
const arrElements = document.querySelectorAll('.floating-tooltip');
arrElements.forEach((e,i) => {
initToolTip(e, tooltip)
})
}
function initToolTip(element, tooltip) {
const tw = tooltip.getBoundingClientRect().width;
const suffix = element.getAttribute('data-suffix') || '';
const updateTooltip = () => {
const elementRect = element.getBoundingClientRect();
const w = elementRect.width - tw / 2;
const percent = rangeToPercent(
element.value,
element.min,
element.max,
);
tooltip.textContent = suffix !== '' ? `${element.value} ${suffix}` : String(element.value);
tooltip.style.left = `${elementRect.left + Math.round(w * percent) - tw / 4}px`;
tooltip.style.top = `${elementRect.top - 32}px`;
};
if (element) {
element.addEventListener('mouseenter', () => {
tooltip.classList.remove('hidden');
tooltip.classList.add('shown');
updateTooltip();
}, false);
element.addEventListener('mouseout', () => {
tooltip.classList.remove('shown');
tooltip.classList.add('hidden');
updateTooltip();
}, false);
element.addEventListener('input', updateTooltip, false);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/index.html
================================================
com.elgato.philips-hue
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/action.js
================================================
/**
@file action.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Prototype which represents an action
function Action(inContext, inSettings, jsn) {
// Init Action
let instance = this;
let debounceDelay = 50;
// Private variable containing the context of the action
let context = inContext;
this.isEncoder = jsn?.payload?.controller == 'Encoder';
this.isInMultiAction = jsn?.payload?.isInMultiAction;
this.savedValue = -1;
this.savedPower = null;
// Private variable containing the settings of the action
let settings = inSettings;
let updateActionsEvent = new CustomEvent('updateActions', {detail: {sender: this}} );
// Set the default values
setDefaults();
// Public function returning the context
this.getContext = () => {
return context;
};
// Public function returning the settings
this.getSettings = () => {
return settings;
};
// Public function for settings the settings
this.setSettings = inSettings => {
settings = inSettings;
};
// Public function called when new cache is available
this.newCacheAvailable = inCallback => {
// Set default settings
setDefaults(inCallback);
};
this.updateAllActions = () => {
document.dispatchEvent(updateActionsEvent);
};
this.updateActionIfCacheAvailable = (ctx) => {
// update the action and its display
const cacheSize = Object.keys(cache.data).length;
if(cacheSize === 0) {
// after a willAppear event, the cache is not yet available
wait(1000).then(() => {
this.updateAction();
});
} else {
this.updateAction();
}
}
this.setFeedback = (context, value, opacity) => {
console.assert(websocket, 'no connection to websocket');
if(websocket && this.isEncoder) {
// send the values to the encoder (SD+)
setFeedback(context, {
value: {
value,
opacity
},
indicator: {
value,
opacity
}
});
}
};
this.updateDisplay = (lightOrGroup, property) => {
if(!lightOrGroup) {
if(!this.getCurrentLightOrGroup) return;
const curLightOrGroup = this.getCurrentLightOrGroup();
if(curLightOrGroup) {
lightOrGroup = curLightOrGroup.objCache;
this.savedValue = -1; // force update
}
console.assert(lightOrGroup, 'no light or group', curLightOrGroup);
if(!lightOrGroup) return;
};
if(this.isInMultiAction || !this.isEncoder) return;
const powerHue = property == 'power' ? !lightOrGroup?.power : lightOrGroup?.power;
let actionValue = lightOrGroup?.[this.property];
// check if the values have changed
if(actionValue === this.savedValue && powerHue === this.savedPower) {
return;
}
// cache the values
this.savedValue = actionValue;
this.savedPower = powerHue;
// values in hue are 0-254, convert to 0-100 // !this is not true for temperature
let value;
if(this.property == 'temperature') {
const ct = lightOrGroup.originalValue?.capabilities?.control?.ct;
console.assert(ct, 'no ct in capabilities', lightOrGroup);
if(!ct) return;
value = parseInt(Utils.percent(lightOrGroup.temperature, ct.min, ct.max));
} else {
value = parseInt(actionValue / 2.54);
}
// if the light is off, set the opacity to 0.5
const opacity = powerHue ? 1 :0.5;
this.setFeedback(inContext, value, opacity);
};
this.togglePower = (inContext) => {
const target = this.getCurrentLightOrGroup();
if(!target) return;
const targetState = !target.objCache.power;
target.obj.setPower(targetState, (success, error) => {
if (success) {
target.objCache.power = targetState;
// cache.refresh();
this.updateAllActions();
}
else {
log(error);
showAlert(inContext);
}
});
return target;
};
this.getVerifiedSettings = function(inContext, requiredPropertySetting = null) {
// Check if any bridge is configured
if(!('bridge' in settings)) {
log('No bridge configured');
showAlert(inContext);
return false;
}
// Check if the configured bridge is in the cache
if(!(settings.bridge in cache.data)) {
log(`Bridge ${settings.bridge} not found in cache`);
showAlert(inContext);
return false;
}
// Check if any light is configured
if(!('light' in settings)) {
log('No light or group configured');
showAlert(inContext);
return false;
}
if(requiredPropertySetting) {
if(!(requiredPropertySetting in settings)) {
log(`No ${requiredPropertySetting} configured`);
showAlert(inContext);
return;
}
}
// Find the configured bridge
let bridgeCache = cache.data[settings.bridge];
if(bridgeCache === false) {
console.warn('getVerifiedSettings: no bridge in cache');
return false;
};
// Check if the configured light or group is in the cache
if(!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) {
log(`Light or group ${settings.light} not found in cache`, settings, bridgeCache);
showAlert(inContext);
return false;
}
return settings;
};
// Private function to set the defaults
function setDefaults(inCallback) {
// If at least one bridge is paired
if (!(Object.keys(cache.data).length > 0)) {
// If a callback function was given
if (inCallback !== undefined) {
// Execute the callback function
inCallback();
}
return;
}
// Find out type of action
let action;
if (instance instanceof PowerAction) {
action = 'com.elgato.philips-hue.power';
}
else if (instance instanceof ColorAction) {
action = 'com.elgato.philips-hue.color';
}
else if (instance instanceof CycleAction) {
action = 'com.elgato.philips-hue.cycle';
}
else if (instance instanceof BrightnessAction) {
action = 'com.elgato.philips-hue.brightness';
}
else if (instance instanceof BrightnessRelAction) {
action = 'com.elgato.philips-hue.brightness-rel';
}
else if (instance instanceof SceneAction) {
action = 'com.elgato.philips-hue.scene';
}
// If no bridge is set for this action
if (!('bridge' in settings)) {
// Sort the bridges alphabetically
let bridgeIDsSorted = Object.keys(cache.data).sort((a, b) => {
return cache.data[a].name.localeCompare(cache.data[b].name);
});
// Set the bridge automatically to the first one
settings.bridge = bridgeIDsSorted[0];
// Save the settings
saveSettings(action, inContext, settings);
}
// Find the configured bridge
let bridgeCache = cache.data[settings.bridge];
// If no light is set for this action
if (!('light' in settings)) {
// First try to set a group, because scenes only support groups
// If the bridge has at least one group
if (Object.keys(bridgeCache.groups).length > 0) {
// Sort the groups automatically
let groupIDsSorted = Object.keys(bridgeCache.groups).sort((a, b) => {
return bridgeCache.groups[a].name.localeCompare(bridgeCache.groups[b].name);
});
// Set the light automatically to the first group
settings.light = groupIDsSorted[0];
// Save the settings
saveSettings(action, inContext, settings);
}
else if (Object.keys(bridgeCache.lights).length > 0) {
// Sort the lights automatically
let lightIDsSorted = Object.keys(bridgeCache.lights).sort((a, b) => {
return bridgeCache.lights[a].name.localeCompare(bridgeCache.lights[b].name);
});
// Set the light automatically to the first light
settings.light = lightIDsSorted[0];
// Save the settings
saveSettings(action, inContext, settings);
}
}
// If a callback function was given
if (inCallback !== undefined) {
// Execute the callback function
inCallback();
}
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/brightnessAction.js
================================================
/**
@file brightnessAction.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
function BrightnessAction(inContext, inSettings, jsn) {
this.property = 'brightness';
// Inherit from PropertyAction
PropertyAction.call(this, inContext, inSettings, jsn);
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/brightnessRelAction.js
================================================
/**
@file brightnessRelAction.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Prototype which represents a brightness action
function BrightnessRelAction(inContext, inSettings) {
// Init BrightnessRelAction
let instance = this;
// Inherit from Action
Action.call(this, inContext, inSettings);
// Set the default values
setDefaults();
// Public function called on key up event
this.onKeyUp = (inContext, inSettings, inCoordinates, inUserDesiredState, inState) => {
// If onKeyUp was triggered manually, load settings
if (inSettings === undefined) {
inSettings = instance.getSettings();
}
// Set icon according to relative value
setState(inContext, inSettings.brightnessRel >= 0 ? 0 : 1);
// Check if any bridge is configured
if (!('bridge' in inSettings)) {
log('No bridge configured');
showAlert(inContext);
return;
}
// Check if the configured bridge is in the cache
if (!(inSettings.bridge in cache.data)) {
log(`Bridge ${inSettings.bridge} not found in cache`);
showAlert(inContext);
return;
}
// Find the configured bridge
let bridgeCache = cache.data[inSettings.bridge];
// Check if any light is configured
if (!('light' in inSettings)) {
log('No light or group configured');
showAlert(inContext);
return;
}
// Check if the configured light or group is in the cache
if (!(inSettings.light in bridgeCache.lights || inSettings.light in bridgeCache.groups)) {
log(`Light or group ${inSettings.light} not found in cache`);
showAlert(inContext);
return;
}
// Check if any brightness is configured
if (!('brightnessRel' in inSettings)) {
log('No relative brightness configured');
showAlert(inContext);
return;
}
// Create a bridge instance
let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username);
// Create a light or group object
let objCache, obj;
if (inSettings.light.indexOf('l') !== -1) {
objCache = bridgeCache.lights[inSettings.light];
obj = new Light(bridge, objCache.id);
}
else {
objCache = bridgeCache.groups[inSettings.light];
obj = new Group(bridge, objCache.id);
}
// Convert brightness
let brightness;
if (objCache.power) {
let brightnessRel = (objCache.brightness / 2.54) + parseInt(inSettings.brightnessRel);
brightness = Math.round(brightnessRel * 2.54);
}
else {
brightness = parseInt(inSettings.brightnessRel);
}
if (brightness > 254) {
brightness = 254;
}
else if (brightness < 0) {
brightness = 0;
}
// Turn lights off if brightness is 0
if (brightness <= 0) {
obj.setPower(false, (inSuccess, inError) => {
if (inSuccess) {
objCache.power = false;
}
else {
log(inError);
showAlert(inContext);
}
});
}
else {
// Set light or group state
obj.setBrightness(brightness, (inSuccess, inError) => {
if (inSuccess) {
objCache.brightness = brightness;
}
else {
log(inError);
showAlert(inContext);
}
});
}
};
// Before overwriting parent method, save a copy of it
let actionNewCacheAvailable = this.newCacheAvailable;
// Public function called when new cache is available
this.newCacheAvailable = inCallback => {
// Call actions newCacheAvailable method
actionNewCacheAvailable.call(instance, () => {
// Set defaults
setDefaults();
// Call the callback function
inCallback();
});
};
// Private function to set the defaults
function setDefaults() {
// Get the settings and the context
let settings = instance.getSettings();
let context = instance.getContext();
// If brightness is already set for this action
if ('brightnessRel' in settings) {
return;
}
// Set the relative brightness to 0
settings.brightnessRel = 0;
// Save the settings
saveSettings('com.elgato.philips-hue.brightness-rel', context, settings);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/colorAction.js
================================================
/**
@file colorAction.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Prototype which represents a color action
function ColorAction(inContext, inSettings) {
// Init ColorAction
let instance = this;
// Inherit from Action
Action.call(this, inContext, inSettings);
// Set the default values
setDefaults();
// Public function called on key up event
this.onKeyUp = (inContext) => {
const settings = this.getVerifiedSettings(inContext, 'color');
if(false === settings) return;
let bridgeCache = cache.data[settings.bridge];
// Create a bridge instance
let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username);
// Create a light or group object
let objCache, obj;
if (settings.light.indexOf('l') !== -1) {
objCache = bridgeCache.lights[settings.light];
obj = new Light(bridge, objCache.id);
}
else {
objCache = bridgeCache.groups[settings.light];
obj = new Group(bridge, objCache.id);
}
// Check if this is a color or temperature light
if (settings.color.indexOf('#') !== -1) {
// Convert light color to hardware independent XY color
let xy = Bridge.hex2xy(settings.color);
// Set light or group state
obj.setXY(xy, (inSuccess, inError) => {
if (inSuccess) {
objCache.xy = xy;
}
else {
log(inError);
showAlert(inContext);
}
});
}
else {
// Note: Some lights do not support the full range
let min = 153.0;
let max = 500.0;
let minK = 2000.0;
let maxK = 6500.0;
// Convert light color
let percentage = (settings.color - minK) / (maxK - minK);
let invertedPercentage = -1 * (percentage - 1.0);
let temperature = Math.round(invertedPercentage * (max - min) + min);
// Set light or group state
obj.setTemperature(temperature, (inSuccess, inError) => {
if (inSuccess) {
objCache.ct = temperature;
}
else {
log(inError);
showAlert(inContext);
}
});
}
};
// Before overwriting parent method, save a copy of it
let actionNewCacheAvailable = this.newCacheAvailable;
// Public function called when new cache is available
this.newCacheAvailable = inCallback => {
// Call actions newCacheAvailable method
actionNewCacheAvailable.call(instance, () => {
// Set defaults
setDefaults();
// Call the callback function
inCallback();
});
};
// Private function to set the defaults
function setDefaults() {
// Get the settings and the context
let settings = instance.getSettings();
let context = instance.getContext();
// Check if any bridge is configured
if (!('bridge' in settings)) {
return;
}
// Check if the configured bridge is in the cache
if (!(settings.bridge in cache.data)) {
return;
}
// Find the configured bridge
let bridgeCache = cache.data[settings.bridge];
// Check if a light was set for this action
if (!('light' in settings)) {
return;
}
// Check if the configured light or group is in the cache
if (!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) {
return;
}
// Get a light or group cache
let lightCache;
if (settings.light.indexOf('l-') !== -1) {
lightCache = bridgeCache.lights[settings.light];
}
else {
lightCache = bridgeCache.groups[settings.light];
}
// Check if any color is configured
if ('color' in settings) {
// Check if the set color is supported by the light
if (settings.color.charAt(0) === '#' && lightCache.xy != null) {
return;
}
else if (settings.color.charAt(0) !== '#' && lightCache.xy == null) {
return;
}
}
// Check if the light supports all colors
if (lightCache.xy != null) {
// Set white as the default color
settings.color = '#ffffff';
}
else {
// Set white as the default temperature
settings.color = '4250';
}
// Save the settings
saveSettings('com.elgato.philips-hue.color', context, settings);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/cycleAction.js
================================================
/**
@file cycleAction.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Prototype which represents a color action
function CycleAction(inContext, inSettings) {
// Init CycleAction
let instance = this;
// Index of current active Color
let currentColor = -1;
// Inherit from Action
Action.call(this, inContext, inSettings);
// Set the default values
setDefaults();
// Public function called on key up event
this.onKeyUp = (inContext) => {
const settings = this.getVerifiedSettings(inContext, 'colors');
if(false === settings) return;
let bridgeCache = cache.data[settings.bridge];
// Create a bridge instance
let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username);
// Create a light or group object
let objCache, obj;
if(settings.light.indexOf('l') !== -1) {
objCache = bridgeCache.lights[settings.light];
obj = new Light(bridge, objCache.id);
}
else {
objCache = bridgeCache.groups[settings.light];
obj = new Group(bridge, objCache.id);
}
// Reset current Color index
if(currentColor + 1 >= settings.colors.length) {
currentColor = -1;
}
let colorIndex = currentColor + 1;
// Check if this is a color or temperature light
if(settings.colors[colorIndex].indexOf('#') !== -1) {
// Convert light color to hardware independent XY color
let xy = Bridge.hex2xy(settings.colors[colorIndex]);
// Set light or group state
obj.setXY(xy, (inSuccess, inError) => {
if(inSuccess) {
objCache.xy = xy;
++currentColor;
}
else {
log(inError);
showAlert(inContext);
}
});
}
else {
// Note: Some lights do not support the full range
let min = 153.0;
let max = 500.0;
let minK = 2000.0;
let maxK = 6500.0;
// Convert light color
let percentage = (settings.colors[colorIndex] - minK) / (maxK - minK);
let invertedPercentage = -1 * (percentage - 1.0);
let temperature = Math.round(invertedPercentage * (max - min) + min);
// Set light or group state
obj.setTemperature(temperature, (inSuccess, inError) => {
if(inSuccess) {
objCache.ct = temperature;
++currentColor;
}
else {
log(inError);
showAlert(inContext);
}
});
}
};
// Before overwriting parent method, save a copy of it
let actionNewCacheAvailable = this.newCacheAvailable;
// Public function called when new cache is available
this.newCacheAvailable = inCallback => {
// Call actions newCacheAvailable method
actionNewCacheAvailable.call(instance, () => {
// Set defaults
setDefaults();
// Call the callback function
inCallback();
});
};
// Private function to set the defaults
function setDefaults() {
// Get the settings and the context
let settings = instance.getSettings();
let context = instance.getContext();
// Check if any bridge is configured
if(!('bridge' in settings)) {
return;
}
// Check if the configured bridge is in the cache
if(!(settings.bridge in cache.data)) {
return;
}
// Find the configured bridge
let bridgeCache = cache.data[settings.bridge];
// Check if a light was set for this action
if(!('light' in settings)) {
return;
}
// Check if the configured light or group is in the cache
if(!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) {
return;
}
// Get a light or group cache
let lightCache;
if(settings.light.indexOf('l-') !== -1) {
lightCache = bridgeCache.lights[settings.light];
}
else {
lightCache = bridgeCache.groups[settings.light];
}
// Check if any color is configured
if('colors' in settings) {
// Check if the set color is supported by the light
if(settings.colors[0].charAt(0) === '#' && lightCache.xy != null) {
return;
}
else if(settings.colors[0].charAt(0) !== '#' && lightCache.xy == null) {
return;
}
}
// Check if the light supports all colors
if(lightCache.xy != null) {
// Set white as the default color
settings.colors = ['#ff0000', '#00ff00', '#0000ff'];
}
else {
// Set white as the default temperature
settings.colors = ['2230', '4250', '6410'];
}
// Save the settings
saveSettings('com.elgato.philips-hue.cycle', context, settings);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/main.js
================================================
/**
@file main.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Global web socket
var websocket = null;
// Global cache
var cache = {};
// Global settings
var globalSettings = {};
const throttleDialRotate = Utils.throttle((fn) => {
if (fn) fn();
}, 60);
const debounceDialRotate = Utils.debounce((jsonObj) => {
console.log('debounceDialRotate', jsonObj);
}, 300);
// Setup the websocket and handle communication
function connectElgatoStreamDeckSocket(inPort, inPluginUUID, inRegisterEvent, inInfo) {
// Create array of currently used actions
let actions = {};
window.MACTIONS = actions;
// Create a cache
cache = new Cache();
// Open the web socket to Stream Deck
// Use 127.0.0.1 because Windows needs 300ms to resolve localhost
websocket = new WebSocket(`ws://127.0.0.1:${inPort}`);
const _info = JSON.parse(inInfo);
const [ version, major, minor, build ] = _info.application.version.split(".").map(e => parseInt(e, 10));
const hasDialPress = version == 6 && major < 4;
// Web socket is connected
websocket.onopen = () => {
// Register plugin to Stream Deck
registerPluginOrPI(inRegisterEvent, inPluginUUID);
// Request the global settings of the plugin
requestGlobalSettings(inPluginUUID);
};
document.addEventListener('updateActions', (e) => {
// updateAction carries the sender of the event so we can skip it
const sender = e.detail?.sender;
Object.keys(actions).forEach(inContext => {
if(actions[inContext].updateAction) {
// don't update the sender
if(actions[inContext] === sender) return;
actions[inContext].updateAction();
}
});
}, false);
// Add event listener
document.addEventListener('newCacheAvailable', () => {
// When a new cache is available
Object.keys(actions).forEach(inContext => {
// Inform all used actions that a new cache is available
actions[inContext].newCacheAvailable(() => {
let action;
// Find out type of action
if (actions[inContext] instanceof PowerAction) {
action = 'com.elgato.philips-hue.power';
}
else if (actions[inContext] instanceof ColorAction) {
action = 'com.elgato.philips-hue.color';
}
else if (actions[inContext] instanceof CycleAction) {
action = 'com.elgato.philips-hue.cycle';
}
else if (actions[inContext] instanceof BrightnessAction) {
action = 'com.elgato.philips-hue.brightness';
if(actions[inContext].updateAction) {
actions[inContext].updateAction();
}
}
else if (actions[inContext] instanceof TemperatureAction) {
action = 'com.elgato.philips-hue.temperature';
if(actions[inContext].updateAction) {
actions[inContext].updateAction();
}
}
else if (actions[inContext] instanceof BrightnessRelAction) {
action = 'com.elgato.philips-hue.brightness-rel';
}
else if (actions[inContext] instanceof SceneAction) {
action = 'com.elgato.philips-hue.scene';
}
// Inform PI of new cache
sendToPropertyInspector(action, inContext, cache.data);
});
});
}, false);
// Web socked received a message
websocket.onmessage = inEvent => {
// Parse parameter from string to object
let jsonObj = JSON.parse(inEvent.data);
// Extract payload information
let event = jsonObj['event'];
let action = jsonObj['action'];
let context = jsonObj['context'];
let jsonPayload = jsonObj['payload'];
let settings;
if(event === 'dialRotate') {
if(actions[context]?.onDialRotate) {
throttleDialRotate(() => {
actions[context].onDialRotate(jsonObj);
});
// debounceDialRotate(jsonObj);
// actions[context].onDialRotate(jsonObj);
}
} else if(!hasDialPress && event === 'dialUp') {
if(actions[ context ]?.onDialUp) {
actions[ context ].onDialUp(jsonObj);
}
} else if(!hasDialPress && event === 'dialDown') {
if(actions[ context ]?.onDialDown) {
actions[ context ].onDialDown(jsonObj);
}
} else if(hasDialPress && event === 'dialPress') {
if(actions[context]?.onDialPress) {
actions[context].onDialPress(jsonObj);
}
} else if(event === 'touchTap') {
if(actions[context]?.onTouchTap) {
actions[context].onTouchTap(jsonObj);
}
} else if (event === 'keyUp') {
settings = jsonPayload['settings'];
let coordinates = jsonPayload['coordinates'];
let userDesiredState = jsonPayload['userDesiredState'];
let state = jsonPayload['state'];
// Send onKeyUp event to actions
if (context in actions) {
actions[context].onKeyUp(context, settings, coordinates, userDesiredState, state);
}
// Refresh the cache
cache.refresh();
}
else if (event === 'willAppear') {
settings = jsonPayload['settings'];
// If this is the first visible action
if (Object.keys(actions).length === 0) {
// Start polling
cache.startPolling();
}
// Add current instance is not in actions array
if (!(context in actions)) {
// Add current instance to array
if (action === 'com.elgato.philips-hue.power') {
actions[context] = new PowerAction(context, settings);
}
else if (action === 'com.elgato.philips-hue.color') {
actions[context] = new ColorAction(context, settings);
}
else if (action === 'com.elgato.philips-hue.cycle') {
actions[context] = new CycleAction(context, settings);
}
else if (action === 'com.elgato.philips-hue.brightness') {
actions[context] = new BrightnessAction(context, settings, jsonObj);
}
else if (action === 'com.elgato.philips-hue.temperature') {
actions[context] = new TemperatureAction(context, settings, jsonObj);
}
else if (action === 'com.elgato.philips-hue.brightness-rel') {
actions[context] = new BrightnessRelAction(context, settings);
}
else if (action === 'com.elgato.philips-hue.scene') {
actions[context] = new SceneAction(context, settings);
}
}
}
else if (event === 'willDisappear') {
// Remove current instance from array
if (context in actions) {
delete actions[context];
}
// If this is the last visible action
if (Object.keys(actions).length === 0) {
// Stop polling
cache.stopPolling();
}
}
else if (event === 'didReceiveGlobalSettings') {
// Set global settings
globalSettings = jsonPayload['settings'];
// If at least one action is active
if (Object.keys(actions).length > 0) {
// Refresh the cache
cache.refresh();
}
}
else if (event === 'didReceiveSettings') {
settings = jsonPayload['settings'];
// Set settings
if (context in actions) {
actions[context].setSettings(settings);
}
// Refresh the cache
cache.refresh();
}
else if (event === 'propertyInspectorDidAppear') {
// Send cache to PI
sendToPropertyInspector(action, context, cache.data);
}
else if (event === 'sendToPlugin') {
let piEvent = jsonPayload['piEvent'];
if (piEvent === 'valueChanged') {
// Only color, brightness and scene support live preview
if (action !== 'com.elgato.philips-hue.power' && action !== 'com.elgato.philips-hue.cycle') {
// Send manual onKeyUp event to action
if (context in actions) {
actions[context].onKeyUp(context);
}
}
} else if (piEvent === 'lightsChanged') {
// console.log("lightsChanged", action, context, jsonPayload);
if (context in actions) {
if(actions[context].updateDisplay) {
actions[context].updateDisplay();
};
}
}
}
};
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/philips/cache.js
================================================
/**
@file cache.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Prototype for a data cache
function Cache() {
// Init Cache
let instance = this;
// Refresh time of the cache in seconds
let autoRefreshTime = 60;
// Private timer instance
let timer = null;
// Private bridge discovery
let discovery = null;
// Public variable containing the cached data
this.data = {};
// Private function to discover all bridges on the network
function buildDiscovery(inCallback) {
// Check if discovery ran already
if (discovery != null) {
inCallback(true);
return;
}
// Init discovery variable to indicate that it ran already
discovery = {};
// Run discovery
Bridge.discover((inSuccess, inBridges) => {
// If the discovery was not successful
if (!inSuccess) {
log(inBridges);
inCallback(false);
return;
}
// For all discovered bridges
inBridges.forEach(inBridge => {
// Add new bridge to discovery object
discovery[inBridge.getID()] = {
ip: inBridge.getIP()
};
});
inCallback(true);
});
}
// Gather all required information by a Bridge via ID
function refreshBridge(pairedBridgeID, pairedBridge) {
// Older Bridges in Settings may have the ID stored inside the object
if (!pairedBridge.id) {
pairedBridge.id = pairedBridgeID;
}
// Older Bridges in Settings may have no IP stored
if (!pairedBridge.ip) {
// Trying to receive the IP trough auto-discovery
if (discovery[pairedBridge.id]) {
pairedBridge.ip = discovery[pairedBridge.id].ip;
}
// If no IP can be found for this Bridge we need to stop here
else {
log(`No IP found for paired Bridge ID: ${pairedBridge.id}`);
return;
}
}
// Create a bridge instance
let bridge = new Bridge(pairedBridge.ip, pairedBridge.id, pairedBridge.username);
// Create bridge cache
let bridgeCache = { 'lights': {}, 'groups': {} };
bridgeCache.id = bridge.getID();
bridgeCache.ip = bridge.getIP();
bridgeCache.username = bridge.getUsername();
// Load the bridge name
bridge.getName((inSuccess, inName) => {
// If getName was not successful
if (!inSuccess) {
log(inName);
return;
}
// Save the name
bridgeCache.name = inName;
// Add bridge to the cache
// instance.data[bridge.getID()] = bridgeCache;
// Request all lights of the bridge
bridge.getLights((inSuccess, inLights) => {
// If getLights was not successful
if (!inSuccess) {
log(inLights);
return;
}
// Create cache for each light
inLights.forEach(inLight => {
// Add light to cache
bridgeCache.lights['l-' + inLight.getID()] = {
id: inLight.getID(),
name: inLight.getName(),
type: inLight.getType(),
power: inLight.getPower(),
brightness: inLight.getBrightness(),
xy: inLight.getXY(),
temperature: inLight.getTemperature(),
originalValue: inLight.originalValue,
};
});
// Request all groups of the bridge
bridge.getGroups((inSuccess, inGroups) => {
// If getGroups was not successful
if (!inSuccess) {
log(inGroups);
return;
}
// Create cache for each group
inGroups.forEach(inGroup => {
// Add group to cache
bridgeCache.groups['g-' + inGroup.getID()] = {
id: inGroup.getID(),
name: inGroup.getName(),
type: inGroup.getType(),
power: inGroup.getPower(),
brightness: inGroup.getBrightness(),
xy: inGroup.getXY(),
temperature: inGroup.getTemperature(),
scenes: {},
};
// If this is the last group
if (Object.keys(bridgeCache.groups).length === inGroups.length) {
// Request all scenes of the bridge
bridge.getScenes((inSuccess, inScenes) => {
// If getScenes was not successful
if (!inSuccess) {
log(inScenes);
return;
}
// Create cache for each scene
inScenes.forEach(inScene => {
// Check if this is a group scene
if (inScene.getType() !== 'GroupScene') {
return;
}
// If scenes group is in cache
if ('g-' + inScene.getGroup() in bridgeCache.groups) {
// Add scene to cache
bridgeCache.groups['g-' + inScene.getGroup()].scenes[inScene.getID()] = {
id: inScene.getID(),
name: inScene.getName(),
type: inScene.getType(),
group: inScene.getGroup(),
};
}
});
// console.log(bridgeCache);
instance.data[bridge.getID()] = bridgeCache;
// Inform keys that updated cache is available
let event = new CustomEvent('newCacheAvailable');
document.dispatchEvent(event);
});
}
});
});
});
});
}
// Public function to start polling
this.startPolling = () => {
// Log to the global log file
log('Start polling to create cache');
// Start a timer
instance.refresh();
timer = setInterval(instance.refresh, autoRefreshTime * 1000);
}
// Public function to stop polling
this.stopPolling = () => {
// Log to the global log file
log('Stop polling to create cache');
// Invalidate the timer
clearInterval(timer);
timer = null;
}
this.refresh = Utils.debounce(function () {
// Build discovery if necessary
buildDiscovery(() => {
if (globalSettings.bridges) {
Object.keys(globalSettings.bridges).forEach(bridgeID => refreshBridge(bridgeID, globalSettings.bridges[bridgeID]));
}
})
}, 200); // avoid multiple calls in a short time
// Private function to build a cache
this.refresh2 = () => {
// Build discovery if necessary
buildDiscovery(() => {
if (globalSettings.bridges) {
Object.keys(globalSettings.bridges).forEach(bridgeID => refreshBridge(bridgeID, globalSettings.bridges[bridgeID]));
}
})
};
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/philips/meethue.js
================================================
/**
@file meethue.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
const MDEBOUNCEDELAYMS = 80;
// Prototype which represents a Philips Hue bridge
function Bridge(ip = null, id = null, username = null) {
// Init Bridge
let instance = this;
// Public function to pair with a bridge
this.pair = (callback) => {
if (ip) {
let url = `http://${ip}/api`;
let xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open('POST', url, true);
xhr.timeout = 2500;
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
if (xhr.response !== undefined && xhr.response != null) {
let result = xhr.response[0];
if ('success' in result) {
username = result['success']['username'];
callback(true, result);
}
else {
let message = result['error']['description'];
callback(false, message);
}
}
else {
callback(false, 'Bridge response is undefined or null.');
}
}
else {
callback(false, 'Could not connect to the bridge.');
}
};
xhr.onerror = () => {
callback(false, 'Unable to connect to the bridge.');
};
xhr.ontimeout = () => {
callback(false, 'Connection to the bridge timed out.');
};
let obj = {};
obj.devicetype = 'stream_deck';
let data = JSON.stringify(obj);
xhr.send(data);
}
else {
callback(false, 'No IP address given.');
}
};
// Public function to retrieve the username
this.getUsername = () => {
return username;
};
// Public function to retrieve the IP address
this.getIP = () => {
return ip;
};
// Public function to retrieve the ID
this.getID = () => {
return id;
};
// Public function to retrieve the name
this.getName = callback => {
let url = `http://${ip}/api/${username}/config`;
let xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open('GET', url, true);
xhr.timeout = 5000;
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
let result = xhr.response;
if (result !== undefined && result != null) {
if ('name' in result) {
let name = result['name'];
callback(true, name);
}
else {
let message = result[0]['error']['description'];
callback(false, message);
}
}
else {
callback(false, 'Bridge response is undefined or null.');
}
}
else {
callback(false, 'Could not connect to the bridge.');
}
};
xhr.onerror = () => {
callback(false, 'Unable to connect to the bridge.');
};
xhr.ontimeout = () => {
callback(false, 'Connection to the bridge timed out.');
};
xhr.send();
};
// Private function to retrieve objects
function getMeetHues(type, callback) {
let url;
if (type === 'light') {
url = `http://${ip}/api/${username}/lights`;
}
else if (type === 'group') {
url = `http://${ip}/api/${username}/groups`;
}
else if (type === 'scene') {
url = `http://${ip}/api/${username}/scenes`;
}
else {
callback(false, 'Type does not exist.');
return;
}
let xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open('GET', url, true);
xhr.timeout = 5000;
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
let result = xhr.response;
if (result !== undefined && result != null) {
if (!Array.isArray(result)) {
let objects = [];
Object.keys(result).forEach(key => {
let value = result[key];
if (type === 'light') {
// console.log("Light", value.name, value.capabilities?.control);
let light = new Light(instance, key, value.name, value.type, value.state.on, value.state.bri, value.state.xy, value.state.ct);
light.originalValue = value;
objects.push(light);
}
else if (type === 'group') {
objects.push(new Group(instance, key, value.name, value.type, value.state.all_on, value.action.bri, value.action.xy, value.action.ct));
}
else if (type === 'scene') {
objects.push(new Scene(instance, key, value.name, value.type, value.group));
}
});
callback(true, objects);
}
else {
let message = result[0]['error']['description'];
callback(false, message);
}
}
else {
callback(false, 'Bridge response is undefined or null.');
}
}
else {
callback(false, 'Unable to get objects of type ' + type + '.');
}
};
xhr.onerror = () => {
callback(false, 'Unable to connect to the bridge.');
};
xhr.ontimeout = () => {
callback(false, 'Connection to the bridge timed out.');
};
xhr.send();
}
// Public function to retrieve the lights
this.getLights = callback => {
getMeetHues('light', callback);
};
// Public function to retrieve the groups
this.getGroups = callback => {
getMeetHues('group', callback);
};
// Public function to retrieve the scenes
this.getScenes = callback => {
getMeetHues('scene', callback);
};
}
// Static function to discover bridges
Bridge.discover = callback => {
let url = 'https://discovery.meethue.com';
let xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open('GET', url, true);
xhr.timeout = 10000;
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
if (xhr.response !== undefined && xhr.response != null) {
let bridges = [];
xhr.response.forEach(bridge => {
bridges.push(new Bridge(bridge.internalipaddress, bridge.id));
});
callback(true, bridges);
}
else {
callback(false, 'Meethue server response is undefined or null.');
}
}
else {
callback(false, 'Unable to discover bridges.');
}
};
xhr.onerror = () => {
callback(false, 'Unable to connect to the internet.');
};
xhr.ontimeout = () => {
callback(false, 'Connection to the internet timed out.');
};
xhr.send();
};
// Check if a Bridge is available under a certain IP address
// If a username is set it will check that too
Bridge.check = (ip, username, callback) => {
let url = username ? `http://${ip}/api/${username}config` : `http://${ip}/api/config`;
let xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open('GET', url, true);
xhr.timeout = 10000;
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200 &&
xhr.response !== undefined && xhr.response != null &&
xhr.response.hasOwnProperty('bridgeid') &&
(!username || xhr.response.hasOwnProperty('ipaddress'))
) {
// at this point the bridge has been found and added to list
callback(true, {
ip: ip,
id: xhr.response.bridgeid.toLowerCase(),
});
}
callback(false);
};
xhr.onerror = xhr.ontimeout = () => {
callback(false);
};
xhr.send();
};
// Static function to convert hex to rgb
Bridge.hex2rgb = inHex => {
// Remove hash if it exists
if (inHex.charAt(0) === '#') {
inHex = inHex.substr(1);
}
// Split hex into RGB components
let rgbArray = inHex.match(/.{1,2}/g);
// Convert RGB component into decimals
let red = parseInt(rgbArray[0], 16);
let green = parseInt(rgbArray[1], 16);
let blue = parseInt(rgbArray[2], 16);
return {
r: red,
g: green,
b: blue,
};
}
// Static function to convert rgb to hex
Bridge.rgb2hex = inRGB => {
return '#' + ((1 << 24) + (inRGB.r << 16) + (inRGB.g << 8) + inRGB.b).toString(16).slice(1);
}
// Static function to convert rgb to hsv
Bridge.rgb2hsv = inRGB => {
// Calculate the brightness and saturation value
let max = Math.max(inRGB.r, inRGB.g, inRGB.b);
let min = Math.min(inRGB.r, inRGB.g, inRGB.b);
let d = max - min;
let s = (max === 0 ? 0 : d / max);
let v = max / 255;
// Calculate the hue value
let h;
switch (max) {
case min:
h = 0;
break;
case inRGB.r:
h = (inRGB.g - inRGB.b) + d * (inRGB.g < inRGB.b ? 6: 0);
h /= 6 * d;
break;
case inRGB.g:
h = (inRGB.b - inRGB.r) + d * 2;
h /= 6 * d;
break;
case inRGB.b:
h = (inRGB.r - inRGB.g) + d * 4;
h /= 6 * d;
break;
}
return {h, s, v};
}
// Static function to convert hsv to rgb
Bridge.hsv2rgb = inHSV => {
let r = null;
let g = null;
let b = null;
let i = Math.floor(inHSV.h * 6);
let f = inHSV.h * 6 - i;
let p = inHSV.v * (1 - inHSV.s);
let q = inHSV.v * (1 - f * inHSV.s);
let t = inHSV.v * (1 - (1 - f) * inHSV.s);
// Calculate red, green and blue
switch (i % 6) {
case 0:
r = inHSV.v;
g = t;
b = p;
break;
case 1:
r = q;
g = inHSV.v;
b = p;
break;
case 2:
r = p;
g = inHSV.v;
b = t;
break;
case 3:
r = p;
g = q;
b = inHSV.v;
break;
case 4:
r = t;
g = p;
b = inHSV.v;
break;
case 5:
r = inHSV.v;
g = p;
b = q;
break;
}
// Convert rgb values to int
let red = Math.round(r * 255);
let green = Math.round(g * 255);
let blue = Math.round(b * 255);
return {
r: red,
g: green,
b: blue,
};
}
// Static function to convert hex to hsv
Bridge.hex2hsv = inHex => {
// Convert hex to rgb
let rgb = Bridge.hex2rgb(inHex);
// Convert rgb to hsv
return Bridge.rgb2hsv(rgb);
}
// Static function to convert hsv to hex
Bridge.hsv2hex = inHSV => {
// Convert hsv to rgb
let rgb = Bridge.hsv2rgb(inHSV);
// Convert rgb to hex
return Bridge.rgb2hex(rgb);
}
// Static function to convert hex to xy
Bridge.hex2xy = inHex => {
// Convert hex to rgb
let rgb = Bridge.hex2rgb(inHex);
// Concert RGB components to floats
let red = rgb.r / 255;
let green = rgb.g / 255;
let blue = rgb.b / 255;
// Convert RGB to XY
let r = red > 0.04045 ? Math.pow(((red + 0.055) / 1.055), 2.4000000953674316) : red / 12.92;
let g = green > 0.04045 ? Math.pow(((green + 0.055) / 1.055), 2.4000000953674316) : green / 12.92;
let b = blue > 0.04045 ? Math.pow(((blue + 0.055) / 1.055), 2.4000000953674316) : blue / 12.92;
let x = r * 0.664511 + g * 0.154324 + b * 0.162028;
let y = r * 0.283881 + g * 0.668433 + b * 0.047685;
let z = r * 8.8E-5 + g * 0.07231 + b * 0.986039;
// Convert XYZ zo XY
let xy = [x / (x + y + z), y / (x + y + z)];
if (isNaN(xy[0])) {
xy[0] = 0.0;
}
if (isNaN(xy[1])) {
xy[1] = 0.0;
}
return xy;
};
// Prototype which represents a Philips Hue object
function MeetHue(bridge = null, id = null, name = null, type = null) {
// Init MeetHue
let instance = this;
// Override in child prototype
let url = null;
this.originalValue = null;
// Public function to retrieve the type
this.getType = () => {
return type;
};
// Public function to retrieve the name
this.getName = () => {
return name;
};
// Public function to retrieve the ID
this.getID = () => {
return id;
};
// Public function to retrieve the URL
this.getURL = () => {
return url;
};
// Public function to set the URL
this.setURL = inURL => {
url = inURL;
}
// Public function to set light state
this.setState = (state, callback) => {
// Check if the URL was set
if (instance.getURL() == null) {
callback(false, 'URL is not set.');
return;
}
let xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open('PUT', instance.getURL(), true);
xhr.timeout = 2500;
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
if (xhr.response !== undefined && xhr.response != null) {
let result = xhr.response[0];
if ('success' in result) {
callback(true, result);
}
else {
let message = result['error']['description'];
callback(false, message);
}
}
else {
callback(false, 'Bridge response is undefined or null.');
}
}
else {
callback(false, 'Could not set state.');
}
};
xhr.onerror = () => {
callback(false, 'Unable to connect to the bridge.');
};
xhr.ontimeout = () => {
callback(false, 'Connection to the bridge timed out.');
};
let data = JSON.stringify(state);
xhr.send(data);
};
}
// Prototype which represents a scene
function Scene(bridge = null, id = null, name = null, type = null, group = null) {
// Init Scene
let instance = this;
// Inherit from MeetHue
MeetHue.call(this, bridge, id, name, type);
// Set the URL
this.setURL(`http://${bridge.getIP()}/api/${bridge.getUsername()}/groups/0/action`);
// Public function to retrieve the group
this.getGroup = () => {
return group;
};
// Public function to set the scene
this.on = callback => {
// Define state object
let state = {};
state.scene = id;
// Send new state
instance.setState(state, callback);
};
}
// Prototype which represents an illumination
function Illumination(bridge = null, id = null, name = null, type = null, power = null, brightness = null, xy = null, temperature = null) {
// Init Illumination
let instance = this;
// Inherit from MeetHue
MeetHue.call(this, bridge, id, name, type);
// Public function to retrieve the power state
this.getPower = () => {
return power;
};
// Public function to retrieve the brightness
this.getBrightness = () => {
return brightness;
};
// Public function to retrieve xy
this.getXY = () => {
return xy;
};
// Public function to retrieve the temperature
this.getTemperature = () => {
return temperature;
};
// Public function to set the power status of the light
this.setPower = (power, callback) => {
// Define state object
let state = {};
state.on = power;
// Send new state
instance.setState(state, callback);
};
// Public function to set the brightness
this.setBrightness = Utils.debounce((brightness, callback) => {
// Define state object
let state = {};
state.bri = brightness;
// To modify the brightness, the light needs to be on
state.on = true;
// Send new state
instance.setState(state, callback);
}, MDEBOUNCEDELAYMS);
// Public function set the xy value
this.setXY = (xy, callback) => {
// Define state object
let state = {};
state.xy = xy;
// To modify the color, the light needs to be on
state.on = true;
// Send new state
instance.setState(state, callback);
};
// Public function set the temperature value
this.setTemperature = Utils.debounce((temperature, callback) => {
// Define state object
let state = {};
state.ct = temperature;
// To modify the temperature, the light needs to be on
state.on = true;
// Send new state
instance.setState(state, callback);
}, MDEBOUNCEDELAYMS);
}
// Prototype which represents a light
function Light(bridge = null, id = null, name = null, type = null, power = null, brightness = null, xy = null, temperature = null) {
// Inherit from Illumination
Illumination.call(this, bridge, id, name, type, power, brightness, xy, temperature);
// Set the URL
this.setURL(`http://${bridge.getIP()}/api/${bridge.getUsername()}/lights/${id}/state`);
}
// Prototype which represents a group
function Group(bridge = null, id = null, name = null, type = null, power = null, brightness = null, xy = null, temperature = null) {
// Inherit from Illumination
Illumination.call(this, bridge, id, name, type, power, brightness, xy, temperature);
// Set the URL
this.setURL(`http://${bridge.getIP()}/api/${bridge.getUsername()}/groups/${id}/action`);
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/powerAction.js
================================================
/**
@file powerAction.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Prototype which represents a power action
function PowerAction(inContext, inSettings) {
// Init PowerAction
let instance = this;
// Inherit from Action
Action.call(this, inContext, inSettings);
// Update the state
updateState();
this.updateAction = function() {
updateState();
};
// Public function called on key up event
this.onKeyUp = (inContext, inSettings, inCoordinates, inUserDesiredState, inState) => {
const settings = this.getVerifiedSettings(inContext);
if(false === settings) return;
let bridgeCache = cache.data[settings.bridge];
// Create a bridge instance
let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username);
// Create a light or group object
let objCache, obj;
if (settings.light.indexOf('l-') !== -1) {
objCache = bridgeCache.lights[settings.light];
obj = new Light(bridge, objCache.id);
}
else {
objCache = bridgeCache.groups[settings.light];
obj = new Group(bridge, objCache.id);
}
// Check for multi action
let targetState;
if (inUserDesiredState !== undefined) {
targetState = !inUserDesiredState;
}
else {
targetState = !objCache.power;
}
// Set light or group state
obj.setPower(targetState, (success, error) => {
if (success) {
setActionState(inContext, targetState ? 0 : 1);
objCache.power = targetState;
cache.refresh();
}
else {
log(error);
setActionState(inContext, inState);
showAlert(inContext);
}
});
};
// Before overwriting parent method, save a copy of it
let actionNewCacheAvailable = this.newCacheAvailable;
// Public function called when new cache is available
this.newCacheAvailable = inCallback => {
// Call actions newCacheAvailable method
actionNewCacheAvailable.call(instance, () => {
// Update the state
updateState();
// Call the callback function
inCallback();
});
};
function updateState() {
// Get the settings and the context
let settings = instance.getSettings();
let context = instance.getContext();
// Check if any bridge is configured
if (!('bridge' in settings)) {
return;
}
// Check if the configured bridge is in the cache
if (!(settings.bridge in cache.data)) {
return;
}
// Find the configured bridge
let bridgeCache = cache.data[settings.bridge];
// Check if a light was set for this action
if (!('light' in settings)) {
return;
}
// Check if the configured light or group is in the cache
if (!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) {
return;
}
// Find out if it is a light or a group
let objCache;
if (settings.light.indexOf('l-') !== -1) {
objCache = bridgeCache.lights[settings.light];
}
else {
objCache = bridgeCache.groups[settings.light];
}
// Set the target state
let targetState = objCache.power;
// Set the new action state
setActionState(context, targetState ? 0 : 1);
}
// Private function to set the state
function setActionState(inContext, inState) {
setState(inContext, inState);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/propertyAction.js
================================================
/**
@file propertyAction.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Prototype which represents a brightness action
function PropertyAction(inContext, inSettings, jsn) {
let instance = this;
this.keyIsDown = false;
this.actionTriggered = false;
const setStateFunction = `set${Utils.capitalize(this.property)}`;
// Inherit from Action
Action.call(this, inContext, inSettings, jsn);
// Set the default values
setDefaults();
this.updateAction = function() {
const target = this.getCurrentLightOrGroup();
if(target === false) return;
this.updateDisplay(target.objCache, this.property);
};
if(this.isEncoder) {
let timer = setInterval(() => {
this.updateAction();
}, 5000);
}
this.getCurrentLightOrGroup = function() {
let settings = this.getVerifiedSettings(inContext);
if(settings === false) return false; // break if settings are not valid
let bridgeCache = cache.data[settings.bridge]; // we have a valid bridge (was checked in getVerifiedSettings)
let objCache = {};
let obj = {};
let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username);
if(settings.light.indexOf('l') !== -1) {
objCache = bridgeCache.lights[settings.light];
if(objCache) {
obj = new Light(bridge, objCache.id);
}
}
else {
objCache = bridgeCache.groups[settings.light];
if(objCache) {
obj = new Group(bridge, objCache.id);
}
}
return {obj, objCache};
};
this.setValue = function(inValue, jsn) {
const target = this.getCurrentLightOrGroup();
if(target) {
if(target.objCache.power === false) return;
let value = inValue ? inValue : target.objCache[this.property];
if(jsn?.payload?.ticks) {
let settings = this.getSettings();
const scaleTicks = settings?.scaleTicks || 1;
const multiplier = scaleTicks * jsn.payload.ticks;
value = Utils.minmax(parseInt(value + multiplier * 2.55), 0,255);
// value = parseInt(value + jsn.payload.ticks * 2.55);
}
// just update the panel optimistically
// note: this didn't work well for me, so I'm not using it
// this.setFeedback(inContext, parseInt(value / 2.54), 1);
target.obj[setStateFunction](value, (inSuccess, inError) => {
if(inSuccess) {
target.objCache[this.property] = value;
this.updateDisplay(target.objCache, this.property);
this.updateAllActions();
} else {
log(inError);
showAlert(inContext);
}
});
}
};
this.onDialUp = function(jsn) {
// console.log('onDialUp', jsn);
if(this.getVerifiedSettings(inContext) === false) return;
this.keyIsDown = false;
if(!this.actionTriggered) {
if(this.isEncoder) {
return this.togglePower(inContext);
}
const target = this.getCurrentLightOrGroup();
// check if light is off, and if it is, turn it on
if(target.objCache.power === false) {
this.togglePower(inContext);
this.updateDisplay(target.objCache, 'power');
} else {
// otherwise, just change the property to the configured value
this.onKeyUp(inContext);
}
}
};
this.onDialDown = function(jsn) {
// console.log('onDialDown', jsn);
if(this.getVerifiedSettings(inContext) === false) return;
// temporarily set a flag to mark that the key is down
this.keyIsDown = true;
this.actionTriggered = false;
setTimeout(function() {
if(instance.keyIsDown) {
// console.log("***** long keypress detected:", instance.keyIsDown,inContext);
const target = instance.togglePower(inContext);
instance.updateDisplay(target.objCache, 'power');
instance.actionTriggered = true;
}
instance.keyIsDown = false;
}, 500);
};
this.onDialPress = function(jsn) {
if(this.getVerifiedSettings(inContext) === false) return;
if(jsn?.payload?.pressed === true) { // dial pressed == down
this.onDialDown(jsn);
} else { // dial released == up
this.onDialUp(jsn);
}
};
this.onDialRotate = function(jsn) {
this.setValue(null, jsn);
};
this.onTouchTap = function(jsn) {
this.togglePower(inContext);
};
// Public function called on key up event
this.onKeyUp = (inContext) => {
const settings = this.getVerifiedSettings(inContext);
if(settings === false) return;
// Convert value
// Hack to circumvent original code that converts values from 0-255
let value = this.property == 'temperature' ? Number(settings[this.property]) : Math.round(settings[this.property] * 2.54);
this.setValue(value);
};
// Before overwriting parent method, save a copy of it
let actionNewCacheAvailable = this.newCacheAvailable;
// Public function called when new cache is available
this.newCacheAvailable = inCallback => {
// Call actions newCacheAvailable method
actionNewCacheAvailable.call(instance, () => {
// Set defaults
setDefaults();
// Call the callback function
inCallback();
});
};
// Private function to set the defaults
function setDefaults() {
// Get the settings and the context
let settings = instance.getSettings();
let context = instance.getContext();
// If property is already set for this action
if(this.property in settings) {
return;
}
// Set the property to 100
settings[this.property] = 100;
// Save the settings
saveSettings(`com.elgato.philips-hue.${this.property}`, context, settings);
}
// update the action and its display
this.updateActionIfCacheAvailable(inContext);
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/sceneAction.js
================================================
/**
@file sceneAction.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Prototype which represents a scene action
function SceneAction(inContext, inSettings) {
// Init SceneAction
let instance = this;
// Inherit from Action
Action.call(this, inContext, inSettings);
// Set the default values
setDefaults();
// Public function called on key up event
this.onKeyUp = (inContext, inSettings, inCoordinates, inUserDesiredState, inState) => {
const settings = this.getVerifiedSettings(inContext, 'scene');
if(false === settings) return;
let bridgeCache = cache.data[settings.bridge];
// Find the configured group
let groupCache = bridgeCache.groups[inSettings.light];
// Check if any scene is configured
if (!('scene' in inSettings)) {
log('No scene configured');
showAlert(inContext);
return;
}
// Check if the configured scene is in the group cache
if (!(settings.scene in groupCache.scenes)) {
log(`Scene ${settings.scene} not found in cache`);
showAlert(inContext);
return;
}
// Find the configured scene
let sceneCache = groupCache.scenes[inSettings.scene];
// Create a bridge instance
let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username);
// Create a scene instance
let scene = new Scene(bridge, sceneCache.id);
// Set scene
scene.on((inSuccess, inError) => {
// Check if setting the scene was successful
if (!(inSuccess)) {
log(inError);
showAlert(inContext);
}
});
};
// Before overwriting parent method, save a copy of it
let actionNewCacheAvailable = this.newCacheAvailable;
// Public function called when new cache is available
this.newCacheAvailable = (inCallback) => {
// Call actions newCacheAvailable method
actionNewCacheAvailable.call(instance, () => {
// Set defaults
setDefaults();
// Call the callback function
inCallback();
});
};
// Private function to set the defaults
function setDefaults() {
// Get the settings and the context
let settings = instance.getSettings();
let context = instance.getContext();
// Check if any bridge is configured
if (!('bridge' in settings)) {
return;
}
// Check if the configured bridge is in the cache
if (!(settings.bridge in cache.data)) {
return;
}
// Find the configured bridge
let bridgeCache = cache.data[settings.bridge];
// Check if a light was set for this action
if (!('light' in settings)) {
return;
}
// Check if the light was set to a group
if (!(settings.light.indexOf('g-') !== -1)) {
return;
}
// Check if the configured group is in the cache
if (!(settings.light in bridgeCache.groups)) {
return;
}
// Find the configured group
let groupCache = bridgeCache.groups[settings.light];
// Check if a scene was configured for this action
if ('scene' in settings) {
// Check if the scene is part of the set group
if (settings.scene in groupCache.scenes) {
return;
}
}
// Check if the group has at least one scene
if (!(Object.keys(groupCache.scenes).length > 0)) {
return;
}
// Sort the scenes alphabetically
let sceneIDsSorted = Object.keys(groupCache.scenes).sort((a, b) => {
return groupCache.scenes[a].name.localeCompare(groupCache.scenes[b].name);
});
// Set the action automatically to the first one
settings.scene = sceneIDsSorted[0];
// Save the settings
saveSettings('com.elgato.philips-hue.scene', context, settings);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/temperatureAction.js
================================================
/**
@file temperatureAction.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
/**
* Color temperature range of Philips lights is: 2200K to 6500K == 455 to 154 Mired. //154 is the coolest, 500 is the warmest
*/
const percentOfRange = (value, min = 0, max = 100) => {
return parseInt((max - min) * (value / 100) + min + 1);
};
function TemperatureAction(inContext, inSettings, jsn) {
this.property = 'temperature';
const setStateFunction = `set${Utils.capitalize(this.property)}`;
// Inherit from PropertyAction
PropertyAction.call(this, inContext, inSettings, jsn);
// setValue is sent from the 'keyUp' event and
// contains the value of the slider (0-100)
this.setValue = (inValue, jsn) => {
const target = this.getCurrentLightOrGroup();
if(target === false) return;
if(target.objCache.power === false) return;
const ct = target.objCache?.originalValue?.capabilities?.control?.ct;
if(!ct) return;
let value = inValue ? percentOfRange(inValue, ct.min, ct.max) : target.objCache[this.property];
if(jsn?.payload?.ticks) {
const settings = this.getSettings();
const scaleTicks = settings?.scaleTicks || 1;
const multiplier = scaleTicks * jsn.payload.ticks;
let addThis = (ct.max - ct.min) * (multiplier / 100);
addThis = addThis > 0 ? Math.floor(addThis) : Math.ceil(addThis);
value = Utils.minmax(parseInt(value + addThis), ct.min, ct.max);
}
target.obj[setStateFunction](value, (inSuccess, inError) => {
if(inSuccess) {
target.objCache[this.property] = value;
this.updateDisplay(target.objCache, this.property, jsn);
this.updateAllActions();
} else {
log(inError);
showAlert(inContext);
}
});
};
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/timers.js
================================================
/* global ESDTimerWorker */
/*eslint no-unused-vars: "off"*/
/*eslint-env es6*/
let ESDTimerWorker = new Worker(URL.createObjectURL(
new Blob([timerFn.toString().replace(/^[^{]*{\s*/, '').replace(/\s*}[^}]*$/, '')], {type: 'text/javascript'})
));
ESDTimerWorker.timerId = 1;
ESDTimerWorker.timers = {};
const ESDDefaultTimeouts = {
timeout: 0,
interval: 10
};
Object.freeze(ESDDefaultTimeouts);
function _setTimer(callback, delay, type, params) {
const id = ESDTimerWorker.timerId++;
ESDTimerWorker.timers[id] = {callback, params};
ESDTimerWorker.onmessage = (e) => {
if (ESDTimerWorker.timers[e.data.id]) {
if (e.data.type === 'clearTimer') {
delete ESDTimerWorker.timers[e.data.id];
} else {
const cb = ESDTimerWorker.timers[e.data.id].callback;
if (cb && typeof cb === 'function') cb(...ESDTimerWorker.timers[e.data.id].params);
}
}
};
ESDTimerWorker.postMessage({type, id, delay});
return id;
}
function _setTimeoutESD(...args) {
let [callback, delay = 0, ...params] = [...args];
return _setTimer(callback, delay, 'setTimeout', params);
}
function _setIntervalESD(...args) {
let [callback, delay = 0, ...params] = [...args];
return _setTimer(callback, delay, 'setInterval', params);
}
function _clearTimeoutESD(id) {
ESDTimerWorker.postMessage({type: 'clearTimeout', id}); // ESDTimerWorker.postMessage({type: 'clearInterval', id}); = same thing
delete ESDTimerWorker.timers[id];
}
window.setTimeout = _setTimeoutESD;
window.setInterval = _setIntervalESD;
window.clearTimeout = _clearTimeoutESD; //timeout and interval share the same timer-pool
window.clearInterval = _clearTimeoutESD;
/** This is our worker-code
* It is executed in it's own (global) scope
* which is wrapped above @ `let ESDTimerWorker`
*/
function timerFn() {
/*eslint indent: ["error", 4, { "SwitchCase": 1 }]*/
let timers = {};
let debug = false;
let supportedCommands = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
function log(e) {
console.log('Worker-Info::Timers', timers);
}
function clearTimerAndRemove(id) {
if (timers[id]) {
if (debug) console.log('clearTimerAndRemove', id, timers[id], timers);
clearTimeout(timers[id]);
delete timers[id];
postMessage({type: 'clearTimer', id: id});
if (debug) log();
}
}
onmessage = function (e) {
// first see, if we have a timer with this id and remove it
// this automatically fulfils clearTimeout and clearInterval
supportedCommands.includes(e.data.type) && timers[e.data.id] && clearTimerAndRemove(e.data.id);
if (e.data.type === 'setTimeout') {
timers[e.data.id] = setTimeout(() => {
postMessage({id: e.data.id});
clearTimerAndRemove(e.data.id); //cleaning up
}, Math.max(e.data.delay || 0));
} else if (e.data.type === 'setInterval') {
timers[e.data.id] = setInterval(() => {
postMessage({id: e.data.id});
}, Math.max(e.data.delay || ESDDefaultTimeouts.interval));
}
};
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/utils.js
================================================
/**
@file utils.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Register the plugin or PI
function registerPluginOrPI(inEvent, inUUID) {
if (websocket) {
websocket.send(JSON.stringify({
event: inEvent,
uuid: inUUID,
}));
}
}
// Save settings
function saveSettings(inAction, inUUID, inSettings) {
if (websocket) {
websocket.send(JSON.stringify({
action: inAction,
event: 'setSettings',
context: inUUID,
payload: inSettings,
}));
}
}
// Save global settings
function saveGlobalSettings(inUUID) {
if (websocket) {
websocket.send(JSON.stringify({
event: 'setGlobalSettings',
context: inUUID,
payload: globalSettings,
}));
}
}
// Request global settings for the plugin
function requestGlobalSettings(inUUID) {
if (websocket) {
websocket.send(JSON.stringify({
event: 'getGlobalSettings',
context: inUUID,
}));
}
}
// Log to the global log file
function logToFile(inMessage) {
// Log to the developer console
let timeString = new Date().toLocaleString();
// Log to the Stream Deck log file
if (websocket) {
websocket.send(JSON.stringify({
event: 'logMessage',
payload: {
message: inMessage,
},
}));
}
}
const log = console.log.bind(
console,
'%c [HUE]',
'color: #66c',
);
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// const debug = true;
// Object.defineProperty(this, "log", {
// get: function () {
// return debug ? console.log.bind(window.console, test(), '[DEBUG]') : function(){};
// }
// });
const debounce = (callback, time = 800) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => callback(...args), time);
};
};
// Show alert icon on the key
function showAlert(inUUID) {
if (websocket) {
websocket.send(JSON.stringify({
event: 'showAlert',
context: inUUID,
}));
}
}
// Set the state of a key
function setState(inContext, inState) {
if (websocket) {
websocket.send(JSON.stringify({
event: 'setState',
context: inContext,
payload: {
state: inState,
},
}));
}
}
// Set data to PI
function sendToPropertyInspector(inAction, inContext, inData) {
if (websocket) {
websocket.send(JSON.stringify({
action: inAction,
event: 'sendToPropertyInspector',
context: inContext,
payload: inData,
}));
}
}
// Set data to plugin
function sendToPlugin(inAction, inContext, inData) {
if (websocket) {
websocket.send(JSON.stringify({
action: inAction,
event: 'sendToPlugin',
context: inContext,
payload: inData,
}));
}
}
// Send feedback to the Stream Deck+ panel (SD+)
function setFeedback(inContext, inPayload) {
if (websocket) {
websocket.send(JSON.stringify({
event: 'setFeedback',
context: inContext,
payload: inPayload,
}));
}
}
// Load the localizations
function getLocalization(inLanguage, inCallback) {
let url = `../${inLanguage}.json`;
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
try {
let data = JSON.parse(xhr.responseText);
let localization = data['Localization'];
inCallback(true, localization);
}
catch(e) {
inCallback(false, 'Localizations is not a valid json.');
}
}
else {
inCallback(false, 'Could not load the localizations.');
}
};
xhr.onerror = () => {
inCallback(false, 'An error occurred while loading the localizations.');
};
xhr.ontimeout = () => {
inCallback(false, 'Localization timed out.');
};
xhr.send();
}
const Utils = {};
Utils.debounce = function(func, wait = 100) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
};
};
Utils.throttle = function(fn, threshold = 250, context) {
let last, timer;
return function() {
var ctx = context || this;
var now = new Date().getTime(),
args = arguments;
if(last && now < last + threshold) {
clearTimeout(timer);
timer = setTimeout(function() {
last = now;
fn.apply(ctx, args);
}, threshold);
} else {
last = now;
fn.apply(ctx, args);
}
};
};
Utils.capitalize = function(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
};
Utils.minmax = function(v = 0, min = 0, max = 100) {
return Math.min(max, Math.max(min, v));
};
Utils.percent = (value, min, max) => {
return ((value - min) / (max - min)) * 100;
};
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/css/main.css
================================================
/**
@file main.css
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
html, body {
margin: 0;
color: #e6e6e6;
background-color: #2d2d2d;
font-family: 'Helvetica Light', 'Helvetica', Arial, sans-serif;
height: 100%;
-webkit-user-select: none;
user-select: none;
overflow: hidden;
}
h1, h2 {
text-align: center;
}
h1 {
margin-top: 10px;
margin-bottom: 10px;
}
h2 {
color: #969696;
font-weight: 100;
margin-bottom: 5px;
}
.header {
padding: 25px 25px 5px;
}
#content {
padding: 0 25px 15px;
}
.main {
min-width: 400px;
}
@media (min-width: 600px) {
.main {
max-width: 500px;
height: 100%;
width: 100%;
display: table;
margin-left: auto;
margin-right: auto;
}
.center {
display: table-cell;
height: 100%;
vertical-align: middle;
}
.border {
border: 1px solid #3c3c3c;
border-radius: 5px;
margin: 10px;
min-height: 625px;
}
}
p, li {
line-height: 1.5;
text-align: center;
}
.status-bar {
display: table;
width: 100%;
border-spacing: 3px;
border-collapse: separate;
}
.status-row {
display: table-row;
}
.status-cell {
display: table-cell;
background-color: #3d3d3d;
height: 10px;
}
.status-cell.active {
background-color: #007dff;
}
.image {
width: 250px;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
padding-top: 5px;
padding-bottom: 20px;
}
label {
width: 100%;
font-weight: bold;
text-align: center;
display: inline-block;
box-sizing: border-box;
}
input {
width: 65%;
color: #fff;
border: 1px solid #8d8d8d;
background-color: #3d3d3d;
border-radius: 4px;
padding: 8px;
margin: 8px auto 16px;
text-align: center;
font-size: 1rem;
display: block;
box-sizing: border-box;
}
input:focus, input:active {
border-color: #007dff;
}
.button, .button-main, .button-transparent {
padding: 8px 50px;
display: table;
margin: 8px auto;
cursor: pointer;
border-radius: 4px;
}
.button.block, .button-main.block {
width: 65%;
padding: 8px 16px;
display: block;
box-sizing: border-box;
text-align: center;
}
.button {
background-color: #3d3d3d;
}
.button-main {
background-color: #535353;
}
.button:hover, .button-main:hover {
background-color: #007dff;
}
.button-transparent:hover {
color: #969696;
}
.hide {
display: none;
}
.error-container > div {
color: #fff;
background-color: rgba(255, 0, 0, .2);
border: 1px solid rgba(255, 0, 0, .5);
border-radius: 4px;
text-align: center;
width: 80%;
box-sizing: border-box;
margin: 0 auto 24px;
padding: 8px;
}
#loader {
width: 40px;
height: 40px;
background-color: #666;
margin: 20px auto;
-webkit-animation: sk-rotateplane 1.2s infinite ease-in-out;
animation: sk-rotateplane 1.2s infinite ease-in-out;
}
@-webkit-keyframes sk-rotateplane {
0% {
-webkit-transform: perspective(120px)
}
50% {
-webkit-transform: perspective(120px) rotateY(180deg)
}
100% {
-webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg)
}
}
@keyframes sk-rotateplane {
0% {
transform: perspective(120px) rotateX(0deg) rotateY(0deg);
-webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)
}
50% {
transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
-webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)
}
100% {
transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
-webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/index.html
================================================
com.elgato.philips-hue.setup
Philips Hue
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/discoveryView.js
================================================
/**
@file discoveryView.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Load the discovery view
function loadDiscoveryView() {
// Delay the result for 1.5 seconds
let resultDelay = 1500;
// Set the status bar
setStatusBar('discovery');
// Fill the title
document.getElementById('title').innerHTML = localization['Discovery']['Title'];
// Fill the content area
document.getElementById('content').innerHTML = `
`;
// Start the discovery
autoDiscovery();
// Discover all bridges
function autoDiscovery() {
Bridge.discover((status, data) => {
if (status) {
// Bridge discovery request was successful
bridges = data;
// Delay displaying the result
setTimeout(() => {
if (bridges.length === 0) {
// No bridges were found
// Fill the title
document.getElementById('title').innerHTML = localization['Discovery']['TitleNone'];
// Fill the content area
document.getElementById('content').innerHTML = `
${localization['Discovery']['DescriptionNone']}
${localization['Discovery']['Retry']}
${localization['Discovery']['Close']}
`;
// Add event listener
document.getElementById('retry').addEventListener('click', retry);
document.addEventListener('enterPressed', retry);
document.getElementById('close').addEventListener('click', close);
document.addEventListener('escPressed', close);
}
else {
// At least one bridge was found
let content;
if (bridges.length === 1) {
// Exactly one bridge was found
// Fill the title
document.getElementById('title').innerHTML = localization['Discovery']['TitleOne'];
// Fill the content area
content = `
${localization['Discovery']['DescriptionFound']}
`;
}
else {
// At least 2 bridges were found
// Fill the title
document.getElementById('title').innerHTML = localization['Discovery']['TitleMultiple'].replace('{{ number }}', bridges.length);
// Fill the content area
content = `
`;
// Add event listener
document.getElementById('pair').addEventListener('click', pair);
document.addEventListener('enterPressed', pair);
document.getElementById('retry').addEventListener('click', retry);
document.addEventListener('escPressed', retry);
}
}, resultDelay);
}
else {
// An error occurred while contacting the meethue discovery service
document.getElementById('content').innerHTML = `
${data}
`;
}
});
}
// Open pairing view
function pair() {
unloadDiscoveryView();
loadPairingView();
}
// Retry discovery by reloading the view
function retry() {
unloadDiscoveryView();
loadDiscoveryView();
}
// Close the window
function close() {
window.close();
}
// Unload view
function unloadDiscoveryView() {
// Remove event listener
document.removeEventListener('enterPressed', retry);
document.removeEventListener('enterPressed', pair);
document.removeEventListener('escPressed', close);
document.removeEventListener('escPressed', retry);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/introView.js
================================================
/**
@file introView.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Load the intro view
function loadIntroView() {
// Set the status bar
setStatusBar('intro');
// Fill the title
document.getElementById('title').innerHTML = localization['Intro']['Title'];
// Fill the content area
document.getElementById('content').innerHTML = `
${localization['Intro']['Description']}
${localization['Intro']['Start']}
${localization['Intro']['Manual']}
${localization['Intro']['Close']}
`;
// Add event listener
document.getElementById('start').addEventListener('click', startPairing);
document.addEventListener('enterPressed', startPairing);
document.getElementById('manual').addEventListener('click', startManual);
document.getElementById('close').addEventListener('click', close);
document.addEventListener('escPressed', close);
// Load the pairing view
function startPairing() {
unloadIntroView();
loadDiscoveryView();
}
// Load the manual view
function startManual() {
unloadIntroView();
loadManualView();
}
// Close the window
function close() {
window.close();
}
// Unload view
function unloadIntroView() {
// Remove event listener
document.removeEventListener('enterPressed', startPairing);
document.removeEventListener('escPressed', close);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/main.js
================================================
/**
@file main.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Global variable containing the localizations
var localization = null;
// Global variable containing the discovered bridges
var bridges = [];
// Global variable containing the paired bridge
var bridge = null;
// Global function to set the status bar to the correct view
function setStatusBar(view) {
// Remove active status from all status cells
let statusCells = document.getElementsByClassName('status-cell');
Array.from(statusCells).forEach((cell) => {
cell.classList.remove('active');
});
// Set it only to the current one
document.getElementById('status-' + view).classList.add('active');
}
// Main function run after the page is fully loaded
window.onload = () => {
// Bind enter and ESC keys
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
let event = new CustomEvent('enterPressed');
document.dispatchEvent(event);
}
else if (e.key === 'Esc' || e.key === 'Escape') {
let event = new CustomEvent('escPressed');
document.dispatchEvent(event);
}
});
// Get the url parameter
let url = new URL(window.location.href);
let language = url.searchParams.get('language');
// Load the localizations
getLocalization(language, (inStatus, inLocalization) => {
if (inStatus) {
// Save the localizations globally
localization = inLocalization['Setup'];
// Show the intro view
loadIntroView();
}
else {
document.getElementById('content').innerHTML = `
${inLocalization}
`;
}
});
};
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/manualView.js
================================================
/**
@file manualView.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Load the manual view
function loadManualView() {
// Set the status bar
setStatusBar('discovery');
// Fill the title
document.getElementById('title').innerHTML = localization['Manual']['Title'];
// Fill the content area
document.getElementById('content').innerHTML = `
${localization['Manual']['Description']}
${localization['Manual']['Check']}
${localization['Manual']['Close']}
`;
// Set cursor to input field
document.getElementById('ip').focus();
// Add event listener
document.getElementById('check').addEventListener('click', check);
document.addEventListener('enterPressed', check);
document.getElementById('close').addEventListener('click', close);
document.addEventListener('escPressed', close);
// Print error message
function printError(error) {
document.getElementById('ip-validation').innerHTML = `
${error}
`;
}
// Check ip address
function check() {
let ip = document.getElementById('ip').value.trim();
// check if input is empty
if (!ip) {
printError(localization['Manual']['Error']['Empty']);
return;
}
// check if ip is invalid
let ipV4Regex = '^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}$';
if (!(new RegExp(ipV4Regex)).test(ip)) {
printError(localization['Manual']['Error']['Invalid']);
return;
}
Bridge.check(ip, null, (success, data) => {
if (success) {
bridges = [
new Bridge(data.ip, data.id),
];
pair();
}
else {
printError(localization['Manual']['Error']['Unreachable']);
}
});
}
// Open pairing view
function pair() {
unloadManualView();
loadPairingView();
}
// Close the window
function close() {
window.close();
}
// Unload view
function unloadManualView() {
// Remove event listener
document.removeEventListener('enterPressed', check);
document.removeEventListener('escPressed', close);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/pairingView.js
================================================
/**
@file pairingView.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Load the pairing view
function loadPairingView() {
// Time used to automatically pair bridges
let autoPairingTimeout = 30;
// Define local timer
let timer = null;
// Set the status bar
setStatusBar('pairing');
// Fill the title
document.getElementById('title').innerHTML = localization['Pairing']['Title'];
// Fill the content area
document.getElementById('content').innerHTML = `
${localization['Pairing']['Description']}
`;
// Start the pairing
autoPairing();
// For n seconds try to connect to the bridge automatically
function autoPairing() {
// Define local timer counter
let timerCounter = 0;
// Start a new timer to auto connect to the bridges
timer = setInterval(() => {
if (timerCounter < autoPairingTimeout) {
// Try to connect for n seconds
pair();
timerCounter++;
}
else {
// If auto connect was not successful for n times,
// stop auto connecting and show controls
// Stop the timer
clearInterval(timer);
timer = null;
// Hide the loader animation
document.getElementById('loader').classList.add('hide');
// Show manual user controls instead
document.getElementById('controls').innerHTML = `
${localization['Pairing']['Retry']}
${localization['Pairing']['Close']}
`;
// Add event listener
document.getElementById('retry').addEventListener('click', retry);
document.addEventListener('enterPressed', retry);
document.getElementById('close').addEventListener('click', close);
document.addEventListener('escPressed', close);
}
}, 1000)
}
// Try to pair with all discovered bridges
function pair() {
bridges.forEach(item => {
item.pair((status, data) => {
if (status) {
// Pairing was successful
bridge = item;
// Show the save view
unloadPairingView();
loadSaveView();
}
});
});
}
// Retry pairing by reloading the view
function retry() {
unloadPairingView();
loadPairingView();
}
// Close the window
function close() {
window.close();
}
// Unload view
function unloadPairingView() {
// Stop the timer
clearInterval(timer);
timer = null;
// Remove event listener
document.removeEventListener('escPressed', retry);
document.removeEventListener('enterPressed', close);
}
}
================================================
FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/saveView.js
================================================
/**
@file saveView.js
@brief Philips Hue Plugin
@copyright (c) 2019, Corsair Memory, Inc.
@license This source code is licensed under the MIT-style license found in the LICENSE file.
*/
// Load the save view
function loadSaveView() {
// Set the status bar
setStatusBar('save');
// Fill the title
document.getElementById('title').innerHTML = localization['Save']['Title'];
// Fill the content area
document.getElementById('content').innerHTML = `