]> git.basschouten.com Git - openhab-addons.git/blob
748cb938b8266ecdf580c87d3b0b3c5ba1c0ce31
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.binding.netatmo.internal.thermostat;
14
15 import static org.openhab.binding.netatmo.internal.APIUtils.*;
16 import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
17 import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
18
19 import java.math.BigDecimal;
20 import java.math.RoundingMode;
21 import java.util.ArrayList;
22 import java.util.Calendar;
23 import java.util.List;
24 import java.util.Optional;
25 import java.util.stream.Stream;
26
27 import javax.measure.quantity.Temperature;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.netatmo.internal.NATherm1StateDescriptionProvider;
32 import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
33 import org.openhab.core.config.core.Configuration;
34 import org.openhab.core.i18n.TimeZoneProvider;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.StateOption;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import io.swagger.client.api.ThermostatApi;
51 import io.swagger.client.model.NASetpoint;
52 import io.swagger.client.model.NAThermProgram;
53 import io.swagger.client.model.NAThermostat;
54 import io.swagger.client.model.NATimeTableItem;
55 import io.swagger.client.model.NAZone;
56
57 /**
58  * {@link NATherm1Handler} is the class used to handle the thermostat
59  * module of a thermostat set
60  *
61  * @author GaĆ«l L'hopital - Initial contribution OH2 version
62  *
63  */
64 @NonNullByDefault
65 public class NATherm1Handler extends NetatmoModuleHandler<NAThermostat> {
66     private final Logger logger = LoggerFactory.getLogger(NATherm1Handler.class);
67     private final NATherm1StateDescriptionProvider stateDescriptionProvider;
68
69     public NATherm1Handler(Thing thing, NATherm1StateDescriptionProvider stateDescriptionProvider,
70             final TimeZoneProvider timeZoneProvider) {
71         super(thing, timeZoneProvider);
72         this.stateDescriptionProvider = stateDescriptionProvider;
73     }
74
75     @Override
76     protected void updateProperties(NAThermostat moduleData) {
77         updateProperties(moduleData.getFirmware(), moduleData.getType());
78     }
79
80     @Override
81     public void updateChannels(Object moduleObject) {
82         super.updateChannels(moduleObject);
83         getModule().ifPresent(this::updateStateDescription);
84     }
85
86     private void updateStateDescription(NAThermostat thermostat) {
87         List<StateOption> options = new ArrayList<>();
88         for (NAThermProgram planning : nonNullList(thermostat.getThermProgramList())) {
89             options.add(new StateOption(planning.getProgramId(), planning.getName()));
90         }
91         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_PLANNING), options);
92     }
93
94     @Override
95     protected State getNAThingProperty(String channelId) {
96         Optional<NAThermostat> thermostat = getModule();
97         switch (channelId) {
98             case CHANNEL_THERM_ORIENTATION:
99                 return thermostat.map(m -> toDecimalType(m.getThermOrientation())).orElse(UnDefType.UNDEF);
100             case CHANNEL_THERM_RELAY:
101                 return thermostat.map(m -> m.getThermRelayCmd() == 100 ? (State) OnOffType.ON : (State) OnOffType.OFF)
102                         .orElse(UnDefType.UNDEF);
103             case CHANNEL_TEMPERATURE:
104                 return thermostat.map(m -> toQuantityType(m.getMeasured().getTemperature(), API_TEMPERATURE_UNIT))
105                         .orElse(UnDefType.UNDEF);
106             case CHANNEL_SETPOINT_TEMP:
107                 return getCurrentSetpoint();
108             case CHANNEL_TIMEUTC:
109                 return thermostat.map(m -> toDateTimeType(m.getMeasured().getTime(), timeZoneProvider.getTimeZone()))
110                         .orElse(UnDefType.UNDEF);
111             case CHANNEL_SETPOINT_END_TIME: {
112                 if (thermostat.isPresent()) {
113                     NASetpoint setpoint = thermostat.get().getSetpoint();
114                     if (setpoint != null) {
115                         Integer endTime = setpoint.getSetpointEndtime();
116                         if (endTime == null) {
117                             endTime = getNextProgramTime(nonNullList(thermostat.get().getThermProgramList()));
118                         }
119                         return toDateTimeType(endTime, timeZoneProvider.getTimeZone());
120                     }
121                     return UnDefType.NULL;
122                 }
123                 return UnDefType.UNDEF;
124             }
125             case CHANNEL_SETPOINT_MODE:
126                 return getSetpoint();
127             case CHANNEL_PLANNING: {
128                 String currentPlanning = "-";
129                 if (thermostat.isPresent()) {
130                     for (NAThermProgram program : nonNullList(thermostat.get().getThermProgramList())) {
131                         if (Boolean.TRUE.equals(program.isSelected())) {
132                             currentPlanning = program.getProgramId();
133                         }
134                     }
135                     return toStringType(currentPlanning);
136                 }
137                 return UnDefType.UNDEF;
138             }
139         }
140         return super.getNAThingProperty(channelId);
141     }
142
143     private State getSetpoint() {
144         return getModule()
145                 .map(m -> m.getSetpoint() != null ? toStringType(m.getSetpoint().getSetpointMode()) : UnDefType.NULL)
146                 .orElse(UnDefType.UNDEF);
147     }
148
149     private State getCurrentSetpoint() {
150         Optional<NAThermostat> thermostat = getModule();
151         if (thermostat.isPresent()) {
152             NASetpoint setPoint = thermostat.get().getSetpoint();
153             if (setPoint != null) {
154                 String currentMode = setPoint.getSetpointMode();
155
156                 NAThermProgram currentProgram = nonNullStream(thermostat.get().getThermProgramList())
157                         .filter(p -> p.isSelected() != null && p.isSelected()).findFirst().get();
158                 switch (currentMode) {
159                     case CHANNEL_SETPOINT_MODE_MANUAL:
160                         return toDecimalType(setPoint.getSetpointTemp());
161                     case CHANNEL_SETPOINT_MODE_AWAY:
162                         NAZone zone = getZone(currentProgram.getZones(), 2);
163                         return toDecimalType(zone.getTemp());
164                     case CHANNEL_SETPOINT_MODE_HG:
165                         NAZone zone1 = getZone(currentProgram.getZones(), 3);
166                         return toDecimalType(zone1.getTemp());
167                     case CHANNEL_SETPOINT_MODE_PROGRAM:
168                         NATimeTableItem currentProgramMode = getCurrentProgramMode(
169                                 nonNullList(thermostat.get().getThermProgramList()));
170                         if (currentProgramMode != null) {
171                             NAZone zone2 = getZone(currentProgram.getZones(), currentProgramMode.getId());
172                             return toDecimalType(zone2.getTemp());
173                         }
174                     case CHANNEL_SETPOINT_MODE_OFF:
175                     case CHANNEL_SETPOINT_MODE_MAX:
176                         return UnDefType.UNDEF;
177                 }
178             }
179         }
180         return UnDefType.NULL;
181     }
182
183     private NAZone getZone(List<NAZone> zones, int searchedId) {
184         return nonNullStream(zones).filter(z -> z.getId() == searchedId).findFirst().get();
185     }
186
187     private long getNetatmoProgramBaseTime() {
188         Calendar mondayZero = Calendar.getInstance();
189         mondayZero.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
190         mondayZero.set(Calendar.HOUR_OF_DAY, 0);
191         mondayZero.set(Calendar.MINUTE, 0);
192         mondayZero.set(Calendar.SECOND, 0);
193         return mondayZero.getTimeInMillis();
194     }
195
196     private @Nullable NATimeTableItem getCurrentProgramMode(List<NAThermProgram> thermProgramList) {
197         NATimeTableItem lastProgram = null;
198         Calendar now = Calendar.getInstance();
199         long diff = (now.getTimeInMillis() - getNetatmoProgramBaseTime()) / 1000 / 60;
200
201         Optional<NAThermProgram> currentProgram = thermProgramList.stream()
202                 .filter(p -> p.isSelected() != null && p.isSelected()).findFirst();
203
204         if (currentProgram.isPresent()) {
205             Stream<NATimeTableItem> pastPrograms = nonNullStream(currentProgram.get().getTimetable())
206                     .filter(t -> t.getMOffset() < diff);
207             Optional<NATimeTableItem> program = pastPrograms.reduce((first, second) -> second);
208             if (program.isPresent()) {
209                 lastProgram = program.get();
210             }
211         }
212
213         return lastProgram;
214     }
215
216     private int getNextProgramTime(List<NAThermProgram> thermProgramList) {
217         Calendar now = Calendar.getInstance();
218         long diff = (now.getTimeInMillis() - getNetatmoProgramBaseTime()) / 1000 / 60;
219
220         int result = -1;
221
222         for (NAThermProgram thermProgram : thermProgramList) {
223             if (thermProgram.isSelected() != null && thermProgram.isSelected()) {
224                 // By default we'll use the first slot of next week - this case will be true if
225                 // we are in the last schedule of the week so below loop will not exit by break
226                 List<NATimeTableItem> timetable = thermProgram.getTimetable();
227                 int next = timetable.get(0).getMOffset() + (7 * 24 * 60);
228
229                 for (NATimeTableItem timeTable : timetable) {
230                     if (timeTable.getMOffset() > diff) {
231                         next = timeTable.getMOffset();
232                         break;
233                     }
234                 }
235
236                 result = (int) (next * 60 + (getNetatmoProgramBaseTime() / 1000));
237             }
238         }
239         return result;
240     }
241
242     @Override
243     public void handleCommand(ChannelUID channelUID, Command command) {
244         super.handleCommand(channelUID, command);
245         if (!(command instanceof RefreshType)) {
246             try {
247                 switch (channelUID.getId()) {
248                     case CHANNEL_SETPOINT_MODE: {
249                         String targetMode = command.toString();
250                         if (CHANNEL_SETPOINT_MODE_MANUAL.equals(targetMode)) {
251                             logger.info(
252                                     "Switching to manual mode is done by assigning a setpoint temperature - command dropped");
253                             updateState(channelUID, getSetpoint());
254                         } else {
255                             pushSetpointUpdate(targetMode, null, null);
256                         }
257                         break;
258                     }
259                     case CHANNEL_SETPOINT_TEMP: {
260                         BigDecimal spTemp = null;
261                         if (command instanceof QuantityType) {
262                             @SuppressWarnings("unchecked")
263                             QuantityType<Temperature> quantity = ((QuantityType<Temperature>) command)
264                                     .toUnit(API_TEMPERATURE_UNIT);
265                             if (quantity != null) {
266                                 spTemp = quantity.toBigDecimal().setScale(1, RoundingMode.HALF_UP);
267                             }
268                         } else {
269                             spTemp = new BigDecimal(command.toString()).setScale(1, RoundingMode.HALF_UP);
270                         }
271                         if (spTemp != null) {
272                             pushSetpointUpdate(CHANNEL_SETPOINT_MODE_MANUAL, getSetpointEndTime(), spTemp.floatValue());
273                         }
274
275                         break;
276                     }
277                     case CHANNEL_PLANNING: {
278                         getApi().ifPresent(api -> {
279                             api.switchschedule(getParentId(), getId(), command.toString());
280                             updateState(channelUID, new StringType(command.toString()));
281                             invalidateParentCacheAndRefresh();
282                         });
283                     }
284                 }
285             } catch (Exception e) {
286                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
287             }
288         }
289     }
290
291     private void pushSetpointUpdate(String target_mode, @Nullable Integer setpointEndtime,
292             @Nullable Float setpointTemp) {
293         getApi().ifPresent(api -> {
294             api.setthermpoint(getParentId(), getId(), target_mode, setpointEndtime, setpointTemp);
295             invalidateParentCacheAndRefresh();
296         });
297     }
298
299     private int getSetpointEndTime() {
300         Calendar cal = Calendar.getInstance();
301         cal.add(Calendar.MINUTE, getSetPointDefaultDuration());
302         cal.set(Calendar.SECOND, 0);
303         cal.set(Calendar.MILLISECOND, 0);
304         return (int) (cal.getTimeInMillis() / 1000);
305     }
306
307     private int getSetPointDefaultDuration() {
308         // TODO : this informations could be sourced from Netatmo API instead of a local configuration element
309         Configuration conf = config;
310         Object defaultDuration = conf != null ? conf.get(SETPOINT_DEFAULT_DURATION) : null;
311         if (defaultDuration instanceof BigDecimal) {
312             return ((BigDecimal) defaultDuration).intValue();
313         }
314         return 60;
315     }
316
317     private Optional<ThermostatApi> getApi() {
318         return getBridgeHandler().flatMap(handler -> handler.getThermostatApi());
319     }
320 }