2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschshc.internal.devices;
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;
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;
44 import com.google.gson.JsonElement;
47 * The {@link BoschSHCHandler} represents Bosch Things. Each type of device
48 * or system service inherits from this abstract thing handler.
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
56 public abstract class BoschSHCHandler extends BaseThingHandler {
59 * Service State for a Bosch device.
61 class DeviceService<TState extends BoschSHCServiceState> {
65 * @param service Service which belongs to the device.
66 * @param affectedChannels Channels which are affected by the state of this service.
68 public DeviceService(BoschSHCService<TState> service, Collection<String> affectedChannels) {
69 this.service = service;
70 this.affectedChannels = affectedChannels;
74 * Service which belongs to the device.
76 public final BoschSHCService<TState> service;
79 * Channels which are affected by the state of this service.
81 public final Collection<String> affectedChannels;
84 private final Logger logger = LoggerFactory.getLogger(getClass());
87 * Services of the device.
89 private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
91 protected BoschSHCHandler(Thing thing) {
96 * Returns the unique id of the Bosch device or service.
98 * For physical devices, the ID looks like
101 * hdm:Cameras:d20354de-44b5-3acc-924c-24c98d59da42
102 * hdm:ZigBee:000d6f0016d1c087
105 * For virtual devices / services, static IDs like the following are used:
109 * smokeDetectionSystem
110 * intrusionDetectionSystem
113 * @return Unique ID of the Bosch device or service.
115 public abstract @Nullable String getBoschID();
118 * Initializes this handler.
121 public void initialize() {
122 // Initialize device services
124 this.initializeServices();
125 } catch (BoschSHCException e) {
126 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
130 this.updateStatus(ThingStatus.ONLINE);
134 * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
135 * states of services).
137 * @param channelUID {@link ChannelUID} of the channel to which the command was sent
138 * @param command {@link Command}
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);
153 * Processes an update which is received from the bridge.
155 * @param serviceName Name of service the update came from.
156 * @param stateData Current state of device service. Serialized as JSON.
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);
169 * Processes an update for a logical child device.
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
175 public void processChildUpdate(String childDeviceId, String serviceName, @Nullable JsonElement stateData) {
176 // default implementation is empty, subclasses may override
180 * Processes a device-specific message from the Bosch Smart Home Controller.
182 * @param message the message published by the controller
184 public void processMessage(Message message) {
185 // default implementation is empty, subclasses may override
189 * Use this method to register all services of the device with
190 * {@link #registerService(BoschSHCService, Consumer, Collection, boolean)}.
192 protected void initializeServices() throws BoschSHCException {
193 // default implementation is empty, subclasses may override
197 * Returns the bridge handler for this thing handler.
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.
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()));
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()));
213 return bridgeHandler;
217 * Query the Bosch Smart Home Controller for the state of the service with the specified name.
219 * @implNote Use services instead of directly requesting a state.
221 * @param stateName Name of the service to query
222 * @param classOfT Class to convert the resulting JSON to
224 protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
225 String deviceId = this.getBoschID();
226 if (deviceId == null) {
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()));
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();
245 * Creates and registers a new service for this device.
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
252 * @param affectedChannels Channels which are affected by the state of this
254 * @return Instance of registered service.
255 * @throws BoschSHCException
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);
264 * Creates and registers a new service for this device.
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
271 * @param affectedChannels Channels which are affected by the state of this
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
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);
287 * Registers a service for this device.
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
294 * @param affectedChannels Channels which are affected by the state of this
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.
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);
306 * Registers a service for this device.
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
313 * @param affectedChannels Channels which are affected by the state of this
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.
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);
327 if (shouldFetchInitialState) {
328 fetchInitialState(service, stateUpdateListener);
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.
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
341 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
342 TService service, Consumer<TState> stateUpdateListener) {
345 TState serviceState = service.getState();
346 if (serviceState != null) {
347 stateUpdateListener.accept(serviceState);
349 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
350 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
352 } catch (InterruptedException e) {
353 Thread.currentThread().interrupt();
354 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
360 * Registers a write-only service that does not receive states from the bridge.
362 * Examples for such services are the actions of the intrusion detection service.
364 * @param <TService> Type of service.
365 * @param service Service to register.
366 * @throws BoschSHCException If no device ID is set.
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
376 * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case.
378 * @return the Bosch ID, if present
379 * @throws BoschSHCException if no Bosch ID is set
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()));
391 * Updates the state of a device service.
392 * Sets the status of the device to offline if setting the state fails.
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.
399 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
400 TService service, TState state) {
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();
414 * Lets a service handle a received command.
415 * Sets the status of the device to offline if handling the command fails.
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.
422 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
423 TService service, Command command) {
425 if (command instanceof RefreshType) {
426 this.refreshServiceState(service);
428 TState state = service.handleCommand(command);
429 this.updateServiceState(service, state);
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()));
439 * Requests a service to refresh its state.
440 * Sets the device offline if request fails.
442 * @param <TService> Type of service.
443 * @param <TState> Type of service state.
444 * @param service Service to refresh state for.
446 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
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(),
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();
462 * Registers a service of this device.
464 * @param service Service which belongs to this device
465 * @param affectedChannels Channels which are affected by the state of this
468 private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
469 Collection<String> affectedChannels) {
470 this.services.add(new DeviceService<>(service, affectedChannels));
474 * Sends a HTTP POST request with empty body.
476 * @param <TService> Type of service.
477 * @param service Service implementing the action
479 protected <TService extends AbstractStatelessBoschSHCService> void postAction(TService service) {
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()));
493 * Sends a HTTP POST request with the given request body.
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
500 protected <TService extends AbstractStatelessBoschSHCServiceWithRequestBody<TState>, TState extends BoschSHCServiceState> void postAction(
501 TService service, TState request) {
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()));