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