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