added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.paradoxalarm</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,184 @@
# Paradox Alarm System binding
This binding is intended to provide basic support for Paradox Alarm system.
With the power of openHAB this binding can be used for complex decision rules combining motion/magnetic sensor or whole partitions states with different scenarios.
Examples:
* All partitions are armed, therefore there is no one at home.
* Window is opened for more than 10 minutes and temperature outside is bellow XXX degrees, send mail/any other supported notification to particular people.
## Supported Paradox panels/systems
Currently binding supports the following panels: EVO192, EVO48(not tested), EVO96(not tested)
## Supported things
| Thing | Thing Type | Description |
|------------|------------|----------------------------------------------------------------|
| ip150 | Bridge | The bridge is used to communicate with IP150 ethernet module attached to Paradox security system.|
| panel | Thing | This is representation of Paradox panel. Has the general information about the main panel module, i.e. serial number, firmware/hardware/software versions, panel type, etc...|
| partition | Thing | The partition is grouped aggregation of multiple zones. It's also referred in Paradox Babyware as "Area". |
| zone | Thing | Paradox zone. Can be anything - magnetic, motion or any other opened/closed sensor. State channel is contact, "low battery" and "is tampered" channels are switch, label is String |
## Things configuration
### IP150 bridge parameters
| Parameter | Description |
|-------------------|----------------------------------------|
| refresh | Value is in seconds. Defines the refresh interval when the binding polls from paradox system. Optional parameter. Default 5 seconds.|
| ip150Password | The password to your IP150 (not your panel PIN). Mandatory parameter. |
| pcPassword | The panel programming code 3012 setting. Optional parameter. Default value is 0000.|
| ipAddress | IP address or hostname of your IP150. If hostname is used must be resolvable by OpenHAB. Mandatory parameter. |
| port | The port used for data communication. Optional parameter. Default value is 10000.|
| panelType | If parameter is passed, auto-discovery of panel type will be skipped. Provide string - EVO48, EVO96, EVO192, etc... Optional parameter. |
| reconnectWaitTime | Value is in seconds. The time to wait before a reconnect occurs after socket timeout. Optional parameter. Default value is 30 seconds.|
| maxPartitions | Sets maximum partitions to use during refresh. If not set, maximum allowed amount from panelType will be used. Optional parameter. |
| maxZones | Sets maximum zones to use during refresh. If not set, maximum allowed amount from panelType will be used. Optional parameter.|
| encrypt | Sets if encryption has to be used. Optional parameter. Default value is false |
### IP150 bridge channels
| Channel | Description |
|---------------------|------------------------------------------------|
|communicationCommand | Possible values [LOGOUT, LOGIN, RESET] |
|communicationState | Shows the communication status to Paradox. Different from Bridge status. Bridge may be online and able to receive commands but communication may be offline due to various reasons. Possible values [Offline, Online] |
#### Communication command channel allowed values
| Value | Description |
|--------|------------------------------------------------------------------------------------|
| LOGOUT | Logs out and disconnects from Paradox alarm system |
| LOGIN | Creates socket if necessary, connects to paradox system and uses the logon data from the thing parameters to connect.|
| RESET | Does logout and then login with recreation of communicator objects inside the code.|
### Entities (zones, partitions) configuration parameters:
| Value | Description |
|-------------------|------------------------------------------------------------------------------------|
| id | The numeric ID of the zone/partition |
| disarmEnabled | Optional boolean flag. Valid for partitions. When set to true the command DISARM will be allowed for the partition where the flag is enabled. CAUTION: Enabling DISARM command can be dangerous. If attacker can gain access to your openHAB (via API or UI), this command can be used to disarm your armed partition (area) |
### Panel channels:
| Channel | Type | Description |
|--------------------------|----------------------------|-------------------------------------------------------------------------------------------|
| state | String | Overall panel state |
| inputVoltage | Number:ElectricPotential | Supply Voltage |
| boardVoltage | Number:ElectricPotential | Board DC Voltage |
| batteryVoltage | Number:ElectricPotential | Battery Voltage |
| panelTime | DateTime | Panel internal time (Timezone is set to default zone of the Java virtual machine) |
### Partition channels:
| Channel | Type | Description |
|--------------------------|---------|-----------------------------------------------------------------------------------------------|
| partitionLabel | String | Label of partition inside Paradox configuration |
| state | String |State of partition (armed, disarmed, in alarm) |
| additionalState | String | This used to be a channel where all different states were consolidated as semi-colon separated string. With implementation of each state as channel additional states should be no longer used. (deprecated channel) |
| readyToArm | Switch | Partition is Ready to arm |
| inExitDelay | Switch | Partition is in Exit delay |
| inEntryDelay | Switch | Partition in Entry Delay |
| inTrouble | Switch | Partition has trouble |
| alarmInMemory | Switch | Partition has alarm in memory |
| zoneBypass | Switch | Partition is in Zone Bypass |
| zoneInTamperTrouble | Switch | Partition is in Tamper Trouble |
| zoneInLowBatteryTrouble | Switch | Partition has zone in Low Battery Trouble |
| zoneInFireLoopTrouble | Switch | Partition has zone in Fire Loop Trouble |
| zoneInSupervisionTrouble | Switch | Partition has zone in Supervision Trouble |
| stayInstantReady | Switch | Partition is in state Stay Instant Ready |
| forceReady | Switch | Partition is in state Force Ready |
| bypassReady | Switch | Partition is in state Bypass Ready |
| inhibitReady | Switch | Partition is in state Inhibit Ready |
| allZonesClosed | Contact | All zones in partition are currently closed |
| command | String | Command to be send to partition. Can be (ARM, DISARM, FORCE_ARM, INSTANT_ARM, STAY_ARM, BEEP) |
### 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 |
## Example things configuration
```java
Bridge paradoxalarm:ip150:ip150 [refresh=5, panelType="EVO192", ip150Password="********", pcPassword="0000", ipAddress=XXX.XXX.XXX.XXX", port=10000, reconnectWaitTime=10, maxPartitions=4, maxZones=50, encrypt=true ] {
Thing panel panel
Thing partition partition1 [id=1]
Thing partition partition2 [id=2]
Thing partition partition3 [id=3]
Thing partition partition4 [id=4]
Thing zone MotionSensor1 [id=1]
Thing zone MagneticSensorWindow1 [id=2]
Thing zone MotionSensor2 [id=3]
Thing zone MagneticSensorWindow2 [id=4]
}
```
## Example items configuration
```java
//Groups
Group Paradox "Paradox security group"
Group Partitions "Paradox partitions" (Paradox)
Group Floor1MUC "Magnetic sensors - Floor 1" (Paradox)
Group Floor2MUC "Magnetic sensors - Floor 2" (Paradox)
Group Floor3MUC "Magnetic sensors - Floor 3" (Paradox)
Group PIRSensors "Motion sensors" (Paradox)
//COMMUNICATOR BRIDGE
String paradoxSendCommand "Send command to IP150" {channel="paradoxalarm:ip150:ip150:communicationCommand"}
String panelState "Paradox panel state: [%s]"<network> (Paradox) { channel = "paradoxalarm:ip150:ip150:communicationState" }
Number paradoxAcVoltage Input Voltage: [%.1f V] (Paradox) { channel = paradoxalarm:panel:ip150:panel:inputVoltage }
Number paradoxDcVoltage Board DC Voltage: [%.1f V] (Paradox) { channel = paradoxalarm:panel:ip150:panel:boardVoltage }
Number paradoxBatteryVoltage Battery Voltage: [%.1f V] (Paradox) { channel = paradoxalarm:panel:ip150:panel:batteryVoltage }
DateTime paradoxTime "Paradox Time: [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1tS]" <lock> (Paradox) { channel = "paradoxalarm:panel:ip150:panel:panelTime" }
//PARTITIONS
String partition1State "Magnetic sensors - Floor 1: [%s]" (Partitions) { channel = "paradoxalarm:partition:ip150:partition1:state" }
String partition1Command "Command for MUCFL1: [%s]" <lock> (Partitions) { channel = "paradoxalarm:partition:ip150:partition1:command" }
//ZONES
Contact CorridorFl1_PIR_state "Corridor Fl1 motion: [%s]" (PIRSensors) { channel = "paradoxalarm:zone:ip150:MotionSensor1:opened" }
Contact CorridorFl1_MUC_state "Corridor Fl1 window: [%s]" (Floor1MUC) { channel = "paradoxalarm:zone:ip150:MagneticSensorWindow1:opened" }
```
## Example sitemap configuration
```java
Text label="Security" icon="lock" {
Frame label="IP150 communication" {
Text item=panelState valuecolor=[panelState=="Online"="green", panelState=="Offline"="red"]
Selection item=paradoxSendCommand mappings=["LOGOUT"="Logout", "LOGIN"="Login", "RESET"="Reset"]
}
Frame label="Panel" {
Text item=paradoxTime
Text item=paradoxAcVoltage
Text item=paradoxDcVoltage
Text item=paradoxBatteryVoltage
}
Frame label="Partitions" {
Text item=partition1State valuecolor=[partition1State=="Disarmed"="green", partition1State=="Armed"="red"]
Selection item=partition1Command mappings=["ARM"="Arm", "FORCE_ARM"="Force Arm", "STAY_ARM"="Stay Arm", "INSTANT_ARM"="Instant Arm", "BEEP"="Keyboard Beep"]
}
Frame label="Zones" {
Group item=Floor1MUC
Group item=Floor2MUC
Group item=Floor3MUC
Group item=PIRSensors
}
}
```
## Acknowledgements
This binding would not be possible without the reverse engineering of the byte level protocol and the development by other authors in python, C# and other languages. Many thanks to the following authors and their respective GitHub repositories for their development that helped in creating this binding:
João Paulo Barraca - https://github.com/ParadoxAlarmInterface/pai
Jean Henning - repository not available
Tertuish - https://github.com/Tertiush/ParadoxIP150v2 / https://github.com/Tertiush/ParadoxIP150

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<groupId>org.openhab.addons.bundles</groupId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.paradoxalarm</artifactId>
<name>openHAB Add-ons :: Bundles :: ParadoxAlarm Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.paradoxalarm-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-paradoxalarm" description="Paradox Alarm System Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.paradoxalarm/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,207 @@
/**
* Copyright (c) 2010-2020 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 java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.ScheduledExecutorService;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AbstractCommunicator} Abstract class with common low-level communication logic. Extended by the
* communicator classes.
*
* @author Konstantin Polihronov - Initial contribution
*/
public abstract class AbstractCommunicator implements IParadoxInitialLoginCommunicator {
protected static final int SOCKET_TIMEOUT = 4000;
private static final long PACKET_EXPIRATION_TRESHOLD_MILLISECONDS = 2000;
private final Logger logger = LoggerFactory.getLogger(AbstractCommunicator.class);
protected ScheduledExecutorService scheduler;
protected Collection<IDataUpdateListener> listeners;
protected Socket socket;
private ISocketTimeOutListener stoListener;
private final String ipAddress;
private final int tcpPort;
private DataOutputStream tx;
private DataInputStream rx;
private boolean isOnline;
public AbstractCommunicator(String ipAddress, int tcpPort, ScheduledExecutorService scheduler)
throws UnknownHostException, IOException {
this.ipAddress = ipAddress;
this.tcpPort = tcpPort;
logger.debug("IP Address={}, TCP Port={}", ipAddress, tcpPort);
this.scheduler = scheduler;
initializeSocket();
}
protected void initializeSocket() throws IOException, UnknownHostException {
if (socket != null && !socket.isClosed()) {
close();
}
socket = new Socket(ipAddress, tcpPort);
socket.setSoTimeout(SOCKET_TIMEOUT);
tx = new DataOutputStream(socket.getOutputStream());
rx = new DataInputStream(socket.getInputStream());
}
@Override
public synchronized void close() {
logger.info("Stopping communication to Paradox system");
setOnline(false);
try {
tx.close();
rx.close();
socket.close();
} catch (IOException e) {
logger.warn("IO exception during socket/stream close operation.", e);
}
}
@Override
public void submitRequest(IRequest request) {
if (isEncrypted()) {
request.getRequestPacket().encrypt();
}
SyncQueue syncQueue = SyncQueue.getInstance();
syncQueue.add(request);
communicateToParadox();
}
protected void communicateToParadox() {
SyncQueue syncQueue = SyncQueue.getInstance();
synchronized (syncQueue) {
if (syncQueue.hasPacketToReceive()) {
receivePacket();
} else if (syncQueue.hasPacketsToSend()) {
sendPacket();
}
// Recursively check if there are more packets to send in TX queue until it becomes empty
if (syncQueue.hasPacketsToSend() || syncQueue.hasPacketToReceive()) {
communicateToParadox();
}
}
}
protected void sendPacket() {
SyncQueue syncQueue = SyncQueue.getInstance();
IRequest request = syncQueue.peekSendQueue();
try {
logger.trace("Sending packet with request={}", request);
byte[] packetBytes = request.getRequestPacket().getBytes();
ParadoxUtil.printPacket("Tx Packet:", packetBytes);
tx.write(packetBytes);
syncQueue.moveRequest();
} catch (SocketException e) {
logger.debug("Socket time out occurred. Informing listener. Request={}. Exception=", request, e);
syncQueue.removeSendRequest();
stoListener.onSocketTimeOutOccurred(e);
} catch (IOException e) {
logger.debug("Error while sending packet with request={}. IOException=", request, e);
syncQueue.removeSendRequest();
}
}
protected void receivePacket() {
SyncQueue syncQueue = SyncQueue.getInstance();
try {
logger.trace("Found packet to receive in queue...");
byte[] result = new byte[256];
int readBytes = rx.read(result);
if (readBytes > 0 && result[1] > 0 && result[1] + 16 < 256) {
logger.trace("Successfully read valid packet from Rx");
IRequest request = syncQueue.poll();
byte[] bytesData = Arrays.copyOfRange(result, 0, readBytes);
IResponse response = new Response(request, bytesData, isEncrypted());
if (response.getPayload() == null || response.getHeader() == null) {
handleWrongPacket(result, request);
}
IResponseReceiver responseReceiver = request.getResponseReceiver();
if (responseReceiver != null) {
responseReceiver.receiveResponse(response, this);
}
} else if (SyncQueue.getInstance().peekReceiveQueue()
.isTimeStampExpired(PACKET_EXPIRATION_TRESHOLD_MILLISECONDS)) {
logger.trace("Unable to receive proper package for {} time. Rescheduling...",
PACKET_EXPIRATION_TRESHOLD_MILLISECONDS);
} else {
IRequest requestInQueue = syncQueue.poll();
logger.debug("Error receiving packet after reaching the set timeout of {}ms. Request: {}",
PACKET_EXPIRATION_TRESHOLD_MILLISECONDS, requestInQueue);
}
} catch (SocketException e) {
IRequest request = syncQueue.poll();
logger.debug("Socket time out occurred. Informing listener. Request={}, SocketException=", request, e);
stoListener.onSocketTimeOutOccurred(e);
} catch (IOException e) {
IRequest request = syncQueue.poll();
logger.debug("Unable to receive package due to IO Exception. Request {}, IOException=", request, e);
}
}
protected void handleWrongPacket(byte[] result, IRequest request) throws IOException {
logger.trace(
"Payload or header are null. Probably unexpected package has been read. Need to retry read the same request.");
rx.read(result);
ParadoxUtil.printPacket("Flushing packet:", result);
submitRequest(request);
}
@Override
public boolean isOnline() {
return isOnline;
}
@Override
public void setOnline(boolean flag) {
this.isOnline = flag;
}
public DataInputStream getRx() {
return rx;
}
protected abstract void receiveEpromResponse(IResponse response);
protected abstract void receiveRamResponse(IResponse response);
public ISocketTimeOutListener getStoListener() {
return stoListener;
}
@Override
public void setStoListener(ISocketTimeOutListener stoListener) {
this.stoListener = stoListener;
}
}

View File

