2025-07-21 22:28:31 +02:00
import { connectHub , connectHA } from "./client" ;
2025-07-22 23:21:34 +02:00
import { log } from "./logger" ;
import { FimpResponse , sendFimpMsg , setFimp } from "./fimp/fimp" ;
2025-07-23 15:34:22 +02:00
import { haCommandHandlers , setHa , setHaCommandHandlers } from "./ha/globals" ;
import { CommandHandlers , haPublishDevice } from "./ha/publish_device" ;
2025-07-23 14:01:59 +02:00
import { haUpdateState , haUpdateStateSensorReport } from "./ha/update_state" ;
2025-07-22 23:21:34 +02:00
import { VinculumPd7Device } from "./fimp/vinculum_pd7_device" ;
import { haUpdateAvailability } from "./ha/update_availability" ;
2025-07-21 22:28:31 +02:00
( async ( ) = > {
2025-07-23 20:38:17 +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-22 23:21:34 +02:00
log . info ( "Connecting to HA broker..." ) ;
const { ha , retainedMessages } = await connectHA ( { mqttHost , mqttPort , mqttUsername , mqttPassword , } ) ;
setHa ( ha ) ;
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 ) ) {
log . info ( "Empty username or password in non-demo mode. Removing all Futurehome devices from Home Assistant..." ) ;
retainedMessages . forEach ( ( retainedMessage ) = > {
ha ? . publish ( retainedMessage . topic , '' , { retain : true , qos : 2 } ) ;
} ) ;
return ;
}
2025-07-21 22:28:31 +02:00
// 2) Connect to Futurehome hub (FIMP traffic)
2025-07-22 23:21:34 +02:00
log . info ( "Connecting to Futurehome hub..." ) ;
2025-07-23 20:25:01 +02:00
const fimp = await connectHub ( { hubIp , username : hubUsername , password : hubPassword , demo : demoMode } ) ;
2025-07-22 23:21:34 +02:00
fimp . subscribe ( "#" ) ;
setFimp ( fimp ) ;
log . info ( "Connected to Futurehome hub" ) ;
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' ,
val : { cmd : "get" , component : null , param : { components : [ 'house' ] } } ,
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' ,
val : { cmd : "get" , component : null , param : { components : [ 'device' ] } } ,
val_t : 'object' ,
2025-07-23 20:25:01 +02:00
timeoutMs : 30000 ,
2025-07-22 23:21:34 +02:00
} ) ;
const haConfig = retainedMessages . filter ( msg = > msg . topic . endsWith ( "/config" ) ) ;
const regex = new RegExp ( ` ^homeassistant/device/futurehome_ ${ hubId } _([a-zA-Z0-9]+)/config $ ` ) ;
for ( const haDevice of haConfig ) {
log . debug ( 'Found existing HA device' , haDevice . topic )
const match = haDevice . topic . match ( regex ) ;
if ( match ) {
const deviceId = match [ 1 ] ;
const idNumber = Number ( deviceId ) ;
if ( ! isNaN ( idNumber ) ) {
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 ; ;
if ( ! basicDeviceData || ! firstServiceAddr ) {
log . debug ( 'Device was removed, removing from HA.' ) ;
ha ? . publish ( haDevice . topic , '' , { retain : true , qos : 2 } ) ;
}
} else if ( deviceId . toLowerCase ( ) === "hub" ) {
// Hub admin tools, ignore
} else {
log . debug ( 'Invalid format, removing.' ) ;
ha ? . publish ( haDevice . topic , '' , { retain : true , qos : 2 } ) ;
}
} else {
log . debug ( 'Invalid format, removing.' ) ;
ha ? . publish ( haDevice . topic , '' , { retain : true , qos : 2 } ) ;
}
}
2025-07-23 15:34:22 +02:00
const commandHandlers : CommandHandlers = { } ;
2025-07-22 23:21:34 +02:00
for ( const device of devices . val . param . device ) {
2025-07-23 20:24:14 +02:00
try {
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-22 23:21:34 +02:00
2025-07-23 20:24:14 +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-22 23:21:34 +02:00
2025-07-23 20:24:14 +02:00
const result = haPublishDevice ( { hubId , vinculumDeviceData , deviceInclusionReport } ) ;
2025-07-22 23:21:34 +02:00
2025-07-23 20:24:14 +02:00
Object . assign ( commandHandlers , result . commandHandlers ) ;
2025-07-22 23:21:34 +02:00
2025-07-23 20:24:14 +02:00
if ( ! retainedMessages . some ( msg = > msg . topic === ` homeassistant/device/futurehome_ ${ hubId } _ ${ deviceId } /availability ` ) ) {
// Set initial availability
haUpdateAvailability ( { hubId , deviceAvailability : { address : deviceId , status : 'UP' } } ) ;
}
} catch ( e ) {
log . error ( 'Failed publishing device' , device , e ) ;
2025-07-22 23:21:34 +02:00
}
}
2025-07-23 15:34:22 +02:00
setHaCommandHandlers ( commandHandlers ) ;
2025-07-22 23:21:34 +02:00
// todo
// exposeSmarthubTools();
2025-07-22 00:51:14 +02:00
2025-07-21 22:28:31 +02:00
fimp . on ( "message" , ( topic , buf ) = > {
try {
2025-07-22 23:21:34 +02:00
const msg : FimpResponse = JSON . parse ( buf . toString ( ) ) ;
2025-07-23 14:01:59 +02:00
log . debug ( ` Received FIMP message on topic " ${ topic } ": \ n ${ JSON . stringify ( msg , null , 0 ) } ` ) ;
switch ( msg . type ) {
case 'evt.pd7.response' : {
const devicesState = msg . val ? . param ? . state ? . devices ;
if ( ! devicesState ) { return ; }
for ( const deviceState of devicesState ) {
haUpdateState ( { hubId , deviceState } ) ;
}
break ;
2025-07-22 23:21:34 +02:00
}
2025-07-23 15:34:22 +02:00
case 'evt.sensor.report' :
case 'evt.presence.report' :
case 'evt.open.report' :
case 'evt.lvl.report' :
case 'evt.alarm.report' :
case 'evt.binary.report' :
{
haUpdateStateSensorReport ( { topic , value : msg.val , attrName : msg.type.split ( '.' ) [ 1 ] } )
break ;
}
2025-07-23 14:01:59 +02:00
case 'evt.network.all_nodes_report' : {
const devicesAvailability = msg . val ;
if ( ! devicesAvailability ) { return ; }
for ( const deviceAvailability of devicesAvailability ) {
haUpdateAvailability ( { hubId , deviceAvailability } ) ;
}
break ;
2025-07-22 23:21:34 +02:00
}
2025-07-21 22:28:31 +02:00
}
} catch ( e ) {
2025-07-22 23:21:34 +02:00
log . warn ( "Bad FIMP JSON" , e , topic , buf ) ;
2025-07-21 22:28:31 +02:00
}
} ) ;
2025-07-22 23:21:34 +02:00
// Request initial state
await sendFimpMsg ( {
address : '/rt:app/rn:vinculum/ad:1' ,
service : 'vinculum' ,
cmd : 'cmd.pd7.request' ,
val : { cmd : "get" , component : null , param : { components : [ 'state' ] } } ,
val_t : 'object' ,
2025-07-23 20:25:01 +02:00
timeoutMs : 30000 ,
2025-07-22 23:21:34 +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 ) {
log . debug ( ` Handling Home Assistant command topic: ${ topic } , payload: ${ buf . toString ( ) } ` ) ;
handler ( buf . toString ( ) ) . catch ( ( e ) = > {
log . warn ( ` Failed executing handler for topic: ${ topic } , payload: ${ buf . toString ( ) } ` , e ) ;
} ) ;
}
} )
2025-07-21 22:28:31 +02:00
} ) ( ) ;