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)
87 // TargetTemperature not provided; simulate by forwarding to HeatingThresholdTemperature and
88 // CoolingThresholdTemperature
90 if (!targetTemperatureCharacteristic.isPresent()) {
91 if (Arrays.stream(targetHeatingCoolingStateCharacteristic.getValidValues())
92 .anyMatch(v -> v.equals(TargetHeatingCoolingStateEnum.HEAT))
93 && !heatingThresholdTemperatureCharacteristic.isPresent()) {
94 throw new HomekitException(
95 "HeatingThresholdTemperature must be provided if HEAT mode is allowed and TargetTemperature is not provided.");
97 if (Arrays.stream(targetHeatingCoolingStateCharacteristic.getValidValues())
98 .anyMatch(v -> v.equals(TargetHeatingCoolingStateEnum.COOL))
99 && !coolingThresholdTemperatureCharacteristic.isPresent()) {
100 throw new HomekitException(
101 "CoolingThresholdTemperature must be provided if COOL mode is allowed and TargetTemperature is not provided.");
104 double minValue, maxValue, minStep;
105 if (coolingThresholdTemperatureCharacteristic.isPresent()
106 && heatingThresholdTemperatureCharacteristic.isPresent()) {
107 minValue = Math.min(coolingThresholdTemperatureCharacteristic.get().getMinValue(),
108 heatingThresholdTemperatureCharacteristic.get().getMinValue());
109 maxValue = Math.max(coolingThresholdTemperatureCharacteristic.get().getMaxValue(),
110 heatingThresholdTemperatureCharacteristic.get().getMaxValue());
111 minStep = Math.min(coolingThresholdTemperatureCharacteristic.get().getMinStep(),
112 heatingThresholdTemperatureCharacteristic.get().getMinStep());
113 } else if (coolingThresholdTemperatureCharacteristic.isPresent()) {
114 minValue = coolingThresholdTemperatureCharacteristic.get().getMinValue();
115 maxValue = coolingThresholdTemperatureCharacteristic.get().getMaxValue();
116 minStep = coolingThresholdTemperatureCharacteristic.get().getMinStep();
118 minValue = heatingThresholdTemperatureCharacteristic.get().getMinValue();
119 maxValue = heatingThresholdTemperatureCharacteristic.get().getMaxValue();
120 minStep = heatingThresholdTemperatureCharacteristic.get().getMinStep();
122 targetTemperatureCharacteristic = Optional
123 .of(new TargetTemperatureCharacteristic(minValue, maxValue, minStep, () -> {
124 // return the value from the characteristic corresponding to the current mode
126 switch (targetHeatingCoolingStateCharacteristic.getEnumValue().get()) {
128 return heatingThresholdTemperatureCharacteristic.get().getValue();
130 return coolingThresholdTemperatureCharacteristic.get().getValue();
132 return CompletableFuture.completedFuture(
133 (heatingThresholdTemperatureCharacteristic.get().getValue().get()
134 + coolingThresholdTemperatureCharacteristic.get().getValue().get())
137 } catch (InterruptedException | ExecutionException e) {
142 // set the charactestic corresponding to the current mode
143 switch (targetHeatingCoolingStateCharacteristic.getEnumValue().get()) {
145 heatingThresholdTemperatureCharacteristic.get().setValue(value);
148 coolingThresholdTemperatureCharacteristic.get().setValue(value);
153 } catch (InterruptedException | ExecutionException e) {
154 // can't happen, since the futures are synchronous
157 targetTemperatureCallback = cb;
158 if (heatingThresholdTemperatureCharacteristic.isPresent()) {
159 getUpdater().subscribe(
160 (GenericItem) getCharacteristic(HEATING_THRESHOLD_TEMPERATURE).get().getItem(),
161 TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
163 if (coolingThresholdTemperatureCharacteristic.isPresent()) {
164 getUpdater().subscribe(
165 (GenericItem) getCharacteristic(COOLING_THRESHOLD_TEMPERATURE).get().getItem(),
166 TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
168 getUpdater().subscribe(
169 (GenericItem) getCharacteristic(TARGET_HEATING_COOLING_STATE).get().getItem(),
170 TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
172 if (heatingThresholdTemperatureCharacteristic.isPresent()) {
173 getUpdater().unsubscribe(
174 (GenericItem) getCharacteristic(HEATING_THRESHOLD_TEMPERATURE).get().getItem(),
175 TARGET_TEMPERATURE.getTag());
177 if (coolingThresholdTemperatureCharacteristic.isPresent()) {
178 getUpdater().unsubscribe(
179 (GenericItem) getCharacteristic(COOLING_THRESHOLD_TEMPERATURE).get().getItem(),
180 TARGET_TEMPERATURE.getTag());
182 getUpdater().unsubscribe(
183 (GenericItem) getCharacteristic(TARGET_HEATING_COOLING_STATE).get().getItem(),
184 TARGET_TEMPERATURE.getTag());
185 targetTemperatureCallback = null;
189 // This characteristic is technically mandatory, but we provide a default if it's not provided
190 var displayUnitCharacteristic = getCharacteristic(TemperatureDisplayUnitCharacteristic.class)
191 .orElseGet(() -> HomekitCharacteristicFactory.createSystemTemperatureDisplayUnitCharacteristic());
193 addService(new ThermostatService(getCharacteristic(CurrentHeatingCoolingStateCharacteristic.class).get(),
194 targetHeatingCoolingStateCharacteristic,
195 getCharacteristic(CurrentTemperatureCharacteristic.class).get(), targetTemperatureCharacteristic.get(),
196 displayUnitCharacteristic));
199 private void thresholdTemperatureChanged() {
200 targetTemperatureCallback.changed();