]> git.basschouten.com Git - openhab-addons.git/blob
5fbbe50a65341c9bc395afa7981680705bcb0773
[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.boschshc.internal.devices;
14
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.List;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.TimeoutException;
20 import java.util.function.Consumer;
21 import java.util.function.Supplier;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
26 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
27 import org.openhab.binding.boschshc.internal.services.AbstractBoschSHCService;
28 import org.openhab.binding.boschshc.internal.services.AbstractStatelessBoschSHCService;
29 import org.openhab.binding.boschshc.internal.services.AbstractStatelessBoschSHCServiceWithRequestBody;
30 import org.openhab.binding.boschshc.internal.services.BoschSHCService;
31 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
32 import org.openhab.core.thing.Bridge;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.binding.BaseThingHandler;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import com.google.gson.Gson;
44 import com.google.gson.JsonElement;
45
46 /**
47  * The {@link BoschSHCHandler} represents Bosch Things. Each type of device
48  * or system service inherits from this abstract thing handler.
49  *
50  * @author Stefan Kästle - Initial contribution
51  * @author Christian Oeing - refactorings of e.g. server registration
52  * @author David Pace - Handler abstraction
53  */
54 @NonNullByDefault
55 public abstract class BoschSHCHandler extends BaseThingHandler {
56
57     /**
58      * Service State for a Bosch device.
59      */
60     class DeviceService<TState extends BoschSHCServiceState> {
61         /**
62          * Constructor.
63          *
64          * @param service Service which belongs to the device.
65          * @param affectedChannels Channels which are affected by the state of this service.
66          */
67         public DeviceService(BoschSHCService<TState> service, Collection<String> affectedChannels) {
68             this.service = service;
69             this.affectedChannels = affectedChannels;
70         }
71
72         /**
73          * Service which belongs to the device.
74          */
75         public final BoschSHCService<TState> service;
76
77         /**
78          * Channels which are affected by the state of this service.
79          */
80         public final Collection<String> affectedChannels;
81     }
82
83     /**
84      * Reusable gson instance to convert a class to json string and back in derived classes.
85      */
86     protected static final Gson GSON = new Gson();
87
88     protected final Logger logger = LoggerFactory.getLogger(getClass());
89
90     /**
91      * Services of the device.
92      */
93     private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
94
95     protected BoschSHCHandler(Thing thing) {
96         super(thing);
97     }
98
99     /**
100      * Returns the unique id of the Bosch device or service.
101      * <p>
102      * For physical devices, the ID looks like
103      * 
104      * <pre>
105      * hdm:Cameras:d20354de-44b5-3acc-924c-24c98d59da42
106      * hdm:ZigBee:000d6f0016d1c087
107      * </pre>
108      * 
109      * For virtual devices / services, static IDs like the following are used:
110      * 
111      * <pre>
112      * ventilationService
113      * smokeDetectionSystem
114      * intrusionDetectionSystem
115      * </pre>
116      *
117      * @return Unique ID of the Bosch device or service.
118      */
119     public abstract @Nullable String getBoschID();
120
121     /**
122      * Initializes this handler. Use this method to register all services of the device with
123      * {@link #registerService(BoschSHCService)}.
124      */
125     @Override
126     public void initialize() {
127
128         // Initialize device services
129         try {
130             this.initializeServices();
131         } catch (BoschSHCException e) {
132             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
133             return;
134         }
135
136         this.updateStatus(ThingStatus.ONLINE);
137     }
138
139     /**
140      * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
141      * states of services).
142      *
143      * @param channelUID {@link ChannelUID} of the channel to which the command was sent
144      * @param command {@link Command}
145      */
146     @Override
147     public void handleCommand(ChannelUID channelUID, Command command) {
148         if (command instanceof RefreshType) {
149             // Refresh state of services that affect the channel
150             for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
151                 if (deviceService.affectedChannels.contains(channelUID.getIdWithoutGroup())) {
152                     this.refreshServiceState(deviceService.service);
153                 }
154             }
155         }
156     }
157
158     /**
159      * Processes an update which is received from the bridge.
160      *
161      * @param serviceName Name of service the update came from.
162      * @param stateData Current state of device service. Serialized as JSON.
163      */
164     public void processUpdate(String serviceName, JsonElement stateData) {
165         // Check services of device to correctly
166         for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
167             BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
168             if (serviceName.equals(service.getServiceName())) {
169                 service.onStateUpdate(stateData);
170             }
171         }
172     }
173
174     /**
175      * Should be used by handlers to create their required services.
176      */
177     protected void initializeServices() throws BoschSHCException {
178     }
179
180     /**
181      * Returns the bridge handler for this thing handler.
182      *
183      * @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration.
184      * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
185      */
186     protected BridgeHandler getBridgeHandler() throws BoschSHCException {
187         Bridge bridge = this.getBridge();
188         if (bridge == null) {
189             throw new BoschSHCException(String.format("No valid bridge set for %s (%s)", this.getThing().getLabel(),
190                     this.getThing().getUID().getAsString()));
191         }
192         BridgeHandler bridgeHandler = (BridgeHandler) bridge.getHandler();
193         if (bridgeHandler == null) {
194             throw new BoschSHCException(String.format("Bridge of %s (%s) has no valid bridge handler",
195                     this.getThing().getLabel(), this.getThing().getUID().getAsString()));
196         }
197         return bridgeHandler;
198     }
199
200     /**
201      * Query the Bosch Smart Home Controller for the state of the service with the specified name.
202      *
203      * @note Use services instead of directly requesting a state.
204      *
205      * @param stateName Name of the service to query
206      * @param classOfT Class to convert the resulting JSON to
207      */
208     protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
209         String deviceId = this.getBoschID();
210         if (deviceId == null) {
211             return null;
212         }
213         try {
214             BridgeHandler bridgeHandler = this.getBridgeHandler();
215             return bridgeHandler.getState(deviceId, stateName, classOfT);
216         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
217             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
218                     String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage()));
219             return null;
220         } catch (InterruptedException e) {
221             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
222                     String.format("Interrupted refresh state from service %s: %s", stateName, e.getMessage()));
223             Thread.currentThread().interrupt();
224             return null;
225         }
226     }
227
228     /**
229      * Creates and registers a new service for this device.
230      *
231      * @param <TService> Type of service.
232      * @param <TState> Type of service state.
233      * @param newService Supplier function to create a new instance of the service.
234      * @param stateUpdateListener Function to call when a state update was received
235      *            from the device.
236      * @param affectedChannels Channels which are affected by the state of this
237      *            service.
238      * @return Instance of registered service.
239      * @throws BoschSHCException
240      */
241     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
242             Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
243             throws BoschSHCException {
244         TService service = newService.get();
245         this.registerService(service, stateUpdateListener, affectedChannels);
246         return service;
247     }
248
249     /**
250      * Registers a service for this device.
251      *
252      * @param <TService> Type of service.
253      * @param <TState> Type of service state.
254      * @param service Service to register.
255      * @param stateUpdateListener Function to call when a state update was received
256      *            from the device.
257      * @param affectedChannels Channels which are affected by the state of this
258      *            service.
259      * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
260      * @throws BoschSHCException If no device id is set.
261      */
262     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
263             TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
264             throws BoschSHCException {
265         registerService(service, stateUpdateListener, affectedChannels, false);
266     }
267
268     /**
269      * Registers a service for this device.
270      *
271      * @param <TService> Type of service.
272      * @param <TState> Type of service state.
273      * @param service Service to register.
274      * @param stateUpdateListener Function to call when a state update was received
275      *            from the device.
276      * @param affectedChannels Channels which are affected by the state of this
277      *            service.
278      * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
279      *            or service. Useful if state updates are not included in long poll results.
280      * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
281      * @throws BoschSHCException If no device id is set.
282      */
283     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
284             TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
285             boolean shouldFetchInitialState) throws BoschSHCException {
286
287         String deviceId = verifyBoschID();
288         service.initialize(getBridgeHandler(), deviceId, stateUpdateListener);
289         this.registerService(service, affectedChannels);
290
291         if (shouldFetchInitialState) {
292             fetchInitialState(service, stateUpdateListener);
293         }
294     }
295
296     /**
297      * Actively requests the initial state for the given service. This is required if long poll results do not contain
298      * status updates for the given service.
299      * 
300      * @param <TService> Type of the service for which the state should be obtained
301      * @param <TState> Type of the objects to serialize and deserialize the service state
302      * @param service Service for which the state should be requested
303      * @param stateUpdateListener Function to process the obtained state
304      */
305     private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
306             TService service, Consumer<TState> stateUpdateListener) {
307
308         try {
309             @Nullable
310             TState serviceState = service.getState();
311             if (serviceState != null) {
312                 stateUpdateListener.accept(serviceState);
313             }
314         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
315             logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
316                     getBoschID());
317         } catch (InterruptedException e) {
318             Thread.currentThread().interrupt();
319             logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
320                     getBoschID());
321         }
322     }
323
324     /**
325      * Registers a write-only service that does not receive states from the bridge.
326      * <p>
327      * Examples for such services are the actions of the intrusion detection service.
328      * 
329      * @param <TService> Type of service.
330      * @param service Service to register.
331      * @throws BoschSHCException If no device ID is set.
332      */
333     protected <TService extends AbstractBoschSHCService> void registerStatelessService(TService service)
334             throws BoschSHCException {
335
336         String deviceId = verifyBoschID();
337         service.initialize(getBridgeHandler(), deviceId);
338         // do not register in service list because the service can not receive state updates
339     }
340
341     /**
342      * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case.
343      * 
344      * @return the Bosch ID, if present
345      * @throws BoschSHCException if no Bosch ID is set
346      */
347     private String verifyBoschID() throws BoschSHCException {
348         String deviceId = this.getBoschID();
349         if (deviceId == null) {
350             throw new BoschSHCException(
351                     String.format("Could not register service for %s, no device id set", this.getThing()));
352         }
353         return deviceId;
354     }
355
356     /**
357      * Updates the state of a device service.
358      * Sets the status of the device to offline if setting the state fails.
359      *
360      * @param <TService> Type of service.
361      * @param <TState> Type of service state.
362      * @param service Service to set state for.
363      * @param state State to set.
364      */
365     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
366             TService service, TState state) {
367         try {
368             service.setState(state);
369         } catch (TimeoutException | ExecutionException e) {
370             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
371                     "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
372         } catch (InterruptedException e) {
373             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
374                     .format("Interrupted update state for service %s: %s", service.getServiceName(), e.getMessage()));
375             Thread.currentThread().interrupt();
376         }
377     }
378
379     /**
380      * Lets a service handle a received command.
381      * Sets the status of the device to offline if handling the command fails.
382      *
383      * @param <TService> Type of service.
384      * @param <TState> Type of service state.
385      * @param service Service which should handle command.
386      * @param command Command to handle.
387      */
388     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
389             TService service, Command command) {
390         try {
391             if (command instanceof RefreshType) {
392                 this.refreshServiceState(service);
393             } else {
394                 TState state = service.handleCommand(command);
395                 this.updateServiceState(service, state);
396             }
397         } catch (BoschSHCException e) {
398             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
399                     String.format("Error when service %s should handle command %s: %s", service.getServiceName(),
400                             command.getClass().getName(), e.getMessage()));
401         }
402     }
403
404     /**
405      * Requests a service to refresh its state.
406      * Sets the device offline if request fails.
407      * 
408      * @param <TService> Type of service.
409      * @param <TState> Type of service state.
410      * @param service Service to refresh state for.
411      */
412     private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
413             TService service) {
414         try {
415             service.refreshState();
416         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
417             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
418                     String.format("Error when trying to refresh state from service %s: %s", service.getServiceName(),
419                             e.getMessage()));
420         } catch (InterruptedException e) {
421             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
422                     .format("Interrupted refresh state from service %s: %s", service.getServiceName(), e.getMessage()));
423             Thread.currentThread().interrupt();
424         }
425     }
426
427     /**
428      * Registers a service of this device.
429      *
430      * @param service Service which belongs to this device
431      * @param affectedChannels Channels which are affected by the state of this
432      *            service
433      */
434     private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
435             Collection<String> affectedChannels) {
436         this.services.add(new DeviceService<TState>(service, affectedChannels));
437     }
438
439     /**
440      * Sends a HTTP POST request with empty body.
441      * 
442      * @param <TService> Type of service.
443      * @param service Service implementing the action
444      */
445     protected <TService extends AbstractStatelessBoschSHCService> void postAction(TService service) {
446         try {
447             service.postAction();
448         } catch (ExecutionException | TimeoutException e) {
449             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
450                     String.format("Error while triggering action %s", service.getEndpoint()));
451         } catch (InterruptedException e) {
452             Thread.currentThread().interrupt();
453             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
454                     String.format("Error while triggering action %s", service.getEndpoint()));
455         }
456     }
457
458     /**
459      * Sends a HTTP POST request with the given request body.
460      * 
461      * @param <TService> Type of service.
462      * @param <TState> Type of the request to be sent.
463      * @param service Service implementing the action
464      * @param request Request object to be serialized to JSON
465      */
466     protected <TService extends AbstractStatelessBoschSHCServiceWithRequestBody<TState>, TState extends BoschSHCServiceState> void postAction(
467             TService service, TState request) {
468         try {
469             service.postAction(request);
470         } catch (ExecutionException | TimeoutException e) {
471             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
472                     String.format("Error while triggering action %s", service.getEndpoint()));
473         } catch (InterruptedException e) {
474             Thread.currentThread().interrupt();
475             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
476                     String.format("Error while triggering action %s", service.getEndpoint()));
477         }
478     }
479 }