]> git.basschouten.com Git - openhab-addons.git/blob
23387dcd763c4749b4f0b1247cda31b0f4f88e60
[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.nuki.internal.handler;
14
15 import java.time.OffsetDateTime;
16 import java.time.ZoneId;
17 import java.time.ZonedDateTime;
18 import java.time.format.DateTimeParseException;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.function.Consumer;
22 import java.util.function.Function;
23 import java.util.regex.Pattern;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.nuki.internal.configuration.NukiDeviceConfiguration;
28 import org.openhab.binding.nuki.internal.constants.NukiBindingConstants;
29 import org.openhab.binding.nuki.internal.dataexchange.BridgeListResponse;
30 import org.openhab.binding.nuki.internal.dataexchange.BridgeLockStateResponse;
31 import org.openhab.binding.nuki.internal.dataexchange.NukiBaseResponse;
32 import org.openhab.binding.nuki.internal.dataexchange.NukiHttpClient;
33 import org.openhab.binding.nuki.internal.dto.BridgeApiDeviceStateDto;
34 import org.openhab.binding.nuki.internal.dto.BridgeApiListDeviceDto;
35 import org.openhab.core.config.core.Configuration;
36 import org.openhab.core.library.types.DateTimeType;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.Channel;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.ThingStatusInfo;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.BridgeHandler;
48 import org.openhab.core.thing.binding.ThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.openhab.core.types.State;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57  * The {@link AbstractNukiDeviceHandler} is a base class for implementing ThingHandlers for Nuki devices
58  *
59  * @author Jan Vybíral - Initial contribution
60  */
61 @NonNullByDefault
62 public abstract class AbstractNukiDeviceHandler<T extends NukiDeviceConfiguration> extends BaseThingHandler {
63
64     protected final Logger logger = LoggerFactory.getLogger(getClass());
65     private static final int JOB_INTERVAL = 60;
66     private static final Pattern NUKI_ID_HEX_PATTERN = Pattern.compile("[A-F\\d]{8}", Pattern.CASE_INSENSITIVE);
67
68     @Nullable
69     protected ScheduledFuture<?> reInitJob;
70     protected T configuration;
71     @Nullable
72     private NukiHttpClient nukiHttpClient;
73
74     public AbstractNukiDeviceHandler(Thing thing) {
75         super(thing);
76         this.configuration = getConfigAs(getConfigurationClass());
77     }
78
79     private static String hexToDecimal(String hexString) {
80         return String.valueOf(Integer.parseInt(hexString, 16));
81     }
82
83     protected void withHttpClient(Consumer<NukiHttpClient> consumer) {
84         withHttpClient(client -> {
85             consumer.accept(client);
86             return null;
87         }, null);
88     }
89
90     protected <U> U withHttpClient(Function<NukiHttpClient, U> consumer, U defaultValue) {
91         NukiHttpClient client = this.nukiHttpClient;
92         if (client == null) {
93             logger.warn("Nuki HTTP client is null. This is a bug in Nuki Binding, please report it",
94                     new IllegalStateException());
95             return defaultValue;
96         } else {
97             return consumer.apply(client);
98         }
99     }
100
101     /**
102      * Performs migration of old device configuration
103      * 
104      * @return true if configuration was change and reload is needed
105      */
106     protected boolean migrateConfiguration() {
107         String nukiId = getConfig().get(NukiBindingConstants.PROPERTY_NUKI_ID).toString();
108         // legacy support - check if nukiId is hexadecimal (which might have been set by previous binding version)
109         // and convert it to decimal
110         if (NUKI_ID_HEX_PATTERN.matcher(nukiId).matches()) {
111             logger.warn(
112                     "SmartLock '{}' was created by old version of binding. It is recommended to delete it and discover again",
113                     thing.getUID());
114             Configuration newConfig = editConfiguration();
115             newConfig.put(NukiBindingConstants.PROPERTY_NUKI_ID, hexToDecimal(nukiId));
116             updateConfiguration(newConfig);
117             return true;
118         } else {
119             return false;
120         }
121     }
122
123     @Override
124     public void initialize() {
125         this.configuration = getConfigAs(getConfigurationClass());
126         if (migrateConfiguration()) {
127             this.configuration = getConfigAs(getConfigurationClass());
128         }
129         scheduler.execute(this::initializeHandler);
130     }
131
132     @Override
133     public void dispose() {
134         stopReInitJob();
135     }
136
137     private void initializeHandler() {
138         Bridge bridge = getBridge();
139         if (bridge == null) {
140             initializeHandler(null, null);
141         } else {
142             initializeHandler(bridge.getHandler(), bridge.getStatus());
143         }
144     }
145
146     private void initializeHandler(@Nullable ThingHandler handler, @Nullable ThingStatus bridgeStatus) {
147         if (handler instanceof NukiBridgeHandler && bridgeStatus != null) {
148             NukiBridgeHandler bridgeHandler = (NukiBridgeHandler) handler;
149             if (bridgeStatus == ThingStatus.ONLINE) {
150                 this.nukiHttpClient = bridgeHandler.getNukiHttpClient();
151                 withHttpClient(client -> {
152                     BridgeListResponse bridgeListResponse = client.getList();
153                     if (handleResponse(bridgeListResponse, null, null)) {
154                         BridgeApiListDeviceDto device = bridgeListResponse.getDevice(configuration.nukiId);
155                         if (device == null) {
156                             logger.warn("Configured Smart Lock [{}] not present in bridge device list",
157                                     configuration.nukiId);
158                         } else {
159                             updateStatus(ThingStatus.ONLINE);
160                             refreshData(device);
161                             stopReInitJob();
162                         }
163                     }
164                 });
165             } else {
166                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
167                 stopReInitJob();
168             }
169         } else {
170             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
171             stopReInitJob();
172         }
173     }
174
175     protected void refreshData(BridgeApiListDeviceDto device) {
176         updateProperty(NukiBindingConstants.PROPERTY_NAME, device.getName());
177         if (device.getFirmwareVersion() != null) {
178             updateProperty(NukiBindingConstants.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
179         }
180
181         if (device.getLastKnownState() != null) {
182             refreshState(device.getLastKnownState());
183         }
184     }
185
186     /**
187      * Method to refresh state of this thing. Implementors should read values from state and update corresponding
188      * channels.
189      * 
190      * @param state Current state of this thing as obtained from Bridge API
191      */
192     public abstract void refreshState(BridgeApiDeviceStateDto state);
193
194     protected <U> void updateState(String channelId, U state, Function<U, State> transform) {
195         Channel channel = thing.getChannel(channelId);
196         if (channel != null) {
197             updateState(channel.getUID(), state, transform);
198         }
199     }
200
201     protected void triggerChannel(String channelId, String event) {
202         Channel channel = thing.getChannel(channelId);
203         if (channel != null) {
204             triggerChannel(channel.getUID(), event);
205         }
206     }
207
208     protected <U> void updateState(ChannelUID channel, U state, Function<U, State> transform) {
209         updateState(channel, state == null ? UnDefType.NULL : transform.apply(state));
210     }
211
212     protected State toDateTime(String dateTimeString) {
213         try {
214             ZonedDateTime date = OffsetDateTime.parse(dateTimeString).atZoneSameInstant(ZoneId.systemDefault());
215             return new DateTimeType(date);
216         } catch (DateTimeParseException e) {
217             logger.debug("Failed to parse date from '{}'", dateTimeString);
218             return UnDefType.UNDEF;
219         }
220     }
221
222     @Override
223     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
224         scheduler.execute(() -> {
225             Bridge bridge = getBridge();
226             if (bridge == null) {
227                 initializeHandler(null, bridgeStatusInfo.getStatus());
228             } else {
229                 initializeHandler(bridge.getHandler(), bridgeStatusInfo.getStatus());
230             }
231         });
232     }
233
234     @Override
235     public void handleCommand(ChannelUID channelUID, Command command) {
236         logger.trace("handleCommand({}, {})", channelUID, command);
237
238         if (getThing().getStatus() != ThingStatus.ONLINE) {
239             logger.debug("Thing is not ONLINE; command[{}] for channelUID[{}] is ignored", command, channelUID);
240         } else if (command instanceof RefreshType) {
241             scheduler.execute(() -> {
242                 withHttpClient(client -> {
243                     BridgeLockStateResponse bridgeLockStateResponse = client.getBridgeLockState(configuration.nukiId,
244                             getDeviceType());
245                     if (handleResponse(bridgeLockStateResponse, channelUID.getAsString(), command.toString())) {
246                         if (!doHandleRefreshCommand(channelUID, command, bridgeLockStateResponse)) {
247                             logger.debug("Command[{}] for channelUID[{}] not implemented!", command, channelUID);
248                         }
249                     }
250                 });
251             });
252         } else {
253             scheduler.execute(() -> {
254                 if (!doHandleCommand(channelUID, command)) {
255                     logger.debug("Unexpected command[{}] for channelUID[{}]!", command, channelUID);
256                 }
257             });
258         }
259     }
260
261     /**
262      * Get type of this device
263      * 
264      * @return Device type
265      */
266     protected abstract int getDeviceType();
267
268     /**
269      * Get class of configuration
270      *
271      * @return Configuration class
272      */
273     protected abstract Class<T> getConfigurationClass();
274
275     /**
276      * Method to handle channel command - will not receive REFRESH command
277      * 
278      * @param channelUID Channel which received command
279      * @param command Command received
280      * @return true if command was handled
281      */
282     protected abstract boolean doHandleCommand(ChannelUID channelUID, Command command);
283
284     /**
285      * Method for handlign {@link RefreshType} command
286      * 
287      * @param channelUID Channel which received command
288      * @param command Command received, will always be {@link RefreshType}
289      * @param response Response from /lockState endpoint of Bridge API
290      * @return true if command was handled
291      */
292     protected boolean doHandleRefreshCommand(ChannelUID channelUID, Command command, BridgeLockStateResponse response) {
293         refreshState(response.getBridgeApiLockStateDto());
294         return true;
295     }
296
297     protected boolean handleResponse(NukiBaseResponse nukiBaseResponse, @Nullable String channelUID,
298             @Nullable String command) {
299         if (nukiBaseResponse.getStatus() == 200 && nukiBaseResponse.isSuccess()) {
300             logger.debug("Command[{}] succeeded for channelUID[{}] on nukiId[{}]!", command, channelUID,
301                     configuration.nukiId);
302             return true;
303         } else if (nukiBaseResponse.getStatus() != 200) {
304             logger.debug("Request to Bridge failed! status[{}] - message[{}]", nukiBaseResponse.getStatus(),
305                     nukiBaseResponse.getMessage());
306         } else if (!nukiBaseResponse.isSuccess()) {
307             logger.debug(
308                     "Request from Bridge to Smart Lock failed! status[{}] - message[{}] - isSuccess[{}]. Check if Nuki Smart Lock is powered on!",
309                     nukiBaseResponse.getStatus(), nukiBaseResponse.getMessage(), nukiBaseResponse.isSuccess());
310         }
311         logger.debug("Could not handle command[{}] for channelUID[{}] on nukiId[{}]!", command, channelUID,
312                 configuration.nukiId);
313
314         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, nukiBaseResponse.getMessage());
315
316         updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_LOCK, OnOffType.OFF, Function.identity());
317         updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_STATE, NukiBindingConstants.LOCK_STATES_UNDEFINED,
318                 DecimalType::new);
319         updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_DOOR_STATE, NukiBindingConstants.DOORSENSOR_STATES_UNKNOWN,
320                 DecimalType::new);
321
322         withBridgeAsync(bridge -> {
323             bridge.checkBridgeOnline();
324             startReInitJob();
325         });
326         return false;
327     }
328
329     private void withBridgeAsync(Consumer<NukiBridgeHandler> handler) {
330         Bridge bridge = getBridge();
331         if (bridge != null) {
332             BridgeHandler bridgeHandler = bridge.getHandler();
333             if (bridgeHandler instanceof NukiBridgeHandler) {
334                 scheduler.execute(() -> handler.accept((NukiBridgeHandler) bridgeHandler));
335             }
336         }
337     }
338
339     private void startReInitJob() {
340         logger.trace("Starting reInitJob with interval of  {}secs for Smart Lock[{}].", JOB_INTERVAL,
341                 configuration.nukiId);
342         if (reInitJob != null) {
343             logger.trace("Already started reInitJob for Smart Lock[{}].", configuration.nukiId);
344             return;
345         }
346         reInitJob = scheduler.scheduleWithFixedDelay(this::initializeHandler, 1, JOB_INTERVAL, TimeUnit.SECONDS);
347     }
348
349     private void stopReInitJob() {
350         logger.trace("Stopping reInitJob for Smart Lock[{}].", configuration.nukiId);
351         ScheduledFuture<?> job = reInitJob;
352         if (job != null) {
353             job.cancel(true);
354             logger.trace("Stopped reInitJob for Smart Lock[{}].", configuration.nukiId);
355         }
356         reInitJob = null;
357     }
358 }