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