]> git.basschouten.com Git - openhab-addons.git/blob
13f764f9386a565fb0438b60f2aba0aac4fff0b7
[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         // Initialize device services
128         try {
129             this.initializeServices();
130         } catch (BoschSHCException e) {
131             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
132             return;
133         }
134
135         this.updateStatus(ThingStatus.ONLINE);
136     }
137
138     /**
139      * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
140      * states of services).
141      *
142      * @param channelUID {@link ChannelUID} of the channel to which the command was sent
143      * @param command {@link Command}
144      */
145     @Override
146     public void handleCommand(ChannelUID channelUID, Command command) {
147         if (command instanceof RefreshType) {
148             // Refresh state of services that affect the channel
149             for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
150                 if (deviceService.affectedChannels.contains(channelUID.getIdWithoutGroup())) {
151                     this.refreshServiceState(deviceService.service);
152                 }
153             }
154         }
155     }
156
157     /**
158      * Processes an update which is received from the bridge.
159      *
160      * @param serviceName Name of service the update came from.
161      * @param stateData Current state of device service. Serialized as JSON.
162      */
163     public void processUpdate(String serviceName, @Nullable JsonElement stateData) {
164         // Check services of device to correctly
165         for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
166             BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
167             if (serviceName.equals(service.getServiceName())) {
168                 service.onStateUpdate(stateData);
169             }
170         }
171     }
172
173     /**
174      * Should be used by handlers to create their required services.
175      */
176     protected void initializeServices() throws BoschSHCException {
177     }
178
179     /**
180      * Returns the bridge handler for this thing handler.
181      *
182      * @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration.
183      * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
184      */
185     protected BridgeHandler getBridgeHandler() throws BoschSHCException {
186         Bridge bridge = this.getBridge();
187         if (bridge == null) {
188             throw new BoschSHCException(String.format("No valid bridge set for %s (%s)", this.getThing().getLabel(),
189                     this.getThing().getUID().getAsString()));
190         }
191         BridgeHandler bridgeHandler = (BridgeHandler) bridge.getHandler();
192         if (bridgeHandler == null) {
193             throw new BoschSHCException(String.format("Bridge of %s (%s) has no valid bridge handler",
194                     this.getThing().getLabel(), this.getThing().getUID().getAsString()));
195         }
196         return bridgeHandler;
197     }
198
199     /**
200      * Query the Bosch Smart Home Controller for the state of the service with the specified name.
201      *
202      * @note Use services instead of directly requesting a state.
203      *
204      * @param stateName Name of the service to query
205      * @param classOfT Class to convert the resulting JSON to
206      */
207     protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
208         String deviceId = this.getBoschID();
209         if (deviceId == null) {
210             return null;
211         }
212         try {
213             BridgeHandler bridgeHandler = this.getBridgeHandler();
214             return bridgeHandler.getState(deviceId, stateName, classOfT);
215         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
216             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
217                     String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage()));
218             return null;
219         } catch (InterruptedException e) {
220             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
221                     String.format("Interrupted refresh state from service %s: %s", stateName, e.getMessage()));
222             Thread.currentThread().interrupt();
223             return null;
224         }
225     }
226
227     /**
228      * Creates and registers a new service for this device.
229      *
230      * @param <TService> Type of service.
231      * @param <TState> Type of service state.
232      * @param newService Supplier function to create a new instance of the service.
233      * @param stateUpdateListener Function to call when a state update was received
234      *            from the device.
235      * @param affectedChannels Channels which are affected by the state of this
236      *            service.
237      * @return Instance of registered service.
238      * @throws BoschSHCException
239      */
240     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
241             Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
242             throws BoschSHCException {
243         return createService(newService, stateUpdateListener, affectedChannels, false);
244     }
245
246     /**
247      * Creates and registers a new service for this device.
248      *
249      * @param <TService> Type of service.
250      * @param <TState> Type of service state.
251      * @param newService Supplier function to create a new instance of the service.
252      * @param stateUpdateListener Function to call when a state update was received
253      *            from the device.
254      * @param affectedChannels Channels which are affected by the state of this
255      *            service.
256      * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
257      *            or service. Useful if state updates are not included in long poll results.
258      * @return Instance of registered service.
259      * @throws BoschSHCException
260      */
261     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
262             Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
263             boolean shouldFetchInitialState) throws BoschSHCException {
264         TService service = newService.get();
265         this.registerService(service, stateUpdateListener, affectedChannels, shouldFetchInitialState);
266         return service;
267     }
268
269     /**
270      * Registers a service for this device.
271      *
272      * @param <TService> Type of service.
273      * @param <TState> Type of service state.
274      * @param service Service to register.
275      * @param stateUpdateListener Function to call when a state update was received
276      *            from the device.
277      * @param affectedChannels Channels which are affected by the state of this
278      *            service.
279      * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
280      * @throws BoschSHCException If no device id is set.
281      */
282     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
283             TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
284             throws BoschSHCException {
285         registerService(service, stateUpdateListener, affectedChannels, false);
286     }
287
288     /**
289      * Registers a service for this device.
290      *
291      * @param <TService> Type of service.
292      * @param <TState> Type of service state.
293      * @param service Service to register.
294      * @param stateUpdateListener Function to call when a state update was received
295      *            from the device.
296      * @param affectedChannels Channels which are affected by the state of this
297      *            service.
298      * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
299      *            or service. Useful if state updates are not included in long poll results.
300      * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
301      * @throws BoschSHCException If no device id is set.
302      */
303     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
304             TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
305             boolean shouldFetchInitialState) throws BoschSHCException {
306         String deviceId = verifyBoschID();
307         service.initialize(getBridgeHandler(), deviceId, stateUpdateListener);
308         this.registerService(service, affectedChannels);
309
310         if (shouldFetchInitialState) {
311             fetchInitialState(service, stateUpdateListener);
312         }
313     }
314
315     /**
316      * Actively requests the initial state for the given service. This is required if long poll results do not contain
317      * status updates for the given service.
318      *
319      * @param <TService> Type of the service for which the state should be obtained
320      * @param <TState> Type of the objects to serialize and deserialize the service state
321      * @param service Service for which the state should be requested
322      * @param stateUpdateListener Function to process the obtained state
323      */
324     private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
325             TService service, Consumer<TState> stateUpdateListener) {
326         try {
327             @Nullable
328             TState serviceState = service.getState();
329             if (serviceState != null) {
330                 stateUpdateListener.accept(serviceState);
331             }
332         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
333             logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
334                     getBoschID());
335         } catch (InterruptedException e) {
336             Thread.currentThread().interrupt();
337             logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
338                     getBoschID());
339         }
340     }
341
342     /**
343      * Registers a write-only service that does not receive states from the bridge.
344      * <p>
345      * Examples for such services are the actions of the intrusion detection service.
346      *
347      * @param <TService> Type of service.
348      * @param service Service to register.
349      * @throws BoschSHCException If no device ID is set.
350      */
351     protected <TService extends AbstractBoschSHCService> void registerStatelessService(TService service)
352             throws BoschSHCException {
353         String deviceId = verifyBoschID();
354         service.initialize(getBridgeHandler(), deviceId);
355         // do not register in service list because the service can not receive state updates
356     }
357
358     /**
359      * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case.
360      *
361      * @return the Bosch ID, if present
362      * @throws BoschSHCException if no Bosch ID is set
363      */
364     private String verifyBoschID() throws BoschSHCException {
365         String deviceId = this.getBoschID();
366         if (deviceId == null) {
367             throw new BoschSHCException(
368                     String.format("Could not register service for %s, no device id set", this.getThing()));
369         }
370         return deviceId;
371     }
372
373     /**
374      * Updates the state of a device service.
375      * Sets the status of the device to offline if setting the state fails.
376      *
377      * @param <TService> Type of service.
378      * @param <TState> Type of service state.
379      * @param service Service to set state for.
380      * @param state State to set.
381      */
382     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
383             TService service, TState state) {
384         try {
385             service.setState(state);
386         } catch (TimeoutException | ExecutionException e) {
387             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
388                     "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
389         } catch (InterruptedException e) {
390             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
391                     .format("Interrupted update state for service %s: %s", service.getServiceName(), e.getMessage()));
392             Thread.currentThread().interrupt();
393         }
394     }
395
396     /**
397      * Lets a service handle a received command.
398      * Sets the status of the device to offline if handling the command fails.
399      *
400      * @param <TService> Type of service.
401      * @param <TState> Type of service state.
402      * @param service Service which should handle command.
403      * @param command Command to handle.
404      */
405     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
406             TService service, Command command) {
407         try {
408             if (command instanceof RefreshType) {
409                 this.refreshServiceState(service);
410             } else {
411                 TState state = service.handleCommand(command);
412                 this.updateServiceState(service, state);
413             }
414         } catch (BoschSHCException e) {
415             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
416                     String.format("Error when service %s should handle command %s: %s", service.getServiceName(),
417                             command.getClass().getName(), e.getMessage()));
418         }
419     }
420
421     /**
422      * Requests a service to refresh its state.
423      * Sets the device offline if request fails.
424      *
425      * @param <TService> Type of service.
426      * @param <TState> Type of service state.
427      * @param service Service to refresh state for.
428      */
429     private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
430             TService service) {
431         try {
432             service.refreshState();
433         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
434             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
435                     String.format("Error when trying to refresh state from service %s: %s", service.getServiceName(),
436                             e.getMessage()));
437         } catch (InterruptedException e) {
438             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
439                     .format("Interrupted refresh state from service %s: %s", service.getServiceName(), e.getMessage()));
440             Thread.currentThread().interrupt();
441         }
442     }
443
444     /**
445      * Registers a service of this device.
446      *
447      * @param service Service which belongs to this device
448      * @param affectedChannels Channels which are affected by the state of this
449      *            service
450      */
451     private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
452             Collection<String> affectedChannels) {
453         this.services.add(new DeviceService<TState>(service, affectedChannels));
454     }
455
456     /**
457      * Sends a HTTP POST request with empty body.
458      *
459      * @param <TService> Type of service.
460      * @param service Service implementing the action
461      */
462     protected <TService extends AbstractStatelessBoschSHCService> void postAction(TService service) {
463         try {
464             service.postAction();
465         } catch (ExecutionException | TimeoutException e) {
466             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
467                     String.format("Error while triggering action %s", service.getEndpoint()));
468         } catch (InterruptedException e) {
469             Thread.currentThread().interrupt();
470             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
471                     String.format("Error while triggering action %s", service.getEndpoint()));
472         }
473     }
474
475     /**
476      * Sends a HTTP POST request with the given request body.
477      *
478      * @param <TService> Type of service.
479      * @param <TState> Type of the request to be sent.
480      * @param service Service implementing the action
481      * @param request Request object to be serialized to JSON
482      */
483     protected <TService extends AbstractStatelessBoschSHCServiceWithRequestBody<TState>, TState extends BoschSHCServiceState> void postAction(
484             TService service, TState request) {
485         try {
486             service.postAction(request);
487         } catch (ExecutionException | TimeoutException e) {
488             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
489                     String.format("Error while triggering action %s", service.getEndpoint()));
490         } catch (InterruptedException e) {
491             Thread.currentThread().interrupt();
492             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
493                     String.format("Error while triggering action %s", service.getEndpoint()));
494         }
495     }
496 }