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