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