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