/** * Copyright (c) 2010-2015, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.zwave.internal.protocol.commandclass; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.openhab.binding.zwave.internal.protocol.SerialMessage; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageClass; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessagePriority; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageType; import org.openhab.binding.zwave.internal.protocol.ZWaveController; import org.openhab.binding.zwave.internal.protocol.ZWaveEndpoint; import org.openhab.binding.zwave.internal.protocol.ZWaveNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.thoughtworks.xstream.annotations.XStreamAlias; /** * Handles the UserCode command class. * @author Dave Badia * @since TODO: DB set version once done */ @XStreamAlias("userCodeCommandClass") public class ZWaveUserCodeCommandClass extends ZWaveCommandClass implements ZWaveCommandClassDynamicState { private static final Logger logger = LoggerFactory.getLogger(ZWaveUserCodeCommandClass.class); public static final int USER_CODE_MIN_LENGTH = 4; public static final int USER_CODE_MAX_LENGTH = 10; private static final int USER_CODE_SET = 0x01; private static final int USER_CODE_GET = 0x02; private static final int USER_CODE_REPORT = 0x03; /** * Request the number of user codes that can be stored */ private static final int USER_NUMBER_GET = 0x04; private static final int USER_NUMBER_REPORT = 0x05; private static final int UNKNOWN = -1; /** * The total number of users that the device supports as * determined by {@link #USER_NUMBER_REPORT} */ private int numberOfUsersSupported = UNKNOWN; private List<UserCode> userCodeList = new ArrayList<UserCode>(); /** * Creates a new instance of the ZWaveDoorLockCommandClass class. * @param node the node this command class belongs to * @param controller the controller to use * @param endpoint the endpoint this Command class belongs to */ public ZWaveUserCodeCommandClass(ZWaveNode node, ZWaveController controller, ZWaveEndpoint endpoint) { super(node, controller, endpoint); } /** * {@inheritDoc} */ @Override public CommandClass getCommandClass() { return CommandClass.USER_CODE; } @Override public Collection<SerialMessage> getDynamicValues(boolean refresh) { logger.debug("NODE {}: User Code initialize starting, refresh={}", getNode().getNodeId()); Collection<SerialMessage> result = new ArrayList<SerialMessage>(); if(refresh == true) { if(numberOfUsersSupported == ZWaveUserCodeCommandClass.UNKNOWN) { // Request it and wait for response logger.debug("NODE {}: numberOfUsersSupported=-1, refreshing}", getNode().getNodeId()); SerialMessage message = buildGetMessage(ZWaveUserCodeCommandClass.USER_NUMBER_GET); result.add(message); // when we receive USER_NUMBER_REPORT it will kickoff syncUserCodes(); } else if(numberOfUsersSupported == 0) { logger.error("NODE {}: door lock does not support any user codes, no codes set", getNode().getNodeId()); } else { syncUserCodes(); } } return result; } /** * {@inheritDoc} */ @Override public void handleApplicationCommandRequest(SerialMessage serialMessage, int offset, int endpoint) { logger.debug(String.format("NODE %d: Received UserCode Request", this.getNode().getNodeId())); final int command = serialMessage.getMessagePayloadByte(offset); switch (command) { case USER_NUMBER_REPORT: numberOfUsersSupported = serialMessage.getMessagePayloadByte(offset + 1); logger.info(String.format("NODE %d: ZWaveUserCodeCommandClass numberOfUsersSupported=%d", getNode().getNodeId(), numberOfUsersSupported)); syncUserCodes(); break; case USER_CODE_REPORT: logger.info(String.format("NODE %d: USER_CODE_REPORT %s", getNode().getNodeId(), SerialMessage.bb2hex(serialMessage.getMessagePayload()))); // TODO: confirm the data was set properly break; case USER_CODE_GET: case USER_CODE_SET: case USER_NUMBER_GET: default: logger.warn(String.format("NODE %d: Unsupported Command 0x%02X for command class %s (0x%02X): %s", getNode().getNodeId(), command, this.getCommandClass().getLabel(), this.getCommandClass().getKey(), SerialMessage.bb2hex(serialMessage.getMessagePayload()))); break; } } public SerialMessage buildGetMessage(int type, Integer... argBytes) { if(type != USER_NUMBER_GET && type != USER_CODE_GET) { logger.error("NODE {}: Called buildGetMessage with invalid type of {}", this.getNode().getNodeId(), (0xff & type)); return null; } logger.debug("NODE {}: Creating new message for application command USER_CODE {}", this.getNode().getNodeId(), (0xff & type)); SerialMessage message = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData, SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler, SerialMessagePriority.Get); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write((byte) this.getNode().getNodeId()); baos.write(2 + argBytes.length); baos.write((byte) getCommandClass().getKey()); baos.write((byte) (type & 0xff)); for(Integer aArgByte : argBytes) { baos.write((byte) (0xff & aArgByte)); } message.setMessagePayload(baos.toByteArray()); return message; } private void syncUserCodes() { // Sanity check if(numberOfUsersSupported == 0) { logger.error("NODE {}: does not support any user codes; no codes set", getNode().getNodeId()); return; } if(userCodeList.size() > numberOfUsersSupported) { logger.error("NODE {}: too many user codes given, device only supports {}; no codes set", getNode().getNodeId(), numberOfUsersSupported); return; } for (int i = 0; i < userCodeList.size(); i++) { UserCode userCode = userCodeList.get(i); // Take all codes from the serialize xml and send them to the lock String code = userCode.code; // zeros means delete the code boolean codeIsZeros = Integer.parseInt(code) == 0; // no NumberFormatException since we called userCodeIsValid if(codeIsZeros) { code = ""; // send no code since we will set UserIdStatusType.AVAILBLE } if(codeIsZeros || userCodeIsValid(userCode, getNode().getNodeId())) { logger.debug("NODE {}: {} user code for {}", this.getNode().getNodeId(), codeIsZeros ? "Removing" : "Sending", userCode.friendlyName); SerialMessage message = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData, SerialMessageType.Request, SerialMessageClass.SendData, SerialMessagePriority.Get); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write((byte) this.getNode().getNodeId()); baos.write(4 + code.length()); baos.write((byte) getCommandClass().getKey()); baos.write(USER_CODE_SET); baos.write(i + 1); // identifier, must be 1 or higher if(codeIsZeros) { baos.write(UserIdStatusType.AVAILBLE.key); // status } else { baos.write(UserIdStatusType.OCCUPIED.key); // status try { for(Byte aCodeDigit : code.getBytes("UTF-8")) { baos.write(aCodeDigit); } } catch (UnsupportedEncodingException e) { logger.error("Got UnsupportedEncodingException", e); } } message.setMessagePayload(baos.toByteArray()); getNode().getController().enqueue(message); } } } public static boolean userCodeIsValid(UserCode userCode, Integer nodeId) { // We ignore userCode.name since it is just a friendly name // Check length of userCode.code if(userCode.code.length() < USER_CODE_MIN_LENGTH || userCode.code.length() > USER_CODE_MAX_LENGTH) { logger.error("NODE {}: Ignoring user code {}: was {} digits but must be between {} and {}", nodeId, userCode.code, userCode.code.length(), USER_CODE_MIN_LENGTH, USER_CODE_MAX_LENGTH); return false; } // Check that userCode.code is numeric for(char c : userCode.code.toCharArray()) { if(!Character.isDigit(c)) { logger.error("NODE {}: Ignoring user code {}: found non-digit of '{}' in code", nodeId, userCode.code, c); return false; } } return true; } @XStreamAlias("userCode") public static class UserCode { /** * {@link #friendlyName} is never sent to the device */ private final String friendlyName; private final String code; public UserCode(String friendlyName, String code) { this.friendlyName = friendlyName; this.code = code; } public String getCode() { return code; } public String getFriendlyName() { return friendlyName; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("UserCode [user=").append(friendlyName).append(", code=") .append(code).append("]"); return builder.toString(); } } /** * Z-Wave UserIDStatus enumeration. The user ID status type indicates * the state of the user ID. * * @see {@link ZWaveUserCodeCommandClass#USER_CODE_GET} * @see {@link ZWaveUserCodeCommandClass#USER_CODE_SET} */ @XStreamAlias("userIdStatusType") static enum UserIdStatusType { AVAILBLE(0x00, "Available (not set)"), OCCUPIED(0x01, "Occupied"), RESERVED_BY_ADMINISTRATOR(0x02, "Reserved by administrator"), STATUS_NOT_AVAILABLE(0x11, "Status not available"), ; /** * A mapping between the integer code and its corresponding door * lock state type to facilitate lookup by code. */ private static Map<Integer, UserIdStatusType> codeToUserIdStatusTypeMapping; private int key; private String label; private static void initMapping() { codeToUserIdStatusTypeMapping = new ConcurrentHashMap<Integer, UserIdStatusType>(); for (UserIdStatusType d : values()) { codeToUserIdStatusTypeMapping.put(d.key, d); } } /** * Lookup function based on the user id status type code. * Returns null if the code does not exist. * @param i the code to lookup * @return enumeration value of the user id status type. */ public static UserIdStatusType getDoorLockStateType(int i) { if (codeToUserIdStatusTypeMapping == null) { initMapping(); } return codeToUserIdStatusTypeMapping.get(i); } UserIdStatusType(int key, String label) { this.key = key; this.label = label; } /** * @return the key */ public int getKey() { return key; } /** * @return the label */ public String getLabel() { return label; } } }