@@ -325,6 +325,39 @@ const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
325325 return feature . property ;
326326} ;
327327
328+ const isSensitiveCompositeFeature = ( feature : zhc . Feature ) : boolean => {
329+ return / p a s s ( w o r d ) ? | t o k e n | s e c r e t | c r e d e n t i a l | k e y / i. test ( feature . name ) || / p a s s ( w o r d ) ? | t o k e n | s e c r e t | c r e d e n t i a l | k e y / i. test ( feature . property ) ;
330+ } ;
331+
332+ const compositePathValueTemplate = ( path : string [ ] , lower = false ) : string => {
333+ const pathExpression = path . map ( ( part ) => `["${ part } "]` ) . join ( "" ) ;
334+ const checks = path . map (
335+ ( part , index ) =>
336+ `${
337+ index === 0
338+ ? "value_json"
339+ : `value_json${ path
340+ . slice ( 0 , index )
341+ . map ( ( p ) => `["${ p } "]` )
342+ . join ( "" ) } `
343+ } ["${ part } "] is defined`,
344+ ) ;
345+ const value = `value_json${ pathExpression } ` ;
346+ const renderedValue = lower ? `${ value } | string | lower` : value ;
347+
348+ return `{% if ${ checks . join ( " and " ) } %}{{ ${ renderedValue } }}{% endif %}` ;
349+ } ;
350+
351+ const compositePathCommandTemplate = ( path : string [ ] , valueTemplate : string ) : string => {
352+ let result = valueTemplate ;
353+
354+ for ( let i = path . length - 1 ; i >= 0 ; i -- ) {
355+ result = `{"${ path [ i ] } ": ${ result } }` ;
356+ }
357+
358+ return result ;
359+ } ;
360+
328361/**
329362 * This class handles the bridge entity configuration for Home Assistant Discovery.
330363 */
@@ -1292,6 +1325,110 @@ export class HomeAssistant extends Extension {
12921325 break ;
12931326 }
12941327
1328+ if ( firstExposeTyped . type === "composite" ) {
1329+ const firstComposite = firstExposeTyped as zhc . Composite ;
1330+ const addCompositeFeatureDiscovery = ( feature : zhc . Feature , path : string [ ] , labelParts : string [ ] ) : void => {
1331+ if ( isSensitiveCompositeFeature ( feature ) ) {
1332+ return ;
1333+ }
1334+
1335+ if ( feature . type === "composite" ) {
1336+ for ( const child of ( feature as zhc . Composite ) . features ) {
1337+ addCompositeFeatureDiscovery ( child , [ ...path , child . property ] , [ ...labelParts , child . label ] ) ;
1338+ }
1339+ return ;
1340+ }
1341+
1342+ const allowsState = ! ! ( feature . access & ACCESS_STATE ) ;
1343+ const allowsSet = ! ! ( feature . access & ACCESS_SET ) ;
1344+ if ( ! allowsState && ! allowsSet ) {
1345+ return ;
1346+ }
1347+
1348+ const objectId = path . join ( "_" ) ;
1349+ const name = endpointName ? `${ labelParts . join ( " " ) } ${ endpointName } ` : labelParts . join ( " " ) ;
1350+ const discoveryPayload : KeyValue = {
1351+ name,
1352+ state_topic : allowsState ,
1353+ ...( allowsState && {
1354+ value_template : compositePathValueTemplate ( path , isBinaryExpose ( feature ) && typeof feature . value_on === "boolean" ) ,
1355+ } ) ,
1356+ ...( allowsSet && {
1357+ command_topic : true ,
1358+ optimistic : ! allowsState ,
1359+ } ) ,
1360+ } ;
1361+
1362+ if ( isNumericExpose ( feature ) ) {
1363+ if ( allowsSet ) discoveryPayload . command_template = compositePathCommandTemplate ( path , "{{ value }}" ) ;
1364+ if ( feature . unit ) discoveryPayload . unit_of_measurement = feature . unit ;
1365+ if ( feature . value_step ) discoveryPayload . step = feature . value_step ;
1366+ if ( feature . value_min != null ) discoveryPayload . min = feature . value_min ;
1367+ if ( feature . value_max != null ) discoveryPayload . max = feature . value_max ;
1368+ Object . assign ( discoveryPayload , NUMERIC_DISCOVERY_LOOKUP [ feature . name ] ) ;
1369+
1370+ if ( NUMERIC_DISCOVERY_LOOKUP [ feature . name ] ?. device_class === "temperature" ) {
1371+ discoveryPayload . device_class = NUMERIC_DISCOVERY_LOOKUP [ feature . name ] ?. device_class ;
1372+ } else {
1373+ delete discoveryPayload . device_class ;
1374+ }
1375+
1376+ discoveryEntries . push ( {
1377+ type : allowsSet ? "number" : "sensor" ,
1378+ object_id : objectId ,
1379+ mockProperties : [ { property : firstComposite . property , value : null } ] ,
1380+ discovery_payload : discoveryPayload ,
1381+ } ) ;
1382+ } else if ( isEnumExpose ( feature ) ) {
1383+ if ( allowsSet ) {
1384+ discoveryPayload . options = feature . values . map ( ( v ) => v . toString ( ) ) ;
1385+ discoveryPayload . command_template = compositePathCommandTemplate ( path , "{{ value | tojson }}" ) ;
1386+ }
1387+ Object . assign ( discoveryPayload , ENUM_DISCOVERY_LOOKUP [ feature . name ] ) ;
1388+ discoveryEntries . push ( {
1389+ type : allowsSet ? "select" : "sensor" ,
1390+ object_id : objectId ,
1391+ mockProperties : [ { property : firstComposite . property , value : null } ] ,
1392+ discovery_payload : discoveryPayload ,
1393+ } ) ;
1394+ } else if ( isBinaryExpose ( feature ) ) {
1395+ discoveryPayload . payload_on = feature . value_on . toString ( ) ;
1396+ discoveryPayload . payload_off = feature . value_off . toString ( ) ;
1397+ if ( allowsSet ) {
1398+ discoveryPayload . command_template = compositePathCommandTemplate (
1399+ path ,
1400+ typeof feature . value_on === "boolean" ? "{{ value }}" : "{{ value | tojson }}" ,
1401+ ) ;
1402+ }
1403+ Object . assign ( discoveryPayload , BINARY_DISCOVERY_LOOKUP [ feature . name ] ) ;
1404+ discoveryEntries . push ( {
1405+ type : allowsSet ? "switch" : "binary_sensor" ,
1406+ object_id : objectId ,
1407+ mockProperties : [ { property : firstComposite . property , value : null } ] ,
1408+ discovery_payload : discoveryPayload ,
1409+ } ) ;
1410+ } else if ( feature . type === "text" ) {
1411+ const textFeature = feature as zhc . Text ;
1412+ if ( allowsSet ) discoveryPayload . command_template = compositePathCommandTemplate ( path , "{{ value | tojson }}" ) ;
1413+ Object . assign ( discoveryPayload , LIST_DISCOVERY_LOOKUP [ textFeature . name ] ) ;
1414+ discoveryEntries . push ( {
1415+ type : allowsSet ? "text" : "sensor" ,
1416+ object_id : objectId ,
1417+ mockProperties : [ { property : firstComposite . property , value : null } ] ,
1418+ discovery_payload : discoveryPayload ,
1419+ } ) ;
1420+ }
1421+ } ;
1422+
1423+ for ( const feature of firstComposite . features ) {
1424+ addCompositeFeatureDiscovery ( feature , [ firstComposite . property , feature . property ] , [ firstComposite . label , feature . label ] ) ;
1425+ }
1426+
1427+ if ( discoveryEntries . length > 0 ) {
1428+ break ;
1429+ }
1430+ }
1431+
12951432 if ( firstExposeTyped . type === "text" && firstExposeTyped . access & ACCESS_SET ) {
12961433 discoveryEntries . push ( {
12971434 type : "text" ,
0 commit comments