2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.homekit.internal.accessories;
15 import static org.openhab.io.homekit.internal.HomekitCharacteristicType.*;
17 import java.util.Arrays;
18 import java.util.List;
19 import java.util.Optional;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.concurrent.ExecutionException;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.core.items.GenericItem;
26 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
27 import org.openhab.io.homekit.internal.HomekitException;
28 import org.openhab.io.homekit.internal.HomekitSettings;
29 import org.openhab.io.homekit.internal.HomekitTaggedItem;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
33 import io.github.hapjava.characteristics.Characteristic;
34 import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
35 import io.github.hapjava.characteristics.impl.thermostat.CoolingThresholdTemperatureCharacteristic;
36 import io.github.hapjava.characteristics.impl.thermostat.CurrentHeatingCoolingStateCharacteristic;
37 import io.github.hapjava.characteristics.impl.thermostat.CurrentTemperatureCharacteristic;
38 import io.github.hapjava.characteristics.impl.thermostat.HeatingThresholdTemperatureCharacteristic;
39 import io.github.hapjava.characteristics.impl.thermostat.TargetHeatingCoolingStateCharacteristic;
40 import io.github.hapjava.characteristics.impl.thermostat.TargetHeatingCoolingStateEnum;
41 import io.github.hapjava.characteristics.impl.thermostat.TargetTemperatureCharacteristic;
42 import io.github.hapjava.characteristics.impl.thermostat.TemperatureDisplayUnitCharacteristic;
43 import io.github.hapjava.services.impl.ThermostatService;
46 * Implements Thermostat as a GroupedAccessory made up of multiple items:
48 * <li>Current Temperature: Number type</li>
49 * <li>Target Temperature: Number type</li>
50 * <li>Current Heating/Cooling Mode: String type (see HomekitSettings.thermostat*Mode)</li>
51 * <li>Target Heating/Cooling Mode: String type (see HomekitSettings.thermostat*Mode)</li>
54 * @author Andy Lintner - Initial contribution
57 class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl {
58 private final Logger logger = LoggerFactory.getLogger(HomekitThermostatImpl.class);
59 private @Nullable HomekitCharacteristicChangeCallback targetTemperatureCallback = null;
61 public HomekitThermostatImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
62 List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater,
63 HomekitSettings settings) {
64 super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
68 public void init() throws HomekitException {
71 var coolingThresholdTemperatureCharacteristic = getCharacteristic(
72 CoolingThresholdTemperatureCharacteristic.class);
73 var heatingThresholdTemperatureCharacteristic = getCharacteristic(
74 HeatingThresholdTemperatureCharacteristic.class);
75 var targetTemperatureCharacteristic = getCharacteristic(TargetTemperatureCharacteristic.class);
77 if (!coolingThresholdTemperatureCharacteristic.isPresent()
78 && !heatingThresholdTemperatureCharacteristic.isPresent()
79 && !targetTemperatureCharacteristic.isPresent()) {
80 throw new HomekitException(
81 "Unable to create thermostat; at least one of TargetTemperature, CoolingThresholdTemperature, or HeatingThresholdTemperature is required.");
84 var targetHeatingCoolingStateCharacteristic = getCharacteristic(TargetHeatingCoolingStateCharacteristic.class)
86 if (Arrays.stream(targetHeatingCoolingStateCharacteristic.getValidValues())
87 .anyMatch(v -> v.equals(TargetHeatingCoolingStateEnum.AUTO))
88 && (!coolingThresholdTemperatureCharacteristic.isPresent()
89 || !heatingThresholdTemperatureCharacteristic.isPresent())) {
90 throw new HomekitException(
91 "Both HeatingThresholdTemperature and CoolingThresholdTemperature must be provided if AUTO mode is allowed.");
94 // TargetTemperature not provided; simulate by forwarding to HeatingThresholdTemperature and
95 // CoolingThresholdTemperature
97 if (!targetTemperatureCharacteristic.isPresent()) {
98 if (Arrays.stream(targetHeatingCoolingStateCharacteristic.getValidValues())
99 .anyMatch(v -> v.equals(TargetHeatingCoolingStateEnum.HEAT))
100 && !heatingThresholdTemperatureCharacteristic.isPresent()) {
101 throw new HomekitException(
102 "HeatingThresholdTemperature must be provided if HEAT mode is allowed and TargetTemperature is not provided.");
104 if (Arrays.stream(targetHeatingCoolingStateCharacteristic.getValidValues())
105 .anyMatch(v -> v.equals(TargetHeatingCoolingStateEnum.COOL))
106 && !coolingThresholdTemperatureCharacteristic.isPresent()) {
107 throw new HomekitException(
108 "CoolingThresholdTemperature must be provided if COOL mode is allowed and TargetTemperature is not provided.");
111 double minValue, maxValue, minStep;
112 if (coolingThresholdTemperatureCharacteristic.isPresent()
113 && heatingThresholdTemperatureCharacteristic.isPresent()) {
114 minValue = Math.min(coolingThresholdTemperatureCharacteristic.get().getMinValue(),
115 heatingThresholdTemperatureCharacteristic.get().getMinValue());
116 maxValue = Math.max(coolingThresholdTemperatureCharacteristic.get().getMaxValue(),
117 heatingThresholdTemperatureCharacteristic.get().getMaxValue());
118 minStep = Math.min(coolingThresholdTemperatureCharacteristic.get().getMinStep(),
119 heatingThresholdTemperatureCharacteristic.get().getMinStep());
120 } else if (coolingThresholdTemperatureCharacteristic.isPresent()) {
121 minValue = coolingThresholdTemperatureCharacteristic.get().getMinValue();
122 maxValue = coolingThresholdTemperatureCharacteristic.get().getMaxValue();
123 minStep = coolingThresholdTemperatureCharacteristic.get().getMinStep();
125 minValue = heatingThresholdTemperatureCharacteristic.get().getMinValue();
126 maxValue = heatingThresholdTemperatureCharacteristic.get().getMaxValue();
127 minStep = heatingThresholdTemperatureCharacteristic.get().getMinStep();
129 targetTemperatureCharacteristic = Optional
130 .of(new TargetTemperatureCharacteristic(minValue, maxValue, minStep, () -> {
131 // return the value from the characteristic corresponding to the current mode
133 switch (targetHeatingCoolingStateCharacteristic.getEnumValue().get()) {
135 return heatingThresholdTemperatureCharacteristic.get().getValue();
137 return coolingThresholdTemperatureCharacteristic.get().getValue();
139 return CompletableFuture.completedFuture(
140 (heatingThresholdTemperatureCharacteristic.get().getValue().get()
141 + coolingThresholdTemperatureCharacteristic.get().getValue().get())
144 } catch (InterruptedException | ExecutionException e) {
149 // set the charactestic corresponding to the current mode
150 switch (targetHeatingCoolingStateCharacteristic.getEnumValue().get()) {
152 heatingThresholdTemperatureCharacteristic.get().setValue(value);
155 coolingThresholdTemperatureCharacteristic.get().setValue(value);
160 } catch (InterruptedException | ExecutionException e) {
161 // can't happen, since the futures are synchronous
164 targetTemperatureCallback = cb;
165 if (heatingThresholdTemperatureCharacteristic.isPresent()) {
166 getUpdater().subscribe(
167 (GenericItem) getCharacteristic(HEATING_THRESHOLD_TEMPERATURE).get().getItem(),
168 TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
170 if (coolingThresholdTemperatureCharacteristic.isPresent()) {
171 getUpdater().subscribe(
172 (GenericItem) getCharacteristic(COOLING_THRESHOLD_TEMPERATURE).get().getItem(),
173 TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
175 getUpdater().subscribe(
176 (GenericItem) getCharacteristic(TARGET_HEATING_COOLING_STATE).get().getItem(),
177 TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
179 if (heatingThresholdTemperatureCharacteristic.isPresent()) {
180 getUpdater().unsubscribe(
181 (GenericItem) getCharacteristic(HEATING_THRESHOLD_TEMPERATURE).get().getItem(),
182 TARGET_TEMPERATURE.getTag());
184 if (coolingThresholdTemperatureCharacteristic.isPresent()) {
185 getUpdater().unsubscribe(
186 (GenericItem) getCharacteristic(COOLING_THRESHOLD_TEMPERATURE).get().getItem(),
187 TARGET_TEMPERATURE.getTag());
189 getUpdater().unsubscribe(
190 (GenericItem) getCharacteristic(TARGET_HEATING_COOLING_STATE).get().getItem(),
191 TARGET_TEMPERATURE.getTag());
192 targetTemperatureCallback = null;
196 // This characteristic is technically mandatory, but we provide a default if it's not provided
197 var displayUnitCharacteristic = getCharacteristic(TemperatureDisplayUnitCharacteristic.class)
198 .orElseGet(() -> HomekitCharacteristicFactory.createSystemTemperatureDisplayUnitCharacteristic());
200 addService(new ThermostatService(getCharacteristic(CurrentHeatingCoolingStateCharacteristic.class).get(),
201 targetHeatingCoolingStateCharacteristic,
202 getCharacteristic(CurrentTemperatureCharacteristic.class).get(), targetTemperatureCharacteristic.get(),
203 displayUnitCharacteristic));
206 private void thresholdTemperatureChanged() {
207 targetTemperatureCallback.changed();