]> git.basschouten.com Git - openhab-addons.git/blob
c88199deb0a4dc6f144cda647eb047bb23b73ef4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.boschshc.internal.devices;
14
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;
22
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;
39
40 import com.google.gson.Gson;
41 import com.google.gson.JsonElement;
42
43 /**
44  * The {@link BoschSHCHandler} represents Bosch Things. Each type of device
45  * inherits from this abstract thing handler.
46  *
47  * @author Stefan Kästle - Initial contribution
48  * @author Christian Oeing - refactorings of e.g. server registration
49  */
50 @NonNullByDefault
51 public abstract class BoschSHCHandler extends BaseThingHandler {
52
53     /**
54      * Service State for a Bosch device.
55      */
56     class DeviceService<TState extends BoschSHCServiceState> {
57         /**
58          * Constructor.
59          *
60          * @param service Service which belongs to the device.
61          * @param affectedChannels Channels which are affected by the state of this service.
62          */
63         public DeviceService(BoschSHCService<TState> service, Collection<String> affectedChannels) {
64             this.service = service;
65             this.affectedChannels = affectedChannels;
66         }
67
68         /**
69          * Service which belongs to the device.
70          */
71         public final BoschSHCService<TState> service;
72
73         /**
74          * Channels which are affected by the state of this service.
75          */
76         public final Collection<String> affectedChannels;
77     }
78
79     /**
80      * Reusable gson instance to convert a class to json string and back in derived classes.
81      */
82     protected static final Gson GSON = new Gson();
83
84     protected final Logger logger = LoggerFactory.getLogger(getClass());
85
86     /**
87      * Bosch SHC configuration loaded from openHAB configuration.
88      */
89     private @Nullable BoschSHCConfiguration config;
90
91     /**
92      * Services of the device.
93      */
94     private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
95
96     public BoschSHCHandler(Thing thing) {
97         super(thing);
98     }
99
100     /**
101      * Returns the unique id of the Bosch device.
102      *
103      * @return Unique id of the Bosch device.
104      */
105     public @Nullable String getBoschID() {
106         BoschSHCConfiguration config = this.config;
107         if (config != null) {
108             return config.id;
109         } else {
110             return null;
111         }
112     }
113
114     /**
115      * Initializes this handler. Use this method to register all services of the device with
116      * {@link #registerService(BoschSHCService)}.
117      */
118     @Override
119     public void initialize() {
120         var config = this.config = getConfigAs(BoschSHCConfiguration.class);
121
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");
126             return;
127         }
128
129         // Try to get device info to make sure the device exists
130         try {
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());
136             return;
137         }
138
139         // Initialize device services
140         try {
141             this.initializeServices();
142         } catch (BoschSHCException e) {
143             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
144             return;
145         }
146
147         this.updateStatus(ThingStatus.ONLINE);
148     }
149
150     /**
151      * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
152      * states of services).
153      *
154      * @param channelUID {@link ChannelUID} of the channel to which the command was sent
155      * @param command {@link Command}
156      */
157     @Override
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);
164                 }
165             }
166         }
167     }
168
169     /**
170      * Processes an update which is received from the bridge.
171      *
172      * @param serviceName Name of service the update came from.
173      * @param stateData Current state of device service. Serialized as JSON.
174      */
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);
181             }
182         }
183     }
184
185     /**
186      * Should be used by handlers to create their required services.
187      */
188     protected void initializeServices() throws BoschSHCException {
189     }
190
191     /**
192      * Returns the bridge handler for this thing handler.
193      *
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.
196      */
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()));
202         }
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()));
207         }
208         return bridgeHandler;
209     }
210
211     /**
212      * Query the Bosch Smart Home Controller for the state of the service with the specified name.
213      *
214      * @note Use services instead of directly requesting a state.
215      *
216      * @param stateName Name of the service to query
217      * @param classOfT Class to convert the resulting JSON to
218      */
219     protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
220         String deviceId = this.getBoschID();
221         if (deviceId == null) {
222             return null;
223         }
224         try {
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()));
230             return null;
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();
235             return null;
236         }
237     }
238
239     /**
240      * Creates and registers a new service for this device.
241      *
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
246      *            from the device.
247      * @param affectedChannels Channels which are affected by the state of this
248      *            service.
249      * @return Instance of registered service.
250      * @throws BoschSHCException
251      */
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);
257         return service;
258     }
259
260     /**
261      * Registers a service for this device.
262      *
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
267      *            from the device.
268      * @param affectedChannels Channels which are affected by the state of this
269      *            service.
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.
272      */
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();
277
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()));
282         }
283
284         service.initialize(bridgeHandler, deviceId, stateUpdateListener);
285         this.registerService(service, affectedChannels);
286     }
287
288     /**
289      * Updates the state of a device service.
290      * Sets the status of the device to offline if setting the state fails.
291      *
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.
296      */
297     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
298             TService service, TState state) {
299         try {
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();
308         }
309     }
310
311     /**
312      * Lets a service handle a received command.
313      * Sets the status of the device to offline if handling the command fails.
314      *
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.
319      */
320     protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
321             TService service, Command command) {
322         try {
323             if (command instanceof RefreshType) {
324                 this.refreshServiceState(service);
325             } else {
326                 TState state = service.handleCommand(command);
327                 this.updateServiceState(service, state);
328             }
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()));
333         }
334     }
335
336     /**
337      * Requests a service to refresh its state.
338      * Sets the device offline if request fails.
339      * 
340      * @param <TService> Type of service.
341      * @param <TState> Type of service state.
342      * @param service Service to refresh state for.
343      */
344     private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
345             TService service) {
346         try {
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(),
351                             e.getMessage()));
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();
356         }
357     }
358
359     /**
360      * Registers a service of this device.
361      *
362      * @param service Service which belongs to this device
363      * @param affectedChannels Channels which are affected by the state of this
364      *            service
365      */
366     private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
367             Collection<String> affectedChannels) {
368         this.services.add(new DeviceService<TState>(service, affectedChannels));
369     }
370 }