2 * Copyright (c) 2010-2022 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.xml.PlugwiseHAXStream;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link PlugwiseHAController} class provides the interface to the Plugwise
46 * Home Automation API and stores/caches the object model for use by the various
47 * ThingHandlers of this binding.
49 * @author B. van Wetten - Initial contribution
52 public class PlugwiseHAController {
54 // Private member variables/constants
56 private static final int MAX_AGE_MINUTES_FULL_REFRESH = 15;
57 private static final DateTimeFormatter FORMAT = DateTimeFormatter.RFC_1123_DATE_TIME; // default Date format that
58 // will be used in conversion
60 private final Logger logger = LoggerFactory.getLogger(PlugwiseHAController.class);
62 private final HttpClient httpClient;
63 private final PlugwiseHAXStream xStream;
64 private final Transformer domainObjectsTransformer;
66 private final String host;
67 private final int port;
68 private final String username;
69 private final String smileId;
70 private final int maxAgeSecondsRefresh;
72 private @Nullable ZonedDateTime gatewayUpdateDateTime;
73 private @Nullable ZonedDateTime gatewayFullUpdateDateTime;
74 private @Nullable DomainObjects domainObjects;
76 public PlugwiseHAController(HttpClient httpClient, String host, int port, String username, String smileId,
77 int maxAgeSecondsRefresh) throws PlugwiseHAException {
78 this.httpClient = httpClient;
81 this.username = username;
82 this.smileId = smileId;
83 this.maxAgeSecondsRefresh = maxAgeSecondsRefresh;
85 this.xStream = new PlugwiseHAXStream();
87 ClassLoader localClassLoader = getClass().getClassLoader();
88 if (localClassLoader != null) {
89 this.domainObjectsTransformer = PlugwiseHAController
90 .setXSLT(new StreamSource(localClassLoader.getResourceAsStream("domain_objects.xslt")));
92 throw new PlugwiseHAException("PlugwiseHAController.domainObjectsTransformer could not be initialized");
98 public void start(Runnable callback) throws PlugwiseHAException {
103 public void refresh() throws PlugwiseHAException {
104 synchronized (this) {
105 this.getUpdatedDomainObjects();
109 // Public API methods
111 public GatewayInfo getGatewayInfo() throws PlugwiseHAException {
112 return getGatewayInfo(false);
115 public GatewayInfo getGatewayInfo(Boolean forceRefresh) throws PlugwiseHAException {
116 GatewayInfo gatewayInfo = null;
117 DomainObjects localDomainObjects = this.domainObjects;
118 if (localDomainObjects != null) {
119 gatewayInfo = localDomainObjects.getGatewayInfo();
122 if (!forceRefresh && gatewayInfo != null) {
123 this.logger.debug("Found Plugwise Home Automation gateway");
126 PlugwiseHAControllerRequest<DomainObjects> request;
128 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
130 request.setPath("/core/domain_objects");
131 request.addPathParameter("class", "Gateway");
133 DomainObjects domainObjects = executeRequest(request);
134 this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
136 return mergeDomainObjects(domainObjects).getGatewayInfo();
140 public Appliances getAppliances(Boolean forceRefresh) throws PlugwiseHAException {
141 Appliances appliances = null;
142 DomainObjects localDomainObjects = this.domainObjects;
143 if (localDomainObjects != null) {
144 appliances = localDomainObjects.getAppliances();
147 if (!forceRefresh && appliances != null) {
150 PlugwiseHAControllerRequest<DomainObjects> request;
152 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
154 request.setPath("/core/domain_objects");
155 request.addPathParameter("class", "Appliance");
157 DomainObjects domainObjects = executeRequest(request);
158 this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
160 if (!(domainObjects.getAppliances() == null)) {
161 size = domainObjects.getAppliances().size();
163 this.logger.debug("Found {} Plugwise Home Automation appliance(s)", size);
165 return mergeDomainObjects(domainObjects).getAppliances();
169 public @Nullable Appliance getAppliance(String id) throws PlugwiseHAException {
170 Appliances appliances = this.getAppliances(false);
171 if (!appliances.containsKey(id)) {
172 appliances = this.getAppliances(true);
175 if (!appliances.containsKey(id)) {
176 this.logger.debug("Plugwise Home Automation Appliance with id {} is not known", id);
179 return appliances.get(id);
183 public Locations getLocations(Boolean forceRefresh) throws PlugwiseHAException {
184 Locations locations = null;
185 DomainObjects localDomainObjects = this.domainObjects;
186 if (localDomainObjects != null) {
187 locations = localDomainObjects.getLocations();
190 if (!forceRefresh && locations != null) {
193 PlugwiseHAControllerRequest<DomainObjects> request;
195 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
197 request.setPath("/core/domain_objects");
198 request.addPathParameter("class", "Location");
200 DomainObjects domainObjects = executeRequest(request);
201 this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
203 if (!(domainObjects.getLocations() == null)) {
204 size = domainObjects.getLocations().size();
206 this.logger.debug("Found {} Plugwise Home Automation Zone(s)", size);
207 return mergeDomainObjects(domainObjects).getLocations();
211 public @Nullable Location getLocation(String id) throws PlugwiseHAException {
212 Locations locations = this.getLocations(false);
213 if (!locations.containsKey(id)) {
214 locations = this.getLocations(true);
217 if (!locations.containsKey(id)) {
218 this.logger.debug("Plugwise Home Automation Zone with {} is not known", id);
221 return locations.get(id);
225 public @Nullable DomainObjects getDomainObjects() throws PlugwiseHAException {
226 PlugwiseHAControllerRequest<DomainObjects> request;
228 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
230 request.setPath("/core/domain_objects");
231 request.addPathParameter("@locale", "en-US");
232 DomainObjects domainObjects = executeRequest(request);
234 ZonedDateTime serverTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
235 this.gatewayUpdateDateTime = serverTime;
236 this.gatewayFullUpdateDateTime = serverTime;
238 return mergeDomainObjects(domainObjects);
241 public @Nullable DomainObjects getUpdatedDomainObjects() throws PlugwiseHAException {
242 ZonedDateTime localGatewayUpdateDateTime = this.gatewayUpdateDateTime;
243 ZonedDateTime localGatewayFullUpdateDateTime = this.gatewayFullUpdateDateTime;
245 if (localGatewayUpdateDateTime == null || localGatewayFullUpdateDateTime == null) {
246 return getDomainObjects();
247 } else if (localGatewayUpdateDateTime.isBefore(ZonedDateTime.now().minusSeconds(maxAgeSecondsRefresh))) {
248 return getUpdatedDomainObjects(localGatewayUpdateDateTime);
249 } else if (localGatewayFullUpdateDateTime
250 .isBefore(ZonedDateTime.now().minusMinutes(MAX_AGE_MINUTES_FULL_REFRESH))) {
251 return getDomainObjects();
257 public @Nullable DomainObjects getUpdatedDomainObjects(ZonedDateTime since) throws PlugwiseHAException {
258 return getUpdatedDomainObjects(since.toEpochSecond());
261 public @Nullable DomainObjects getUpdatedDomainObjects(Long since) throws PlugwiseHAException {
262 PlugwiseHAControllerRequest<DomainObjects> request;
264 request = newRequest(DomainObjects.class, this.domainObjectsTransformer);
266 request.setPath("/core/domain_objects");
267 request.addPathFilter("modified_date", "ge", since);
268 request.addPathFilter("deleted_date", "ge", "0");
269 request.addPathParameter("@memberModifiedDate", since);
270 request.addPathParameter("@locale", "en-US");
272 DomainObjects domainObjects = executeRequest(request);
273 this.gatewayUpdateDateTime = ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
275 return mergeDomainObjects(domainObjects);
278 public void setLocationThermostat(Location location, Double temperature) throws PlugwiseHAException {
279 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
280 Optional<ActuatorFunctionality> thermostat = location.getActuatorFunctionalities().getFunctionalityThermostat();
282 if (thermostat.isPresent()) {
283 request.setPath("/core/locations");
285 request.addPathParameter("id", String.format("%s/thermostat", location.getId()));
286 request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
287 request.setBodyParameter(new ActuatorFunctionalityThermostat(temperature));
289 executeRequest(request);
293 public void setThermostat(Appliance appliance, Double temperature) throws PlugwiseHAException {
294 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
295 Optional<ActuatorFunctionality> thermostat = appliance.getActuatorFunctionalities()
296 .getFunctionalityThermostat();
298 if (thermostat.isPresent()) {
299 request.setPath("/core/appliances");
301 request.addPathParameter("id", String.format("%s/thermostat", appliance.getId()));
302 request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
303 request.setBodyParameter(new ActuatorFunctionalityThermostat(temperature));
305 executeRequest(request);
309 public void setOffsetTemperature(Appliance appliance, Double temperature) throws PlugwiseHAException {
310 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
311 Optional<ActuatorFunctionality> offsetTemperatureFunctionality = appliance.getActuatorFunctionalities()
312 .getFunctionalityOffsetTemperature();
314 if (offsetTemperatureFunctionality.isPresent()) {
315 request.setPath("/core/appliances");
317 request.addPathParameter("id", String.format("%s/offset", appliance.getId()));
318 request.addPathParameter("id", String.format("%s", offsetTemperatureFunctionality.get().getId()));
319 request.setBodyParameter(new ActuatorFunctionalityOffsetTemperature(temperature));
321 executeRequest(request);
325 public void switchRelay(Appliance appliance, String state) throws PlugwiseHAException {
326 List<String> allowStates = Arrays.asList("on", "off");
327 if (allowStates.contains(state.toLowerCase())) {
328 if (state.toLowerCase().equals("on")) {
329 switchRelayOn(appliance);
331 switchRelayOff(appliance);
336 public void setPreHeating(Location location, Boolean state) throws PlugwiseHAException {
337 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
338 Optional<ActuatorFunctionality> thermostat = location.getActuatorFunctionalities().getFunctionalityThermostat();
340 request.setPath("/core/locations");
341 request.addPathParameter("id", String.format("%s/thermostat", location.getId()));
342 request.addPathParameter("id", String.format("%s", thermostat.get().getId()));
343 request.setBodyParameter(new ActuatorFunctionalityThermostat(state));
345 executeRequest(request);
348 public void switchRelayOn(Appliance appliance) throws PlugwiseHAException {
349 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
351 request.setPath("/core/appliances");
352 request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
353 request.setBodyParameter(new ActuatorFunctionalityRelay("on"));
355 executeRequest(request);
358 public void switchRelayOff(Appliance appliance) throws PlugwiseHAException {
359 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
361 request.setPath("/core/appliances");
362 request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
363 request.setBodyParameter(new ActuatorFunctionalityRelay("off"));
365 executeRequest(request);
368 public void switchRelayLock(Appliance appliance, String state) throws PlugwiseHAException {
369 List<String> allowStates = Arrays.asList("on", "off");
370 if (allowStates.contains(state.toLowerCase())) {
371 if (state.toLowerCase().equals("on")) {
372 switchRelayLockOn(appliance);
374 switchRelayLockOff(appliance);
379 public void switchRelayLockOff(Appliance appliance) throws PlugwiseHAException {
380 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
382 request.setPath("/core/appliances");
383 request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
384 request.setBodyParameter(new ActuatorFunctionalityRelay(null, false));
386 executeRequest(request);
389 public void switchRelayLockOn(Appliance appliance) throws PlugwiseHAException {
390 PlugwiseHAControllerRequest<Void> request = newRequest(Void.class);
392 request.setPath("/core/appliances");
393 request.addPathParameter("id", String.format("%s/relay", appliance.getId()));
394 request.setBodyParameter(new ActuatorFunctionalityRelay(null, true));
396 executeRequest(request);
399 public ZonedDateTime ping() throws PlugwiseHAException {
400 PlugwiseHAControllerRequest<Void> request;
402 request = newRequest(Void.class, null);
404 request.setPath("/cache/gateways");
405 request.addPathParameter("ping");
407 executeRequest(request);
409 return ZonedDateTime.parse(request.getServerDateTime(), PlugwiseHAController.FORMAT);
412 // Protected and private methods
414 private static Transformer setXSLT(StreamSource xsltSource) throws PlugwiseHAException {
416 return TransformerFactory.newInstance().newTransformer(xsltSource);
417 } catch (TransformerConfigurationException e) {
418 throw new PlugwiseHAException("Could not create XML transformer", e);
422 private <T> PlugwiseHAControllerRequest<T> newRequest(Class<T> responseType, @Nullable Transformer transformer) {
423 return new PlugwiseHAControllerRequest<T>(responseType, this.xStream, transformer, this.httpClient, this.host,
424 this.port, this.username, this.smileId);
427 private <T> PlugwiseHAControllerRequest<T> newRequest(Class<T> responseType) {
428 return new PlugwiseHAControllerRequest<T>(responseType, this.xStream, null, this.httpClient, this.host,
429 this.port, this.username, this.smileId);
432 @SuppressWarnings("null")
433 private <T> T executeRequest(PlugwiseHAControllerRequest<T> request) throws PlugwiseHAException {
435 result = request.execute();
439 private DomainObjects mergeDomainObjects(@Nullable DomainObjects updatedDomainObjects) {
440 DomainObjects localDomainObjects = this.domainObjects;
441 if (localDomainObjects == null && updatedDomainObjects != null) {
442 this.domainObjects = updatedDomainObjects;
443 return updatedDomainObjects;
444 } else if (localDomainObjects != null && updatedDomainObjects == null) {
445 return localDomainObjects;
446 } else if (localDomainObjects != null && updatedDomainObjects != null) {
447 Appliances appliances = updatedDomainObjects.getAppliances();
448 Locations locations = updatedDomainObjects.getLocations();
450 if (appliances != null) {
451 localDomainObjects.mergeAppliances(appliances);
454 if (locations != null) {
455 localDomainObjects.mergeLocations(locations);
457 this.domainObjects = localDomainObjects;
458 return localDomainObjects;
460 return new DomainObjects();