@irusland/homebridge-mqttthing
Version:
Homebridge plugin supporting various services over MQTT (with TLS fixes)
1,142 lines (998 loc) • 186 kB
JavaScript
// MQTT Thing Accessory plugin for Homebridge
// A Homebridge plugin for serveral services, based on homebrige-mqtt-switch and homebridge-mqttlightbulb
/* eslint-disable object-property-newline */
/* eslint-disable no-plusplus */
'use strict'; // eslint-disable-line
var os = require( "os" );
var packagedef = require( './package.json' );
var homebridgeLib = require( 'homebridge-lib' );
var fakegatoHistory = require( 'fakegato-history' );
var fs = require( "fs" );
var path = require( "path" );
var mqttlib = require( './libs/mqttlib' );
const EventEmitter = require( 'events' );
var Service, Characteristic, Eve, HistoryService;
var homebridgePath;
function makeThing( log, accessoryConfig, api ) {
// Create accessory information service
function makeAccessoryInformationService() {
var informationService = new Service.AccessoryInformation();
informationService.setCharacteristic( Characteristic.Manufacturer, accessoryConfig.manufacturer || "mqttthing" );
informationService.setCharacteristic( Characteristic.Model, accessoryConfig.model || accessoryConfig.type );
informationService.setCharacteristic( Characteristic.SerialNumber, accessoryConfig.serialNumber || ( os.hostname() + "-" + accessoryConfig.name ) );
informationService.setCharacteristic( Characteristic.FirmwareRevision, accessoryConfig.firmwareRevision || packagedef.version );
return informationService;
}
//
// MQTT Wrappers
//
// Initialize MQTT client
let ctx = { log, config: accessoryConfig, homebridgePath };
try {
mqttlib.init( ctx );
} catch( ex ) {
log.error( 'MQTT initialisation failed: ' + ex );
return { getServices: () => [] };
}
// MQTT Subscribe
function mqttSubscribe( topic, property, handler ) {
mqttlib.subscribe( ctx, topic, property, handler );
}
// MQTT Publish
function mqttPublish( topic, property, message ) {
mqttlib.publish( ctx, topic, property, message );
}
// Delayed one-shot function call
let throttledCallTimers = {};
let throttledCall = function( func, identifier, timeout ) {
if( throttledCallTimers[ identifier ] ) {
clearTimeout( throttledCallTimers[ identifier ] );
}
throttledCallTimers[ identifier ] = setTimeout( function() {
throttledCallTimers[ identifier ] = null;
func();
}, timeout );
};
// Controllers
let controllers = [];
// Create services
function createServices() {
function configToServices( config ) {
// Adaptive lighting support...
// Our controller
let adaptiveLightingController = null;
let adaptiveLightingEmitter = new EventEmitter();
// Test whether adaptive lighting is active
let isAdaptiveLightingActive = () => adaptiveLightingController && adaptiveLightingController.isAdaptiveLightingActive();
// Disable adaptive lighting (when user sets hue/saturation explicitly)
let disableAdaptiveLighting = function( what ) {
if( isAdaptiveLightingActive() ) {
log( `External control (${what}) disabling adaptive lighting` );
adaptiveLightingController.disableAdaptiveLighting();
}
};
// Do we support adaptive lighting?
let supportAdaptiveLighting = function() {
return ( config.adaptiveLighting !== false ) && api.versionGreaterOrEqual && api.versionGreaterOrEqual( '1.3.0-beta.27' );
};
// Create adaptive lighting controller
let addAdaptiveLightingController = function( service ) {
if( adaptiveLightingController ) {
log.error( 'Logic error: Duplicate call to addAdaptiveLightingController() - ignoring' );
return;
}
log( 'Enabling adaptive lighting' );
adaptiveLightingController = new api.hap.AdaptiveLightingController( service, {
controllerMode: api.hap.AdaptiveLightingControllerMode.AUTOMATIC
} );
controllers.push( adaptiveLightingController );
};
// Migrate old-style history options
if( config.hasOwnProperty( 'history' ) ) {
if( typeof config.history == 'object' ) {
config.historyOptions = config.history;
config.history = true;
} else {
if( !config.hasOwnProperty( 'historyOptions' ) ) {
config.historyOptions = {};
}
}
// migrate negated options for config-ui-x defaults
if( !config.historyOptions.hasOwnProperty( 'noAutoTimer' ) ) {
config.historyOptions.noAutoTimer = ( config.historyOptions.autoTimer === false );
}
if( !config.historyOptions.hasOwnProperty( 'noAutoRepeat' ) ) {
config.historyOptions.noAutoRepeat = ( config.historyOptions.autoRepeat === false );
}
}
// History persistence path
function historyPersistencePath() {
let directory;
if( config.historyOptions && config.historyOptions.persistencePath ) {
if( config.historyOptions.persistencePath[ 0 ] == '/' ) {
// full path
directory = config.historyOptions.persistencePath;
} else {
// assume relative to homebridge path
directory = path.join( homebridgePath, config.historyOptions.persistencePath );
}
} else {
// no path configured - use homebridge path
directory = homebridgePath;
}
return directory;
}
function historyCounterFile() {
const counterFile = path.join( historyPersistencePath(), os.hostname().split( "." )[ 0 ] + "_" + config.name + "_cnt_persist.json" );
return counterFile;
}
const c_mySetContext = { mqttthing: '---my-set-context--' };
// constructor for fakegato-history options
function HistoryOptions( isEventSensor = false ) {
// maximum size of stored data points
this.size = config.historyOptions.size || 4032;
// data will be stored in .homebridge or path specified with homebridge -U option
this.storage = 'fs';
if( config.historyOptions.persistencePath ) {
this.path = historyPersistencePath();
}
if( config.historyOptions.noAutoTimer === true || config.historyOptions.mergeInterval ) {
// disable averaging (and repeating) interval timer
// if mergeInterval is used, then autoTimer has to be deactivated (inconsistencies possible)
this.disableTimer = true;
}
// disable repetition (if no data was received in last interval)
if( config.historyOptions.noAutoRepeat === true ) {
if( isEventSensor ) {
// for 'motion' and 'door' type
this.disableTimer = true;
} else {
// for 'weather', 'room' and 'energy' type
this.disableRepeatLastData = true;
}
}
}
// The states of our characteristics
var state = ctx.state = {};
// Internal event handling
var events = {};
function raiseEvent( property ) {
if( events.hasOwnProperty( property ) ) {
events[ property ]();
}
}
function makeConfirmedPublisher( setTopic, getTopic, property, makeConfirmed ) {
return mqttlib.makeConfirmedPublisher( ctx, setTopic, getTopic, property, makeConfirmed );
}
//! Determine appropriate on/off value for Boolean property (not forced to string) for MQTT publishing.
//! Returns null if no offValue.
function getOnOffPubValue( value ) {
let mqttval;
if( config.onValue ) {
// using onValue/offValue
mqttval = value ? config.onValue : config.offValue;
} else if( config.integerValue ) {
mqttval = value ? 1 : 0;
} else {
mqttval = value ? true : false;
}
if( mqttval === undefined || mqttval === null ) {
return null;
} else {
return mqttval;
}
}
//! Test whether a value represents 'on'
function isRecvValueOn( mqttval ) {
let onval = getOnOffPubValue( true );
return mqttval === onval || mqttval == ( onval + '' );
}
//! Test whether a value represents 'off'
function isRecvValueOff( mqttval ) {
if( config.otherValueOff ) {
if( !isRecvValueOn( mqttval ) ) {
// it's not the on value and we consider any other value to be off
return true;
}
}
let offval = getOnOffPubValue( false );
if( offval === null ) {
// there is no off value
return false;
}
if( mqttval === offval || mqttval == ( offval + '' ) ) {
// off value match - it's definitely off
return true;
}
// not off
return false;
}
function getOnlineOfflinePubValue( value ) {
var pubVal = ( value ? config.onlineValue : config.offlineValue );
if( pubVal === undefined ) {
pubVal = getOnOffPubValue( value );
}
return pubVal;
}
function isRecvValueOnline( mqttval ) {
let onval = getOnlineOfflinePubValue( true );
return mqttval === onval || mqttval == ( onval + '' );
}
function isRecvValueOffline( mqttval ) {
let offval = getOnlineOfflinePubValue( false );
return mqttval === offval || mqttval == ( offval + '' );
}
function mapValueForHomebridge( val, mapValueFunc ) {
if( mapValueFunc ) {
return mapValueFunc( val );
} else {
return val;
}
}
function isOffline() {
return state.online === false;
}
function handleGetStateCallback( callback, value ) {
if( isOffline() ) {
callback( 'offline' );
} else {
callback( null, value );
}
}
function isSet( val ) {
return val !== undefined && val !== null;
}
function isValid( charac, value ) {
// if validation is disabled, accept anything
if( config.validate === false ) {
return true;
}
const format = charac.props.format;
if( format === 'int' || format === "uint8" || format == "uint16" || format == "uint32" ) {
if( !Number.isInteger( value ) ) {
log( `Ignoring invalid value [${value}] for ${charac.displayName} - not an integer` );
return false;
}
if( isSet( charac.props.minValue ) && value < charac.props.minValue ) {
log( `Ignoring invalid value [${value}] for ${charac.displayName} - below minimum (${charac.props.minValue})` );
return false;
}
if( isSet( charac.props.maxValue ) && value > charac.props.maxValue ) {
log( `Ignoring invalid value [${value}] for ${charac.displayName} - above maximum (${charac.props.maxValue})` );
return false;
}
} else if( format === 'float' ) {
if( typeof value !== 'number' || isNaN( value ) ) {
log( `Ignoring invalid value [${value}] for ${charac.displayName} - not a number` );
return false;
}
if( isSet( charac.props.minValue ) && value < charac.props.minValue ) {
log( `Ignoring invalid value [${value}] for ${charac.displayName} - below minimum (${charac.props.minValue})` );
return false;
}
if( isSet( charac.props.maxValue ) && value > charac.props.maxValue ) {
log( `Ignoring invalid value [${value}] for ${charac.displayName} - above maximum (${charac.props.maxValue})` );
return false;
}
} else if( format === 'bool' ) {
if( value !== true && value !== false ) {
log( `Ignoring invalid value [${value}] for ${charac.displayName} - not a Boolean` );
return false;
}
} else if( format === 'string' ) {
if( typeof value !== 'string' ) {
log( `Ignoring invalid value [${value}] for ${charac.displayName} - not a string` );
return false;
}
} else {
log( `Unable to validate ${charac.displayName}, format [${charac.props.format}] - ${JSON.stringify( charac )}` );
}
return true;
}
function setCharacteristic( charac, value ) {
if( isValid( charac, value ) ) {
charac.setValue( value, undefined, c_mySetContext );
}
}
function booleanCharacteristic( service, property, characteristic, setTopic, getTopic, initialValue, mapValueFunc, turnOffAfterms, resetStateAfterms, enableConfirmation ) {
var publish = makeConfirmedPublisher( setTopic, getTopic, property, enableConfirmation );
// auto-turn-off and reset-state timers
var autoOffTimer = null;
var autoResetStateTimer = null;
// default state
state[ property ] = ( initialValue ? true : false );
// set up characteristic
var charac = service.getCharacteristic( characteristic );
charac.on( 'get', function( callback ) {
handleGetStateCallback( callback, state[ property ] );
} );
if( setTopic ) {
charac.on( 'set', function( value, callback, context ) {
if( context !== c_mySetContext ) {
state[ property ] = value;
publish( getOnOffPubValue( value ) );
}
callback();
// optionally turn off after timeout
if( value && turnOffAfterms ) {
if( autoOffTimer ) {
clearTimeout( autoOffTimer );
}
autoOffTimer = setTimeout( function() {
autoOffTimer = null;
state[ property ] = false;
publish( getOnOffPubValue( false ) );
setCharacteristic( charac, mapValueForHomebridge( false, mapValueFunc ) );
}, turnOffAfterms );
}
} );
}
if( initialValue ) {
setCharacteristic( charac, mapValueForHomebridge( initialValue, mapValueFunc ) );
}
// subscribe to get topic
if( getTopic ) {
mqttSubscribe( getTopic, property, function( topic, message ) {
// determine whether this is an on or off value
let newState = false; // assume off
if( isRecvValueOn( message ) ) {
newState = true; // received on value so on
} else if( !isRecvValueOff( message ) ) {
// received value NOT acceptable as 'off' so ignore message
return;
}
// if it changed, set characteristic
if( state[ property ] != newState ) {
state[ property ] = newState;
setCharacteristic( charac, mapValueForHomebridge( newState, mapValueFunc ) );
}
// optionally reset state to OFF after a timeout
if( newState && resetStateAfterms ) {
if( autoResetStateTimer ) {
clearTimeout( autoResetStateTimer );
}
autoResetStateTimer = setTimeout( function() {
autoResetStateTimer = null;
state[ property ] = false;
setCharacteristic( charac, mapValueForHomebridge( false, mapValueFunc ) );
}, resetStateAfterms );
}
} );
}
}
function booleanState( property, getTopic, initialValue, isOnFunc, isOffFunc ) {
// default state
state[ property ] = ( initialValue ? true : false );
// MQTT subscription
if( getTopic ) {
mqttSubscribe( getTopic, property, function( topic, message ) {
if( isOnFunc( message ) ) {
state[ property ] = true;
} else if( isOffFunc( message ) ) {
state[ property ] = false;
}
} );
}
}
function state_Online() {
booleanState( 'online', config.topics.getOnline, true, isRecvValueOnline, isRecvValueOffline );
}
function integerCharacteristic( service, property, characteristic, setTopic, getTopic, options ) {
let initialValue = options && options.initialValue;
let minValue = options && options.minValue;
let maxValue = options && options.maxValue;
// default state
state[ property ] = initialValue || 0;
// set up characteristic
var charac = service.getCharacteristic( characteristic );
// min/max
if( Number.isInteger( minValue ) ) {
charac.props.minValue = minValue;
}
if( Number.isInteger( maxValue ) ) {
charac.props.maxValue = maxValue;
}
// get/set
charac.on( 'get', function( callback ) {
handleGetStateCallback( callback, state[ property ] );
} );
let onSet = function( value, context ) {
if( context !== c_mySetContext ) {
state[ property ] = value;
if( setTopic ) {
mqttPublish( setTopic, property, value );
}
}
if( options && options.onSet ) {
options.onSet( value, context );
}
};
if( setTopic || ( options && options.onSet ) ) {
charac.on( 'set', function( value, callback, context ) {
onSet( value, context );
callback();
} );
}
if( initialValue ) {
setCharacteristic( charac, initialValue );
}
// subscribe to get topic
if( getTopic ) {
mqttSubscribe( getTopic, property, function( topic, message ) {
var newState = parseInt( message );
if( state[ property ] != newState ) {
if( options && options.onMqtt ) {
options.onMqtt( newState );
}
// update state and characteristic
state[ property ] = newState;
setCharacteristic( charac, newState );
}
} );
}
return { onSet };
}
function addCharacteristic( service, property, characteristic, defaultValue, characteristicChanged, adaptiveEventName ) {
state[ property ] = defaultValue;
var charac = service.getCharacteristic( characteristic );
setCharacteristic( charac, defaultValue );
charac.on( 'get', function( callback ) {
let valReturned = state[ property ];
if( !isValid( charac, valReturned ) ) {
valReturned = defaultValue;
}
handleGetStateCallback( callback, valReturned );
} );
if( characteristicChanged ) {
charac.on( 'set', function( value, callback, context ) {
if( context !== c_mySetContext ) {
state[ property ] = value;
characteristicChanged();
}
callback();
} );
if( adaptiveEventName ) {
adaptiveLightingEmitter.addListener( adaptiveEventName, ( value ) => {
state[ property ] = value;
characteristicChanged();
} );
}
}
}
function characteristics_HSVLight( service ) {
let lastpubmsg = '';
function publishNow() {
var bri = state.bri;
if( !config.topics.setOn && !state.on ) {
bri = 0;
}
var msg = state.hue + ',' + state.sat + ',' + bri;
if( msg != lastpubmsg ) {
mqttPublish( config.topics.setHSV, 'HSV', msg );
lastpubmsg = msg;
}
}
function publish() {
throttledCall( publishNow, 'hsv_publish', 20 );
}
if( config.topics.setOn ) {
characteristic_On( service );
} else {
addCharacteristic( service, 'on', Characteristic.On, false, function() {
if( state.on && state.bri == 0 ) {
state.bri = 100;
}
publish();
} );
}
addCharacteristic( service, 'hue', Characteristic.Hue, 0, publish, 'hue' );
addCharacteristic( service, 'sat', Characteristic.Saturation, 0, publish, 'saturation' );
addCharacteristic( service, 'bri', Characteristic.Brightness, 100, function() {
if( state.bri > 0 && !state.on ) {
state.on = true;
}
publish();
} );
if( config.topics.getHSV ) {
mqttSubscribe( config.topics.getHSV, 'HSV', function( topic, message ) {
var comps = ( '' + message ).split( ',' );
if( comps.length == 3 ) {
var hue = parseInt( comps[ 0 ] );
var sat = parseInt( comps[ 1 ] );
var bri = parseInt( comps[ 2 ] );
if( !config.topics.setOn ) {
var on = bri > 0 ? 1 : 0;
if( on != state.on ) {
state.on = on;
setCharacteristic( service.getCharacteristic( Characteristic.On ), on );
}
}
if( hue != state.hue ) {
disableAdaptiveLighting( 'HSV hue' );
state.hue = hue;
//log( 'hue ' + hue );
setCharacteristic( service.getCharacteristic( Characteristic.Hue ), hue );
}
if( sat != state.sat ) {
disableAdaptiveLighting( 'HSV saturation' );
state.sat = sat;
//log( 'sat ' + sat );
setCharacteristic( service.getCharacteristic( Characteristic.Saturation ), sat );
}
if( bri != state.bri ) {
state.bri = bri;
//log( 'bri ' + bri );
setCharacteristic( service.getCharacteristic( Characteristic.Brightness ), bri );
}
}
} );
}
if( supportAdaptiveLighting() ) {
characteristic_ColorTemperature_Internal( service );
}
}
/*
* HSV to RGB conversion from https://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately
* accepts parameters
* h Object = {h:x, s:y, v:z}
* OR
* h, s, v
*/
function HSVtoRGB( h, s, v ) {
var r, g, b, i, f, p, q, t;
if( arguments.length === 1 ) {
s = h.s, v = h.v, h = h.h;
}
i = Math.floor( h * 6 );
f = h * 6 - i;
p = v * ( 1 - s );
q = v * ( 1 - f * s );
t = v * ( 1 - ( 1 - f ) * s );
switch( i % 6 ) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: Math.round( r * 255 ),
g: Math.round( g * 255 ),
b: Math.round( b * 255 )
};
}
function ScaledHSVtoRGB( h, s, v ) {
return HSVtoRGB( h / 360, s / 100, v / 100 );
}
/* accepts parameters
* r Object = {r:x, g:y, b:z}
* OR
* r, g, b
*/
function RGBtoHSV( r, g, b ) {
if( arguments.length === 1 ) {
g = r.g, b = r.b, r = r.r;
}
var max = Math.max( r, g, b ), min = Math.min( r, g, b ),
d = max - min,
h,
s = ( max === 0 ? 0 : d / max ),
v = max / 255;
switch( max ) {
case min: h = 0; break;
case r: h = ( g - b ) + d * ( g < b ? 6 : 0 ); h /= 6 * d; break;
case g: h = ( b - r ) + d * 2; h /= 6 * d; break;
case b: h = ( r - g ) + d * 4; h /= 6 * d; break;
}
return {
h: h,
s: s,
v: v
};
}
function RGBtoScaledHSV( r, g, b ) {
var hsv = RGBtoHSV( r, g, b );
return {
h: hsv.h * 360,
s: hsv.s * 100,
v: hsv.v * 100
};
}
// byte to 2-characters of hex
function toHex( num ) {
var s = '0' + num.toString( 16 );
return s.substr( s.length - 2 );
}
function decodeRGBCommaSeparatedString( rgb ) {
if( rgb ) {
var comps = ( '' + rgb ).split( ',' );
if( comps.length == 3 ) {
return { r: comps[ 0 ], g: comps[ 1 ], b: comps[ 2 ] };
}
}
}
/*function calcWhiteFactor1( rgbin, white ) {
// scale rgb value to full brightness as comparing colours
let compmax = Math.max( rgbin.r, rgbin.g, rgbin.b );
if( compmax < 1 ) {
return 0;
}
let rgbsc = 255 / compmax;
let rgb = {r: rgbin.r * rgbsc, g: rgbin.g * rgbsc, b: rgbin.b * rgbsc};
// calculate factors
var rf = 1, gf = 1, bf = 1;
if( white.r < 255 ) {
rf = ( 255 - rgb.r ) / ( 255 - white.r ) / rgbsc;
}
if( white.g < 255 ) {
gf = ( 255 - rgb.g ) / ( 255 - white.g ) / rgbsc;
}
if( white.b < 255 ) {
bf = ( 255 - rgb.b ) / ( 255 - white.b ) / rgbsc;
}
return Math.min( Math.max( 0, Math.min( rf, gf, bf ) ), 1 );
}*/
function calcWhiteFactor2( rgbin, white ) {
// scale rgb value to full brightness as comparing colours
let compmax = Math.max( rgbin.r, rgbin.g, rgbin.b );
if( compmax < 1 ) {
return 0;
}
let rgbsc = 255 / compmax;
let rgb = { r: rgbin.r * rgbsc, g: rgbin.g * rgbsc, b: rgbin.b * rgbsc };
// calculate factors
let wmin = Math.min( white.r, white.g, white.b );
let cmin = Math.min( rgb.r, rgb.g, rgb.b );
var rf = 1, gf = 1, bf = 1;
if( white.r > wmin ) {
rf = ( rgb.r - cmin ) / ( white.r - wmin ) / rgbsc;
}
if( white.g > wmin ) {
gf = ( rgb.g - cmin ) / ( white.g - wmin ) / rgbsc;
}
if( white.b > wmin ) {
bf = ( rgb.b - cmin ) / ( white.b - wmin ) / rgbsc;
}
return Math.min( Math.max( 0, Math.min( rf, gf, bf ) ), 1 );
}
function calcWhiteFactor( rgb, white ) {
let rf = 1, gf = 1, bf = 1;
if( white.r > 0 ) {
rf = rgb.r / white.r;
}
if( white.g > 0 ) {
gf = rgb.g / white.g;
}
if( white.b > 0 ) {
bf = rgb.b / white.b;
}
return Math.min( Math.max( 0, Math.min( rf, gf, bf, calcWhiteFactor2( rgb, white ) ) ), 1 );
}
function characteristics_RGBLight( service ) {
var warmWhiteRGB, coldWhiteRGB;
state.red = 0;
state.green = 0;
state.blue = 0;
state.white = 0;
state.warmWhite = 0;
state.coldWhite = 0;
var setTopic, getTopic, numComponents, property;
var wwcwComps = false;
var whiteComp = false;
var whiteSep = false;
if( config.topics.setRGBWW ) {
setTopic = config.topics.setRGBWW;
getTopic = config.topics.getRGBWW;
property = 'RGBWW';
wwcwComps = true;
numComponents = 5;
warmWhiteRGB = decodeRGBCommaSeparatedString( config.warmWhite ) || { r: 255, g: 158, b: 61 };
coldWhiteRGB = decodeRGBCommaSeparatedString( config.coldWhite ) || { r: 204, g: 219, b: 255 };
} else if( config.topics.setRGBW ) {
setTopic = config.topics.setRGBW;
getTopic = config.topics.getRGBW;
property = 'RGBW';
whiteComp = true;
numComponents = 4;
} else {
setTopic = config.topics.setRGB;
getTopic = config.topics.getRGB;
property = 'RGB';
if( config.topics.setWhite ) {
whiteSep = true;
}
numComponents = 3;
}
var hexPrefix = null;
if( config.hexPrefix ) {
hexPrefix = config.hexPrefix;
} else if( config.hex ) {
hexPrefix = '';
}
let lastpubmsg = '';
function publishNow() {
var bri = state.bri;
if( !config.topics.setOn && !state.on ) {
bri = 0;
}
var rgb = ScaledHSVtoRGB( state.hue, state.sat, bri );
let orig_rgb = { ww: 0, cw: 0, ...rgb };
if( wwcwComps ) {
//console.log( rgb );
// calculate warm-white and cold-white factors (0-1 indicating proportion of warm/cold white in colour)
let warmFactor = calcWhiteFactor( rgb, warmWhiteRGB );
let coldFactor = calcWhiteFactor( rgb, coldWhiteRGB );
//console.log( "wf: " + warmFactor );
//console.log( "cf: " + coldFactor );
// sum must be below 1
let whiteFactor = warmFactor + coldFactor;
if( whiteFactor > 1 ) {
warmFactor = warmFactor / whiteFactor;
coldFactor = coldFactor / whiteFactor;
whiteFactor = 1;
}
// manipulate RGB values
rgb.ww = Math.floor( warmFactor * 255 );
rgb.cw = Math.floor( coldFactor * 255 );
//console.log( "ww: " + rgb.ww );
//console.log( "cw: " + rgb.cw );
/*rgb.r = Math.floor( rgb.r * ( 1 - whiteFactor ) );
rgb.g = Math.floor( rgb.g * ( 1 - whiteFactor ) );
rgb.b = Math.floor( rgb.b * ( 1 - whiteFactor ) );*/
rgb.r = Math.max( 0, Math.floor( rgb.r - warmFactor * warmWhiteRGB.r - coldFactor * coldWhiteRGB.r ) );
rgb.g = Math.max( 0, Math.floor( rgb.g - warmFactor * warmWhiteRGB.g - coldFactor * coldWhiteRGB.g ) );
rgb.b = Math.max( 0, Math.floor( rgb.b - warmFactor * warmWhiteRGB.b - coldFactor * coldWhiteRGB.b ) );
// any remaining pure white level can be replaced with a mixture of cold and warm white
let min = Math.min( rgb.r, rgb.g, rgb.b, 255 - rgb.ww, 255 - rgb.cw );
rgb.ww += Math.floor( min / 2 );
rgb.cw += Math.floor( min / 2 );
rgb.r -= min;
rgb.g -= min;
rgb.b -= min;
if( config.whiteMix === false || config.noWhiteMix === true ) {
if( ( rgb.ww > 0 || rgb.cw > 0 ) && ( rgb.r > 0 || rgb.g > 0 || rgb.b > 0 ) ) {
// mixing white and colours is not allowed on some devices
let redThreshold = ( config.redThreshold === undefined ) ? 15 : config.redThreshold;
let greenThreshold = ( config.greenThreshold === undefined ) ? 15 : config.greenThreshold;
let blueThreshold = ( config.blueThreshold === undefined ) ? 15 : config.blueThreshold;
if( rgb.r > redThreshold || rgb.g > greenThreshold || rgb.b > blueThreshold ) {
// colour
rgb = orig_rgb;
} else {
// white
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
}
}
}
// store white state
state.warmWhite = rgb.ww;
state.coldWhite = rgb.cw;
} else if( whiteSep || whiteComp ) {
// remove common component from red, green and blue to white
let min = Math.min( rgb.r, rgb.g, rgb.b );
rgb.w = min;
rgb.r -= min;
rgb.g -= min;
rgb.b -= min;
state.white = rgb.w;
}
state.red = rgb.r;
state.green = rgb.g;
state.blue = rgb.b;
var msg;
if( hexPrefix == null ) {
// comma-separated decimal
msg = rgb.r + ',' + rgb.g + ',' + rgb.b;
if( whiteComp ) {
msg += ',' + rgb.w;
} else if( wwcwComps ) {
if( config.switchWhites ) {
msg += ',' + rgb.cw + ',' + rgb.ww;
} else {
msg += ',' + rgb.ww + ',' + rgb.cw;
}
}
} else {
// hex
msg = hexPrefix + toHex( rgb.r ) + toHex( rgb.g ) + toHex( rgb.b );
if( whiteComp ) {
msg += toHex( rgb.w );
} else if( wwcwComps ) {
if( config.switchWhites ) {
msg += toHex( rgb.cw ) + toHex( rgb.ww );
} else {
msg += toHex( rgb.ww ) + toHex( rgb.cw );
}
}
}
if( msg != lastpubmsg ) {
mqttPublish( setTopic, property, msg );
lastpubmsg = msg;
}
if( whiteSep ) {
mqttPublish( config.topics.setWhite, 'white', rgb.w );
}
}
// hold off before publishing to ensure that all updated properties are collected first
function publish() {
throttledCall( publishNow, 'rgb_publish', 20 );
}
if( config.topics.setOn ) {
characteristic_On( service );
} else {
addCharacteristic( service, 'on', Characteristic.On, false, function() {
if( state.on && state.bri == 0 ) {
state.bri = 100;
}
publish();
} );
}
addCharacteristic( service, 'hue', Characteristic.Hue, 0, publish, 'hue' );
addCharacteristic( service, 'sat', Characteristic.Saturation, 0, publish, 'saturation' );
addCharacteristic( service, 'bri', Characteristic.Brightness, 100, function() {
if( state.bri > 0 && !state.on ) {
state.on = true;
}
publish();
} );
function updateColour( red, green, blue, white, warmWhite, coldWhite ) {
// add warm white/cold white in
if( wwcwComps ) {
red += Math.floor( warmWhiteRGB.r * warmWhite / 255 ) + Math.floor( coldWhiteRGB.r * coldWhite / 255 );
green += Math.floor( warmWhiteRGB.g * warmWhite / 255 ) + Math.floor( coldWhiteRGB.g * coldWhite / 255 );
blue += Math.floor( warmWhiteRGB.b * warmWhite / 255 ) + Math.floor( coldWhiteRGB.b * coldWhite / 255 );
}
// add any white component to red, green and blue
red = Math.min( red + white, 255 );
green = Math.min( green + white, 255 );
blue = Math.min( blue + white, 255 );
var hsv = RGBtoScaledHSV( red, green, blue );
var hue = Math.floor( hsv.h );
var sat = Math.floor( hsv.s );
var bri = Math.floor( hsv.v );
if( !config.topics.setOn ) {
var on = bri > 0 ? 1 : 0;
if( on != state.on ) {
state.on = on;
//log( 'on ' + on );
setCharacteristic( service.getCharacteristic( Characteristic.On ), on );
}
}
if( hue != state.hue ) {
disableAdaptiveLighting( 'calculated hue' );
state.hue = hue;
//log( 'hue ' + hue );
setCharacteristic( service.getCharacteristic( Characteristic.Hue ), hue );
}
if( sat != state.sat ) {
disableAdaptiveLighting( 'calculated saturation' );
state.sat = sat;
//log( 'sat ' + sat );
setCharacteristic( service.getCharacteristic( Characteristic.Saturation ), sat );
}
if( bri != state.bri ) {
state.bri = bri;
//log( 'bri ' + bri );
setCharacteristic( service.getCharacteristic( Characteristic.Brightness ), bri );
}
}
if( getTopic ) {
mqttSubscribe( getTopic, property, function( topic, message ) {
var ok = false;
var red, green, blue, white, warmWhite, coldWhite;
if( hexPrefix == null ) {
// comma-separated decimal
var comps = ( '' + message ).split( ',' );
if( comps.length == numComponents ) {
red = parseInt( comps[ 0 ] );
green = parseInt( comps[ 1 ] );
blue = parseInt( comps[ 2 ] );
if( whiteComp ) {
white = parseInt( comps[ 3 ] );
} else if( wwcwComps ) {
warmWhite = parseInt( comps[ 3 ] );
coldWhite = parseInt( comps[ 4 ] );
if( config.switchWhites ) {
let temp = warmWhite;
warmWhite = coldWhite;
coldWhite = temp;
}
}
ok = true;
}
} else {
// hex
if( message.length == hexPrefix.length + 2 * numComponents ) {
message = '' + message;
if( message.substr( 0, hexPrefix.length ) == hexPrefix ) {
red = parseInt( message.substr( hexPrefix.length, 2 ), 16 );
green = parseInt( message.substr( hexPrefix.length + 2, 2 ), 16 );
blue = parseInt( message.substr( hexPrefix.length + 4, 2 ), 16 );
if( whiteComp ) {
white = parseInt( message.substr( hexPrefix.length + 6, 2 ), 16 );
} else if( wwcwComps ) {
warmWhite = parseInt( message.substr( hexPrefix.length + 6, 2 ), 16 );
coldWhite = parseInt( message.substr( hexPrefix.length + 8, 2 ), 16 );
if( config.switchWhites ) {
let temp = warmWhite;
warmWhite = coldWhite;
coldWhite = temp;
}
}
ok = true;
}
}
}
if( ok ) {
state.red = red;
state.green = green;
state.blue = blue;
if( whiteComp ) {
state.white = white;
updateColour( red, green, blue, white );
} else if( wwcwComps ) {
state.warmWhite = warmWhite;
state.coldWhite = coldWhite;
updateColour( red, green, blue, 0, warmWhite, coldWhite );
} else if( whiteSep ) {
updateColour( red, green, blue, state.white );
} else {
updateColour( red, green, blue, 0 );
}
}
} );
}
if( whiteSep ) {
mqttSubscribe( config.topics.getWhite, 'white', function( topic, message ) {
state.white = parseInt( message );
updateColour( state.red, state.green, state.blue, state.white );
} );
}
if( supportAdaptiveLighting() ) {
characteristic_ColorTemperature_Internal( service );
}
}
function characteristics_WhiteLight( service ) {
state.white = 0;
var hexPrefix = null;
if( config.hexPrefix ) {
hexPrefix = config.hexPrefix;
} else if( config.hex ) {
hexPrefix = '';
}
function publish() {
var bri = state.bri;
if( !state.on ) {
bri = 0;
}
var white = Math.min( Math.ceil( bri * 2.55 ), 255 );
var msg;
if( hexPrefix == null ) {
msg = white;
} else {
msg = hexPrefix + toHex( white );
}
mqttPublish( config.topics.setWhite, 'white', msg );
}
addCharacteristic( service, 'on', Characteristic.On, false, function() {
if( state.on && state.bri == 0 ) {
state.bri = 100;
}
publish();
} );
addCharacteristic( service, 'bri', Characteristic.Brightness, 100, function() {
if( state.bri > 0 && !state.on ) {
state.on = true;
}
publish();
} );
if( config.topics.getWhite ) {
mqttSubscribe( config.topics.getWhite, 'white', function( topic, message ) {
var ok = false;
var white;
if( hexPrefix == null ) {
var comps = ( '' + message ).split( ',' );
if( comps.length == 1 ) {