@@ -0,0 +1,421 @@
/**
* Copyright (c) 2010-2020 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 java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.paradoxalarm.internal.communication.crypto.EncryptionHandler;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderMessageType;
import org.openhab.binding.paradoxalarm.internal.communication.messages.IPPacket;
import org.openhab.binding.paradoxalarm.internal.communication.messages.IpMessagesConstants;
import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link CommunicationState}. This is enum based state machine used mostly for the logon process orchestration.
*
* @author Konstantin Polihronov - Initial contribution
*/
public enum CommunicationState implements IResponseReceiver {
START {
@Override
protected CommunicationState nextState() {
return STEP2;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
String password = communicator.getPassword();
logger.debug("Phase {}", this);
if (communicator.isEncrypted()) {
EncryptionHandler.getInstance().updateKey(ParadoxUtil.getBytesFromString(password));
}
ParadoxIPPacket packet = new ParadoxIPPacket(ParadoxUtil.getBytesFromString(password), false)
.setCommand(HeaderCommand.CONNECT_TO_IP_MODULE);
sendLogonPhasePacket(communicator, packet);
}
@Override
protected boolean isPhaseSuccess(IResponse response) {
byte payloadResponseByte = response.getPayload()[0];
if (payloadResponseByte == 0x00) {
logger.info("Login - Login to IP150 - OK");
return true;
}
byte headerResponseByte = response.getHeader()[4];
switch (headerResponseByte) {
case 0x30:
logger.warn("Login - Login to IP150 failed - Incorrect password");
break;
case 0x78:
case 0x79:
logger.warn("Login - IP module is busy");
break;
}
switch (payloadResponseByte) {
case 0x01:
logger.warn("Login - Invalid password");
break;
case 0x02:
case 0x04:
logger.warn("Login - User already connected");
break;
default:
logger.warn("Login - Connection refused");
}
return false;
}
@Override
public void receiveResponse(IResponse response, IParadoxInitialLoginCommunicator communicator) {
if (isPhaseSuccess(response)) {
// Retrieve new encryption key from first packet response and update it in encryption handler
if (communicator.isEncrypted()) {
byte[] payload = response.getPayload();
byte[] receivedKey = Arrays.copyOfRange(payload, 1, 17);
EncryptionHandler.getInstance().updateKey(receivedKey);
}
logger.debug("Phase {} completed successfully.", this);
nextState().runPhase(communicator);
}
}
},
STEP2 {
@Override
protected CommunicationState nextState() {
return STEP3;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
logger.debug("Phase {}", this);
ParadoxIPPacket packet = new ParadoxIPPacket(ParadoxIPPacket.EMPTY_PAYLOAD, false)
.setCommand(HeaderCommand.LOGIN_COMMAND1);
sendLogonPhasePacket(communicator, packet);
}
},
STEP3 {
@Override
protected CommunicationState nextState() {
return STEP4;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
logger.debug("Phase {}", this);
ParadoxIPPacket packet = new ParadoxIPPacket(ParadoxIPPacket.EMPTY_PAYLOAD, false)
.setCommand(HeaderCommand.LOGIN_COMMAND2);
sendLogonPhasePacket(communicator, packet);
}
},
STEP4 {
@Override
protected CommunicationState nextState() {
return STEP5;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
logger.debug("Phase {}", this);
byte[] message4 = new byte[37];
message4[0] = 0x72;
ParadoxIPPacket packet = new ParadoxIPPacket(message4, true)
.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST);
sendLogonPhasePacket(communicator, packet);
}
@Override
public void receiveResponse(IResponse response, IParadoxInitialLoginCommunicator communicator) {
byte[] payload = response.getPayload();
if (payload != null && payload.length >= 37) {
communicator.setPanelInfoBytes(payload);
logger.debug("Phase {} completed successfully.", this);
nextState().runPhase(communicator);
} else {
logger.warn("Received wrong response in phase {}. Response: {}", this, response);
LOGOUT.runPhase(communicator);
}
}
},
STEP5 {
@Override
protected CommunicationState nextState() {
return STEP6;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
logger.debug("Phase {}", this);
ParadoxIPPacket packet = new ParadoxIPPacket(IpMessagesConstants.UNKNOWN_IP150_REQUEST_MESSAGE01, false)
.setCommand(HeaderCommand.SERIAL_CONNECTION_INITIATED);
sendLogonPhasePacket(communicator, packet);
}
},
STEP6 {
@Override
protected CommunicationState nextState() {
return STEP7;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
logger.debug("Phase {}", this);
byte[] message6 = new byte[37];
message6[0] = 0x5F;
message6[1] = 0x20;
ParadoxIPPacket packet = new ParadoxIPPacket(message6, true)
.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST);
sendLogonPhasePacket(communicator, packet);
}
@Override
public void receiveResponse(IResponse response, IParadoxInitialLoginCommunicator communicator) {
byte[] payload = response.getPayload();
ParadoxUtil.printPacket("Init communication sub array: ", payload);
logger.debug("Phase {} completed successfully.", this);
nextState().runPhase(communicator, payload);
}
},
STEP7 {
@Override
protected CommunicationState nextState() {
return INITIALIZE_DATA;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
if (args != null && args.length == 1) {
byte[] initializationMessage = (byte[]) args[0];
logger.debug("Phase {}", this);
byte[] message7 = generateInitializationRequest(initializationMessage,
communicator.getPcPasswordBytes());
ParadoxIPPacket packet = new ParadoxIPPacket(message7, true)
.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST).setUnknown0((byte) 0x14);
sendLogonPhasePacket(communicator, packet);
} else {
logger.error("Error in step {}. Missing argument {}", this, args);
throw new IllegalArgumentException(
"Initialization message not send in request for phase + " + this + ". Arguments= " + args);
}
}
private byte[] generateInitializationRequest(byte[] initializationMessage, byte[] pcPassword) {
byte[] message7 = new byte[] {
// Initialization command
0x00,
// Module address
initializationMessage[1],
// Not used
0x00, 0x00,
// Product ID
initializationMessage[4],
// Software version
initializationMessage[5],
// Software revision
initializationMessage[6],
// Software ID
initializationMessage[7],
// Module ID
initializationMessage[8], initializationMessage[9],
// PC Password
pcPassword[0], pcPassword[1],
// Modem speed
0x08,
// Winload type ID
0x30,
// User code (aligned with PAI)
0x02, 0x02, 0x00,
// Module serial number
initializationMessage[17], initializationMessage[18], initializationMessage[19],
initializationMessage[20],
// EVO section 3030-3038 data
initializationMessage[21], initializationMessage[22], initializationMessage[23],
initializationMessage[24], initializationMessage[25], initializationMessage[26],
initializationMessage[27], initializationMessage[28], initializationMessage[29],
// Not used
0x00, 0x00, 0x00, 0x00,
// Source ID
0x00,
// Carrier length
0x00,
// Checksum
0x00 };
return message7;
}
@Override
public void receiveResponse(IResponse response, IParadoxInitialLoginCommunicator communicator) {
// UGLY - this is the handling of ghost packet which appears after the logon sequence
// Read ghost packet affter 300ms then continue with normal flow
communicator.getScheduler().schedule(() -> {
if (communicator instanceof GenericCommunicator) {
try {
GenericCommunicator genCommunicator = (GenericCommunicator) communicator;
byte[] value = new byte[256];
int packetLength = genCommunicator.getRx().read(value);
logger.debug("Reading ghost packet with length={}", packetLength);
ParadoxUtil.printPacket("Reading ghost packet", value);
} catch (IOException e) {
logger.debug("Error reading ghost packet.", e);
}
super.receiveResponse(response, communicator);
}
}, 300, TimeUnit.MILLISECONDS);
}
},
INITIALIZE_DATA {
@Override
protected CommunicationState nextState() {
return ONLINE;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
if (communicator instanceof IParadoxCommunicator) {
IParadoxCommunicator comm = (IParadoxCommunicator) communicator;
comm.initializeData();
}
nextState().runPhase(communicator);
}
},
ONLINE {
@Override
protected CommunicationState nextState() {
return this;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
logger.debug("Phase {}. Setting communicator to status ONLINE.", this);
communicator.setOnline(true);
logger.info("Successfully established communication with the panel.");
}
},
LOGOUT {
@Override
protected CommunicationState nextState() {
return OFFLINE;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
// For some reason after sending logout packet the connection gets reset from the other end
// currently workaround is to run directly offline phase, i.e. close socket from our end
// logger.info("Logout packet sent to IP150.");
// ParadoxIPPacket logoutPacket = new ParadoxIPPacket(IpMessagesConstants.LOGOUT_MESAGE_BYTES, true)
// .setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST).setUnknown0((byte) 0x14);
// sendPacket(communicator, logoutPacket);
nextState().runPhase(communicator);
}
},
OFFLINE {
@Override
protected CommunicationState nextState() {
return this;
}
@Override
protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args) {
if (communicator != null) {
communicator.close();
}
ParadoxPanel.getInstance().dispose();
}
};
protected final Logger logger = LoggerFactory.getLogger(CommunicationState.class);
private static CommunicationState currentState = CommunicationState.OFFLINE;
// This method is the entry of logon procedure.
public static void login(IParadoxInitialLoginCommunicator communicator) {
START.runPhase(communicator);
}
public static void logout(IParadoxInitialLoginCommunicator communicator) {
LOGOUT.runPhase(communicator);
}
protected abstract CommunicationState nextState();
protected abstract void runPhase(IParadoxInitialLoginCommunicator communicator, Object... args);
protected void runPhase(IParadoxInitialLoginCommunicator communicator) {
setCurrentState(this);
runPhase(communicator, new Object[0]);
}
protected void sendLogonPhasePacket(IParadoxInitialLoginCommunicator communicator, IPPacket packet) {
IRequest request = new LogonRequest(this, packet);
communicator.submitRequest(request);
}
@Override
public void receiveResponse(IResponse response, IParadoxInitialLoginCommunicator communicator) {
if (isPhaseSuccess(response)) {
logger.debug("Phase {} completed successfully.", this);
nextState().runPhase(communicator);
}
}
protected boolean isPhaseSuccess(IResponse response) {
return true;
}
public static CommunicationState getCurrentState() {
return currentState;
}
public static void setCurrentState(CommunicationState currentState) {
CommunicationState.currentState = currentState;
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 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;
import org.openhab.binding.paradoxalarm.internal.model.EntityType;
/**
* The {@link EpromRequest}. Request for retrieving EPROM data from Paradox system.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class EpromRequest extends Request {
private EntityType entityType;
private int entityId;
public EpromRequest(int entityId, EntityType entityType, IPPacket payload, IResponseReceiver receiver) {
super(RequestType.EPROM, payload, receiver);
this.entityId = entityId;
this.entityType = entityType;
}
public EntityType getEntityType() {
return entityType;
}
public int getEntityId() {
return entityId;
}
@Override
public String toString() {
return "EpromRequest [getType()=" + getType() + ", entityType=" + entityType + ", entityId=" + entityId + "]";
}
}

View File

@@ -0,0 +1,420 @@
/**
* Copyright (c) 2010-2020 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 java.io.IOException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.paradoxalarm.internal.communication.messages.EpromRequestPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderMessageType;
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.RamRequestPayload;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxException;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException;
import org.openhab.binding.paradoxalarm.internal.model.EntityType;
import org.openhab.binding.paradoxalarm.internal.model.PanelType;
import org.openhab.binding.paradoxalarm.internal.model.ZoneStateFlags;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link EvoCommunicator} is responsible for handling communication to Evo192 alarm system via IP150 interface.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class EvoCommunicator extends GenericCommunicator implements IParadoxCommunicator {
private static final byte RAM_BLOCK_SIZE = (byte) 64;
private final Logger logger = LoggerFactory.getLogger(EvoCommunicator.class);
private MemoryMap memoryMap;
private Map<EntityType, Map<Integer, String>> entityLabelsMap = new HashMap<>();
private PanelType panelType = PanelType.UNKNOWN;
private Integer maxPartitions;
private Integer maxZones;
private EvoCommunicator(String ipAddress, int tcpPort, String ip150Password, String pcPassword,
ScheduledExecutorService scheduler, PanelType panelType, Integer maxPartitions, Integer maxZones,
boolean useEncryption) throws UnknownHostException, IOException {
super(ipAddress, tcpPort, ip150Password, pcPassword, scheduler, useEncryption);
this.panelType = panelType;
this.maxPartitions = maxPartitions;
this.maxZones = maxZones;
logger.debug("PanelType={}, Max Partitions={}, Max Zones={}", panelType, maxPartitions, maxZones);
initializeMemoryMap();
}
@Override
protected void receiveEpromResponse(IResponse response) {
byte[] payload = response.getPayload();
if (payload != null) {
EpromRequest request = (EpromRequest) response.getRequest();
int entityId = request.getEntityId();
EntityType entityType = request.getEntityType();
updateEntityLabel(entityType, entityId, payload);
} else {
logger.debug("Wrong parsed result. Probably wrong data received in response. Response={}", response);
return;
}
}
@Override
protected void receiveRamResponse(IResponse response) {
byte[] payload = response.getPayload();
if (payload != null && payload.length >= RAM_BLOCK_SIZE) {
RamRequest request = (RamRequest) response.getRequest();
int ramBlockNumber = request.getRamBlockNumber();
memoryMap.updateElement(ramBlockNumber, payload);
if (logger.isTraceEnabled()) {
logger.trace("Result for ramBlock={} is [{}]", ramBlockNumber, ParadoxUtil.byteArrayToString(payload));
}
// Trigger listeners update when last memory page update is received
if (ramBlockNumber == panelType.getRamPagesNumber()) {
updateListeners();
}
} else {
logger.debug("Wrong parsed result. Probably wrong data received in response");
return;
}
}
private void updateEntityLabel(EntityType entityType, int entityId, byte[] payload) {
String label = createString(payload);
logger.debug("{} label updated to: {}", entityType, label);
entityLabelsMap.get(entityType).put(entityId, label);
}
private void retrievePartitionLabel(int partitionNo) {
logger.debug("Submitting request for partition label: {}", partitionNo);
int address = 0x3A6B + (partitionNo) * 107;
byte labelLength = 16;
try {
IPayload payload = new EpromRequestPayload(address, labelLength);
ParadoxIPPacket readEpromIPPacket = createSerialPassthroughPacket(payload);
IRequest epromRequest = new EpromRequest(partitionNo, EntityType.PARTITION, readEpromIPPacket, this);
submitRequest(epromRequest);
} catch (ParadoxException e) {
logger.debug("Error creating request for with number={}, Exception={} ", partitionNo, e.getMessage());
}
}
private ParadoxIPPacket createSerialPassthroughPacket(IPayload payload) {
ParadoxIPPacket packet = new ParadoxIPPacket(payload.getBytes());
packet.setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST).setUnknown0((byte) 0x14);
return packet;
}
private void retrieveZoneLabel(int zoneNumber) {
logger.debug("Submitting request for zone label: {}", zoneNumber);
final byte labelLength = 16;
try {
int address;
if (zoneNumber < 96) {
address = 0x430 + (zoneNumber) * 16;
} else {
address = 0x62F7 + (zoneNumber - 96) * 16;
}
IPayload payload = new EpromRequestPayload(address, labelLength);
ParadoxIPPacket readEpromIPPacket = createSerialPassthroughPacket(payload);
IRequest epromRequest = new EpromRequest(zoneNumber, EntityType.ZONE, readEpromIPPacket, this);
submitRequest(epromRequest);
} catch (ParadoxException e) {
logger.debug("Error creating request with number={}, Exception={} ", zoneNumber, e.getMessage());
}
}
@Override
public List<byte[]> getPartitionFlags() {
List<byte[]> result = new ArrayList<>();
byte[] element = memoryMap.getElement(2);
byte[] firstBlock = Arrays.copyOfRange(element, 32, 64);
element = memoryMap.getElement(3);
byte[] secondBlock = Arrays.copyOfRange(element, 0, 16);
byte[] mergeByteArrays = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
for (int i = 0; i < mergeByteArrays.length; i += 6) {
result.add(Arrays.copyOfRange(mergeByteArrays, i, i + 6));
}
return result;
}
@Override
public ZoneStateFlags getZoneStateFlags() {
ZoneStateFlags result = new ZoneStateFlags();
byte[] firstPage = memoryMap.getElement(0);
byte[] secondPage = memoryMap.getElement(8);
int pageOffset = panelType == PanelType.EVO48 ? 34 : 40;
byte[] firstBlock = Arrays.copyOfRange(firstPage, 28, pageOffset);
if (panelType != PanelType.EVO192) {
result.setZonesOpened(firstBlock);
} else {
byte[] secondBlock = Arrays.copyOfRange(secondPage, 0, 12);
byte[] zonesOpened = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
result.setZonesOpened(zonesOpened);
}
pageOffset = panelType == PanelType.EVO48 ? 46 : 52;
firstBlock = Arrays.copyOfRange(firstPage, 40, pageOffset);
if (panelType != PanelType.EVO192) {
result.setZonesTampered(firstBlock);
} else {
byte[] secondBlock = Arrays.copyOfRange(secondPage, 12, 24);
byte[] zonesTampered = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
result.setZonesTampered(zonesTampered);
}
pageOffset = panelType == PanelType.EVO48 ? 58 : 64;
firstBlock = Arrays.copyOfRange(firstPage, 52, pageOffset);
if (panelType != PanelType.EVO192) {
result.setZonesTampered(firstBlock);
} else {
byte[] secondBlock = Arrays.copyOfRange(secondPage, 24, 36);
byte[] zonesLowBattery = ParadoxUtil.mergeByteArrays(firstBlock, secondBlock);
result.setZonesLowBattery(zonesLowBattery);
}
return result;
}
public void initializeMemoryMap() {
for (EntityType type : EntityType.values()) {
entityLabelsMap.put(type, new HashMap<>());
}
List<byte[]> ramCache = new ArrayList<>(panelType.getRamPagesNumber() + 1);
for (int i = 0; i <= panelType.getRamPagesNumber(); i++) {
ramCache.add(new byte[0]);
}
memoryMap = new MemoryMap(ramCache);
}
@Override
public void refreshMemoryMap() {
if (!isOnline()) {
logger.debug("Attempt to refresh memory map was made, but communicator is not online. Skipping update.");
return;
}
SyncQueue queue = SyncQueue.getInstance();
synchronized (queue) {
for (int i = 1; i <= panelType.getRamPagesNumber(); i++) {
submitRamRequest(i);
}
}
}
private void submitRamRequest(int blockNo) {
try {
logger.trace("Creating RAM page {} read request", blockNo);
IPayload payload = new RamRequestPayload(blockNo, RAM_BLOCK_SIZE);
ParadoxIPPacket ipPacket = createSerialPassthroughPacket(payload);
IRequest ramRequest = new RamRequest(blockNo, ipPacket, this);
submitRequest(ramRequest);
} catch (ParadoxException e) {
logger.debug(
"Unable to create request payload from provided bytes to read. blockNo={}, bytes to read={}. Exception={}",
blockNo, RAM_BLOCK_SIZE, e.getMessage());
}
}
private String createString(byte[] payloadResult) {
return new String(payloadResult, StandardCharsets.US_ASCII);
}
@Override
public void executeCommand(String command) {
IP150Command ip150Command = IP150Command.valueOf(command);
switch (ip150Command) {
case LOGIN:
startLoginSequence();
return;
case LOGOUT:
CommunicationState.LOGOUT.runPhase(this);
return;
case RESET:
CommunicationState.LOGOUT.runPhase(this);
scheduler.schedule(this::startLoginSequence, 5, TimeUnit.SECONDS);
return;
default:
logger.debug("Command {} not implemented.", command);
}
}
@Override
public MemoryMap getMemoryMap() {
return memoryMap;
}
public Map<EntityType, Map<Integer, String>> getEntityLabelsMap() {
return entityLabelsMap;
}
@Override
public Map<Integer, String> getPartitionLabels() {
return entityLabelsMap.get(EntityType.PARTITION);
}
@Override
public Map<Integer, String> getZoneLabels() {
return entityLabelsMap.get(EntityType.ZONE);
}
@Override
public void initializeData() {
synchronized (SyncQueue.getInstance()) {
initializeEpromData();
refreshMemoryMap();
}
}
private void initializeEpromData() {
for (int i = 0; i < maxPartitions; i++) {
retrievePartitionLabel(i);
}
for (int i = 0; i < maxZones; i++) {
retrieveZoneLabel(i);
}
}
public static class EvoCommunicatorBuilder implements ICommunicatorBuilder {
private final Logger logger = LoggerFactory.getLogger(EvoCommunicatorBuilder.class);
// Mandatory parameters
private PanelType panelType;
private String ipAddress;
private String ip150Password;
private ScheduledExecutorService scheduler;
// Non mandatory or with predefined values
private Integer maxPartitions;
private Integer maxZones;
private int tcpPort = 10000;
private String pcPassword = "0000";
private boolean useEncryption;
EvoCommunicatorBuilder(PanelType panelType) {
this.panelType = panelType;
}
@Override
public IParadoxCommunicator build() {
if (ipAddress == null || ipAddress.isEmpty()) {
final String msg = "IP address cannot be empty !";
logger.debug(msg);
throw new ParadoxRuntimeException(msg);
}
if (ip150Password == null || ip150Password.isEmpty()) {
final String msg = "Password for IP150 cannot be empty !";
logger.debug(msg);
throw new ParadoxRuntimeException(msg);
}
if (scheduler == null) {
final String msg = "Scheduler is mandatory parameter !";
logger.debug(msg);
throw new ParadoxRuntimeException(msg);
}
if (maxPartitions == null || maxPartitions < 1) {
this.maxPartitions = panelType.getPartitions();
}
if (maxZones == null || maxZones < 1) {
this.maxZones = panelType.getZones();
}
try {
return new EvoCommunicator(ipAddress, tcpPort, ip150Password, pcPassword, scheduler, panelType,
maxPartitions, maxZones, useEncryption);
} catch (IOException e) {
logger.warn("Unable to create communicator for Panel={}. Message={}", panelType, e.getMessage());
throw new ParadoxRuntimeException(e);
}
}
@Override
public ICommunicatorBuilder withMaxZones(Integer maxZones) {
this.maxZones = maxZones;
return this;
}
@Override
public ICommunicatorBuilder withMaxPartitions(Integer maxPartitions) {
this.maxPartitions = maxPartitions;
return this;
}
@Override
public ICommunicatorBuilder withIp150Password(String ip150Password) {
this.ip150Password = ip150Password;
return this;
}
@Override
public ICommunicatorBuilder withPcPassword(String pcPassword) {
this.pcPassword = pcPassword;
return this;
}
@Override
public ICommunicatorBuilder withIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
return this;
}
@Override
public ICommunicatorBuilder withTcpPort(Integer tcpPort) {
this.tcpPort = tcpPort;
return this;
}
@Override
public ICommunicatorBuilder withScheduler(ScheduledExecutorService scheduler) {
this.scheduler = scheduler;
return this;
}
@Override
public ICommunicatorBuilder withEncryption(boolean useEncryption) {
this.useEncryption = useEncryption;
return this;
}
}
}

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2010-2020 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 java.io.IOException;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.concurrent.ScheduledExecutorService;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link GenericCommunicator} Used for the common communication logic for all types of panels.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class GenericCommunicator extends AbstractCommunicator implements IResponseReceiver {
private final Logger logger = LoggerFactory.getLogger(GenericCommunicator.class);
private final byte[] pcPasswordBytes;
private byte[] panelInfoBytes;
private boolean isEncrypted;
private final String password;
public GenericCommunicator(String ipAddress, int tcpPort, String ip150Password, String pcPassword,
ScheduledExecutorService scheduler, boolean useEncryption) throws UnknownHostException, IOException {
super(ipAddress, tcpPort, scheduler);
this.isEncrypted = useEncryption;
logger.debug("Use encryption={}", isEncrypted);
this.password = ip150Password;
this.pcPasswordBytes = ParadoxUtil.stringToBCD(pcPassword);
}
@Override
public synchronized void startLoginSequence() {
logger.debug("Login sequence started");
if (isOnline()) {
logger.debug("Already logged on. No action needed. Returning.");
return;
}
if (socket.isClosed()) {
try {
initializeSocket();
} catch (IOException e) {
throw new ParadoxRuntimeException(e);
}
}
CommunicationState.login(this);
}
@Override
public byte[] getPanelInfoBytes() {
return panelInfoBytes;
}
@Override
public void setPanelInfoBytes(byte[] panelInfoBytes) {
this.panelInfoBytes = panelInfoBytes;
}
@Override
public byte[] getPcPasswordBytes() {
return pcPasswordBytes;
}
@Override
protected void receiveEpromResponse(IResponse response) {
// Nothing to do here. Override in particular implementation class.
}
@Override
protected void receiveRamResponse(IResponse response) {
// Nothing to do here. Override in particular implementation class.
}
public void refreshMemoryMap() {
// Nothing to do here. Override in particular implementation class.
}
@Override
public ScheduledExecutorService getScheduler() {
return scheduler;
}
@Override
public void setListeners(Collection<IDataUpdateListener> listeners) {
this.listeners = listeners;
}
@Override
public void updateListeners() {
if (listeners != null && !listeners.isEmpty()) {
listeners.forEach(IDataUpdateListener::update);
}
}
@Override
public boolean isEncrypted() {
return isEncrypted;
}
@Override
public String getPassword() {
return password;
}
@Override
public void receiveResponse(IResponse response, IParadoxInitialLoginCommunicator communicator) {
IRequest request = response.getRequest();
logger.trace("Handling response for request={}", request);
ParadoxUtil.printPacket("Full packet", response.getPacketBytes());
RequestType type = request.getType();
if (type == RequestType.RAM) {
receiveRamResponse(response);
} else if (type == RequestType.EPROM) {
receiveEpromResponse(response);
} else {
logger.debug("Probably wrong sender in the request. Request type is not one of the supported methods.");
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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 java.util.concurrent.ScheduledExecutorService;
/**
* The {@link ICommunicatorBuilder} is representing the functionality of communicator builders.
* The idea is to ease initialization of communicators which can have lots of parameters.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface ICommunicatorBuilder {
ICommunicatorBuilder withMaxZones(Integer zones);
ICommunicatorBuilder withMaxPartitions(Integer partitions);
ICommunicatorBuilder withIp150Password(String ip150Password);
ICommunicatorBuilder withPcPassword(String pcPassword);
ICommunicatorBuilder withIpAddress(String ipAddress);
ICommunicatorBuilder withTcpPort(Integer tcpPort);
ICommunicatorBuilder withScheduler(ScheduledExecutorService scheduler);
ICommunicatorBuilder withEncryption(boolean useEncryption);
IParadoxCommunicator build();
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link IConnectionHandler} is base communication interface which defines only the basic communication level.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface IConnectionHandler {
void close();
boolean isOnline();
void setOnline(boolean flag);
void submitRequest(IRequest request);
boolean isEncrypted();
/**
* @param stoListener This method sets a listener which is called in case of socket timeout occurrence.
*/
void setStoListener(ISocketTimeOutListener stoListener);
}

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link IDataUpdateListener} - Listener interface used to provide updates when data is retrieved.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface IDataUpdateListener {
void update();
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link IParadoxCommunicator} is representing the functionality of communication implementation.
* If another Paradox alarm system is used this interface must be implemented.
*
* @author Konstantin Polihronov - Initial contribution
*/
public enum IP150Command {
LOGOUT,
LOGIN,
RESET,
UNIMPLEMENTED
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2020 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 java.util.List;
import java.util.Map;
import org.openhab.binding.paradoxalarm.internal.model.ZoneStateFlags;
/**
* The {@link IParadoxCommunicator} is representing the functionality of communication implementation.
* If another Paradox alarm system is used this interface must be implemented.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface IParadoxCommunicator extends IParadoxInitialLoginCommunicator {
void refreshMemoryMap();
List<byte[]> getPartitionFlags();
ZoneStateFlags getZoneStateFlags();
void executeCommand(String commandAsString);
Map<Integer, String> getPartitionLabels();
Map<Integer, String> getZoneLabels();
void initializeData();
MemoryMap getMemoryMap();
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 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 java.util.Collection;
import java.util.concurrent.ScheduledExecutorService;
/**
* The {@link IParadoxInitialLoginCommunicator} is representing the functionality of generic communication. Only
* login/logout
* sequence which is used to determine the Panel type.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface IParadoxInitialLoginCommunicator extends IConnectionHandler {
void startLoginSequence();
byte[] getPanelInfoBytes();
void setPanelInfoBytes(byte[] panelInfoBytes);
/**
* @return IP150 connection password
*/
String getPassword();
byte[] getPcPasswordBytes();
ScheduledExecutorService getScheduler();
void setListeners(Collection<IDataUpdateListener> listeners);
void updateListeners();
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link IRequest} - interface definition for the request used in the communication.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface IRequest {
IResponseReceiver getResponseReceiver();
IPPacket getRequestPacket();
void setTimeStamp();
boolean isTimeStampExpired(long expirationTreshold);
RequestType getType();
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link IResponse} - interface for the response used in communication on the way back.
*
* @author Konstantin Polihronov - Initial contribution
*/
interface IResponse {
RequestType getType();
IRequest getRequest();
byte[] getPacketBytes();
byte[] getHeader();
byte[] getPayload();
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link IResponseReceiver} Used to pass parsed responses from Paradox to original senders of the requests for
* further processing.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public interface IResponseReceiver {
void receiveResponse(IResponse response, IParadoxInitialLoginCommunicator communicator);
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 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 java.io.IOException;
/**
* The {@link ISocketTimeOutListener} Listener interface for informing listeners who may be interested and act in case
* of socket time out occurs.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface ISocketTimeOutListener {
void onSocketTimeOutOccurred(IOException exception);
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link LogonRequest}. Request for initial logon sequence.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class LogonRequest extends Request {
private CommunicationState logonSequenceSender;
public LogonRequest(CommunicationState logonSequenceSender, IPPacket payload) {
super(RequestType.LOGON_SEQUENCE, payload, logonSequenceSender);
this.logonSequenceSender = logonSequenceSender;
}
public CommunicationState getLogonSequenceSender() {
return logonSequenceSender;
}
@Override
public String toString() {
return "LogonRequest [getType()=" + getType() + ", getLogonSequenceSender()=" + getLogonSequenceSender() + "]";
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 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 java.util.ArrayList;
import java.util.List;
/**
* The {@link MemoryMap} this keeps Paradox RAM map as cached object inside the communicator.
* Every record in the list is byte array which contains 64 byte RAM page.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class MemoryMap {
private List<byte[]> ramCache = new ArrayList<>();
public MemoryMap(List<byte[]> ramCache) {
this.ramCache = ramCache;
}
public List<byte[]> getRamCache() {
return ramCache;
}
public void setRamCache(List<byte[]> ramCache) {
this.ramCache = ramCache;
}
public synchronized byte[] getElement(int index) {
return ramCache.get(index);
}
public synchronized void updateElement(int index, byte[] elementValue) {
ramCache.set(index - 1, elementValue);
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2020 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.exceptions.ParadoxRuntimeException;
import org.openhab.binding.paradoxalarm.internal.model.PanelType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxBuilderFactory} used to create the proper communicator builder objects for different panel
* types.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxBuilderFactory {
private final Logger logger = LoggerFactory.getLogger(ParadoxBuilderFactory.class);
public ICommunicatorBuilder createBuilder(PanelType panelType) {
switch (panelType) {
case EVO48:
case EVO96:
case EVO192:
case EVOHD:
logger.debug("Creating new builder for Paradox {} system", panelType);
return new EvoCommunicator.EvoCommunicatorBuilder(panelType);
default:
logger.debug("Unsupported panel type: {}", panelType);
throw new ParadoxRuntimeException("Unsupported panel type: " + panelType);
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link PartitionCommandRequest} Request object for wrapping partition command.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class PartitionCommandRequest extends Request {
public PartitionCommandRequest(RequestType type, IPPacket packet, IResponseReceiver receiver) {
super(type, packet, receiver);
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link RamRequest}. Request for retrieving RAM pages from Paradox system.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class RamRequest extends Request {
private int ramBlockNumber;
public RamRequest(int ramBlockNumber, IPPacket payload, IResponseReceiver receiver) {
super(RequestType.RAM, payload, receiver);
this.ramBlockNumber = ramBlockNumber;
}
public int getRamBlockNumber() {
return ramBlockNumber;
}
@Override
public String toString() {
return "RamRequest [getType()=" + getType() + ", getRamBlockNumber()=" + getRamBlockNumber() + "]";
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link Request}. Abstract request class. Used to be derived for the particular types of requests to Paradox.
*
* @author Konstantin Polihronov - Initial contribution
*/
public abstract class Request implements IRequest {
private IPPacket packet;
private long timestamp;
private RequestType type;
private IResponseReceiver receiver;
public Request(RequestType type, IPPacket packet, IResponseReceiver receiver) {
this.packet = packet;
this.type = type;
this.receiver = receiver;
}
@Override
public IPPacket getRequestPacket() {
return packet;
}
@Override
public void setTimeStamp() {
timestamp = System.currentTimeMillis();
}
@Override
public boolean isTimeStampExpired(long tresholdInMillis) {
return System.currentTimeMillis() - timestamp >= tresholdInMillis;
}
@Override
public RequestType getType() {
return type;
}
@Override
public String toString() {
return "Request [packet=" + packet + ", timestamp=" + timestamp + ", type=" + type + "]";
}
@Override
public IResponseReceiver getResponseReceiver() {
return receiver;
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link RequestType}. Enum with possible request types to Paradox system.
*
* @author Konstantin Polihronov - Initial contribution
*/
public enum RequestType {
LOGON_SEQUENCE,
RAM,
EPROM,
PARTITION_COMMAND
}

View File

@@ -0,0 +1,144 @@
/**
* Copyright (c) 2010-2020 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 java.util.Arrays;
import org.openhab.binding.paradoxalarm.internal.communication.crypto.EncryptionHandler;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Response}. The response which is returned after receiving data from socket.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class Response implements IResponse {
private final Logger logger = LoggerFactory.getLogger(Response.class);
private IRequest request;
private byte[] packetBytes;
private byte[] header;
private byte[] payload;
public Response(IRequest request, byte[] content, boolean useEncryption) {
this.request = request;
this.packetBytes = content;
ParadoxUtil.printPacket("Rx packet", packetBytes);
if (useEncryption) {
decrypt();
}
parsePacket();
}
@Override
public RequestType getType() {
return request.getType();
}
@Override
public byte[] getPacketBytes() {
return packetBytes;
}
@Override
public byte[] getPayload() {
return payload;
}
@Override
public byte[] getHeader() {
return header;
}
@Override
public IRequest getRequest() {
return request;
}
public void updatePayload(byte[] payload) {
this.packetBytes = payload;
}
private void decrypt() {
byte[] payloadBytes = Arrays.copyOfRange(packetBytes, 16, packetBytes.length);
logger.trace("DECRYPTING. Full packet length={}", packetBytes.length);
EncryptionHandler handler = EncryptionHandler.getInstance();
byte[] decrypted = handler.decrypt(payloadBytes);
header = Arrays.copyOfRange(packetBytes, 0, 16);
payload = Arrays.copyOfRange(decrypted, 0, header[1]);
packetBytes = ParadoxUtil.mergeByteArrays(header, payload);
ParadoxUtil.printByteArray("Decrypted package=", packetBytes, packetBytes.length);
}
/**
* This method parses data from the IP150 module.
* A panel command, e.g. 0x5
* A logon sequence part is starting with 0x0, 0x1 or 0x7
* We ignore invalid packets which do not match our pattern.
*
*/
private void parsePacket() {
// Message too short
if (packetBytes.length < 17) {
logger.debug("Message length is too short. Length={}", packetBytes.length);
return;
}
byte receivedCommand = packetBytes[16];
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;
}
// 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:
header = Arrays.copyOfRange(packetBytes, 0, 16);
payload = Arrays.copyOfRange(packetBytes, 16, packetBytes.length);
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;
}
}
// 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);
header = null;
payload = null;
}
@Override
public String toString() {
return "Response [request=" + request + ", packetBytes=" + ParadoxUtil.byteArrayToString(packetBytes) + "]";
}
}

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2010-2020 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 java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SyncQueue} is used to synchronize communication to/from Paradox system. All requests go into sendQueue and
* upon send are popped from send queue and are pushed into receiveQueue.
* Due to nature of Paradox communication receive queue is with priority, i.e. if there is anything in receive queue we
* attempt to read the socket first and only after receive queue is empty then we attempt to send. We never send any
* packet if we have something to read.
* For more details about usage see method {@link AbstractCommunicator.submitRequest()}
*
* @author Konstantin Polihronov - Initial contribution
*/
public class SyncQueue {
private final Logger logger = LoggerFactory.getLogger(SyncQueue.class);
private BlockingQueue<IRequest> sendQueue = new ArrayBlockingQueue<>(1000, true);
private BlockingQueue<IRequest> receiveQueue = new ArrayBlockingQueue<>(10, true);
private static SyncQueue syncQueue;
private SyncQueue() {
}
public static SyncQueue getInstance() {
SyncQueue temp = syncQueue;
if (temp == null) {
synchronized (SyncQueue.class) {
temp = syncQueue;
if (temp == null) {
syncQueue = new SyncQueue();
}
}
}
return syncQueue;
}
public synchronized void add(IRequest request) {
logger.trace("Adding to queue request={}", request);
sendQueue.add(request);
}
public synchronized void moveRequest() {
IRequest request = sendQueue.poll();
request.setTimeStamp();
logger.trace("Moving from Tx to RX queue request={}", request);
receiveQueue.add(request);
}
public synchronized IRequest poll() {
IRequest request = receiveQueue.poll();
logger.trace("Removing from queue request={}", request);
return request;
}
public synchronized IRequest removeSendRequest() {
IRequest request = sendQueue.poll();
logger.trace("Removing from queue request={}", request);
return request;
}
public synchronized IRequest peekSendQueue() {
return sendQueue.peek();
}
public IRequest peekReceiveQueue() {
return receiveQueue.peek();
}
public synchronized boolean hasPacketToReceive() {
return receiveQueue.peek() != null;
}
public synchronized boolean hasPacketsToSend() {
return sendQueue.peek() != null;
}
public synchronized boolean canSend() {
return receiveQueue.isEmpty();
}
}

