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.List;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeoutException;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.http.HttpStatus;
27 import org.openhab.binding.verisure.internal.VerisureSession;
28 import org.openhab.binding.verisure.internal.dto.VerisureSmartLockDTO;
29 import org.openhab.binding.verisure.internal.dto.VerisureSmartLockDTO.DoorLockVolumeSettings;
30 import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO;
31 import org.openhab.binding.verisure.internal.dto.VerisureSmartLocksDTO.Doorlock;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.thing.Channel;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.openhab.core.types.State;
42 import org.openhab.core.types.UnDefType;
45 * Handler for the Smart Lock Device thing type that Verisure provides.
47 * @author Jan Gustafsson - Initial contribution
51 public class VerisureSmartLockThingHandler extends VerisureThingHandler<VerisureSmartLocksDTO> {
53 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_SMARTLOCK);
55 private static final int REFRESH_DELAY_SECONDS = 10;
57 public VerisureSmartLockThingHandler(Thing thing) {
62 public void handleCommand(ChannelUID channelUID, Command command) {
63 logger.debug("handleCommand, channel: {}, command: {}", channelUID, command);
64 if (command instanceof RefreshType) {
65 super.handleCommand(channelUID, command);
67 } else if (channelUID.getId().equals(CHANNEL_SMARTLOCK_STATUS)) {
68 handleSmartLockState(command);
69 } else if (channelUID.getId().equals(CHANNEL_AUTO_RELOCK)) {
70 handleAutoRelock(command);
71 } else if (channelUID.getId().equals(CHANNEL_SMARTLOCK_VOLUME)) {
72 handleSmartLockVolumeAndVoiceLevel(command, true);
73 } else if (channelUID.getId().equals(CHANNEL_SMARTLOCK_VOICE_LEVEL)) {
74 handleSmartLockVolumeAndVoiceLevel(command, false);
76 logger.warn("Unknown command! {}", command);
79 scheduleImmediateRefresh(REFRESH_DELAY_SECONDS);
82 private void handleSmartLockState(Command command) {
83 String deviceId = config.getDeviceId();
84 VerisureSession session = getSession();
85 if (session != null) {
86 VerisureSmartLocksDTO smartLock = session.getVerisureThing(deviceId, getVerisureThingClass());
87 if (smartLock != null) {
88 BigDecimal installationId = smartLock.getSiteId();
89 String pinCode = session.getPinCode(installationId);
90 if (pinCode != null) {
91 String url = START_GRAPHQL;
93 if (command == OnOffType.OFF) {
94 operation = "DoorUnlock";
95 } else if (command == OnOffType.ON) {
96 operation = "DoorLock";
98 logger.debug("Unknown command! {}", command);
102 ArrayList<SmartLockDTO> list = new ArrayList<>();
103 SmartLockDTO smartLockJSON = new SmartLockDTO();
104 VariablesDTO variables = new VariablesDTO();
105 InputDTO input = new InputDTO();
107 variables.setDeviceLabel(deviceId);
108 variables.setGiid(installationId.toString());
109 input.setCode(pinCode);
110 variables.setInput(input);
111 smartLockJSON.setOperationName(operation);
112 smartLockJSON.setVariables(variables);
113 String query = "mutation " + operation
114 + "($giid: String!, $deviceLabel: String!, $input: LockDoorInput!) {\n " + operation
115 + "(giid: $giid, deviceLabel: $deviceLabel, input: $input)\n}\n";
116 smartLockJSON.setQuery(query);
117 list.add(smartLockJSON);
119 String queryQLSmartLockSetState = gson.toJson(list);
120 logger.debug("Trying to set SmartLock state to {} with URL {} and data {}", operation, url,
121 queryQLSmartLockSetState);
123 int httpResultCode = session.sendCommand(url, queryQLSmartLockSetState, installationId);
124 if (httpResultCode == HttpStatus.OK_200) {
125 logger.debug("SmartLock status successfully changed!");
127 logger.warn("Failed to send command, HTTP result code {}", httpResultCode);
130 logger.warn("PIN code is not configured! It is mandatory to control SmartLock!");
136 private void handeAutoRelockResult(String url, String data, BigDecimal installationId, Command command) {
137 logger.debug("Trying to set Auto Relock {} with URL {} and data {}", command.toString(), url, data);
138 VerisureSession session = getSession();
139 if (session != null) {
140 int httpResultCode = session.sendCommand(url, data, installationId);
141 if (httpResultCode == HttpStatus.OK_200) {
142 logger.debug("AutoRelock successfully changed to {}", command.toString());
144 logger.warn("Failed to send command, HTTP result code {}", httpResultCode);
149 private void handleAutoRelock(Command command) {
150 String deviceId = config.getDeviceId();
151 VerisureSession session = getSession();
152 if (session != null) {
153 VerisureSmartLocksDTO smartLock = session.getVerisureThing(deviceId, getVerisureThingClass());
154 if (smartLock != null) {
155 BigDecimal installationId = smartLock.getSiteId();
157 String csrf = session.getCsrfToken(installationId);
158 StringBuilder sb = new StringBuilder(deviceId);
161 String url = SMARTLOCK_AUTORELOCK_COMMAND;
162 if (command == OnOffType.ON) {
163 data = "enabledDoorLocks=" + sb.toString()
164 + "&doorLockDevices%5B0%5D.autoRelockEnabled=true&_doorLockDevices%5B0%5D.autoRelockEnabled=on&_csrf="
166 handeAutoRelockResult(url, data, installationId, command);
167 } else if (command == OnOffType.OFF) {
168 data = "enabledDoorLocks=&doorLockDevices%5B0%5D.autoRelockEnabled=true&_doorLockDevices%5B0%5D.autoRelockEnabled=on&_csrf="
170 handeAutoRelockResult(url, data, installationId, command);
172 logger.warn("Unknown command! {}", command);
174 } catch (ExecutionException | InterruptedException | TimeoutException e) {
175 logger.debug("Failed to handle auto-relock {}", e.getMessage());
181 private boolean isSettingAllowed(List<String> allowedSettings, String setting) {
182 return allowedSettings.contains(setting);
185 private void handleSmartLockVolumeAndVoiceLevel(Command command, boolean setVolume) {
186 String deviceId = config.getDeviceId();
187 VerisureSession session = getSession();
188 if (session != null) {
189 VerisureSmartLocksDTO smartLocks = session.getVerisureThing(deviceId, getVerisureThingClass());
190 if (smartLocks != null) {
191 VerisureSmartLockDTO smartLock = smartLocks.getSmartLockJSON();
192 if (smartLock != null) {
193 DoorLockVolumeSettings volumeSettings = smartLock.getDoorLockVolumeSettings();
197 List<String> availableVolumes = volumeSettings.getAvailableVolumes();
198 if (isSettingAllowed(availableVolumes, command.toString())) {
199 volume = command.toString();
200 voiceLevel = volumeSettings.getVoiceLevel();
202 logger.warn("Failed to change volume, setting not allowed {}", command.toString());
206 List<String> availableVoiceLevels = volumeSettings.getAvailableVoiceLevels();
207 if (isSettingAllowed(availableVoiceLevels, command.toString())) {
208 volume = volumeSettings.getVolume();
209 voiceLevel = command.toString();
211 logger.warn("Failed to change voice level, setting not allowed {}", command.toString());
214 BigDecimal installationId = smartLocks.getSiteId();
216 String csrf = session.getCsrfToken(installationId);
217 String url = SMARTLOCK_VOLUME_COMMAND;
218 String data = "keypad.volume=MEDIUM&keypad.beepOnKeypress=true&_keypad.beepOnKeypress=on&siren.volume=MEDIUM&voiceDevice.volume=MEDIUM&doorLock.volume="
219 + volume + "&doorLock.voiceLevel=" + voiceLevel
220 + "&_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="
222 logger.debug("Trying to set SmartLock volume with URL {} and data {}", url, data);
223 int httpResultCode = session.sendCommand(url, data, installationId);
224 if (httpResultCode == HttpStatus.OK_200) {
225 logger.debug("SmartLock volume successfully changed!");
227 logger.warn("Failed to send command, HTTP result code {}", httpResultCode);
229 } catch (ExecutionException | InterruptedException | TimeoutException e) {
230 logger.warn("Failed to get CSRF token {}", e.getMessage());
239 public Class<VerisureSmartLocksDTO> getVerisureThingClass() {
240 return VerisureSmartLocksDTO.class;
244 public synchronized void update(VerisureSmartLocksDTO thing) {
245 updateSmartLockState(thing);
246 updateStatus(ThingStatus.ONLINE);
249 private void updateSmartLockState(VerisureSmartLocksDTO smartLocksJSON) {
250 List<Doorlock> doorLockList = smartLocksJSON.getData().getInstallation().getDoorlocks();
251 if (!doorLockList.isEmpty()) {
252 Doorlock doorlock = doorLockList.get(0);
253 String smartLockStatus = doorlock.getCurrentLockState();
254 VerisureSmartLockDTO smartLockJSON = smartLocksJSON.getSmartLockJSON();
255 if (smartLockStatus != null) {
256 getThing().getChannels().stream().map(Channel::getUID)
257 .filter(channelUID -> isLinked(channelUID) && !"timestamp".equals(channelUID.getId()))
258 .forEach(channelUID -> {
259 State state = getValue(channelUID.getId(), doorlock, smartLockStatus, smartLockJSON);
260 updateState(channelUID, state);
262 updateTimeStamp(doorlock.getEventTime());
263 updateInstallationChannels(smartLocksJSON);
265 logger.debug("Smart lock status {} or smartLockJSON {} is null!", smartLockStatus, smartLockJSON);
268 logger.debug("DoorLock list is empty!");
272 public State getValue(String channelId, Doorlock doorlock, String smartLockStatus,
273 @Nullable VerisureSmartLockDTO smartLockJSON) {
275 case CHANNEL_SMARTLOCK_STATUS:
276 if ("LOCKED".equals(smartLockStatus)) {
278 } else if ("UNLOCKED".equals(smartLockStatus)) {
279 return OnOffType.OFF;
280 } else if ("PENDING".equals(smartLockStatus)) {
281 // Schedule another refresh.
282 logger.debug("Issuing another immediate refresh since status is still PENDING ...");
283 this.scheduleImmediateRefresh(REFRESH_DELAY_SECONDS);
286 case CHANNEL_CHANGED_BY_USER:
287 String user = doorlock.getUserString();
288 return user != null ? new StringType(user) : UnDefType.NULL;
289 case CHANNEL_CHANGED_VIA:
290 String method = doorlock.getMethod();
291 return method != null ? new StringType(method) : UnDefType.NULL;
292 case CHANNEL_MOTOR_JAM:
293 return OnOffType.from(doorlock.isMotorJam());
294 case CHANNEL_LOCATION:
295 String location = doorlock.getDevice().getArea();
296 return location != null ? new StringType(location) : UnDefType.NULL;
297 case CHANNEL_AUTO_RELOCK:
298 if (smartLockJSON != null) {
299 return OnOffType.from(smartLockJSON.getAutoRelockEnabled());
301 return UnDefType.NULL;
303 case CHANNEL_SMARTLOCK_VOLUME:
304 if (smartLockJSON != null) {
305 return new StringType(smartLockJSON.getDoorLockVolumeSettings().getVolume());
307 return UnDefType.NULL;
309 case CHANNEL_SMARTLOCK_VOICE_LEVEL:
310 if (smartLockJSON != null) {
311 return new StringType(smartLockJSON.getDoorLockVolumeSettings().getVoiceLevel());
313 return UnDefType.NULL;
316 return UnDefType.UNDEF;
319 private static class SmartLockDTO {
321 @SuppressWarnings("unused")
322 private @Nullable String operationName;
323 @SuppressWarnings("unused")
324 private VariablesDTO variables = new VariablesDTO();
325 @SuppressWarnings("unused")
326 private @Nullable String query;
328 public void setOperationName(String operationName) {
329 this.operationName = operationName;
332 public void setVariables(VariablesDTO variables) {
333 this.variables = variables;
336 public void setQuery(String query) {
341 private static class VariablesDTO {
343 @SuppressWarnings("unused")
344 private @Nullable String giid;
345 @SuppressWarnings("unused")
346 private @Nullable String deviceLabel;
347 @SuppressWarnings("unused")
348 private InputDTO input = new InputDTO();
350 public void setGiid(String giid) {
354 public void setDeviceLabel(String deviceLabel) {
355 this.deviceLabel = deviceLabel;
358 public void setInput(InputDTO input) {
363 private static class InputDTO {
365 @SuppressWarnings("unused")
366 private @Nullable String code;
368 public void setCode(String code) {
374 public void updateTriggerChannel(String event) {
375 logger.debug("SmartLockThingHandler trigger event {}", event);
376 triggerChannel(CHANNEL_SMARTLOCK_TRIGGER_CHANNEL, event);