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