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