2019-12-04 11:17:06 +01:00
const EventEmitter = require ( 'events' ) ;
const mqtt = require ( 'mqtt' ) ;
2021-02-01 21:19:22 +01:00
const Configuration = require ( './Configuration' ) ;
2021-01-21 21:31:37 +01:00
const Logger = require ( './Logger' ) ;
2019-12-04 11:17:06 +01:00
2021-01-28 14:24:04 +01:00
const startTopics = [ 'hass/status' , 'homeassistant/status' ] ;
2019-12-04 11:17:06 +01:00
2021-01-22 15:49:02 +01:00
const logger = Logger . getLogger ( 'plejd-mqtt' ) ;
2019-12-04 11:17:06 +01:00
// #region discovery
const discoveryPrefix = 'homeassistant' ;
const nodeId = 'plejd' ;
const getSubscribePath = ( ) => ` ${ discoveryPrefix } /+/ ${ nodeId } /# ` ;
2021-01-22 15:49:02 +01:00
const getPath = ( { id , type } ) => ` ${ discoveryPrefix } / ${ type } / ${ nodeId } / ${ id } ` ;
const getConfigPath = ( plug ) => ` ${ getPath ( plug ) } /config ` ;
const getStateTopic = ( plug ) => ` ${ getPath ( plug ) } /state ` ;
2021-01-30 10:00:09 +01:00
const getAvailabilityTopic = ( plug ) => ` ${ getPath ( plug ) } /availability ` ;
2021-01-22 15:49:02 +01:00
const getCommandTopic = ( plug ) => ` ${ getPath ( plug ) } /set ` ;
const getSceneEventTopic = ( ) => 'plejd/event/scene' ;
2021-02-01 21:19:22 +01:00
const decodeTopicRegexp = new RegExp (
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/ ,
) ;
const decodeTopic = ( topic ) => {
const matches = decodeTopicRegexp . exec ( topic ) ;
if ( ! matches ) {
return null ;
}
return matches . groups ;
} ;
2021-01-22 15:49:02 +01:00
const getDiscoveryPayload = ( device ) => ( {
2019-12-21 15:01:15 +00:00
schema : 'json' ,
2019-12-10 22:01:12 +01:00
name : device . name ,
2020-01-27 20:43:52 +00:00
unique _id : ` light.plejd. ${ device . name . toLowerCase ( ) . replace ( / /g , '' ) } ` ,
2019-12-10 22:01:12 +01:00
state _topic : getStateTopic ( device ) ,
command _topic : getCommandTopic ( device ) ,
2021-01-29 21:25:34 +01:00
availability _topic : getAvailabilityTopic ( device ) ,
2019-12-21 15:01:15 +00:00
optimistic : false ,
2020-01-21 14:24:02 +00:00
brightness : ` ${ device . dimmable } ` ,
device : {
2021-01-22 15:49:02 +01:00
identifiers : ` ${ device . serialNumber } _ ${ device . id } ` ,
2020-01-21 14:24:02 +00:00
manufacturer : 'Plejd' ,
model : device . typeName ,
name : device . name ,
2021-01-22 15:49:02 +01:00
sw _version : device . version ,
} ,
2019-12-10 22:01:12 +01:00
} ) ;
2021-01-22 15:49:02 +01:00
const getSwitchPayload = ( device ) => ( {
2020-02-20 13:02:47 +01:00
name : device . name ,
state _topic : getStateTopic ( device ) ,
command _topic : getCommandTopic ( device ) ,
optimistic : false ,
device : {
2021-01-22 15:49:02 +01:00
identifiers : ` ${ device . serialNumber } _ ${ device . id } ` ,
2020-02-20 13:02:47 +01:00
manufacturer : 'Plejd' ,
model : device . typeName ,
name : device . name ,
2021-01-22 15:49:02 +01:00
sw _version : device . version ,
} ,
2020-02-20 13:02:47 +01:00
} ) ;
2019-12-04 11:17:06 +01:00
// #endregion
class MqttClient extends EventEmitter {
2021-02-01 21:19:22 +01:00
deviceRegistry ;
constructor ( deviceRegistry ) {
2019-12-04 11:17:06 +01:00
super ( ) ;
2021-02-01 21:19:22 +01:00
this . config = Configuration . getOptions ( ) ;
this . deviceRegistry = deviceRegistry ;
2019-12-04 11:17:06 +01:00
}
init ( ) {
2021-01-22 15:49:02 +01:00
logger . info ( 'Initializing MQTT connection for Plejd addon' ) ;
2019-12-04 11:17:06 +01:00
2021-02-01 21:19:22 +01:00
this . client = mqtt . connect ( this . config . mqttBroker , {
username : this . config . mqttUsername ,
password : this . config . mqttPassword ,
2019-12-04 11:17:06 +01:00
} ) ;
2021-02-10 10:10:28 +01:00
this . client . on ( 'error' , ( err ) => {
logger . warn ( 'Error emitted from mqtt client' , err ) ;
} ) ;
2019-12-04 11:17:06 +01:00
this . client . on ( 'connect' , ( ) => {
2021-01-21 21:31:37 +01:00
logger . info ( 'Connected to MQTT.' ) ;
2019-12-04 11:17:06 +01:00
2021-01-28 14:24:04 +01:00
this . client . subscribe ( startTopics , ( err ) => {
2019-12-04 11:17:06 +01:00
if ( err ) {
2021-01-30 10:00:09 +01:00
logger . error ( 'Unable to subscribe to status topics' ) ;
2019-12-04 11:17:06 +01:00
}
2021-02-01 21:19:22 +01:00
this . emit ( 'connected' ) ;
2019-12-04 11:17:06 +01:00
} ) ;
this . client . subscribe ( getSubscribePath ( ) , ( err ) => {
if ( err ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'Unable to subscribe to control topics' ) ;
2019-12-04 11:17:06 +01:00
}
} ) ;
} ) ;
this . client . on ( 'close' , ( ) => {
2021-01-21 21:31:37 +01:00
logger . verbose ( 'Warning: mqtt channel closed event, reconnecting...' ) ;
2021-02-01 21:19:22 +01:00
this . reconnect ( ) ;
2019-12-04 11:17:06 +01:00
} ) ;
this . client . on ( 'message' , ( topic , message ) => {
2021-01-29 20:41:55 +01:00
if ( startTopics . includes ( topic ) ) {
2021-01-21 21:31:37 +01:00
logger . info ( 'Home Assistant has started. lets do discovery.' ) ;
2021-02-01 21:19:22 +01:00
this . emit ( 'connected' ) ;
} else {
const decodedTopic = decodeTopic ( topic ) ;
if ( decodedTopic ) {
2021-02-10 10:10:28 +01:00
let device = this . deviceRegistry . getDevice ( decodedTopic . id ) ;
const messageString = message . toString ( ) ;
const isJsonMessage = messageString . startsWith ( '{' ) ;
const command = isJsonMessage
? JSON . parse ( messageString )
: messageString ;
if ( ! isJsonMessage && messageString === 'ON' && this . deviceRegistry . getScene ( decodedTopic . id ) ) {
// Guess that id that got state command without dim value belongs to Scene, not Device
// This guess could very well be wrong depending on the installation...
logger . warn ( ` Device id ${ decodedTopic . id } belongs to both scene and device, guessing Scene is what should be set to ON. OFF commands still sent to device. ` ) ;
device = this . deviceRegistry . getScene ( decodedTopic . id ) ;
}
2021-02-01 21:19:22 +01:00
const deviceName = device ? device . name : '' ;
switch ( decodedTopic . command ) {
case 'set' :
logger . verbose (
2021-02-10 10:10:28 +01:00
` Got mqtt SET command for ${ decodedTopic . type } , ${ deviceName } ( ${ decodedTopic . id } ): ${ messageString } ` ,
2021-02-01 21:19:22 +01:00
) ;
if ( device ) {
this . emit ( 'stateChanged' , device , command ) ;
} else {
logger . warn (
` Device for topic ${ topic } not found! Can happen if HA calls previously existing devices. ` ,
) ;
}
break ;
case 'state' :
case 'config' :
case 'availability' :
logger . verbose (
2021-02-01 21:36:40 +01:00
` Sent mqtt ${ decodedTopic . command } command for ${
decodedTopic . type
} , $ { deviceName } ( $ { decodedTopic . id } ) . $ {
2021-02-10 10:10:28 +01:00
decodedTopic . command === 'availability' ? messageString : ''
2021-02-01 21:36:40 +01:00
} ` ,
2021-02-01 21:19:22 +01:00
) ;
break ;
default :
logger . verbose ( ` Warning: Unknown command ${ decodedTopic . command } in decoded topic ` ) ;
}
2021-01-25 08:06:28 +01:00
} else {
2021-02-10 10:10:28 +01:00
logger . verbose ( ` Warning: Got unrecognized mqtt command on ' ${ topic } ': ${ message . toString ( ) } ` ) ;
2021-01-25 08:06:28 +01:00
}
2021-01-21 21:31:37 +01:00
}
2019-12-04 11:17:06 +01:00
} ) ;
}
reconnect ( ) {
this . client . reconnect ( ) ;
}
2021-01-29 21:25:34 +01:00
disconnect ( callback ) {
2021-02-01 21:19:22 +01:00
this . deviceRegistry . allDevices . forEach ( ( device ) => {
2021-01-30 10:00:09 +01:00
this . client . publish ( getAvailabilityTopic ( device ) , 'offline' ) ;
2021-01-29 21:25:34 +01:00
} ) ;
this . client . end ( callback ) ;
}
2021-02-01 21:19:22 +01:00
sendDiscoveryToHomeAssistant ( ) {
logger . debug ( ` Sending discovery of ${ this . deviceRegistry . allDevices . length } device(s). ` ) ;
2019-12-04 11:17:06 +01:00
2021-02-01 21:19:22 +01:00
this . deviceRegistry . allDevices . forEach ( ( device ) => {
2021-01-21 21:31:37 +01:00
logger . debug ( ` Sending discovery for ${ device . name } ` ) ;
2019-12-04 11:17:06 +01:00
2021-02-01 21:36:40 +01:00
const payload = device . type === 'switch' ? getSwitchPayload ( device ) : getDiscoveryPayload ( device ) ;
2021-01-22 15:49:02 +01:00
logger . info (
` Discovered ${ device . type } ( ${ device . typeName } ) named ${ device . name } with PID ${ device . id } . ` ,
) ;
2019-12-13 14:13:00 +01:00
2021-02-01 21:19:22 +01:00
this . client . publish ( getConfigPath ( device ) , JSON . stringify ( payload ) ) ;
2021-01-29 21:25:34 +01:00
setTimeout ( ( ) => {
2021-02-01 21:19:22 +01:00
this . client . publish ( getAvailabilityTopic ( device ) , 'online' ) ;
2021-01-29 21:25:34 +01:00
} , 2000 ) ;
2019-12-04 11:17:06 +01:00
} ) ;
}
2019-12-21 15:01:15 +00:00
updateState ( deviceId , data ) {
2021-02-01 21:19:22 +01:00
const device = this . deviceRegistry . getDevice ( deviceId ) ;
2019-12-04 11:17:06 +01:00
if ( ! device ) {
2021-01-21 21:31:37 +01:00
logger . warn ( ` Unknown device id ${ deviceId } - not handled by us. ` ) ;
2019-12-04 11:17:06 +01:00
return ;
}
2021-01-25 08:06:28 +01:00
logger . verbose (
` Updating state for ${ device . name } : ${ data . state } ${
data . brightness ? ` , dim: ${ data . brightness } ` : ''
} ` ,
) ;
2019-12-21 15:01:15 +00:00
let payload = null ;
2019-12-04 11:17:06 +01:00
2020-02-29 15:54:08 +00:00
if ( device . type === 'switch' ) {
payload = data . state === 1 ? 'ON' : 'OFF' ;
2021-01-22 15:49:02 +01:00
} else {
2020-02-29 15:54:08 +00:00
if ( device . dimmable ) {
payload = {
state : data . state === 1 ? 'ON' : 'OFF' ,
2021-01-22 15:49:02 +01:00
brightness : data . brightness ,
} ;
} else {
2020-02-29 15:54:08 +00:00
payload = {
2021-01-22 15:49:02 +01:00
state : data . state === 1 ? 'ON' : 'OFF' ,
} ;
2020-02-29 15:54:08 +00:00
}
payload = JSON . stringify ( payload ) ;
2019-12-04 11:17:06 +01:00
}
2021-01-22 15:49:02 +01:00
this . client . publish ( getStateTopic ( device ) , payload ) ;
2021-01-30 10:00:09 +01:00
this . client . publish ( getAvailabilityTopic ( device ) , 'online' ) ;
2019-12-04 11:17:06 +01:00
}
2019-12-22 17:48:16 +00:00
2021-02-01 21:19:22 +01:00
sceneTriggered ( sceneId ) {
logger . verbose ( ` Scene triggered: ${ sceneId } ` ) ;
this . client . publish ( getSceneEventTopic ( ) , JSON . stringify ( { scene : sceneId } ) ) ;
2019-12-22 17:48:16 +00:00
}
2019-12-04 11:17:06 +01:00
}
2021-01-22 15:49:02 +01:00
module . exports = MqttClient ;