]> git.basschouten.com Git - openhab-addons.git/blob
93aa7650f1961a27a6489cfb080bac0654710e36
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.livisismarthome.internal.client;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.io.OutputStream;
19 import java.net.HttpURLConnection;
20 import java.nio.charset.StandardCharsets;
21 import java.util.Arrays;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Optional;
26 import java.util.stream.Collectors;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.http.HttpHeader;
31 import org.eclipse.jetty.http.HttpMethod;
32 import org.eclipse.jetty.http.HttpStatus;
33 import org.openhab.binding.livisismarthome.internal.LivisiBindingConstants;
34 import org.openhab.binding.livisismarthome.internal.client.api.entity.StatusResponseDTO;
35 import org.openhab.binding.livisismarthome.internal.client.api.entity.action.ActionDTO;
36 import org.openhab.binding.livisismarthome.internal.client.api.entity.action.ShutterActionDTO;
37 import org.openhab.binding.livisismarthome.internal.client.api.entity.action.ShutterActionType;
38 import org.openhab.binding.livisismarthome.internal.client.api.entity.action.StateActionSetterDTO;
39 import org.openhab.binding.livisismarthome.internal.client.api.entity.capability.CapabilityDTO;
40 import org.openhab.binding.livisismarthome.internal.client.api.entity.capability.CapabilityStateDTO;
41 import org.openhab.binding.livisismarthome.internal.client.api.entity.device.DeviceDTO;
42 import org.openhab.binding.livisismarthome.internal.client.api.entity.device.DeviceStateDTO;
43 import org.openhab.binding.livisismarthome.internal.client.api.entity.device.StateDTO;
44 import org.openhab.binding.livisismarthome.internal.client.api.entity.error.ErrorResponseDTO;
45 import org.openhab.binding.livisismarthome.internal.client.api.entity.location.LocationDTO;
46 import org.openhab.binding.livisismarthome.internal.client.api.entity.message.MessageDTO;
47 import org.openhab.binding.livisismarthome.internal.client.exception.ApiException;
48 import org.openhab.binding.livisismarthome.internal.client.exception.AuthenticationException;
49 import org.openhab.binding.livisismarthome.internal.client.exception.ControllerOfflineException;
50 import org.openhab.binding.livisismarthome.internal.client.exception.InvalidActionTriggeredException;
51 import org.openhab.binding.livisismarthome.internal.client.exception.RemoteAccessNotAllowedException;
52 import org.openhab.binding.livisismarthome.internal.client.exception.ServiceUnavailableException;
53 import org.openhab.binding.livisismarthome.internal.client.exception.SessionExistsException;
54 import org.openhab.binding.livisismarthome.internal.client.exception.SessionNotFoundException;
55 import org.openhab.binding.livisismarthome.internal.handler.LivisiBridgeConfiguration;
56 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
57 import org.openhab.core.auth.client.oauth2.OAuthClientService;
58 import org.openhab.core.auth.client.oauth2.OAuthException;
59 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 import com.google.gson.JsonSyntaxException;
64
65 /**
66  * The main client that handles the communication with the LIVISI SmartHome API service.
67  *
68  * @author Oliver Kuhl - Initial contribution
69  * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
70  * @author Sven Strohschein - Renamed from Innogy to Livisi and refactored
71  */
72 @NonNullByDefault
73 public class LivisiClient {
74
75     private final Logger logger = LoggerFactory.getLogger(LivisiClient.class);
76
77     private final GsonOptional gson;
78     private final LivisiBridgeConfiguration bridgeConfiguration;
79     private final OAuthClientService oAuthService;
80     private final URLConnectionFactory connectionFactory;
81
82     public LivisiClient(final LivisiBridgeConfiguration bridgeConfiguration, final OAuthClientService oAuthService,
83             final URLConnectionFactory connectionFactory) {
84         this.bridgeConfiguration = bridgeConfiguration;
85         this.oAuthService = oAuthService;
86         this.connectionFactory = connectionFactory;
87         this.gson = new GsonOptional();
88     }
89
90     /**
91      * Gets the status
92      * As the API returns the details of the SmartHome controller (SHC), the config version is returned.
93      *
94      * @return config version
95      */
96     public String refreshStatus() throws IOException {
97         logger.debug("Get LIVISI SmartHome status...");
98         final Optional<StatusResponseDTO> status = executeGet(URLCreator.createStatusURL(bridgeConfiguration.host),
99                 StatusResponseDTO.class);
100
101         if (status.isPresent()) {
102             String configVersion = status.get().getConfigVersion();
103             logger.debug("LIVISI SmartHome status loaded. Configuration version is {}.", configVersion);
104             return configVersion;
105         }
106         return "";
107     }
108
109     /**
110      * Executes a HTTP GET request with default headers and returns data as object of type T.
111      *
112      * @param url request URL
113      * @param clazz type of data to return
114      * @return response content
115      */
116     private <T> Optional<T> executeGet(final String url, final Class<T> clazz) throws IOException {
117
118         HttpURLConnection connection = createBaseRequest(url, HttpMethod.GET);
119         String responseContent = executeRequest(connection);
120         return gson.fromJson(responseContent, clazz);
121     }
122
123     /**
124      * Executes a HTTP GET request with default headers and returns data as List of type T.
125      *
126      * @param url request URL
127      * @param clazz array type of data to return as list
128      * @return response content (as a List)
129      */
130     private <T> List<T> executeGetList(final String url, final Class<T[]> clazz) throws IOException {
131         Optional<T[]> objects = executeGet(url, clazz);
132         if (objects.isPresent()) {
133             return Arrays.asList(objects.get());
134         }
135         return Collections.emptyList();
136     }
137
138     /**
139      * Executes a HTTP POST request with the given {@link ActionDTO} as content.
140      *
141      * @param url request URL
142      * @param action action to execute
143      */
144     private void executePost(final String url, final ActionDTO action) throws IOException {
145         final String json = gson.toJson(action);
146         final byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
147         logger.debug("Action {} JSON: {}", action.getType(), json);
148
149         HttpURLConnection connection = createBaseRequest(url, HttpMethod.POST);
150         connection.setDoOutput(true);
151         connection.setRequestProperty(HttpHeader.CONTENT_LENGTH.asString(), String.valueOf(jsonBytes.length));
152         try (OutputStream outputStream = connection.getOutputStream()) {
153             outputStream.write(jsonBytes);
154         }
155
156         executeRequest(connection);
157     }
158
159     private String executeRequest(HttpURLConnection connection) throws IOException {
160         StringBuilder stringBuilder = new StringBuilder();
161         try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
162             String line;
163             while ((line = reader.readLine()) != null) {
164                 stringBuilder.append(line);
165             }
166         }
167
168         String responseContent = stringBuilder.toString();
169         logger.trace("RAW-RESPONSE: {}", responseContent);
170         handleResponseErrors(connection, responseContent);
171         return normalizeResponseContent(responseContent);
172     }
173
174     private HttpURLConnection createBaseRequest(String url, HttpMethod httpMethod) throws IOException {
175
176         final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
177         return connectionFactory.createBaseRequest(url, httpMethod, accessTokenResponse);
178     }
179
180     public AccessTokenResponse getAccessTokenResponse() throws IOException {
181         try {
182             @Nullable
183             final AccessTokenResponse accessTokenResponse = oAuthService.getAccessTokenResponse();
184             if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
185                     || accessTokenResponse.getAccessToken().isBlank()) {
186                 throw new AuthenticationException("No LIVISI SmartHome access token. Is this thing authorized?");
187             }
188             return accessTokenResponse;
189         } catch (OAuthException | OAuthResponseException e) {
190             throw new AuthenticationException("Error fetching access token: " + e.getMessage());
191         }
192     }
193
194     /**
195      * Handles errors from the {@link org.eclipse.jetty.client.api.ContentResponse} and throws the following errors:
196      *
197      * @param connection connection
198      * @param responseContent response content
199      * @throws ControllerOfflineException thrown, if the LIVISI SmartHome controller (SHC) is offline.
200      */
201     private void handleResponseErrors(final HttpURLConnection connection, final String responseContent)
202             throws IOException {
203
204         final int status = connection.getResponseCode();
205         if (HttpStatus.OK_200 == status) {
206             logger.debug("Statuscode is OK: [{}]", connection.getURL());
207         } else if (HttpStatus.SERVICE_UNAVAILABLE_503 == status) {
208             throw new ServiceUnavailableException("LIVISI SmartHome service is unavailable (503).");
209         } else {
210             logger.debug("Statuscode {} is NOT OK: [{}]", status, connection.getURL());
211             String content = normalizeResponseContent(responseContent);
212             try {
213                 logger.trace("Response error content: {}", content);
214                 final Optional<ErrorResponseDTO> errorOptional = gson.fromJson(content, ErrorResponseDTO.class);
215                 if (errorOptional.isPresent()) {
216                     ErrorResponseDTO error = errorOptional.get();
217                     switch (error.getCode()) {
218                         case ErrorResponseDTO.ERR_SESSION_EXISTS:
219                             throw new SessionExistsException("Session exists: " + error.getDescription());
220                         case ErrorResponseDTO.ERR_SESSION_NOT_FOUND:
221                             throw new SessionNotFoundException("Session not found: " + error.getDescription());
222                         case ErrorResponseDTO.ERR_CONTROLLER_OFFLINE:
223                             throw new ControllerOfflineException("Controller offline: " + error.getDescription());
224                         case ErrorResponseDTO.ERR_REMOTE_ACCESS_NOT_ALLOWED:
225                             throw new RemoteAccessNotAllowedException(
226                                     "Remote access not allowed. Access is allowed only from the SHC device network.");
227                         case ErrorResponseDTO.ERR_INVALID_ACTION_TRIGGERED:
228                             throw new InvalidActionTriggeredException(
229                                     "Invalid action triggered. Message: " + error.getDescription());
230                     }
231                     throw new ApiException("Unknown error: " + error);
232                 }
233             } catch (final JsonSyntaxException e) {
234                 throw new ApiException("Invalid JSON syntax in error response: " + content, e);
235             }
236         }
237     }
238
239     /**
240      * Sets a new state of a SwitchActuator.
241      */
242     public void setSwitchActuatorState(final String capabilityId, final boolean state) throws IOException {
243         executePost(createActionURL(),
244                 new StateActionSetterDTO(capabilityId, CapabilityDTO.TYPE_SWITCHACTUATOR, state));
245     }
246
247     /**
248      * Sets the dimmer level of a DimmerActuator.
249      */
250     public void setDimmerActuatorState(final String capabilityId, final int dimLevel) throws IOException {
251         executePost(createActionURL(),
252                 new StateActionSetterDTO(capabilityId, CapabilityDTO.TYPE_DIMMERACTUATOR, dimLevel));
253     }
254
255     /**
256      * Sets the roller shutter level of a RollerShutterActuator.
257      */
258     public void setRollerShutterActuatorState(final String capabilityId, final int rollerShutterLevel)
259             throws IOException {
260         executePost(createActionURL(),
261                 new StateActionSetterDTO(capabilityId, CapabilityDTO.TYPE_ROLLERSHUTTERACTUATOR, rollerShutterLevel));
262     }
263
264     /**
265      * Starts or stops moving a RollerShutterActuator
266      */
267     public void setRollerShutterAction(final String capabilityId, final ShutterActionType rollerShutterAction)
268             throws IOException {
269         executePost(createActionURL(), new ShutterActionDTO(capabilityId, rollerShutterAction));
270     }
271
272     /**
273      * Sets a new state of a VariableActuator.
274      */
275     public void setVariableActuatorState(final String capabilityId, final boolean state) throws IOException {
276         executePost(createActionURL(),
277                 new StateActionSetterDTO(capabilityId, CapabilityDTO.TYPE_VARIABLEACTUATOR, state));
278     }
279
280     /**
281      * Sets the point temperature.
282      */
283     public void setPointTemperatureState(final String capabilityId, final double pointTemperature) throws IOException {
284         executePost(createActionURL(),
285                 new StateActionSetterDTO(capabilityId, CapabilityDTO.TYPE_THERMOSTATACTUATOR, pointTemperature));
286     }
287
288     /**
289      * Sets the operation mode to "Auto" or "Manu".
290      */
291     public void setOperationMode(final String capabilityId, final boolean isAutoMode) throws IOException {
292         executePost(createActionURL(),
293                 new StateActionSetterDTO(capabilityId, CapabilityDTO.TYPE_THERMOSTATACTUATOR, isAutoMode));
294     }
295
296     /**
297      * Sets the alarm state.
298      */
299     public void setAlarmActuatorState(final String capabilityId, final boolean alarmState) throws IOException {
300         executePost(createActionURL(),
301                 new StateActionSetterDTO(capabilityId, CapabilityDTO.TYPE_ALARMACTUATOR, alarmState));
302     }
303
304     /**
305      * Load the device and returns a {@link List} of {@link DeviceDTO}s.
306      * VariableActuators are returned additionally (independent from the device ids),
307      * because VariableActuators are everytime available and never have a device state.
308      *
309      * @param deviceIds Ids of the devices to return
310      * @return List of Devices
311      */
312     public List<DeviceDTO> getDevices(Collection<String> deviceIds) throws IOException {
313         logger.debug("Loading LIVISI SmartHome devices...");
314         List<DeviceDTO> devices = executeGetList(URLCreator.createDevicesURL(bridgeConfiguration.host),
315                 DeviceDTO[].class);
316         return devices.stream().filter(d -> isDeviceUsable(d, deviceIds)).collect(Collectors.toList());
317     }
318
319     /**
320      * Loads the {@link DeviceDTO} with the given deviceId.
321      */
322     public Optional<DeviceDTO> getDeviceById(final String deviceId) throws IOException {
323         logger.debug("Loading device with id {}...", deviceId);
324         return executeGet(URLCreator.createDeviceURL(bridgeConfiguration.host, deviceId), DeviceDTO.class);
325     }
326
327     /**
328      * Loads the states for all {@link DeviceDTO}s.
329      */
330     public List<DeviceStateDTO> getDeviceStates() throws IOException {
331         logger.debug("Loading device states...");
332         return executeGetList(URLCreator.createDeviceStatesURL(bridgeConfiguration.host), DeviceStateDTO[].class);
333     }
334
335     /**
336      * Loads the device state for the given deviceId.
337      */
338     public @Nullable StateDTO getDeviceStateByDeviceId(final String deviceId, final boolean isSHCClassic)
339             throws IOException {
340         logger.debug("Loading device states for device id {}...", deviceId);
341         if (isSHCClassic) {
342             Optional<DeviceStateDTO> deviceState = executeGet(
343                     URLCreator.createDeviceStateURL(bridgeConfiguration.host, deviceId), DeviceStateDTO.class);
344             return deviceState.map(DeviceStateDTO::getState).orElse(null);
345         }
346         return executeGet(URLCreator.createDeviceStateURL(bridgeConfiguration.host, deviceId), StateDTO.class)
347                 .orElse(null);
348     }
349
350     /**
351      * Loads the locations and returns a {@link List} of {@link LocationDTO}s.
352      *
353      * @return a List of Devices
354      */
355     public List<LocationDTO> getLocations() throws IOException {
356         logger.debug("Loading locations...");
357         return executeGetList(URLCreator.createLocationURL(bridgeConfiguration.host), LocationDTO[].class);
358     }
359
360     /**
361      * Loads and returns a {@link List} of {@link CapabilityDTO}s for the given deviceId.
362      *
363      * @param deviceId the id of the {@link DeviceDTO}
364      * @return capabilities of the device
365      */
366     public List<CapabilityDTO> getCapabilitiesForDevice(final String deviceId) throws IOException {
367         logger.debug("Loading capabilities for device {}...", deviceId);
368         return executeGetList(URLCreator.createDeviceCapabilitiesURL(bridgeConfiguration.host, deviceId),
369                 CapabilityDTO[].class);
370     }
371
372     /**
373      * Loads and returns a {@link List} of all {@link CapabilityDTO}s.
374      */
375     public List<CapabilityDTO> getCapabilities() throws IOException {
376         logger.debug("Loading capabilities...");
377         return executeGetList(URLCreator.createCapabilityURL(bridgeConfiguration.host), CapabilityDTO[].class);
378     }
379
380     /**
381      * Loads and returns a {@link List} of all {@link CapabilityDTO}States.
382      */
383     public List<CapabilityStateDTO> getCapabilityStates() throws IOException {
384         logger.debug("Loading capability states...");
385         return executeGetList(URLCreator.createCapabilityStatesURL(bridgeConfiguration.host),
386                 CapabilityStateDTO[].class);
387     }
388
389     /**
390      * Returns a {@link List} of all {@link MessageDTO}s.
391      */
392     public List<MessageDTO> getMessages() throws IOException {
393         logger.debug("Loading messages...");
394         return executeGetList(URLCreator.createMessageURL(bridgeConfiguration.host), MessageDTO[].class);
395     }
396
397     private String createActionURL() {
398         return URLCreator.createActionURL(bridgeConfiguration.host);
399     }
400
401     /**
402      * Decides if a (discovered) device is usable (available and supported).
403      *
404      * @param device device to check
405      * @param activeDeviceIds active device id (devices with an according available device state)
406      * @return true when usable, otherwise false
407      */
408     private static boolean isDeviceUsable(DeviceDTO device, Collection<String> activeDeviceIds) {
409         return activeDeviceIds.contains(device.getId())
410                 || LivisiBindingConstants.DEVICE_VARIABLE_ACTUATOR.equals(device.getType());
411     }
412
413     /**
414      * Normalizes the JSON response content.
415      * The LIVISI SmartHome local API returns "[]" for missing objects instead of "null". This method fixes
416      * this issue.
417      * 
418      * @param responseContent response
419      * @return normalized response content
420      */
421     private static String normalizeResponseContent(String responseContent) {
422         return responseContent.replace("[]", "null");
423     }
424 }