/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* 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.nikobus.internal.config;
import java.math.BigDecimal;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.nikobus.internal.NikobusBinding;
import org.openhab.binding.nikobus.internal.core.NikobusCommand;
import org.openhab.binding.nikobus.internal.core.NikobusModule;
import org.openhab.binding.nikobus.internal.util.CRCUtil;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Group of 6 channels in a Nikobus switch or dimmer module. This can be used to
* represent either channels 0-4 for the compact switch module (05-002-02), or
* channels 1-6 or 7-12 for the large switch module (05-000-02).
*
* Example commands used by Nikobus for module with address 6C94. <br/>
* <br/>
* Request Status First Channel Group: <br/>
* CMD : $10126C946CE5A0. <br/>
* ACK : $0512 <br/>
* REPLY: $1C6C9400000000FF0000557CF8.<br/>
*
* <br/>
* Request Status Second Channel Group: <br/>
* CMD : $10176C948715BB. <br/>
* ACK : $0517 <br/>
* REPLY: $1C6C94000000FF0000FFCF4CC3. <br/>
* <br/>
* <br/>
* Update Status First Channel Group: <br/>
* CMD : $1E156C94000000FF0000FF60E149. <br/>
* ACK : $0515 <br/>
* REPLY: $0EFF6C94009A. <br/>
* <br/>
* Update Status Second Channel Group: <br/>
* CMD : $1E166C940000000000FFFF997295. <br/>
* ACK : $0516 <br/>
* REPLY: $0EFF6C94009A.
*
* @author Davy Vanherbergen
* @since 1.3.0
*/
public class ModuleChannelGroup implements NikobusModule {
public static final String STATUS_REQUEST_CMD = "$10";
public static final String STATUS_REQUEST_ACK = "$05";
public static final String STATUS_REQUEST_GROUP_1 = "12";
public static final String STATUS_REQUEST_GROUP_2 = "17";
public static final String STATUS_RESPONSE = "$1C";
public static final String STATUS_CHANGE_CMD = "$1E";
public static final String STATUS_CHANGE_ACK = "$05";
public static final String STATUS_CHANGE_GROUP_1 = "15";
public static final String STATUS_CHANGE_GROUP_2 = "16";
public static final String HIGH_BYTE = "FF";
public static final String LOW_BYTE = "00";
public static final String UP_BYTE = "01";
public static final String DOWN_BYTE = "02";
private Boolean nextStatusResponseIsForThisGroup;
private String statusRequestGroup;
private String statusUpdateGroup;
private ModuleChannel[] channels = new ModuleChannel[6];
private long lastUpdatedTime;
private String address;
private int group = 1;
private static Logger log = LoggerFactory.getLogger(ModuleChannelGroup.class);
/**
* Default constructor.
*
* @param address
* Nikobus module address.
* @param group
* 1 or 2 indicating channels 1-6 or 7-12 respectively
*
* @param bindingProvider
*/
public ModuleChannelGroup(String address, int group) {
this.address = address;
this.group = group;
if (group == 1) {
statusRequestGroup = STATUS_REQUEST_GROUP_1;
statusUpdateGroup = STATUS_CHANGE_GROUP_1;
} else {
statusRequestGroup = STATUS_REQUEST_GROUP_2;
statusUpdateGroup = STATUS_CHANGE_GROUP_2;
}
}
/**
* Add a new item channel to the switch module.
*
* @param name
* channel name
* @param channelNum
* number 1-12
* @return SwitchModuleChannel bound to the given number.
*/
public ModuleChannel addChannel(String name, int channelNum, List<Class<? extends Command>> supportedCommands) {
log.trace("Adding channel {}", name);
if (channelNum > 6) {
channelNum -= 6;
}
if (channelNum < 1 || channelNum > 6) {
return null;
}
channels[channelNum - 1] = new ModuleChannel(name, address, this, supportedCommands);
return channels[channelNum - 1];
}
/**
* Push the state of all channels to the Nikobus.
*
* @param moduleChannel
*/
public void publishStateToNikobus(ModuleChannel moduleChannel, NikobusBinding binding) {
log.trace("Publishing group {}-{} status to eventbus and nikobus", address, group);
// update the channel on the event bus..
binding.postUpdate(moduleChannel.getName(), moduleChannel.getState());
StringBuilder command = new StringBuilder();
command.append(statusUpdateGroup);
command.append(address);
for (int i = 0; i < 6; i++) {
if (channels[i] == null) {
// no channel defined
command.append(LOW_BYTE);
continue;
}
State channelState = channels[i].getState();
if (channelState == null || channelState.equals(OnOffType.OFF) || channelState.equals(PercentType.ZERO)) {
command.append(LOW_BYTE);
} else if (channelState.equals(UpDownType.UP)) {
command.append(UP_BYTE);
} else if (channelState.equals(UpDownType.DOWN)) {
command.append(DOWN_BYTE);
} else if (channelState instanceof PercentType) {
// calculate dimmer value...
PercentType currentState = (PercentType) channelState;
int value = BigDecimal.valueOf(255).multiply(currentState.toBigDecimal())
.divide(BigDecimal.valueOf(100), 0, BigDecimal.ROUND_UP).intValue();
command.append(StringUtils.leftPad(Integer.toHexString(value), 2, "0").toUpperCase());
} else {
command.append(HIGH_BYTE);
}
}
command.append(HIGH_BYTE);
NikobusCommand cmd = new NikobusCommand(
CRCUtil.appendCRC2(STATUS_CHANGE_CMD + CRCUtil.appendCRC(command.toString())));
try {
binding.sendCommand(cmd);
} catch (Exception e) {
log.error("Error sending command.", e);
}
}
/**
* {@inheritDoc}
*
* The channel group can only process status update commands. These commands
* are sent by the module in response to a status request and contain
* the ON/OFF/Dimming status of the different channels.
*/
@Override
public void processNikobusCommand(NikobusCommand cmd, NikobusBinding binding) {
String command = cmd.getCommand();
// check if it was an ACK for a status request
if (command.startsWith(STATUS_REQUEST_ACK)) {
if (command.startsWith(STATUS_REQUEST_ACK + statusRequestGroup)) {
nextStatusResponseIsForThisGroup = Boolean.TRUE;
} else {
nextStatusResponseIsForThisGroup = Boolean.FALSE;
}
return;
}
if (!command.startsWith(STATUS_RESPONSE)) {
return;
}
if (!command.startsWith(STATUS_RESPONSE + address) || nextStatusResponseIsForThisGroup == null
|| nextStatusResponseIsForThisGroup.equals(Boolean.FALSE)) {
nextStatusResponseIsForThisGroup = null;
return;
}
log.debug("Processing nikobus command {} for module ({}-{})",
new Object[] { cmd.getCommand(), address, Integer.toString(group) });
lastUpdatedTime = System.currentTimeMillis();
// for every channel, update the status if it was changed
for (int i = 0; i < 6; i++) {
if (channels[i] == null) {
continue;
}
State currentState = channels[i].getState();
String newValue = command.substring(9 + (i * 2), 11 + (i * 2));
if (channels[i].supportsPercentType()) {
PercentType value = getPercentTypeFromByteString(newValue);
if (!currentState.equals(value)) {
binding.postUpdate(channels[i].getName(), value);
channels[i].setState(value);
}
} else {
if (newValue.equals(LOW_BYTE)) {
if (channels[i].getState().equals(OnOffType.ON)) {
binding.postUpdate(channels[i].getName(), OnOffType.OFF);
channels[i].setState(OnOffType.OFF);
}
} else {
if (channels[i].getState().equals(OnOffType.OFF)) {
binding.postUpdate(channels[i].getName(), OnOffType.ON);
channels[i].setState(OnOffType.ON);
}
}
}
}
}
/**
* @return time when last status feedback from the switch module was
* received
*/
public long getLastUpdatedTime() {
return lastUpdatedTime;
}
@Override
public String getAddress() {
return address;
}
@Override
public NikobusCommand getStatusRequestCommand() {
return new NikobusCommand(
CRCUtil.appendCRC2(STATUS_REQUEST_CMD + CRCUtil.appendCRC(statusRequestGroup + address)),
STATUS_RESPONSE + address, 2000);
}
@Override
public String getName() {
return address + "-" + group;
}
/**
* Convert a 0-FF scale value to a percent type.
*/
private PercentType getPercentTypeFromByteString(String byteValue) {
long value = Long.parseLong(byteValue, 16);
return new PercentType(BigDecimal.valueOf(value).multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(255), 0, BigDecimal.ROUND_UP).intValue());
}
}