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