2 * Copyright (c) 2010-2022 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.BoschSHCService;
28 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
29 import org.openhab.core.thing.Bridge;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.thing.ThingStatusDetail;
34 import org.openhab.core.thing.binding.BaseThingHandler;
35 import org.openhab.core.types.Command;
36 import org.openhab.core.types.RefreshType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.Gson;
41 import com.google.gson.JsonElement;
44 * The {@link BoschSHCHandler} represents Bosch Things. Each type of device
45 * inherits from this abstract thing handler.
47 * @author Stefan Kästle - Initial contribution
48 * @author Christian Oeing - refactorings of e.g. server registration
51 public abstract class BoschSHCHandler extends BaseThingHandler {
54 * Service State for a Bosch device.
56 class DeviceService<TState extends BoschSHCServiceState> {
60 * @param service Service which belongs to the device.
61 * @param affectedChannels Channels which are affected by the state of this service.
63 public DeviceService(BoschSHCService<TState> service, Collection<String> affectedChannels) {
64 this.service = service;
65 this.affectedChannels = affectedChannels;
69 * Service which belongs to the device.
71 public final BoschSHCService<TState> service;
74 * Channels which are affected by the state of this service.
76 public final Collection<String> affectedChannels;
80 * Reusable gson instance to convert a class to json string and back in derived classes.
82 protected static final Gson GSON = new Gson();
84 protected final Logger logger = LoggerFactory.getLogger(getClass());
87 * Bosch SHC configuration loaded from openHAB configuration.
89 private @Nullable BoschSHCConfiguration config;
92 * Services of the device.
94 private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
96 public BoschSHCHandler(Thing thing) {
101 * Returns the unique id of the Bosch device.
103 * @return Unique id of the Bosch device.
105 public @Nullable String getBoschID() {
106 BoschSHCConfiguration config = this.config;
107 if (config != null) {
115 * Initializes this handler. Use this method to register all services of the device with
116 * {@link #registerService(BoschSHCService)}.
119 public void initialize() {
120 var config = this.config = getConfigAs(BoschSHCConfiguration.class);
122 String deviceId = config.id;
123 if (deviceId == null || deviceId.isEmpty()) {
124 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
125 "@text/offline.conf-error.empty-device-id");
129 // Try to get device info to make sure the device exists
131 var bridgeHandler = this.getBridgeHandler();
132 var info = bridgeHandler.getDeviceInfo(deviceId);
133 logger.trace("Device initialized:\n{}", info.toString());
134 } catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) {
135 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
139 // Initialize device services
141 this.initializeServices();
142 } catch (BoschSHCException e) {
143 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
147 this.updateStatus(ThingStatus.ONLINE);
151 * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
152 * states of services).
154 * @param channelUID {@link ChannelUID} of the channel to which the command was sent
155 * @param command {@link Command}
158 public void handleCommand(ChannelUID channelUID, Command command) {
159 if (command instanceof RefreshType) {
160 // Refresh state of services that affect the channel
161 for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
162 if (deviceService.affectedChannels.contains(channelUID.getIdWithoutGroup())) {
163 this.refreshServiceState(deviceService.service);
170 * Processes an update which is received from the bridge.
172 * @param serviceName Name of service the update came from.
173 * @param stateData Current state of device service. Serialized as JSON.
175 public void processUpdate(String serviceName, JsonElement stateData) {
176 // Check services of device to correctly
177 for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
178 BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
179 if (serviceName.equals(service.getServiceName())) {
180 service.onStateUpdate(stateData);
186 * Should be used by handlers to create their required services.
188 protected void initializeServices() throws BoschSHCException {
192 * Returns the bridge handler for this thing handler.
194 * @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration.
195 * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
197 protected BridgeHandler getBridgeHandler() throws BoschSHCException {
198 Bridge bridge = this.getBridge();
199 if (bridge == null) {
200 throw new BoschSHCException(String.format("No valid bridge set for %s (%s)", this.getThing().getLabel(),
201 this.getThing().getUID().getAsString()));
203 BridgeHandler bridgeHandler = (BridgeHandler) bridge.getHandler();
204 if (bridgeHandler == null) {
205 throw new BoschSHCException(String.format("Bridge of %s (%s) has no valid bridge handler",
206 this.getThing().getLabel(), this.getThing().getUID().getAsString()));
208 return bridgeHandler;
212 * Query the Bosch Smart Home Controller for the state of the service with the specified name.
214 * @note Use services instead of directly requesting a state.
216 * @param stateName Name of the service to query
217 * @param classOfT Class to convert the resulting JSON to
219 protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
220 String deviceId = this.getBoschID();
221 if (deviceId == null) {
225 BridgeHandler bridgeHandler = this.getBridgeHandler();
226 return bridgeHandler.getState(deviceId, stateName, classOfT);
227 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
228 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
229 String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage()));
231 } catch (InterruptedException e) {
232 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
233 String.format("Interrupted refresh state from service %s: %s", stateName, e.getMessage()));
234 Thread.currentThread().interrupt();
240 * Creates and registers a new service for this device.
242 * @param <TService> Type of service.
243 * @param <TState> Type of service state.
244 * @param newService Supplier function to create a new instance of the service.
245 * @param stateUpdateListener Function to call when a state update was received
247 * @param affectedChannels Channels which are affected by the state of this
249 * @return Instance of registered service.
250 * @throws BoschSHCException
252 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
253 Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
254 throws BoschSHCException {
255 TService service = newService.get();
256 this.registerService(service, stateUpdateListener, affectedChannels);
261 * Registers a service for this device.
263 * @param <TService> Type of service.
264 * @param <TState> Type of service state.
265 * @param service Service to register.
266 * @param stateUpdateListener Function to call when a state update was received
268 * @param affectedChannels Channels which are affected by the state of this
270 * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
271 * @throws BoschSHCException If no device id is set.
273 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
274 TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
275 throws BoschSHCException {
276 BridgeHandler bridgeHandler = this.getBridgeHandler();
278 String deviceId = this.getBoschID();
279 if (deviceId == null) {
280 throw new BoschSHCException(
281 String.format("Could not register service for %s, no device id set", this.getThing()));
284 service.initialize(bridgeHandler, deviceId, stateUpdateListener);
285 this.registerService(service, affectedChannels);
289 * Updates the state of a device service.
290 * Sets the status of the device to offline if setting the state fails.
292 * @param <TService> Type of service.
293 * @param <TState> Type of service state.
294 * @param service Service to set state for.
295 * @param state State to set.
297 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
298 TService service, TState state) {
300 service.setState(state);
301 } catch (TimeoutException | ExecutionException e) {
302 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
303 "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
304 } catch (InterruptedException e) {
305 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
306 .format("Interrupted update state for service %s: %s", service.getServiceName(), e.getMessage()));
307 Thread.currentThread().interrupt();
312 * Lets a service handle a received command.
313 * Sets the status of the device to offline if handling the command fails.
315 * @param <TService> Type of service.
316 * @param <TState> Type of service state.
317 * @param service Service which should handle command.
318 * @param command Command to handle.
320 protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
321 TService service, Command command) {
323 if (command instanceof RefreshType) {
324 this.refreshServiceState(service);
326 TState state = service.handleCommand(command);
327 this.updateServiceState(service, state);
329 } catch (BoschSHCException e) {
330 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
331 String.format("Error when service %s should handle command %s: %s", service.getServiceName(),
332 command.getClass().getName(), e.getMessage()));
337 * Requests a service to refresh its state.
338 * Sets the device offline if request fails.
340 * @param <TService> Type of service.
341 * @param <TState> Type of service state.
342 * @param service Service to refresh state for.
344 private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
347 service.refreshState();
348 } catch (TimeoutException | ExecutionException | BoschSHCException e) {
349 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
350 String.format("Error when trying to refresh state from service %s: %s", service.getServiceName(),
352 } catch (InterruptedException e) {
353 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
354 .format("Interrupted refresh state from service %s: %s", service.getServiceName(), e.getMessage()));
355 Thread.currentThread().interrupt();
360 * Registers a service of this device.
362 * @param service Service which belongs to this device
363 * @param affectedChannels Channels which are affected by the state of this
366 private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
367 Collection<String> affectedChannels) {
368 this.services.add(new DeviceService<TState>(service, affectedChannels));