View File

@@ -0,0 +1,285 @@
/**
* Copyright (c) 2010-2020 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.crypto;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is used to encrypt and decrypt communication from/to Paradox system. Singleton pattern.
*
* Paradox encryption is using Rijndael 256-key expansion alghoritm.
* When key is changed the updateKey(byte[]) method needs to be called in front.
* Encrypt and Decrypt methods use the expandedKey field to do their job.
*
* The first packet sent to Paradox is the IP150 password as bytes, extended to 32 bytes with 0xEE.
* The first response contains the key that will be used for the rest of communication.
*
* Most of the coding is copy from Python / rewrite in Java from second link of jpbaracca's PAI repository. Probably
* some of the variables can be named better but I don't understand this code in it's full scope so I preferred to keep
* it as it is.
*
* @author Konstantin Polihronov - Initial contribution
*
* @see <a href=https://www.samiam.org/key-schedule.html>Sam Trendholme's page about AES</a>
* @see <a href=https://github.com/ParadoxAlarmInterface/pai>Github of jpbaracca's work - ParadoxAlarmInterface in
* python</a>
*/
@NonNullByDefault
public class EncryptionHandler {
private final Logger logger = LoggerFactory.getLogger(EncryptionHandler.class);
private static final int KEY_ARRAY_LENGTH = 32;
private static final int TABLE_SIZE = 256;
private static final int KEY_LENGTH = 240;
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 EncryptionHandler instance = new EncryptionHandler(new byte[] {});
static {
generateTables();
}
private static void generateTables() {
int a = 1;
int d;
for (int index = 0; index < 255; index++) {
aTable[index] = a & 0xFF;
/* Multiply by three */
d = (a & 0x80) & 0xFF;
a <<= 1;
if (d == 0x80) {
a ^= 0x1b;
a &= 0xFF;
}
a ^= aTable[index];
a &= 0xFF;
/* Set the log table value */
lTable[aTable[index]] = index & 0xFF;
}
aTable[255] = aTable[0];
lTable[0] = 0;
}
private final int[] expandedKey = new int[KEY_LENGTH];
private EncryptionHandler(byte[] newKey) {
if (newKey.length > 0) {
expandKey(newKey);
}
}
public static EncryptionHandler getInstance() {
return instance;
}
public synchronized EncryptionHandler updateKey(byte[] newKey) {
instance = new EncryptionHandler(newKey);
return instance;
}
public byte[] encrypt(byte[] payload) {
if (payload.length % 16 != 0) {
payload = ParadoxUtil.extendArray(payload, PAYLOAD_RATE_LENGTH);
printArray("Array had to be extended:", payload);
logger.trace("New payload length={}", payload.length);
}
int[] payloadAsIntArray = ParadoxUtil.toIntArray(payload);
final int[] s = EncryptionHandlerConstants.S;
byte[] result = new byte[0];
for (int i = 0; i < payloadAsIntArray.length / 16; i++) {
int[] tempArray = Arrays.copyOfRange(payloadAsIntArray, i * 16, (i + 1) * 16);
keyAddition(tempArray, 0);
for (int r = 1; r <= ROUNDS; r++) {
sBox(tempArray, s);
shiftRow(tempArray, 0);
if (r != ROUNDS) {
mixColumn(tempArray);
}
keyAddition(tempArray, r * 16);
}
result = ParadoxUtil.mergeByteArrays(result, ParadoxUtil.toByteArray(tempArray));
}
printArray("Encrypted array", result);
return result;
}
public byte[] decrypt(byte[] payload) {
int[] payloadAsIntArray = ParadoxUtil.toIntArray(payload);
final int[] si = EncryptionHandlerConstants.Si;
byte[] result = new byte[0];
for (int i = 0; i < payloadAsIntArray.length / 16; i++) {
int[] tempArray = Arrays.copyOfRange(payloadAsIntArray, i * 16, (i + 1) * 16);
for (int r = ROUNDS; r > 0; r--) {
keyAddition(tempArray, r * 16);
if (r != ROUNDS) {
invMixColumn(tempArray);
}
sBox(tempArray, si);
shiftRow(tempArray, 1);
}
keyAddition(tempArray, 0);
result = ParadoxUtil.mergeByteArrays(result, ParadoxUtil.toByteArray(tempArray));
}
printArray("Decrypted array", result);
return result;
}
private void printArray(String description, byte[] array) {
ParadoxUtil.printByteArray(description, array, array.length);
}
private byte[] fillArray(byte[] keyBytes) {
byte[] byteArray = new byte[keyBytes.length];
for (int i = 0; i < keyBytes.length; i++) {
byteArray[i] = (byte) (keyBytes[i] & 0xFF);
}
byte[] expandedArray = ParadoxUtil.extendArray(byteArray, KEY_ARRAY_LENGTH);
return expandedArray;
}
private void expandKey(byte[] input) {
// fill array to 32th byte with 0xEE
byte[] filledArray = fillArray(input);
int[] temp = { 0, 0, 0, 0 };
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
expandedKey[j * 4 + i] = filledArray[i * 4 + j] & 0xFF;
}
for (int j = 0; j < 4; j++) {
expandedKey[j * 4 + i + 16] = filledArray[i * 4 + j + 16] & 0xFF;
}
}
final int[] s = EncryptionHandlerConstants.S;
for (int i = 8; i < 60; i++) {
for (int j = 0; j < 4; j++) {
temp[j] = expandedKey[(((i - 1) & 0xfc) << 2) + ((i - 1) & 0x03) + j * 4];
}
if (i % 4 == 0) {
for (int j = 0; j < 4; j++) {
temp[j] = s[temp[j]];
}
}
if (i % 8 == 0) {
int tmp = temp[0];
for (int j = 1; j < 4; j++) {
temp[j - 1] = temp[j];
}
temp[3] = tmp;
temp[0] ^= EncryptionHandlerConstants.RCON[(i / 8 - 1)];
}
for (int j = 0; j < 4; j++) {
expandedKey[((i & 0xfc) << 2) + (i & 0x03)
+ j * 4] = expandedKey[(((i - 8) & 0xfc) << 2) + ((i - 8) & 0x03) + j * 4] ^ temp[j];
}
}
}
private int gmul(int c, int b) {
int s = lTable[c] + lTable[b];
s %= 255;
s = aTable[s];
if (b == 0 || c == 0) {
s = 0;
}
return s;
}
private void sBox(int[] a, int[] box) {
for (int i = 0; i < 16; i++) {
a[i] = box[a[i]];
}
}
private void mixColumn(int[] a) {
final int[] xtimetbl = EncryptionHandlerConstants.XTIMETABLE;
int[] b = new int[] { 0, 0, 0, 0 };
for (int j = 0; j < 4; j++) {
int tmp = a[j] ^ a[j + 4] ^ a[j + 8] ^ a[j + 12];
for (int i = 0; i < 4; i++) {
b[i] = a[i * 4 + j];
}
b[0] ^= xtimetbl[a[j] ^ a[j + 4]] ^ tmp;
b[1] ^= xtimetbl[a[j + 4] ^ a[j + 8]] ^ tmp;
b[2] ^= xtimetbl[a[j + 8] ^ a[j + 12]] ^ tmp;
b[3] ^= xtimetbl[a[j + 12] ^ a[j]] ^ tmp;
for (int i = 0; i < 4; i++) {
a[i * 4 + j] = b[i];
}
}
}
private void invMixColumn(int[] a) {
int[][] b = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } };
for (int j = 0; j < 4; j++) {
for (int i = 0; i < 4; i++) {
b[i][j] = gmul(0xe, a[i * 4 + j]) ^ gmul(0xb, a[((i + 1) % 4) * 4 + j])
^ gmul(0xd, a[((i + 2) % 4) * 4 + j]) ^ gmul(0x9, a[((i + 3) % 4) * 4 + j]);
}
}
for (int j = 0; j < 4; j++) {
for (int i = 0; i < 4; i++) {
a[i * 4 + j] = b[i][j];
}
}
}
private void shiftRow(int[] a, int d) {
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;
tmpArray[j] = a[index];
}
for (int j = 0; j < 4; j++) {
a[i * 4 + j] = tmpArray[j];
}
}
}
private void keyAddition(int[] result, int startIndex) {
for (int i = 0; i < 16; i++) {
result[i] ^= expandedKey[i + startIndex];
}
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 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.crypto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
*
* Constant
*
* @author Konstantin Polihronov - Initial contribution
*
* @see <a href=https://www.samiam.org/key-schedule.html>Sam Trendholme's page about AES</a>
*/
@NonNullByDefault
public class EncryptionHandlerConstants {
protected static final int[] S = { 99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202,
130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247,
204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178,
117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177,
91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159,
168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151,
68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213,
78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75,
189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17,
105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45,
15, 176, 84, 187, 22 };
public static final int[] Si = { 82, 9, 106, 213, 48, 54, 165, 56, 191, 64, 163, 158, 129, 243, 215, 251, 124, 227,
57, 130, 155, 47, 255, 135, 52, 142, 67, 68, 196, 222, 233, 203, 84, 123, 148, 50, 166, 194, 35, 61, 238,
76, 149, 11, 66, 250, 195, 78, 8, 46, 161, 102, 40, 217, 36, 178, 118, 91, 162, 73, 109, 139, 209, 37, 114,
248, 246, 100, 134, 104, 152, 22, 212, 164, 92, 204, 93, 101, 182, 146, 108, 112, 72, 80, 253, 237, 185,
218, 94, 21, 70, 87, 167, 141, 157, 132, 144, 216, 171, 0, 140, 188, 211, 10, 247, 228, 88, 5, 184, 179, 69,
6, 208, 44, 30, 143, 202, 63, 15, 2, 193, 175, 189, 3, 1, 19, 138, 107, 58, 145, 17, 65, 79, 103, 220, 234,
151, 242, 207, 206, 240, 180, 230, 115, 150, 172, 116, 34, 231, 173, 53, 133, 226, 249, 55, 232, 28, 117,
223, 110, 71, 241, 26, 113, 29, 41, 197, 137, 111, 183, 98, 14, 170, 24, 190, 27, 252, 86, 62, 75, 198, 210,
121, 32, 154, 219, 192, 254, 120, 205, 90, 244, 31, 221, 168, 51, 136, 7, 199, 49, 177, 18, 16, 89, 39, 128,
236, 95, 96, 81, 127, 169, 25, 181, 74, 13, 45, 229, 122, 159, 147, 201, 156, 239, 160, 224, 59, 77, 174,
42, 245, 176, 200, 235, 187, 60, 131, 83, 153, 97, 23, 43, 4, 126, 186, 119, 214, 38, 225, 105, 20, 99, 85,
33, 12, 125 };
protected static final int[][][] SHIFTS = { { { 0, 0 }, { 1, 3 }, { 2, 2 }, { 3, 1 } },
{ { 0, 0 }, { 1, 5 }, { 2, 4 }, { 3, 3 } }, { { 0, 0 }, { 1, 7 }, { 3, 5 }, { 4, 4 } } };
public static final int[] XTIMETABLE = { 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x10, 0x12, 0x14, 0x16,
0x18, 0x1a, 0x1c, 0x1e, 0x20, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2c, 0x2e, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3a,
0x3c, 0x3e, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4a, 0x4c, 0x4e, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5a, 0x5c, 0x5e,
0x60, 0x62, 0x64, 0x66, 0x68, 0x6a, 0x6c, 0x6e, 0x70, 0x72, 0x74, 0x76, 0x78, 0x7a, 0x7c, 0x7e, 0x80, 0x82,
0x84, 0x86, 0x88, 0x8a, 0x8c, 0x8e, 0x90, 0x92, 0x94, 0x96, 0x98, 0x9a, 0x9c, 0x9e, 0xa0, 0xa2, 0xa4, 0xa6,
0xa8, 0xaa, 0xac, 0xae, 0xb0, 0xb2, 0xb4, 0xb6, 0xb8, 0xba, 0xbc, 0xbe, 0xc0, 0xc2, 0xc4, 0xc6, 0xc8, 0xca,
0xcc, 0xce, 0xd0, 0xd2, 0xd4, 0xd6, 0xd8, 0xda, 0xdc, 0xde, 0xe0, 0xe2, 0xe4, 0xe6, 0xe8, 0xea, 0xec, 0xee,
0xf0, 0xf2, 0xf4, 0xf6, 0xf8, 0xfa, 0xfc, 0xfe, 0x1b, 0x19, 0x1f, 0x1d, 0x13, 0x11, 0x17, 0x15, 0x0b, 0x09,
0x0f, 0x0d, 0x03, 0x01, 0x07, 0x05, 0x3b, 0x39, 0x3f, 0x3d, 0x33, 0x31, 0x37, 0x35, 0x2b, 0x29, 0x2f, 0x2d,
0x23, 0x21, 0x27, 0x25, 0x5b, 0x59, 0x5f, 0x5d, 0x53, 0x51, 0x57, 0x55, 0x4b, 0x49, 0x4f, 0x4d, 0x43, 0x41,
0x47, 0x45, 0x7b, 0x79, 0x7f, 0x7d, 0x73, 0x71, 0x77, 0x75, 0x6b, 0x69, 0x6f, 0x6d, 0x63, 0x61, 0x67, 0x65,
0x9b, 0x99, 0x9f, 0x9d, 0x93, 0x91, 0x97, 0x95, 0x8b, 0x89, 0x8f, 0x8d, 0x83, 0x81, 0x87, 0x85, 0xbb, 0xb9,
0xbf, 0xbd, 0xb3, 0xb1, 0xb7, 0xb5, 0xab, 0xa9, 0xaf, 0xad, 0xa3, 0xa1, 0xa7, 0xa5, 0xdb, 0xd9, 0xdf, 0xdd,
0xd3, 0xd1, 0xd7, 0xd5, 0xcb, 0xc9, 0xcf, 0xcd, 0xc3, 0xc1, 0xc7, 0xc5, 0xfb, 0xf9, 0xff, 0xfd, 0xf3, 0xf1,
0xf7, 0xf5, 0xeb, 0xe9, 0xef, 0xed, 0xe3, 0xe1, 0xe7, 0xe5 };
public static final int[] RCON = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab,
0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 };
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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 java.nio.ByteBuffer;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link CommandPayload} Class that structures the payload for partition commands.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class CommandPayload 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 final int partitionNumber;
private final PartitionCommand command;
public CommandPayload(int partitionNumber, PartitionCommand command) {
this.partitionNumber = partitionNumber;
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(EMPTY_FOUR_BYTES);
buf.put(calculateMessageBytes());
buf.put(EMPTY_FOUR_BYTES);
buf.put(CHECKSUM);
return bufferArray;
}
/*
* The message bytes contain nibbles of command information. First byte, first nibble is partition 1, first byte,
* second nibble is partition 2, second byte, first nibble is partition 3, etc...
*
* For command values that are set in byte nibbles, see PartitionCommand enum
*/
private byte[] calculateMessageBytes() {
byte[] result = { 0, 0, 0, 0 };
int index = (partitionNumber - 1) / 2;
result[index] = (byte) (calculateNibbleToSet() & 0xff);
return result;
}
private int calculateNibbleToSet() {
if ((partitionNumber - 1) % 2 == 0) {
return (command.getCommand() << 4) & 0xF0;
} else {
return command.getCommand() & 0x0F;
}
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxException;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link EpromRequestPayload} Object representing payload of IP packet which retrieves data from Paradox EPROM
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class EpromRequestPayload extends MemoryRequestPayload implements IPayload {
private final Logger logger = LoggerFactory.getLogger(EpromRequestPayload.class);
public EpromRequestPayload(int address, byte bytesToRead) throws ParadoxException {
super(address, bytesToRead);
}
@Override
protected byte calculateControlByte() {
int address = getAddress();
logTraceHexFormatted("Address: {}", address);
byte controlByte = 0x00;
byte[] shortToByteArray = ParadoxUtil.intToByteArray(address);
if (shortToByteArray.length > 2) {
byte bit16 = ParadoxUtil.getBit(address, 16);
controlByte |= bit16 << 0;
byte bit17 = ParadoxUtil.getBit(address, 17);
controlByte |= bit17 << 1;
}
logger.trace("ControlByte value: {}", controlByte);
return controlByte;
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link HeaderCommand}
* From Jean's(Jean_Henning from community) excel sheet:
* 0x00: Serial/pass through command any other: IP module command
* 0xF0: Connect to IP module
* 0xF2: (unknown, part of login sequence)
* 0xF3: (unknown, part of login sequence)
* 0xF4: (unknown)
* 0xF8: (unknown, occurs after serial connection is initiated with the panel)
* 0xFB: Multicommand
* 0xFF: Disconnect from IP module (byte 00 in the response payload MUST be 01 to indicate a successful disconnect)
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public enum HeaderCommand {
SERIAL((byte) 0x00),
CONNECT_TO_IP_MODULE((byte) 0xF0),
LOGIN_COMMAND1((byte) 0xF2),
LOGIN_COMMAND2((byte) 0xF3),
UNKNOWN1((byte) 0xF4),
SERIAL_CONNECTION_INITIATED((byte) 0xF8),
MULTI_COMMAND((byte) 0xFB),
DISCONNECT((byte) 0xFF);
private byte value;
HeaderCommand(byte value) {
this.value = value;
}
public byte getValue() {
return value;
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jdt.annotation.NonNullByDefault;
/**
* From Jean's excel:
* 0x03: IP Request
* 0x01: IP Response
* 0x04: Serial/pass through command request
* 0x02: Serial/pass through command response
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public enum HeaderMessageType {
IP_REQUEST((byte) 0x03),
IP_RESPONSE((byte) 0x01),
SERIAL_PASSTHRU_REQUEST((byte) 0x04),
SERIAL_PASSTHRU_RESPONSE((byte) 0x02);
private byte value;
HeaderMessageType(byte value) {
this.value = value;
}
public byte getValue() {
return value;
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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.messages.ParadoxIPPacket.PacketHeader;
/**
* Interface representing the functionality of IP packet.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface IPPacket extends IPayload {
PacketHeader getHeader();
byte[] getPayload();
void encrypt();
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2020 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;
/**
* Interface representing what we need to add IPPacketPayload.
* Not sure if we need it as it needs only getBytes() method so far.
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface IPayload {
byte[] getBytes();
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jdt.annotation.NonNullByDefault;
/**
* Constants representing packet headers / messages which are easier written as static final byte arrays
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class IpMessagesConstants {
public static final byte[] UNKNOWN_IP150_REQUEST_MESSAGE01 = { 0x0A, 0x50, 0x08, 0x00, 0x00, 0x01, 0x00, 0x00,
0x59 };
public static final byte[] EPROM_REQUEST_HEADER = { (byte) 0xAA, 0x08, 0x00, 0x04, 0x08, 0x00, 0x00, 0x14,
(byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE };
public static final byte[] LOGOUT_MESAGE_BYTES = new byte[] { 0x00, 0x07, 0x05, 0x00, 0x00, 0x00, 0x00 };
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2020 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 java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MemoryRequestPayload} Abstract class which contains common logic used in RAM and EPROM payload generation
* classes.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public abstract class MemoryRequestPayload implements IPayload {
private static final int BUFFER_LENGTH = 8;
private static final short MESSAGE_START = (short) ((0x50 << 8) | 0x08);
private final Logger logger = LoggerFactory.getLogger(MemoryRequestPayload.class);
private final int address;
private final byte bytesToRead;
public MemoryRequestPayload(int address, byte bytesToRead) throws ParadoxException {
if (bytesToRead < 1 || bytesToRead > 64) {
throw new ParadoxException("Invalid bytes to read. Valid values are 1 to 64.");
}
this.address = address;
this.bytesToRead = bytesToRead;
logTraceHexFormatted("MessageStart: {}", MESSAGE_START);
}
protected abstract byte calculateControlByte();
@Override
public byte[] getBytes() {
byte[] bufferArray = new byte[BUFFER_LENGTH];
ByteBuffer buffer = ByteBuffer.wrap(bufferArray);
buffer.order(ByteOrder.BIG_ENDIAN).putShort(MESSAGE_START);
buffer.put(calculateControlByte());
buffer.put((byte) 0x00);
buffer.order(ByteOrder.BIG_ENDIAN).putShort((short) address);
buffer.put(bytesToRead);
buffer.put((byte) 0x00);
return bufferArray;
}
protected int getAddress() {
return address;
}
protected void logTraceHexFormatted(String text, int address) {
logTraceOptional(text, "0x%02X,\t", address);
}
private void logTraceOptional(String text, String format, int address) {
if (logger.isTraceEnabled()) {
logger.trace("Address: {}", String.format(format, address));
}
}
}

View File

@@ -0,0 +1,146 @@
/**
* Copyright (c) 2010-2020 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 java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.paradoxalarm.internal.communication.crypto.EncryptionHandler;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
/**
* The {@link ParadoxIPPacket} This class is object representing a full IP request packet. Header and payload together.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class ParadoxIPPacket implements IPPacket {
public static final byte[] EMPTY_PAYLOAD = new byte[0];
private PacketHeader header;
private byte[] payload;
public ParadoxIPPacket(byte[] bytes) {
this(bytes, true);
}
@SuppressWarnings("null")
public ParadoxIPPacket(byte[] payload, boolean isChecksumRequired) {
this.payload = payload != null ? payload : new byte[0];
if (isChecksumRequired) {
payload[payload.length - 1] = ParadoxUtil.calculateChecksum(payload);
}
short payloadLength = (short) (payload != null ? payload.length : 0);
header = new PacketHeader(payloadLength);
}
@Override
public byte[] getBytes() {
final byte[] headerBytes = header.getBytes();
int bufferLength = headerBytes.length + payload.length;
byte[] bufferArray = new byte[bufferLength];
ByteBuffer buf = ByteBuffer.wrap(bufferArray);
buf.put(headerBytes);
buf.put(payload);
return bufferArray;
}
public ParadoxIPPacket setCommand(HeaderCommand command) {
header.command = command.getValue();
return this;
}
public ParadoxIPPacket setMessageType(HeaderMessageType messageType) {
header.messageType = messageType.getValue();
return this;
}
public ParadoxIPPacket setUnknown0(byte unknownByteValue) {
header.unknown0 = unknownByteValue;
return this;
}
@Override
public PacketHeader getHeader() {
return header;
}
@Override
public byte[] getPayload() {
return payload;
}
@Override
public void encrypt() {
EncryptionHandler encryptionHandler = EncryptionHandler.getInstance();
payload = encryptionHandler.encrypt(payload);
header.encryption = 0x09;
}
@Override
public String toString() {
return "ParadoxIPPacket [" + ParadoxUtil.byteArrayToString(getBytes()) + "]";
}
public class PacketHeader {
public PacketHeader(short payloadLength) {
this.payloadLength = payloadLength;
}
private static final int BYTES_LENGTH = 9;
/**
* Start of header - always 0xAA
*/
private byte startOfHeader = (byte) 0xAA;
/**
* Payload length - 2 bytes (LL HH)
*/
private short payloadLength = 0;
/**
* "Message Type: 0x01: IP responses 0x02: Serial/pass through cmd response
* 0x03: IP requests 0x04: Serial/pass through cmd requests"
*/
private byte messageType = 0x03;
/**
* "IP Encryption Disabled=0x08, Enabled=0x09"
*/
private byte encryption = 0x08;
private byte command = 0;
private byte subCommand = 0;
private byte unknown0 = 0x00;
private byte unknown1 = 0x01;
public byte[] getBytes() {
byte[] bufferArray = new byte[BYTES_LENGTH];
ByteBuffer buf = ByteBuffer.wrap(bufferArray);
buf.put(startOfHeader);
buf.order(ByteOrder.LITTLE_ENDIAN).putShort(payloadLength);
buf.put(messageType);
buf.put(encryption);
buf.put(command);
buf.put(subCommand);
buf.put(unknown0);
buf.put(unknown1);
return ParadoxUtil.extendArray(bufferArray, 16);
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PartitionCommand} Enum representing the possible commands for a partition with the respective integer
* values that are sent as nibbles in the packet.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public enum PartitionCommand {
UNKNOWN(0),
ARM(2),
STAY_ARM(3),
INSTANT_ARM(4),
FORCE_ARM(5),
DISARM(6),
BEEP(8);
private final static Logger logger = LoggerFactory.getLogger(PartitionCommand.class);
private int command;
PartitionCommand(int command) {
this.command = command;
}
public int getCommand() {
return command;
}
public static 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;
}
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxException;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
/**
* The {@link RamRequestPayload} Object representing payload of IP packet which retrieves data from Paradox RAM
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class RamRequestPayload extends MemoryRequestPayload implements IPayload {
private static final byte CONTROL_BYTE = ParadoxUtil.setBit((byte) 0, 7, 1);
public RamRequestPayload(int address, byte bytesToRead) throws ParadoxException {
super(address, bytesToRead);
}
@Override
protected byte calculateControlByte() {
return CONTROL_BYTE;
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import static org.openhab.binding.paradoxalarm.internal.handlers.ParadoxAlarmBindingConstants.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openhab.binding.paradoxalarm.internal.communication.IParadoxCommunicator;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException;
import org.openhab.binding.paradoxalarm.internal.handlers.ParadoxIP150BridgeHandler;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxInformation;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
import org.openhab.binding.paradoxalarm.internal.model.Partition;
import org.openhab.binding.paradoxalarm.internal.model.Zone;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxDiscoveryService} is responsible for discovery of partitions, zones and the panel once bridge is
* created.
*
* @author Konstnatin Polihronov - Initial Contribution
*/
public class ParadoxDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(ParadoxDiscoveryService.class);
private ParadoxIP150BridgeHandler ip150BridgeHandler;
public ParadoxDiscoveryService(ParadoxIP150BridgeHandler ip150BridgeHandler) {
super(SUPPORTED_THING_TYPES_UIDS, 15, false);
this.ip150BridgeHandler = ip150BridgeHandler;
}
@Override
protected void startScan() {
IParadoxCommunicator communicator = ip150BridgeHandler.getCommunicator();
if (communicator != null && communicator.isOnline()) {
ParadoxPanel panel = ParadoxPanel.getInstance();
discoverPanel(panel.getPanelInformation());
discoverPartitions(panel.getPartitions());
discoverZones(panel.getZones());
} else {
logger.debug("Communicator null or not online. Trace:", new ParadoxRuntimeException());
}
}
private void discoverPanel(ParadoxInformation panelInformation) {
if (panelInformation != null) {
Map<String, Object> properties = new HashMap<>();
properties.put(PANEL_TYPE_PROPERTY_NAME, panelInformation.getPanelType().name());
properties.put(PANEL_SERIAL_NUMBER_PROPERTY_NAME, panelInformation.getSerialNumber());
properties.put(PANEL_APPLICATION_VERSION_PROPERTY_NAME, panelInformation.getApplicationVersion());
properties.put(PANEL_BOOTLOADER_VERSION_PROPERTY_NAME, panelInformation.getBootLoaderVersion());
properties.put(PANEL_HARDWARE_VERSION_PROPERTY_NAME, panelInformation.getHardwareVersion());
ThingUID bridgeUid = ip150BridgeHandler.getThing().getUID();
ThingUID thingUID = new ThingUID(PANEL_THING_TYPE_UID, bridgeUid, PARADOX_PANEL_THING_TYPE_ID);
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withBridge(bridgeUid).withLabel("Paradox panel - " + panelInformation.getPanelType()).build();
logger.debug("Panel DiscoveryResult={}", result);
thingDiscovered(result);
}
}
private void discoverPartitions(List<Partition> partitions) {
partitions.stream().forEach(partition -> {
String thingId = PARTITION_THING_TYPE_ID + partition.getId();
String label = partition.getLabel();
ThingUID bridgeUid = ip150BridgeHandler.getThing().getUID();
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();
logger.debug("Partition DiscoveryResult={}", result);
thingDiscovered(result);
});
}
private void discoverZones(List<Zone> zones) {
zones.stream().forEach(zone -> {
String thingId = zone.getLabel().replaceAll(" ", "_");
String label = zone.getLabel();
ThingUID bridgeUid = ip150BridgeHandler.getThing().getUID();
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();
logger.debug("Zone DiscoveryResult={}", result);
thingDiscovered(result);
});
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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.exceptions;
/**
* The {@link ParadoxException} Wrapper of Exception class.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxException extends Exception {
private static final long serialVersionUID = -5771699322577106346L;
public ParadoxException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public ParadoxException(String message, Throwable cause) {
super(message, cause);
}
public ParadoxException(String message) {
super(message);
}
public ParadoxException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 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.exceptions;
/**
* The {@link ParadoxRuntimeException} Used for Paradox binding specific runtime exceptions.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxRuntimeException extends RuntimeException {
private static final long serialVersionUID = -4656474289606169766L;
public ParadoxRuntimeException(String message) {
super(message);
}
public ParadoxRuntimeException() {
super();
}
public ParadoxRuntimeException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public ParadoxRuntimeException(String message, Throwable cause) {
super(message, cause);
}
public ParadoxRuntimeException(Throwable cause) {
this("This is a Paradox Binding wrapper of RuntimeException. For detailed error message, see the original exception. Short message: "
+ cause.getMessage(), cause);
}
}

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2020 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.handlers;
/**
* The {@link Commandable} Interface for entities that can handle string commands
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface Commandable {
void handleCommand(String command);
}

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) 2010-2020 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.handlers;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link EntityBaseHandler} abstract handler class that contains common logic for entities.
*
* @author Konstantin Polihronov - Initial contribution
*/
public abstract class EntityBaseHandler extends BaseThingHandler {
private static final long INITIAL_DELAY_SECONDS = 15;
private static final int MAX_WAIT_TIME_MILLIS = 60000;
private long timeStamp;
private final Logger logger = LoggerFactory.getLogger(EntityBaseHandler.class);
protected EntityConfiguration config;
private ScheduledFuture<?> delayedSchedule;
public EntityBaseHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
logger.trace("Start initializing. {}", thing.getUID());
updateStatus(ThingStatus.UNKNOWN);
config = getConfigAs(EntityConfiguration.class);
timeStamp = System.currentTimeMillis();
delayedSchedule = scheduler.schedule(this::initializeDelayed, INITIAL_DELAY_SECONDS, TimeUnit.SECONDS);
}
private void initializeDelayed() {
logger.debug("Start initializeDelayed() in {}", getThing().getUID());
ParadoxPanel panel = ParadoxPanel.getInstance();
// Asynchronous update not yet done
if (panel.getPanelInformation() == null) {
// Retry until reach MAX_WAIT_TIME
if (System.currentTimeMillis() - timeStamp <= MAX_WAIT_TIME_MILLIS) {
logger.debug("Panel information is null. Scheduling initializeDelayed() to be executed again in {} sec",
INITIAL_DELAY_SECONDS);
delayedSchedule = scheduler.schedule(this::initializeDelayed, INITIAL_DELAY_SECONDS, TimeUnit.SECONDS);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
"Panel is not updating the information in " + MAX_WAIT_TIME_MILLIS
+ " ms. Giving up. Cannot update entity=" + this + ".");
}
// Asynchronous update done but panel is not supported
} else if (!panel.isPanelSupported()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Panel is not supported. Cannot update entity=" + this + ".");
// All OK
} else {
updateStatus(ThingStatus.ONLINE);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (ThingStatus.OFFLINE == getThing().getStatus()) {
logger.debug("Received {} command but {} is OFFLINE with the following detailed status {}", command,
getThing().getUID(), getThing().getStatusInfo());
return;
}
if (command instanceof RefreshType) {
updateEntity();
}
}
@Override
public void dispose() {
if (delayedSchedule != null) {
boolean cancelingResult = delayedSchedule.cancel(true);
String cancelingSuccessful = cancelingResult ? "successful" : "failed";
logger.debug("Canceling schedule of {} is {}", delayedSchedule, cancelingSuccessful);
}
}
protected abstract void updateEntity();
protected int calculateEntityIndex() {
return Math.max(0, config.getId() - 1);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 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.handlers;
/**
* The {@link EntityConfiguration} Common configuration class used by all entities at the moment.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class EntityConfiguration {
private int id;
private boolean disarmEnabled;
public int getId() {
return id;
}
public boolean isDisarmEnabled() {
return disarmEnabled;
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 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.handlers;
import java.time.LocalDateTime;
/**
* The {@link PanelConfiguration} Paradox Panel handler configuration.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class PanelConfiguration extends EntityConfiguration {
private double vdcVoltage;
private double dcVoltage;
private double batteryVoltage;
private LocalDateTime panelTime;
public double getVdcVoltage() {
return vdcVoltage;
}
public void setAcVoltage(double vdcVoltage) {
this.vdcVoltage = vdcVoltage;
}
public double getDcVoltage() {
return dcVoltage;
}
public void setDcVoltage(double dcVoltage) {
this.dcVoltage = dcVoltage;
}
public double getBatteryVoltage() {
return batteryVoltage;
}
public void setBatteryVoltage(double batteryVoltage) {
this.batteryVoltage = batteryVoltage;
}
public LocalDateTime getPanelTime() {
return panelTime;
}
public void setPanelTime(LocalDateTime panelTime) {
this.panelTime = panelTime;
}
}

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2010-2020 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.handlers;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link ParadoxAlarmBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class ParadoxAlarmBindingConstants {
public static final String BINDING_ID = "paradoxalarm";
public static final String PARADOX_COMMUNICATOR_THING_TYPE_ID = "ip150";
public static final String PARADOX_PANEL_THING_TYPE_ID = "panel";
public static final String PARTITION_THING_TYPE_ID = "partition";
public static final String ZONE_THING_TYPE_ID = "zone";
// List of all Thing Type UIDs
public static final ThingTypeUID COMMUNICATOR_THING_TYPE_UID = new ThingTypeUID(BINDING_ID,
PARADOX_COMMUNICATOR_THING_TYPE_ID);
public static final ThingTypeUID PANEL_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, PARADOX_PANEL_THING_TYPE_ID);
public static final ThingTypeUID PARTITION_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, PARTITION_THING_TYPE_ID);
public static final ThingTypeUID ZONE_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, ZONE_THING_TYPE_ID);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(COMMUNICATOR_THING_TYPE_UID, PANEL_THING_TYPE_UID, PARTITION_THING_TYPE_UID, ZONE_THING_TYPE_UID)
.collect(Collectors.toSet()));
// List of all Channel UIDs
public static final String IP150_COMMUNICATION_COMMAND_CHANNEL_UID = "communicationCommand";
public static final String IP150_COMMUNICATION_STATE_CHANNEL_UID = "communicationState";
public static final String PANEL_STATE_CHANNEL_UID = "state";
public static final String PANEL_SERIAL_NUMBER_PROPERTY_NAME = "serialNumber";
public static final String PANEL_TYPE_PROPERTY_NAME = "panelType";
public static final String PANEL_HARDWARE_VERSION_PROPERTY_NAME = "hardwareVersion";
public static final String PANEL_APPLICATION_VERSION_PROPERTY_NAME = "applicationVersion";
public static final String PANEL_BOOTLOADER_VERSION_PROPERTY_NAME = "bootloaderVersion";
public static final String PANEL_TIME = "panelTime";
public static final String PANEL_INPUT_VOLTAGE = "inputVoltage";
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_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
public static final String PARTITION_ADDITIONAL_STATES_CHANNEL_UID = "additionalStates";
public static final String PARTITION_READY_TO_ARM_CHANNEL_UID = "readyToArm";
public static final String PARTITION_IN_EXIT_DELAY_CHANNEL_UID = "inExitDelay";
public static final String PARTITION_IN_ENTRY_DELAY_CHANNEL_UID = "inEntryDelay";
public static final String PARTITION_IN_TROUBLE_CHANNEL_UID = "inTrouble";
public static final String PARTITION_ALARM_IN_MEMORY_CHANNEL_UID = "alarmInMemory";
public static final String PARTITION_ZONE_BYPASS_CHANNEL_UID = "zoneBypass";
public static final String PARTITION_ZONE_IN_TAMPER_CHANNEL_UID = "zoneInTamperTrouble";
public static final String PARTITION_ZONE_IN_LOW_BATTERY_CHANNEL_UID = "zoneInLowBatteryTrouble";
public static final String PARTITION_ZONE_IN_FIRE_LOOP_CHANNEL_UID = "zoneInFireLoopTrouble";
public static final String PARTITION_ZONE_IN_SUPERVISION_TROUBLE_CHANNEL_UID = "zoneInSupervisionTrouble";
public static final String PARTITION_STAY_INSTANT_READY_CHANNEL_UID = "stayInstantReady";
public static final String PARTITION_FORCE_READY_CHANNEL_UID = "forceReady";
public static final String PARTITION_BYPASS_READY_CHANNEL_UID = "bypassReady";
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_OPENED_CHANNEL_UID = "opened";
public static final String ZONE_TAMPERED_CHANNEL_UID = "tampered";
public static final String ZONE_LOW_BATTERY_CHANNEL_UID = "lowBattery";
// Misc constants
public static final StringType STATE_OFFLINE = new StringType("Offline");
public static final StringType STATE_ONLINE = new StringType("Online");
}

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2020 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.handlers;
import static org.openhab.binding.paradoxalarm.internal.handlers.ParadoxAlarmBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.paradoxalarm.internal.discovery.ParadoxDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxAlarmHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.paradoxalarm", service = ThingHandlerFactory.class)
public class ParadoxAlarmHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(ParadoxAlarmHandlerFactory.class);
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
ThingUID thingUID = thing.getUID();
if (COMMUNICATOR_THING_TYPE_UID.equals(thingTypeUID)) {
logger.debug("createHandler(): ThingHandler created for {}", thingUID);
ParadoxIP150BridgeHandler paradoxIP150BridgeHandler = new ParadoxIP150BridgeHandler((Bridge) thing);
registerDiscoveryService(paradoxIP150BridgeHandler);
return paradoxIP150BridgeHandler;
} else if (PANEL_THING_TYPE_UID.equals(thingTypeUID)) {
logger.debug("createHandler(): ThingHandler created for {}", thingUID);
return new ParadoxPanelHandler(thing);
} else if (PARTITION_THING_TYPE_UID.equals(thingTypeUID)) {
logger.debug("createHandler(): ThingHandler created for {}", thingUID);
return new ParadoxPartitionHandler(thing);
} else if (ZONE_THING_TYPE_UID.equals(thingTypeUID)) {
logger.debug("createHandler(): ThingHandler created for {}", thingUID);
return new ParadoxZoneHandler(thing);
} else {
logger.warn("Handler implementation not found for Thing: {}", thingUID);
}
return null;
}
private void registerDiscoveryService(ParadoxIP150BridgeHandler paradoxIP150BridgeHandler) {
ParadoxDiscoveryService discoveryService = new ParadoxDiscoveryService(paradoxIP150BridgeHandler);
ServiceRegistration<?> serviceRegistration = bundleContext.registerService(DiscoveryService.class.getName(),
discoveryService, new Hashtable<>());
this.discoveryServiceRegs.put(paradoxIP150BridgeHandler.getThing().getUID(), serviceRegistration);
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof ParadoxIP150BridgeHandler) {
ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.remove(thingHandler.getThing().getUID());
if (serviceReg != null) {
serviceReg.unregister();
}
}
}
}

View File

@@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2020 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.handlers;
/**
* The {@link ParadoxIP150BridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxIP150BridgeConfiguration {
private int refresh;
private String ip150Password;
private String pcPassword;
private String ipAddress;
private int port;
private String panelType;
private int reconnectWaitTime;
private Integer maxZones;
private Integer maxPartitions;
private boolean encrypt;
public int getRefresh() {
return refresh;
}
public void setRefresh(int refresh) {
this.refresh = refresh;
}
public String getIp150Password() {
return ip150Password;
}
public void setIp150Password(String ip150Password) {
this.ip150Password = ip150Password;
}
public String getPcPassword() {
return pcPassword;
}
public void setPcPassword(String pcPassword) {
this.pcPassword = pcPassword;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getPanelType() {
return panelType;
}
public void setPanelType(String panelType) {
this.panelType = panelType;
}
public int getReconnectWaitTime() {
return reconnectWaitTime;
}
public void setReconnectWaitTime(int reconnectWaitTime) {
this.reconnectWaitTime = reconnectWaitTime;
}
public Integer getMaxZones() {
return maxZones;
}
public void setMaxZones(Integer maxZones) {
this.maxZones = maxZones;
}
public Integer getMaxPartitions() {
return maxPartitions;
}
public void setMaxPartitions(Integer maxPartitions) {
this.maxPartitions = maxPartitions;
}
public boolean isEncrypt() {
return encrypt;
}
public void setEncrypt(boolean encrypt) {
this.encrypt = encrypt;
}
}

View File

@@ -0,0 +1,343 @@
/**
* Copyright (c) 2010-2020 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.handlers;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.paradoxalarm.internal.communication.CommunicationState;
import org.openhab.binding.paradoxalarm.internal.communication.GenericCommunicator;
import org.openhab.binding.paradoxalarm.internal.communication.ICommunicatorBuilder;
import org.openhab.binding.paradoxalarm.internal.communication.IDataUpdateListener;
import org.openhab.binding.paradoxalarm.internal.communication.IP150Command;
import org.openhab.binding.paradoxalarm.internal.communication.IParadoxCommunicator;
import org.openhab.binding.paradoxalarm.internal.communication.IParadoxInitialLoginCommunicator;
import org.openhab.binding.paradoxalarm.internal.communication.ISocketTimeOutListener;
import org.openhab.binding.paradoxalarm.internal.communication.ParadoxBuilderFactory;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException;
import org.openhab.binding.paradoxalarm.internal.model.PanelType;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxInformationConstants;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxIP150BridgeHandler} This is the handler that takes care of communication to/from Paradox alarm
* system.
*
* @author Konstantin Polihronov - Initial contribution
*/
@SuppressWarnings("null")
@NonNullByDefault({})
public class ParadoxIP150BridgeHandler extends BaseBridgeHandler
implements IDataUpdateListener, ISocketTimeOutListener {
private static final String RESET_COMMAND = "RESET";
private static final int ONLINE_WAIT_TRESHOLD_MILLIS = 10000;
private static final int INITIAL_SCHEDULE_DELAY_SECONDS = 5;
private final Logger logger = LoggerFactory.getLogger(ParadoxIP150BridgeHandler.class);
private IParadoxCommunicator communicator;
private static ParadoxIP150BridgeConfiguration config;
private @Nullable ScheduledFuture<?> refreshCacheUpdateSchedule;
private long timeStamp = 0;
private ScheduledFuture<?> resetScheduleFuture;
public ParadoxIP150BridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
public void initialize() {
logger.debug("Start initialize()...");
updateStatus(ThingStatus.UNKNOWN);
logger.debug("Starting creation of communicator.");
config = getConfigAs(ParadoxIP150BridgeConfiguration.class);
scheduler.execute(this::initializeCommunicator);
logger.debug("Finished initialize().");
}
private synchronized void initializeCommunicator() {
try {
String ipAddress = config.getIpAddress();
int tcpPort = config.getPort();
String ip150Password = config.getIp150Password();
String pcPassword = config.getPcPassword();
boolean useEncryption = config.isEncrypt();
// Early exit. If panel type is configured and known to the binding skip auto-detection. Saves one full
// initial login process to detect the panel type.
PanelType configuredPanelType = PanelType.from(config.getPanelType());
if (configuredPanelType != PanelType.UNKNOWN) {
logger.debug("Configuration file has pannelType={}. Skipping Phase1 (Autodiscovery)",
configuredPanelType);
scheduler.schedule(() -> createDiscoveredCommunicatorJob(configuredPanelType), 3, TimeUnit.SECONDS);
return;
}
logger.debug("Phase1 - Auto discover communicator");
IParadoxInitialLoginCommunicator initialCommunicator = new GenericCommunicator(ipAddress, tcpPort,
ip150Password, pcPassword, scheduler, useEncryption);
initialCommunicator.startLoginSequence();
timeStamp = System.currentTimeMillis();
scheduler.schedule(() -> doPostOnlineTask(initialCommunicator), 500, TimeUnit.MILLISECONDS);
} catch (UnknownHostException e) {
logger.warn("Error while starting socket communication. {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Unknown host. Probably misconfiguration or DNS issue.");
throw new ParadoxRuntimeException(e);
} catch (IOException e) {
logger.warn("Error while starting socket communication. {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error while starting socket communication.");
throw new ParadoxRuntimeException(e);
}
}
private synchronized void doPostOnlineTask(IParadoxInitialLoginCommunicator initialCommunicator) {
if (!initialCommunicator.isOnline()) {
if (System.currentTimeMillis() - timeStamp <= ONLINE_WAIT_TRESHOLD_MILLIS) {
scheduler.schedule(() -> doPostOnlineTask(initialCommunicator), 500, TimeUnit.MILLISECONDS);
logger.debug("Communicator not yet online. Rescheduling...");
} else {
logger.warn(
"Initial communicator not coming up online for {} seconds. Probably there is something wrong with communication.",
ONLINE_WAIT_TRESHOLD_MILLIS);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error while starting socket communication.");
}
return;
}
byte[] panelInfoBytes = initialCommunicator.getPanelInfoBytes();
PanelType panelType = ParadoxInformationConstants.parsePanelType(panelInfoBytes);
logger.info("Found {} panel type.", panelType);
CommunicationState.logout(initialCommunicator);
// Wait 3 seconds before we create the discovered communicator so we ensure that the socket is closed
// successfully from both ends
scheduler.schedule(() -> createDiscoveredCommunicatorJob(panelType), 3, TimeUnit.SECONDS);
}
protected void createDiscoveredCommunicatorJob(PanelType panelType) {
// If not detected properly, use the value from config
if (panelType == PanelType.UNKNOWN) {
panelType = PanelType.from(config.getPanelType());
}
logger.debug("Phase2 - Creating communicator for panel {}", panelType);
ICommunicatorBuilder builder = new ParadoxBuilderFactory().createBuilder(panelType);
communicator = builder.withIp150Password(config.getIp150Password()).withPcPassword(config.getPcPassword())
.withIpAddress(config.getIpAddress()).withTcpPort(config.getPort())
.withMaxPartitions(config.getMaxPartitions()).withMaxZones(config.getMaxZones())
.withScheduler(scheduler).withEncryption(config.isEncrypt()).build();
ParadoxPanel panel = ParadoxPanel.getInstance();
panel.setCommunicator(communicator);
Collection<IDataUpdateListener> listeners = Arrays.asList(panel, this);
communicator.setListeners(listeners);
communicator.setStoListener(this);
logger.debug("Listeners set to: {}", listeners);
communicator.startLoginSequence();
timeStamp = System.currentTimeMillis();
doPostOnlineFinalCommunicatorJob();
}
private void doPostOnlineFinalCommunicatorJob() {
if (!communicator.isOnline()) {
if (System.currentTimeMillis() - timeStamp <= ONLINE_WAIT_TRESHOLD_MILLIS) {
scheduler.schedule(this::doPostOnlineFinalCommunicatorJob, 1, TimeUnit.SECONDS);
logger.debug("Communicator not yet online. Rescheduling...");
return;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error while starting socket communication.");
ParadoxRuntimeException exception = new ParadoxRuntimeException(
"Communicator didn't go online in defined treshold time. " + ONLINE_WAIT_TRESHOLD_MILLIS
+ "sec. You can still try to reset the bridge with command RESET.");
logger.debug("Problem with communication", exception);
throw exception;
}
}
logger.debug("Communicator created successfully.");
updateStatus(ThingStatus.ONLINE);
}
@Override
public void dispose() {
cancelSchedule(refreshCacheUpdateSchedule);
CommunicationState.logout(communicator);
super.dispose();
}
private void scheduleRefresh() {
logger.debug("Scheduling cache update. Refresh interval: {}s. Starts after: {}s.", config.getRefresh(),
INITIAL_SCHEDULE_DELAY_SECONDS);
refreshCacheUpdateSchedule = scheduler.scheduleWithFixedDelay(communicator::refreshMemoryMap,
INITIAL_SCHEDULE_DELAY_SECONDS, config.getRefresh(), TimeUnit.SECONDS);
}
private void cancelSchedule(@Nullable ScheduledFuture<?> schedule) {
if (schedule != null && !schedule.isCancelled()) {
boolean cancelingResult = schedule.cancel(true);
String cancelingSuccessful = cancelingResult ? "successful" : "failed";
logger.debug("Canceling schedule of {} is {}", schedule, cancelingSuccessful);
}
}
@Override
public void update() {
updateStatusChannel();
Bridge bridge = getThing();
if (bridge.getStatus() == ThingStatus.OFFLINE) {
if (communicator.isOnline()) {
updateStatus(ThingStatus.ONLINE);
} else {
logger.debug("Bridge {} triggered update but is OFFLINE", bridge.getUID());
return;
}
}
List<Thing> things = bridge.getThings();
for (Thing thing : things) {
ThingHandler handler = thing.getHandler();
Channel bridgeChannel = bridge
.getChannel(ParadoxAlarmBindingConstants.IP150_COMMUNICATION_COMMAND_CHANNEL_UID);
if (handler != null && bridgeChannel != null) {
handler.handleCommand(bridgeChannel.getUID(), RefreshType.REFRESH);
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Received command {}", command);
if (ThingStatus.OFFLINE == getThing().getStatus() && command instanceof RefreshType) {
logger.debug("Bridge {} is OFFLINE. Cannot handle refresh commands.", getThing().getUID());
return;
}
if (ParadoxAlarmBindingConstants.IP150_COMMUNICATION_COMMAND_CHANNEL_UID.equals(channelUID.getId())) {
logger.debug("Command is instance of {}", command.getClass());
if (command instanceof StringType) {
String commandAsString = command.toFullString();
if (commandAsString.equals(RESET_COMMAND)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
"Bringing bridge offline due to reinitialization of communicator.");
resetCommunicator();
} else {
communicator.executeCommand(commandAsString);
}
}
}
if (communicator != null && communicator.isOnline()) {
logger.debug("Communicator is online");
communicator.refreshMemoryMap();
updateStatus(ThingStatus.ONLINE);
} else {
logger.debug("Communicator is null or not online");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Device is offline");
}
}
private void resetCommunicator() {
updateStatus(ThingStatus.OFFLINE);
synchronized (communicator) {
if (communicator != null) {
CommunicationState.logout(communicator);
}
initializeCommunicator();
}
resetScheduleFuture = null;
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
updateStatusChannel();
super.updateStatus(status, statusDetail, description);
if (status.equals(ThingStatus.ONLINE)) {
if (refreshCacheUpdateSchedule == null || refreshCacheUpdateSchedule.isDone()) {
scheduleRefresh();
}
} else {
cancelSchedule(refreshCacheUpdateSchedule);
}
}
private void updateStatusChannel() {
StringType communicatorStatus = communicator != null && communicator.isOnline()
? ParadoxAlarmBindingConstants.STATE_ONLINE
: ParadoxAlarmBindingConstants.STATE_OFFLINE;
updateState(ParadoxAlarmBindingConstants.IP150_COMMUNICATION_STATE_CHANNEL_UID, communicatorStatus);
}
public IParadoxCommunicator getCommunicator() {
return communicator;
}
@Override
public void onSocketTimeOutOccurred(IOException exception) {
logger.warn("TIMEOUT! {} received message for socket timeout. ", this, exception);
if (resetScheduleFuture == null) {
communicator.executeCommand(IP150Command.LOGOUT.name());
logger.warn("Reset task is null. Will schedule new task to reset communicator in {} seconds.",
config.getReconnectWaitTime());
resetScheduleFuture = scheduler.schedule(this::resetCommunicator, config.getReconnectWaitTime(),
TimeUnit.SECONDS);
}
}
@Override
public String toString() {
return "ParadoxIP150BridgeHandler [" + getThing().getUID().getAsString() + "]";
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2020 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.handlers;
import static org.openhab.binding.paradoxalarm.internal.handlers.ParadoxAlarmBindingConstants.*;
import javax.measure.quantity.ElectricPotential;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxInformation;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
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.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class ParadoxPanelHandler extends EntityBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ParadoxPanelHandler.class);
public ParadoxPanelHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
config = getConfigAs(PanelConfiguration.class);
}
@Override
protected void updateEntity() {
ParadoxPanel panel = ParadoxPanel.getInstance();
StringType panelState = panel.isOnline() ? STATE_ONLINE : STATE_OFFLINE;
updateState(PANEL_STATE_CHANNEL_UID, panelState);
ParadoxInformation panelInformation = panel.getPanelInformation();
if (panelInformation != null) {
updateProperty(PANEL_SERIAL_NUMBER_PROPERTY_NAME, panelInformation.getSerialNumber());
updateProperty(PANEL_TYPE_PROPERTY_NAME, panelInformation.getPanelType().name());
updateProperty(PANEL_HARDWARE_VERSION_PROPERTY_NAME, panelInformation.getHardwareVersion().toString());
updateProperty(PANEL_APPLICATION_VERSION_PROPERTY_NAME,
panelInformation.getApplicationVersion().toString());
updateProperty(PANEL_BOOTLOADER_VERSION_PROPERTY_NAME, panelInformation.getBootLoaderVersion().toString());
updateState(PANEL_TIME, new DateTimeType(panel.getPanelTime()));
updateState(PANEL_INPUT_VOLTAGE,
new QuantityType<ElectricPotential>(panel.getVdcLevel(), SmartHomeUnits.VOLT));
updateState(PANEL_BOARD_VOLTAGE,
new QuantityType<ElectricPotential>(panel.getDcLevel(), SmartHomeUnits.VOLT));
updateState(PANEL_BATTERY_VOLTAGE,
new QuantityType<ElectricPotential>(panel.getBatteryLevel(), SmartHomeUnits.VOLT));
}
}
}

View File

@@ -0,0 +1,125 @@
/**
* Copyright (c) 2010-2020 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.handlers;
import static org.openhab.binding.paradoxalarm.internal.handlers.ParadoxAlarmBindingConstants.*;
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.model.ParadoxPanel;
import org.openhab.binding.paradoxalarm.internal.model.Partition;
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.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxPartitionHandler} Handler that updates states of paradox partitions from the cache.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxPartitionHandler extends EntityBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ParadoxPartitionHandler.class);
public ParadoxPartitionHandler(@NonNull Thing thing) {
super(thing);
}
@Override
protected void updateEntity() {
Partition partition = getPartition();
if (partition != null) {
updateState(PARTITION_LABEL_CHANNEL_UID, new StringType(partition.getLabel()));
updateState(PARTITION_STATE_CHANNEL_UID, new StringType(partition.getState().getMainState()));
updateState(PARTITION_ADDITIONAL_STATES_CHANNEL_UID,
new StringType("Deprecated field. Use direct channels instead"));
updateState(PARTITION_READY_TO_ARM_CHANNEL_UID, booleanToSwitchState(partition.getState().isReadyToArm()));
updateState(PARTITION_IN_EXIT_DELAY_CHANNEL_UID,
booleanToSwitchState(partition.getState().isInExitDelay()));
updateState(PARTITION_IN_ENTRY_DELAY_CHANNEL_UID,
booleanToSwitchState(partition.getState().isInEntryDelay()));
updateState(PARTITION_IN_TROUBLE_CHANNEL_UID, booleanToSwitchState(partition.getState().isInTrouble()));
updateState(PARTITION_ALARM_IN_MEMORY_CHANNEL_UID,
booleanToSwitchState(partition.getState().isHasAlarmInMemory()));
updateState(PARTITION_ZONE_BYPASS_CHANNEL_UID, booleanToSwitchState(partition.getState().isInZoneBypass()));
updateState(PARTITION_ZONE_IN_TAMPER_CHANNEL_UID,
booleanToSwitchState(partition.getState().isHasZoneInTamperTrouble()));
updateState(PARTITION_ZONE_IN_LOW_BATTERY_CHANNEL_UID,
booleanToSwitchState(partition.getState().isHasZoneInLowBatteryTrouble()));
updateState(PARTITION_ZONE_IN_FIRE_LOOP_CHANNEL_UID,
booleanToSwitchState(partition.getState().isHasZoneInFireLoopTrouble()));
updateState(PARTITION_ZONE_IN_SUPERVISION_TROUBLE_CHANNEL_UID,
booleanToSwitchState(partition.getState().isHasZoneInSupervisionTrouble()));
updateState(PARTITION_STAY_INSTANT_READY_CHANNEL_UID,
booleanToSwitchState(partition.getState().isStayInstantReady()));
updateState(PARTITION_FORCE_READY_CHANNEL_UID, booleanToSwitchState(partition.getState().isForceReady()));
updateState(PARTITION_BYPASS_READY_CHANNEL_UID, booleanToSwitchState(partition.getState().isBypassReady()));
updateState(PARTITION_INHIBIT_READY_CHANNEL_UID,
booleanToSwitchState(partition.getState().isInhibitReady()));
updateState(PARTITION_ALL_ZONES_CLOSED_CHANNEL_UID,
booleanToContactState(partition.getState().isAreAllZoneclosed()));
}
}
protected Partition getPartition() {
int index = calculateEntityIndex();
List<Partition> partitions = ParadoxPanel.getInstance().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;
}
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) {
boolean isDisarmCommand = PartitionCommand.DISARM.name().equals(command.toString());
if (isDisarmCommand && !config.isDisarmEnabled()) {
logger.warn(
"Received DISARM command but Disarm is not enabled. This is security relevant feature. Check documentation!");
return;
}
Partition partition = getPartition();
if (partition != null) {
partition.handleCommand(command.toString());
}
} else {
super.handleCommand(channelUID, command);
}
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2020 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.handlers;
import static org.openhab.binding.paradoxalarm.internal.handlers.ParadoxAlarmBindingConstants.*;
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.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxZoneHandler} Handler that updates states of paradox zones from the cache.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxZoneHandler extends EntityBaseHandler {
private final Logger logger = LoggerFactory.getLogger(ParadoxZoneHandler.class);
public ParadoxZoneHandler(@NonNull Thing thing) {
super(thing);
}
@Override
protected void updateEntity() {
int index = calculateEntityIndex();
List<Zone> zones = ParadoxPanel.getInstance().getZones();
if (zones == null) {
logger.debug(
"Zones collection of Paradox Panel object is null. Probably not yet initialized. Skipping update.");
return;
}
if (zones.size() <= index) {
logger.debug("Attempted to access zone out of bounds of current zone list. Index: {}, List: {}", index,
zones);
return;
}
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()));
}
}
private OpenClosedType booleanToContactState(boolean value) {
return value ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
}
private OnOffType booleanToSwitchState(boolean value) {
return value ? OnOffType.ON : OnOffType.OFF;
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Entity} Entity - base abstract class for Paradox entities (Partitions, zones, etc).
* Extend this class and add entity specific data (states, troubles, etc).
*
* @author Konstantin Polihronov - Initial contribution
*/
public abstract class Entity {
private final Logger logger = LoggerFactory.getLogger(Entity.class);
private int id;
private String label;
public Entity(int id, String label) {
this.id = id;
this.label = label.trim();
logger.debug("Creating entity with label: {} and ID: {}", label, id);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
@Override
public String toString() {
return "Entity [id=" + id + ", label=" + label + "]";
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.model;
/**
* The {@link EntityType} Enum of paradox entity types - zones, partitions, maybe more in the future...
*
* @author Konstantin Polihronov - Initial contribution
*/
public enum EntityType {
ZONE,
PARTITION;
@Override
public String toString() {
String upperCase = super.toString();
String lowerCase = upperCase.toLowerCase();
return lowerCase.replace(lowerCase.charAt(0), upperCase.charAt(0));
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 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.model;
/**
* The {@link PanelType} Enum of all panel types
*
* @author Konstantin Polihronov - Initial contribution
*/
public enum PanelType {
EVO48(4, 48, 2, 16),
EVO96(4, 96, 3, 16),
EVO192(8, 192, 5, 16),
EVOHD(8, 192, 5, 16),
SP5500,
SP6000,
SP7000,
MG5000,
MG5050,
SP4000,
SP65,
UNKNOWN;
private int partitions;
private int zones;
private int pgms; // Programmable outputs
private int ramPagesNumber; // Ram pages 64 bytes each
private PanelType() {
this(0, 0, 0, 0);
}
private PanelType(int numberPartitions, int numberZones, int pgms, int ramPages) {
this.partitions = numberPartitions;
this.zones = numberZones;
this.pgms = pgms;
this.ramPagesNumber = ramPages;
}
public static PanelType from(String panelTypeStr) {
if (panelTypeStr == null) {
return PanelType.UNKNOWN;
}
try {
return PanelType.valueOf(panelTypeStr);
} catch (IllegalArgumentException e) {
return PanelType.UNKNOWN;
}
}
public int getPartitions() {
return partitions;
}
public int getZones() {
return zones;
}
@Override
public String toString() {
return this.name();
}
public int getPgms() {
return pgms;
}
public int getRamPagesNumber() {
return ramPagesNumber;
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 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.model;
import java.util.Arrays;
import org.openhab.binding.paradoxalarm.internal.parsers.IParadoxParser;
import org.openhab.core.util.HexUtils;
/**
* The {@link ParadoxInformation} Class that provides the basic panel
* information (serial number, panel type, application, hardware and bootloader
* versions. It's the object representation of 37 bytes 0x72 serial response.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxInformation {
private PanelType panelType;
private String serialNumber;
private Version applicationVersion;
private Version hardwareVersion;
private Version bootloaderVersion;
public ParadoxInformation(byte[] panelInfoBytes, IParadoxParser parser) {
panelType = ParadoxInformationConstants.parsePanelType(panelInfoBytes);
applicationVersion = parser.parseApplicationVersion(panelInfoBytes);
hardwareVersion = parser.parseHardwareVersion(panelInfoBytes);
bootloaderVersion = parser.parseBootloaderVersion(panelInfoBytes);
byte[] serialNumberBytes = Arrays.copyOfRange(panelInfoBytes, 12, 16);
serialNumber = HexUtils.bytesToHex(serialNumberBytes);
}
public PanelType getPanelType() {
return panelType;
}
public Version getApplicationVersion() {
return applicationVersion;
}
public Version getHardwareVersion() {
return hardwareVersion;
}
public Version getBootLoaderVersion() {
return bootloaderVersion;
}
public String getSerialNumber() {
return serialNumber;
}
@Override
public String toString() {
return "ParadoxInformation [panelType=" + panelType + ", serialNumber=" + serialNumber + ", applicationVersion="
+ applicationVersion + ", hardwareVersion=" + hardwareVersion + ", bootloaderVersion="
+ bootloaderVersion + "]";
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 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.model;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.openhab.core.util.HexUtils;
/**
* The {@link ParadoxInformation} Class that provides the basic panel
* information (serial number, panel type, application, hardware and bootloader
* versions. It's the object representation of 37 bytes 0x72 serial response.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxInformationConstants {
public static Map<String, PanelType> panelTypes = new HashMap<>();
static {
panelTypes.put("0xA122", PanelType.EVO48);
panelTypes.put("0xA133", PanelType.EVO48);
panelTypes.put("0xA159", PanelType.EVO48);
panelTypes.put("0xA123", PanelType.EVO192);
panelTypes.put("0xA15A", PanelType.EVO192);
panelTypes.put("0xA16D", PanelType.EVOHD);
panelTypes.put("0xA41E", PanelType.SP5500);
panelTypes.put("0xA450", PanelType.SP5500);
panelTypes.put("0xA41F", PanelType.SP6000);
panelTypes.put("0xA451", PanelType.SP6000);
panelTypes.put("0xA420", PanelType.SP7000);
panelTypes.put("0xA452", PanelType.SP7000);
panelTypes.put("0xA524", PanelType.MG5000);
panelTypes.put("0xA526", PanelType.MG5050);
panelTypes.put("0xAE53", PanelType.SP4000);
panelTypes.put("0xAE54", PanelType.SP65);
}
public static PanelType parsePanelType(byte[] infoPacket) {
if (infoPacket == null || infoPacket.length != 37) {
return PanelType.UNKNOWN;
}
byte[] panelTypeBytes = Arrays.copyOfRange(infoPacket, 6, 8);
String key = "0x" + HexUtils.bytesToHex(panelTypeBytes);
return ParadoxInformationConstants.panelTypes.getOrDefault(key, PanelType.UNKNOWN);
}
}

View File

@@ -0,0 +1,209 @@
/**
* Copyright (c) 2010-2020 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.model;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.binding.paradoxalarm.internal.communication.IDataUpdateListener;
import org.openhab.binding.paradoxalarm.internal.communication.IParadoxCommunicator;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException;
import org.openhab.binding.paradoxalarm.internal.parsers.EvoParser;
import org.openhab.binding.paradoxalarm.internal.parsers.IParadoxParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxPanel} Composition class which contains all Paradox entities.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxPanel implements IDataUpdateListener {
private final Logger logger = LoggerFactory.getLogger(ParadoxPanel.class);
@NonNull
private static ParadoxPanel paradoxPanel = new ParadoxPanel();
private ParadoxInformation panelInformation;
private List<Partition> partitions;
private List<Zone> zones;
private IParadoxParser parser;
private IParadoxCommunicator communicator;
private double vdcLevel;
private double batteryLevel;
private double dcLevel;
private ZonedDateTime panelTime;
private ParadoxPanel() {
this.parser = new EvoParser();
}
public void createModelEntities() {
byte[] panelInfoBytes = communicator.getPanelInfoBytes();
panelInformation = new ParadoxInformation(panelInfoBytes, parser);
if (isPanelSupported()) {
logger.info("Paradox system is supported. Panel data retrieved={} ", panelInformation);
createPartitions();
createZones();
} else {
throw new ParadoxRuntimeException(
"Unsupported panel type. Type: " + panelInformation.getPanelType().name());
}
}
public static ParadoxPanel getInstance() {
return paradoxPanel;
}
public boolean isPanelSupported() {
PanelType panelType = panelInformation.getPanelType();
return panelType == PanelType.EVO48 || panelType == PanelType.EVO192 || panelType == PanelType.EVOHD;
}
public void updateEntitiesStates() {
if (!isOnline()) {
logger.debug("Not online. Unable to update entities states. ");
return;
}
List<byte[]> currentPartitionFlags = communicator.getPartitionFlags();
for (int i = 0; i < partitions.size(); i++) {
Partition partition = partitions.get(i);
if (i < currentPartitionFlags.size()) {
partition.setState(parser.calculatePartitionState(currentPartitionFlags.get(i)));
} else {
logger.debug("Partition flags collection is smaller than the number of partitions.");
}
}
ZoneStateFlags zoneStateFlags = communicator.getZoneStateFlags();
for (int i = 0; i < zones.size(); i++) {
Zone zone = zones.get(i);
zone.setZoneState(parser.calculateZoneState(zone.getId(), zoneStateFlags));
}
byte[] firstRamPage = communicator.getMemoryMap().getElement(0);
panelTime = constructPanelTime(firstRamPage);
vdcLevel = Math.max(0, (firstRamPage[25] & 0xFF) * (20.3 - 1.4) / 255.0 + 1.4);
dcLevel = Math.max(0, (firstRamPage[27] & 0xFF) * 22.8 / 255);
batteryLevel = Math.max(0, (firstRamPage[26] & 0xFF) * 22.8 / 255);
}
protected ZonedDateTime constructPanelTime(byte[] firstPage) {
try {
int year = firstPage[18] * 100 + firstPage[19];
return ZonedDateTime.of(
LocalDateTime.of(year, firstPage[20], firstPage[21], firstPage[22], firstPage[23], firstPage[24]),
TimeZone.getDefault().toZoneId());
} catch (DateTimeException e) {
logger.debug("Received exception during parsing panel time. Falling back to old time.", e);
return panelTime;
}
}
private List<Zone> createZones() {
zones = new ArrayList<>();
Map<Integer, String> zoneLabels = communicator.getZoneLabels();
for (int i = 0; i < zoneLabels.size(); i++) {
Zone zone = new Zone(i + 1, zoneLabels.get(i));
zones.add(zone);
}
return zones;
}
private List<Partition> createPartitions() {
partitions = new ArrayList<>();
Map<Integer, String> partitionLabels = communicator.getPartitionLabels();
for (int i = 0; i < partitionLabels.size(); i++) {
Partition partition = new Partition(i + 1, partitionLabels.get(i));
partitions.add(partition);
logger.debug("Partition {}:\t{}", i + 1, partition.getState().getMainState());
}
return partitions;
}
@Override
public void update() {
if (panelInformation == null || partitions == null || zones == null) {
createModelEntities();
}
updateEntitiesStates();
}
public void dispose() {
this.panelInformation = null;
this.partitions = null;
this.zones = null;
}
public ParadoxInformation getPanelInformation() {
return panelInformation;
}
public List<Partition> getPartitions() {
return partitions;
}
public void setPartitions(List<Partition> partitions) {
this.partitions = partitions;
}
public List<Zone> getZones() {
return zones;
}
public void setZones(List<Zone> zones) {
this.zones = zones;
}
public boolean isOnline() {
return communicator != null && communicator.isOnline();
}
public double getVdcLevel() {
return vdcLevel;
}
public double getBatteryLevel() {
return batteryLevel;
}
public double getDcLevel() {
return dcLevel;
}
public ZonedDateTime getPanelTime() {
return panelTime;
}
public void setCommunicator(IParadoxCommunicator communicator) {
this.communicator = communicator;
}
public IParadoxCommunicator getCommunicator() {
return communicator;
}
@Override
public String toString() {
return "ParadoxPanel [panelInformation=" + panelInformation + "]";
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.openhab.binding.paradoxalarm.internal.communication.IParadoxCommunicator;
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.handlers.Commandable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Partition} Paradox partition.
* ID is always numeric (1-8 for Evo192)
*
* @author Konstantin Polihronov - Initial contribution
*/
public class Partition extends Entity implements Commandable {
private final Logger logger = LoggerFactory.getLogger(Partition.class);
private PartitionState state = new PartitionState();
public Partition(int id, String label) {
super(id, label);
}
public PartitionState getState() {
return state;
}
public Partition setState(PartitionState state) {
this.state = state;
logger.debug("Partition {}:\t{}", getLabel(), getState().getMainState());
return this;
}
@Override
public void handleCommand(String command) {
PartitionCommand partitionCommand = PartitionCommand.parse(command);
if (partitionCommand == PartitionCommand.UNKNOWN) {
logger.debug("Command UNKNOWN will be ignored.");
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);
IParadoxCommunicator communicator = ParadoxPanel.getInstance().getCommunicator();
communicator.submitRequest(request);
}
}

View File

@@ -0,0 +1,263 @@
/**
* Copyright (c) 2010-2020 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.model;
/**
* The {@link Partition} Paradox partition states. Retrieved and parsed from RAM memory responses.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class PartitionState {
private boolean isArmed;
private boolean isArmedInAway;
private boolean isArmedInStay;
private boolean isArmedInNoEntry;
private boolean isInAlarm;
private boolean isInSilentAlarm;
private boolean isInAudibleAlarm;
private boolean isInFireAlarm;
private boolean isReadyToArm;
private boolean isInExitDelay;
private boolean isInEntryDelay;
private boolean isInTrouble;
private boolean hasAlarmInMemory;
private boolean isInZoneBypass;
private boolean hasZoneInTamperTrouble;
private boolean hasZoneInLowBatteryTrouble;
private boolean hasZoneInFireLoopTrouble;
private boolean hasZoneInSupervisionTrouble;
private boolean isStayInstantReady;
private boolean isForceReady;
private boolean isBypassReady;
private boolean isInhibitReady;
private boolean areAllZoneclosed;
public String getMainState() {
if (isInAlarm) {
return "InAlarm";
} else {
return isArmed || isArmedInAway || isArmedInStay || isArmedInNoEntry ? "Armed" : "Disarmed";
}
}
@Override
public String toString() {
return "PartitionState [isArmed=" + isArmed + ", isArmedInAway=" + isArmedInAway + ", isArmedInStay="
+ isArmedInStay + ", isArmedInNoEntry=" + isArmedInNoEntry + ", isInAlarm=" + isInAlarm
+ ", isInSilentAlarm=" + isInSilentAlarm + ", isInAudibleAlarm=" + isInAudibleAlarm + ", isInFireAlarm="
+ isInFireAlarm + ", isReadyToArm=" + isReadyToArm + ", isInExitDelay=" + isInExitDelay
+ ", isInEntryDelay=" + isInEntryDelay + ", isInTrouble=" + isInTrouble + ", hasAlarmInMemory="
+ hasAlarmInMemory + ", isInZoneBypass=" + isInZoneBypass + ", hasZoneInTamperTrouble="
+ hasZoneInTamperTrouble + ", hasZoneInLowBatteryTrouble=" + hasZoneInLowBatteryTrouble
+ ", hasZoneInFireLoopTrouble=" + hasZoneInFireLoopTrouble + ", hasZoneInSupervisionTrouble="
+ hasZoneInSupervisionTrouble + ", isStayInstantReady=" + isStayInstantReady + ", isForceReady="
+ isForceReady + ", isBypassReady=" + isBypassReady + ", isInhibitReady=" + isInhibitReady
+ ", areAllZoneclosed=" + areAllZoneclosed + "]";
}
public boolean isArmed() {
return isArmed;
}
public void setArmed(boolean isArmed) {
this.isArmed = isArmed;
}
public boolean isArmedInAway() {
return isArmedInAway;
}
public void setArmedInAway(boolean isArmedInAway) {
this.isArmedInAway = isArmedInAway;
}
public boolean isArmedInStay() {
return isArmedInStay;
}
public void setArmedInStay(boolean isArmedInStay) {
this.isArmedInStay = isArmedInStay;
}
public boolean isArmedInNoEntry() {
return isArmedInNoEntry;
}
public void setArmedInNoEntry(boolean isArmedInNoEntry) {
this.isArmedInNoEntry = isArmedInNoEntry;
}
public boolean isInAlarm() {
return isInAlarm;
}
public void setInAlarm(boolean isInAlarm) {
this.isInAlarm = isInAlarm;
}
public boolean isInSilentAlarm() {
return isInSilentAlarm;
}
public void setInSilentAlarm(boolean isInSilentAlarm) {
this.isInSilentAlarm = isInSilentAlarm;
}
public boolean isInAudibleAlarm() {
return isInAudibleAlarm;
}
public void setInAudibleAlarm(boolean isInAudibleAlarm) {
this.isInAudibleAlarm = isInAudibleAlarm;
}
public boolean isInFireAlarm() {
return isInFireAlarm;
}
public void setInFireAlarm(boolean isInFireAlarm) {
this.isInFireAlarm = isInFireAlarm;
}
public boolean isReadyToArm() {
return isReadyToArm;
}
public void setReadyToArm(boolean isReadyToArm) {
this.isReadyToArm = isReadyToArm;
}
public boolean isInExitDelay() {
return isInExitDelay;
}
public void setInExitDelay(boolean isInExitDelay) {
this.isInExitDelay = isInExitDelay;
}
public boolean isInEntryDelay() {
return isInEntryDelay;
}
public void setInEntryDelay(boolean isInEntryDelay) {
this.isInEntryDelay = isInEntryDelay;
}
public boolean isInTrouble() {
return isInTrouble;
}
public void setInTrouble(boolean isInTrouble) {
this.isInTrouble = isInTrouble;
}
public boolean isHasAarmInMemory() {
return hasAlarmInMemory;
}
public void setHasAarmInMemory(boolean hasAarmInMemory) {
this.hasAlarmInMemory = hasAarmInMemory;
}
public boolean isInZoneBypass() {
return isInZoneBypass;
}
public void setInZoneBypass(boolean isInZoneBypass) {
this.isInZoneBypass = isInZoneBypass;
}
public boolean isHasZoneInTamperTrouble() {
return hasZoneInTamperTrouble;
}
public void setHasZoneInTamperTrouble(boolean hasZoneInTamperTrouble) {
this.hasZoneInTamperTrouble = hasZoneInTamperTrouble;
}
public boolean isHasZoneInLowBatteryTrouble() {
return hasZoneInLowBatteryTrouble;
}
public void setHasZoneInLowBatteryTrouble(boolean hasZoneInLowBatteryTrouble) {
this.hasZoneInLowBatteryTrouble = hasZoneInLowBatteryTrouble;
}
public boolean isHasZoneInFireLoopTrouble() {
return hasZoneInFireLoopTrouble;
}
public void setHasZoneInFireLoopTrouble(boolean hasZoneInFireLoopTrouble) {
this.hasZoneInFireLoopTrouble = hasZoneInFireLoopTrouble;
}
public boolean isHasZoneInSupervisionTrouble() {
return hasZoneInSupervisionTrouble;
}
public void setHasZoneInSupervisionTrouble(boolean hasZoneInSupervisionTrouble) {
this.hasZoneInSupervisionTrouble = hasZoneInSupervisionTrouble;
}
public boolean isStayInstantReady() {
return isStayInstantReady;
}
public void setStayInstantReady(boolean isStayInstantReady) {
this.isStayInstantReady = isStayInstantReady;
}
public boolean isForceReady() {
return isForceReady;
}
public void setForceReady(boolean isForceReady) {
this.isForceReady = isForceReady;
}
public boolean isBypassReady() {
return isBypassReady;
}
public void setBypassReady(boolean isBypassReady) {
this.isBypassReady = isBypassReady;
}
public boolean isInhibitReady() {
return isInhibitReady;
}
public void setInhibitReady(boolean isInhibitReady) {
this.isInhibitReady = isInhibitReady;
}
public boolean isAreAllZoneclosed() {
return areAllZoneclosed;
}
public void setAllZoneClosed(boolean areAllZoneclosed) {
this.areAllZoneclosed = areAllZoneclosed;
}
public boolean isHasAlarmInMemory() {
return hasAlarmInMemory;
}
public void setHasAlarmInMemory(boolean hasAlarmInMemory) {
this.hasAlarmInMemory = hasAlarmInMemory;
}
}

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2020 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.model;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Version} This class holds version information
*
* @author Konstantin Polihronov - Initial contribution
*/
public class Version {
private final Logger logger = LoggerFactory.getLogger(Version.class);
private Byte version;
private Byte revision;
private Byte build;
private Date buildTime;
public Version(Byte version, Byte revision) {
this(version, revision, null);
}
public Version(Byte version, Byte revision, Byte build) {
this(version, revision, build, null);
}
public Version(Byte version, Byte revision, Byte build, Date buildTime) {
this.version = version;
this.revision = revision;
this.build = build;
this.buildTime = buildTime;
logger.debug("version={}", this);
}
public Byte getVersion() {
return version;
}
public void setVersion(Byte version) {
this.version = version;
}
public Byte getRevision() {
return revision;
}
public void setRevision(Byte revision) {
this.revision = revision;
}
public Byte getBuild() {
return build;
}
public void setBuild(Byte build) {
this.build = build;
}
public Date getBuildTime() {
return buildTime;
}
public void setBuildTime(Date buildTime) {
this.buildTime = buildTime;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Version: ");
sb.append(version);
sb.append(".");
sb.append(revision);
if (build != null) {
sb.append(".");
sb.append(build);
}
if (buildTime != null) {
sb.append("/");
sb.append(buildTime);
}
return sb.toString();
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Zone} Paradox zone.
* ID is always numeric (1-192 for Evo192)
* States are taken from cached RAM memory map and parsed.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class Zone extends Entity {
private final Logger logger = LoggerFactory.getLogger(Zone.class);
private ZoneState zoneState;
public Zone(int id, String label) {
super(id, label);
}
public 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());
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 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.model;
/**
* The {@link ZoneStateFlags} Paradox zone state flags. Retrieved and parsed from RAM memory responses.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ZoneState {
private boolean isOpened;
private boolean isTampered;
private boolean hasLowBattery;
public ZoneState(boolean isOpened, boolean isTampered, boolean hasLowBattery) {
this.isOpened = isOpened;
this.isTampered = isTampered;
this.hasLowBattery = hasLowBattery;
}
public boolean isOpened() {
return isOpened;
}
public void setOpened(boolean isOpened) {
this.isOpened = isOpened;
}
public boolean isTampered() {
return isTampered;
}
public void setTampered(boolean isTampered) {
this.isTampered = isTampered;
}
public boolean hasLowBattery() {
return hasLowBattery;
}
public void setHasLowBattery(boolean hasLowBattery) {
this.hasLowBattery = hasLowBattery;
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 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.model;
/**
* The {@link ZoneStateFlags} Paradox zone state flags. Retrieved and parsed from RAM memory responses.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ZoneStateFlags {
private byte[] zonesOpened;
private byte[] zonesTampered;
private byte[] zonesLowBattery;
public byte[] getZonesOpened() {
return zonesOpened;
}
public void setZonesOpened(byte[] zonesOpened) {
this.zonesOpened = zonesOpened;
}
public byte[] getZonesTampered() {
return zonesTampered;
}
public void setZonesTampered(byte[] zonesTampered) {
this.zonesTampered = zonesTampered;
}
public byte[] getZonesLowBattery() {
return zonesLowBattery;
}
public void setZonesLowBattery(byte[] zonesLowBattery) {
this.zonesLowBattery = zonesLowBattery;
}
}

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 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.parsers;
import org.openhab.binding.paradoxalarm.internal.model.Version;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AbstractParser} Contains parsing methods irelevant from panel type
*
* @author Konstantin Polihronov - Initial contribution
*/
public abstract class AbstractParser implements IParadoxParser {
private final Logger logger = LoggerFactory.getLogger(AbstractParser.class);
@Override
public Version parseApplicationVersion(byte[] panelInfoBytes) {
return new Version(translateAsNibbles(panelInfoBytes[9]), translateAsNibbles(panelInfoBytes[10]),
translateAsNibbles(panelInfoBytes[11]));
}
@Override
public Version parseHardwareVersion(byte[] panelInfoBytes) {
return new Version(translateAsNibbles(panelInfoBytes[16]), translateAsNibbles(panelInfoBytes[17]));
}
@Override
public Version parseBootloaderVersion(byte[] panelInfoBytes) {
return new Version(translateAsNibbles(panelInfoBytes[18]), translateAsNibbles(panelInfoBytes[19]),
translateAsNibbles(panelInfoBytes[20]));
}
private byte translateAsNibbles(byte byteValue) {
return (byte) ((ParadoxUtil.getHighNibble(byteValue) * 10 + ParadoxUtil.getLowNibble(byteValue)) & 0xFF);
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 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.parsers;
import org.openhab.binding.paradoxalarm.internal.model.PartitionState;
import org.openhab.binding.paradoxalarm.internal.model.ZoneState;
import org.openhab.binding.paradoxalarm.internal.model.ZoneStateFlags;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
/**
* The {@link EvoParser} Implementation of parser interface for EVO type panels
*
* @author Konstantin Polihronov - Initial contribution
*/
public class EvoParser extends AbstractParser {
@Override
public PartitionState calculatePartitionState(byte[] partitionFlags) {
byte firstByte = partitionFlags[0];
PartitionState state = new PartitionState();
state.setArmed(ParadoxUtil.isBitSet(firstByte, 0));
state.setArmedInAway(ParadoxUtil.isBitSet(firstByte, 1));
state.setArmedInStay(ParadoxUtil.isBitSet(firstByte, 2));
state.setArmedInNoEntry(ParadoxUtil.isBitSet(firstByte, 3));
state.setInAlarm(ParadoxUtil.isBitSet(firstByte, 4));
state.setInSilentAlarm(ParadoxUtil.isBitSet(firstByte, 5));
state.setInAudibleAlarm(ParadoxUtil.isBitSet(firstByte, 6));
state.setInFireAlarm(ParadoxUtil.isBitSet(firstByte, 7));
byte secondByte = partitionFlags[1];
state.setReadyToArm(ParadoxUtil.isBitSet(secondByte, 0));
state.setInExitDelay(ParadoxUtil.isBitSet(secondByte, 1));
state.setInEntryDelay(ParadoxUtil.isBitSet(secondByte, 2));
state.setInTrouble(ParadoxUtil.isBitSet(secondByte, 3));
state.setHasAlarmInMemory(ParadoxUtil.isBitSet(secondByte, 4));
state.setInZoneBypass(ParadoxUtil.isBitSet(secondByte, 5));
byte thirdByte = partitionFlags[2];
state.setHasZoneInTamperTrouble(ParadoxUtil.isBitSet(thirdByte, 4));
state.setHasZoneInLowBatteryTrouble(ParadoxUtil.isBitSet(thirdByte, 5));
state.setHasZoneInFireLoopTrouble(ParadoxUtil.isBitSet(thirdByte, 6));
state.setHasZoneInSupervisionTrouble(ParadoxUtil.isBitSet(thirdByte, 7));
byte sixthByte = partitionFlags[5];
state.setStayInstantReady(ParadoxUtil.isBitSet(sixthByte, 0));
state.setForceReady(ParadoxUtil.isBitSet(sixthByte, 1));
state.setBypassReady(ParadoxUtil.isBitSet(sixthByte, 2));
state.setInhibitReady(ParadoxUtil.isBitSet(sixthByte, 3));
state.setAllZoneClosed(ParadoxUtil.isBitSet(sixthByte, 4));
return state;
}
@Override
public ZoneState calculateZoneState(int id, ZoneStateFlags zoneStateFlags) {
int index = (id - 1) / 8;
int bitNumber = id % 8 - 1;
byte[] zonesOpened = zoneStateFlags.getZonesOpened();
boolean isOpened = ParadoxUtil.isBitSet(zonesOpened[index], bitNumber);
byte[] zonesTampered = zoneStateFlags.getZonesTampered();
boolean isTampered = ParadoxUtil.isBitSet(zonesTampered[index], bitNumber);
byte[] zonesLowBattery = zoneStateFlags.getZonesLowBattery();
boolean hasLowBattery = ParadoxUtil.isBitSet(zonesLowBattery[index], bitNumber);
return new ZoneState(isOpened, isTampered, hasLowBattery);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 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.parsers;
import org.openhab.binding.paradoxalarm.internal.model.PartitionState;
import org.openhab.binding.paradoxalarm.internal.model.Version;
import org.openhab.binding.paradoxalarm.internal.model.ZoneState;
import org.openhab.binding.paradoxalarm.internal.model.ZoneStateFlags;
/**
* The {@link IParadoxParser} Interface for Paradox Parsers implementations
*
* @author Konstantin Polihronov - Initial contribution
*/
public interface IParadoxParser {
PartitionState calculatePartitionState(byte[] partitionFlags);
ZoneState calculateZoneState(int id, ZoneStateFlags zoneStateFlags);
Version parseApplicationVersion(byte[] panelInfoBytes);
Version parseHardwareVersion(byte[] panelInfoBytes);
Version parseBootloaderVersion(byte[] panelInfoBytes);
}

View File

@@ -0,0 +1,229 @@
/**
* Copyright (c) 2010-2020 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.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ParadoxUtil} Utility class for different calculations / manipulations of data in the model and
* communicators.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class ParadoxUtil {
private static final String SPACE_DELIMITER = " ";
private final static Logger logger = LoggerFactory.getLogger(ParadoxUtil.class);
public static byte calculateChecksum(byte[] payload) {
int result = 0;
for (byte everyByte : payload) {
result += everyByte;
}
return (byte) (result % 256);
}
public static byte getBit(int value, int bitNumber) {
return (byte) ((value >> bitNumber) & 1);
}
public static boolean isBitSet(int value, int bitNumber) {
return ((value >> bitNumber) & 1) == 1;
}
public static void printPacket(String description, byte[] array) {
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");
return;
}
printByteArray(description, array, array.length);
}
public static void printByteArray(String description, byte[] array, int length) {
if (!logger.isTraceEnabled()) {
return;
}
String result = byteArrayToString(array, length);
if (!result.isEmpty()) {
logger.trace("{}", description + SPACE_DELIMITER + result);
}
}
public static String byteArrayToString(byte[] array) {
return byteArrayToString(array, array.length);
}
/**
*
* Returns passed array as HEX string. On every 8 bytes we put space for better readability. Example 16
* bytes array output: AA47000263000000 03EE00EEEEEEB727
*
* @param array
* @param length
* @return String
*/
public static String byteArrayToString(byte[] array, int length) {
if (array == null) {
throw new IllegalArgumentException("Array must not be null.");
}
if (length > array.length) {
throw new IllegalArgumentException("Length should be lower than or equal to array length. Length=" + length
+ ". Array length=" + array.length);
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
if (i != 0 && i % 8 == 0) {
sb.append(SPACE_DELIMITER);
}
sb.append(String.format("%02X", array[i]));
}
return sb.toString();
}
public static byte setBit(byte byteValue, int i, int j) {
if (j == 1) {
return (byte) (byteValue | (1 << i));
} else {
return (byte) (byteValue & ~(1 << i));
}
}
public static byte getHighNibble(byte value) {
return (byte) ((value & 0xF0) >> 4);
}
public static byte getLowNibble(byte value) {
return (byte) (value & 0x0F);
}
public static byte[] mergeByteArrays(byte[]... arrays) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
for (byte[] array : arrays) {
outputStream.write(array);
}
byte[] byteArray = outputStream.toByteArray();
return byteArray;
} catch (IOException e) {
logger.warn("Exception merging arrays:", e);
return new byte[0];
}
}
public static byte[] intToByteArray(int value) {
return ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).order(ByteOrder.BIG_ENDIAN).putInt(value).array();
}
public static byte[] shortToByteArray(short value) {
return ByteBuffer.allocate(Short.SIZE / Byte.SIZE).order(ByteOrder.BIG_ENDIAN).putShort(value).array();
}
public static byte[] stringToBCD(String pcPassword) {
return stringToBCD(pcPassword, 4);
}
public static byte[] stringToBCD(String pcPassword, int numberOfDigits) {
byte[] result = new byte[numberOfDigits / 2];
for (int i = 0, j = 0; i < 2; i++, j += 2) {
String substring = pcPassword.substring(j, j + 1);
int parseInt = Integer.parseInt(substring);
result[i] = (byte) ((parseInt & 0x0F) << 4);
substring = pcPassword.substring(j + 1, j + 2);
parseInt = Integer.parseInt(substring);
result[i] |= (byte) (parseInt & 0x0F);
}
return result;
}
/**
* This method fills array with 0xEE based on rate.
* Example: If input array length is 5 and rate is 8 the array will be extended with 3 more bytes filled with 0xEE
*
* @param inputArray
* @param rate
* @return byte[]
*/
public static byte[] extendArray(byte[] inputArray, int rate) {
if (inputArray == null || inputArray.length % rate == 0) {
return inputArray;
}
final int newLength = inputArray.length + (rate - inputArray.length % rate);
byte[] result = new byte[newLength];
for (int i = 0; i < result.length; i++) {
if (i < inputArray.length) {
result[i] = inputArray[i];
} else {
result[i] = (byte) 0xEE;
}
}
return result;
}
/**
* Returns bytes from string with standard US_ASCII standard charset to ensure everywhere in the binding we use same
* charset.
*
* @param str
* @return byte[]
*
*/
public static byte[] getBytesFromString(String str) {
if (str == null) {
throw new IllegalArgumentException("String must not be null !");
}
return str.getBytes(StandardCharsets.US_ASCII);
}
public static int[] toIntArray(byte[] input) {
if (input == null) {
throw new IllegalArgumentException("Input array must not be null");
}
int[] result = new int[input.length];
for (int i = 0; i < input.length; i++) {
result[i] = input[i] & 0xFF;
}
return result;
}
public static byte[] toByteArray(int[] input) {
if (input == null) {
throw new IllegalArgumentException("Input array must not be null");
}
byte[] result = new byte[input.length];
for (int i = 0; i < input.length; i++) {
result[i] = (byte) (input[i]);
}
return result;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="paradoxalarm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>ParadoxAlarm Binding</name>
<description>This is the binding for ParadoxAlarm.</description>
<author>Konstantin Polihronov</author>
</binding:binding>

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="paradoxalarm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="ip150">
<label>Paradox IP150 Module Connector</label>
<description>Paradox IP150 module connector</description>
<channels>
<channel id="communicationCommand" typeId="command"/>
<channel id="communicationState" typeId="communicationState"/>
</channels>
<config-description>
<parameter name="refresh" type="integer" min="1" max="600">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in seconds.</description>
<default>5</default>
</parameter>
<parameter name="panelType" type="text" required="false">
<label>Panel Type</label>
<description>PanelType</description>
</parameter>
<parameter name="ip150Password" type="text" required="true">
<label>IP150 Password</label>
<description>Password for accessing IP150</description>
<context>password</context>
<required>true</required>
</parameter>
<parameter name="pcPassword" type="text" required="true">
<label>PC Password</label>
<description>PC Password (section 3012 of configuration)</description>
<default>0000</default>
<context>password</context>
</parameter>
<parameter name="ipAddress" type="text" required="true">
<label>Network Address</label>
<description>IP address or host name of IP150</description>
<context>network-address</context>
<required>true</required>
</parameter>
<parameter name="port" type="integer" required="true">
<label>Port</label>
<description>Port to connect to IP150</description>
<default>10000</default>
</parameter>
<parameter name="reconnectWaitTime" type="integer" required="true" unit="s">
<label>Reconnect Wait Time</label>
<description>The time to wait before a reconnect occurs after socket timeout. Value is in seconds</description>
<default>30</default>
</parameter>
<parameter name="maxZones" type="integer" unit="s">
<label>Max Zones</label>
<description>Maximum number of configured zones to check (from zone 1 to maxZones)</description>
<default>0</default>
</parameter>
<parameter name="maxPartitions" type="integer" unit="s">
<label>Max Partitions</label>
<description>Maximum number of configured partitions to check (from partition 1 to maxPartitions)</description>
<default>0</default>
</parameter>
<parameter name="encrypt" type="boolean">
<label>Use Encryption</label>
<description>If set to true will use encrypted communication. Default=false for backward compatibility</description>
<default>false</default>
</parameter>
</config-description>
</bridge-type>
<channel-type id="command">
<item-type>String</item-type>
<label>Communicator Command</label>
<description>Send Command</description>
</channel-type>
<channel-type id="communicationState">
<item-type>String</item-type>
<label>Bridge Communication State</label>
<description>Status of connection to Paradox system</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="paradoxalarm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="panel">
<supported-bridge-type-refs>
<bridge-type-ref id="ip150"/>
</supported-bridge-type-refs>
<label>Paradox Panel</label>
<description>Paradox panel various information</description>
<channels>
<channel id="state" typeId="panelState"/>
<channel id="inputVoltage" typeId="voltage"/>
<channel id="boardVoltage" typeId="voltage"/>
<channel id="batteryVoltage" typeId="voltage"/>
<channel id="panelTime" typeId="panelTime"/>
</channels>
<properties>
<property name="serialNumber"/>
<property name="panelType"/>
<property name="hardwareVersion"/>
<property name="applicationVersion"/>
<property name="bootloaderVersion"/>
</properties>
</thing-type>
<channel-type id="panelState">
<item-type>String</item-type>
<label>Panel State</label>
<description>Shows the state of the communication to panel (online/offline)</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="serialNumber">
<item-type>String</item-type>
<label>Serial Number</label>
<description>Panel serial number</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="panelType">
<item-type>String</item-type>
<label>Panel Type</label>
<description>Panel type (Evo, SP, etc)</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="hardwareVersion">
<item-type>String</item-type>
<label>Hardware Version</label>
<description>Panel hardware version</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="applicationVersion">
<item-type>String</item-type>
<label>Application Version</label>
<description>Panel application version</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="bootloaderVersion">
<item-type>String</item-type>
<label>Boot Loader Version</label>
<description>Boot loader version</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="voltage">
<item-type>Number:ElectricPotential</item-type>
<label>Voltage</label>
<description>Voltage</description>
<state readOnly="true" pattern="%f %unit%"/>
</channel-type>
<channel-type id="panelTime">
<item-type>DateTime</item-type>
<label>Date</label>
<description>The current date and time on the panel</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="paradoxalarm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="partition">
<supported-bridge-type-refs>
<bridge-type-ref id="ip150"/>
</supported-bridge-type-refs>
<label>Paradox Partition</label>
<description>Paradox Partition</description>
<channels>
<channel id="label" typeId="partitionLabel"/>
<channel id="state" typeId="state"/>
<channel id="additionalStates" typeId="additionalState"/>
<channel id="readyToArm" typeId="readyToArm"/>
<channel id="inExitDelay" typeId="inExitDelay"/>
<channel id="inEntryDelay" typeId="inEntryDelay"/>
<channel id="inTrouble" typeId="inTrouble"/>
<channel id="alarmInMemory" typeId="alarmInMemory"/>
<channel id="zoneBypass" typeId="zoneBypass"/>
<channel id="zoneInTamperTrouble" typeId="zoneInTamperTrouble"/>
<channel id="zoneInLowBatteryTrouble" typeId="zoneInLowBatteryTrouble"/>
<channel id="zoneInFireLoopTrouble" typeId="zoneInFireLoopTrouble"/>
<channel id="zoneInSupervisionTrouble" typeId="zoneInSupervisionTrouble"/>
<channel id="stayInstantReady" typeId="stayInstantReady"/>
<channel id="forceReady" typeId="forceReady"/>
<channel id="bypassReady" typeId="bypassReady"/>
<channel id="inhibitReady" typeId="inhibitReady"/>
<channel id="allZonesClosed" typeId="allZonesClosed"/>
<channel id="command" typeId="command"/>
</channels>
<config-description>
<parameter name="id" type="integer" min="1" max="8" required="true">
<label>Partition Id</label>
<description>This is the partition ID of the Paradox partition (1-8 for Evo)</description>
</parameter>
<parameter name="disarmEnabled" type="boolean">
<label>Disarm Command Enabled</label>
<description>Parameter that enables option to send DISARM command to the partition. (DANGEROUS !)</description>
<default>false</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="partitionLabel">
<item-type>String</item-type>
<label>Partition Label</label>
<description>Label of partition</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="state">
<item-type>String</item-type>
<label>Partition State</label>
<description>State of partition</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="additionalState">
<item-type>String</item-type>
<label>Partition Additional States</label>
<description>Additional states provided by panel (deprecated channel)</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="readyToArm">
<item-type>switch</item-type>
<label>Partition Ready To Arm</label>
<description>Partition ready to arm</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="inExitDelay">
<item-type>switch</item-type>
<label>Partition In Exit Delay</label>
<description>Partition in Exit Delay</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="inEntryDelay">
<item-type>switch</item-type>
<label>Partition In Entry Delay</label>
<description>Partition in Entry Delay</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="inTrouble">
<item-type>switch</item-type>
<label>Partition In Trouble</label>
<description>Partition in Trouble</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="alarmInMemory">
<item-type>switch</item-type>
<label>Partition Has Alarm In Memory</label>
<description>Partition has Alarm in Memory</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="zoneBypass">
<item-type>switch</item-type>
<label>Partition Has Zone Bypass</label>
<description>Partition has Zone Bypass</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="zoneInTamperTrouble">
<item-type>switch</item-type>
<label>Partition Has Zone In Tamper Trouble</label>
<description>Partition has in Tamper Trouble</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="zoneInLowBatteryTrouble">
<item-type>switch</item-type>
<label>Partition Has Zone In Low Battery Trouble</label>
<description>Partition has in Low Battery Trouble</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="zoneInFireLoopTrouble">
<item-type>switch</item-type>
<label>Partition Has Zone In Fire Loop Trouble</label>
<description>Partition has in Fire Loop Trouble</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="zoneInSupervisionTrouble">
<item-type>switch</item-type>
<label>Partition Has Zone In Supervision Trouble</label>
<description>Partition has in Supervision Trouble</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="stayInstantReady">
<item-type>switch</item-type>
<label>Partition Is Stay Instant Ready</label>
<description>Partition is Stay Instant Ready</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="forceReady">
<item-type>switch</item-type>
<label>Partition Is Force Ready</label>
<description>Partition is Force Ready</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="bypassReady">
<item-type>switch</item-type>
<label>Partition Is Bypass Ready</label>
<description>Partition is Bypass Ready</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="inhibitReady">
<item-type>switch</item-type>
<label>Partition Is Inhibit Ready</label>
<description>Partition is Inhibit Ready</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="allZonesClosed">
<item-type>contact</item-type>
<label>Partition Has All Zones Closed</label>
<description>Partition has All Zones closed</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="command">
<item-type>String</item-type>
<label>Partition Command</label>
<description>Send command</description>
<state>
<options>
<option value="ARM">Arm</option>
<option value="STAY_ARM">Stay Arm</option>
<option value="INSTANT_ARM">Instant Arm</option>
<option value="FORCE_ARM">Force Arm</option>
<option value="DISARM">Disarm</option>
<option value="BEEP">Keyboard Beep</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="paradoxalarm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="zone">
<supported-bridge-type-refs>
<bridge-type-ref id="ip150"/>
</supported-bridge-type-refs>
<label>Paradox Zone</label>
<description>Paradox zone</description>
<channels>
<channel id="label" typeId="zoneLabel"/>
<channel id="opened" typeId="openedState"/>
<channel id="tampered" typeId="tamperedState"/>
<channel id="lowBattery" typeId="system.low-battery"/>
</channels>
<config-description>
<parameter name="id" type="integer" min="1" max="192" required="true">
<label>Zone Id</label>
<description>This is the zone ID in the Paradox configuration</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="zoneLabel">
<item-type>String</item-type>
<label>Zone Label</label>
<description>Label of zone</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="openedState">
<item-type>contact</item-type>
<label>Zone State</label>
<description>State of zone</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="tamperedState">
<item-type>switch</item-type>
<label>Tampered</label>
<description>State of zone</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2020 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 main;
import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.paradoxalarm.internal.communication.ICommunicatorBuilder;
import org.openhab.binding.paradoxalarm.internal.communication.IParadoxCommunicator;
import org.openhab.binding.paradoxalarm.internal.communication.ParadoxBuilderFactory;
import org.openhab.binding.paradoxalarm.internal.model.PanelType;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link Main} - used for testing purposes of low-level stuff.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class Main {
private static Logger logger = LoggerFactory.getLogger(Main.class);
private static String ipAddress;
private static int port;
// PASSWORD is your IP150 password
private static String ip150Password;
// PC Password is the value of section 3012, i.e. if value is 0987, PC Password is two bytes 0x09, 0x87
private static String pcPassword;
private static ScheduledExecutorService scheduler;
private static IParadoxCommunicator communicator;
public static void main(String[] args) {
readArguments(args);
try {
scheduler = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
logger.info("System properties={}",
System.getProperties().get(org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY));
ParadoxBuilderFactory factory = new ParadoxBuilderFactory();
ICommunicatorBuilder builder = factory.createBuilder(PanelType.EVO192);
communicator = builder.withIp150Password(ip150Password).withPcPassword(pcPassword).withIpAddress(ipAddress)
.withTcpPort(port).withMaxPartitions(4).withMaxZones(20).withScheduler(scheduler)
.withEncryption(true).build();
ParadoxPanel panel = ParadoxPanel.getInstance();
panel.setCommunicator(communicator);
communicator.setListeners(Arrays.asList(panel));
communicator.startLoginSequence();
scheduler.scheduleWithFixedDelay(() -> {
refreshMemoryMap(communicator, false);
}, 7, 5, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("Exception: ", e);
System.exit(0);
}
}
private static void refreshMemoryMap(IParadoxCommunicator communicator, boolean withEpromValues) {
logger.debug("Refreshing memory map");
communicator.refreshMemoryMap();
ParadoxPanel panel = ParadoxPanel.getInstance();
panel.getPartitions().stream().forEach(partition -> logger.debug("Partition={}", partition));
panel.getZones().stream().filter(zone -> zone.getId() == 19).forEach(zone -> logger.debug("Zone={}", zone));
logger.debug("PanelTime={}, ACLevel={}, DCLevel={}, BatteryLevel={}", panel.getPanelTime(), panel.getVdcLevel(),
panel.getDcLevel(), panel.getBatteryLevel());
}
private static void readArguments(String[] args) {
MainMethodArgParser parser = new MainMethodArgParser(args);
logger.info("Arguments retrieved successfully from CLI.");
ip150Password = parser.getPassword();
logger.info("IP150 Password: {}", ip150Password);
pcPassword = parser.getPcPassword();
logger.info("PC Password: {}", pcPassword);
ipAddress = parser.getIpAddress();
logger.info("IP150 IP Address: {}", ipAddress);
port = new Integer(parser.getPort());
logger.info("IP150 port: {}", port);
}
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2020 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 main;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MainMethodArgParser} Helper class to parse the arguments of main method, which is used for direct
* communication testing.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class MainMethodArgParser {
private final Logger logger = LoggerFactory.getLogger(MainMethodArgParser.class);
private static final int NUMBER_OF_ARGUMENTS = 8;
private static final String USAGE_TEXT = "Usage: application --password <YOUR_PASSWORD_FOR_IP150> --pc_password <your PC_password> --ip_address <address of IP150> --port <port of Paradox>\n (pc password default is 0000, can be obtained by checking section 3012), default port is 10000";
private static final String KEY_PREFIX = "--";
private static final String PASSWORD_KEY = "--password";
private static final String PC_PASSWORD_KEY = "--pc_password";
private static final String IP_ADDRESS_KEY = "--ip_address";
private static final String PORT_KEY = "--port";
private static final List<String> ARGUMENT_KEYS = Arrays.asList(PASSWORD_KEY, PC_PASSWORD_KEY, IP_ADDRESS_KEY,
PORT_KEY);
private Map<String, String> argumentsMap;
public MainMethodArgParser(String[] args) {
this.argumentsMap = parseArguments(args);
validateArguments();
}
private Map<String, String> parseArguments(String[] args) {
if (args == null || args.length < NUMBER_OF_ARGUMENTS) {
logger.error(USAGE_TEXT);
throw new IllegalArgumentException("Arguments= " + Arrays.asList(args));
}
Map<String, String> result = new HashMap<>();
for (int i = 0; i < args.length;) {
if (!args[i].startsWith(KEY_PREFIX)) {
throw new IllegalArgumentException("Argument " + args[i] + " does not start with --");
}
String key = args[i];
String value;
if (args[i + 1] != null && args[i + 1].startsWith(KEY_PREFIX)) {
value = null;
i++;
} else {
value = args[i + 1];
i += 2;
}
result.put(key, value);
}
return result;
}
private void validateArguments() {
for (String argKey : ARGUMENT_KEYS) {
String value = argumentsMap.get(argKey);
if (value == null) {
logger.error(USAGE_TEXT);
throw new IllegalArgumentException("Argument " + argKey + "is mandatory");
}
}
}
public String getPassword() {
return argumentsMap.get(PASSWORD_KEY);
}
public String getPcPassword() {
return argumentsMap.get(PC_PASSWORD_KEY);
}
public String getIpAddress() {
return argumentsMap.get(IP_ADDRESS_KEY);
}
public String getPort() {
return argumentsMap.get(PORT_KEY);
}
}

View File

@@ -0,0 +1 @@
org.slf4j.simpleLogger.defaultLogLevel=trace

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 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 tests;
import org.junit.Assert;
import org.junit.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.util.ParadoxUtil;
/**
* The {@link TestCreateCommandPayload} This test tests the proper build of command payload object for partition entity
* with all types of commands.
*
* @author Konstantin Polihronov - Initial contribution
*/
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);
assertNibble(partitionNumber, command, payload);
}
}
}
private void assertNibble(int partitionNumber, PartitionCommand command, CommandPayload payload) {
byte[] bytes = payload.getBytes();
int payloadIndexOfByteToCheck = 6 + (partitionNumber - 1) / 2;
byte byteValue = bytes[payloadIndexOfByteToCheck];
if ((partitionNumber - 1) % 2 == 0) {
Assert.assertTrue(ParadoxUtil.getHighNibble(byteValue) == command.getCommand());
} else {
Assert.assertTrue(ParadoxUtil.getLowNibble(byteValue) == command.getCommand());
}
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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 tests;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;
import org.openhab.binding.paradoxalarm.internal.communication.crypto.EncryptionHandler;
import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderCommand;
import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
/**
* The {@link TestEncryptionHandler} This test tests various functions from ParadoxUtils class
*
* @author Konstantin Polihronov - Initial contribution
*/
public class TestEncryptionHandler {
private static final String INPUT_STRING = "My test string for encryption.";
private static final String KEY = "MyKeyToEncrypt";
@Test
public void testEncryptDecryptString() {
EncryptionHandler.getInstance().updateKey(ParadoxUtil.getBytesFromString(KEY));
byte[] originalBytes = ParadoxUtil.getBytesFromString(INPUT_STRING);
ParadoxUtil.printByteArray("Original=", originalBytes);
byte[] encrypted = EncryptionHandler.getInstance().encrypt(originalBytes);
assertNotEquals(originalBytes, encrypted);
byte[] decrypted = EncryptionHandler.getInstance().decrypt(encrypted);
byte[] result = decrypted.length != originalBytes.length ? Arrays.copyOf(decrypted, originalBytes.length)
: decrypted;
ParadoxUtil.printByteArray("Result=", result);
assertEquals(originalBytes.length, result.length);
assertEquals(INPUT_STRING, new String(result));
}
private static final byte[] ENCRYPTION_KEY_BYTES = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10 };
private static final byte[] ENCRYPTED_EXPECTED2 = { (byte) 0xAA, 0x0A, 0x00, 0x03, 0x09, (byte) 0xF0, 0x00, 0x00,
0x01, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE,
(byte) 0xF9, 0x11, 0x5A, (byte) 0xD7, 0x7C, (byte) 0xCB, (byte) 0xF4, 0x75, (byte) 0xB0, 0x49, (byte) 0xC3,
0x11, 0x1A, 0x41, (byte) 0x94, (byte) 0xE0 };
@Test
public void testCreateAndEncryptStartingPacket() {
ParadoxIPPacket paradoxIPPacket = new ParadoxIPPacket(ENCRYPTION_KEY_BYTES, false)
.setCommand(HeaderCommand.CONNECT_TO_IP_MODULE);
EncryptionHandler.getInstance().updateKey(ENCRYPTION_KEY_BYTES);
paradoxIPPacket.encrypt();
final byte[] packetBytes = paradoxIPPacket.getBytes();
ParadoxUtil.printByteArray("Expected=", ENCRYPTED_EXPECTED2);
ParadoxUtil.printByteArray("Packet= ", packetBytes);
Assert.assertTrue(Arrays.equals(packetBytes, ENCRYPTED_EXPECTED2));
}
}

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2020 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 tests;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.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.exceptions.ParadoxException;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TestGetBytes} This test tests creation of IP packet and it's getBytes() method
*
* @author Konstantin Polihronov - Initial contribution
*/
public class TestGetBytes {
private static final int PARTITION_NUMBER = 1;
static {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "TRACE");
}
private final static Logger logger = LoggerFactory.getLogger(ParadoxUtil.class);
private static final byte[] EXPECTED1 = { (byte) 0xAA, 0x0A, 0x00, 0x03, 0x08, (byte) 0xF0, 0x00, 0x00, 0x01,
(byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10 };
private static final byte[] PAYLOAD = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10 };
@Test
public void testParadoxIPPacket() {
ParadoxIPPacket paradoxIPPacket = new ParadoxIPPacket(PAYLOAD, false)
.setCommand(HeaderCommand.CONNECT_TO_IP_MODULE);
final byte[] packetBytes = paradoxIPPacket.getBytes();
ParadoxUtil.printByteArray("Expected =", EXPECTED1);
ParadoxUtil.printByteArray("Packet =", packetBytes);
Assert.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 };
@Test
public void testCommandPayload() {
CommandPayload payload = new CommandPayload(PARTITION_NUMBER, PartitionCommand.ARM);
final byte[] packetBytes = payload.getBytes();
ParadoxUtil.printByteArray("Expected =", EXPECTED_COMMAND_PAYLOAD);
ParadoxUtil.printByteArray("Result =", packetBytes);
Assert.assertTrue(Arrays.equals(packetBytes, EXPECTED_COMMAND_PAYLOAD));
}
private static final byte[] EXPECTED_MEMORY_PAYLOAD = { (byte) 0xAA, 0x0A, 0x00, 0x03, 0x08, (byte) 0xF0, 0x00,
0x00, 0x01, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, 0x01,
0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10 };
@Test
public void testMemoryRequestPayload() throws ParadoxException {
int address = 0x3A6B + (PARTITION_NUMBER) * 107;
byte labelLength = 16;
IPayload payload = new EpromRequestPayload(address, labelLength);
byte[] bytes = payload.getBytes();
ParadoxUtil.printByteArray("Expected =", EXPECTED_MEMORY_PAYLOAD);
ParadoxUtil.printByteArray("Result =", bytes);
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2020 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 tests;
import org.junit.Assert;
import org.junit.Test;
import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil;
/**
* The {@link TestParadoxUtil} This test tests various functions from ParadoxUtils class
*
* @author Konstantin Polihronov - Initial contribution
*/
public class TestParadoxUtil {
@Test
public void testExtendArray() {
byte[] arrayToExtend = { 0x0A, 0x50, 0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x59 };
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,
(byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE, (byte) 0xEE };
Assert.assertArrayEquals(EXPECTED_RESULT, 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[] EXPECTED_RESULT = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09 };
Assert.assertArrayEquals(EXPECTED_RESULT, mergedArrays);
}
}