From bd949518de3ac55349a1882efecf63940073437d Mon Sep 17 00:00:00 2001 From: eugen Date: Thu, 4 Feb 2021 07:06:20 +0100 Subject: [PATCH] [homekit] mapping configuration improvements (#9932) * add support for custom mapping * add screenshot * remove unnecessary variable * make parameter nullable to avoid not required list instances Signed-off-by: Eugen Freiter --- bundles/org.openhab.io.homekit/README.md | 401 +++++++++++------- .../doc/mode_mapping.png | Bin 0 -> 52799 bytes .../homekit/internal/HomekitAuthInfoImpl.java | 5 +- .../AbstractHomekitAccessoryImpl.java | 20 +- .../accessories/HomekitHeaterCoolerImpl.java | 21 +- .../HomekitSecuritySystemImpl.java | 23 +- .../accessories/HomekitThermostatImpl.java | 135 +++--- 7 files changed, 349 insertions(+), 256 deletions(-) create mode 100644 bundles/org.openhab.io.homekit/doc/mode_mapping.png diff --git a/bundles/org.openhab.io.homekit/README.md b/bundles/org.openhab.io.homekit/README.md index 6cc248fe47..60c7d786b5 100644 --- a/bundles/org.openhab.io.homekit/README.md +++ b/bundles/org.openhab.io.homekit/README.md @@ -108,10 +108,10 @@ org.openhab.homekit:maximumTemperature=100 | pin | Pin code used for pairing with iOS devices. Apparently, pin codes are provided by Apple and represent specific device types, so they cannot be chosen freely. The pin code 031-45-154 is used in sample applications and known to work. | 031-45-154 | | startDelay | HomeKit start delay in seconds in case the number of accessories is lower than last time. This helps to avoid resetting home app in case not all items have been initialised properly before HomeKit integration start. | 30 | | useFahrenheitTemperature | Set to true to use Fahrenheit degrees, or false to use Celsius degrees. | false | -| thermostatTargetModeCool | Word used for activating the cooling mode of the device (if applicable). | CoolOn | -| thermostatTargetModeHeat | Word used for activating the heating mode of the device (if applicable). | HeatOn | -| thermostatTargetModeAuto | Word used for activating the automatic mode of the device (if applicable). | Auto | -| thermostatTargetModeOff | Word used to set the thermostat mode of the device to off (if applicable). | Off | +| thermostatTargetModeCool | Word used for activating the cooling mode of the device (if applicable). It can be overwritten at item level. | CoolOn | +| thermostatTargetModeHeat | Word used for activating the heating mode of the device (if applicable). It can be overwritten at item level. | HeatOn | +| thermostatTargetModeAuto | Word used for activating the automatic mode of the device (if applicable). It can be overwritten at item level. | Auto | +| thermostatTargetModeOff | Word used to set the thermostat mode of the device to off (if applicable). It can be overwritten at item level. | Off | | minimumTemperature | Lower bound of possible temperatures, used in the user interface of the iOS device to display the allowed temperature range. Note that this setting applies to all devices in HomeKit. | -100 | | maximumTemperature | Upper bound of possible temperatures, used in the user interface of the iOS device to display the allowed temperature range. Note that this setting applies to all devices in HomeKit. | 100 | | name | Name under which this HomeKit bridge is announced on the network. This is also the name displayed on the iOS device when searching for available bridges. | openHAB | @@ -198,6 +198,242 @@ Switch light1 "Light 1" (gLight) {homekit="Lighting.OnState"} Switch light2 "Light 2" (gLight) {homekit="Lighting.OnState"} ``` + +## Accessory Configuration Details + +This section provides examples widely used accessory types. +For complete list of supported accessory types and characteristics please see section [Supported accessory type](#Supported accessory type) + +### Dimmers + +The way HomeKit handles dimmer devices can be different to the actual dimmers' way of working. +HomeKit home app sends following commands/update: + +- On brightness change home app sends "ON" event along with target brightness, e.g. "Brightness = 50%" + "State = ON". +- On "ON" event home app sends "ON" along with brightness 100%, i.e. "Brightness = 100%" + "State = ON" +- On "OFF" event home app sends "OFF" without brightness information. + +However, some dimmer devices for example do not expect brightness on "ON" event, some others do not expect "ON" upon brightness change. +In order to support different devices HomeKit integration can filter some events. Which events should be filtered is defined via dimmerMode configuration. + +```xtend +Dimmer dimmer_light "Dimmer Light" {homekit="Lighting, Lighting.Brightness" [dimmerMode=""]} +``` + +Following modes are supported: + +- "normal" - no filtering. The commands will be sent to device as received from HomeKit. This is default mode. +- "filterOn" - ON events are filtered out. only OFF events and brightness information are sent +- "filterBrightness100" - only Brightness=100% is filtered out. everything else sent unchanged. This allows custom logic for soft launch in devices. +- "filterOnExceptBrightness100" - ON events are filtered out in all cases except of brightness = 100%. + +Examples: + + ```xtend + Dimmer dimmer_light_1 "Dimmer Light 1" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterOn"]} + Dimmer dimmer_light_2 "Dimmer Light 2" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterBrightness100"]} + Dimmer dimmer_light_3 "Dimmer Light 3" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterOnExceptBrightness100"]} + ``` + +### Windows Covering (Blinds) / Window / Door + +HomeKit Windows Covering, Window and Door accessory types have following mandatory characteristics: + +- CurrentPosition (0-100% of current window covering position) +- TargetPosition (0-100% of target position) +- PositionState (DECREASING,INCREASING or STOPPED as state). If no state provided, HomeKit will send STOPPED + +These characteristics can be mapped to a single openHAB rollershutter item. In such case currentPosition will always equal target position, means if you request to close a blind/window/door, HomeKit will immediately report that the blind/window/door is closed. +As discussed above, one can use full or shorthand definition. Following two definitions are equal: + +```xtend +Rollershutter window "Window" {homekit = "Window"} +Rollershutter door "Door" {homekit = "Door"} +Rollershutter window_covering "Window Rollershutter" {homekit = "WindowCovering"} +Rollershutter window_covering_long "Window Rollershutter long" {homekit = "WindowCovering, WindowCovering.CurrentPosition, WindowCovering.TargetPosition, WindowCovering.PositionState"} + ``` + +openHAB Rollershutter is defined by default as: + +- OPEN if position is 0%, +- CLOSED if position is 100%. + +In contrast, HomeKit window covering/door/window have inverted mapping + +- OPEN if position 100% +- CLOSED if position is 0% + +Therefore, HomeKit integration inverts by default the values between openHAB and HomeKit, e.g. if openHAB current position is 30% then it will send 70% to HomeKit app. +In case you need to disable this logic you can do it with configuration parameter inverted="false", e.g. + +```xtend +Rollershutter window_covering "Window Rollershutter" {homekit = "WindowCovering" [inverted="false"]} +Rollershutter window "Window" {homekit = "Window" [inverted="false"]} +Rollershutter door "Door" {homekit = "Door" [inverted="false"]} + + ``` + +Window covering can have a number of optional characteristics like horizontal & vertical tilt, obstruction status and hold position trigger. +If your blind supports tilt, and you want to control tilt via HomeKit you need to define blind as a group. +e.g. + +```xtend +Group gBlind "Blind with tilt" {homekit = "WindowCovering"} +Rollershutter window_covering "Blind" (gBlind) {homekit = "WindowCovering"} +Dimmer window_covering_htilt "Blind horizontal tilt" (gBlind) {homekit = "WindowCovering.CurrentHorizontalTiltAngle, WindowCovering.TargetHorizontalTiltAngle"} +Dimmer window_covering_vtilt "Blind vertical tilt" (gBlind) {homekit = "WindowCovering.CurrentVerticalTiltAngle, WindowCovering.TargetVerticalTiltAngle"} + ``` +### Thermostat +A HomeKit thermostat has following mandatory characteristics: + +- CurrentTemperature +- TargetTemperature +- CurrentHeatingCoolingMode +- TargetHeatingCoolingMode + +In order to define a thermostat you need to create a group with at least these 4 items. +Example: + +```xtend +Group gThermostat "Thermostat" {homekit = "Thermostat"} +Number thermostat_current_temp "Thermostat Current Temp [%.1f C]" (gThermostat) {homekit = "CurrentTemperature"} +Number thermostat_target_temp "Thermostat Target Temp[%.1f C]" (gThermostat) {homekit = "TargetTemperature"} +String thermostat_current_mode "Thermostat Current Mode" (gThermostat) {homekit = "CurrentHeatingCoolingMode"} +String thermostat_target_mode "Thermostat Target Mode" (gThermostat) {homekit = "TargetHeatingCoolingMode"} +``` + +In addition, thermostat can have thresholds for cooling and heating modes. +Example with thresholds: + +```xtend +Group gThermostat "Thermostat" {homekit = "Thermostat"} +Number thermostat_current_temp "Thermostat Current Temp [%.1f C]" (gThermostat) {homekit = "CurrentTemperature"} +Number thermostat_target_temp "Thermostat Target Temp[%.1f C]" (gThermostat) {homekit = "TargetTemperature"} +String thermostat_current_mode "Thermostat Current Mode" (gThermostat) {homekit = "CurrentHeatingCoolingMode"} +String thermostat_target_mode "Thermostat Target Mode" (gThermostat) {homekit = "TargetHeatingCoolingMode"} +Number thermostat_cool_thrs "Thermostat Cool Threshold Temp [%.1f C]" (gThermostat) {homekit = "CoolingThresholdTemperature"} +Number thermostat_heat_thrs "Thermostat Heat Threshold Temp [%.1f C]" (gThermostat) {homekit = "HeatingThresholdTemperature"} +``` + +#### Min / max temperatures + +Current and target temperatures have default min and max values. Any values below or above max limits will be replaced with min or max limits. +Default limits are: +- current temperature: min value = 0 C, max value = 100 C +- target temperature: min value = 10 C, max value = 38 C + +You can overwrite default values using minValue and maxValue configuration at item level, e.g. + +```xtend +Number thermostat_current_temp "Thermostat Current Temp [%.1f C]" (gThermostat) {homekit = "CurrentTemperature" [minValue=5, maxValue=30]} +Number thermostat_target_temp "Thermostat Target Temp[%.1f C]" (gThermostat) {homekit = "TargetTemperature" [minValue=10.5, maxValue=27]} +``` + +#### Thermostat modes + +HomeKit thermostat supports following modes + +- CurrentHeatingCoolingMode: OFF, HEAT, COOL +- TargetHeatingCoolingMode: OFF, HEAT, COOL, AUTO + +These modes are mapped to string values of openHAB items using either global configuration (see [Global Configuration](#Global Configuration)) or configuration at item level. +e.g. if your current mode item can have following values: "OFF", "HEATING", "COOLING" then you need following mapping at item level + +```xtend +String thermostat_current_mode "Thermostat Current Mode" (gThermostat) {homekit = "CurrentHeatingCoolingMode" [OFF="OFF", HEAT="HEATING", COOL="COOLING"]} +``` + +You can provide mapping for target mode in similar way. + +The custom mapping at item level can be also used to reduce number of modes shown in home app. The modes can be only reduced, but not added, i.e. it is not possible to add new custom mode to HomeKit thermostat. + +Example: if your thermostat does not support cooling, then you need to limit mapping to OFF and HEAT values only: + +```xtend +String thermostat_current_mode "Thermostat Current Mode" (gThermostat) {homekit = "CurrentHeatingCoolingMode" [HEAT="HEATING", OFF="OFF"]} +String thermostat_target_mode "Thermostat Target Mode" (gThermostat) {homekit = "TargetHeatingCoolingMode" [HEAT="HEATING", OFF="OFF"]} +``` + +The mapping using main UI looks like following: + +![mode_mapping.png](doc/mode_mapping.png) + +### Valve + +The HomeKit valve accessory supports following 2 optional characteristics: + +- duration: this describes how long the valve should set "InUse" once it is activated. The duration changes will apply to the next operation. If valve is already active then duration changes have no effect. + +- remaining duration: this describes the remaining duration on the valve. Notifications on this characteristic must only be used if the remaining duration increases/decreases from the accessoryʼs usual countdown of remaining duration. + +Upon valve activation in home app, home app starts to count down from the "duration" to "0" without contacting the server. Home app also does not trigger any action if it remaining duration get 0. +It is up to valve to have an own timer and stop valve once the timer is over. +Some valves have such timer, e.g. pretty common for sprinklers. +In case the valve has no timer capability, openHAB can take care on this - start an internal timer and send "Off" command to the valve once the timer is over. + +configuration for these two cases looks as follow: + +- valve with timer: + +```xtend +Group gValve "Valve Group" {homekit="Valve" [homekitValveType="Irrigation"]} +Switch valve_active "Valve active" (gValve) {homekit = "Valve.ActiveStatus, Valve.InUseStatus"} +Number valve_duration "Valve duration" (gValve) {homekit = "Valve.Duration"} +Number valve_remaining_duration "Valve remaining duration" (gValve) {homekit = "Valve.RemainingDuration"} +``` + +- valve without timer (no item for remaining duration required) + +```xtend +Group gValve "Valve Group" {homekit="Valve" [homekitValveType="Irrigation", homekitTimer="true]} +Switch valve_active "Valve active" (gValve) {homekit = "Valve.ActiveStatus, Valve.InUseStatus"} +Number valve_duration "Valve duration" (gValve) {homekit = "Valve.Duration" [homekitDefaultDuration = 1800]} +``` + +### Sensors + +Sensors have typically one mandatory characteristic, e.g. temperature or lead trigger, and several optional characteristics which are typically used for battery powered sensors and/or wireless sensors. +Following table summarizes the optional characteristics supported by sensors. + +| Characteristics | Supported openHAB items | Description | +|:-----------------------------|:-------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Name | String | Name of the sensor. This characteristic is interesting only for very specific cases in which the name of accessory is dynamic. if you not sure then you don't need it. | +| ActiveStatus | Switch, Contact | Accessory current working status. "ON"/"OPEN" indicates that the accessory is active and is functioning without any errors. | +| FaultStatus | Switch, Contact | Accessory fault status. "ON"/"OPEN" value indicates that the accessory has experienced a fault that may be interfering with its intended functionality. A value of "OFF"/"CLOSED" indicates that there is no fault. | +| TamperedStatus | Switch, Contact | Accessory tampered status. "ON"/"OPEN" indicates that the accessory has been tampered. Value should return to "OFF"/"CLOSED" when the accessory has been reset to a non-tampered state. | +| BatteryLowStatus | Switch, Contact | Accessory battery status. "ON"/"OPEN" indicates that the battery level of the accessory is low. Value should return to "OFF"/"CLOSED" when the battery charges to a level thats above the low threshold. | + +Examples of sensor definitions. +Sensors without optional characteristics: + +```xtend +Switch leaksensor_single "Leak Sensor" {homekit="LeakSensor"} +Number light_sensor "Light Sensor" {homekit="LightSensor"} +Number temperature_sensor "Temperature Sensor [%.1f C]" {homekit="TemperatureSensor"} +Contact contact_sensor "Contact Sensor" {homekit="ContactSensor"} +Switch occupancy_sensor "Occupancy Sensor" {homekit="OccupancyDetectedState"} +Switch motion_sensor "Motion Sensor" {homekit="MotionSensor"} +Number humidity_sensor "Humidity Sensor" {homekit="HumiditySensor"} +``` + +Sensors with optional characteristics: + +```xtend +Group gLeakSensor "Leak Sensor" {homekit="LeakSensor"} +Switch leaksensor "Leak Sensor State" (gLeakSensor) {homekit="LeakDetectedState"} +Switch leaksensor_bat "Leak Sensor Battery" (gLeakSensor) {homekit="BatteryLowStatus"} +Switch leaksensor_active "Leak Sensor Active" (gLeakSensor) {homekit="ActiveStatus"} +Switch leaksensor_fault "Leak Sensor Fault" (gLeakSensor) {homekit="FaultStatus"} +Switch leaksensor_tampered "Leak Sensor Tampered" (gLeakSensor) {homekit="TamperedStatus"} + +Group gMotionSensor "Motion Sensor" {homekit="MotionSensor"} +Switch motionsensor "Motion Sensor State" (gMotionSensor) {homekit="MotionDetectedState"} +Switch motionsensor_bat "Motion Sensor Battery" (gMotionSensor) {homekit="BatteryLowStatus"} +Switch motionsensor_active "Motion Sensor Active" (gMotionSensor) {homekit="ActiveStatus"} +Switch motionsensor_fault "Motion Sensor Fault" (gMotionSensor) {homekit="FaultStatus"} +Switch motionsensor_tampered "Motion Sensor Tampered" (gMotionSensor) {homekit="TamperedStatus"} +``` + ## Supported accessory type | Accessory Tag | Mandatory Characteristics | Optional Characteristics | Supported OH items | Description | @@ -461,163 +697,6 @@ Number cooler_cool_thrs "Cooler Cool Threshold Temp [%.1f C]" (gCo Number cooler_heat_thrs "Cooler Heat Threshold Temp [%.1f C]" (gCooler) {homekit="HeatingThresholdTemperature" [minValue=0.5, maxValue=20]} ``` -## Accessory Configuration Details - -### Dimmers - -The way HomeKit handles dimmer devices can be different to the actual dimmers' way of working. -HomeKit home app sends following commands/update: - -- On brightness change home app sends "ON" event along with target brightness, e.g. "Brightness = 50%" + "State = ON". -- On "ON" event home app sends "ON" along with brightness 100%, i.e. "Brightness = 100%" + "State = ON" -- On "OFF" event home app sends "OFF" without brightness information. - -However, some dimmer devices for example do not expect brightness on "ON" event, some others do not expect "ON" upon brightness change. -In order to support different devices HomeKit integration can filter some events. Which events should be filtered is defined via dimmerMode configuration. - -```xtend -Dimmer dimmer_light "Dimmer Light" {homekit="Lighting, Lighting.Brightness" [dimmerMode=""]} -``` - -Following modes are supported: - -- "normal" - no filtering. The commands will be sent to device as received from HomeKit. This is default mode. -- "filterOn" - ON events are filtered out. only OFF events and brightness information are sent -- "filterBrightness100" - only Brightness=100% is filtered out. everything else sent unchanged. This allows custom logic for soft launch in devices. -- "filterOnExceptBrightness100" - ON events are filtered out in all cases except of brightness = 100%. - - Examples: - - ```xtend - Dimmer dimmer_light_1 "Dimmer Light 1" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterOn"]} - Dimmer dimmer_light_2 "Dimmer Light 2" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterBrightness100"]} - Dimmer dimmer_light_3 "Dimmer Light 3" {homekit="Lighting, Lighting.Brightness" [dimmerMode="filterOnExceptBrightness100"]} - ``` - -### Windows Covering (Blinds) / Window / Door - -HomeKit Windows Covering, Window and Door accessory types have following mandatory characteristics: - -- CurrentPosition (0-100% of current window covering position) -- TargetPosition (0-100% of target position) -- PositionState (DECREASING,INCREASING or STOPPED as state). If no state provided, HomeKit will send STOPPED - -These characteristics can be mapped to a single openHAB rollershutter item. In such case currentPosition will always equal target position, means if you request to close a blind/window/door, HomeKit will immediately report that the blind/window/door is closed. -As discussed above, one can use full or shorthand definition. Following two definitions are equal: - -```xtend -Rollershutter window "Window" {homekit = "Window"} -Rollershutter door "Door" {homekit = "Door"} -Rollershutter window_covering "Window Rollershutter" {homekit = "WindowCovering"} -Rollershutter window_covering_long "Window Rollershutter long" {homekit = "WindowCovering, WindowCovering.CurrentPosition, WindowCovering.TargetPosition, WindowCovering.PositionState"} - ``` - -openHAB Rollershutter is defined by default as: - -- OPEN if position is 0%, -- CLOSED if position is 100%. - -In contrast, HomeKit window covering/door/window have inverted mapping - -- OPEN if position 100% -- CLOSED if position is 0% - -Therefore, HomeKit integration inverts by default the values between openHAB and HomeKit, e.g. if openHAB current position is 30% then it will send 70% to HomeKit app. -In case you need to disable this logic you can do it with configuration parameter inverted="false", e.g. - -```xtend -Rollershutter window_covering "Window Rollershutter" {homekit = "WindowCovering" [inverted="false"]} -Rollershutter window "Window" {homekit = "Window" [inverted="false"]} -Rollershutter door "Door" {homekit = "Door" [inverted="false"]} - - ``` - -Window covering can have a number of optional characteristics like horizontal & vertical tilt, obstruction status and hold position trigger. -If your blind supports tilt, and you want to control tilt via HomeKit you need to define blind as a group. -e.g. - -```xtend -Group gBlind "Blind with tilt" {homekit = "WindowCovering"} -Rollershutter window_covering "Blind" (gBlind) {homekit = "WindowCovering"} -Dimmer window_covering_htilt "Blind horizontal tilt" (gBlind) {homekit = "WindowCovering.CurrentHorizontalTiltAngle, WindowCovering.TargetHorizontalTiltAngle"} -Dimmer window_covering_vtilt "Blind vertical tilt" (gBlind) {homekit = "WindowCovering.CurrentVerticalTiltAngle, WindowCovering.TargetVerticalTiltAngle"} - ``` - -### Valve - -The HomeKit valve accessory supports following 2 optional characteristics: - -- duration: this describes how long the valve should set "InUse" once it is activated. The duration changes will apply to the next operation. If valve is already active then duration changes have no effect. - -- remaining duration: this describes the remaining duration on the valve. Notifications on this characteristic must only be used if the remaining duration increases/decreases from the accessoryʼs usual countdown of remaining duration. - -Upon valve activation in home app, home app starts to count down from the "duration" to "0" without contacting the server. Home app also does not trigger any action if it remaining duration get 0. -It is up to valve to have an own timer and stop valve once the timer is over. -Some valves have such timer, e.g. pretty common for sprinklers. -In case the valve has no timer capability, openHAB can take care on this - start an internal timer and send "Off" command to the valve once the timer is over. - -configuration for these two cases looks as follow: - -- valve with timer: - -```xtend -Group gValve "Valve Group" {homekit="Valve" [homekitValveType="Irrigation"]} -Switch valve_active "Valve active" (gValve) {homekit = "Valve.ActiveStatus, Valve.InUseStatus"} -Number valve_duration "Valve duration" (gValve) {homekit = "Valve.Duration"} -Number valve_remaining_duration "Valve remaining duration" (gValve) {homekit = "Valve.RemainingDuration"} -``` - -- valve without timer (no item for remaining duration required) - -```xtend -Group gValve "Valve Group" {homekit="Valve" [homekitValveType="Irrigation", homekitTimer="true]} -Switch valve_active "Valve active" (gValve) {homekit = "Valve.ActiveStatus, Valve.InUseStatus"} -Number valve_duration "Valve duration" (gValve) {homekit = "Valve.Duration" [homekitDefaultDuration = 1800]} -``` - -### Sensors - -Sensors have typically one mandatory characteristic, e.g. temperature or lead trigger, and several optional characteristics which are typically used for battery powered sensors and/or wireless sensors. -Following table summarizes the optional characteristics supported by sensors. - -| Characteristics | Supported openHAB items | Description | -|:-----------------------------|:-------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Name | String | Name of the sensor. This characteristic is interesting only for very specific cases in which the name of accessory is dynamic. if you not sure then you don't need it. | -| ActiveStatus | Switch, Contact | Accessory current working status. "ON"/"OPEN" indicates that the accessory is active and is functioning without any errors. | -| FaultStatus | Switch, Contact | Accessory fault status. "ON"/"OPEN" value indicates that the accessory has experienced a fault that may be interfering with its intended functionality. A value of "OFF"/"CLOSED" indicates that there is no fault. | -| TamperedStatus | Switch, Contact | Accessory tampered status. "ON"/"OPEN" indicates that the accessory has been tampered. Value should return to "OFF"/"CLOSED" when the accessory has been reset to a non-tampered state. | -| BatteryLowStatus | Switch, Contact | Accessory battery status. "ON"/"OPEN" indicates that the battery level of the accessory is low. Value should return to "OFF"/"CLOSED" when the battery charges to a level thats above the low threshold. | - -Examples of sensor definitions. -Sensors without optional characteristics: - -```xtend -Switch leaksensor_single "Leak Sensor" {homekit="LeakSensor"} -Number light_sensor "Light Sensor" {homekit="LightSensor"} -Number temperature_sensor "Temperature Sensor [%.1f C]" {homekit="TemperatureSensor"} -Contact contact_sensor "Contact Sensor" {homekit="ContactSensor"} -Switch occupancy_sensor "Occupancy Sensor" {homekit="OccupancyDetectedState"} -Switch motion_sensor "Motion Sensor" {homekit="MotionSensor"} -Number humidity_sensor "Humidity Sensor" {homekit="HumiditySensor"} -``` - -Sensors with optional characteristics: - -```xtend -Group gLeakSensor "Leak Sensor" {homekit="LeakSensor"} -Switch leaksensor "Leak Sensor State" (gLeakSensor) {homekit="LeakDetectedState"} -Switch leaksensor_bat "Leak Sensor Battery" (gLeakSensor) {homekit="BatteryLowStatus"} -Switch leaksensor_active "Leak Sensor Active" (gLeakSensor) {homekit="ActiveStatus"} -Switch leaksensor_fault "Leak Sensor Fault" (gLeakSensor) {homekit="FaultStatus"} -Switch leaksensor_tampered "Leak Sensor Tampered" (gLeakSensor) {homekit="TamperedStatus"} - -Group gMotionSensor "Motion Sensor" {homekit="MotionSensor"} -Switch motionsensor "Motion Sensor State" (gMotionSensor) {homekit="MotionDetectedState"} -Switch motionsensor_bat "Motion Sensor Battery" (gMotionSensor) {homekit="BatteryLowStatus"} -Switch motionsensor_active "Motion Sensor Active" (gMotionSensor) {homekit="ActiveStatus"} -Switch motionsensor_fault "Motion Sensor Fault" (gMotionSensor) {homekit="FaultStatus"} -Switch motionsensor_tampered "Motion Sensor Tampered" (gMotionSensor) {homekit="TamperedStatus"} -``` ## Common Problems diff --git a/bundles/org.openhab.io.homekit/doc/mode_mapping.png b/bundles/org.openhab.io.homekit/doc/mode_mapping.png new file mode 100644 index 0000000000000000000000000000000000000000..54a08a905c0008c40fdfb0ea62ba45bac3bba630 GIT binary patch literal 52799 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java index 234a25c086..ae164900b5 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/HomekitAuthInfoImpl.java @@ -58,7 +58,10 @@ public class HomekitAuthInfoImpl implements HomekitAuthInfo { @Override public void createUser(String username, byte[] publicKey) { logger.trace("Create user {}", username); - storage.put(createUserKey(username), Base64.getEncoder().encodeToString(publicKey)); + final String userKey = createUserKey(username); + final String encodedPublicKey = Base64.getEncoder().encodeToString(publicKey); + storage.put(userKey, encodedPublicKey); + logger.trace("Stored user key {} with value {}", userKey, encodedPublicKey); } @Override diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java index 520c156791..9791afcf07 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/AbstractHomekitAccessoryImpl.java @@ -218,20 +218,32 @@ abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory { * * @param characteristicType characteristicType to identify item * @param map mapping to update + * @param customEnumList list to store custom state enumeration */ @NonNullByDefault - protected void updateMapping(HomekitCharacteristicType characteristicType, Map map) { + protected void updateMapping(HomekitCharacteristicType characteristicType, Map map, + @Nullable List customEnumList) { getCharacteristic(characteristicType).ifPresent(c -> { final Map configuration = c.getConfiguration(); if (configuration != null) { - map.replaceAll((k, current_value) -> { - final Object new_value = configuration.get(current_value); - return (new_value instanceof String) ? (String) new_value : current_value; + map.forEach((k, current_value) -> { + final Object new_value = configuration.get(k.toString()); + if (new_value instanceof String) { + map.put(k, (String) new_value); + if (customEnumList != null) { + customEnumList.add(k); + } + } }); } }); } + @NonNullByDefault + protected void updateMapping(HomekitCharacteristicType characteristicType, Map map) { + updateMapping(characteristicType, map, null); + } + /** * takes item state as value and retrieves the key for that value from mapping. * e.g. used to map StringItem value to HomeKit Enum diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java index 038b0affc1..3aa396261a 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitHeaterCoolerImpl.java @@ -16,6 +16,7 @@ import static org.openhab.io.homekit.internal.HomekitCharacteristicType.ACTIVE_S import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CURRENT_HEATER_COOLER_STATE; import static org.openhab.io.homekit.internal.HomekitCharacteristicType.TARGET_HEATER_COOLER_STATE; +import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; @@ -71,13 +72,16 @@ public class HomekitHeaterCoolerImpl extends AbstractHomekitAccessoryImpl implem } }; + private final List customCurrentStateList = new ArrayList<>(); + private final List customTargetStateList = new ArrayList<>(); + public HomekitHeaterCoolerImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) throws IncompleteAccessoryException { super(taggedItem, mandatoryCharacteristics, updater, settings); activeReader = new BooleanItemReader(getItem(ACTIVE_STATUS, GenericItem.class) .orElseThrow(() -> new IncompleteAccessoryException(ACTIVE_STATUS)), OnOffType.ON, OpenClosedType.OPEN); - updateMapping(CURRENT_HEATER_COOLER_STATE, currentStateMapping); - updateMapping(TARGET_HEATER_COOLER_STATE, targetStateMapping); + updateMapping(CURRENT_HEATER_COOLER_STATE, currentStateMapping, customCurrentStateList); + updateMapping(TARGET_HEATER_COOLER_STATE, targetStateMapping, customTargetStateList); final HeaterCoolerService service = new HeaterCoolerService(this); service.addOptionalCharacteristic(new TemperatureDisplayUnitCharacteristic(this::getTemperatureDisplayUnit, this::setTemperatureDisplayUnit, this::subscribeTemperatureDisplayUnit, @@ -85,6 +89,19 @@ public class HomekitHeaterCoolerImpl extends AbstractHomekitAccessoryImpl implem getServices().add(service); } + @Override + public CurrentHeaterCoolerStateEnum[] getCurrentHeaterCoolerStateValidValues() { + return customCurrentStateList.isEmpty() + ? currentStateMapping.keySet().toArray(new CurrentHeaterCoolerStateEnum[0]) + : customCurrentStateList.toArray(new CurrentHeaterCoolerStateEnum[0]); + } + + @Override + public TargetHeaterCoolerStateEnum[] getTargetHeaterCoolerStateValidValues() { + return customTargetStateList.isEmpty() ? targetStateMapping.keySet().toArray(new TargetHeaterCoolerStateEnum[0]) + : customTargetStateList.toArray(new TargetHeaterCoolerStateEnum[0]); + } + @Override public CompletableFuture getCurrentTemperature() { final @Nullable DecimalType state = getStateAs(HomekitCharacteristicType.CURRENT_TEMPERATURE, diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSecuritySystemImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSecuritySystemImpl.java index 433d8b280d..22604127ea 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSecuritySystemImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitSecuritySystemImpl.java @@ -15,6 +15,7 @@ package org.openhab.io.homekit.internal.accessories; import static org.openhab.io.homekit.internal.HomekitCharacteristicType.SECURITY_SYSTEM_CURRENT_STATE; import static org.openhab.io.homekit.internal.HomekitCharacteristicType.SECURITY_SYSTEM_TARGET_STATE; +import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; @@ -62,15 +63,33 @@ public class HomekitSecuritySystemImpl extends AbstractHomekitAccessoryImpl impl put(TargetSecuritySystemStateEnum.NIGHT_ARM, "NIGHT_ARM"); } }; + private final List customCurrentStateList; + private final List customTargetStateList; public HomekitSecuritySystemImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); - updateMapping(SECURITY_SYSTEM_CURRENT_STATE, currentStateMapping); - updateMapping(SECURITY_SYSTEM_TARGET_STATE, targetStateMapping); + customCurrentStateList = new ArrayList<>(); + customTargetStateList = new ArrayList<>(); + updateMapping(SECURITY_SYSTEM_CURRENT_STATE, currentStateMapping, customCurrentStateList); + updateMapping(SECURITY_SYSTEM_TARGET_STATE, targetStateMapping, customTargetStateList); getServices().add(new SecuritySystemService(this)); } + @Override + public CurrentSecuritySystemStateEnum[] getCurrentSecuritySystemStateValidValues() { + return customCurrentStateList.isEmpty() + ? currentStateMapping.keySet().toArray(new CurrentSecuritySystemStateEnum[0]) + : customCurrentStateList.toArray(new CurrentSecuritySystemStateEnum[0]); + } + + @Override + public TargetSecuritySystemStateEnum[] getTargetSecuritySystemStateValidValues() { + return customTargetStateList.isEmpty() + ? targetStateMapping.keySet().toArray(new TargetSecuritySystemStateEnum[0]) + : customTargetStateList.toArray(new TargetSecuritySystemStateEnum[0]); + } + @Override public CompletableFuture getCurrentSecuritySystemState() { return CompletableFuture.completedFuture(getKeyFromMapping(SECURITY_SYSTEM_CURRENT_STATE, currentStateMapping, diff --git a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java index 36cc9137a2..8f997a86d1 100644 --- a/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java +++ b/bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java @@ -12,8 +12,14 @@ */ package org.openhab.io.homekit.internal.accessories; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.CURRENT_HEATING_COOLING_STATE; +import static org.openhab.io.homekit.internal.HomekitCharacteristicType.TARGET_HEATING_COOLING_STATE; + import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -49,43 +55,52 @@ import io.github.hapjava.services.impl.ThermostatService; */ class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl implements ThermostatAccessory { private final Logger logger = LoggerFactory.getLogger(HomekitThermostatImpl.class); + private final Map currentHeatingCoolingStateMapping; + private final Map targetHeatingCoolingStateMapping; + private final List customCurrentHeatingCoolingStateList; + private final List customTargetHeatingCoolingStateList; public HomekitThermostatImpl(HomekitTaggedItem taggedItem, List mandatoryCharacteristics, HomekitAccessoryUpdater updater, HomekitSettings settings) { super(taggedItem, mandatoryCharacteristics, updater, settings); + currentHeatingCoolingStateMapping = new EnumMap<>(CurrentHeatingCoolingStateEnum.class); + currentHeatingCoolingStateMapping.put(CurrentHeatingCoolingStateEnum.OFF, settings.thermostatCurrentModeOff); + currentHeatingCoolingStateMapping.put(CurrentHeatingCoolingStateEnum.COOL, + settings.thermostatCurrentModeCooling); + currentHeatingCoolingStateMapping.put(CurrentHeatingCoolingStateEnum.HEAT, + settings.thermostatCurrentModeHeating); + targetHeatingCoolingStateMapping = new EnumMap<>(TargetHeatingCoolingStateEnum.class); + targetHeatingCoolingStateMapping.put(TargetHeatingCoolingStateEnum.OFF, settings.thermostatTargetModeOff); + targetHeatingCoolingStateMapping.put(TargetHeatingCoolingStateEnum.COOL, settings.thermostatTargetModeCool); + targetHeatingCoolingStateMapping.put(TargetHeatingCoolingStateEnum.HEAT, settings.thermostatTargetModeHeat); + targetHeatingCoolingStateMapping.put(TargetHeatingCoolingStateEnum.AUTO, settings.thermostatTargetModeAuto); + customCurrentHeatingCoolingStateList = new ArrayList<>(); + customTargetHeatingCoolingStateList = new ArrayList<>(); + updateMapping(CURRENT_HEATING_COOLING_STATE, currentHeatingCoolingStateMapping, + customCurrentHeatingCoolingStateList); + updateMapping(TARGET_HEATING_COOLING_STATE, targetHeatingCoolingStateMapping, + customTargetHeatingCoolingStateList); this.getServices().add(new ThermostatService(this)); } + @Override + public CurrentHeatingCoolingStateEnum[] getCurrentHeatingCoolingStateValidValues() { + return customCurrentHeatingCoolingStateList.isEmpty() + ? currentHeatingCoolingStateMapping.keySet().toArray(new CurrentHeatingCoolingStateEnum[0]) + : customCurrentHeatingCoolingStateList.toArray(new CurrentHeatingCoolingStateEnum[0]); + } + + @Override + public TargetHeatingCoolingStateEnum[] getTargetHeatingCoolingStateValidValues() { + return customTargetHeatingCoolingStateList.isEmpty() + ? targetHeatingCoolingStateMapping.keySet().toArray(new TargetHeatingCoolingStateEnum[0]) + : customTargetHeatingCoolingStateList.toArray(new TargetHeatingCoolingStateEnum[0]); + } + @Override public CompletableFuture getCurrentState() { - final HomekitSettings settings = getSettings(); - String stringValue = settings.thermostatCurrentModeOff; - final Optional characteristic = getCharacteristic( - HomekitCharacteristicType.CURRENT_HEATING_COOLING_STATE); - if (characteristic.isPresent()) { - stringValue = characteristic.get().getItem().getState().toString(); - } else { - logger.warn("Missing mandatory characteristic {}", HomekitCharacteristicType.CURRENT_HEATING_COOLING_STATE); - } - - CurrentHeatingCoolingStateEnum mode; - - if (stringValue.equalsIgnoreCase(settings.thermostatCurrentModeCooling)) { - mode = CurrentHeatingCoolingStateEnum.COOL; - } else if (stringValue.equalsIgnoreCase(settings.thermostatCurrentModeHeating)) { - mode = CurrentHeatingCoolingStateEnum.HEAT; - } else if (stringValue.equalsIgnoreCase(settings.thermostatCurrentModeOff)) { - mode = CurrentHeatingCoolingStateEnum.OFF; - } else if (stringValue.equals("UNDEF") || stringValue.equals("NULL")) { - logger.warn("Heating cooling current mode not available. Relaying value of OFF to Homekit"); - mode = CurrentHeatingCoolingStateEnum.OFF; - } else { - logger.warn("Unrecognized heatingCoolingCurrentMode: {}. Expected {}, {}, or {} strings in value.", - stringValue, settings.thermostatCurrentModeCooling, settings.thermostatCurrentModeHeating, - settings.thermostatCurrentModeOff); - mode = CurrentHeatingCoolingStateEnum.OFF; - } - return CompletableFuture.completedFuture(mode); + return CompletableFuture.completedFuture(getKeyFromMapping(CURRENT_HEATING_COOLING_STATE, + currentHeatingCoolingStateMapping, CurrentHeatingCoolingStateEnum.OFF)); } @Override @@ -114,36 +129,8 @@ class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl implements Ther @Override public CompletableFuture getTargetState() { - final HomekitSettings settings = getSettings(); - String stringValue = settings.thermostatTargetModeOff; - - final Optional characteristic = getCharacteristic( - HomekitCharacteristicType.TARGET_HEATING_COOLING_STATE); - if (characteristic.isPresent()) { - stringValue = characteristic.get().getItem().getState().toString(); - } else { - logger.warn("Missing mandatory characteristic {}", HomekitCharacteristicType.TARGET_HEATING_COOLING_STATE); - } - TargetHeatingCoolingStateEnum mode; - - if (stringValue.equalsIgnoreCase(settings.thermostatTargetModeCool)) { - mode = TargetHeatingCoolingStateEnum.COOL; - } else if (stringValue.equalsIgnoreCase(settings.thermostatTargetModeHeat)) { - mode = TargetHeatingCoolingStateEnum.HEAT; - } else if (stringValue.equalsIgnoreCase(settings.thermostatTargetModeAuto)) { - mode = TargetHeatingCoolingStateEnum.AUTO; - } else if (stringValue.equalsIgnoreCase(settings.thermostatTargetModeOff)) { - mode = TargetHeatingCoolingStateEnum.OFF; - } else if (stringValue.equals("UNDEF") || stringValue.equals("NULL")) { - logger.warn("Heating cooling target mode not available. Relaying value of OFF to Homekit"); - mode = TargetHeatingCoolingStateEnum.OFF; - } else { - logger.warn("Unrecognized heating cooling target mode: {}. Expected {}, {}, {}, or {} strings in value.", - stringValue, settings.thermostatTargetModeCool, settings.thermostatTargetModeHeat, - settings.thermostatTargetModeAuto, settings.thermostatTargetModeOff); - mode = TargetHeatingCoolingStateEnum.OFF; - } - return CompletableFuture.completedFuture(mode); + return CompletableFuture.completedFuture(getKeyFromMapping(TARGET_HEATING_COOLING_STATE, + targetHeatingCoolingStateMapping, TargetHeatingCoolingStateEnum.OFF)); } @Override @@ -166,32 +153,8 @@ class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl implements Ther @Override public void setTargetState(TargetHeatingCoolingStateEnum mode) { - final HomekitSettings settings = getSettings(); - String modeString = null; - switch (mode) { - case AUTO: - modeString = settings.thermostatTargetModeAuto; - break; - - case COOL: - modeString = settings.thermostatTargetModeCool; - break; - - case HEAT: - modeString = settings.thermostatTargetModeHeat; - break; - - case OFF: - modeString = settings.thermostatTargetModeOff; - break; - } - final Optional characteristic = getCharacteristic( - HomekitCharacteristicType.TARGET_HEATING_COOLING_STATE); - if (characteristic.isPresent()) { - ((StringItem) characteristic.get().getItem()).send(new StringType(modeString)); - } else { - logger.warn("Missing mandatory characteristic {}", HomekitCharacteristicType.TARGET_HEATING_COOLING_STATE); - } + getItem(TARGET_HEATING_COOLING_STATE, StringItem.class) + .ifPresent(item -> item.send(new StringType(targetHeatingCoolingStateMapping.get(mode)))); } @Override @@ -226,7 +189,7 @@ class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl implements Ther @Override public void subscribeCurrentState(HomekitCharacteristicChangeCallback callback) { - subscribe(HomekitCharacteristicType.CURRENT_HEATING_COOLING_STATE, callback); + subscribe(CURRENT_HEATING_COOLING_STATE, callback); } @Override @@ -251,7 +214,7 @@ class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl implements Ther @Override public void unsubscribeCurrentState() { - unsubscribe(HomekitCharacteristicType.CURRENT_HEATING_COOLING_STATE); + unsubscribe(CURRENT_HEATING_COOLING_STATE); } @Override