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() {
127 // Initialize device services
129 this.initializeServices();
130 } catch (BoschSHCException e) {
131 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
135 this.updateStatus(ThingStatus.ONLINE);
139 * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
140 * states of services).
142 * @param channelUID {@link ChannelUID} of the channel to which the command was sent
143 * @param command {@link Command}
146 public void handleCommand(ChannelUID channelUID, Command command) {
147 if (command instanceof RefreshType) {
148 // Refresh state of services that affect the channel
149 for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
150 if (deviceService.affectedChannels.contains(channelUID.getIdWithoutGroup())) {
151 this.refreshServiceState(deviceService.service);
158 * Processes an update which is received from the bridge.
160 * @param serviceName Name of service the update came from.
161 * @param stateData Current state of device service. Serialized as JSON.
163 public void processUpdate(String serviceName, @Nullable JsonElement stateData) {
164 // Check services of device to correctly
165 for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
166 BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
167 if (serviceName.equals(service.getServiceName())) {
168 service.onStateUpdate(stateData);
174 * Should be used by handlers to create their required services.
176 protected void initializeServices() throws BoschSHCException {
180 * Returns the bridge handler for this thing handler.
182 * @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration.
183 * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
185 protected BridgeHandler getBridgeHandler() throws BoschSHCException {
186 Bridge bridge = this.getBridge();
187 if (bridge == null) {
188 throw new BoschSHCException(String.format("No valid bridge set for %s (%s)", this.getThing().getLabel(),
189 this.getThing().getUID().getAsString()));
191 BridgeHandler bridgeHandler = (BridgeHandler) bridge.getHandler();
192 if (bridgeHandler == null) {
193 throw new BoschSHCException(String.format("Bridge of %s (%s) has no valid bridge handler",
194 this.getThing().getLabel(), this.getThing().getUID().getAsString()));
196 return bridgeHandler;
200 * Query the Bosch Smart Home Controller for the state of the service with the specified name.
202 * @note Use services instead of directly requesting a state.
204 * @param stateName Name of the service to query
205 * @param classOfT Class to convert the resulting JSON to
207 protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
208 String deviceId = this.getBoschID();
209 if (deviceId == null) {
213 BridgeHandler bridgeHandler = this.getBridgeHandler();
214 return bridgeHandler.getState(deviceId, stateName, classOfT);
215 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
216 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
217 String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage()));
219 } catch (InterruptedException e) {
220 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
221 String.format("Interrupted refresh state from service %s: %s", stateName, e.getMessage()));
222 Thread.currentThread().interrupt();
228 * Creates and registers a new service for this device.
230 * @param <TService> Type of service.
231 * @param <TState> Type of service state.
232 * @param newService Supplier function to create a new instance of the service.
233 * @param stateUpdateListener Function to call when a state update was received
235 * @param affectedChannels Channels which are affected by the state of this
237 * @return Instance of registered service.
238 * @throws BoschSHCException
240 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
241 Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
242 throws BoschSHCException {
243 return createService(newService, stateUpdateListener, affectedChannels, false);
247 * Creates and registers a new service for this device.
249 * @param <TService> Type of service.
250 * @param <TState> Type of service state.
251 * @param newService Supplier function to create a new instance of the service.
252 * @param stateUpdateListener Function to call when a state update was received
254 * @param affectedChannels Channels which are affected by the state of this
256 * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
257 * or service. Useful if state updates are not included in long poll results.
258 * @return Instance of registered service.
259 * @throws BoschSHCException
261 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
262 Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
263 boolean shouldFetchInitialState) throws BoschSHCException {
264 TService service = newService.get();
265 this.registerService(service, stateUpdateListener, affectedChannels, shouldFetchInitialState);
270 * Registers a service for this device.
272 * @param <TService> Type of service.
273 * @param <TState> Type of service state.
274 * @param service Service to register.
275 * @param stateUpdateListener Function to call when a state update was received
277 * @param affectedChannels Channels which are affected by the state of this
279 * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
280 * @throws BoschSHCException If no device id is set.
282 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
283 TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
284 throws BoschSHCException {
285 registerService(service, stateUpdateListener, affectedChannels, false);
289 * Registers a service for this device.
291 * @param <TService> Type of service.
292 * @param <TState> Type of service state.
293 * @param service Service to register.
294 * @param stateUpdateListener Function to call when a state update was received
296 * @param affectedChannels Channels which are affected by the state of this
298 * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
299 * or service. Useful if state updates are not included in long poll results.
300 * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
301 * @throws BoschSHCException If no device id is set.
303 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
304 TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
305 boolean shouldFetchInitialState) throws BoschSHCException {
306 String deviceId = verifyBoschID();
307 service.initialize(getBridgeHandler(), deviceId, stateUpdateListener);
308 this.registerService(service, affectedChannels);
310 if (shouldFetchInitialState) {
311 fetchInitialState(service, stateUpdateListener);
316 * Actively requests the initial state for the given service. This is required if long poll results do not contain
317 * status updates for the given service.
319 * @param <TService> Type of the service for which the state should be obtained
320 * @param <TState> Type of the objects to serialize and deserialize the service state
321 * @param service Service for which the state should be requested
322 * @param stateUpdateListener Function to process the obtained state
324 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
325 TService service, Consumer<TState> stateUpdateListener) {
328 TState serviceState = service.getState();
329 if (serviceState != null) {
330 stateUpdateListener.accept(serviceState);
332 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
333 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
335 } catch (InterruptedException e) {
336 Thread.currentThread().interrupt();
337 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
343 * Registers a write-only service that does not receive states from the bridge.
345 * Examples for such services are the actions of the intrusion detection service.
347 * @param <TService> Type of service.
348 * @param service Service to register.
349 * @throws BoschSHCException If no device ID is set.
351 protected <TService extends AbstractBoschSHCService> void registerStatelessService(TService service)
352 throws BoschSHCException {
353 String deviceId = verifyBoschID();
354 service.initialize(getBridgeHandler(), deviceId);
355 // do not register in service list because the service can not receive state updates
359 * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case.
361 * @return the Bosch ID, if present
362 * @throws BoschSHCException if no Bosch ID is set
364 private String verifyBoschID() throws BoschSHCException {
365 String deviceId = this.getBoschID();
366 if (deviceId == null) {
367 throw new BoschSHCException(
368 String.format("Could not register service for %s, no device id set", this.getThing()));
374 * Updates the state of a device service.
375 * Sets the status of the device to offline if setting the state fails.
377 * @param <TService> Type of service.
378 * @param <TState> Type of service state.
379 * @param service Service to set state for.
380 * @param state State to set.
382 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
383 TService service, TState state) {
385 service.setState(state);
386 } catch (TimeoutException | ExecutionException e) {
387 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
388 "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
389 } catch (InterruptedException e) {
390 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
391 .format("Interrupted update state for service %s: %s", service.getServiceName(), e.getMessage()));
392 Thread.currentThread().interrupt();
397 * Lets a service handle a received command.
398 * Sets the status of the device to offline if handling the command fails.
400 * @param <TService> Type of service.
401 * @param <TState> Type of service state.
402 * @param service Service which should handle command.
403 * @param command Command to handle.
405 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
406 TService service, Command command) {
408 if (command instanceof RefreshType) {
409 this.refreshServiceState(service);
411 TState state = service.handleCommand(command);
412 this.updateServiceState(service, state);
414 } catch (BoschSHCException e) {
415 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
416 String.format("Error when service %s should handle command %s: %s", service.getServiceName(),
417 command.getClass().getName(), e.getMessage()));
422 * Requests a service to refresh its state.
423 * Sets the device offline if request fails.
425 * @param <TService> Type of service.
426 * @param <TState> Type of service state.
427 * @param service Service to refresh state for.
429 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
432 service.refreshState();
433 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
434 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
435 String.format("Error when trying to refresh state from service %s: %s", service.getServiceName(),
437 } catch (InterruptedException e) {
438 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
439 .format("Interrupted refresh state from service %s: %s", service.getServiceName(), e.getMessage()));
440 Thread.currentThread().interrupt();
445 * Registers a service of this device.
447 * @param service Service which belongs to this device
448 * @param affectedChannels Channels which are affected by the state of this
451 private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
452 Collection<String> affectedChannels) {
453 this.services.add(new DeviceService<TState>(service, affectedChannels));
457 * Sends a HTTP POST request with empty body.
459 * @param <TService> Type of service.
460 * @param service Service implementing the action
462 protected <TService extends AbstractStatelessBoschSHCService> void postAction(TService service) {
464 service.postAction();
465 } catch (ExecutionException | TimeoutException e) {
466 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
467 String.format("Error while triggering action %s", service.getEndpoint()));
468 } catch (InterruptedException e) {
469 Thread.currentThread().interrupt();
470 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
471 String.format("Error while triggering action %s", service.getEndpoint()));
476 * Sends a HTTP POST request with the given request body.
478 * @param <TService> Type of service.
479 * @param <TState> Type of the request to be sent.
480 * @param service Service implementing the action
481 * @param request Request object to be serialized to JSON
483 protected <TService extends AbstractStatelessBoschSHCServiceWithRequestBody<TState>, TState extends BoschSHCServiceState> void postAction(
484 TService service, TState request) {
486 service.postAction(request);
487 } catch (ExecutionException | TimeoutException e) {
488 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
489 String.format("Error while triggering action %s", service.getEndpoint()));
490 } catch (InterruptedException e) {
491 Thread.currentThread().interrupt();
492 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
493 String.format("Error while triggering action %s", service.getEndpoint()));