2 * Copyright (c) 2010-2024 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.venstarthermostat.internal.handler;
15 import static org.openhab.binding.venstarthermostat.internal.VenstarThermostatBindingConstants.*;
16 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.net.MalformedURLException;
21 import java.net.URISyntaxException;
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;
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;
40 import javax.measure.Quantity;
41 import javax.measure.Unit;
42 import javax.measure.quantity.Dimensionless;
43 import javax.measure.quantity.Temperature;
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;
95 import com.google.gson.Gson;
96 import com.google.gson.GsonBuilder;
97 import com.google.gson.JsonSyntaxException;
100 * The {@link VenstarThermostatHandler} is responsible for handling commands, which are
101 * sent to one of the channels.
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
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;
119 private final HttpClient httpClient;
120 private final Gson gson;
122 // Venstar Thermostats are most commonly installed in the US, so start with a reasonable default.
123 private Unit<Temperature> unitSystem = ImperialUnits.FAHRENHEIT;
125 public VenstarThermostatHandler(Thing 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();
136 log.trace("VenstarThermostatHandler for thing {}", getThing().getUID());
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());
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());
155 if (config.refresh < 10) {
156 log.warn("refresh is too small: {}", config.refresh);
158 status.add(ConfigStatusMessage.Builder.error(CONFIG_REFRESH).withMessageKeySuffix(REFRESH_INVALID)
159 .withArguments(CONFIG_REFRESH).build());
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");
172 if (command instanceof RefreshType) {
173 log.debug("Refresh command requested for {}", channelUID);
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;
191 if (command instanceof StringType stringCommand) {
192 value = VenstarSystemMode.valueOf(stringCommand.toString().toUpperCase());
194 value = VenstarSystemMode.fromInt(((DecimalType) command).intValue());
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");
202 } else if (channelUID.getId().equals(CHANNEL_AWAY_MODE)) {
203 VenstarAwayMode value;
205 if (command instanceof StringType stringCommand) {
206 value = VenstarAwayMode.valueOf(stringCommand.toString().toUpperCase());
208 value = VenstarAwayMode.fromInt(((DecimalType) command).intValue());
210 log.debug("Setting away mode to {}", value);
212 updateIfChanged(CHANNEL_AWAY_MODE_RAW, new StringType(value.toString()));
213 } catch (IllegalArgumentException e) {
214 log.warn("Invalid Away Mode");
217 } else if (channelUID.getId().equals(CHANNEL_FAN_MODE)) {
218 VenstarFanMode value;
220 if (command instanceof StringType stringCommand) {
221 value = VenstarFanMode.valueOf(stringCommand.toString().toUpperCase());
223 value = VenstarFanMode.fromInt(((DecimalType) command).intValue());
225 log.debug("Setting fan mode to {}", value);
227 updateIfChanged(CHANNEL_FAN_MODE_RAW, new StringType(value.toString()));
228 } catch (IllegalArgumentException e) {
229 log.warn("Invalid Fan Mode");
231 } else if (channelUID.getId().equals(CHANNEL_SCHEDULE_MODE)) {
232 VenstarScheduleMode value;
234 if (command instanceof StringType stringCommand) {
235 value = VenstarScheduleMode.valueOf(stringCommand.toString().toUpperCase());
237 value = VenstarScheduleMode.fromInt(((DecimalType) command).intValue());
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");
247 startUpdatesTask(UPDATE_AFTER_COMMAND_SECONDS);
252 public void dispose() {
254 if (httpClient.isStarted()) {
257 } catch (Exception e) {
258 log.debug("Could not stop HttpClient", e);
264 public void initialize() {
268 protected void goOnline() {
269 if (getThing().getStatus() != ThingStatus.ONLINE) {
270 updateStatus(ThingStatus.ONLINE);
274 protected void goOffline(ThingStatusDetail detail, String reason) {
275 if (getThing().getStatus() != ThingStatus.OFFLINE) {
276 updateStatus(ThingStatus.OFFLINE, detail, reason);
280 @SuppressWarnings("null") // compiler does not see new URL(url) as never being null
281 private void connect() {
283 VenstarThermostatConfiguration config = getConfigAs(VenstarThermostatConfiguration.class);
285 baseURL = new URL(config.url);
286 if (!httpClient.isStarted()) {
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;
295 } catch (Exception e) {
296 log.debug("Could not conntect to URL {}", config.url, e);
297 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
302 * Start the poller after an initial delay
304 * @param initialDelay
306 private synchronized void startUpdatesTask(int initialDelay) {
308 updatesTask = scheduler.scheduleWithFixedDelay(this::updateData, initialDelay, refresh, TimeUnit.SECONDS);
314 @SuppressWarnings("null")
315 private void stopUpdateTasks() {
316 Future<?> localUpdatesTask = updatesTask;
317 if (isFutureValid(localUpdatesTask)) {
318 localUpdatesTask.cancel(false);
322 private boolean isFutureValid(@Nullable Future<?> future) {
323 return future != null && !future.isCancelled();
326 private State getTemperature() {
327 Optional<VenstarSensor> optSensor = sensorData.stream()
328 .filter(sensor -> "Thermostat".equalsIgnoreCase(sensor.getName())).findAny();
329 if (optSensor.isPresent()) {
330 return new QuantityType<Temperature>(optSensor.get().getTemp(), unitSystem);
333 return UnDefType.UNDEF;
336 private State getHumidity() {
337 Optional<VenstarSensor> optSensor = sensorData.stream()
338 .filter(sensor -> "Thermostat".equalsIgnoreCase(sensor.getName())).findAny();
339 if (optSensor.isPresent()) {
340 return new QuantityType<Dimensionless>(optSensor.get().getHum(), Units.PERCENT);
343 return UnDefType.UNDEF;
346 private State getOutdoorTemperature() {
347 Optional<VenstarSensor> optSensor = sensorData.stream()
348 .filter(sensor -> "Outdoor".equalsIgnoreCase(sensor.getName())).findAny();
349 if (optSensor.isPresent()) {
350 return new QuantityType<Temperature>(optSensor.get().getTemp(), unitSystem);
353 return UnDefType.UNDEF;
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);
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);
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);
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);
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.
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.
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);
397 log.debug("Updated thermostat");
398 // update our local copy until the next refresh occurs
399 infoData.setAwayMode(away);
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.
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.
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);
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
423 private QuantityType<Temperature> getCoolingSetpoint() {
424 return new QuantityType<Temperature>(infoData.getCooltemp(), unitSystem);
427 private QuantityType<Temperature> getHeatingSetpoint() {
428 return new QuantityType<Temperature>(infoData.getHeattemp(), unitSystem);
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 return ZonedDateTime.ofInstant(Instant.ofEpochSecond(runtime.getTimeStamp() - diff), zoneId);
438 private void updateScheduleMode(VenstarScheduleMode schedule) {
441 private void updateControls(double heat, double cool, VenstarSystemMode mode, VenstarFanMode fanmode) {
442 // this function corresponds to the thermostat local API POST /control instruction
443 // the function can be expanded with other parameters which are changed via POST /control
444 // controls that can be included are thermostat mode, fan mode, heat temp, cool temp (all done already)
445 Map<String, String> params = new HashMap<>();
447 params.put("heattemp", String.valueOf(heat));
450 params.put("cooltemp", String.valueOf(cool));
452 params.put("mode", String.valueOf(mode.mode()));
453 params.put("fan", String.valueOf(fanmode.mode()));
454 VenstarResponse res = updateThermostat("/control", params);
456 log.debug("Updated thermostat");
457 // update our local copy until the next refresh occurs
458 infoData.setCooltemp(cool);
459 infoData.setHeattemp(heat);
460 infoData.setSystemMode(mode);
461 infoData.setFanMode(fanmode);
462 // add other parameters here in the same way
467 * Function to send data to the thermostat and update the Thing state if there is an error
471 * @return VenstarResponse object or null if there was an error
473 private @Nullable VenstarResponse updateThermostat(String path, Map<String, String> params) {
475 String result = postData(path, params);
476 VenstarResponse res = gson.fromJson(result, VenstarResponse.class);
477 if (res != null && res.isSuccess()) {
480 String reason = res == null ? "invalid response" : res.getReason();
481 log.debug("Failed to update thermostat: {}", reason);
482 goOffline(ThingStatusDetail.COMMUNICATION_ERROR, reason);
484 } catch (VenstarCommunicationException | JsonSyntaxException e) {
485 log.debug("Unable to fetch info data", e);
486 String message = e.getMessage();
487 goOffline(ThingStatusDetail.COMMUNICATION_ERROR, message != null ? message : "");
488 } catch (VenstarAuthenticationException e) {
489 goOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Authorization Failed");
494 private void updateData() {
496 Future<?> localUpdatesTask = updatesTask;
497 String response = getData("/query/sensors");
498 if (!isFutureValid(localUpdatesTask)) {
501 VenstarSensorData res = gson.fromJson(response, VenstarSensorData.class);
502 sensorData = res.getSensors();
503 updateIfChanged(CHANNEL_TEMPERATURE, getTemperature());
504 updateIfChanged(CHANNEL_EXTERNAL_TEMPERATURE, getOutdoorTemperature());
505 updateIfChanged(CHANNEL_HUMIDITY, getHumidity());
507 response = getData("/query/runtimes");
508 if (!isFutureValid(localUpdatesTask)) {
512 runtimeData = Objects.requireNonNull(gson.fromJson(response, VenstarRuntimeData.class));
513 List<VenstarRuntime> runtimes = runtimeData.getRuntimes();
514 Collections.reverse(runtimes);// reverse the list so that the most recent runtime data is first in the list
515 int nRuntimes = Math.min(7, runtimes.size());// check how many runtimes are available, might be less than
516 // seven if equipment
517 // was reset, and also might be more than 7, so limit to 7
518 for (int i = 0; i < nRuntimes; i++) {
519 VenstarRuntime rt = runtimes.get(i);
520 updateIfChanged(CHANNEL_TIMESTAMP_RUNTIME_DAY + i, new DateTimeType(getTimestampRuntime(rt)));
521 updateIfChanged(CHANNEL_HEAT1_RUNTIME_DAY + i, new DecimalType(rt.getHeat1Runtime()));
522 updateIfChanged(CHANNEL_HEAT2_RUNTIME_DAY + i, new DecimalType(rt.getHeat2Runtime()));
523 updateIfChanged(CHANNEL_COOL1_RUNTIME_DAY + i, new DecimalType(rt.getCool1Runtime()));
524 updateIfChanged(CHANNEL_COOL2_RUNTIME_DAY + i, new DecimalType(rt.getCool2Runtime()));
525 updateIfChanged(CHANNEL_AUX1_RUNTIME_DAY + i, new DecimalType(rt.getAux1Runtime()));
526 updateIfChanged(CHANNEL_AUX2_RUNTIME_DAY + i, new DecimalType(rt.getAux2Runtime()));
527 updateIfChanged(CHANNEL_FC_RUNTIME_DAY + i, new DecimalType(rt.getFreeCoolRuntime()));
530 response = getData("/query/info");
531 if (!isFutureValid(localUpdatesTask)) {
534 infoData = Objects.requireNonNull(gson.fromJson(response, VenstarInfoData.class));
535 updateUnits(infoData);
536 updateIfChanged(CHANNEL_HEATING_SETPOINT, getHeatingSetpoint());
537 updateIfChanged(CHANNEL_COOLING_SETPOINT, getCoolingSetpoint());
538 updateIfChanged(CHANNEL_SYSTEM_STATE, new StringType(infoData.getSystemState().stateName()));
539 updateIfChanged(CHANNEL_SYSTEM_MODE, new StringType(infoData.getSystemMode().modeName()));
540 updateIfChanged(CHANNEL_SYSTEM_STATE_RAW, new DecimalType(infoData.getSystemState().state()));
541 updateIfChanged(CHANNEL_SYSTEM_MODE_RAW, new DecimalType(infoData.getSystemMode().mode()));
542 updateIfChanged(CHANNEL_AWAY_MODE, new StringType(infoData.getAwayMode().modeName()));
543 updateIfChanged(CHANNEL_AWAY_MODE_RAW, new DecimalType(infoData.getAwayMode().mode()));
544 updateIfChanged(CHANNEL_FAN_MODE, new StringType(infoData.getFanMode().modeName()));
545 updateIfChanged(CHANNEL_FAN_MODE_RAW, new DecimalType(infoData.getFanMode().mode()));
546 updateIfChanged(CHANNEL_FAN_STATE, OnOffType.from(infoData.getFanState().stateName()));
547 updateIfChanged(CHANNEL_FAN_STATE_RAW, new DecimalType(infoData.getFanState().state()));
548 updateIfChanged(CHANNEL_SCHEDULE_MODE, new StringType(infoData.getScheduleMode().modeName()));
549 updateIfChanged(CHANNEL_SCHEDULE_MODE_RAW, new DecimalType(infoData.getScheduleMode().mode()));
550 updateIfChanged(CHANNEL_SCHEDULE_PART, new StringType(infoData.getSchedulePart().partName()));
551 updateIfChanged(CHANNEL_SCHEDULE_PART_RAW, new DecimalType(infoData.getSchedulePart().part()));
554 } catch (VenstarCommunicationException | JsonSyntaxException e) {
555 log.debug("Unable to fetch info data", e);
556 String message = e.getMessage();
557 goOffline(ThingStatusDetail.COMMUNICATION_ERROR, message != null ? message : "");
558 } catch (VenstarAuthenticationException e) {
559 goOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Authorization Failed");
563 private void updateIfChanged(String channelID, State state) {
564 ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelID);
565 State oldState = stateMap.put(channelUID.toString(), state);
566 if (!state.equals(oldState)) {
567 log.trace("updating channel {} with state {} (old state {})", channelUID, state, oldState);
568 updateState(channelUID, state);
572 private void updateUnits(VenstarInfoData infoData) {
573 int tempunits = infoData.getTempunits();
574 if (tempunits == 0) {
575 unitSystem = ImperialUnits.FAHRENHEIT;
576 } else if (tempunits == 1) {
577 unitSystem = SIUnits.CELSIUS;
579 log.warn("Thermostat returned unknown unit system type: {}", tempunits);
583 private String getData(String path) throws VenstarAuthenticationException, VenstarCommunicationException {
585 URL getURL = new URL(baseURL, path);
586 Request request = httpClient.newRequest(getURL.toURI()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS);
587 return sendRequest(request);
588 } catch (MalformedURLException | URISyntaxException e) {
589 throw new VenstarCommunicationException(e);
593 private String postData(String path, Map<String, String> params)
594 throws VenstarAuthenticationException, VenstarCommunicationException {
596 URL postURL = new URL(baseURL, path);
597 Request request = httpClient.newRequest(postURL.toURI()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
598 .method(HttpMethod.POST);
599 params.forEach(request::param);
600 return sendRequest(request);
601 } catch (MalformedURLException | URISyntaxException e) {
602 throw new VenstarCommunicationException(e);
606 private String sendRequest(Request request) throws VenstarAuthenticationException, VenstarCommunicationException {
607 log.trace("sendRequest: requesting {}", request.getURI());
609 ContentResponse response = request.send();
610 log.trace("Response code {}", response.getStatus());
611 if (response.getStatus() == 401) {
612 throw new VenstarAuthenticationException();
615 if (response.getStatus() != 200) {
616 throw new VenstarCommunicationException(
617 "Error communicating with thermostat. Error Code: " + response.getStatus());
619 String content = response.getContentAsString();
620 log.trace("sendRequest: response {}", content);
622 } catch (InterruptedException | TimeoutException | ExecutionException e) {
623 throw new VenstarCommunicationException(e);
627 @SuppressWarnings("unchecked")
628 protected <U extends Quantity<U>> QuantityType<U> commandToQuantityType(Command command, Unit<U> defaultUnit) {
629 if (command instanceof QuantityType) {
630 return (QuantityType<U>) command;
632 return new QuantityType<U>(new BigDecimal(command.toString()), defaultUnit);
635 protected DecimalType commandToDecimalType(Command command) {
636 if (command instanceof DecimalType decimalCommand) {
637 return decimalCommand;
639 return new DecimalType(new BigDecimal(command.toString()));
642 private BigDecimal quantityToRoundedTemperature(QuantityType<Temperature> quantity, Unit<Temperature> unit)
643 throws IllegalArgumentException {
644 QuantityType<Temperature> temparatureQuantity = quantity.toUnit(unit);
645 if (temparatureQuantity == null) {
646 return quantity.toBigDecimal();
649 BigDecimal value = temparatureQuantity.toBigDecimal();
650 BigDecimal increment = CELSIUS == unit ? new BigDecimal("0.5") : new BigDecimal("1");
651 BigDecimal divisor = value.divide(increment, 0, RoundingMode.HALF_UP);
652 return divisor.multiply(increment);
655 @SuppressWarnings("serial")
656 private class VenstarAuthenticationException extends Exception {
657 public VenstarAuthenticationException() {
658 super("Invalid Credentials");
662 @SuppressWarnings("serial")
663 private class VenstarCommunicationException extends Exception {
664 public VenstarCommunicationException(Exception e) {
668 public VenstarCommunicationException(String message) {