UNPKG

@irusland/homebridge-mqttthing

Version:

Homebridge plugin supporting various services over MQTT (with TLS fixes)

1,142 lines (998 loc) 186 kB
// 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 ) {