/**
* 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.satel.config;
import java.util.Map;
import org.apache.commons.lang.ArrayUtils;
import org.openhab.binding.satel.SatelBindingConfig;
import org.openhab.binding.satel.command.ClearTroublesCommand;
import org.openhab.binding.satel.command.ControlObjectCommand;
import org.openhab.binding.satel.command.IntegraStateCommand;
import org.openhab.binding.satel.command.SatelCommand;
import org.openhab.binding.satel.internal.event.IntegraStateEvent;
import org.openhab.binding.satel.internal.event.SatelEvent;
import org.openhab.binding.satel.internal.types.DoorsControl;
import org.openhab.binding.satel.internal.types.DoorsState;
import org.openhab.binding.satel.internal.types.IntegraType;
import org.openhab.binding.satel.internal.types.ObjectType;
import org.openhab.binding.satel.internal.types.OutputControl;
import org.openhab.binding.satel.internal.types.OutputState;
import org.openhab.binding.satel.internal.types.PartitionControl;
import org.openhab.binding.satel.internal.types.PartitionState;
import org.openhab.binding.satel.internal.types.StateType;
import org.openhab.binding.satel.internal.types.TroubleMemoryState;
import org.openhab.binding.satel.internal.types.TroubleState;
import org.openhab.binding.satel.internal.types.ZoneControl;
import org.openhab.binding.satel.internal.types.ZoneState;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.model.item.binding.BindingConfigParseException;
/**
* This class implements binding configuration for all items that represent
* Integra zones/partitions/outputs state.
*
* Supported options:
* <ul>
* <li>commands_only - binding does not update state of the item, but accepts commands</li>
* <li>force_arm - forces arming for items that accept arming commands</li>
* <li>invert_state - uses 0 as active state</li>
* </ul>
*
* @author Krzysztof Goworek
* @since 1.7.0
*/
public class IntegraStateBindingConfig extends SatelBindingConfig {
private StateType stateType;
private int[] objectNumbers;
private IntegraStateBindingConfig(StateType stateType, int[] objectNumbers, Map<String, String> options) {
super(options);
this.stateType = stateType;
this.objectNumbers = objectNumbers;
}
/**
* Parses given binding configuration and creates configuration object.
*
* @param bindingConfig
* config to parse
* @return parsed config object or <code>null</code> if config does not
* match
* @throws BindingConfigParseException
* in case of parse errors
*/
public static IntegraStateBindingConfig parseConfig(String bindingConfig) throws BindingConfigParseException {
ConfigIterator iterator = new ConfigIterator(bindingConfig);
ObjectType objectType;
// parse object type, mandatory
try {
objectType = iterator.nextOfType(ObjectType.class, "object type");
} catch (Exception e) {
// wrong config type, skip parsing
return null;
}
// parse state type, mandatory except for output
StateType stateType = null;
int[] objectNumbers = {};
switch (objectType) {
case ZONE:
stateType = iterator.nextOfType(ZoneState.class, "zone state type");
break;
case PARTITION:
stateType = iterator.nextOfType(PartitionState.class, "partition state type");
break;
case OUTPUT:
stateType = OutputState.OUTPUT;
break;
case DOORS:
stateType = iterator.nextOfType(DoorsState.class, "doors state type");
break;
case TROUBLE:
stateType = iterator.nextOfType(TroubleState.class, "trouble state type");
break;
case TROUBLE_MEMORY:
stateType = iterator.nextOfType(TroubleMemoryState.class, "trouble memory state type");
break;
}
// parse object number, if provided
if (iterator.hasNext()) {
try {
int objectNumberMax = 8 * stateType.getBytesCount(true);
String[] objectNumbersStr = iterator.next().split(",");
objectNumbers = new int[objectNumbersStr.length];
for (int i = 0; i < objectNumbersStr.length; ++i) {
int objectNumber = Integer.parseInt(objectNumbersStr[i]);
// range from parsed state type (number of state bytes)
if (objectNumber < 1 || objectNumber > objectNumberMax) {
throw new BindingConfigParseException(
String.format("Invalid object number: %s", bindingConfig));
}
objectNumbers[i] = objectNumber;
}
} catch (NumberFormatException e) {
throw new BindingConfigParseException(String.format("Invalid object number: %s", bindingConfig));
}
}
return new IntegraStateBindingConfig(stateType, objectNumbers, iterator.parseOptions());
}
/**
* {@inheritDoc}
*/
@Override
public State convertEventToState(Item item, SatelEvent event) {
if (!(event instanceof IntegraStateEvent) || hasOptionEnabled(Options.COMMANDS_ONLY)) {
return null;
}
IntegraStateEvent stateEvent = (IntegraStateEvent) event;
if (!stateEvent.hasDataForState(this.stateType)) {
return null;
}
if (this.objectNumbers.length == 1) {
int bitNbr = this.objectNumbers[0] - 1;
boolean invertState = hasOptionEnabled(Options.INVERT_STATE)
&& (this.stateType.getObjectType() == ObjectType.ZONE
|| this.stateType.getObjectType() == ObjectType.OUTPUT);
return booleanToState(item, stateEvent.isSet(this.stateType, bitNbr) ^ invertState);
} else if (this.objectNumbers.length == 0) {
int statesSet = stateEvent.statesSet(this.stateType);
if (item instanceof NumberItem) {
return new DecimalType(statesSet);
} else {
return booleanToState(item, statesSet > 0);
}
} else if (this.objectNumbers.length == 2 && item instanceof RollershutterItem) {
// roller shutter support
int upBitNbr = this.objectNumbers[0] - 1;
int downBitNbr = this.objectNumbers[1] - 1;
if (stateEvent.isSet(this.stateType, upBitNbr)) {
if (!stateEvent.isSet(this.stateType, downBitNbr)) {
return UpDownType.UP;
}
} else if (stateEvent.isSet(this.stateType, downBitNbr)) {
return UpDownType.DOWN;
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public SatelCommand convertCommand(Command command, IntegraType integraType, String userCode) {
if (command instanceof OnOffType && this.objectNumbers.length == 1) {
boolean switchOn = ((OnOffType) command == OnOffType.ON);
boolean force_arm = hasOptionEnabled(Options.FORCE_ARM);
switch (this.stateType.getObjectType()) {
case OUTPUT:
byte[] outputs = getObjectBitset((integraType == IntegraType.I256_PLUS) ? 32 : 16);
boolean invertState = hasOptionEnabled(Options.INVERT_STATE);
return new ControlObjectCommand(switchOn ^ invertState ? OutputControl.ON : OutputControl.OFF,
outputs, userCode);
case DOORS:
// for doors you can also control outputs of type 101, but we do not support this feature
// anyway we need to send list of outputs, so we have 'dummy' array for this purpose
byte[] doors = getObjectBitset(8);
byte[] dummy = new byte[(integraType == IntegraType.I256_PLUS) ? 32 : 16];
if (switchOn) {
return new ControlObjectCommand(DoorsControl.OPEN, ArrayUtils.addAll(dummy, doors), userCode);
} else {
return null;
}
case ZONE:
byte[] zones = getObjectBitset((integraType == IntegraType.I256_PLUS) ? 32 : 16);
switch ((ZoneState) this.stateType) {
case BYPASS:
return new ControlObjectCommand(switchOn ? ZoneControl.BYPASS : ZoneControl.UNBYPASS, zones,
userCode);
case ISOLATE:
if (switchOn) {
return new ControlObjectCommand(ZoneControl.ISOLATE, zones, userCode);
} else {
return null;
}
default:
// do nothing for other types of state
break;
}
break;
case PARTITION:
byte[] partitions = getObjectBitset(4);
switch ((PartitionState) this.stateType) {
// clear alarms on OFF command
case ALARM:
case ALARM_MEMORY:
case FIRE_ALARM:
case FIRE_ALARM_MEMORY:
case VERIFIED_ALARMS:
case WARNING_ALARMS:
if (switchOn) {
return null;
} else {
return new ControlObjectCommand(PartitionControl.CLEAR_ALARM, partitions, userCode);
}
// arm or disarm, depending on command
case ARMED:
case REALLY_ARMED:
return new ControlObjectCommand(
switchOn ? (force_arm ? PartitionControl.FORCE_ARM_MODE_0
: PartitionControl.ARM_MODE_0) : PartitionControl.DISARM,
partitions, userCode);
case ARMED_MODE_1:
return new ControlObjectCommand(
switchOn ? (force_arm ? PartitionControl.FORCE_ARM_MODE_1
: PartitionControl.ARM_MODE_1) : PartitionControl.DISARM,
partitions, userCode);
case ARMED_MODE_2:
return new ControlObjectCommand(
switchOn ? (force_arm ? PartitionControl.FORCE_ARM_MODE_2
: PartitionControl.ARM_MODE_2) : PartitionControl.DISARM,
partitions, userCode);
case ARMED_MODE_3:
return new ControlObjectCommand(
switchOn ? (force_arm ? PartitionControl.FORCE_ARM_MODE_3
: PartitionControl.ARM_MODE_3) : PartitionControl.DISARM,
partitions, userCode);
default:
// do nothing for other types of state
break;
}
case TROUBLE:
case TROUBLE_MEMORY:
// clear troubles
if (switchOn) {
return null;
} else {
return new ClearTroublesCommand(userCode);
}
}
} else if (this.stateType.getObjectType() == ObjectType.OUTPUT && this.objectNumbers.length == 2) {
// roller shutter support
if (command == UpDownType.UP) {
byte[] outputs = getObjectBitset((integraType == IntegraType.I256_PLUS) ? 32 : 16, 0);
return new ControlObjectCommand(OutputControl.ON, outputs, userCode);
} else if (command == UpDownType.DOWN) {
byte[] outputs = getObjectBitset((integraType == IntegraType.I256_PLUS) ? 32 : 16, 1);
return new ControlObjectCommand(OutputControl.ON, outputs, userCode);
} else if (command == StopMoveType.STOP) {
byte[] outputs = getObjectBitset((integraType == IntegraType.I256_PLUS) ? 32 : 16);
return new ControlObjectCommand(OutputControl.OFF, outputs, userCode);
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public SatelCommand buildRefreshCommand(IntegraType integraType) {
return new IntegraStateCommand(this.stateType, integraType == IntegraType.I256_PLUS);
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i : this.objectNumbers) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(Integer.toString(i));
}
return String.format("IntegraStateBindingConfig: object = %s, state = %s, object nbr = %s, options = %s",
this.stateType.getObjectType(), this.stateType, sb.toString(), this.optionsAsString());
}
private byte[] getObjectBitset(int size) {
byte[] bitset = new byte[size];
for (int objectNumber : this.objectNumbers) {
int bitNbr = objectNumber - 1;
bitset[bitNbr / 8] |= (byte) (1 << (bitNbr % 8));
}
return bitset;
}
private byte[] getObjectBitset(int size, int bitToSet) {
byte[] bitset = new byte[size];
int bitNbr = this.objectNumbers[bitToSet] - 1;
bitset[bitNbr / 8] |= (byte) (1 << (bitNbr % 8));
return bitset;
}
}