2 * Copyright (c) 2010-2020 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.binding.netatmo.internal.thermostat;
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.*;
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;
27 import javax.measure.quantity.Temperature;
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;
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;
59 * {@link NATherm1Handler} is the class used to handle the thermostat
60 * module of a thermostat set
62 * @author Gaƫl L'hopital - Initial contribution OH2 version
66 public class NATherm1Handler extends NetatmoModuleHandler<NAThermostat> {
67 private final Logger logger = LoggerFactory.getLogger(NATherm1Handler.class);
68 private final NATherm1StateDescriptionProvider stateDescriptionProvider;
70 public NATherm1Handler(Thing thing, NATherm1StateDescriptionProvider stateDescriptionProvider,
71 final TimeZoneProvider timeZoneProvider) {
72 super(thing, timeZoneProvider);
73 this.stateDescriptionProvider = stateDescriptionProvider;
77 protected void updateProperties(NAThermostat moduleData) {
78 updateProperties(moduleData.getFirmware(), moduleData.getType());
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,
88 measurableChannels.setMeasures(measures);
91 setRefreshRequired(false);
93 super.updateChannels(moduleObject);
95 getModule().ifPresent(this::updateStateDescription);
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()));
103 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_PLANNING), options);
107 protected State getNAThingProperty(String channelId) {
108 Optional<NAThermostat> thermostat = getModule();
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()));
131 return toDateTimeType(endTime, timeZoneProvider.getTimeZone());
133 return UnDefType.NULL;
135 return UnDefType.UNDEF;
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();
147 return toStringType(currentPlanning);
149 return UnDefType.UNDEF;
152 return super.getNAThingProperty(channelId);
155 private State getSetpoint() {
157 .map(m -> m.getSetpoint() != null ? toStringType(m.getSetpoint().getSetpointMode()) : UnDefType.NULL)
158 .orElse(UnDefType.UNDEF);
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();
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());
186 case CHANNEL_SETPOINT_MODE_OFF:
187 case CHANNEL_SETPOINT_MODE_MAX:
188 return UnDefType.UNDEF;
192 return UnDefType.NULL;
195 private NAZone getZone(List<NAZone> zones, int searchedId) {
196 return nonNullStream(zones).filter(z -> z.getId() == searchedId).findFirst().get();
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();
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;
213 Optional<NAThermProgram> currentProgram = thermProgramList.stream()
214 .filter(p -> p.isSelected() != null && p.isSelected()).findFirst();
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();
228 private int getNextProgramTime(List<NAThermProgram> thermProgramList) {
229 Calendar now = Calendar.getInstance();
230 long diff = (now.getTimeInMillis() - getNetatmoProgramBaseTime()) / 1000 / 60;
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);
241 for (NATimeTableItem timeTable : timetable) {
242 if (timeTable.getMOffset() > diff) {
243 next = timeTable.getMOffset();
248 result = (int) (next * 60 + (getNetatmoProgramBaseTime() / 1000));
255 public void handleCommand(ChannelUID channelUID, Command command) {
256 super.handleCommand(channelUID, command);
257 if (!(command instanceof RefreshType)) {
259 switch (channelUID.getId()) {
260 case CHANNEL_SETPOINT_MODE: {
261 String targetMode = command.toString();
262 if (CHANNEL_SETPOINT_MODE_MANUAL.equals(targetMode)) {
264 "Switching to manual mode is done by assigning a setpoint temperature - command dropped");
265 updateState(channelUID, getSetpoint());
267 pushSetpointUpdate(targetMode, null, null);
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);
281 spTemp = new BigDecimal(command.toString()).setScale(1, RoundingMode.HALF_UP);
283 if (spTemp != null) {
284 pushSetpointUpdate(CHANNEL_SETPOINT_MODE_MANUAL, getSetpointEndTime(), spTemp.floatValue());
289 case CHANNEL_PLANNING: {
290 getApi().ifPresent(api -> {
291 api.switchschedule(getParentId(), getId(), command.toString());
292 updateState(channelUID, new StringType(command.toString()));
293 invalidateParentCacheAndRefresh();
297 } catch (Exception e) {
298 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
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();
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);
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();
329 private Optional<ThermostatApi> getApi() {
330 return getBridgeHandler().flatMap(handler -> handler.getThermostatApi());