2 * Copyright (c) 2010-2023 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.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;
43 import com.google.gson.JsonElement;
46 * The {@link BoschSHCHandler} represents Bosch Things. Each type of device
47 * or system service inherits from this abstract thing handler.
49 * @author Stefan Kästle - Initial contribution
50 * @author Christian Oeing - refactorings of e.g. server registration
51 * @author David Pace - Handler abstraction
54 public abstract class BoschSHCHandler extends BaseThingHandler {
57 * Service State for a Bosch device.
59 class DeviceService<TState extends BoschSHCServiceState> {
63 * @param service Service which belongs to the device.
64 * @param affectedChannels Channels which are affected by the state of this service.
66 public DeviceService(BoschSHCService<TState> service, Collection<String> affectedChannels) {
67 this.service = service;
68 this.affectedChannels = affectedChannels;
72 * Service which belongs to the device.
74 public final BoschSHCService<TState> service;
77 * Channels which are affected by the state of this service.
79 public final Collection<String> affectedChannels;
82 private final Logger logger = LoggerFactory.getLogger(getClass());
85 * Services of the device.
87 private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
89 protected BoschSHCHandler(Thing thing) {
94 * Returns the unique id of the Bosch device or service.
96 * For physical devices, the ID looks like
99 * hdm:Cameras:d20354de-44b5-3acc-924c-24c98d59da42
100 * hdm:ZigBee:000d6f0016d1c087
103 * For virtual devices / services, static IDs like the following are used:
107 * smokeDetectionSystem
108 * intrusionDetectionSystem
111 * @return Unique ID of the Bosch device or service.
113 public abstract @Nullable String getBoschID();
116 * Initializes this handler. Use this method to register all services of the device with
117 * {@link #registerService(BoschSHCService)}.
120 public void initialize() {
121 // Initialize device services
123 this.initializeServices();
124 } catch (BoschSHCException e) {
125 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
129 this.updateStatus(ThingStatus.ONLINE);
133 * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
134 * states of services).
136 * @param channelUID {@link ChannelUID} of the channel to which the command was sent
137 * @param command {@link Command}
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);
152 * Processes an update which is received from the bridge.
154 * @param serviceName Name of service the update came from.
155 * @param stateData Current state of device service. Serialized as JSON.
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);
168 * Should be used by handlers to create their required services.
170 protected void initializeServices() throws BoschSHCException {
174 * Returns the bridge handler for this thing handler.
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.
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()));
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()));
190 return bridgeHandler;
194 * Query the Bosch Smart Home Controller for the state of the service with the specified name.
196 * @note Use services instead of directly requesting a state.
198 * @param stateName Name of the service to query
199 * @param classOfT Class to convert the resulting JSON to
201 protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
202 String deviceId = this.getBoschID();
203 if (deviceId == null) {
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()));
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();
222 * Creates and registers a new service for this device.
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
229 * @param affectedChannels Channels which are affected by the state of this
231 * @return Instance of registered service.
232 * @throws BoschSHCException
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);
241 * Creates and registers a new service for this device.
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
248 * @param affectedChannels Channels which are affected by the state of this
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
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);
264 * Registers a service for this device.
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
271 * @param affectedChannels Channels which are affected by the state of this
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.
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);
283 * Registers a service for this device.
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
290 * @param affectedChannels Channels which are affected by the state of this
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.
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);
304 if (shouldFetchInitialState) {
305 fetchInitialState(service, stateUpdateListener);
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.
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
318 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
319 TService service, Consumer<TState> stateUpdateListener) {
322 TState serviceState = service.getState();
323 if (serviceState != null) {
324 stateUpdateListener.accept(serviceState);
326 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
327 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
329 } catch (InterruptedException e) {
330 Thread.currentThread().interrupt();
331 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
337 * Registers a write-only service that does not receive states from the bridge.
339 * Examples for such services are the actions of the intrusion detection service.
341 * @param <TService> Type of service.
342 * @param service Service to register.
343 * @throws BoschSHCException If no device ID is set.
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
353 * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case.
355 * @return the Bosch ID, if present
356 * @throws BoschSHCException if no Bosch ID is set
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()));
368 * Updates the state of a device service.
369 * Sets the status of the device to offline if setting the state fails.
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.
376 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
377 TService service, TState state) {
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();
391 * Lets a service handle a received command.
392 * Sets the status of the device to offline if handling the command fails.
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.
399 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
400 TService service, Command command) {
402 if (command instanceof RefreshType) {
403 this.refreshServiceState(service);
405 TState state = service.handleCommand(command);
406 this.updateServiceState(service, state);
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()));
416 * Requests a service to refresh its state.
417 * Sets the device offline if request fails.
419 * @param <TService> Type of service.
420 * @param <TState> Type of service state.
421 * @param service Service to refresh state for.
423 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
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(),
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();
439 * Registers a service of this device.
441 * @param service Service which belongs to this device
442 * @param affectedChannels Channels which are affected by the state of this
445 private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
446 Collection<String> affectedChannels) {
447 this.services.add(new DeviceService<>(service, affectedChannels));
451 * Sends a HTTP POST request with empty body.
453 * @param <TService> Type of service.
454 * @param service Service implementing the action
456 protected <TService extends AbstractStatelessBoschSHCService> void postAction(TService service) {
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()));
470 * Sends a HTTP POST request with the given request body.
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
477 protected <TService extends AbstractStatelessBoschSHCServiceWithRequestBody<TState>, TState extends BoschSHCServiceState> void postAction(
478 TService service, TState request) {
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()));