]> git.basschouten.com Git - openhab-addons.git/blob
0ddb7ca420608f143bb6d1371929aeec4dada0c9
[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.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, @Nullable 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         return createService(newService, stateUpdateListener, affectedChannels, false);
245     }
246
247     /**
248      * Creates and registers a new service for this device.
249      *
250      * @param <TService> Type of service.
251      * @param <TState> Type of service state.
252      * @param newService Supplier function to create a new instance of the service.
253      * @param stateUpdateListener Function to call when a state update was received
254      *            from the device.
255      * @param affectedChannels Channels which are affected by the state of this
256      *            service.
257      * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
258      *            or service. Useful if state updates are not included in long poll results.
259      * @return Instance of registered service.
260      * @throws BoschSHCException
261      */
262     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
263             Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
264             boolean shouldFetchInitialState) throws BoschSHCException {
265         TService service = newService.get();
266         this.registerService(service, stateUpdateListener, affectedChannels, shouldFetchInitialState);
267         return service;
268     }
269
270     /**
271      * Registers a service for this device.
272      *
273      * @param <TService> Type of service.
274      * @param <TState> Type of service state.
275      * @param service Service to register.
276      * @param stateUpdateListener Function to call when a state update was received
277      *            from the device.
278      * @param affectedChannels Channels which are affected by the state of this
279      *            service.
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             throws BoschSHCException {
286         registerService(service, stateUpdateListener, affectedChannels, false);
287     }
288
289     /**
290      * Registers a service for this device.
291      *
292      * @param <TService> Type of service.
293      * @param <TState> Type of service state.
294      * @param service Service to register.
295      * @param stateUpdateListener Function to call when a state update was received
296      *            from the device.
297      * @param affectedChannels Channels which are affected by the state of this
298      *            service.
299      * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
300      *            or service. Useful if state updates are not included in long poll results.
301      * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
302      * @throws BoschSHCException If no device id is set.
303      */
304     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
305             TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
306             boolean shouldFetchInitialState) throws BoschSHCException {
307
308         String deviceId = verifyBoschID();
309         service.initialize(getBridgeHandler(), deviceId, stateUpdateListener);
310         this.registerService(service, affectedChannels);
311
312         if (shouldFetchInitialState) {
313             fetchInitialState(service, stateUpdateListener);
314         }
315     }
316
317     /**
318      * Actively requests the initial state for the given service. This is required if long poll results do not contain
319      * status updates for the given service.
320      *
321      * @param <TService> Type of the service for which the state should be obtained
322      * @param <TState> Type of the objects to serialize and deserialize the service state
323      * @param service Service for which the state should be requested
324      * @param stateUpdateListener Function to process the obtained state
325      */
326     private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
327             TService service, Consumer<TState> stateUpdateListener) {
328
329         try {
330             @Nullable
331             TState serviceState = service.getState();
332             if (serviceState != null) {
333                 stateUpdateListener.accept(serviceState);
334             }
335         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
336             logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
337                     getBoschID());
338         } catch (InterruptedException e) {
339             Thread.currentThread().interrupt();
340             logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
341                     getBoschID());
342         }
343     }
344
345     /**
346      * Registers a write-only service that does not receive states from the bridge.
347      * <p>
348      * Examples for such services are the actions of the intrusion detection service.
349      *
350      * @param <TService> Type of service.
351      * @param service Service to register.
352      * @throws BoschSHCException If no device ID is set.
353      */
354     protected <TService extends AbstractBoschSHCService> void registerStatelessService(TService service)
355             throws BoschSHCException {
356
357         String deviceId = verifyBoschID();
358         service.initialize(getBridgeHandler(), deviceId);
359         // do not register in service list because the service can not receive state updates
360     }
361
362     /**
363      * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case.
364      *
365      * @return the Bosch ID, if present
366      * @throws BoschSHCException if no Bosch ID is set
367      */
368     private String verifyBoschID() throws BoschSHCException {
369         String deviceId = this.getBoschID();
370         if (deviceId == null) {
371             throw new BoschSHCException(
372                     String.format("Could not register service for %s, no device id set", this.getThing()));
373         }
374         return deviceId;
375     }
376
377     /**
378      * Updates the state of a device service.
379      * Sets the status of the device to offline if setting the state fails.
380      *
381      * @param <TService> Type of service.
382      * @param <TState> Type of service state.
383      * @param service Service to set state for.
384      * @param state State to set.
385      */
386     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
387             TService service, TState state) {
388         try {
389             service.setState(state);
390         } catch (TimeoutException | ExecutionException e) {
391             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
392                     "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
393         } catch (InterruptedException e) {
394             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
395                     .format("Interrupted update state for service %s: %s", service.getServiceName(), e.getMessage()));
396             Thread.currentThread().interrupt();
397         }
398     }
399
400     /**
401      * Lets a service handle a received command.
402      * Sets the status of the device to offline if handling the command fails.
403      *
404      * @param <TService> Type of service.
405      * @param <TState> Type of service state.
406      * @param service Service which should handle command.
407      * @param command Command to handle.
408      */
409     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
410             TService service, Command command) {
411         try {
412             if (command instanceof RefreshType) {
413                 this.refreshServiceState(service);
414             } else {
415                 TState state = service.handleCommand(command);
416                 this.updateServiceState(service, state);
417             }
418         } catch (BoschSHCException e) {
419             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
420                     String.format("Error when service %s should handle command %s: %s", service.getServiceName(),
421                             command.getClass().getName(), e.getMessage()));
422         }
423     }
424
425     /**
426      * Requests a service to refresh its state.
427      * Sets the device offline if request fails.
428      *
429      * @param <TService> Type of service.
430      * @param <TState> Type of service state.
431      * @param service Service to refresh state for.
432      */
433     private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
434             TService service) {
435         try {
436             service.refreshState();
437         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
438             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
439                     String.format("Error when trying to refresh state from service %s: %s", service.getServiceName(),
440                             e.getMessage()));
441         } catch (InterruptedException e) {
442             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
443                     .format("Interrupted refresh state from service %s: %s", service.getServiceName(), e.getMessage()));
444             Thread.currentThread().interrupt();
445         }
446     }
447
448     /**
449      * Registers a service of this device.
450      *
451      * @param service Service which belongs to this device
452      * @param affectedChannels Channels which are affected by the state of this
453      *            service
454      */
455     private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
456             Collection<String> affectedChannels) {
457         this.services.add(new DeviceService<TState>(service, affectedChannels));
458     }
459
460     /**
461      * Sends a HTTP POST request with empty body.
462      *
463      * @param <TService> Type of service.
464      * @param service Service implementing the action
465      */
466     protected <TService extends AbstractStatelessBoschSHCService> void postAction(TService service) {
467         try {
468             service.postAction();
469         } catch (ExecutionException | TimeoutException e) {
470             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
471                     String.format("Error while triggering action %s", service.getEndpoint()));
472         } catch (InterruptedException e) {
473             Thread.currentThread().interrupt();
474             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
475                     String.format("Error while triggering action %s", service.getEndpoint()));
476         }
477     }
478
479     /**
480      * Sends a HTTP POST request with the given request body.
481      *
482      * @param <TService> Type of service.
483      * @param <TState> Type of the request to be sent.
484      * @param service Service implementing the action
485      * @param request Request object to be serialized to JSON
486      */
487     protected <TService extends AbstractStatelessBoschSHCServiceWithRequestBody<TState>, TState extends BoschSHCServiceState> void postAction(
488             TService service, TState request) {
489         try {
490             service.postAction(request);
491         } catch (ExecutionException | TimeoutException e) {
492             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
493                     String.format("Error while triggering action %s", service.getEndpoint()));
494         } catch (InterruptedException e) {
495             Thread.currentThread().interrupt();
496             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
497                     String.format("Error while triggering action %s", service.getEndpoint()));
498         }
499     }
500 }