[paradoxalarm] Implement zone bypass command and additional zone states (#14557)

* Reduce warnings 1

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* 2

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* 3

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Initial files and package refactoring

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Implemented zone commands without checksum calculation

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* More stuff

* Added the checksum functionality
* Added more examples to the test

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Implement ZoneCommand and necessary classes

* Refactor the common logic
* Extract interface Command
* CHange the Response class to use Switch/case

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Fully implement the test for creating zone command payload

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Fix build / add headers and author to the new files

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Add command handling to the zone handler

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Add command channel to the Zone thing

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Research of zone states and some TODO notes

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Retrieval of zone special states from the panel

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Fix build

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Add the new channels to the metadata file

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Add new channels to zone handler

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Fix indexing in memory map and add more logging

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Refactoring and potential NPE access fixes

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Add new property "label" to the discovered zones and partitions

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Fix zone command issues

* Fix checksum creation
* Fix the parse and confirmation of the response

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Add the new channels to the README.md

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Fixed issue with not updating new channels in the zones

* A silly copy/paste mistake

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Change the type of the new channels from contact to switch

As per community discussion this makes more sense and will be more
intuitive - when something is true -> make it ON, when it's false ->
make it OFF. OPEN and CLOSED are not fitting so well here...

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Fix issue that the channel label is always NULL

* For both zone and partitions

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

* Add new types and channels to the i18n

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>

---------

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>
This commit is contained in:
Konstantin Polihronov 2023-07-25 20:53:09 +03:00 committed by GitHub
parent 51f3bf6b86
commit 33dd5e7f70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 776 additions and 157 deletions

View File

@ -97,11 +97,21 @@ Currently binding supports the following panels: EVO192, EVO48(not tested), EVO9
### Zone channels:
| Channel | Type | Description |
|-----------------|---------|--------------------------------------------------------------------------------|
| zoneLabel | String | Label of zone inside Paradox configuration |
| openedState | Contact | Zone opened / closed |
| tamperedState | Switch | Zone is tampered / not tampered |
| Channel | Type | Description |
|------------------------------------|---------|--------------------------------------------------------------------------------|
| zoneLabel | String | Label of zone inside Paradox configuration |
| openedState | Contact | Zone opened / closed |
| tamperedState | Switch | Zone is tampered |
| supervisionTrouble | Switch | Zone is in supervision trouble |
| inTxDelay | Switch | Zone is in txDelay |
| shutdown | Switch | Zone is shutdown |
| bypassed | Switch | Zone is bypassed |
| hasActivatedIntellizoneDelay | Switch | Zone is has an activated Intellizone delay |
| hasActivatedEntryDelay | Switch | Zone is has an activated entry delay |
| presentlyInAlarm | Switch | Zone is currently in alarm |
| generatedAlarm | Switch | Zone has generated an alarm |
| command | String | Command for zone. Can be (BYPASS, CLEAR_BYPASS) |
## Example things configuration

View File

@ -87,7 +87,7 @@ public class EvoCommunicator extends GenericCommunicator implements IParadoxComm
if (payload != null && payload.length >= RAM_BLOCK_SIZE) {
RamRequest request = (RamRequest) response.getRequest();
int ramBlockNumber = request.getRamBlockNumber();
memoryMap.updateElement(ramBlockNumber, payload);
memoryMap.updateElement(ramBlockNumber - 1, payload);
if (logger.isTraceEnabled()) {
logger.trace("Result for ramBlock={} is [{}]", ramBlockNumber, ParadoxUtil.byteArrayToString(payload));
}
@ -176,7 +176,21 @@ public class EvoCommunicator extends GenericCommunicator implements IParadoxComm
byte[] firstPage = memoryMap.getElement(0);
byte[] secondPage = memoryMap.getElement(8);
createZoneOpenedFlags(result, firstPage, secondPage);
createZoneTamperedFlags(result, firstPage, secondPage);
createZoneLowbatteryFlags(result, firstPage, secondPage);
createSpecialZoneFlags(result, memoryMap);
ParadoxUtil.printByteArray("Zone opened flags", result.getZonesOpened());
ParadoxUtil.printByteArray("Zone tampered flags", result.getZonesTampered());
ParadoxUtil.printByteArray("Zone low battery flags", result.getZonesLowBattery());
ParadoxUtil.printByteArray("Zone special flags", result.getZoneSpecialFlags());
return result;
}
private void createZoneOpenedFlags(ZoneStateFlags result, byte[] firstPage, byte[] secondPage) {
int pageOffset = panelType == PanelType.EVO48 ? 34 : 40;
byte[] firstBlock = Arrays.copyOfRange(firstPage, 28, pageOffset);
if (panelType != PanelType.EVO192) {
@ -186,9 +200,11 @@ public class EvoCommunicator extends GenericCommunicator implements IParadoxComm
byte[] zonesOpened = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
result.setZonesOpened(zonesOpened);
}
}
pageOffset = panelType == PanelType.EVO48 ? 46 : 52;
firstBlock = Arrays.copyOfRange(firstPage, 40, pageOffset);
private void createZoneTamperedFlags(ZoneStateFlags result, byte[] firstPage, byte[] secondPage) {
int pageOffset = panelType == PanelType.EVO48 ? 46 : 52;
byte[] firstBlock = Arrays.copyOfRange(firstPage, 40, pageOffset);
if (panelType != PanelType.EVO192) {
result.setZonesTampered(firstBlock);
} else {
@ -196,18 +212,51 @@ public class EvoCommunicator extends GenericCommunicator implements IParadoxComm
byte[] zonesTampered = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
result.setZonesTampered(zonesTampered);
}
}
pageOffset = panelType == PanelType.EVO48 ? 58 : 64;
firstBlock = Arrays.copyOfRange(firstPage, 52, pageOffset);
private void createZoneLowbatteryFlags(ZoneStateFlags result, byte[] firstPage, byte[] secondPage) {
int pageOffset = panelType == PanelType.EVO48 ? 58 : 64;
byte[] firstBlock = Arrays.copyOfRange(firstPage, 52, pageOffset);
if (panelType != PanelType.EVO192) {
result.setZonesTampered(firstBlock);
result.setZonesLowBattery(firstBlock);
} else {
byte[] secondBlock = Arrays.copyOfRange(secondPage, 24, 36);
byte[] zonesLowBattery = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
result.setZonesLowBattery(zonesLowBattery);
}
}
return result;
@SuppressWarnings("incomplete-switch")
private void createSpecialZoneFlags(ZoneStateFlags result, MemoryMap memoryMap) {
byte[] page2 = memoryMap.getElement(1);
byte[] page3 = memoryMap.getElement(2);
byte[] page7 = memoryMap.getElement(8);
byte[] page8 = memoryMap.getElement(9);
byte[] page9 = memoryMap.getElement(10);
switch (panelType) {
case EVO48:
byte[] firstBlock = Arrays.copyOfRange(page2, 0, 48);
result.setZoneSpecialFlags(firstBlock);
break;
case EVO96:
firstBlock = Arrays.copyOf(page2, 64);
byte[] secondBlock = Arrays.copyOfRange(page3, 0, 32);
byte[] specialZoneFlags = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
result.setZoneSpecialFlags(specialZoneFlags);
break;
case EVO192:
case EVOHD:
firstBlock = Arrays.copyOf(page2, 64);
secondBlock = Arrays.copyOfRange(page3, 0, 32);
byte[] thirdBlock = Arrays.copyOfRange(page7, 36, 64);
byte[] fourthBlock = Arrays.copyOf(page8, 64);
byte[] fifthBlock = Arrays.copyOfRange(page9, 0, 4);
specialZoneFlags = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock, thirdBlock, fourthBlock,
fifthBlock);
result.setZoneSpecialFlags(specialZoneFlags);
break;
}
}
public void initializeMemoryMap() {

View File

@ -41,6 +41,6 @@ public class MemoryMap {
}
public synchronized void updateElement(int index, byte[] elementValue) {
ramCache.set(index - 1, elementValue);
ramCache.set(index, elementValue);
}
}

