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.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
52 * @author David Pace - Support for child device updates
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;
83 private final Logger logger = LoggerFactory.getLogger(getClass());
86 * Services of the device.
88 private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
90 protected BoschSHCHandler(Thing thing) {
95 * Returns the unique id of the Bosch device or service.
97 * For physical devices, the ID looks like
100 * hdm:Cameras:d20354de-44b5-3acc-924c-24c98d59da42
101 * hdm:ZigBee:000d6f0016d1c087
104 * For virtual devices / services, static IDs like the following are used:
108 * smokeDetectionSystem
109 * intrusionDetectionSystem
112 * @return Unique ID of the Bosch device or service.
114 public abstract @Nullable String getBoschID();
117 * Initializes this handler.
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 // 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);
168 * Processes an update for a logical child device.
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
174 public void processChildUpdate(String childDeviceId, String serviceName, @Nullable JsonElement stateData) {
175 // default implementation is empty, subclasses may override
179 * Use this method to register all services of the device with
180 * {@link #registerService(BoschSHCService, Consumer, Collection, boolean)}.
182 protected void initializeServices() throws BoschSHCException {
183 // default implementation is empty, subclasses may override
187 * Returns the bridge handler for this thing handler.
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.
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()));
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()));
203 return bridgeHandler;
207 * Query the Bosch Smart Home Controller for the state of the service with the specified name.
209 * @implNote Use services instead of directly requesting a state.
211 * @param stateName Name of the service to query
212 * @param classOfT Class to convert the resulting JSON to
214 protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
215 String deviceId = this.getBoschID();
216 if (deviceId == null) {
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()));
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();
235 * Creates and registers a new service for this device.
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
242 * @param affectedChannels Channels which are affected by the state of this
244 * @return Instance of registered service.
245 * @throws BoschSHCException
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);
254 * Creates and registers a new service for this device.
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
261 * @param affectedChannels Channels which are affected by the state of this
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
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);
277 * Registers a service for this device.
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
284 * @param affectedChannels Channels which are affected by the state of this
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.
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);
296 * Registers a service for this device.
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
303 * @param affectedChannels Channels which are affected by the state of this
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.
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);
317 if (shouldFetchInitialState) {
318 fetchInitialState(service, stateUpdateListener);
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.
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
331 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
332 TService service, Consumer<TState> stateUpdateListener) {
335 TState serviceState = service.getState();
336 if (serviceState != null) {
337 stateUpdateListener.accept(serviceState);
339 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
340 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
342 } catch (InterruptedException e) {
343 Thread.currentThread().interrupt();
344 logger.debug("Could not retrieve the initial state for service {} of device {}", service.getServiceName(),
350 * Registers a write-only service that does not receive states from the bridge.
352 * Examples for such services are the actions of the intrusion detection service.
354 * @param <TService> Type of service.
355 * @param service Service to register.
356 * @throws BoschSHCException If no device ID is set.
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
366 * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case.
368 * @return the Bosch ID, if present
369 * @throws BoschSHCException if no Bosch ID is set
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()));
381 * Updates the state of a device service.
382 * Sets the status of the device to offline if setting the state fails.
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.
389 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
390 TService service, TState state) {
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();
404 * Lets a service handle a received command.
405 * Sets the status of the device to offline if handling the command fails.
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.
412 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
413 TService service, Command command) {
415 if (command instanceof RefreshType) {
416 this.refreshServiceState(service);
418 TState state = service.handleCommand(command);
419 this.updateServiceState(service, state);
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()));
429 * Requests a service to refresh its state.
430 * Sets the device offline if request fails.
432 * @param <TService> Type of service.
433 * @param <TState> Type of service state.
434 * @param service Service to refresh state for.
436 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
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(),
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();
452 * Registers a service of this device.
454 * @param service Service which belongs to this device
455 * @param affectedChannels Channels which are affected by the state of this
458 private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
459 Collection<String> affectedChannels) {
460 this.services.add(new DeviceService<>(service, affectedChannels));
464 * Sends a HTTP POST request with empty body.
466 * @param <TService> Type of service.
467 * @param service Service implementing the action
469 protected <TService extends AbstractStatelessBoschSHCService> void postAction(TService service) {
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()));
483 * Sends a HTTP POST request with the given request body.
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
490 protected <TService extends AbstractStatelessBoschSHCServiceWithRequestBody<TState>, TState extends BoschSHCServiceState> void postAction(
491 TService service, TState request) {
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()));