2 * Copyright (c) 2010-2023 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.plugwiseha.internal.api.model;
15 import java.time.ZonedDateTime;
16 import java.time.format.DateTimeFormatter;
17 import java.util.Arrays;
18 import java.util.List;
19 import java.util.Optional;
21 import javax.xml.transform.Transformer;
22 import javax.xml.transform.TransformerConfigurationException;
23 import javax.xml.transform.TransformerFactory;
24 import javax.xml.transform.stream.StreamSource;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
30 import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionality;
31 import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityOffsetTemperature;
32 import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityRelay;
33 import org.openhab.binding.plugwiseha.internal.api.model.dto.ActuatorFunctionalityThermostat;
34 import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliance;
35 import org.openhab.binding.plugwiseha.internal.api.model.dto.Appliances;
36 import org.openhab.binding.plugwiseha.internal.api.model.dto.DomainObjects;
37 import org.openhab.binding.plugwiseha.internal.api.model.dto.GatewayInfo;
38 import org.openhab.binding.plugwiseha.internal.api.model.dto.Location;
39 import org.openhab.binding.plugwiseha.internal.api.model.dto.Locations;
40 import org.openhab.binding.plugwiseha.internal.api.model.dto.LocationsArray;
41 import org.openhab.binding.plugwiseha.internal.api.xml.PlugwiseHAXStream;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * The {@link PlugwiseHAController} class provides the interface to the Plugwise
47 * Home Automation API and stores/caches the object model for use by the various
48 * ThingHandlers of this binding.
50 * @author B. van Wetten - Initial contribution
53 public class PlugwiseHAController {
55 // Private member variables/constants
57 private static final int MAX_AGE_MINUTES_FULL_REFRESH = 15;
58 private static final DateTimeFormatter FORMAT = DateTimeFormatter.RFC_1123_DATE_TIME; // default Date format that
59 // will be used in conversion
61 private final Logger logger = LoggerFactory.getLogger(PlugwiseHAController.class);
63 private final HttpClient httpClient;
64 private final PlugwiseHAXStream xStream;
65 private final Transformer domainObjectsTransformer;
67 private final String host;
68 private final int port;
69 private final String username;
70 private final String smileId;
71 private final int maxAgeSecondsRefresh;
73 private @Nullable ZonedDateTime gatewayUpdateDateTime;
74 private @Nullable ZonedDateTime gatewayFullUpdateDateTime;
75 private @Nullable DomainObjects domainObjects;
77 public PlugwiseHAController(HttpClient httpClient, String host, int port, String username, String smileId,
78 int maxAgeSecondsRefresh) throws PlugwiseHAException {
79 this.httpClient = httpClient;
82 this.username = username;
83 this.smileId = smileId;
84 this.maxAgeSecondsRefresh = maxAgeSecondsRefresh;
86 this.xStream = new PlugwiseHAXStream();
88 ClassLoader localClassLoader = getClass().getClassLoader();
89 if (localClassLoader != null) {
90 this.domainObjectsTransformer = PlugwiseHAController
91 .setXSLT(new StreamSource(localClassLoader.getResourceAsStream("domain_objects.xslt")));
93 throw new PlugwiseHAException("PlugwiseHAController.domainObjectsTransformer could not be initialized");
99 public void start(Runnable callback) throws PlugwiseHAException {
104 public void refresh() throws PlugwiseHAException {
105 synchronized (this) {
106 this.getUpdatedDomainObjects();
110 // Public API methods
112 public GatewayInfo getGatewayInfo() throws PlugwiseHAException {
113 return getGatewayInfo(false);
116 public GatewayInfo getGatewayInfo(Boolean forceRefresh) throws PlugwiseHAException {
117 GatewayInfo gatewayInfo = null;
118 DomainObjects localDomainObjects = this.domainObjects;
119 if (localDomainObjects != null) {
120 gatewayInfo = localDomainObjects.getGatewayInfo();
123 if (!forceRefresh && gatewayInfo != null) {
124 this.logger.debug("Found Plugwise Home Automation gateway");
127 PlugwiseHAControllerRequest<DomainObjects> request;
129 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
131 request.setPath("/core/domain_objects");
132 request.addPathParameter("class", "Gateway");
134 DomainObjects domainObjects = executeRequest(request);
135 this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
137 return mergeDomainObjects(domainObjects).getGatewayInfo();
141 public Appliances getAppliances(Boolean forceRefresh) throws PlugwiseHAException {
142 Appliances appliances = null;
143 DomainObjects localDomainObjects = this.domainObjects;
144 if (localDomainObjects != null) {
145 appliances = localDomainObjects.getAppliances();
148 if (!forceRefresh && appliances != null) {
151 PlugwiseHAControllerRequest<DomainObjects> request;
153 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
155 request.setPath("/core/domain_objects");
156 request.addPathParameter("class", "Appliance");
158 DomainObjects domainObjects = executeRequest(request);
159 this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
161 if (domainObjects.getAppliances() != null) {
162 size = domainObjects.getAppliances().size();
164 this.logger.debug("Found {} Plugwise Home Automation appliance(s)", size);
166 return mergeDomainObjects(domainObjects).getAppliances();
170 public @Nullable Appliance getAppliance(String id) throws PlugwiseHAException {
171 Appliances appliances = this.getAppliances(false);
172 if (!appliances.containsKey(id)) {
173 appliances = this.getAppliances(true);
176 if (!appliances.containsKey(id)) {
177 this.logger.debug("Plugwise Home Automation Appliance with id {} is not known", id);
180 return appliances.get(id);
184 public Locations getLocations(Boolean forceRefresh) throws PlugwiseHAException {
185 Locations locations = null;
186 DomainObjects localDomainObjects = this.domainObjects;
187 if (localDomainObjects != null) {
188 locations = localDomainObjects.getLocations();
191 if (!forceRefresh && locations != null) {
194 PlugwiseHAControllerRequest<DomainObjects> request;
196 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
198 request.setPath("/core/domain_objects");
199 request.addPathParameter("class", "Location");
201 DomainObjects domainObjects = executeRequest(request);
202 this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
204 if (domainObjects.getLocations() != null) {
205 size = domainObjects.getLocations().size();
207 this.logger.debug("Found {} Plugwise Home Automation Zone(s)", size);
208 return mergeDomainObjects(domainObjects).getLocations();
212 public @Nullable Location getLocation(String id) throws PlugwiseHAException {
213 Locations locations = this.getLocations(false);
214 if (!locations.containsKey(id)) {
215 locations = this.getLocations(true);
218 if (!locations.containsKey(id)) {
219 this.logger.debug("Plugwise Home Automation Zone with {} is not known", id);
222 return locations.get(id);
226 public @Nullable DomainObjects getDomainObjects() throws PlugwiseHAException {
227 PlugwiseHAControllerRequest<DomainObjects> request;
229 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
231 request.setPath("/core/domain_objects");
232 request.addPathParameter("@locale", "en-US");
233 DomainObjects domainObjects = executeRequest(request);
235 ZonedDateTime serverTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
236 this.gatewayUpdateDateTime = serverTime;
237 this.gatewayFullUpdateDateTime = serverTime;
239 return mergeDomainObjects(domainObjects);
242 public @Nullable DomainObjects getUpdatedDomainObjects() throws PlugwiseHAException {
243 ZonedDateTime localGatewayUpdateDateTime = this.gatewayUpdateDateTime;
244 ZonedDateTime localGatewayFullUpdateDateTime = this.gatewayFullUpdateDateTime;
246 if (localGatewayUpdateDateTime == null || localGatewayFullUpdateDateTime == null) {
247 return getDomainObjects();
248 } else if (localGatewayUpdateDateTime.isBefore(ZonedDateTime.now().minusSeconds(maxAgeSecondsRefresh))) {
249 return getUpdatedDomainObjects(localGatewayUpdateDateTime);
250 } else if (localGatewayFullUpdateDateTime
251 .isBefore(ZonedDateTime.now().minusMinutes(MAX_AGE_MINUTES_FULL_REFRESH))) {
252 return getDomainObjects();
258 public @Nullable DomainObjects getUpdatedDomainObjects(ZonedDateTime since) throws PlugwiseHAException {
259 return getUpdatedDomainObjects(since.toEpochSecond());
262 public @Nullable DomainObjects getUpdatedDomainObjects(Long since) throws PlugwiseHAException {
263 PlugwiseHAControllerRequest<DomainObjects> request;
265 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
267 request.setPath("/core/domain_objects");
268 request.addPathFilter("modified_date", "ge", since);
269 request.addPathFilter("deleted_date", "ge", "0");
270 request.addPathParameter("@memberModifiedDate", since);
271 request.addPathParameter("@locale", "en-US");
273 DomainObjects domainObjects = executeRequest(request);
274 this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
276 return mergeDomainObjects(domainObjects);
279 public void setLocationThermostat(Location location, Double temperature) throws PlugwiseHAException {
280 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
281 Optional<ActuatorFunctionality> thermostat = location.getActuatorFunctionalities().getFunctionalityThermostat();
283 if (thermostat.isPresent()) {
284 request.setPath("/core/locations");
286 request.addPathParameter("id", String.format("%s/thermostat", location.getId()));
287 request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
288 request.setBodyParameter(new ActuatorFunctionalityThermostat(temperature));
290 executeRequest(request);
294 public void setThermostat(Appliance appliance, Double temperature) throws PlugwiseHAException {
295 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
296 Optional<ActuatorFunctionality> thermostat = appliance.getActuatorFunctionalities()
297 .getFunctionalityThermostat();
299 if (thermostat.isPresent()) {
300 request.setPath("/core/appliances");
302 request.addPathParameter("id", String.format("%s/thermostat", appliance.getId()));
303 request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
304 request.setBodyParameter(new ActuatorFunctionalityThermostat(temperature));
306 executeRequest(request);
310 public void setOffsetTemperature(Appliance appliance, Double temperature) throws PlugwiseHAException {
311 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
312 Optional<ActuatorFunctionality> offsetTemperatureFunctionality = appliance.getActuatorFunctionalities()
313 .getFunctionalityOffsetTemperature();
315 if (offsetTemperatureFunctionality.isPresent()) {
316 request.setPath("/core/appliances");
318 request.addPathParameter("id", String.format("%s/offset", appliance.getId()));
319 request.addPathParameter("id", String.format("%s", offsetTemperatureFunctionality.get().getId()));
320 request.setBodyParameter(new ActuatorFunctionalityOffsetTemperature(temperature));
322 executeRequest(request);
326 public void setPreHeating(Location location, Boolean state) throws PlugwiseHAException {
327 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
328 Optional<ActuatorFunctionality> thermostat = location.getActuatorFunctionalities().getFunctionalityThermostat();
330 request.setPath("/core/locations");
331 request.addPathParameter("id", String.format("%s/thermostat", location.getId()));
332 request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
333 request.setBodyParameter(new ActuatorFunctionalityThermostat(state, null, null));
335 executeRequest(request);
338 public void setAllowCooling(Location location, Boolean state) throws PlugwiseHAException {
339 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
340 Optional<ActuatorFunctionality> thermostat = location.getActuatorFunctionalities().getFunctionalityThermostat();
342 request.setPath("/core/locations");
343 request.addPathParameter("id", String.format("%s/thermostat", location.getId()));
344 request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
345 request.setBodyParameter(new ActuatorFunctionalityThermostat(null, state, null));
347 executeRequest(request);
350 public void setRegulationControl(Location location, String state) throws PlugwiseHAException {
351 List<String> allowStates = Arrays.asList("active", "passive", "off");
352 if (!allowStates.contains(state.toLowerCase())) {
353 this.logger.warn("Trying to set the regulation control to an invalid state");
357 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
358 Optional<ActuatorFunctionality> thermostat = location.getActuatorFunctionalities().getFunctionalityThermostat();
360 request.setPath("/core/locations");
361 request.addPathParameter("id", String.format("%s/thermostat", location.getId()));
362 request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
363 request.setBodyParameter(new ActuatorFunctionalityThermostat(null, null, state));
365 executeRequest(request);
368 public void setRelay(Appliance appliance, Boolean state) throws PlugwiseHAException {
369 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
371 request.setPath("/core/appliances");
372 request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
373 request.setBodyParameter(new ActuatorFunctionalityRelay(state ? "on" : "off"));
375 executeRequest(request);
378 public void setRelayLock(Appliance appliance, Boolean state) throws PlugwiseHAException {
379 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
381 request.setPath("/core/appliances");
382 request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
383 request.setBodyParameter(new ActuatorFunctionalityRelay(null, state));
385 executeRequest(request);
388 public void setPresetScene(Location location, String state) throws PlugwiseHAException {
389 List<String> allowStates = Arrays.asList("home", "asleep", "away", "vacation", "no_frost");
390 if (!allowStates.contains(state.toLowerCase())) {
391 this.logger.warn("Trying to set the preset scene to an invalid state");
395 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
397 request.setPath("/core/locations");
398 request.addPathParameter("id", String.format("%s", location.getId()));
400 Location locationWithChangesOnly = new Location();
401 locationWithChangesOnly.setPreset(state);
402 locationWithChangesOnly.setId(location.getId());
404 LocationsArray locations = new LocationsArray();
405 locations.items = new Location[] { locationWithChangesOnly };
407 request.setBodyParameter(locations);
409 executeRequest(request);
412 public ZonedDateTime ping() throws PlugwiseHAException {
413 PlugwiseHAControllerRequest<Void> request;
415 request = newRequest(Void.class, null);
417 request.setPath("/cache/gateways");
418 request.addPathParameter("ping");
420 executeRequest(request);
422 return ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
425 // Protected and private methods
427 private static Transformer setXSLT(StreamSource xsltSource) throws PlugwiseHAException {
429 return TransformerFactory.newInstance().newTransformer(xsltSource);
430 } catch (TransformerConfigurationException e) {
431 throw new PlugwiseHAException("Could not create XML transformer", e);
435 private <T> PlugwiseHAControllerRequest<T> newRequest(Class<T> responseType, @Nullable Transformer transformer) {
436 return new PlugwiseHAControllerRequest<T>(responseType, this.xStream, transformer, this.httpClient, this.host,
437 this.port, this.username, this.smileId);
440 private <T> PlugwiseHAControllerRequest<T> newRequest(Class<T> responseType) {
441 return new PlugwiseHAControllerRequest<T>(responseType, this.xStream, null, this.httpClient, this.host,
442 this.port, this.username, this.smileId);
445 @SuppressWarnings("null")
446 private <T> T executeRequest(PlugwiseHAControllerRequest<T> request) throws PlugwiseHAException {
448 result = request.execute();
452 private DomainObjects mergeDomainObjects(@Nullable DomainObjects updatedDomainObjects) {
453 DomainObjects localDomainObjects = this.domainObjects;
454 if (localDomainObjects == null && updatedDomainObjects != null) {
455 this.domainObjects = updatedDomainObjects;
456 return updatedDomainObjects;
457 } else if (localDomainObjects != null && updatedDomainObjects == null) {
458 return localDomainObjects;
459 } else if (localDomainObjects != null && updatedDomainObjects != null) {
460 Appliances appliances = updatedDomainObjects.getAppliances();
461 Locations locations = updatedDomainObjects.getLocations();
463 if (appliances != null) {
464 localDomainObjects.mergeAppliances(appliances);
467 if (locations != null) {
468 localDomainObjects.mergeLocations(locations);
470 this.domainObjects = localDomainObjects;
471 return localDomainObjects;
473 return new DomainObjects();