]> git.basschouten.com Git - openhab-addons.git/blob
7dc53c48a2f3e499f04ddc09b737b57fb6617e96
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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         // This function updates the away mode via a POST to the thermostat's local API's /settings endpoint.
386         //
387         // The /settings endpoint supports a number of additional parameters (tempunits, de/humedifier
388         // setpoints, etc). However, newer Venstar firmwares will reject any POST to /settings that
389         // contains a `schedule` parameter when the thermostat is currently in away mode.
390         //
391         // Separating the updates to change `schedule` and `away` ensures that the thermostat will not
392         // reject attempts to un-set away mode due to the presence of the `schedule` parameter.
393         Map<String, String> params = new HashMap<>();
394         params.put("away", String.valueOf(away.mode()));
395         VenstarResponse res = updateThermostat("/settings", params);
396         if (res != null) {
397             log.debug("Updated thermostat");
398             // update our local copy until the next refresh occurs
399             infoData.setAwayMode(away);
400         }
401     }
402
403     private void setScheduleMode(VenstarScheduleMode schedule) {
404         // This function updates the schedule mode via a POST to the thermostat's local API's /settings endpoint.
405         //
406         // The /settings endpoint supports a number of additional parameters (tempunits, de/humedifier
407         // setpoints, etc). However, newer Venstar firmwares will reject any POST to /settings that
408         // contains a `schedule` parameter when the thermostat is currently in away mode.
409         //
410         // Separating the updates to change `schedule` and `away` ensures that the thermostat will not
411         // reject attempts to un-set away mode due to the presence of the `schedule` parameter.
412         Map<String, String> params = new HashMap<>();
413         params.put("schedule", String.valueOf(schedule.mode()));
414         VenstarResponse res = updateThermostat("/settings", params);
415         if (res != null) {
416             log.debug("Updated thermostat");
417             // update our local copy until the next refresh occurs
418             infoData.setScheduleMode(schedule);
419             // add other parameters here in the same way
420         }
421     }
422
423     private QuantityType<Temperature> getCoolingSetpoint() {
424         return new QuantityType<Temperature>(infoData.getCooltemp(), unitSystem);
425     }
426
427     private QuantityType<Temperature> getHeatingSetpoint() {
428         return new QuantityType<Temperature>(infoData.getHeattemp(), unitSystem);
429     }
430
431     private ZonedDateTime getTimestampRuntime(VenstarRuntime runtime) {
432         ZoneId zoneId = ZoneId.systemDefault();
433         ZonedDateTime now = LocalDateTime.now().atZone(zoneId);
434         int diff = now.getOffset().getTotalSeconds();
435         ZonedDateTime z = ZonedDateTime.ofInstant(Instant.ofEpochSecond(runtime.getTimeStamp() - diff), zoneId);
436         return z;
437     }
438
439     private void updateScheduleMode(VenstarScheduleMode schedule) {
440     }
441
442     private void updateControls(double heat, double cool, VenstarSystemMode mode, VenstarFanMode fanmode) {
443         // this function corresponds to the thermostat local API POST /control instruction
444         // the function can be expanded with other parameters which are changed via POST /control
445         // controls that can be included are thermostat mode, fan mode, heat temp, cool temp (all done already)
446         Map<String, String> params = new HashMap<>();
447         if (heat > 0) {
448             params.put("heattemp", String.valueOf(heat));
449         }
450         if (cool > 0) {
451             params.put("cooltemp", String.valueOf(cool));
452         }
453         params.put("mode", String.valueOf(mode.mode()));
454         params.put("fan", String.valueOf(fanmode.mode()));
455         VenstarResponse res = updateThermostat("/control", params);
456         if (res != null) {
457             log.debug("Updated thermostat");
458             // update our local copy until the next refresh occurs
459             infoData.setCooltemp(cool);
460             infoData.setHeattemp(heat);
461             infoData.setSystemMode(mode);
462             infoData.setFanMode(fanmode);
463             // add other parameters here in the same way
464         }
465     }
466
467     /**
468      * Function to send data to the thermostat and update the Thing state if there is an error
469      *
470      * @param path
471      * @param params
472      * @return VenstarResponse object or null if there was an error
473      */
474     private @Nullable VenstarResponse updateThermostat(String path, Map<String, String> params) {
475         try {
476             String result = postData(path, params);
477             VenstarResponse res = gson.fromJson(result, VenstarResponse.class);
478             if (res != null && res.isSuccess()) {
479                 return res;
480             } else {
481                 String reason = res == null ? "invalid response" : res.getReason();
482                 log.debug("Failed to update thermostat: {}", reason);
483                 goOffline(ThingStatusDetail.COMMUNICATION_ERROR, reason);
484             }
485         } catch (VenstarCommunicationException | JsonSyntaxException e) {
486             log.debug("Unable to fetch info data", e);
487             String message = e.getMessage();
488             goOffline(ThingStatusDetail.COMMUNICATION_ERROR, message != null ? message : "");
489         } catch (VenstarAuthenticationException e) {
490             goOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Authorization Failed");
491         }
492         return null;
493     }
494
495     private void updateData() {
496         try {
497             Future<?> localUpdatesTask = updatesTask;
498             String response = getData("/query/sensors");
499             if (!isFutureValid(localUpdatesTask)) {
500                 return;
501             }
502             VenstarSensorData res = gson.fromJson(response, VenstarSensorData.class);
503             sensorData = res.getSensors();
504             updateIfChanged(CHANNEL_TEMPERATURE, getTemperature());
505             updateIfChanged(CHANNEL_EXTERNAL_TEMPERATURE, getOutdoorTemperature());
506             updateIfChanged(CHANNEL_HUMIDITY, getHumidity());
507
508             response = getData("/query/runtimes");
509             if (!isFutureValid(localUpdatesTask)) {
510                 return;
511             }
512
513             runtimeData = Objects.requireNonNull(gson.fromJson(response, VenstarRuntimeData.class));
514             List<VenstarRuntime> runtimes = runtimeData.getRuntimes();
515             Collections.reverse(runtimes);// reverse the list so that the most recent runtime data is first in the list
516             int nRuntimes = Math.min(7, runtimes.size());// check how many runtimes are available, might be less than
517                                                          // seven if equipment
518                                                          // was reset, and also might be more than 7, so limit to 7
519             for (int i = 0; i < nRuntimes; i++) {
520                 VenstarRuntime rt = runtimes.get(i);
521                 updateIfChanged(CHANNEL_TIMESTAMP_RUNTIME_DAY + i, new DateTimeType(getTimestampRuntime(rt)));
522                 updateIfChanged(CHANNEL_HEAT1_RUNTIME_DAY + i, new DecimalType(rt.getHeat1Runtime()));
523                 updateIfChanged(CHANNEL_HEAT2_RUNTIME_DAY + i, new DecimalType(rt.getHeat2Runtime()));
524                 updateIfChanged(CHANNEL_COOL1_RUNTIME_DAY + i, new DecimalType(rt.getCool1Runtime()));
525                 updateIfChanged(CHANNEL_COOL2_RUNTIME_DAY + i, new DecimalType(rt.getCool2Runtime()));
526                 updateIfChanged(CHANNEL_AUX1_RUNTIME_DAY + i, new DecimalType(rt.getAux1Runtime()));
527                 updateIfChanged(CHANNEL_AUX2_RUNTIME_DAY + i, new DecimalType(rt.getAux2Runtime()));
528                 updateIfChanged(CHANNEL_FC_RUNTIME_DAY + i, new DecimalType(rt.getFreeCoolRuntime()));
529             }
530
531             response = getData("/query/info");
532             if (!isFutureValid(localUpdatesTask)) {
533                 return;
534             }
535             infoData = Objects.requireNonNull(gson.fromJson(response, VenstarInfoData.class));
536             updateUnits(infoData);
537             updateIfChanged(CHANNEL_HEATING_SETPOINT, getHeatingSetpoint());
538             updateIfChanged(CHANNEL_COOLING_SETPOINT, getCoolingSetpoint());
539             updateIfChanged(CHANNEL_SYSTEM_STATE, new StringType(infoData.getSystemState().stateName()));
540             updateIfChanged(CHANNEL_SYSTEM_MODE, new StringType(infoData.getSystemMode().modeName()));
541             updateIfChanged(CHANNEL_SYSTEM_STATE_RAW, new DecimalType(infoData.getSystemState().state()));
542             updateIfChanged(CHANNEL_SYSTEM_MODE_RAW, new DecimalType(infoData.getSystemMode().mode()));
543             updateIfChanged(CHANNEL_AWAY_MODE, new StringType(infoData.getAwayMode().modeName()));
544             updateIfChanged(CHANNEL_AWAY_MODE_RAW, new DecimalType(infoData.getAwayMode().mode()));
545             updateIfChanged(CHANNEL_FAN_MODE, new StringType(infoData.getFanMode().modeName()));
546             updateIfChanged(CHANNEL_FAN_MODE_RAW, new DecimalType(infoData.getFanMode().mode()));
547             updateIfChanged(CHANNEL_FAN_STATE, OnOffType.from(infoData.getFanState().stateName()));
548             updateIfChanged(CHANNEL_FAN_STATE_RAW, new DecimalType(infoData.getFanState().state()));
549             updateIfChanged(CHANNEL_SCHEDULE_MODE, new StringType(infoData.getScheduleMode().modeName()));
550             updateIfChanged(CHANNEL_SCHEDULE_MODE_RAW, new DecimalType(infoData.getScheduleMode().mode()));
551             updateIfChanged(CHANNEL_SCHEDULE_PART, new StringType(infoData.getSchedulePart().partName()));
552             updateIfChanged(CHANNEL_SCHEDULE_PART_RAW, new DecimalType(infoData.getSchedulePart().part()));
553
554             goOnline();
555         } catch (VenstarCommunicationException | JsonSyntaxException e) {
556             log.debug("Unable to fetch info data", e);
557             String message = e.getMessage();
558             goOffline(ThingStatusDetail.COMMUNICATION_ERROR, message != null ? message : "");
559         } catch (VenstarAuthenticationException e) {
560             goOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Authorization Failed");
561         }
562     }
563
564     private void updateIfChanged(String channelID, State state) {
565         ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelID);
566         State oldState = stateMap.put(channelUID.toString(), state);
567         if (!state.equals(oldState)) {
568             log.trace("updating channel {} with state {} (old state {})", channelUID, state, oldState);
569             updateState(channelUID, state);
570         }
571     }
572
573     private void updateUnits(VenstarInfoData infoData) {
574         int tempunits = infoData.getTempunits();
575         if (tempunits == 0) {
576             unitSystem = ImperialUnits.FAHRENHEIT;
577         } else if (tempunits == 1) {
578             unitSystem = SIUnits.CELSIUS;
579         } else {
580             log.warn("Thermostat returned unknown unit system type: {}", tempunits);
581         }
582     }
583
584     private String getData(String path) throws VenstarAuthenticationException, VenstarCommunicationException {
585         try {
586             URL getURL = new URL(baseURL, path);
587             Request request = httpClient.newRequest(getURL.toURI()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS);
588             return sendRequest(request);
589         } catch (MalformedURLException | URISyntaxException e) {
590             throw new VenstarCommunicationException(e);
591         }
592     }
593
594     private String postData(String path, Map<String, String> params)
595             throws VenstarAuthenticationException, VenstarCommunicationException {
596         try {
597             URL postURL = new URL(baseURL, path);
598             Request request = httpClient.newRequest(postURL.toURI()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
599                     .method(HttpMethod.POST);
600             params.forEach(request::param);
601             return sendRequest(request);
602         } catch (MalformedURLException | URISyntaxException e) {
603             throw new VenstarCommunicationException(e);
604         }
605     }
606
607     private String sendRequest(Request request) throws VenstarAuthenticationException, VenstarCommunicationException {
608         log.trace("sendRequest: requesting {}", request.getURI());
609         try {
610             ContentResponse response = request.send();
611             log.trace("Response code {}", response.getStatus());
612             if (response.getStatus() == 401) {
613                 throw new VenstarAuthenticationException();
614             }
615
616             if (response.getStatus() != 200) {
617                 throw new VenstarCommunicationException(
618                         "Error communicating with thermostat. Error Code: " + response.getStatus());
619             }
620             String content = response.getContentAsString();
621             log.trace("sendRequest: response {}", content);
622             return content;
623         } catch (InterruptedException | TimeoutException | ExecutionException e) {
624             throw new VenstarCommunicationException(e);
625         }
626     }
627
628     @SuppressWarnings("unchecked")
629     protected <U extends Quantity<U>> QuantityType<U> commandToQuantityType(Command command, Unit<U> defaultUnit) {
630         if (command instanceof QuantityType) {
631             return (QuantityType<U>) command;
632         }
633         return new QuantityType<U>(new BigDecimal(command.toString()), defaultUnit);
634     }
635
636     protected DecimalType commandToDecimalType(Command command) {
637         if (command instanceof DecimalType) {
638             return (DecimalType) command;
639         }
640         return new DecimalType(new BigDecimal(command.toString()));
641     }
642
643     private BigDecimal quantityToRoundedTemperature(QuantityType<Temperature> quantity, Unit<Temperature> unit)
644             throws IllegalArgumentException {
645         QuantityType<Temperature> temparatureQuantity = quantity.toUnit(unit);
646         if (temparatureQuantity == null) {
647             return quantity.toBigDecimal();
648         }
649
650         BigDecimal value = temparatureQuantity.toBigDecimal();
651         BigDecimal increment = CELSIUS == unit ? new BigDecimal("0.5") : new BigDecimal("1");
652         BigDecimal divisor = value.divide(increment, 0, RoundingMode.HALF_UP);
653         return divisor.multiply(increment);
654     }
655
656     @SuppressWarnings("serial")
657     private class VenstarAuthenticationException extends Exception {
658         public VenstarAuthenticationException() {
659             super("Invalid Credentials");
660         }
661     }
662
663     @SuppressWarnings("serial")
664     private class VenstarCommunicationException extends Exception {
665         public VenstarCommunicationException(Exception e) {
666             super(e);
667         }
668
669         public VenstarCommunicationException(String message) {
670             super(message);
671         }
672     }
673 }