]> git.basschouten.com Git - openhab-addons.git/blob
b1bbb1e0976864c194cfa3413d6f32d943772ec3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.io.homekit.internal.accessories;
14
15 import static org.openhab.io.homekit.internal.HomekitCharacteristicType.*;
16
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;
22
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;
32
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;
44
45 /**
46  * Implements Thermostat as a GroupedAccessory made up of multiple items:
47  * <ul>
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>
52  * </ul>
53  *
54  * @author Andy Lintner - Initial contribution
55  */
56 @NonNullByDefault
57 class HomekitThermostatImpl extends AbstractHomekitAccessoryImpl {
58     private final Logger logger = LoggerFactory.getLogger(HomekitThermostatImpl.class);
59     private @Nullable HomekitCharacteristicChangeCallback targetTemperatureCallback = null;
60
61     public HomekitThermostatImpl(HomekitTaggedItem taggedItem, List<HomekitTaggedItem> mandatoryCharacteristics,
62             List<Characteristic> mandatoryRawCharacteristics, HomekitAccessoryUpdater updater,
63             HomekitSettings settings) {
64         super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
65     }
66
67     @Override
68     public void init() throws HomekitException {
69         super.init();
70
71         var coolingThresholdTemperatureCharacteristic = getCharacteristic(
72                 CoolingThresholdTemperatureCharacteristic.class);
73         var heatingThresholdTemperatureCharacteristic = getCharacteristic(
74                 HeatingThresholdTemperatureCharacteristic.class);
75         var targetTemperatureCharacteristic = getCharacteristic(TargetTemperatureCharacteristic.class);
76
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.");
82         }
83
84         var targetHeatingCoolingStateCharacteristic = getCharacteristic(TargetHeatingCoolingStateCharacteristic.class)
85                 .get();
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.");
92         }
93
94         // TargetTemperature not provided; simulate by forwarding to HeatingThresholdTemperature and
95         // CoolingThresholdTemperature
96         // as appropriate
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.");
103             }
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.");
109             }
110
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();
124             } else {
125                 minValue = heatingThresholdTemperatureCharacteristic.get().getMinValue();
126                 maxValue = heatingThresholdTemperatureCharacteristic.get().getMaxValue();
127                 minStep = heatingThresholdTemperatureCharacteristic.get().getMinStep();
128             }
129             targetTemperatureCharacteristic = Optional
130                     .of(new TargetTemperatureCharacteristic(minValue, maxValue, minStep, () -> {
131                         // return the value from the characteristic corresponding to the current mode
132                         try {
133                             switch (targetHeatingCoolingStateCharacteristic.getEnumValue().get()) {
134                                 case HEAT:
135                                     return heatingThresholdTemperatureCharacteristic.get().getValue();
136                                 case COOL:
137                                     return coolingThresholdTemperatureCharacteristic.get().getValue();
138                                 default:
139                                     return CompletableFuture.completedFuture(
140                                             (heatingThresholdTemperatureCharacteristic.get().getValue().get()
141                                                     + coolingThresholdTemperatureCharacteristic.get().getValue().get())
142                                                     / 2);
143                             }
144                         } catch (InterruptedException | ExecutionException e) {
145                             return null;
146                         }
147                     }, value -> {
148                         try {
149                             // set the charactestic corresponding to the current mode
150                             switch (targetHeatingCoolingStateCharacteristic.getEnumValue().get()) {
151                                 case HEAT:
152                                     heatingThresholdTemperatureCharacteristic.get().setValue(value);
153                                     break;
154                                 case COOL:
155                                     coolingThresholdTemperatureCharacteristic.get().setValue(value);
156                                     break;
157                                 default:
158                                     // ignore
159                             }
160                         } catch (InterruptedException | ExecutionException e) {
161                             // can't happen, since the futures are synchronous
162                         }
163                     }, cb -> {
164                         targetTemperatureCallback = cb;
165                         if (heatingThresholdTemperatureCharacteristic.isPresent()) {
166                             getUpdater().subscribe(
167                                     (GenericItem) getCharacteristic(HEATING_THRESHOLD_TEMPERATURE).get().getItem(),
168                                     TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
169                         }
170                         if (coolingThresholdTemperatureCharacteristic.isPresent()) {
171                             getUpdater().subscribe(
172                                     (GenericItem) getCharacteristic(COOLING_THRESHOLD_TEMPERATURE).get().getItem(),
173                                     TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
174                         }
175                         getUpdater().subscribe(
176                                 (GenericItem) getCharacteristic(TARGET_HEATING_COOLING_STATE).get().getItem(),
177                                 TARGET_TEMPERATURE.getTag(), this::thresholdTemperatureChanged);
178                     }, () -> {
179                         if (heatingThresholdTemperatureCharacteristic.isPresent()) {
180                             getUpdater().unsubscribe(
181                                     (GenericItem) getCharacteristic(HEATING_THRESHOLD_TEMPERATURE).get().getItem(),
182                                     TARGET_TEMPERATURE.getTag());
183                         }
184                         if (coolingThresholdTemperatureCharacteristic.isPresent()) {
185                             getUpdater().unsubscribe(
186                                     (GenericItem) getCharacteristic(COOLING_THRESHOLD_TEMPERATURE).get().getItem(),
187                                     TARGET_TEMPERATURE.getTag());
188                         }
189                         getUpdater().unsubscribe(
190                                 (GenericItem) getCharacteristic(TARGET_HEATING_COOLING_STATE).get().getItem(),
191                                 TARGET_TEMPERATURE.getTag());
192                         targetTemperatureCallback = null;
193                     }));
194         }
195
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());
199
200         addService(new ThermostatService(getCharacteristic(CurrentHeatingCoolingStateCharacteristic.class).get(),
201                 targetHeatingCoolingStateCharacteristic,
202                 getCharacteristic(CurrentTemperatureCharacteristic.class).get(), targetTemperatureCharacteristic.get(),
203                 displayUnitCharacteristic));
204     }
205
206     private void thresholdTemperatureChanged() {
207         targetTemperatureCallback.changed();
208     }
209 }