View File

@ -21,5 +21,6 @@ public enum RequestType {
LOGON_SEQUENCE,
RAM,
EPROM,
PARTITION_COMMAND
PARTITION_COMMAND,
ZONE_COMMAND
}

View File

@ -103,36 +103,52 @@ public class Response implements IResponse {
byte highNibble = ParadoxUtil.getHighNibble(receivedCommand);
RequestType requestType = request.getType();
// For EPROM and RAM messages received command must be 0x5x
if (requestType == RequestType.EPROM || requestType == RequestType.RAM) {
if (highNibble == 0x5) {
header = Arrays.copyOfRange(packetBytes, 0, 22);
payload = Arrays.copyOfRange(packetBytes, 22, packetBytes.length - 1);
return;
}
switch (requestType) {
// For EPROM and RAM messages received command must be 0x5x
case EPROM:
case RAM:
if (highNibble == 0x5) {
header = Arrays.copyOfRange(packetBytes, 0, 22);
payload = Arrays.copyOfRange(packetBytes, 22, packetBytes.length - 1);
return;
}
break;
// For logon sequence packets there are various commands but their high nibbles should be either 0x0, 0x1 or
// 0x7
} else if (requestType == RequestType.LOGON_SEQUENCE) {
switch (highNibble) {
case 0x0:
case 0x1:
case 0x7:
case LOGON_SEQUENCE:
switch (highNibble) {
case 0x0:
case 0x1:
case 0x7:
header = Arrays.copyOfRange(packetBytes, 0, 16);
payload = Arrays.copyOfRange(packetBytes, 16, packetBytes.length);
return;
}
break;
case PARTITION_COMMAND:
if (highNibble == 0x4) {
header = Arrays.copyOfRange(packetBytes, 0, 16);
payload = Arrays.copyOfRange(packetBytes, 16, packetBytes.length);
payload = Arrays.copyOfRange(packetBytes, 16, 16 + packetBytes[1]);
logger.debug("Received a valid response for partition command");
return;
}
} else if (requestType == RequestType.PARTITION_COMMAND) {
if (highNibble == 0x4) {
header = Arrays.copyOfRange(packetBytes, 0, 16);
payload = Arrays.copyOfRange(packetBytes, 16, 16 + packetBytes[1]);
logger.debug("Received valid response for partition command");
return;
}
}
break;
case ZONE_COMMAND:
if (highNibble == 0xD) {
header = Arrays.copyOfRange(packetBytes, 0, 16);
payload = Arrays.copyOfRange(packetBytes, 16, 16 + packetBytes[1]);
logger.debug("Received a valid response for zone command");
return;
}
break;
}
// All other cases are considered wrong results for the parser and are probably live events which cannot be
// parsed currently
logger.debug("Message command not expected. Received command={}", receivedCommand);
logger.debug("Message command not expected. Received command={}", String.format("0x%08X", receivedCommand));
header = null;
payload = null;
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.paradoxalarm.internal.communication;
import org.openhab.binding.paradoxalarm.internal.communication.messages.IPPacket;
/**
* @author Konstantin Polihronov - Initial contribution
*/
public class ZoneCommandRequest extends Request {
public ZoneCommandRequest(RequestType type, IPPacket packet, IResponseReceiver receiver) {
super(type, packet, receiver);
}
}

View File

@ -50,8 +50,8 @@ public class EncryptionHandler {
private static final int PAYLOAD_RATE_LENGTH = 16;
private static final int ROUNDS = 14;
private static final int[] lTable = new int[TABLE_SIZE];
private static final int[] aTable = new int[TABLE_SIZE];
private static final int[] L_TABLE = new int[TABLE_SIZE];
private static final int[] A_TABLE = new int[TABLE_SIZE];
private static EncryptionHandler instance = new EncryptionHandler(new byte[] {});
static {
@ -62,7 +62,7 @@ public class EncryptionHandler {
int a = 1;
int d;
for (int index = 0; index < 255; index++) {
aTable[index] = a & 0xFF;
A_TABLE[index] = a & 0xFF;
/* Multiply by three */
d = (a & 0x80) & 0xFF;
a <<= 1;
@ -70,13 +70,13 @@ public class EncryptionHandler {
a ^= 0x1b;
a &= 0xFF;
}
a ^= aTable[index];
a ^= A_TABLE[index];
a &= 0xFF;
/* Set the log table value */
lTable[aTable[index]] = index & 0xFF;
L_TABLE[A_TABLE[index]] = index & 0xFF;
}
aTable[255] = aTable[0];
lTable[0] = 0;
A_TABLE[255] = A_TABLE[0];
L_TABLE[0] = 0;
}
private final int[] expandedKey = new int[KEY_LENGTH];
@ -196,9 +196,7 @@ public class EncryptionHandler {
if (i % 8 == 0) {
int tmp = temp[0];
for (int j = 1; j < 4; j++) {
temp[j - 1] = temp[j];
}
System.arraycopy(temp, 1, temp, 0, temp.length - 1);
temp[3] = tmp;
temp[0] ^= EncryptionHandlerConstants.RCON[(i / 8 - 1)];
@ -212,9 +210,9 @@ public class EncryptionHandler {
}
private int gmul(int c, int b) {
int s = lTable[c] + lTable[b];
int s = L_TABLE[c] + L_TABLE[b];
s %= 255;
s = aTable[s];
s = A_TABLE[s];
if (b == 0 || c == 0) {
s = 0;
}
@ -267,8 +265,7 @@ public class EncryptionHandler {
int[] tmpArray = new int[] { 0, 0, 0, 0 };
for (int i = 1; i < 4; i++) {
for (int j = 0; j < 4; j++) {
int[][][] shifts = EncryptionHandlerConstants.SHIFTS;
int index = i * 4 + (j + shifts[0][i][d]) % 4;
int index = i * 4 + (j + EncryptionHandlerConstants.SHIFTS[0][i][d]) % 4;
tmpArray[j] = a[index];
}
for (int j = 0; j < 4; j++) {

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.paradoxalarm.internal.communication.messages;
import org.openhab.binding.paradoxalarm.internal.communication.IRequest;
/**
* More generic interface for creating command requests
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface Command {
IRequest getRequest(int id);
}

View File

@ -10,9 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.paradoxalarm.internal.communication.messages;
package org.openhab.binding.paradoxalarm.internal.communication.messages.partition;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.paradoxalarm.internal.communication.IRequest;
import org.openhab.binding.paradoxalarm.internal.communication.PartitionCommandRequest;
import org.openhab.binding.paradoxalarm.internal.communication.RequestType;
import org.openhab.binding.paradoxalarm.internal.communication.messages.Command;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderMessageType;
import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -23,8 +30,7 @@ import org.slf4j.LoggerFactory;
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public enum PartitionCommand {
UNKNOWN(0),
public enum PartitionCommand implements Command {
ARM(2),
STAY_ARM(3),
INSTANT_ARM(4),
@ -32,7 +38,7 @@ public enum PartitionCommand {
DISARM(6),
BEEP(8);
private static final Logger logger = LoggerFactory.getLogger(PartitionCommand.class);
private static final Logger LOGGER = LoggerFactory.getLogger(PartitionCommand.class);
private int command;
@ -44,12 +50,20 @@ public enum PartitionCommand {
return command;
}
public static PartitionCommand parse(String command) {
public static @Nullable PartitionCommand parse(String command) {
try {
return PartitionCommand.valueOf(command);
} catch (IllegalArgumentException e) {
logger.debug("Unable to parse command={}. Fallback to UNKNOWN.", command);
return PartitionCommand.UNKNOWN;
LOGGER.debug("Unable to parse command={}. Fallback to UNKNOWN.", command);
return null;
}
}
@Override
public IRequest getRequest(int partitionId) {
PartitionCommandPayload payload = new PartitionCommandPayload(partitionId, this);
ParadoxIPPacket packet = new ParadoxIPPacket(payload.getBytes())
.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST);
return new PartitionCommandRequest(RequestType.PARTITION_COMMAND, packet, null);
}
}

View File

@ -10,31 +10,32 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.paradoxalarm.internal.communication.messages;
package org.openhab.binding.paradoxalarm.internal.communication.messages.partition;
import java.nio.ByteBuffer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.paradoxalarm.internal.communication.messages.IPayload;
/**
* The {@link CommandPayload} Class that structures the payload for partition commands.
* The {@link PartitionCommandPayload} Class that structures the payload for partition commands.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class CommandPayload implements IPayload {
public class PartitionCommandPayload implements IPayload {
private static final int BYTES_LENGTH = 15;
private final byte MESSAGE_START = 0x40;
private final byte PAYLOAD_SIZE = 0x0f;
private final byte[] EMPTY_FOUR_BYTES = { 0, 0, 0, 0 };
private final byte CHECKSUM = 0;
private static final byte MESSAGE_START = 0x40;
private static final byte PAYLOAD_SIZE = 0x0f;
private static final byte[] EMPTY_FOUR_BYTES = { 0, 0, 0, 0 };
private static final byte CHECKSUM = 0;
private final int partitionNumber;
private final PartitionCommand command;
public CommandPayload(int partitionNumber, PartitionCommand command) {
public PartitionCommandPayload(int partitionNumber, PartitionCommand command) {
this.partitionNumber = partitionNumber;
this.command = command;
}

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.paradoxalarm.internal.communication.messages.zone;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.paradoxalarm.internal.communication.IRequest;
import org.openhab.binding.paradoxalarm.internal.communication.RequestType;
import org.openhab.binding.paradoxalarm.internal.communication.ZoneCommandRequest;
import org.openhab.binding.paradoxalarm.internal.communication.messages.Command;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderMessageType;
import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public enum ZoneCommand implements Command {
CLEAR_BYPASS(0),
BYPASS(8);
private static final Logger LOGGER = LoggerFactory.getLogger(ZoneCommand.class);
private byte command;
ZoneCommand(int command) {
this.command = (byte) command;
}
public byte getCommand() {
return command;
}
public static @Nullable ZoneCommand parse(@Nullable String command) {
if (command == null) {
return null;
}
try {
return ZoneCommand.valueOf(command);
} catch (IllegalArgumentException e) {
LOGGER.debug("Unable to parse command={}. Fallback to UNKNOWN.", command);
return null;
}
}
@Override
public IRequest getRequest(int zoneId) {
ZoneCommandPayload payload = new ZoneCommandPayload(zoneId, this);
ParadoxIPPacket packet = new ParadoxIPPacket(payload.getBytes(), false)
.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST);
return new ZoneCommandRequest(RequestType.ZONE_COMMAND, packet, null);
}
}

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.paradoxalarm.internal.communication.messages.zone;
import java.nio.ByteBuffer;
import org.openhab.binding.paradoxalarm.internal.communication.messages.IPayload;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
/**
* @author Konstantin Polihronov - Initial contribution
*/
public class ZoneCommandPayload implements IPayload {
private static final int BYTES_LENGTH = 31;
private static final byte MESSAGE_START = (byte) 0xD0;
private static final byte PAYLOAD_SIZE = 0x1f;
private static final byte ZONE_FLAG = 0x08; // "bypassed" flag (5th bit)
private static final byte[] EMPTY_TWO_BYTES = { 0, 0 };
private static final byte CHECKSUM = 0;
private int zoneNumber;
private ZoneCommand command;
public ZoneCommandPayload(int zoneNumber, ZoneCommand command) {
this.zoneNumber = zoneNumber;
this.command = command;
}
@Override
public byte[] getBytes() {
byte[] bufferArray = new byte[BYTES_LENGTH];
ByteBuffer buf = ByteBuffer.wrap(bufferArray);
buf.put(MESSAGE_START);
buf.put(PAYLOAD_SIZE);
buf.put(ZONE_FLAG);
buf.put(command.getCommand());
buf.put(EMPTY_TWO_BYTES);
buf.put(calculateMessageBytes());
buf.put(ParadoxUtil.calculateChecksum(bufferArray));
return bufferArray;
}
/**
* The total zone message consists of 24 bytes (8 bits each) which results of 192 bits for each zone in case of
* Evo192 (i.e. 24x8).
* The low nible of each byte represents the first 4 zones of each group as a bit, the high nible is
* the second 4 zones. The zone groups are considered every 8 zones represented by a byte in this array (1-8,9-16,
* 17-24, etc)<br>
*
* Example: So if we address zone 1 for example the value of first byte will be 0x01, for zone 2 - 0x02, for zone 3
* - 0x04
* (third bit set to 1), for zone 4 - 0x08(fourth bit set to 1),<br>
* for zone 5 - 0x10, for zone 6 - 0x20, for zone 7 - 0x40, for zone 8 - 0x80.<br>
* For examples see TestGetBytes.java
*
* @return 24 bytes array with the needed zone to be set to bypass/clear bypass
*/
private byte[] calculateMessageBytes() {
byte[] zoneMessage = new byte[24];
int byteIndex = (zoneNumber - 1) / 8;
byte zoneByteGroup = zoneMessage[byteIndex];
int bitNumber = calculateBitNumber();
zoneMessage[byteIndex] = ParadoxUtil.setBit(zoneByteGroup, bitNumber - 1, 1);
return zoneMessage;
}
private int calculateBitNumber() {
int residual = zoneNumber % 8;
return residual != 0 ? residual : 8;
}
}

View File

@ -89,7 +89,7 @@ public class ParadoxDiscoveryService extends AbstractDiscoveryService {
ThingUID thingUID = new ThingUID(PARTITION_THING_TYPE_UID, bridgeUid, thingId);
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUid)
.withLabel("Partition " + label).withProperty(PARTITION_THING_TYPE_ID, thingId)
.withProperty("id", partition.getId()).build();
.withProperty("label", label).withProperty("id", partition.getId()).build();
logger.debug("Partition DiscoveryResult={}", result);
thingDiscovered(result);
@ -105,7 +105,7 @@ public class ParadoxDiscoveryService extends AbstractDiscoveryService {
ThingUID thingUID = new ThingUID(ZONE_THING_TYPE_UID, bridgeUid, thingId);
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUid)
.withLabel("Zone " + label).withProperty(ZONE_THING_TYPE_ID, thingId)
.withProperty("id", zone.getId()).build();
.withProperty("id", zone.getId()).withProperty("label", label).build();
logger.debug("Zone DiscoveryResult={}", result);
thingDiscovered(result);

View File

@ -67,7 +67,7 @@ public class ParadoxAlarmBindingConstants {
public static final String PANEL_BOARD_VOLTAGE = "boardVoltage";
public static final String PANEL_BATTERY_VOLTAGE = "batteryVoltage";
public static final String PARTITION_LABEL_CHANNEL_UID = "partitionLabel";
public static final String PARTITION_LABEL_CHANNEL_UID = "label";
public static final String PARTITION_STATE_CHANNEL_UID = "state";
@Deprecated // After implementation of channels for every possible state, the summarized additional states is no
// longer needed. We'll keep it for backward compatibility
@ -88,11 +88,20 @@ public class ParadoxAlarmBindingConstants {
public static final String PARTITION_INHIBIT_READY_CHANNEL_UID = "inhibitReady";
public static final String PARTITION_ALL_ZONES_CLOSED_CHANNEL_UID = "allZonesClosed";
public static final String ZONE_LABEL_CHANNEL_UID = "zoneLabel";
public static final String ZONE_LABEL_CHANNEL_UID = "label";
public static final String ZONE_OPENED_CHANNEL_UID = "opened";
public static final String ZONE_TAMPERED_CHANNEL_UID = "tampered";
public static final String ZONE_LOW_BATTERY_CHANNEL_UID = "lowBattery";
public static final String ZONE_SUPERVISION_TROUBLE_UID = "supervisionTrouble";
public static final String ZONE_IN_TX_DELAY_UID = "inTxDelay";
public static final String ZONE_SHUTDOWN_UID = "shutdown";
public static final String ZONE_BYPASSED_UID = "bypassed";
public static final String ZONE_HAS_ACTIVATED_INTELLIZONE_DELAY_UID = "hasActivatedIntellizoneDelay";
public static final String ZONE_HAS_ACTIVATED_ENTRY_DELAY_UID = "hasActivatedEntryDelay";
public static final String ZONE_PRESENTLY_IN_ALARM_UID = "presentlyInAlarm";
public static final String ZONE_GENERATED_ALARM_UID = "generatedAlarm";
// Misc constants
public static final StringType STATE_OFFLINE = new StringType("Offline");
public static final StringType STATE_ONLINE = new StringType("Online");

View File

@ -24,8 +24,6 @@ import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxPanelHandler} This is the handler that takes care of the panel related stuff.
@ -35,8 +33,6 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
public class ParadoxPanelHandler extends EntityBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ParadoxPanelHandler.class);
public ParadoxPanelHandler(Thing thing) {
super(thing);
}

View File

@ -17,7 +17,7 @@ import static org.openhab.binding.paradoxalarm.internal.handlers.ParadoxAlarmBin
import java.util.List;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.binding.paradoxalarm.internal.communication.messages.PartitionCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.partition.PartitionCommand;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
import org.openhab.binding.paradoxalarm.internal.model.Partition;
import org.openhab.core.library.types.OnOffType;
@ -78,26 +78,6 @@ public class ParadoxPartitionHandler extends EntityBaseHandler {
}
}
protected Partition getPartition() {
int index = calculateEntityIndex();
ParadoxIP150BridgeHandler bridge = (ParadoxIP150BridgeHandler) getBridge().getHandler();
ParadoxPanel panel = bridge.getPanel();
List<Partition> partitions = panel.getPartitions();
if (partitions == null) {
logger.debug(
"Partitions collection of Paradox Panel object is null. Probably not yet initialized. Skipping update.");
return null;
}
if (partitions.size() <= index) {
logger.debug("Attempted to access partition out of bounds of current partitions list. Index: {}, List: {}",
index, partitions);
return null;
}
Partition partition = partitions.get(index);
return partition;
}
private OpenClosedType booleanToContactState(boolean value) {
return value ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
}
@ -124,4 +104,24 @@ public class ParadoxPartitionHandler extends EntityBaseHandler {
super.handleCommand(channelUID, command);
}
}
protected Partition getPartition() {
int index = calculateEntityIndex();
ParadoxIP150BridgeHandler bridge = (ParadoxIP150BridgeHandler) getBridge().getHandler();
ParadoxPanel panel = bridge.getPanel();
List<Partition> partitions = panel.getPartitions();
if (partitions == null) {
logger.debug(
"Partitions collection of Paradox Panel object is null. Probably not yet initialized. Skipping update.");
return null;
}
if (partitions.size() <= index) {
logger.debug("Attempted to access partition out of bounds of current partitions list. Index: {}, List: {}",
index, partitions);
return null;
}
Partition partition = partitions.get(index);
return partition;
}
}

View File

@ -19,10 +19,14 @@ import java.util.List;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
import org.openhab.binding.paradoxalarm.internal.model.Zone;
import org.openhab.binding.paradoxalarm.internal.model.ZoneState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -41,15 +45,21 @@ public class ParadoxZoneHandler extends EntityBaseHandler {
@Override
protected void updateEntity() {
int index = calculateEntityIndex();
ParadoxIP150BridgeHandler bridge = (ParadoxIP150BridgeHandler) getBridge().getHandler();
ParadoxPanel panel = bridge.getPanel();
ParadoxIP150BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
logger.debug("Paradox bridge handler is null. Skipping update.");
return;
}
ParadoxPanel panel = bridgeHandler.getPanel();
List<Zone> zones = panel.getZones();
if (zones == null) {
logger.debug(
"Zones collection of Paradox Panel object is null. Probably not yet initialized. Skipping update.");
return;
}
int index = calculateEntityIndex();
if (zones.size() <= index) {
logger.debug("Attempted to access zone out of bounds of current zone list. Index: {}, List: {}", index,
zones);
@ -59,9 +69,23 @@ public class ParadoxZoneHandler extends EntityBaseHandler {
Zone zone = zones.get(index);
if (zone != null) {
updateState(ZONE_LABEL_CHANNEL_UID, new StringType(zone.getLabel()));
updateState(ZONE_OPENED_CHANNEL_UID, booleanToContactState(zone.getZoneState().isOpened()));
updateState(ZONE_TAMPERED_CHANNEL_UID, booleanToSwitchState(zone.getZoneState().isTampered()));
updateState(ZONE_LOW_BATTERY_CHANNEL_UID, booleanToSwitchState(zone.getZoneState().hasLowBattery()));
ZoneState zoneState = zone.getZoneState();
if (zoneState != null) {
updateState(ZONE_OPENED_CHANNEL_UID, booleanToContactState(zoneState.isOpened()));
updateState(ZONE_TAMPERED_CHANNEL_UID, booleanToSwitchState(zoneState.isTampered()));
updateState(ZONE_LOW_BATTERY_CHANNEL_UID, booleanToSwitchState(zoneState.hasLowBattery()));
updateState(ZONE_SUPERVISION_TROUBLE_UID, booleanToSwitchState(zoneState.isSupervisionTrouble()));
updateState(ZONE_IN_TX_DELAY_UID, booleanToSwitchState(zoneState.isInTxDelay()));
updateState(ZONE_SHUTDOWN_UID, booleanToSwitchState(zoneState.isShutdown()));
updateState(ZONE_BYPASSED_UID, booleanToSwitchState(zoneState.isBypassed()));
updateState(ZONE_HAS_ACTIVATED_INTELLIZONE_DELAY_UID,
booleanToSwitchState(zoneState.isHasActivatedIntellizoneDelay()));
updateState(ZONE_HAS_ACTIVATED_ENTRY_DELAY_UID,
booleanToSwitchState(zoneState.isHasActivatedEntryDelay()));
updateState(ZONE_PRESENTLY_IN_ALARM_UID, booleanToSwitchState(zoneState.isPresentlyInAlarm()));
updateState(ZONE_GENERATED_ALARM_UID, booleanToSwitchState(zoneState.isGeneratedAlarm()));
}
}
}
@ -72,4 +96,52 @@ public class ParadoxZoneHandler extends EntityBaseHandler {
private OnOffType booleanToSwitchState(boolean value) {
return value ? OnOffType.ON : OnOffType.OFF;
}
@Override
public void handleCommand(@NonNull ChannelUID channelUID, @NonNull Command command) {
if (command instanceof StringType) {
Zone zone = getZone();
if (zone != null) {
zone.handleCommand(command.toString());
}
} else {
super.handleCommand(channelUID, command);
}
}
protected Zone getZone() {
ParadoxIP150BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
logger.debug("Paradox bridge handler is null. Skipping update.");
return null;
}
ParadoxPanel panel = bridgeHandler.getPanel();
List<Zone> zones = panel.getZones();
if (zones == null) {
logger.debug(
"Zones collection of Paradox Panel object is null. Probably not yet initialized. Skipping update.");
return null;
}
int index = calculateEntityIndex();
if (zones.size() <= index) {
logger.debug("Attempted to access a zone out of bounds of current zone list. Index: {}, List: {}", index,
zones);
return null;
}
Zone zone = zones.get(index);
return zone;
}
private ParadoxIP150BridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("Paradox bridge is null. Skipping update.");
return null;
}
return (ParadoxIP150BridgeHandler) bridge.getHandler();
}
}

View File

@ -12,12 +12,8 @@
*/
package org.openhab.binding.paradoxalarm.internal.model;
import org.openhab.binding.paradoxalarm.internal.communication.PartitionCommandRequest;
import org.openhab.binding.paradoxalarm.internal.communication.RequestType;
import org.openhab.binding.paradoxalarm.internal.communication.messages.CommandPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderMessageType;
import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket;
import org.openhab.binding.paradoxalarm.internal.communication.messages.PartitionCommand;
import org.openhab.binding.paradoxalarm.internal.communication.IRequest;
import org.openhab.binding.paradoxalarm.internal.communication.messages.partition.PartitionCommand;
import org.openhab.binding.paradoxalarm.internal.handlers.Commandable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -51,16 +47,13 @@ public class Partition extends Entity implements Commandable {
@Override
public void handleCommand(String command) {
PartitionCommand partitionCommand = PartitionCommand.parse(command);
if (partitionCommand == PartitionCommand.UNKNOWN) {
logger.debug("Command UNKNOWN will be ignored.");
if (partitionCommand == null) {
logger.debug("Command {} is parsed to null. Skipping it", command);
return;
}
logger.debug("Submitting command={} for partition=[{}]", partitionCommand, this);
CommandPayload payload = new CommandPayload(getId(), partitionCommand);
ParadoxIPPacket packet = new ParadoxIPPacket(payload.getBytes())
.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST);
PartitionCommandRequest request = new PartitionCommandRequest(RequestType.PARTITION_COMMAND, packet, null);
IRequest request = partitionCommand.getRequest(getId());
getPanel().getCommunicator().submitRequest(request);
}
}

View File

@ -12,6 +12,11 @@
*/
package org.openhab.binding.paradoxalarm.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.paradoxalarm.internal.communication.IRequest;
import org.openhab.binding.paradoxalarm.internal.communication.messages.zone.ZoneCommand;
import org.openhab.binding.paradoxalarm.internal.handlers.Commandable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -22,23 +27,36 @@ import org.slf4j.LoggerFactory;
*
* @author Konstantin Polihronov - Initial contribution
*/
public class Zone extends Entity {
@NonNullByDefault
public class Zone extends Entity implements Commandable {
private final Logger logger = LoggerFactory.getLogger(Zone.class);
private ZoneState zoneState;
private @Nullable ZoneState zoneState;
public Zone(ParadoxPanel panel, int id, String label) {
public Zone(ParadoxPanel panel, int id, @Nullable String label) {
super(panel, id, label);
}
public ZoneState getZoneState() {
public @Nullable ZoneState getZoneState() {
return zoneState;
}
public void setZoneState(ZoneState zoneState) {
this.zoneState = zoneState;
logger.debug("Zone {} state updated to:\tOpened: {}, Tampered: {}, LowBattery: {}", getLabel(),
zoneState.isOpened(), zoneState.isTampered(), zoneState.hasLowBattery());
logger.debug("Zone {} state updated to: {}", getLabel(), zoneState);
}
@Override
public void handleCommand(@Nullable String command) {
ZoneCommand zoneCommand = ZoneCommand.parse(command);
if (zoneCommand == null) {
logger.debug("Command {} is parsed to null. Skipping it", command);
return;
}
logger.debug("Submitting command={} for partition=[{}]", zoneCommand, this);
IRequest request = zoneCommand.getRequest(getId());
getPanel().getCommunicator().submitRequest(request);
}
}

View File

@ -18,10 +18,21 @@ package org.openhab.binding.paradoxalarm.internal.model;
* @author Konstantin Polihronov - Initial contribution
*/
public class ZoneState {
// Regular states
private boolean isOpened;
private boolean isTampered;
private boolean hasLowBattery;
// Special flag states
private boolean supervisionTrouble;
private boolean inTxDelay;
private boolean shuttedDown;
private boolean bypassed;
private boolean hasActivatedIntellizoneDelay;
private boolean hasActivatedEntryDelay;
private boolean presentlyInAlarm;
private boolean generatedAlarm;
public ZoneState(boolean isOpened, boolean isTampered, boolean hasLowBattery) {
this.isOpened = isOpened;
this.isTampered = isTampered;
@ -51,4 +62,77 @@ public class ZoneState {
public void setHasLowBattery(boolean hasLowBattery) {
this.hasLowBattery = hasLowBattery;
}
public void setSupervisionTrouble(boolean supervisionTrouble) {
this.supervisionTrouble = supervisionTrouble;
}
public boolean isSupervisionTrouble() {
return supervisionTrouble;
}
public boolean isInTxDelay() {
return inTxDelay;
}
public void setInTxDelay(boolean inTxDelay) {
this.inTxDelay = inTxDelay;
}
public boolean isShutdown() {
return shuttedDown;
}
public void setShuttedDown(boolean shuttedDown) {
this.shuttedDown = shuttedDown;
}
public boolean isBypassed() {
return bypassed;
}
public void setBypassed(boolean bypassed) {
this.bypassed = bypassed;
}
public boolean isHasActivatedIntellizoneDelay() {
return hasActivatedIntellizoneDelay;
}
public void setHasActivatedIntellizoneDelay(boolean hasActivatedIntellizoneDelay) {
this.hasActivatedIntellizoneDelay = hasActivatedIntellizoneDelay;
}
public boolean isHasActivatedEntryDelay() {
return hasActivatedEntryDelay;
}
public void setHasActivatedEntryDelay(boolean hasActivatedEntryDelay) {
this.hasActivatedEntryDelay = hasActivatedEntryDelay;
}
public boolean isPresentlyInAlarm() {
return presentlyInAlarm;
}
public void setPresentlyInAlarm(boolean presentlyInAlarm) {
this.presentlyInAlarm = presentlyInAlarm;
}
public boolean isGeneratedAlarm() {
return generatedAlarm;
}
public void setGeneratedAlarm(boolean generatedAlarm) {
this.generatedAlarm = generatedAlarm;
}
@Override
public String toString() {
return "ZoneState [isOpened=" + isOpened + ", isTampered=" + isTampered + ", hasLowBattery=" + hasLowBattery
+ ", supervisionTrouble=" + supervisionTrouble + ", inTxDelay=" + inTxDelay + ", shuttedDown="
+ shuttedDown + ", bypassed=" + bypassed + ", hasActivatedIntellizoneDelay="
+ hasActivatedIntellizoneDelay + ", hasActivatedEntryDelay=" + hasActivatedEntryDelay
+ ", presentlyInAlarm=" + presentlyInAlarm + ", generatedAlarm=" + generatedAlarm + "]";
}
}

View File

@ -21,6 +21,7 @@ public class ZoneStateFlags {
private byte[] zonesOpened;
private byte[] zonesTampered;
private byte[] zonesLowBattery;
private byte[] zoneSpecialFlags;
public byte[] getZonesOpened() {
return zonesOpened;
@ -45,4 +46,12 @@ public class ZoneStateFlags {
public void setZonesLowBattery(byte[] zonesLowBattery) {
this.zonesLowBattery = zonesLowBattery;
}
public byte[] getZoneSpecialFlags() {
return zoneSpecialFlags;
}
public void setZoneSpecialFlags(byte[] zoneSpecialFlags) {
this.zoneSpecialFlags = zoneSpecialFlags;
}
}

View File

@ -64,10 +64,11 @@ public class EvoParser extends AbstractParser {
@Override
public ZoneState calculateZoneState(int id, ZoneStateFlags zoneStateFlags) {
int index = (id - 1) / 8;
int bitNumber = id % 8 - 1;
// Every zone state is represented by a bit set/unset in the big byte array retrieved from the memory of the
// panel
byte[] zonesOpened = zoneStateFlags.getZonesOpened();
boolean isOpened = ParadoxUtil.isBitSet(zonesOpened[index], bitNumber);
@ -77,6 +78,38 @@ public class EvoParser extends AbstractParser {
byte[] zonesLowBattery = zoneStateFlags.getZonesLowBattery();
boolean hasLowBattery = ParadoxUtil.isBitSet(zonesLowBattery[index], bitNumber);
return new ZoneState(isOpened, isTampered, hasLowBattery);
ZoneState zoneState = new ZoneState(isOpened, isTampered, hasLowBattery);
calculateSpecialFlags(zoneStateFlags, id, zoneState);
return zoneState;
}
private void calculateSpecialFlags(ZoneStateFlags zoneStateFlags, int index, ZoneState zoneState) {
// Each byte is filled with 8 special zone flags.
// Each bit of the byte represents a specific flag.
// Zone Flags:
// 0 = Zone supervision trouble
// 1 = Zone in TX delay
// 2 = Zone shutted down
// 3 = Zone bypassed
// 4 = Zone activated intellizone delay
// 5 = Zone activated entry delay
// 6 = Zone presently in alarm
// 7 = Zone generated an alarm
// The index of the actual zones and partitions enumerates from 1-N. In the arrays we need to index it from 0.
int specialFlagsIndex = index - 1;
byte[] zoneSpecialFlags = zoneStateFlags.getZoneSpecialFlags();
byte currentZoneFlags = zoneSpecialFlags[specialFlagsIndex];
zoneState.setSupervisionTrouble(ParadoxUtil.isBitSet(currentZoneFlags, 0));
zoneState.setInTxDelay(ParadoxUtil.isBitSet(currentZoneFlags, 1));
zoneState.setShuttedDown(ParadoxUtil.isBitSet(currentZoneFlags, 2));
zoneState.setBypassed(ParadoxUtil.isBitSet(currentZoneFlags, 3));
zoneState.setHasActivatedIntellizoneDelay(ParadoxUtil.isBitSet(currentZoneFlags, 4));
zoneState.setHasActivatedEntryDelay(ParadoxUtil.isBitSet(currentZoneFlags, 5));
zoneState.setPresentlyInAlarm(ParadoxUtil.isBitSet(currentZoneFlags, 6));
zoneState.setGeneratedAlarm(ParadoxUtil.isBitSet(currentZoneFlags, 7));
}
}

View File

@ -30,7 +30,8 @@ import org.slf4j.LoggerFactory;
public class ParadoxUtil {
private static final String SPACE_DELIMITER = " ";
private static final Logger logger = LoggerFactory.getLogger(ParadoxUtil.class);
private static final Logger LOGGER = LoggerFactory.getLogger(ParadoxUtil.class);
public static byte calculateChecksum(byte[] payload) {
int result = 0;
@ -50,28 +51,28 @@ public class ParadoxUtil {
}
public static void printPacket(String description, byte[] array) {
if (logger.isTraceEnabled()) {
logger.trace("Packet payload size: {}", array[1]);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Packet payload size: {}", array[1]);
printByteArray(description, array, array[1] + 16);
}
}
public static void printByteArray(String description, byte[] array) {
if (array == null) {
logger.trace("Array is null");
LOGGER.trace("Array is null");
return;
}
printByteArray(description, array, array.length);
}
public static void printByteArray(String description, byte[] array, int length) {
if (!logger.isTraceEnabled()) {
if (!LOGGER.isTraceEnabled()) {
return;
}
String result = byteArrayToString(array, length);
if (!result.isEmpty()) {
logger.trace("{}", description + SPACE_DELIMITER + result);
LOGGER.trace("{}", description + SPACE_DELIMITER + result);
}
}
@ -131,7 +132,7 @@ public class ParadoxUtil {
byte[] byteArray = outputStream.toByteArray();
return byteArray;
} catch (IOException e) {
logger.warn("Exception merging arrays:", e);
LOGGER.warn("Exception merging arrays:", e);
return new byte[0];
}
}

View File

@ -1,8 +1,3 @@
# add-on
addon.paradoxalarm.name = ParadoxAlarm Binding
addon.paradoxalarm.description = This is the binding for ParadoxAlarm.
# thing types
thing-type.paradoxalarm.ip150.label = Paradox IP150 Module Connector
@ -57,6 +52,9 @@ channel-type.paradoxalarm.bootloaderVersion.label = Boot Loader Version
channel-type.paradoxalarm.bootloaderVersion.description = Boot loader version
channel-type.paradoxalarm.bypassReady.label = Partition Is Bypass Ready
channel-type.paradoxalarm.bypassReady.description = Partition is Bypass Ready
channel-type.paradoxalarm.bypassed.label = Bypassed
channel-type.paradoxalarm.command.label = Communicator Command
channel-type.paradoxalarm.command.description = Send Command
channel-type.paradoxalarm.command.label = Partition Command
channel-type.paradoxalarm.command.description = Send command
channel-type.paradoxalarm.command.state.option.ARM = Arm
@ -65,20 +63,26 @@ channel-type.paradoxalarm.command.state.option.INSTANT_ARM = Instant Arm
channel-type.paradoxalarm.command.state.option.FORCE_ARM = Force Arm
channel-type.paradoxalarm.command.state.option.DISARM = Disarm
channel-type.paradoxalarm.command.state.option.BEEP = Keyboard Beep
channel-type.paradoxalarm.command.label = Communicator Command
channel-type.paradoxalarm.command.description = Send Command
channel-type.paradoxalarm.command.label = Zone Command
channel-type.paradoxalarm.command.description = Send command for a zone
channel-type.paradoxalarm.command.state.option.BYPASS = Bypass
channel-type.paradoxalarm.command.state.option.CLEAR_BYPASS = Clear Bypass
channel-type.paradoxalarm.communicationState.label = Bridge Communication State
channel-type.paradoxalarm.communicationState.description = Status of connection to Paradox system
channel-type.paradoxalarm.forceReady.label = Partition Is Force Ready
channel-type.paradoxalarm.forceReady.description = Partition is Force Ready
channel-type.paradoxalarm.generatedAlarm.label = Generated an Alarm
channel-type.paradoxalarm.hardwareVersion.label = Hardware Version
channel-type.paradoxalarm.hardwareVersion.description = Panel hardware version
channel-type.paradoxalarm.hasActivatedEntryDelay.label = Has Activated Entry Delay
channel-type.paradoxalarm.hasActivatedIntellizoneDelay.label = Has Activated Intellizone Delay
channel-type.paradoxalarm.inEntryDelay.label = Partition In Entry Delay
channel-type.paradoxalarm.inEntryDelay.description = Partition in Entry Delay
channel-type.paradoxalarm.inExitDelay.label = Partition In Exit Delay
channel-type.paradoxalarm.inExitDelay.description = Partition in Exit Delay
channel-type.paradoxalarm.inTrouble.label = Partition In Trouble
channel-type.paradoxalarm.inTrouble.description = Partition in Trouble
channel-type.paradoxalarm.inTxDelay.label = In TX Delay
channel-type.paradoxalarm.inhibitReady.label = Partition Is Inhibit Ready
channel-type.paradoxalarm.inhibitReady.description = Partition is Inhibit Ready
channel-type.paradoxalarm.openedState.label = Zone State
@ -91,14 +95,17 @@ channel-type.paradoxalarm.panelType.label = Panel Type
channel-type.paradoxalarm.panelType.description = Panel type (Evo, SP, etc)
channel-type.paradoxalarm.partitionLabel.label = Partition Label
channel-type.paradoxalarm.partitionLabel.description = Label of partition
channel-type.paradoxalarm.presentlyInAlarm.label = Currently in Alarm
channel-type.paradoxalarm.readyToArm.label = Partition Ready To Arm
channel-type.paradoxalarm.readyToArm.description = Partition ready to arm
channel-type.paradoxalarm.serialNumber.label = Serial Number
channel-type.paradoxalarm.serialNumber.description = Panel serial number
channel-type.paradoxalarm.shutdown.label = Shutdown
channel-type.paradoxalarm.state.label = Partition State
channel-type.paradoxalarm.state.description = State of partition
channel-type.paradoxalarm.stayInstantReady.label = Partition Is Stay Instant Ready
channel-type.paradoxalarm.stayInstantReady.description = Partition is Stay Instant Ready
channel-type.paradoxalarm.supervisionTrouble.label = Supervision Trouble
channel-type.paradoxalarm.tamperedState.label = Tampered
channel-type.paradoxalarm.tamperedState.description = State of zone
channel-type.paradoxalarm.voltage.label = Voltage
@ -115,3 +122,8 @@ channel-type.paradoxalarm.zoneInTamperTrouble.label = Partition Has Zone In Tamp
channel-type.paradoxalarm.zoneInTamperTrouble.description = Partition has in Tamper Trouble
channel-type.paradoxalarm.zoneLabel.label = Zone Label
channel-type.paradoxalarm.zoneLabel.description = Label of zone
# add-on
addon.paradoxalarm.name = ParadoxAlarm Binding
addon.paradoxalarm.description = This is the binding for ParadoxAlarm.

View File

@ -16,6 +16,16 @@
<channel id="opened" typeId="openedState"/>
<channel id="tampered" typeId="tamperedState"/>
<channel id="lowBattery" typeId="system.low-battery"/>
<channel id="command" typeId="command"/>
<channel id="supervisionTrouble" typeId="supervisionTrouble"/>
<channel id="inTxDelay" typeId="inTxDelay"/>
<channel id="shutdown" typeId="shutdown"/>
<channel id="bypassed" typeId="bypassed"/>
<channel id="hasActivatedIntellizoneDelay" typeId="hasActivatedIntellizoneDelay"/>
<channel id="hasActivatedEntryDelay" typeId="hasActivatedEntryDelay"/>
<channel id="presentlyInAlarm" typeId="presentlyInAlarm"/>
<channel id="generatedAlarm" typeId="generatedAlarm"/>
</channels>
<config-description>
@ -44,4 +54,55 @@
<description>State of zone</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="command">
<item-type>String</item-type>
<label>Zone Command</label>
<description>Send command for a zone</description>
<state>
<options>
<option value="BYPASS">Bypass</option>
<option value="CLEAR_BYPASS">Clear Bypass</option>
</options>
</state>
</channel-type>
<channel-type id="supervisionTrouble">
<item-type>Switch</item-type>
<label>Supervision Trouble</label>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="inTxDelay">
<item-type>Switch</item-type>
<label>In TX Delay</label>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="shutdown">
<item-type>Switch</item-type>
<label>Shutdown</label>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="bypassed">
<item-type>Switch</item-type>
<label>Bypassed</label>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="hasActivatedIntellizoneDelay">
<item-type>Switch</item-type>
<label>Has Activated Intellizone Delay</label>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="hasActivatedEntryDelay">
<item-type>Switch</item-type>
<label>Has Activated Entry Delay</label>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="presentlyInAlarm">
<item-type>Switch</item-type>
<label>Currently in Alarm</label>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="generatedAlarm">
<item-type>Switch</item-type>
<label>Generated an Alarm</label>
<state readOnly="true" pattern="%s"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -12,11 +12,12 @@
*/
package org.openhab.binding.paradoxalarm.internal;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.paradoxalarm.internal.communication.messages.CommandPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.PartitionCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.partition.PartitionCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.partition.PartitionCommandPayload;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
/**
@ -25,19 +26,20 @@ import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class TestCreateCommandPayload {
@Test
public void testCreatePayload() {
for (PartitionCommand command : PartitionCommand.values()) {
for (int partitionNumber = 1; partitionNumber <= 8; partitionNumber++) {
CommandPayload payload = new CommandPayload(partitionNumber, command);
PartitionCommandPayload payload = new PartitionCommandPayload(partitionNumber, command);
assertNibble(partitionNumber, command, payload);
}
}
}
private void assertNibble(int partitionNumber, PartitionCommand command, CommandPayload payload) {
private void assertNibble(int partitionNumber, PartitionCommand command, PartitionCommandPayload payload) {
byte[] bytes = payload.getBytes();
int payloadIndexOfByteToCheck = 6 + (partitionNumber - 1) / 2;
byte byteValue = bytes[payloadIndexOfByteToCheck];

View File

@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.*;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.paradoxalarm.internal.communication.crypto.EncryptionHandler;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderCommand;
@ -27,6 +28,7 @@ import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class TestEncryptionHandler {
private static final String INPUT_STRING = "My test string for encryption.";

View File

@ -12,17 +12,20 @@
*/
package org.openhab.binding.paradoxalarm.internal;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.paradoxalarm.internal.communication.messages.CommandPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.EpromRequestPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.IPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket;
import org.openhab.binding.paradoxalarm.internal.communication.messages.PartitionCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.partition.PartitionCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.partition.PartitionCommandPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.zone.ZoneCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.zone.ZoneCommandPayload;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxException;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
@ -33,6 +36,7 @@ import org.slf4j.LoggerFactory;
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class TestGetBytes {
private static final int PARTITION_NUMBER = 1;
@ -61,18 +65,18 @@ public class TestGetBytes {
assertTrue(Arrays.equals(packetBytes, EXPECTED1));
}
private static final byte[] EXPECTED_COMMAND_PAYLOAD = { 0x40, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00 };
private static final byte[] EXPECTED_PARTITION_COMMAND_PAYLOAD = { 0x40, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
@Test
public void testCommandPayload() {
CommandPayload payload = new CommandPayload(PARTITION_NUMBER, PartitionCommand.ARM);
public void testPartitionCommandPayload() {
PartitionCommandPayload payload = new PartitionCommandPayload(PARTITION_NUMBER, PartitionCommand.ARM);
final byte[] packetBytes = payload.getBytes();
ParadoxUtil.printByteArray("Expected =", EXPECTED_COMMAND_PAYLOAD);
ParadoxUtil.printByteArray("Expected =", EXPECTED_PARTITION_COMMAND_PAYLOAD);
ParadoxUtil.printByteArray("Result =", packetBytes);
assertTrue(Arrays.equals(packetBytes, EXPECTED_COMMAND_PAYLOAD));
assertTrue(Arrays.equals(packetBytes, EXPECTED_PARTITION_COMMAND_PAYLOAD));
}
private static final byte[] EXPECTED_MEMORY_PAYLOAD = { (byte) 0xAA, 0x0A, 0x00, 0x03, 0x08, (byte) 0xF0, 0x00,
@ -88,4 +92,39 @@ public class TestGetBytes {
ParadoxUtil.printByteArray("Expected =", EXPECTED_MEMORY_PAYLOAD);
ParadoxUtil.printByteArray("Result =", bytes);
}
private static final byte[] EXPECTED_ZONE_5_BYPASS_COMMAND = { (byte) 0xd0, 0x1f, 0x08, 0x08, 0x00, 0x00, 0x10,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f };
private static final byte[] EXPECTED_ZONE_5_CLEAR_BYPASS_COMMAND = { (byte) 0xd0, 0x1f, 0x08, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07 };
private static final byte[] EXPECTED_ZONE_20_BYPASS_COMMAND = { (byte) 0xd0, 0x1f, 0x08, 0x08, 0x00, 0x00, 0x00,
0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07 };
private static final byte[] EXPECTED_ZONE_23_BYPASS_COMMAND = { (byte) 0xd0, 0x1f, 0x08, 0x08, 0x00, 0x00, 0x00,
0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3f };
private static final byte[] EXPECTED_ZONE_23_CLEAR_BYPASS_COMMAND = { (byte) 0xd0, 0x1f, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37 };
@Test
public void testZoneCommandPayload() {
testZoneCommandPayload(5, ZoneCommand.BYPASS, EXPECTED_ZONE_5_BYPASS_COMMAND);
testZoneCommandPayload(5, ZoneCommand.CLEAR_BYPASS, EXPECTED_ZONE_5_CLEAR_BYPASS_COMMAND);
testZoneCommandPayload(20, ZoneCommand.BYPASS, EXPECTED_ZONE_20_BYPASS_COMMAND);
testZoneCommandPayload(23, ZoneCommand.BYPASS, EXPECTED_ZONE_23_BYPASS_COMMAND);
testZoneCommandPayload(23, ZoneCommand.CLEAR_BYPASS, EXPECTED_ZONE_23_CLEAR_BYPASS_COMMAND);
}
private void testZoneCommandPayload(int zoneNumber, ZoneCommand command, byte[] expected) {
ZoneCommandPayload payload = new ZoneCommandPayload(zoneNumber, command);
final byte[] packetBytes = payload.getBytes();
ParadoxUtil.printByteArray("Expected " + zoneNumber + " =", expected);
ParadoxUtil.printByteArray("Result " + zoneNumber + " =", packetBytes);
assertTrue(Arrays.equals(packetBytes, expected));
}
}

View File

@ -12,8 +12,9 @@
*/
package org.openhab.binding.paradoxalarm.internal;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
@ -22,6 +23,7 @@ import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class TestParadoxUtil {
@Test
@ -30,19 +32,19 @@ public class TestParadoxUtil {
final int rate = 16;
byte[] extendedArray = ParadoxUtil.extendArray(arrayToExtend, rate);
final byte[] EXPECTED_RESULT = { 0x0A, 0x50, 0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x59, (byte) 0xEE, (byte) 0xEE,
final byte[] expectedResult = { 0x0A, 0x50, 0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x59, (byte) 0xEE, (byte) 0xEE,
(byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE };
assertArrayEquals(EXPECTED_RESULT, extendedArray); //
assertArrayEquals(expectedResult, extendedArray); //
}
@Test
public void testMergeArrays() {
final byte[] ARR1 = { 0x01, 0x02, 0x03 };
final byte[] ARR2 = { 0x04, 0x05, 0x06 };
final byte[] ARR3 = { 0x07, 0x08, 0x09 };
byte[] mergedArrays = ParadoxUtil.mergeByteArrays(ARR1, ARR2, ARR3);
final byte[] arr1 = { 0x01, 0x02, 0x03 };
final byte[] arr2 = { 0x04, 0x05, 0x06 };
final byte[] arr3 = { 0x07, 0x08, 0x09 };
byte[] mergedArrays = ParadoxUtil.mergeByteArrays(arr1, arr2, arr3);
final byte[] EXPECTED_RESULT = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 };
assertArrayEquals(EXPECTED_RESULT, mergedArrays);
final byte[] expectedResult = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 };
assertArrayEquals(expectedResult, mergedArrays);
}
}