@@ -11,20 +11,7 @@ import {logger} from "./logger";
1111import type { ElectricityMeterArgs } from "./modernExtend" ;
1212import { payload } from "./reporting" ;
1313import * as globalStore from "./store" ;
14- import type {
15- Configure ,
16- DefinitionExposesFunction ,
17- DefinitionMeta ,
18- DummyDevice ,
19- Expose ,
20- Fz ,
21- KeyValue ,
22- KeyValueAny ,
23- ModernExtend ,
24- OnEvent ,
25- Tz ,
26- Zh ,
27- } from "./types" ;
14+ import type { Configure , DefinitionExposesFunction , DummyDevice , Expose , Fz , KeyValue , KeyValueAny , ModernExtend , OnEvent , Tz , Zh } from "./types" ;
2815import * as utils from "./utils" ;
2916import { sleep , toNumber } from "./utils" ;
3017
@@ -3160,6 +3147,7 @@ const boschThermostatLookup = {
31603147 systemModes : {
31613148 heat : 0x04 ,
31623149 cool : 0x03 ,
3150+ off : 0x00 ,
31633151 } ,
31643152 raRunningStates : < ( "idle" | "heat" | "cool" | "fan_only" ) [ ] > [ "idle" , "heat" ] ,
31653153 heaterType : {
@@ -3255,6 +3243,16 @@ export const boschThermostatExtend = {
32553243 entityCategory : "config" ,
32563244 } ) ,
32573245 humidity : ( ) => m . humidity ( { reporting : false } ) ,
3246+ operatingMode : ( args ?: { enableReporting : boolean } ) =>
3247+ m . enumLookup < "hvacThermostat" , BoschThermostatCluster > ( {
3248+ name : "operating_mode" ,
3249+ cluster : "hvacThermostat" ,
3250+ attribute : "operatingMode" ,
3251+ description : "Bosch-specific operating mode" ,
3252+ lookup : { schedule : 0x00 , manual : 0x01 , pause : 0x05 } ,
3253+ reporting : args ?. enableReporting ? { min : "MIN" , max : "MAX" , change : null } : false ,
3254+ entityCategory : "config" ,
3255+ } ) ,
32583256 windowOpenMode : ( args ?: { enableReporting : boolean } ) =>
32593257 m . binary < "hvacThermostat" , BoschThermostatCluster > ( {
32603258 name : "window_open_mode" ,
@@ -3389,7 +3387,7 @@ export const boschThermostatExtend = {
33893387 rmThermostat : ( ) : ModernExtend => {
33903388 const thermostat = m . thermostat ( {
33913389 localTemperature : {
3392- configure : { reporting : false } ,
3390+ configure : { reporting : { min : 30 , max : 900 , change : 10 } } ,
33933391 } ,
33943392 localTemperatureCalibration : {
33953393 values : { min : - 5 , max : 5 , step : 0.1 } ,
@@ -3403,121 +3401,25 @@ export const boschThermostatExtend = {
34033401 configure : { reporting : { min : "10_SECONDS" , max : "MAX" , change : 50 } } ,
34043402 } ,
34053403 systemMode : {
3406- values : [ "heat" , "cool" ] ,
3407- toZigbee : { skip : true } ,
3408- configure : { skip : true } ,
3404+ values : [ "off" , "heat" , "cool" ] ,
3405+ configure : { reporting : { min : "MIN" , max : "MAX" , change : null } } ,
34093406 } ,
34103407 runningState : {
34113408 values : [ "idle" , "heat" , "cool" ] ,
34123409 configure : { reporting : { min : "MIN" , max : "MAX" , change : null } } ,
34133410 } ,
34143411 } ) ;
34153412
3416- const expose : DefinitionExposesFunction = ( device : Zh . Device | DummyDevice , options : KeyValue ) => {
3417- const returnedThermostat = < Expose [ ] > thermostat . exposes ;
3418-
3419- if ( utils . isDummyDevice ( device ) ) {
3420- return returnedThermostat ;
3421- }
3422-
3423- let currentSystemMode : string ;
3424- try {
3425- currentSystemMode = utils . getFromLookupByValue (
3426- device . getEndpoint ( 1 ) . getClusterAttributeValue ( "hvacThermostat" , "systemMode" ) ,
3427- boschThermostatLookup . systemModes ,
3428- ) ;
3429- } catch {
3430- currentSystemMode = "heat" ;
3431- }
3432-
3433- // The thermostat is a singleton, thus the values must be set
3434- // manually as filtering will lead to an array without
3435- // heat/cool in them after two systemMode changes.
3436- returnedThermostat [ 0 ] . features . forEach ( ( exposedAttribute , index , array ) => {
3437- if ( exposedAttribute . type === "enum" ) {
3438- if ( exposedAttribute . name === "system_mode" ) {
3439- exposedAttribute . label = "Active system mode" ;
3440- exposedAttribute . description =
3441- "Currently used system mode by the thermostat. This field is primarily " +
3442- "used to configure the thermostat in Home Assistant correctly." ;
3443- exposedAttribute . values = [ currentSystemMode ] ;
3444- exposedAttribute . access = ea . STATE ;
3445- }
3446-
3447- if ( exposedAttribute . name === "running_state" ) {
3448- exposedAttribute . values = [ "idle" , currentSystemMode ] ;
3449- }
3450- }
3451- } ) ;
3452- return returnedThermostat ;
3453- } ;
3413+ const exposes : ( Expose | DefinitionExposesFunction ) [ ] = thermostat . exposes ;
34543414
34553415 return {
3456- exposes : [ expose ] ,
3416+ exposes : exposes ,
34573417 fromZigbee : thermostat . fromZigbee ,
34583418 toZigbee : thermostat . toZigbee ,
34593419 configure : thermostat . configure ,
34603420 isModernExtend : true ,
34613421 } ;
34623422 } ,
3463- customSystemMode : ( ) : ModernExtend => {
3464- const exposes : Expose [ ] = [
3465- e
3466- . enum ( "custom_system_mode" , ea . ALL , Object . keys ( boschThermostatLookup . systemModes ) )
3467- . withLabel ( "Available system modes" )
3468- . withDescription ( "Select if the thermostat is connected to a heating or a cooling device" )
3469- . withCategory ( "config" ) ,
3470- ] ;
3471-
3472- const fromZigbee = [
3473- {
3474- cluster : "hvacThermostat" ,
3475- type : [ "attributeReport" , "readResponse" ] ,
3476- convert : ( model , msg , publish , options , meta ) => {
3477- const result : KeyValue = { } ;
3478- const data = msg . data ;
3479-
3480- if ( data . systemMode !== undefined ) {
3481- result . custom_system_mode = utils . getFromLookupByValue ( data . systemMode , boschThermostatLookup . systemModes ) ;
3482- meta . deviceExposesChanged ( ) ;
3483- }
3484-
3485- return result ;
3486- } ,
3487- } satisfies Fz . Converter < "hvacThermostat" , undefined , [ "attributeReport" , "readResponse" ] > ,
3488- ] ;
3489-
3490- const toZigbee : Tz . Converter [ ] = [
3491- {
3492- key : [ "custom_system_mode" ] ,
3493- convertSet : async ( entity , key , value , meta ) => {
3494- await entity . write ( "hvacThermostat" , {
3495- systemMode : utils . toNumber ( utils . getFromLookup ( value , boschThermostatLookup . systemModes ) ) ,
3496- } ) ;
3497-
3498- return { state : { custom_system_mode : value } } ;
3499- } ,
3500- convertGet : async ( entity , key , meta ) => {
3501- await entity . read ( "hvacThermostat" , [ "systemMode" ] ) ;
3502- } ,
3503- } ,
3504- ] ;
3505-
3506- const configure : Configure [ ] = [
3507- m . setupConfigureForReporting ( "hvacThermostat" , "systemMode" , {
3508- config : false ,
3509- access : ea . ALL ,
3510- } ) ,
3511- ] ;
3512-
3513- return {
3514- exposes,
3515- fromZigbee,
3516- toZigbee,
3517- configure,
3518- isModernExtend : true ,
3519- } ;
3520- } ,
35213423 raThermostat : ( ) : ModernExtend => {
35223424 // Native thermostat
35233425 const thermostat = m . thermostat ( {
@@ -3629,92 +3531,6 @@ export const boschThermostatExtend = {
36293531 isModernExtend : true ,
36303532 } ;
36313533 } ,
3632- operatingMode : ( args ?: { enableReporting : boolean } ) : ModernExtend => {
3633- const operatingModeLookup = { schedule : 0x00 , manual : 0x01 , pause : 0x05 } ;
3634-
3635- const operatingMode = m . enumLookup < "hvacThermostat" , BoschThermostatCluster > ( {
3636- name : "operating_mode" ,
3637- cluster : "hvacThermostat" ,
3638- attribute : "operatingMode" ,
3639- description : "Bosch-specific operating mode. This is being used as mode on the exposed thermostat when using Home Assistant." ,
3640- lookup : operatingModeLookup ,
3641- reporting : args ?. enableReporting ? { min : "MIN" , max : "MAX" , change : null } : false ,
3642- entityCategory : "config" ,
3643- } ) ;
3644-
3645- const exposes : ( Expose | DefinitionExposesFunction ) [ ] = operatingMode . exposes ;
3646- const fromZigbee = operatingMode . fromZigbee ;
3647- const toZigbee : Tz . Converter [ ] = operatingMode . toZigbee ;
3648- const configure : Configure [ ] = operatingMode . configure ;
3649-
3650- const removeLowAndHighTemperatureFields = ( payload : KeyValueAny ) => {
3651- payload . temperature_high_command_topic = undefined ;
3652- payload . temperature_low_command_topic = undefined ;
3653-
3654- payload . temperature_high_state_template = undefined ;
3655- payload . temperature_low_state_template = undefined ;
3656-
3657- payload . temperature_high_state_topic = undefined ;
3658- payload . temperature_low_state_topic = undefined ;
3659- } ;
3660-
3661- // Override the payload send to Home Assistant to achieve the following:
3662- // 1. Use the Bosch operating mode instead of system modes
3663- // See: https://github.com/Koenkk/zigbee2mqtt/pull/23075#issue-2355829475
3664- // 2. Remove setpoints not compatible with the currently used system mode
3665- // See: https://github.com/Koenkk/zigbee2mqtt/issues/28892
3666- const meta : DefinitionMeta = {
3667- overrideHaDiscoveryPayload : ( payload ) => {
3668- if ( payload . modes !== undefined ) {
3669- if ( payload . modes . includes ( "heat" ) ) {
3670- payload . mode_command_template =
3671- `{% set values = { 'auto':'schedule', 'heat':'manual', 'off':'pause' } %}` +
3672- `{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}` ;
3673- payload . mode_state_template =
3674- `{% set values = { 'schedule':'auto', 'manual':'heat', 'pause':'off' } %}` +
3675- "{% set value = value_json.operating_mode %}" +
3676- `{{ values[value] if value in values.keys() else 'off' }}` ;
3677-
3678- if ( payload . temperature_low_command_topic !== undefined ) {
3679- payload . temperature_command_topic = payload . temperature_low_command_topic ;
3680- payload . temperature_state_template = payload . temperature_low_state_template ;
3681- payload . temperature_state_topic = payload . temperature_low_state_topic ;
3682-
3683- removeLowAndHighTemperatureFields ( payload ) ;
3684- }
3685- } else if ( payload . modes . includes ( "cool" ) ) {
3686- payload . mode_command_template =
3687- `{% set values = { 'auto':'schedule', 'cool':'manual', 'off':'pause' } %}` +
3688- `{"operating_mode": "{{ values[value] if value in values.keys() else 'pause' }}"}` ;
3689- payload . mode_state_template =
3690- `{% set values = { 'schedule':'auto', 'manual':'cool', 'pause':'off' } %}` +
3691- "{% set value = value_json.operating_mode %}" +
3692- `{{ values[value] if value in values.keys() else 'off' }}` ;
3693-
3694- if ( payload . temperature_high_command_topic !== undefined ) {
3695- payload . temperature_command_topic = payload . temperature_high_command_topic ;
3696- payload . temperature_state_template = payload . temperature_high_state_template ;
3697- payload . temperature_state_topic = payload . temperature_high_state_topic ;
3698-
3699- removeLowAndHighTemperatureFields ( payload ) ;
3700- }
3701- }
3702-
3703- payload . modes = [ "off" , ...payload . modes , "auto" ] ;
3704- payload . mode_command_topic = payload . mode_command_topic . replace ( "/system_mode" , "" ) ;
3705- }
3706- } ,
3707- } ;
3708-
3709- return {
3710- exposes,
3711- fromZigbee,
3712- toZigbee,
3713- configure,
3714- meta,
3715- isModernExtend : true ,
3716- } ;
3717- } ,
37183534 boostHeating : ( args ?: { enableReporting : boolean } ) : ModernExtend => {
37193535 const boostHeatingLookup : KeyValue = {
37203536 OFF : 0x00 ,
0 commit comments