]> git.basschouten.com Git - openhab-addons.git/commitdiff
[homekit] Synthesize Thermostat.TargetTemperature in some cases (#17060)
authorCody Cutrer <cody@cutrer.us>
Mon, 22 Jul 2024 18:16:47 +0000 (12:16 -0600)
committerGitHub <noreply@github.com>
Mon, 22 Jul 2024 18:16:47 +0000 (20:16 +0200)
* [homekit] synthesize Thermostat.TargetTemperature in some cases

Signed-off-by: Cody Cutrer <cody@cutrer.us>
bundles/org.openhab.io.homekit/README.md
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitAccessoryFactory.java
bundles/org.openhab.io.homekit/src/main/java/org/openhab/io/homekit/internal/accessories/HomekitThermostatImpl.java

index f1b539848599dbc2ca93bbbadd517850dfd3ab76..16c263a3c3533ec5b4b2311bcef98299aa56283a 100644 (file)
@@ -508,6 +508,10 @@ String          thermostat_target_mode     "Thermostat Target Mode"
 ```
 
 In addition, thermostat can have thresholds for cooling and heating modes.
+When a thermostat is configured with all three of TargetTemperature, HeatingThresholdTemperature, and CoolingThresholdTemperature, Home will set the characteristics as follows:
+ * TargetTemperature is used when the thermostat is in HEAT or COOL TargetHeatingCoolingMode.
+ * CoolingThresholdThemperature and HeatingThresholdTemperature are _only_ used in AUTO TargetHeatingCoolingMode.
+ * In AUTO TargetHeatingCoolingMode, TargetTemperature will be set to the average of CoolingThresholdThemperature and HeatingThresholdTemperature.
 Example with thresholds:
 
 ```xtend
@@ -520,6 +524,17 @@ Number          thermostat_cool_thrs       "Thermostat Cool Threshold Temp [%.1f
 Number          thermostat_heat_thrs       "Thermostat Heat Threshold Temp [%.1f °C]" (gThermostat) {homekit = "HeatingThresholdTemperature"}
 ```
 
+If your thermostat simply has a heating set point and cooling set point, and uses those points regardless of HEAT, COOL, or AUTO mode, you may simply omit the TargetTemperature characteristic, and the add-on will dynamically route to those characteristics as appropriate:
+
+```java
+Group           gThermostat                "Thermostat"                                             {homekit = "Thermostat"}
+Number          thermostat_current_temp    "Thermostat Current Temp [%.1f °C]"        (gThermostat) {homekit = "CurrentTemperature"}
+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.
@@ -891,13 +906,13 @@ All accessories also support the following optional characteristic that can be l
 |                      |                             | BatteryLowStatus            | Contact, Number, Switch                 | Battery status                                                                                                                                                                                                                                                                                                                                                | inverted (false), lowThreshold (20)                                   |                                                                                                             |
 |                      |                             | FaultStatus                 | Contact, Number, String, Switch         | Fault status                                                                                                                                                                                                                                                                                                                                                  | inverted (false)                                                      | NO_FAULT (0, OFF, CLOSED), GENERAL_FAULT (1, ON, OPEN)                                                      |
 |                      |                             | TamperedStatus              | Contact, Number, String, Switch         | Tampered status                                                                                                                                                                                                                                                                                                                                               | inverted (false)                                                      | NOT_TAMPERED (0, OFF, CLOSED), TAMPERED (1, ON, OPEN)                                                       |
-| Thermostat           |                             |                             |                                         | A thermostat requires all mandatory characteristics defined below                                                                                                                                                                                                                                                                                             |                                                                       |                                                                                                             |
+| Thermostat           |                             |                             |                                         | A thermostat requires at least one of TargetTemperature, CoolingThresholdTemperature, or HeatingThresholdTemperature must be provided.                                                                                                                                                                                                                        |                                                                       |                                                                                                             |
 |                      | CurrentHeatingCoolingMode   |                             | Number, String                          | Current heating cooling mode                                                                                                                                                                                                                                                                                                                                  |                                                                       | OFF (0, OFF), HEAT (1, ON), COOL (2)                                                                        |
 |                      | CurrentTemperature          |                             | Number                                  | Current temperature.                                                                                                                                                                                                                                                                                                                                          | minValue (0), maxValue (100), step (0.1)                              |                                                                                                             |
 |                      | TargetHeatingCoolingMode    |                             | Number, String                          | Target heating cooling mode                                                                                                                                                                                                                                                                                                                                   |                                                                       | OFF (0, OFF), HEAT (1, ON), COOL (2), AUTO (3) [*](#customizable-enum)                                      |
-|                      | TargetTemperature           |                             | Number                                  | Target temperature.                                                                                                                                                                                                                                                                                                                                           | minValue (10), maxValue (38), step (0.1)                              |                                                                                                             |
-|                      |                             | CoolingThresholdTemperature | Number                                  | Maximum temperature that must be reached before cooling is turned on                                                                                                                                                                                                                                                                                          | minValue (10), maxValue (35), step (0.1)                              |                                                                                                             |
-|                      |                             | HeatingThresholdTemperature | Number                                  | Minimum temperature that must be reached before heating is turned on                                                                                                                                                                                                                                                                                          | minValue (0), maxValue (25), step (0.1)                               |                                                                                                             |
+|                      |                             | TargetTemperature           | Number                                  | Target temperature. If CoolingThresholdTemperature and HeatingThresholdTemperature are also provided, this characteristic is used when the thermostat is in HEAT or COOL mode. In AUTO mode, this characteristic receives the average of the two thresholds.                                                                                                  | minValue (10), maxValue (38), step (0.1)                              |                                                                                                             |
+|                      |                             | CoolingThresholdTemperature | Number                                  | Maximum temperature that must be reached before cooling is turned on. If TargetTemperature is not provided, this characteristic will also be used in COOL mode.                                                                                                                                                                                               | minValue (10), maxValue (35), step (0.1)                              |                                                                                                             |
+|                      |                             | HeatingThresholdTemperature | Number                                  | Minimum temperature that must be reached before heating is turned on. If TargetTemperature is not provided, this characteristic will also be used in HEAT mode.                                                                                                                                                                                               | minValue (0), maxValue (25), step (0.1)                               |                                                                                                             |
 |                      |                             | RelativeHumidity            | Number                                  | Relative humidity in % between 0 and 100.                                                                                                                                                                                                                                                                                                                     |                                                                       |                                                                                                             |
 |                      |                             | TargetRelativeHumidity      | Number                                  | Target relative humidity in % between 0 and 100.                                                                                                                                                                                                                                                                                                              |                                                                       |                                                                                                             |
 |                      |                             | TemperatureUnit             | Number, String, Switch                  | The units the accessory itself uses to display the temperature. Can also be configured via metadata, e.g. [TemperatureUnit="CELSIUS"]                                                                                                                                                                                                                         |                                                                       | CELSIUS (0, OFF), FAHRENHEIT (1, ON)                                                                        |
index ad14edb65f6ffd5994a8a4665c1ef1475c21a25e..6dbca81247194d79d27f76c47857ca9ca4f6d96f 100644 (file)
@@ -85,7 +85,7 @@ public class HomekitAccessoryFactory {
             put(LIGHT_SENSOR, new HomekitCharacteristicType[] { LIGHT_LEVEL });
             put(TEMPERATURE_SENSOR, new HomekitCharacteristicType[] { CURRENT_TEMPERATURE });
             put(THERMOSTAT, new HomekitCharacteristicType[] { CURRENT_HEATING_COOLING_STATE,
-                    TARGET_HEATING_COOLING_STATE, CURRENT_TEMPERATURE, TARGET_TEMPERATURE });
+                    TARGET_HEATING_COOLING_STATE, CURRENT_TEMPERATURE });
             put(LOCK, new HomekitCharacteristicType[] { LOCK_CURRENT_STATE, LOCK_TARGET_STATE });
             put(VALVE, new HomekitCharacteristicType[] { ACTIVE_STATUS, INUSE_STATUS });
             put(SECURITY_SYSTEM,
index b3b6e756ffdda9bbf8beb883eeeb13664e81c0ec..b1bbb1e0976864c194cfa3413d6f32d943772ec3 100644 (file)
@@ -14,8 +14,15 @@ package org.openhab.io.homekit.internal.accessories;
 
 import static org.openhab.io.homekit.internal.HomekitCharacteristicType.*;
 
+import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.items.GenericItem;
 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
 import org.openhab.io.homekit.internal.HomekitException;
 import org.openhab.io.homekit.internal.HomekitSettings;
@@ -24,9 +31,13 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import io.github.hapjava.characteristics.Characteristic;
+import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
+import io.github.hapjava.characteristics.impl.thermostat.CoolingThresholdTemperatureCharacteristic;
 import io.github.hapjava.characteristics.impl.thermostat.CurrentHeatingCoolingStateCharacteristic;
 import io.github.hapjava.characteristics.impl.thermostat.CurrentTemperatureCharacteristic;
+import io.github.hapjava.characteristics.impl.thermostat.HeatingThresholdTemperatureCharacteristic;
 import io.github.hapjava.characteristics.impl.thermostat.TargetHeatingCoolingStateCharacteristic;
+import io.github.hapjava.characteristics.impl.thermostat.TargetHeatingCoolingStateEnum;
 import io.github.hapjava.characteristics.impl.thermostat.TargetTemperatureCharacteristic;
 import io.github.hapjava.characteristics.impl.thermostat.TemperatureDisplayUnitCharacteristic;
 import io.github.hapjava.services.impl.ThermostatService;
@@ -42,8 +53,10 @@ import io.github.hapjava.services.impl.ThermostatService;
  *
  * @author Andy Lintner - Initial contribution
  */
+@NonNullByDefault
 class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl {
     private final Logger logger = LoggerFactory.getLogger(HomekitThermostatImpl.class);
+    private @Nullable HomekitCharacteristicChangeCallback targetTemperatureCallback = null;
 
     public HomekitThermostatImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
             List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater,
@@ -55,13 +68,142 @@ class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl {
     public void init() throws HomekitException {
         super.init();
 
+        var coolingThresholdTemperatureCharacteristic = getCharacteristic(
+                CoolingThresholdTemperatureCharacteristic.class);
+        var heatingThresholdTemperatureCharacteristic = getCharacteristic(
+                HeatingThresholdTemperatureCharacteristic.class);
+        var targetTemperatureCharacteristic = getCharacteristic(TargetTemperatureCharacteristic.class);
+
+        if (!coolingThresholdTemperatureCharacteristic.isPresent()
+                && !heatingThresholdTemperatureCharacteristic.isPresent()
+                && !targetTemperatureCharacteristic.isPresent()) {
+            throw new HomekitException(
+                    "Unable to create thermostat; at least one of TargetTemperature, CoolingThresholdTemperature, or HeatingThresholdTemperature is required.");
+        }
+
+        var targetHeatingCoolingStateCharacteristic = getCharacteristic(TargetHeatingCoolingStateCharacteristic.class)
+                .get();
+        if (Arrays.stream(targetHeatingCoolingStateCharacteristic.getValidValues())
+                .anyMatch(v -> v.equals(TargetHeatingCoolingStateEnum.AUTO))
+                && (!coolingThresholdTemperatureCharacteristic.isPresent()
+                        || !heatingThresholdTemperatureCharacteristic.isPresent())) {
+            throw new HomekitException(
+                    "Both HeatingThresholdTemperature and CoolingThresholdTemperature must be provided if AUTO mode is allowed.");
+        }
+
+        // TargetTemperature not provided; simulate by forwarding to HeatingThresholdTemperature and
+        // CoolingThresholdTemperature
+        // as appropriate
+        if (!targetTemperatureCharacteristic.isPresent()) {
+            if (Arrays.stream(targetHeatingCoolingStateCharacteristic.getValidValues())
+                    .anyMatch(v -> v.equals(TargetHeatingCoolingStateEnum.HEAT))
+                    && !heatingThresholdTemperatureCharacteristic.isPresent()) {
+                throw new HomekitException(
+                        "HeatingThresholdTemperature must be provided if HEAT mode is allowed and TargetTemperature is not provided.");
+            }
+            if (Arrays.stream(targetHeatingCoolingStateCharacteristic.getValidValues())
+                    .anyMatch(v -> v.equals(TargetHeatingCoolingStateEnum.COOL))
+                    && !coolingThresholdTemperatureCharacteristic.isPresent()) {
+                throw new HomekitException(
+                        "CoolingThresholdTemperature must be provided if COOL mode is allowed and TargetTemperature is not provided.");
+            }
+
+            double minValue, maxValue, minStep;
+            if (coolingThresholdTemperatureCharacteristic.isPresent()
+                    && heatingThresholdTemperatureCharacteristic.isPresent()) {
+                minValue = Math.min(coolingThresholdTemperatureCharacteristic.get().getMinValue(),
+                        heatingThresholdTemperatureCharacteristic.get().getMinValue());
+                maxValue = Math.max(coolingThresholdTemperatureCharacteristic.get().getMaxValue(),
+                        heatingThresholdTemperatureCharacteristic.get().getMaxValue());
+                minStep = Math.min(coolingThresholdTemperatureCharacteristic.get().getMinStep(),
+                        heatingThresholdTemperatureCharacteristic.get().getMinStep());
+            } else if (coolingThresholdTemperatureCharacteristic.isPresent()) {
+                minValue = coolingThresholdTemperatureCharacteristic.get().getMinValue();
+                maxValue = coolingThresholdTemperatureCharacteristic.get().getMaxValue();
+                minStep = coolingThresholdTemperatureCharacteristic.get().getMinStep();
+            } else {
+                minValue = heatingThresholdTemperatureCharacteristic.get().getMinValue();
+                maxValue = heatingThresholdTemperatureCharacteristic.get().getMaxValue();
+                minStep = heatingThresholdTemperatureCharacteristic.get().getMinStep();
+            }
+            targetTemperatureCharacteristic = Optional
+                    .of(new TargetTemperatureCharacteristic(minValue, maxValue, minStep, () -> {
+                        // return the value from the characteristic corresponding to the current mode
+                        try {
+                            switch (targetHeatingCoolingStateCharacteristic.getEnumValue().get()) {
+                                case HEAT:
+                                    return heatingThresholdTemperatureCharacteristic.get().getValue();
+                                case COOL:
+                                    return coolingThresholdTemperatureCharacteristic.get().getValue();
+                                default:
+                                    return CompletableFuture.completedFuture(
+                                            (heatingThresholdTemperatureCharacteristic.get().getValue().get()
+                                                    + coolingThresholdTemperatureCharacteristic.get().getValue().get())
+                                                    / 2);
+                            }
+                        } catch (InterruptedException | ExecutionException e) {
+                            return null;
+                        }
+                    }, value -> {
+                        try {
+                            // set the charactestic corresponding to the current mode
+                            switch (targetHeatingCoolingStateCharacteristic.getEnumValue().get()) {
+                                case HEAT:
+                                    heatingThresholdTemperatureCharacteristic.get().setValue(value);
+                                    break;
+                                case COOL:
+                                    coolingThresholdTemperatureCharacteristic.get().setValue(value);
+                                    break;
+                                default:
+                                    // ignore
+                            }
+                        } catch (InterruptedException | ExecutionException e) {
+                            // can't happen, since the futures are synchronous
+                        }
+                    }, cb -> {
+                        targetTemperatureCallback = cb;
+                        if (heatingThresholdTemperatureCharacteristic.isPresent()) {
+                            getUpdater().subscribe(
+                                    (GenericItem) getCharacteristic(HEATING_THRESHOLD_TEMPERATURE).get().getItem(),
+                                    TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
+                        }
+                        if (coolingThresholdTemperatureCharacteristic.isPresent()) {
+                            getUpdater().subscribe(
+                                    (GenericItem) getCharacteristic(COOLING_THRESHOLD_TEMPERATURE).get().getItem(),
+                                    TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
+                        }
+                        getUpdater().subscribe(
+                                (GenericItem) getCharacteristic(TARGET_HEATING_COOLING_STATE).get().getItem(),
+                                TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
+                    }, () -> {
+                        if (heatingThresholdTemperatureCharacteristic.isPresent()) {
+                            getUpdater().unsubscribe(
+                                    (GenericItem) getCharacteristic(HEATING_THRESHOLD_TEMPERATURE).get().getItem(),
+                                    TARGET_TEMPERATURE.getTag());
+                        }
+                        if (coolingThresholdTemperatureCharacteristic.isPresent()) {
+                            getUpdater().unsubscribe(
+                                    (GenericItem) getCharacteristic(COOLING_THRESHOLD_TEMPERATURE).get().getItem(),
+                                    TARGET_TEMPERATURE.getTag());
+                        }
+                        getUpdater().unsubscribe(
+                                (GenericItem) getCharacteristic(TARGET_HEATING_COOLING_STATE).get().getItem(),
+                                TARGET_TEMPERATURE.getTag());
+                        targetTemperatureCallback = null;
+                    }));
+        }
+
         // This characteristic is technically mandatory, but we provide a default if it's not provided
         var displayUnitCharacteristic = getCharacteristic(TemperatureDisplayUnitCharacteristic.class)
                 .orElseGet(() -> HomekitCharacteristicFactory.createSystemTemperatureDisplayUnitCharacteristic());
 
         addService(new ThermostatService(getCharacteristic(CurrentHeatingCoolingStateCharacteristic.class).get(),
-                getCharacteristic(TargetHeatingCoolingStateCharacteristic.class).get(),
-                getCharacteristic(CurrentTemperatureCharacteristic.class).get(),
-                getCharacteristic(TargetTemperatureCharacteristic.class).get(), displayUnitCharacteristic));
+                targetHeatingCoolingStateCharacteristic,
+                getCharacteristic(CurrentTemperatureCharacteristic.class).get(), targetTemperatureCharacteristic.get(),
+                displayUnitCharacteristic));
+    }
+
+    private void thresholdTemperatureChanged() {
+        targetTemperatureCallback.changed();
     }
 }