[homekit] Synthesize Thermostat.TargetTemperature in some cases (#17060)

* [homekit] synthesize Thermostat.TargetTemperature in some cases

Signed-off-by: Cody Cutrer <cody@cutrer.us>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Cody Cutrer 2024-07-22 12:16:47 -06:00 committed by Ciprian Pascu
parent 4c494a6064
commit cee789c75f
3 changed files with 165 additions and 8 deletions

View 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) |

View 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,

View 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();
}
}