2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nuki.internal.handler;
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;
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;
57 * The {@link AbstractNukiDeviceHandler} is a base class for implementing ThingHandlers for Nuki devices
59 * @author Jan Vybíral - Initial contribution
62 public abstract class AbstractNukiDeviceHandler<T extends NukiDeviceConfiguration> extends BaseThingHandler {
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);
70 protected ScheduledFuture<?> reInitJob;
71 protected T configuration;
73 private NukiHttpClient nukiHttpClient;
74 protected final boolean readOnly;
76 public AbstractNukiDeviceHandler(Thing thing, boolean readOnly) {
78 this.readOnly = readOnly;
79 this.configuration = getConfigAs(getConfigurationClass());
82 private static String hexToDecimal(String hexString) {
83 return String.valueOf(Integer.parseInt(hexString, 16));
86 protected void withHttpClient(Consumer<NukiHttpClient> consumer) {
87 withHttpClient(client -> {
88 consumer.accept(client);
93 protected <U> U withHttpClient(Function<NukiHttpClient, U> consumer, U defaultValue) {
94 NukiHttpClient client = this.nukiHttpClient;
96 logger.warn("Nuki HTTP client is null. This is a bug in Nuki Binding, please report it",
97 new IllegalStateException());
100 return consumer.apply(client);
105 * Performs migration of old device configuration
107 * @return true if configuration was change and reload is needed
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()) {
116 "SmartLock '{}' was created by old version of binding. It is recommended to delete it and discover again",
119 Configuration newConfig = editConfiguration();
120 newConfig.put(NukiBindingConstants.PROPERTY_NUKI_ID, hexToDecimal(nukiId));
121 updateConfiguration(newConfig);
129 public void initialize() {
130 this.configuration = getConfigAs(getConfigurationClass());
131 if (migrateConfiguration()) {
132 this.configuration = getConfigAs(getConfigurationClass());
134 scheduler.execute(this::initializeHandler);
138 public void dispose() {
142 private void initializeHandler() {
143 Bridge bridge = getBridge();
144 if (bridge == null) {
145 initializeHandler(null, null);
147 initializeHandler(bridge.getHandler(), bridge.getStatus());
151 private void initializeHandler(@Nullable ThingHandler handler, @Nullable ThingStatus bridgeStatus) {
152 if (handler instanceof NukiBridgeHandler && bridgeStatus != null) {
153 NukiBridgeHandler bridgeHandler = (NukiBridgeHandler) handler;
154 if (bridgeStatus == ThingStatus.ONLINE) {
155 this.nukiHttpClient = bridgeHandler.getNukiHttpClient();
156 withHttpClient(client -> {
157 BridgeListResponse bridgeListResponse = client.getList();
158 if (handleResponse(bridgeListResponse, null, null)) {
159 BridgeApiListDeviceDto device = bridgeListResponse.getDevice(configuration.nukiId);
160 if (device == null) {
161 logger.warn("Configured Smart Lock [{}] not present in bridge device list",
162 configuration.nukiId);
164 updateStatus(ThingStatus.ONLINE);
171 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
180 protected void refreshData(BridgeApiListDeviceDto device) {
181 updateProperty(NukiBindingConstants.PROPERTY_NAME, device.getName());
182 if (device.getFirmwareVersion() != null) {
183 updateProperty(NukiBindingConstants.PROPERTY_FIRMWARE_VERSION, device.getFirmwareVersion());
186 if (device.getLastKnownState() != null) {
187 refreshState(device.getLastKnownState());
192 * Method to refresh state of this thing. Implementors should read values from state and update corresponding
195 * @param state Current state of this thing as obtained from Bridge API
197 public abstract void refreshState(BridgeApiDeviceStateDto state);
199 protected <U> void updateState(String channelId, U state, Function<U, State> transform) {
200 Channel channel = thing.getChannel(channelId);
201 if (channel != null) {
202 updateState(channel.getUID(), state, transform);
206 protected void triggerChannel(String channelId, String event) {
207 Channel channel = thing.getChannel(channelId);
208 if (channel != null) {
209 triggerChannel(channel.getUID(), event);
213 protected <U> void updateState(ChannelUID channel, U state, Function<U, State> transform) {
214 updateState(channel, state == null ? UnDefType.NULL : transform.apply(state));
217 protected State toDateTime(String dateTimeString) {
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;
228 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
229 scheduler.execute(() -> {
230 Bridge bridge = getBridge();
231 if (bridge == null) {
232 initializeHandler(null, bridgeStatusInfo.getStatus());
234 initializeHandler(bridge.getHandler(), bridgeStatusInfo.getStatus());
240 public void handleCommand(ChannelUID channelUID, Command command) {
241 logger.trace("handleCommand({}, {})", channelUID, command);
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,
250 if (handleResponse(bridgeLockStateResponse, channelUID.getAsString(), command.toString())) {
251 if (!doHandleRefreshCommand(channelUID, command, bridgeLockStateResponse)) {
252 logger.debug("Command[{}] for channelUID[{}] not implemented!", command, channelUID);
258 scheduler.execute(() -> {
259 if (!doHandleCommand(channelUID, command)) {
260 logger.debug("Unexpected command[{}] for channelUID[{}]!", command, channelUID);
267 * Get type of this device
269 * @return Device type
271 protected abstract int getDeviceType();
274 * Get class of configuration
276 * @return Configuration class
278 protected abstract Class<T> getConfigurationClass();
281 * Method to handle channel command - will not receive REFRESH command
283 * @param channelUID Channel which received command
284 * @param command Command received
285 * @return true if command was handled
287 protected abstract boolean doHandleCommand(ChannelUID channelUID, Command command);
290 * Method for handlign {@link RefreshType} command
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
297 protected boolean doHandleRefreshCommand(ChannelUID channelUID, Command command, BridgeLockStateResponse response) {
298 refreshState(response.getBridgeApiLockStateDto());
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);
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()) {
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());
316 logger.debug("Could not handle command[{}] for channelUID[{}] on nukiId[{}]!", command, channelUID,
317 configuration.nukiId);
319 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, nukiBaseResponse.getMessage());
321 updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_LOCK, OnOffType.OFF, Function.identity());
322 updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_STATE, NukiBindingConstants.LOCK_STATES_UNDEFINED,
324 updateState(NukiBindingConstants.CHANNEL_SMARTLOCK_DOOR_STATE, NukiBindingConstants.DOORSENSOR_STATES_UNKNOWN,
327 withBridgeAsync(bridge -> {
328 bridge.checkBridgeOnline();
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) {
339 scheduler.execute(() -> handler.accept((NukiBridgeHandler) bridgeHandler));
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);
351 reInitJob = scheduler.scheduleWithFixedDelay(this::initializeHandler, 1, JOB_INTERVAL, TimeUnit.SECONDS);
354 private void stopReInitJob() {
355 logger.trace("Stopping reInitJob for Smart Lock[{}].", configuration.nukiId);
356 ScheduledFuture<?> job = reInitJob;
359 logger.trace("Stopped reInitJob for Smart Lock[{}].", configuration.nukiId);