2025-07-24 16:34:58 +02:00
import { connectHub , connectHA , RetainedMessage } from './client' ;
import { log } from './logger' ;
import { FimpResponse , sendFimpMsg , setFimp } from './fimp/fimp' ;
import { haCommandHandlers , setHa , setHaCommandHandlers } from './ha/globals' ;
import { CommandHandlers , haPublishDevice } from './ha/publish_device' ;
import { haUpdateState , haUpdateStateSensorReport } from './ha/update_state' ;
import { VinculumPd7Device } from './fimp/vinculum_pd7_device' ;
import { haUpdateAvailability } from './ha/update_availability' ;
import { delay } from './utils' ;
2025-07-21 22:28:31 +02:00
( async ( ) = > {
2025-07-24 16:34:58 +02:00
const hubIp = process . env . FH_HUB_IP || 'futurehome-smarthub.local' ;
2025-07-23 20:25:01 +02:00
const hubUsername = process . env . FH_USERNAME || '' ;
const hubPassword = process . env . FH_PASSWORD || '' ;
const demoMode = ( process . env . DEMO_MODE || '' ) . toLowerCase ( ) . includes ( 'true' ) ;
2025-07-21 22:28:31 +02:00
2025-07-23 20:25:01 +02:00
const mqttHost = process . env . MQTT_HOST || '' ;
const mqttPort = Number ( process . env . MQTT_PORT || '1883' ) ;
const mqttUsername = process . env . MQTT_USER || '' ;
const mqttPassword = process . env . MQTT_PWD || '' ;
2025-07-21 23:04:44 +02:00
2025-07-23 20:25:01 +02:00
// 1) Connect to HA broker (for discovery + state + availability + commands)
2025-07-24 16:34:58 +02:00
log . info ( 'Connecting to HA broker...' ) ;
const { ha , retainedMessages } = await connectHA ( {
mqttHost ,
mqttPort ,
mqttUsername ,
mqttPassword ,
} ) ;
2025-07-22 23:21:34 +02:00
setHa ( ha ) ;
2025-07-24 16:34:58 +02:00
log . info ( 'Connected to HA broker' ) ;
2025-07-21 22:28:31 +02:00
2025-07-23 20:25:01 +02:00
if ( ! demoMode && ( ! hubUsername || ! hubPassword ) ) {
2025-07-24 16:34:58 +02:00
log . info (
'Empty username or password in non-demo mode. Removing all Futurehome devices from Home Assistant...' ,
) ;
2025-07-24 02:46:20 +02:00
2025-07-23 22:31:26 +02:00
const publishWithDelay = ( messages : RetainedMessage [ ] , index = 0 ) = > {
if ( index >= messages . length ) return ;
const msg = messages [ index ] ;
ha ? . publish ( msg . topic , '' , { retain : true , qos : 2 } ) ;
2025-07-23 23:19:20 +02:00
setTimeout ( ( ) = > publishWithDelay ( messages , index + 1 ) , 50 ) ; // 50 milliseconds between each publish
2025-07-23 22:31:26 +02:00
} ;
publishWithDelay ( retainedMessages ) ;
2025-07-23 20:25:01 +02:00
return ;
}
2025-07-21 22:28:31 +02:00
// 2) Connect to Futurehome hub (FIMP traffic)
2025-07-24 16:34:58 +02:00
log . info ( 'Connecting to Futurehome hub...' ) ;
const fimp = await connectHub ( {
hubIp ,
username : hubUsername ,
password : hubPassword ,
demo : demoMode ,
} ) ;
fimp . subscribe ( '#' ) ;
2025-07-22 23:21:34 +02:00
setFimp ( fimp ) ;
2025-07-24 16:34:58 +02:00
log . info ( 'Connected to Futurehome hub' ) ;
2025-07-22 23:21:34 +02:00
2025-07-23 20:38:17 +02:00
const house = await sendFimpMsg ( {
2025-07-22 23:21:34 +02:00
address : '/rt:app/rn:vinculum/ad:1' ,
service : 'vinculum' ,
cmd : 'cmd.pd7.request' ,
2025-07-24 16:34:58 +02:00
val : { cmd : 'get' , component : null , param : { components : [ 'house' ] } } ,
2025-07-22 23:21:34 +02:00
val_t : 'object' ,
2025-07-23 20:25:01 +02:00
timeoutMs : 30000 ,
2025-07-22 23:21:34 +02:00
} ) ;
2025-07-23 20:38:17 +02:00
const hubId = house . val . param . house . hubId ;
2025-07-21 22:28:31 +02:00
2025-07-23 20:38:17 +02:00
const devices = await sendFimpMsg ( {
2025-07-22 23:21:34 +02:00
address : '/rt:app/rn:vinculum/ad:1' ,
service : 'vinculum' ,
cmd : 'cmd.pd7.request' ,
2025-07-24 16:34:58 +02:00
val : { cmd : 'get' , component : null , param : { components : [ 'device' ] } } ,
2025-07-22 23:21:34 +02:00
val_t : 'object' ,
2025-07-23 20:25:01 +02:00
timeoutMs : 30000 ,
2025-07-22 23:21:34 +02:00
} ) ;
2025-07-24 16:34:58 +02:00
const haConfig = retainedMessages . filter ( ( msg ) = >
msg . topic . endsWith ( '/config' ) ,
) ;
2025-07-22 23:21:34 +02:00
2025-07-24 16:34:58 +02:00
const regex = new RegExp (
` ^homeassistant/device/futurehome_ ${ hubId } _([a-zA-Z0-9]+)/config $ ` ,
) ;
2025-07-22 23:21:34 +02:00
for ( const haDevice of haConfig ) {
2025-07-24 16:34:58 +02:00
log . debug ( 'Found existing HA device' , haDevice . topic ) ;
2025-07-22 23:21:34 +02:00
const match = haDevice . topic . match ( regex ) ;
if ( match ) {
const deviceId = match [ 1 ] ;
const idNumber = Number ( deviceId ) ;
if ( ! isNaN ( idNumber ) ) {
2025-07-24 16:34:58 +02:00
const basicDeviceData : { services ? : { [ key : string ] : any } } =
devices . val . param . device . find ( ( d : any ) = > d ? . id === idNumber ) ;
const firstServiceAddr = basicDeviceData ? . services
? Object . values ( basicDeviceData . services ) [ 0 ] ? . addr
: undefined ;
2025-07-22 23:21:34 +02:00
if ( ! basicDeviceData || ! firstServiceAddr ) {
log . debug ( 'Device was removed, removing from HA.' ) ;
ha ? . publish ( haDevice . topic , '' , { retain : true , qos : 2 } ) ;
2025-07-23 22:31:26 +02:00
await delay ( 50 ) ;
2025-07-22 23:21:34 +02:00
}
2025-07-24 16:34:58 +02:00
} else if ( deviceId . toLowerCase ( ) === 'hub' ) {
2025-07-22 23:21:34 +02:00
// Hub admin tools, ignore
} else {
log . debug ( 'Invalid format, removing.' ) ;
ha ? . publish ( haDevice . topic , '' , { retain : true , qos : 2 } ) ;
2025-07-23 22:31:26 +02:00
await delay ( 50 ) ;
2025-07-22 23:21:34 +02:00
}
} else {
log . debug ( 'Invalid format, removing.' ) ;
ha ? . publish ( haDevice . topic , '' , { retain : true , qos : 2 } ) ;
2025-07-23 22:31:26 +02:00
await delay ( 50 ) ;
2025-07-22 23:21:34 +02:00
}
}
2025-07-24 00:33:49 +02:00
const vinculumDevicesToHa = async ( devices : FimpResponse ) = > {
const commandHandlers : CommandHandlers = { } ;
for ( const device of devices . val . param . device ) {
try {
2025-07-24 16:34:58 +02:00
const vinculumDeviceData : VinculumPd7Device = device ;
const deviceId = vinculumDeviceData . id . toString ( ) ;
const firstServiceAddr = vinculumDeviceData . services
? Object . values ( vinculumDeviceData . services ) [ 0 ] ? . addr
: undefined ;
if ( ! firstServiceAddr ) {
continue ;
}
2025-07-24 00:33:49 +02:00
// This is problematic when the adapter doesn't respond, so we are not getting the inclusion report for now. I'm leaving it here since we might want it in the future.
// // Get additional metadata like manufacutrer or sw/hw version directly from the adapter
// const adapterAddress = adapterAddressFromServiceAddress(firstServiceAddr)
// const adapterService = adapterServiceFromServiceAddress(firstServiceAddr)
// const deviceInclusionReport = await getInclusionReport({ adapterAddress, adapterService, deviceId });
const deviceInclusionReport = undefined ;
2025-07-24 16:34:58 +02:00
const result = haPublishDevice ( {
hubId ,
vinculumDeviceData ,
deviceInclusionReport ,
} ) ;
2025-07-24 00:33:49 +02:00
await delay ( 50 ) ;
2025-07-22 23:21:34 +02:00
2025-07-24 00:33:49 +02:00
Object . assign ( commandHandlers , result . commandHandlers ) ;
2025-07-22 23:21:34 +02:00
2025-07-24 16:34:58 +02:00
if (
! retainedMessages . some (
( msg ) = >
msg . topic ===
` homeassistant/device/futurehome_ ${ hubId } _ ${ deviceId } /availability ` ,
)
) {
2025-07-24 00:33:49 +02:00
// Set initial availability
2025-07-24 16:34:58 +02:00
haUpdateAvailability ( {
hubId ,
deviceAvailability : { address : deviceId , status : 'UP' } ,
} ) ;
2025-07-24 00:33:49 +02:00
await delay ( 50 ) ;
}
} catch ( e ) {
log . error ( 'Failed publishing device' , device , e ) ;
2025-07-23 20:24:14 +02:00
}
2025-07-22 23:21:34 +02:00
}
2025-07-24 00:33:49 +02:00
setHaCommandHandlers ( commandHandlers ) ;
} ;
vinculumDevicesToHa ( devices ) ;
let knownDeviceIds = new Set ( devices . val . param . device . map ( ( d : any ) = > d ? . id ) ) ;
2025-07-22 23:21:34 +02:00
// todo
// exposeSmarthubTools();
2025-07-22 00:51:14 +02:00
2025-07-24 16:34:58 +02:00
fimp . on ( 'message' , async ( topic , buf ) = > {
2025-07-21 22:28:31 +02:00
try {
2025-07-22 23:21:34 +02:00
const msg : FimpResponse = JSON . parse ( buf . toString ( ) ) ;
2025-07-24 16:34:58 +02:00
log . debug (
` Received FIMP message on topic " ${ topic } ": \ n ${ JSON . stringify ( msg , null , 0 ) } ` ,
) ;
2025-07-23 14:01:59 +02:00
switch ( msg . type ) {
case 'evt.pd7.response' : {
2025-07-24 00:33:49 +02:00
// Handle vinculum 'state'
2025-07-23 14:01:59 +02:00
const devicesState = msg . val ? . param ? . state ? . devices ;
2025-07-24 00:33:49 +02:00
if ( devicesState ) {
for ( const deviceState of devicesState ) {
haUpdateState ( { hubId , deviceState } ) ;
await delay ( 50 ) ;
}
}
// Handle vinculum 'device's
const devices = msg . val . param . device ;
if ( devices ) {
const newDeviceIds = new Set ( devices . map ( ( d : any ) = > d ? . id ) ) ;
2025-07-24 16:34:58 +02:00
const addedDeviceIds = [ . . . newDeviceIds ] . filter (
( id ) = > ! knownDeviceIds . has ( id ) ,
) ;
const removedDeviceIds = [ . . . knownDeviceIds ] . filter (
( id ) = > ! newDeviceIds . has ( id ) ,
) ;
2025-07-24 00:33:49 +02:00
log . info ( ` Added devices: ${ addedDeviceIds } ` ) ;
log . info ( ` Removed devices: ${ removedDeviceIds } ` ) ;
for ( const id of removedDeviceIds ) {
const topic = ` homeassistant/device/futurehome_ ${ hubId } _ ${ id } /config ` ;
ha ? . publish ( topic , '' , { retain : true , qos : 2 } ) ;
await delay ( 50 ) ;
const availTopic = ` homeassistant/device/futurehome_ ${ hubId } _ ${ id } /availability ` ;
ha ? . publish ( availTopic , '' , { retain : true , qos : 2 } ) ;
await delay ( 50 ) ;
}
knownDeviceIds = newDeviceIds ;
vinculumDevicesToHa ( msg ) ;
2025-07-23 14:01:59 +02:00
}
break ;
2025-07-22 23:21:34 +02:00
}
2025-07-24 16:48:33 +02:00
2025-07-23 15:34:22 +02:00
case 'evt.alarm.report' :
2025-07-24 16:48:33 +02:00
case 'evt.binary.report' :
case 'evt.color.report' :
case 'evt.lvl.report' :
case 'evt.mode.report' :
case 'evt.open.report' :
case 'evt.presence.report' :
case 'evt.scene.report' :
case 'evt.sensor.report' :
case 'evt.setpoint.report' : {
2025-07-24 16:34:58 +02:00
haUpdateStateSensorReport ( {
topic ,
value : msg.val ,
attrName : msg.type.split ( '.' ) [ 1 ] ,
} ) ;
break ;
}
2025-07-24 16:48:33 +02:00
2025-07-23 14:01:59 +02:00
case 'evt.network.all_nodes_report' : {
const devicesAvailability = msg . val ;
2025-07-24 16:34:58 +02:00
if ( ! devicesAvailability ) {
return ;
}
2025-07-23 14:01:59 +02:00
for ( const deviceAvailability of devicesAvailability ) {
haUpdateAvailability ( { hubId , deviceAvailability } ) ;
2025-07-23 22:31:26 +02:00
await delay ( 50 ) ;
2025-07-23 14:01:59 +02:00
}
break ;
2025-07-22 23:21:34 +02:00
}
2025-07-21 22:28:31 +02:00
}
} catch ( e ) {
2025-07-24 16:34:58 +02:00
log . warn ( 'Bad FIMP JSON' , e , topic , buf ) ;
2025-07-21 22:28:31 +02:00
}
} ) ;
2025-07-23 23:23:29 +02:00
const pollState = ( ) = > {
2025-07-24 16:34:58 +02:00
log . debug ( 'Refreshing Vinculum state after 30 seconds...' ) ;
2025-07-24 00:33:49 +02:00
2025-07-23 23:23:29 +02:00
sendFimpMsg ( {
address : '/rt:app/rn:vinculum/ad:1' ,
service : 'vinculum' ,
cmd : 'cmd.pd7.request' ,
2025-07-24 16:34:58 +02:00
val : { cmd : 'get' , component : null , param : { components : [ 'state' ] } } ,
2025-07-23 23:23:29 +02:00
val_t : 'object' ,
timeoutMs : 30000 ,
2025-07-24 16:34:58 +02:00
} ) . catch ( ( e ) = > log . warn ( 'Failed to request state' , e ) ) ;
2025-07-23 23:23:29 +02:00
} ;
2025-07-22 23:21:34 +02:00
// Request initial state
2025-07-23 23:23:29 +02:00
pollState ( ) ;
// Then poll every 30 seconds
2025-07-24 16:34:58 +02:00
if ( ! demoMode ) {
setInterval ( pollState , 30 * 1000 ) ;
}
2025-07-23 15:34:22 +02:00
2025-07-24 00:33:49 +02:00
const pollDevices = ( ) = > {
2025-07-24 16:34:58 +02:00
log . debug ( 'Refreshing Vinculum devices after 30 minutes...' ) ;
2025-07-24 00:33:49 +02:00
sendFimpMsg ( {
address : '/rt:app/rn:vinculum/ad:1' ,
service : 'vinculum' ,
cmd : 'cmd.pd7.request' ,
2025-07-24 16:34:58 +02:00
val : { cmd : 'get' , component : null , param : { components : [ 'device' ] } } ,
2025-07-24 00:33:49 +02:00
val_t : 'object' ,
timeoutMs : 30000 ,
2025-07-24 16:34:58 +02:00
} ) . catch ( ( e ) = > log . warn ( 'Failed to request state' , e ) ) ;
2025-07-24 00:33:49 +02:00
} ;
// Poll devices every 30 minutes (1800000 ms)
2025-07-24 16:34:58 +02:00
if ( ! demoMode ) {
setInterval ( pollDevices , 30 * 60 * 1000 ) ;
}
2025-07-24 00:33:49 +02:00
2025-07-23 15:34:22 +02:00
ha . on ( 'message' , ( topic , buf ) = > {
// Handle Home Assistant command messages
const handler = haCommandHandlers ? . [ topic ] ;
if ( handler ) {
2025-07-24 16:34:58 +02:00
log . debug (
` Handling Home Assistant command topic: ${ topic } , payload: ${ buf . toString ( ) } ` ,
) ;
2025-07-23 15:34:22 +02:00
handler ( buf . toString ( ) ) . catch ( ( e ) = > {
2025-07-24 16:34:58 +02:00
log . warn (
` Failed executing handler for topic: ${ topic } , payload: ${ buf . toString ( ) } ` ,
e ,
) ;
2025-07-23 15:34:22 +02:00
} ) ;
}
2025-07-24 16:34:58 +02:00
} ) ;
2025-07-21 22:28:31 +02:00
} ) ( ) ;