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.Gson;
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
55 public abstract class BoschSHCHandler extends BaseThingHandler {
58 * Service State for a Bosch device.
60 class DeviceService<TState extends BoschSHCServiceState> {
64 * @param service Service which belongs to the device.
65 * @param affectedChannels Channels which are affected by the state of this service.
67 public DeviceService(BoschSHCService<TState> service, Collection<String> affectedChannels) {
68 this.service = service;
69 this.affectedChannels = affectedChannels;
73 * Service which belongs to the device.
75 public final BoschSHCService<TState> service;
78 * Channels which are affected by the state of this service.
80 public final Collection<String> affectedChannels;
84 * Reusable gson instance to convert a class to json string and back in derived classes.
86 protected static final Gson GSON = new Gson();
88 protected final Logger logger = LoggerFactory.getLogger(getClass());
91 * Services of the device.
93 private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
95 protected BoschSHCHandler(Thing thing) {
100 * Returns the unique id of the Bosch device or service.
102 * For physical devices, the ID looks like
105 * hdm:Cameras:d20354de-44b5-3acc-924c-24c98d59da42
106 * hdm:ZigBee:000d6f0016d1c087
109 * For virtual devices / services, static IDs like the following are used:
113 * smokeDetectionSystem
114 * intrusionDetectionSystem
117 * @return Unique ID of the Bosch device or service.
119 public abstract @Nullable String getBoschID();
122 * Initializes this handler. Use this method to register all services of the device with
123 * {@link #registerService(BoschSHCService)}.
126 public void initialize() {
128 // Initialize device services
130 this.initializeServices();
131 } catch (BoschSHCException e) {
132 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
136 this.updateStatus(ThingStatus.ONLINE);
140 * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
141 * states of services).
143 * @param channelUID {@link ChannelUID} of the channel to which the command was sent
144 * @param command {@link Command}
147 public void handleCommand(ChannelUID channelUID, Command command) {
148 if (command instanceof RefreshType) {
149 // Refresh state of services that affect the channel
150 for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
151 if (deviceService.affectedChannels.contains(channelUID.getIdWithoutGroup())) {
152 this.refreshServiceState(deviceService.service);
159 * Processes an update which is received from the bridge.
161 * @param serviceName Name of service the update came from.
162 * @param stateData Current state of device service. Serialized as JSON.
164 public void processUpdate(String serviceName, @Nullable JsonElement stateData) {
165 // Check services of device to correctly
166 for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
167 BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
168 if (serviceName.equals(service.getServiceName())) {
169 service.onStateUpdate(stateData);
175 * Should be used by handlers to create their required services.
177 protected void initializeServices() throws BoschSHCException {
181 * Returns the bridge handler for this thing handler.
183 * @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration.
184 * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
186 protected BridgeHandler getBridgeHandler() throws BoschSHCException {
187 Bridge bridge = this.getBridge();
188 if (bridge == null) {
189 throw new BoschSHCException(String.format("No valid bridge set for %s (%s)", this.getThing().getLabel(),
190 this.getThing().getUID().getAsString()));
192 BridgeHandler bridgeHandler = (BridgeHandler) bridge.getHandler();
193 if (bridgeHandler == null) {
194 throw new BoschSHCException(String.format("Bridge of %s (%s) has no valid bridge handler",
195 this.getThing().getLabel(), this.getThing().getUID().getAsString()));
197 return bridgeHandler;
201 * Query the Bosch Smart Home Controller for the state of the service with the specified name.
203 * @note Use services instead of directly requesting a state.
205 * @param stateName Name of the service to query
206 * @param classOfT Class to convert the resulting JSON to
208 protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
209 String deviceId = this.getBoschID();
210 if (deviceId == null) {
214 BridgeHandler bridgeHandler = this.getBridgeHandler();
215 return bridgeHandler.getState(deviceId, stateName, classOfT);
216 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
217 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
218 String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage()));
220 } catch (InterruptedException e) {
221 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
222 String.format("Interrupted refresh state from service %s: %s", stateName, e.getMessage()));
223 Thread.currentThread().interrupt();
229 * Creates and registers a new service for this device.
231 * @param <TService> Type of service.
232 * @param <TState> Type of service state.
233 * @param newService Supplier function to create a new instance of the service.
234 * @param stateUpdateListener Function to call when a state update was received
236 * @param affectedChannels Channels which are affected by the state of this
238 * @return Instance of registered service.
239 * @throws BoschSHCException
241 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
242 Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
243 throws BoschSHCException {
244 return createService(newService, stateUpdateListener, affectedChannels, false);
248 * Creates and registers a new service for this device.
250 * @param <TService> Type of service.
251 * @param <TState> Type of service state.
252 * @param newService Supplier function to create a new instance of the service.
253 * @param stateUpdateListener Function to call when a state update was received
255 * @param affectedChannels Channels which are affected by the state of this
257 * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
258 * or service. Useful if state updates are not included in long poll results.
259 * @return Instance of registered service.
260 * @throws BoschSHCException
262 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
263 Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
264 boolean shouldFetchInitialState) throws BoschSHCException {
265 TService service = newService.get();
266 this.registerService(service, stateUpdateListener, affectedChannels, shouldFetchInitialState);
271 * Registers a service for this device.
273 * @param <TService> Type of service.
274 * @param <TState> Type of service state.
275 * @param service Service to register.
276 * @param stateUpdateListener Function to call when a state update was received
278 * @param affectedChannels Channels which are affected by the state of this
280 * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
281 * @throws BoschSHCException If no device id is set.
283 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
284 TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
285 throws BoschSHCException {
286 registerService(service, stateUpdateListener, affectedChannels, false);
290 * Registers a service for this device.
292 * @param <TService> Type of service.
293 * @param <TState> Type of service state.
294 * @param service Service to register.
295 * @param stateUpdateListener Function to call when a state update was received
297 * @param affectedChannels Channels which are affected by the state of this
299 * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
300 * or service. Useful if state updates are not included in long poll results.
301 * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
302 * @throws BoschSHCException If no device id is set.
304 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
305 TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
306 boolean shouldFetchInitialState) throws BoschSHCException {
308 String deviceId = verifyBoschID();
309 service.initialize(getBridgeHandler(), deviceId, stateUpdateListener);
310 this.registerService(service, affectedChannels);
312 if (shouldFetchInitialState) {
313 fetchInitialState(service, stateUpdateListener);
318 * Actively requests the initial state for the given service. This is required if long poll results do not contain
319 * status updates for the given service.
321 * @param <TService> Type of the service for which the state should be obtained
322 * @param <TState> Type of the objects to serialize and deserialize the service state
323 * @param service Service for which the state should be requested
324 * @param stateUpdateListener Function to process the obtained state
326 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
327 TService service, Consumer<TState> stateUpdateListener) {
331 TState serviceState = service.getState();
332 if (serviceState != null) {
333 stateUpdateListener.accept(serviceState);
335 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
336 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
338 } catch (InterruptedException e) {
339 Thread.currentThread().interrupt();
340 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
346 * Registers a write-only service that does not receive states from the bridge.
348 * Examples for such services are the actions of the intrusion detection service.
350 * @param <TService> Type of service.
351 * @param service Service to register.
352 * @throws BoschSHCException If no device ID is set.
354 protected <TService extends AbstractBoschSHCService> void registerStatelessService(TService service)
355 throws BoschSHCException {
357 String deviceId = verifyBoschID();
358 service.initialize(getBridgeHandler(), deviceId);
359 // do not register in service list because the service can not receive state updates
363 * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case.
365 * @return the Bosch ID, if present
366 * @throws BoschSHCException if no Bosch ID is set
368 private String verifyBoschID() throws BoschSHCException {
369 String deviceId = this.getBoschID();
370 if (deviceId == null) {
371 throw new BoschSHCException(
372 String.format("Could not register service for %s, no device id set", this.getThing()));
378 * Updates the state of a device service.
379 * Sets the status of the device to offline if setting the state fails.
381 * @param <TService> Type of service.
382 * @param <TState> Type of service state.
383 * @param service Service to set state for.
384 * @param state State to set.
386 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
387 TService service, TState state) {
389 service.setState(state);
390 } catch (TimeoutException | ExecutionException e) {
391 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
392 "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
393 } catch (InterruptedException e) {
394 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
395 .format("Interrupted update state for service %s: %s", service.getServiceName(), e.getMessage()));
396 Thread.currentThread().interrupt();
401 * Lets a service handle a received command.
402 * Sets the status of the device to offline if handling the command fails.
404 * @param <TService> Type of service.
405 * @param <TState> Type of service state.
406 * @param service Service which should handle command.
407 * @param command Command to handle.
409 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
410 TService service, Command command) {
412 if (command instanceof RefreshType) {
413 this.refreshServiceState(service);
415 TState state = service.handleCommand(command);
416 this.updateServiceState(service, state);
418 } catch (BoschSHCException e) {
419 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
420 String.format("Error when service %s should handle command %s: %s", service.getServiceName(),
421 command.getClass().getName(), e.getMessage()));
426 * Requests a service to refresh its state.
427 * Sets the device offline if request fails.
429 * @param <TService> Type of service.
430 * @param <TState> Type of service state.
431 * @param service Service to refresh state for.
433 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
436 service.refreshState();
437 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
438 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
439 String.format("Error when trying to refresh state from service %s: %s", service.getServiceName(),
441 } catch (InterruptedException e) {
442 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
443 .format("Interrupted refresh state from service %s: %s", service.getServiceName(), e.getMessage()));
444 Thread.currentThread().interrupt();
449 * Registers a service of this device.
451 * @param service Service which belongs to this device
452 * @param affectedChannels Channels which are affected by the state of this
455 private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
456 Collection<String> affectedChannels) {
457 this.services.add(new DeviceService<TState>(service, affectedChannels));
461 * Sends a HTTP POST request with empty body.
463 * @param <TService> Type of service.
464 * @param service Service implementing the action
466 protected <TService extends AbstractStatelessBoschSHCService> void postAction(TService service) {
468 service.postAction();
469 } catch (ExecutionException | TimeoutException e) {
470 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
471 String.format("Error while triggering action %s", service.getEndpoint()));
472 } catch (InterruptedException e) {
473 Thread.currentThread().interrupt();
474 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
475 String.format("Error while triggering action %s", service.getEndpoint()));
480 * Sends a HTTP POST request with the given request body.
482 * @param <TService> Type of service.
483 * @param <TState> Type of the request to be sent.
484 * @param service Service implementing the action
485 * @param request Request object to be serialized to JSON
487 protected <TService extends AbstractStatelessBoschSHCServiceWithRequestBody<TState>, TState extends BoschSHCServiceState> void postAction(
488 TService service, TState request) {
490 service.postAction(request);
491 } catch (ExecutionException | TimeoutException e) {
492 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
493 String.format("Error while triggering action %s", service.getEndpoint()));
494 } catch (InterruptedException e) {
495 Thread.currentThread().interrupt();
496 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
497 String.format("Error while triggering action %s", service.getEndpoint()));