]> git.basschouten.com Git - openhab-addons.git/blob
03927e7337058aef45a3334ffdb79f613d0b1ddb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.venstarthermostat.internal.handler;
14
15 import static org.openhab.binding.venstarthermostat.internal.VenstarThermostatBindingConstants.*;
16 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
17
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.net.MalformedURLException;
21 import java.net.URISyntaxException;
22 import java.net.URL;
23 import java.time.Instant;
24 import java.time.LocalDateTime;
25 import java.time.ZoneId;
26 import java.time.ZonedDateTime;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Objects;
34 import java.util.Optional;
35 import java.util.concurrent.ExecutionException;
36 import java.util.concurrent.Future;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.TimeoutException;
39
40 import javax.measure.Quantity;
41 import javax.measure.Unit;
42 import javax.measure.quantity.Dimensionless;
43 import javax.measure.quantity.Temperature;
44
45 import org.eclipse.jdt.annotation.NonNullByDefault;
46 import org.eclipse.jdt.annotation.Nullable;
47 import org.eclipse.jetty.client.HttpClient;
48 import org.eclipse.jetty.client.api.ContentResponse;
49 import org.eclipse.jetty.client.api.Request;
50 import org.eclipse.jetty.client.util.DigestAuthentication;
51 import org.eclipse.jetty.http.HttpMethod;
52 import org.eclipse.jetty.util.ssl.SslContextFactory;
53 import org.openhab.binding.venstarthermostat.internal.VenstarThermostatConfiguration;
54 import org.openhab.binding.venstarthermostat.internal.dto.VenstarAwayMode;
55 import org.openhab.binding.venstarthermostat.internal.dto.VenstarAwayModeSerializer;
56 import org.openhab.binding.venstarthermostat.internal.dto.VenstarFanMode;
57 import org.openhab.binding.venstarthermostat.internal.dto.VenstarFanModeSerializer;
58 import org.openhab.binding.venstarthermostat.internal.dto.VenstarFanState;
59 import org.openhab.binding.venstarthermostat.internal.dto.VenstarFanStateSerializer;
60 import org.openhab.binding.venstarthermostat.internal.dto.VenstarInfoData;
61 import org.openhab.binding.venstarthermostat.internal.dto.VenstarResponse;
62 import org.openhab.binding.venstarthermostat.internal.dto.VenstarRuntime;
63 import org.openhab.binding.venstarthermostat.internal.dto.VenstarRuntimeData;
64 import org.openhab.binding.venstarthermostat.internal.dto.VenstarScheduleMode;
65 import org.openhab.binding.venstarthermostat.internal.dto.VenstarScheduleModeSerializer;
66 import org.openhab.binding.venstarthermostat.internal.dto.VenstarSchedulePart;
67 import org.openhab.binding.venstarthermostat.internal.dto.VenstarSchedulePartSerializer;
68 import org.openhab.binding.venstarthermostat.internal.dto.VenstarSensor;
69 import org.openhab.binding.venstarthermostat.internal.dto.VenstarSensorData;
70 import org.openhab.binding.venstarthermostat.internal.dto.VenstarSystemMode;
71 import org.openhab.binding.venstarthermostat.internal.dto.VenstarSystemModeSerializer;
72 import org.openhab.binding.venstarthermostat.internal.dto.VenstarSystemState;
73 import org.openhab.binding.venstarthermostat.internal.dto.VenstarSystemStateSerializer;
74 import org.openhab.core.config.core.status.ConfigStatusMessage;
75 import org.openhab.core.library.types.DateTimeType;
76 import org.openhab.core.library.types.DecimalType;
77 import org.openhab.core.library.types.OnOffType;
78 import org.openhab.core.library.types.QuantityType;
79 import org.openhab.core.library.types.StringType;
80 import org.openhab.core.library.unit.ImperialUnits;
81 import org.openhab.core.library.unit.SIUnits;
82 import org.openhab.core.library.unit.Units;
83 import org.openhab.core.thing.ChannelUID;
84 import org.openhab.core.thing.Thing;
85 import org.openhab.core.thing.ThingStatus;
86 import org.openhab.core.thing.ThingStatusDetail;
87 import org.openhab.core.thing.binding.ConfigStatusThingHandler;
88 import org.openhab.core.types.Command;
89 import org.openhab.core.types.RefreshType;
90 import org.openhab.core.types.State;
91 import org.openhab.core.types.UnDefType;
92 import org.slf4j.Logger;
93 import org.slf4j.LoggerFactory;
94
95 import com.google.gson.Gson;
96 import com.google.gson.GsonBuilder;
97 import com.google.gson.JsonSyntaxException;
98
99 /**
100  * The {@link VenstarThermostatHandler} is responsible for handling commands, which are
101  * sent to one of the channels.
102  *
103  * @author William Welliver - Initial contribution
104  * @author Dan Cunningham - Migration to Jetty, annotations and various improvements
105  * @author Matthew Davies - added code to include away mode in binding
106  */
107 @NonNullByDefault
108 public class VenstarThermostatHandler extends ConfigStatusThingHandler {
109     private static final int TIMEOUT_SECONDS = 30;
110     private static final int UPDATE_AFTER_COMMAND_SECONDS = 2;
111     private Logger log = LoggerFactory.getLogger(VenstarThermostatHandler.class);
112     private List<VenstarSensor> sensorData = new ArrayList<>();
113     private VenstarInfoData infoData = new VenstarInfoData();
114     private VenstarRuntimeData runtimeData = new VenstarRuntimeData();
115     private Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
116     private @Nullable Future<?> updatesTask;
117     private @Nullable URL baseURL;
118     private int refresh;
119     private final HttpClient httpClient;
120     private final Gson gson;
121
122     // Venstar Thermostats are most commonly installed in the US, so start with a reasonable default.
123     private Unit<Temperature> unitSystem = ImperialUnits.FAHRENHEIT;
124
125     public VenstarThermostatHandler(Thing thing) {
126         super(thing);
127         httpClient = new HttpClient(new SslContextFactory.Client(true));
128         gson = new GsonBuilder().registerTypeAdapter(VenstarSystemState.class, new VenstarSystemStateSerializer())
129                 .registerTypeAdapter(VenstarSystemMode.class, new VenstarSystemModeSerializer())
130                 .registerTypeAdapter(VenstarAwayMode.class, new VenstarAwayModeSerializer())
131                 .registerTypeAdapter(VenstarFanMode.class, new VenstarFanModeSerializer())
132                 .registerTypeAdapter(VenstarFanState.class, new VenstarFanStateSerializer())
133                 .registerTypeAdapter(VenstarScheduleMode.class, new VenstarScheduleModeSerializer())
134                 .registerTypeAdapter(VenstarSchedulePart.class, new VenstarSchedulePartSerializer()).create();
135
136         log.trace("VenstarThermostatHandler for thing {}", getThing().getUID());
137     }
138
139     @Override
140     public Collection<ConfigStatusMessage> getConfigStatus() {
141         Collection<ConfigStatusMessage> status = new ArrayList<>();
142         VenstarThermostatConfiguration config = getConfigAs(VenstarThermostatConfiguration.class);
143         if (config.username.isBlank()) {
144             log.warn("username is empty");
145             status.add(ConfigStatusMessage.Builder.error(CONFIG_USERNAME).withMessageKeySuffix(EMPTY_INVALID)
146                     .withArguments(CONFIG_USERNAME).build());
147         }
148
149         if (config.password.isBlank()) {
150             log.warn("password is empty");
151             status.add(ConfigStatusMessage.Builder.error(CONFIG_PASSWORD).withMessageKeySuffix(EMPTY_INVALID)
152                     .withArguments(CONFIG_PASSWORD).build());
153         }
154
155         if (config.refresh < 10) {
156             log.warn("refresh is too small: {}", config.refresh);
157
158             status.add(ConfigStatusMessage.Builder.error(CONFIG_REFRESH).withMessageKeySuffix(REFRESH_INVALID)
159                     .withArguments(CONFIG_REFRESH).build());
160         }
161         return status;
162     }
163
164     @Override
165     public void handleCommand(ChannelUID channelUID, Command command) {
166         if (getThing().getStatus() != ThingStatus.ONLINE) {
167             log.debug("Controller is NOT ONLINE and is not responding to commands");
168             return;
169         }
170
171         stopUpdateTasks();
172         if (command instanceof RefreshType) {
173             log.debug("Refresh command requested for {}", channelUID);
174             stateMap.clear();
175             startUpdatesTask(0);
176         } else {
177             stateMap.remove(channelUID.getAsString());
178             if (channelUID.getId().equals(CHANNEL_HEATING_SETPOINT)) {
179                 QuantityType<Temperature> quantity = commandToQuantityType(command, unitSystem);
180                 double value = quantityToRoundedTemperature(quantity, unitSystem).doubleValue();
181                 log.debug("Setting heating setpoint to {}", value);
182                 setHeatingSetpoint(value);
183             } else if (channelUID.getId().equals(CHANNEL_COOLING_SETPOINT)) {
184                 QuantityType<Temperature> quantity = commandToQuantityType(command, unitSystem);
185                 double value = quantityToRoundedTemperature(quantity, unitSystem).doubleValue();
186                 log.debug("Setting cooling setpoint to {}", value);
187                 setCoolingSetpoint(value);
188             } else if (channelUID.getId().equals(CHANNEL_SYSTEM_MODE)) {
189                 VenstarSystemMode value;
190                 try {
191                     if (command instanceof StringType) {
192                         value = VenstarSystemMode.valueOf(((StringType) command).toString().toUpperCase());
193                     } else {
194                         value = VenstarSystemMode.fromInt(((DecimalType) command).intValue());
195                     }
196                     log.debug("Setting system mode to  {}", value);
197                     setSystemMode(value);
198                     updateIfChanged(CHANNEL_SYSTEM_MODE_RAW, new StringType(value.toString()));
199                 } catch (IllegalArgumentException e) {
200                     log.warn("Invalid System Mode");
201                 }
202             } else if (channelUID.getId().equals(CHANNEL_AWAY_MODE)) {
203                 VenstarAwayMode value;
204                 try {
205                     if (command instanceof StringType) {
206                         value = VenstarAwayMode.valueOf(((StringType) command).toString().toUpperCase());
207                     } else {
208                         value = VenstarAwayMode.fromInt(((DecimalType) command).intValue());
209                     }
210                     log.debug("Setting away mode to  {}", value);
211                     setAwayMode(value);
212                     updateIfChanged(CHANNEL_AWAY_MODE_RAW, new StringType(value.toString()));
213                 } catch (IllegalArgumentException e) {
214                     log.warn("Invalid Away Mode");
215                 }
216
217             } else if (channelUID.getId().equals(CHANNEL_FAN_MODE)) {
218                 VenstarFanMode value;
219                 try {
220                     if (command instanceof StringType) {
221                         value = VenstarFanMode.valueOf(((StringType) command).toString().toUpperCase());
222                     } else {
223                         value = VenstarFanMode.fromInt(((DecimalType) command).intValue());
224                     }
225                     log.debug("Setting fan mode to  {}", value);
226                     setFanMode(value);
227                     updateIfChanged(CHANNEL_FAN_MODE_RAW, new StringType(value.toString()));
228                 } catch (IllegalArgumentException e) {
229                     log.warn("Invalid Fan Mode");
230                 }
231             } else if (channelUID.getId().equals(CHANNEL_SCHEDULE_MODE)) {
232                 VenstarScheduleMode value;
233                 try {
234                     if (command instanceof StringType) {
235                         value = VenstarScheduleMode.valueOf(((StringType) command).toString().toUpperCase());
236                     } else {
237                         value = VenstarScheduleMode.fromInt(((DecimalType) command).intValue());
238                     }
239                     log.debug("Setting schedule mode to  {}", value);
240                     setScheduleMode(value);
241                     updateIfChanged(CHANNEL_SCHEDULE_MODE_RAW, new StringType(value.toString()));
242                 } catch (IllegalArgumentException e) {
243                     log.warn("Invalid Schedule Mode");
244                 }
245             }
246
247             startUpdatesTask(UPDATE_AFTER_COMMAND_SECONDS);
248         }
249     }
250
251     @Override
252     public void dispose() {
253         stopUpdateTasks();
254         if (httpClient.isStarted()) {
255             try {
256                 httpClient.stop();
257             } catch (Exception e) {
258                 log.debug("Could not stop HttpClient", e);
259             }
260         }
261     }
262
263     @Override
264     public void initialize() {
265         connect();
266     }
267
268     protected void goOnline() {
269         if (getThing().getStatus() != ThingStatus.ONLINE) {
270             updateStatus(ThingStatus.ONLINE);
271         }
272     }
273
274     protected void goOffline(ThingStatusDetail detail, String reason) {
275         if (getThing().getStatus() != ThingStatus.OFFLINE) {
276             updateStatus(ThingStatus.OFFLINE, detail, reason);
277         }
278     }
279
280     @SuppressWarnings("null") // compiler does not see new URL(url) as never being null
281     private void connect() {
282         stopUpdateTasks();
283         VenstarThermostatConfiguration config = getConfigAs(VenstarThermostatConfiguration.class);
284         try {
285             baseURL = new URL(config.url);
286             if (!httpClient.isStarted()) {
287                 httpClient.start();
288             }
289             httpClient.getAuthenticationStore().clearAuthentications();
290             httpClient.getAuthenticationStore().clearAuthenticationResults();
291             httpClient.getAuthenticationStore().addAuthentication(
292                     new DigestAuthentication(baseURL.toURI(), "thermostat", config.username, config.password));
293             refresh = config.refresh;
294             startUpdatesTask(0);
295         } catch (Exception e) {
296             log.debug("Could not conntect to URL  {}", config.url, e);
297             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
298         }
299     }
300
301     /**
302      * Start the poller after an initial delay
303      *
304      * @param initialDelay
305      */
306     private synchronized void startUpdatesTask(int initialDelay) {
307         stopUpdateTasks();
308         updatesTask = scheduler.scheduleWithFixedDelay(this::updateData, initialDelay, refresh, TimeUnit.SECONDS);
309     }
310
311     /**
312      * Stop the poller
313      */
314     @SuppressWarnings("null")
315     private void stopUpdateTasks() {
316         Future<?> localUpdatesTask = updatesTask;
317         if (isFutureValid(localUpdatesTask)) {
318             localUpdatesTask.cancel(false);
319         }
320     }
321
322     private boolean isFutureValid(@Nullable Future<?> future) {
323         return future != null && !future.isCancelled();
324     }
325
326     private State getTemperature() {
327         Optional<VenstarSensor> optSensor = sensorData.stream()
328                 .filter(sensor -> sensor.getName().equalsIgnoreCase("Thermostat")).findAny();
329         if (optSensor.isPresent()) {
330             return new QuantityType<Temperature>(optSensor.get().getTemp(), unitSystem);
331         }
332
333         return UnDefType.UNDEF;
334     }
335
336     private State getHumidity() {
337         Optional<VenstarSensor> optSensor = sensorData.stream()
338                 .filter(sensor -> sensor.getName().equalsIgnoreCase("Thermostat")).findAny();
339         if (optSensor.isPresent()) {
340             return new QuantityType<Dimensionless>(optSensor.get().getHum(), Units.PERCENT);
341         }
342
343         return UnDefType.UNDEF;
344     }
345
346     private State getOutdoorTemperature() {
347         Optional<VenstarSensor> optSensor = sensorData.stream()
348                 .filter(sensor -> sensor.getName().equalsIgnoreCase("Outdoor")).findAny();
349         if (optSensor.isPresent()) {
350             return new QuantityType<Temperature>(optSensor.get().getTemp(), unitSystem);
351         }
352
353         return UnDefType.UNDEF;
354     }
355
356     private void setCoolingSetpoint(double cool) {
357         double heat = getHeatingSetpoint().doubleValue();
358         VenstarSystemMode mode = infoData.getSystemMode();
359         VenstarFanMode fanmode = infoData.getFanMode();
360         updateControls(heat, cool, mode, fanmode);
361     }
362
363     private void setSystemMode(VenstarSystemMode mode) {
364         double cool = getCoolingSetpoint().doubleValue();
365         double heat = getHeatingSetpoint().doubleValue();
366         VenstarFanMode fanmode = infoData.getFanMode();
367         updateControls(heat, cool, mode, fanmode);
368     }
369
370     private void setHeatingSetpoint(double heat) {
371         double cool = getCoolingSetpoint().doubleValue();
372         VenstarSystemMode mode = infoData.getSystemMode();
373         VenstarFanMode fanmode = infoData.getFanMode();
374         updateControls(heat, cool, mode, fanmode);
375     }
376
377     private void setFanMode(VenstarFanMode fanmode) {
378         double cool = getCoolingSetpoint().doubleValue();
379         double heat = getHeatingSetpoint().doubleValue();
380         VenstarSystemMode mode = infoData.getSystemMode();
381         updateControls(heat, cool, mode, fanmode);
382     }
383
384     private void setAwayMode(VenstarAwayMode away) {
385         VenstarScheduleMode schedule = infoData.getScheduleMode();
386         updateSettings(away, schedule);
387     }
388
389     private void setScheduleMode(VenstarScheduleMode schedule) {
390         VenstarAwayMode away = infoData.getAwayMode();
391         updateSettings(away, schedule);
392     }
393
394     private QuantityType<Temperature> getCoolingSetpoint() {
395         return new QuantityType<Temperature>(infoData.getCooltemp(), unitSystem);
396     }
397
398     private QuantityType<Temperature> getHeatingSetpoint() {
399         return new QuantityType<Temperature>(infoData.getHeattemp(), unitSystem);
400     }
401
402     private ZonedDateTime getTimestampRuntime(VenstarRuntime runtime) {
403         ZoneId zoneId = ZoneId.systemDefault();
404         ZonedDateTime now = LocalDateTime.now().atZone(zoneId);
405         int diff = now.getOffset().getTotalSeconds();
406         ZonedDateTime z = ZonedDateTime.ofInstant(Instant.ofEpochSecond(runtime.getTimeStamp() - diff), zoneId);
407         return z;
408     }
409
410     private void updateSettings(VenstarAwayMode away, VenstarScheduleMode schedule) {
411         // this function corresponds to the thermostat local API POST /settings instruction
412         // the function can be expanded with other parameters which are changed via POST /settings
413         // settings that can be included are tempunits, away mode, schedule mode, humidifier setpoint, dehumidifier
414         // setpoint
415         // (hum/dehum are the only ones missing)
416         Map<String, String> params = new HashMap<>();
417         params.put("away", String.valueOf(away.mode()));
418         params.put("schedule", String.valueOf(schedule.mode()));
419         VenstarResponse res = updateThermostat("/settings", params);
420         if (res != null) {
421             log.debug("Updated thermostat");
422             // update our local copy until the next refresh occurs
423             infoData.setAwayMode(away);
424             infoData.setScheduleMode(schedule);
425             // add other parameters here in the same way
426         }
427     }
428
429     private void updateControls(double heat, double cool, VenstarSystemMode mode, VenstarFanMode fanmode) {
430         // this function corresponds to the thermostat local API POST /control instruction
431         // the function can be expanded with other parameters which are changed via POST /control
432         // controls that can be included are thermostat mode, fan mode, heat temp, cool temp (all done already)
433         Map<String, String> params = new HashMap<>();
434         if (heat > 0) {
435             params.put("heattemp", String.valueOf(heat));
436         }
437         if (cool > 0) {
438             params.put("cooltemp", String.valueOf(cool));
439         }
440         params.put("mode", String.valueOf(mode.mode()));
441         params.put("fan", String.valueOf(fanmode.mode()));
442         VenstarResponse res = updateThermostat("/control", params);
443         if (res != null) {
444             log.debug("Updated thermostat");
445             // update our local copy until the next refresh occurs
446             infoData.setCooltemp(cool);
447             infoData.setHeattemp(heat);
448             infoData.setSystemMode(mode);
449             infoData.setFanMode(fanmode);
450             // add other parameters here in the same way
451         }
452     }
453
454     /**
455      * Function to send data to the thermostat and update the Thing state if there is an error
456      *
457      * @param path
458      * @param params
459      * @return VenstarResponse object or null if there was an error
460      */
461     private @Nullable VenstarResponse updateThermostat(String path, Map<String, String> params) {
462         try {
463             String result = postData(path, params);
464             VenstarResponse res = gson.fromJson(result, VenstarResponse.class);
465             if (res != null && res.isSuccess()) {
466                 return res;
467             } else {
468                 String reason = res == null ? "invalid response" : res.getReason();
469                 log.debug("Failed to update thermostat: {}", reason);
470                 goOffline(ThingStatusDetail.COMMUNICATION_ERROR, reason);
471             }
472         } catch (VenstarCommunicationException | JsonSyntaxException e) {
473             log.debug("Unable to fetch info data", e);
474             String message = e.getMessage();
475             goOffline(ThingStatusDetail.COMMUNICATION_ERROR, message != null ? message : "");
476         } catch (VenstarAuthenticationException e) {
477             goOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Authorization Failed");
478         }
479         return null;
480     }
481
482     private void updateData() {
483         try {
484             Future<?> localUpdatesTask = updatesTask;
485             String response = getData("/query/sensors");
486             if (!isFutureValid(localUpdatesTask)) {
487                 return;
488             }
489             VenstarSensorData res = gson.fromJson(response, VenstarSensorData.class);
490             sensorData = res.getSensors();
491             updateIfChanged(CHANNEL_TEMPERATURE, getTemperature());
492             updateIfChanged(CHANNEL_EXTERNAL_TEMPERATURE, getOutdoorTemperature());
493             updateIfChanged(CHANNEL_HUMIDITY, getHumidity());
494
495             response = getData("/query/runtimes");
496             if (!isFutureValid(localUpdatesTask)) {
497                 return;
498             }
499
500             runtimeData = Objects.requireNonNull(gson.fromJson(response, VenstarRuntimeData.class));
501             List<VenstarRuntime> runtimes = runtimeData.getRuntimes();
502             Collections.reverse(runtimes);// reverse the list so that the most recent runtime data is first in the list
503             int nRuntimes = Math.min(7, runtimes.size());// check how many runtimes are available, might be less than
504                                                          // seven if equipment
505                                                          // was reset, and also might be more than 7, so limit to 7
506             for (int i = 0; i < nRuntimes; i++) {
507                 VenstarRuntime rt = runtimes.get(i);
508                 updateIfChanged(CHANNEL_TIMESTAMP_RUNTIME_DAY + i, new DateTimeType(getTimestampRuntime(rt)));
509                 updateIfChanged(CHANNEL_HEAT1_RUNTIME_DAY + i, new DecimalType(rt.getHeat1Runtime()));
510                 updateIfChanged(CHANNEL_HEAT2_RUNTIME_DAY + i, new DecimalType(rt.getHeat2Runtime()));
511                 updateIfChanged(CHANNEL_COOL1_RUNTIME_DAY + i, new DecimalType(rt.getCool1Runtime()));
512                 updateIfChanged(CHANNEL_COOL2_RUNTIME_DAY + i, new DecimalType(rt.getCool2Runtime()));
513                 updateIfChanged(CHANNEL_AUX1_RUNTIME_DAY + i, new DecimalType(rt.getAux1Runtime()));
514                 updateIfChanged(CHANNEL_AUX2_RUNTIME_DAY + i, new DecimalType(rt.getAux2Runtime()));
515                 updateIfChanged(CHANNEL_FC_RUNTIME_DAY + i, new DecimalType(rt.getFreeCoolRuntime()));
516             }
517
518             response = getData("/query/info");
519             if (!isFutureValid(localUpdatesTask)) {
520                 return;
521             }
522             infoData = Objects.requireNonNull(gson.fromJson(response, VenstarInfoData.class));
523             updateUnits(infoData);
524             updateIfChanged(CHANNEL_HEATING_SETPOINT, getHeatingSetpoint());
525             updateIfChanged(CHANNEL_COOLING_SETPOINT, getCoolingSetpoint());
526             updateIfChanged(CHANNEL_SYSTEM_STATE, new StringType(infoData.getSystemState().stateName()));
527             updateIfChanged(CHANNEL_SYSTEM_MODE, new StringType(infoData.getSystemMode().modeName()));
528             updateIfChanged(CHANNEL_SYSTEM_STATE_RAW, new DecimalType(infoData.getSystemState().state()));
529             updateIfChanged(CHANNEL_SYSTEM_MODE_RAW, new DecimalType(infoData.getSystemMode().mode()));
530             updateIfChanged(CHANNEL_AWAY_MODE, new StringType(infoData.getAwayMode().modeName()));
531             updateIfChanged(CHANNEL_AWAY_MODE_RAW, new DecimalType(infoData.getAwayMode().mode()));
532             updateIfChanged(CHANNEL_FAN_MODE, new StringType(infoData.getFanMode().modeName()));
533             updateIfChanged(CHANNEL_FAN_MODE_RAW, new DecimalType(infoData.getFanMode().mode()));
534             updateIfChanged(CHANNEL_FAN_STATE, OnOffType.from(infoData.getFanState().stateName()));
535             updateIfChanged(CHANNEL_FAN_STATE_RAW, new DecimalType(infoData.getFanState().state()));
536             updateIfChanged(CHANNEL_SCHEDULE_MODE, new StringType(infoData.getScheduleMode().modeName()));
537             updateIfChanged(CHANNEL_SCHEDULE_MODE_RAW, new DecimalType(infoData.getScheduleMode().mode()));
538             updateIfChanged(CHANNEL_SCHEDULE_PART, new StringType(infoData.getSchedulePart().partName()));
539             updateIfChanged(CHANNEL_SCHEDULE_PART_RAW, new DecimalType(infoData.getSchedulePart().part()));
540
541             goOnline();
542         } catch (VenstarCommunicationException | JsonSyntaxException e) {
543             log.debug("Unable to fetch info data", e);
544             String message = e.getMessage();
545             goOffline(ThingStatusDetail.COMMUNICATION_ERROR, message != null ? message : "");
546         } catch (VenstarAuthenticationException e) {
547             goOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Authorization Failed");
548         }
549     }
550
551     private void updateIfChanged(String channelID, State state) {
552         ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelID);
553         State oldState = stateMap.put(channelUID.toString(), state);
554         if (!state.equals(oldState)) {
555             log.trace("updating channel {} with state {} (old state {})", channelUID, state, oldState);
556             updateState(channelUID, state);
557         }
558     }
559
560     private void updateUnits(VenstarInfoData infoData) {
561         int tempunits = infoData.getTempunits();
562         if (tempunits == 0) {
563             unitSystem = ImperialUnits.FAHRENHEIT;
564         } else if (tempunits == 1) {
565             unitSystem = SIUnits.CELSIUS;
566         } else {
567             log.warn("Thermostat returned unknown unit system type: {}", tempunits);
568         }
569     }
570
571     private String getData(String path) throws VenstarAuthenticationException, VenstarCommunicationException {
572         try {
573             URL getURL = new URL(baseURL, path);
574             Request request = httpClient.newRequest(getURL.toURI()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS);
575             return sendRequest(request);
576         } catch (MalformedURLException | URISyntaxException e) {
577             throw new VenstarCommunicationException(e);
578         }
579     }
580
581     private String postData(String path, Map<String, String> params)
582             throws VenstarAuthenticationException, VenstarCommunicationException {
583         try {
584             URL postURL = new URL(baseURL, path);
585             Request request = httpClient.newRequest(postURL.toURI()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
586                     .method(HttpMethod.POST);
587             params.forEach(request::param);
588             return sendRequest(request);
589         } catch (MalformedURLException | URISyntaxException e) {
590             throw new VenstarCommunicationException(e);
591         }
592     }
593
594     private String sendRequest(Request request) throws VenstarAuthenticationException, VenstarCommunicationException {
595         log.trace("sendRequest: requesting {}", request.getURI());
596         try {
597             ContentResponse response = request.send();
598             log.trace("Response code {}", response.getStatus());
599             if (response.getStatus() == 401) {
600                 throw new VenstarAuthenticationException();
601             }
602
603             if (response.getStatus() != 200) {
604                 throw new VenstarCommunicationException(
605                         "Error communicating with thermostat. Error Code: " + response.getStatus());
606             }
607             String content = response.getContentAsString();
608             log.trace("sendRequest: response {}", content);
609             return content;
610         } catch (InterruptedException | TimeoutException | ExecutionException e) {
611             throw new VenstarCommunicationException(e);
612         }
613     }
614
615     @SuppressWarnings("unchecked")
616     protected <U extends Quantity<U>> QuantityType<U> commandToQuantityType(Command command, Unit<U> defaultUnit) {
617         if (command instanceof QuantityType) {
618             return (QuantityType<U>) command;
619         }
620         return new QuantityType<U>(new BigDecimal(command.toString()), defaultUnit);
621     }
622
623     protected DecimalType commandToDecimalType(Command command) {
624         if (command instanceof DecimalType) {
625             return (DecimalType) command;
626         }
627         return new DecimalType(new BigDecimal(command.toString()));
628     }
629
630     private BigDecimal quantityToRoundedTemperature(QuantityType<Temperature> quantity, Unit<Temperature> unit)
631             throws IllegalArgumentException {
632         QuantityType<Temperature> temparatureQuantity = quantity.toUnit(unit);
633         if (temparatureQuantity == null) {
634             return quantity.toBigDecimal();
635         }
636
637         BigDecimal value = temparatureQuantity.toBigDecimal();
638         BigDecimal increment = CELSIUS == unit ? new BigDecimal("0.5") : new BigDecimal("1");
639         BigDecimal divisor = value.divide(increment, 0, RoundingMode.HALF_UP);
640         return divisor.multiply(increment);
641     }
642
643     @SuppressWarnings("serial")
644     private class VenstarAuthenticationException extends Exception {
645         public VenstarAuthenticationException() {
646             super("Invalid Credentials");
647         }
648     }
649
650     @SuppressWarnings("serial")
651     private class VenstarCommunicationException extends Exception {
652         public VenstarCommunicationException(Exception e) {
653             super(e);
654         }
655
656         public VenstarCommunicationException(String message) {
657             super(message);
658         }
659     }
660 }