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.verisure.internal.handler;
15 import static org.openhab.binding.verisure.internal.VerisureBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.List;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.TimeoutException;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.http.HttpStatus;
28 import org.openhab.binding.verisure.internal.VerisureSession;
29 import org.openhab.binding.verisure.internal.dto.VerisureSmartLockDTO;
30 import org.openhab.binding.verisure.internal.dto.VerisureSmartLockDTO.DoorLockVolumeSettings;
31 import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO;
32 import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO.Doorlock;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.Channel;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.State;
43 import org.openhab.core.types.UnDefType;
46 * Handler for the Smart Lock Device thing type that Verisure provides.
48 * @author Jan Gustafsson - Initial contribution
52 public class VerisureSmartLockThingHandler extends VerisureThingHandler<VerisureSmartLocksDTO> {
54 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_SMARTLOCK);
56 private static final int REFRESH_DELAY_SECONDS = 10;
58 public VerisureSmartLockThingHandler(Thing thing) {
63 public void handleCommand(ChannelUID channelUID, Command command) {
64 logger.debug("handleCommand, channel: {}, command: {}", channelUID, command);
65 if (command instanceof RefreshType) {
66 super.handleCommand(channelUID, command);
68 } else if (channelUID.getId().equals(CHANNEL_SMARTLOCK_STATUS)) {
69 handleSmartLockState(command);
70 } else if (channelUID.getId().equals(CHANNEL_AUTO_RELOCK)) {
71 handleAutoRelock(command);
72 } else if (channelUID.getId().equals(CHANNEL_SMARTLOCK_VOLUME)) {
73 handleSmartLockVolumeAndVoiceLevel(command, true);
74 } else if (channelUID.getId().equals(CHANNEL_SMARTLOCK_VOICE_LEVEL)) {
75 handleSmartLockVolumeAndVoiceLevel(command, false);
77 logger.warn("Unknown command! {}", command);
80 scheduleImmediateRefresh(REFRESH_DELAY_SECONDS);
83 private void handleSmartLockState(Command command) {
84 String deviceId = config.getDeviceId();
85 VerisureSession session = getSession();
86 if (session != null) {
87 VerisureSmartLocksDTO smartLock = session.getVerisureThing(deviceId, getVerisureThingClass());
88 if (smartLock != null) {
89 BigDecimal installationId = smartLock.getSiteId();
90 String pinCode = session.getPinCode(installationId);
91 if (pinCode != null) {
92 String url = START_GRAPHQL;
94 if (command == OnOffType.OFF) {
95 operation = "DoorUnlock";
96 } else if (command == OnOffType.ON) {
97 operation = "DoorLock";
99 logger.debug("Unknown command! {}", command);
103 ArrayList<SmartLockDTO> list = new ArrayList<>();
104 SmartLockDTO smartLockJSON = new SmartLockDTO();
105 VariablesDTO variables = new VariablesDTO();
106 InputDTO input = new InputDTO();
108 variables.setDeviceLabel(deviceId);
109 variables.setGiid(installationId.toString());
110 input.setCode(pinCode);
111 variables.setInput(input);
112 smartLockJSON.setOperationName(operation);
113 smartLockJSON.setVariables(variables);
114 String query = "mutation " + operation
115 + "($giid: String!, $deviceLabel: String!, $input: LockDoorInput!) {\n " + operation
116 + "(giid: $giid, deviceLabel: $deviceLabel, input: $input)\n}\n";
117 smartLockJSON.setQuery(query);
118 list.add(smartLockJSON);
120 String queryQLSmartLockSetState = gson.toJson(list);
121 logger.debug("Trying to set SmartLock state to {} with URL {} and data {}", operation, url,
122 queryQLSmartLockSetState);
124 int httpResultCode = session.sendCommand(url, queryQLSmartLockSetState, installationId);
125 if (httpResultCode == HttpStatus.OK_200) {
126 logger.debug("SmartLock status successfully changed!");
128 logger.warn("Failed to send command, HTTP result code {}", httpResultCode);
131 logger.warn("PIN code is not configured! It is mandatory to control SmartLock!");
137 private void handeAutoRelockResult(String url, String data, BigDecimal installationId, Command command) {
138 logger.debug("Trying to set Auto Relock {} with URL {} and data {}", command.toString(), url, data);
139 VerisureSession session = getSession();
140 if (session != null) {
141 int httpResultCode = session.sendCommand(url, data, installationId);
142 if (httpResultCode == HttpStatus.OK_200) {
143 logger.debug("AutoRelock successfully changed to {}", command.toString());
145 logger.warn("Failed to send command, HTTP result code {}", httpResultCode);
150 private void handleAutoRelock(Command command) {
151 String deviceId = config.getDeviceId();
152 VerisureSession session = getSession();
153 if (session != null) {
154 VerisureSmartLocksDTO smartLock = session.getVerisureThing(deviceId, getVerisureThingClass());
155 if (smartLock != null) {
156 BigDecimal installationId = smartLock.getSiteId();
158 String csrf = session.getCsrfToken(installationId);
159 StringBuilder sb = new StringBuilder(deviceId);
162 String url = SMARTLOCK_AUTORELOCK_COMMAND;
163 if (command == OnOffType.ON) {
164 data = "enabledDoorLocks=" + sb.toString()
165 + "&doorLockDevices%5B0%5D.autoRelockEnabled=true&_doorLockDevices%5B0%5D.autoRelockEnabled=on&_csrf="
167 handeAutoRelockResult(url, data, installationId, command);
168 } else if (command == OnOffType.OFF) {
169 data = "enabledDoorLocks=&doorLockDevices%5B0%5D.autoRelockEnabled=true&_doorLockDevices%5B0%5D.autoRelockEnabled=on&_csrf="
171 handeAutoRelockResult(url, data, installationId, command);
173 logger.warn("Unknown command! {}", command);
175 } catch (ExecutionException | InterruptedException | TimeoutException e) {
176 logger.debug("Failed to handle auto-relock {}", e.getMessage());
182 private boolean isSettingAllowed(List<String> allowedSettings, String setting) {
183 return allowedSettings.contains(setting);
186 private void handleSmartLockVolumeAndVoiceLevel(Command command, boolean setVolume) {
187 String deviceId = config.getDeviceId();
188 VerisureSession session = getSession();
189 if (session != null) {
190 VerisureSmartLocksDTO smartLocks = session.getVerisureThing(deviceId, getVerisureThingClass());
191 if (smartLocks != null) {
192 VerisureSmartLockDTO smartLock = smartLocks.getSmartLockJSON();
193 if (smartLock != null) {
194 DoorLockVolumeSettings volumeSettings = smartLock.getDoorLockVolumeSettings();
198 List<String> availableVolumes = volumeSettings.getAvailableVolumes();
199 if (isSettingAllowed(availableVolumes, command.toString())) {
200 volume = command.toString();
201 voiceLevel = volumeSettings.getVoiceLevel();
203 logger.warn("Failed to change volume, setting not allowed {}", command.toString());
207 List<String> availableVoiceLevels = volumeSettings.getAvailableVoiceLevels();
208 if (isSettingAllowed(availableVoiceLevels, command.toString())) {
209 volume = volumeSettings.getVolume();
210 voiceLevel = command.toString();
212 logger.warn("Failed to change voice level, setting not allowed {}", command.toString());
215 BigDecimal installationId = smartLocks.getSiteId();
217 String csrf = session.getCsrfToken(installationId);
218 String url = SMARTLOCK_VOLUME_COMMAND;
219 String data = "keypad.volume=MEDIUM&keypad.beepOnKeypress=true&_keypad.beepOnKeypress=on&siren.volume=MEDIUM&voiceDevice.volume=MEDIUM&doorLock.volume="
220 + volume + "&doorLock.voiceLevel=" + voiceLevel
221 + "&_devices%5B0%5D.on=on&devices%5B1%5D.on=true&_devices%5B1%5D.on=on&devices%5B2%5D.on=true&_devices%5B2%5D.on=on&_devices%5B3%5D.on=on&_keypad.keypadsPlayChime=on&_siren.sirensPlayChime=on&_csrf="
223 logger.debug("Trying to set SmartLock volume with URL {} and data {}", url, data);
224 int httpResultCode = session.sendCommand(url, data, installationId);
225 if (httpResultCode == HttpStatus.OK_200) {
226 logger.debug("SmartLock volume successfully changed!");
228 logger.warn("Failed to send command, HTTP result code {}", httpResultCode);
230 } catch (ExecutionException | InterruptedException | TimeoutException e) {
231 logger.warn("Failed to get CSRF token {}", e.getMessage());
240 public Class<VerisureSmartLocksDTO> getVerisureThingClass() {
241 return VerisureSmartLocksDTO.class;
245 public synchronized void update(VerisureSmartLocksDTO thing) {
246 updateSmartLockState(thing);
247 updateStatus(ThingStatus.ONLINE);
250 private void updateSmartLockState(VerisureSmartLocksDTO smartLocksJSON) {
251 List<Doorlock> doorLockList = smartLocksJSON.getData().getInstallation().getDoorlocks();
252 if (!doorLockList.isEmpty()) {
253 Doorlock doorlock = doorLockList.get(0);
254 String smartLockStatus = doorlock.getCurrentLockState();
255 VerisureSmartLockDTO smartLockJSON = smartLocksJSON.getSmartLockJSON();
256 if (smartLockStatus != null) {
257 getThing().getChannels().stream().map(Channel::getUID)
258 .filter(channelUID -> isLinked(channelUID) && !channelUID.getId().equals("timestamp"))
259 .forEach(channelUID -> {
260 State state = getValue(channelUID.getId(), doorlock, smartLockStatus, smartLockJSON);
261 updateState(channelUID, state);
263 updateTimeStamp(doorlock.getEventTime());
264 updateInstallationChannels(smartLocksJSON);
266 logger.debug("Smart lock status {} or smartLockJSON {} is null!", smartLockStatus, smartLockJSON);
269 logger.debug("DoorLock list is empty!");
273 public State getValue(String channelId, Doorlock doorlock, String smartLockStatus,
274 @Nullable VerisureSmartLockDTO smartLockJSON) {
276 case CHANNEL_SMARTLOCK_STATUS:
277 if ("LOCKED".equals(smartLockStatus)) {
279 } else if ("UNLOCKED".equals(smartLockStatus)) {
280 return OnOffType.OFF;
281 } else if ("PENDING".equals(smartLockStatus)) {
282 // Schedule another refresh.
283 logger.debug("Issuing another immediate refresh since status is still PENDING ...");
284 this.scheduleImmediateRefresh(REFRESH_DELAY_SECONDS);
287 case CHANNEL_CHANGED_BY_USER:
288 String user = doorlock.getUserString();
289 return user != null ? new StringType(user) : UnDefType.NULL;
290 case CHANNEL_CHANGED_VIA:
291 String method = doorlock.getMethod();
292 return method != null ? new StringType(method) : UnDefType.NULL;
293 case CHANNEL_MOTOR_JAM:
294 return OnOffType.from(doorlock.isMotorJam());
295 case CHANNEL_LOCATION:
296 String location = doorlock.getDevice().getArea();
297 return location != null ? new StringType(location) : UnDefType.NULL;
298 case CHANNEL_AUTO_RELOCK:
299 if (smartLockJSON != null) {
300 return OnOffType.from(smartLockJSON.getAutoRelockEnabled());
302 return UnDefType.NULL;
304 case CHANNEL_SMARTLOCK_VOLUME:
305 if (smartLockJSON != null) {
306 return new StringType(smartLockJSON.getDoorLockVolumeSettings().getVolume());
308 return UnDefType.NULL;
310 case CHANNEL_SMARTLOCK_VOICE_LEVEL:
311 if (smartLockJSON != null) {
312 return new StringType(smartLockJSON.getDoorLockVolumeSettings().getVoiceLevel());
314 return UnDefType.NULL;
317 return UnDefType.UNDEF;
320 private static class SmartLockDTO {
322 @SuppressWarnings("unused")
323 private @Nullable String operationName;
324 @SuppressWarnings("unused")
325 private VariablesDTO variables = new VariablesDTO();
326 @SuppressWarnings("unused")
327 private @Nullable String query;
329 public void setOperationName(String operationName) {
330 this.operationName = operationName;
333 public void setVariables(VariablesDTO variables) {
334 this.variables = variables;
337 public void setQuery(String query) {
342 private static class VariablesDTO {
344 @SuppressWarnings("unused")
345 private @Nullable String giid;
346 @SuppressWarnings("unused")
347 private @Nullable String deviceLabel;
348 @SuppressWarnings("unused")
349 private InputDTO input = new InputDTO();
351 public void setGiid(String giid) {
355 public void setDeviceLabel(String deviceLabel) {
356 this.deviceLabel = deviceLabel;
359 public void setInput(InputDTO input) {
364 private static class InputDTO {
366 @SuppressWarnings("unused")
367 private @Nullable String code;
369 public void setCode(String code) {
375 public void updateTriggerChannel(String event) {
376 logger.debug("SmartLockThingHandler trigger event {}", event);
377 triggerChannel(CHANNEL_SMARTLOCK_TRIGGER_CHANNEL, event);