@@ -872,15 +872,17 @@ export default class HomeAssistant extends Extension {
872
872
discovery_payload : {
873
873
name : null ,
874
874
state_topic : true ,
875
- state_value_template : '{{ value_json.fan_state }}' ,
876
875
command_topic : true ,
877
- command_topic_postfix : 'fan_state' ,
878
876
} ,
879
877
} ;
880
878
881
- const speed = ( firstExpose as zhc . Fan ) . features . filter ( isEnumExpose ) . find ( ( e ) => e . name === 'mode' ) ;
879
+ const modeEmulatedSpeed = ( firstExpose as zhc . Fan ) . features . filter ( isEnumExpose ) . find ( ( e ) => e . name === 'mode' ) ;
880
+ const nativeSpeed = ( firstExpose as zhc . Fan ) . features . filter ( isNumericExpose ) . find ( ( e ) => e . name === 'speed' ) ;
882
881
883
- if ( speed ) {
882
+ // Exactly one mode needs to be active (logical xor)
883
+ assert ( ! modeEmulatedSpeed != ! nativeSpeed , 'Fans need to be either mode- or speed-controlled' ) ;
884
+
885
+ if ( modeEmulatedSpeed ) {
884
886
// A fan entity in Home Assistant 2021.3 and above may have a speed,
885
887
// controlled by a percentage from 1 to 100, and/or non-speed presets.
886
888
// The MQTT Fan integration allows the speed percentage to be mapped
@@ -894,9 +896,9 @@ export default class HomeAssistant extends Extension {
894
896
// ZCL. This supports a generic ZCL HVAC Fan Control fan. "Off" is
895
897
// always a valid speed.
896
898
let speeds = [ 'off' ] . concat (
897
- [ 'low' , 'medium' , 'high' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' ] . filter ( ( s ) => speed . values . includes ( s ) ) ,
899
+ [ 'low' , 'medium' , 'high' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' ] . filter ( ( s ) => modeEmulatedSpeed . values . includes ( s ) ) ,
898
900
) ;
899
- let presets = [ 'on' , 'auto' , 'smart' ] . filter ( ( s ) => speed . values . includes ( s ) ) ;
901
+ let presets = [ 'on' , 'auto' , 'smart' ] . filter ( ( s ) => modeEmulatedSpeed . values . includes ( s ) ) ;
900
902
901
903
if ( [ '99432' ] . includes ( definition ! . model ) ) {
902
904
// The Hampton Bay 99432 fan implements 4 speeds using the ZCL
@@ -908,22 +910,37 @@ export default class HomeAssistant extends Extension {
908
910
}
909
911
910
912
const allowed = [ ...speeds , ...presets ] ;
911
- speed . values . forEach ( ( s ) => assert ( allowed . includes ( s . toString ( ) ) ) ) ;
913
+ modeEmulatedSpeed . values . forEach ( ( s ) => assert ( allowed . includes ( s . toString ( ) ) ) ) ;
912
914
const percentValues = speeds . map ( ( s , i ) => `'${ s } ':${ i } ` ) . join ( ', ' ) ;
913
915
const percentCommands = speeds . map ( ( s , i ) => `${ i } :'${ s } '` ) . join ( ', ' ) ;
914
916
const presetList = presets . map ( ( s ) => `'${ s } '` ) . join ( ', ' ) ;
915
917
916
918
discoveryEntry . discovery_payload . percentage_state_topic = true ;
917
- discoveryEntry . discovery_payload . percentage_command_topic = true ;
918
- discoveryEntry . discovery_payload . percentage_value_template = `{{ {${ percentValues } }[value_json.${ speed . property } ] | default('None') }}` ;
919
+ discoveryEntry . discovery_payload . percentage_command_topic = 'fan_mode' ;
920
+ discoveryEntry . discovery_payload . percentage_value_template = `{{ {${ percentValues } }[value_json.${ modeEmulatedSpeed . property } ] | default('None') }}` ;
919
921
discoveryEntry . discovery_payload . percentage_command_template = `{{ {${ percentCommands } }[value] | default('') }}` ;
920
922
discoveryEntry . discovery_payload . speed_range_min = 1 ;
921
923
discoveryEntry . discovery_payload . speed_range_max = speeds . length - 1 ;
922
924
assert ( presets . length !== 0 ) ;
923
925
discoveryEntry . discovery_payload . preset_mode_state_topic = true ;
924
926
discoveryEntry . discovery_payload . preset_mode_command_topic = 'fan_mode' ;
925
- discoveryEntry . discovery_payload . preset_mode_value_template = `{{ value_json.${ speed . property } if value_json.${ speed . property } in [${ presetList } ] else 'None' | default('None') }}` ;
927
+ discoveryEntry . discovery_payload . preset_mode_value_template = `{{ value_json.${ modeEmulatedSpeed . property } if value_json.${ modeEmulatedSpeed . property } in [${ presetList } ] else 'None' | default('None') }}` ;
926
928
discoveryEntry . discovery_payload . preset_modes = presets ;
929
+
930
+ // Emulate state based on mode
931
+ discoveryEntry . discovery_payload . state_value_template = '{{ value_json.fan_state }}' ;
932
+ discoveryEntry . discovery_payload . command_topic_postfix = 'fan_state' ;
933
+ } else if ( nativeSpeed ) {
934
+ discoveryEntry . discovery_payload . percentage_state_topic = true ;
935
+ discoveryEntry . discovery_payload . percentage_command_topic = 'speed' ;
936
+ discoveryEntry . discovery_payload . percentage_value_template = `{{ value_json.${ nativeSpeed . property } | default('None') }}` ;
937
+ discoveryEntry . discovery_payload . percentage_command_template = `{{ value | default('') }}` ;
938
+ discoveryEntry . discovery_payload . speed_range_min = nativeSpeed . value_min ;
939
+ discoveryEntry . discovery_payload . speed_range_max = nativeSpeed . value_max ;
940
+
941
+ // Speed-controlled fans generally have an onOff cluster, use that for state
942
+ discoveryEntry . discovery_payload . state_value_template = '{{ value_json.state }}' ;
943
+ discoveryEntry . discovery_payload . command_topic_postfix = 'state' ;
927
944
}
928
945
929
946
discoveryEntries . push ( discoveryEntry ) ;
@@ -1621,7 +1638,7 @@ export default class HomeAssistant extends Extension {
1621
1638
}
1622
1639
1623
1640
if ( payload . percentage_command_topic ) {
1624
- payload . percentage_command_topic = `${ baseTopic } /${ commandTopicPrefix } set/fan_mode ` ;
1641
+ payload . percentage_command_topic = `${ baseTopic } /${ commandTopicPrefix } set/${ payload . percentage_command_topic } ` ;
1625
1642
}
1626
1643
1627
1644
if ( payload . preset_mode_state_topic ) {
0 commit comments