Hi,
I was planning to write a small tutorial on how to integrate a Texecom alarm panel with OpenHAB. It wasn’t high on my todo list, since I had the impression I was the only one with a Texecom alarm panel on this forum. That has changed now, so here we go.
I’ll expand this tutorial with additional information in the future if needed. Let me know if you are stuck with something, and I’ll try to help.
What the Texecom-OpenHAB integration offers
With this integration you can:
- Read the status of each contact in your alarm system (open/closed/sabotage);
- Read the status of the alarm (armed/disarmed) and see which user has changed this status;
- Simulate all key presses of you key panel. This means you could arm/disarm the alarm as well;
- View what is currently displayed on the LCD panel of your alarm key pad;
The hardware part
I have a “Texecom Premier Elite 88” alarm system. I don’t have motion sensors, I only use window contacts, door contacts and glass break detectors. So far, this integration hasn’t been tested with other Texecom alarm systems, but I’m pretty sure it works with any Texecom alarm system that supports the “Crestron Protocol”.
To connect my alarm with my internal network, I also needed the:
- Texecom PC-COM Serial Interface RS232 Connector. This provides an RS232 output.
- Moxa NPort to transmit the data from RS232 to my LAN;
Currently, also an “Texecom Com-IP module” exists, which connects your Texecom directly to your LAN, making the Moxa NPort obsolete. I don’t know if the “Com-IP module” also uses the same protocol as the “PC-COM” interface.
And finally, Texecom has also released a “Connect Hub” which works with API’s. If I would need to start from scratch, I would go for this option.
Which interface protocol to use
My scenario is based on the “Crestron Protocol”. There are other protocols as well (“COM IP Protocol”, “Simple Protocol” and “Connect Hub”). The protocol documentation is unfortunately not publicly available. You can request this via their support department (techsupport@texe.com). After signing a NDA agreement, they send you all the information. I used information I could find online (e.g. homebridge-texecom integration).
The reason for choosing the “Crestron Protocol” for me was simple: In the past, I had a “Gira Homeserver” that was able to integrate my Texecom alarm with my home. I didn’t develop the Texecom-plugin for Gira myself (I purchased it). But was able to re-use some of the logic of that module for my new integration with OpenHAB. The logic was based on the Crestron protocol.
So I’m not saying that Crestron is the best or only way to integrate it. But it works fine for me.
Some things to be aware of:
- Check with your alarm installer and/or your insurance company if integrating your alarm system might make your theft insurance invalid. Regulations differ per country;
- This interface is not using any form of authentication. The interface is just sending and receiving UDP packages (which could be intercepted with tools such as Wireshark). If other users would have access to your LAN, they could communicate with your alarm system. Luckily, they can’t just switch of the alarm, they would need to send a valid code first. So this is more “security by obscurity”.
Configuring your Texecom
To configure your COM1 port for the Crestron protocol:
- Enter your engineer code
- Scroll until you find “UDL/Digi Options”
- Press 8 to jump to “Com Port Setup”
- Scroll to “Com Port 1”
- Press “No” to edit the port
- Press 8 to jump to “Crestron System”
- Press “Yes” to confirm and save.
- Press “Menu” repeatedly to exit the engineer menu.
If you don’t have the engineer code of your system, you will need to reach out to your installer.
Configuring your Moxa Nport
- Allocate a fixed IP address to your Moxa;
- In “serial settings”, define the following:
- Baud rate: 19200;
- Data bits: 8;
- Stop bits: 1;
- Parity: None;
- Flow control: None;
- FIFO: Enable;
- In “Operation mode”, define the following:
- Operation mode: UDP;
- Packing length: 0;
- Delimiter 1: a (enable);
- Delimiter process: strip delimiter;
- Force transmit: 0;
- Destination IP address and port: the IP address of your OpenHAB instance. Also choose a (unused) port, e.g. 9999.
- Local Listen port: choose a (unused) port, e.g. 9999.
OpenHAB
I’m currently running a snapshot version of OpenHAB 2.3. I’m pretty sure this works with any OpenHAB 2.X release, and maybe also with older releases. The binding I’m using is a 1.X binding, you might need to tweak the rules a little bit if you have OpenHAB 1.X.
OpenHAB – installing the UDP binding
I’m using the TCP/UDP binding to send and receive UDP packages from/to OpenHAB. You can install it via PaperUI.
In your /services folder, you should create a file called “udp.cfg”. Be aware that OH automatically creates the tcp.cfg but not the udp.cfg.
The contents if my file:
# Port to listen for incoming connections
port=<choose an unused port>
# Allow masks in ip:port addressing, e.g. 192.168.0.1:* etc
addressmask=true
# Timeout - or 'refresh interval', in milliseconds, of the worker thread
refreshinterval=250
# Used character set
charset=ASCII
your code goes here
Your OpenHAB.log should display something like this:
2018-01-09 22:57:51.772 [INFO ] [g.tcp.AbstractDatagramChannelBinding] - The maximum buffer will be set to the default value of 1024
2018-01-09 22:57:51.773 [INFO ] [g.tcp.AbstractDatagramChannelBinding] - The interval to retry connection setups will be set to the default value of 5
2018-01-09 22:57:51.776 [INFO ] [g.tcp.AbstractDatagramChannelBinding] - The cron job to reset connections will be set to the default value of 0 0 0 * * ?
2018-01-09 22:57:51.776 [INFO ] [g.tcp.AbstractDatagramChannelBinding] - The setting to share channels within an Item will be set to the default value of true
2018-01-09 22:57:51.777 [INFO ] [g.tcp.AbstractDatagramChannelBinding] - The setting to share channels between the items with the same direction will be set to the default vaulue of true
2018-01-09 22:57:51.778 [INFO ] [g.tcp.AbstractDatagramChannelBinding] - The setting to share channels between directions will be set to the default vaulue of true
2018-01-09 22:57:51.802 [INFO ] [g.tcp.AbstractDatagramChannelBinding] - Listening for incoming data on /0:0:0:0:0:0:0:0:9999
2018-01-09 22:57:51.803 [INFO ] [ing.tcp.protocol.internal.UDPBinding] - The maximum timeout for blocking write operations will be set to the default value of 3000
2018-01-09 22:57:51.804 [INFO ] [ing.tcp.protocol.internal.UDPBinding] - The blocking nature of read/write operations will be set to the default value of false
2018-01-09 22:57:51.805 [INFO ] [ing.tcp.protocol.internal.UDPBinding] - The preamble for all write operations will be set to the default value of
2018-01-09 22:57:51.805 [INFO ] [ing.tcp.protocol.internal.UDPBinding] - The postamble for all write operations will be set to the default value of
2018-01-09 22:57:51.806 [INFO ] [ing.tcp.protocol.internal.UDPBinding] - Updating states with returned values will be set to the default value of true
OpenHAB – defining items to store incoming/outgoing UDP information
You will need to define two items that will use the UDP binding, one for receiving and one for sending:
/* Alarm interface */
String Tex_UDP_Receive "Receive UDP String [%s]" <settings> (gSet) { udp="<[192.168.X.X:*:'REGEX((.*))']" }
String Tex_UDP_Send "Send UDP String" <settings> (gSet) { udp=">[192.168.X.X:9999:'REGEX((.*))']" }
The IP address should be the IP address of your Moxa NPort. The port should be the port Moxa NPort is listening to.
Secondly, you will need to define items to capture all incoming UDP information. Rules will be used to analyse incoming UDP packages and update the right items.
/* Generic alarm items */
Number Tex_Alarm_Status "Alarm status [MAP(tex_alarmstatus.map):%s]" <alarm> (gAlarm, gAlarmlogging)
Number Tex_Alarm_Partition "Alarm Partitie [%s]" <alarm> (gAlarm)
String Tex_Alarm_Textline1 "Alarm Display – line 1 [%s]" <text> (gAlarm)
String Tex_Alarm_Textline2 " Alarm Display – line 2 [%s]" <text> (gAlarm)
Number Tex_Alarm_User "User [MAP(tex_alarmusers.map):%s]" <man_3> (gAlarm, gAlarmlogging)
Number Tex_Alarm_ArmDisarmUser "User [MAP(tex_alarmusers.map):%s]" <man_3> (gAlarm, gAlarmlogging)
DateTime Tex_Alarm_Status_Update "Time alarm status changed [%1$td.%1$tm.%1$tY %1$tT]" <time> (gAlarm, gAlarmlogging)
Also, each alarm contact should have its own item. Here is an extract from my items:
/* Zone Status */
Number Tex_Zone_Z001 "Z01 - Sabotage Sirene [MAP(tex_contacts.map):%s]" <error> (gAlarm, gAlarmcontact)
Number Tex_Zone_Z002 "Z02 - Glasbreukdetector praktijkruimte [MAP(tex_contacts.map):%s]" <error> (gKE_Praktijkruimte, gGlasbreuk, gAlarmcontact)
Number Tex_Zone_Z003 "Z03 - Glasbreukdetector garage [MAP(tex_contacts.map):%s]" <error> (gGV_Garage, gGlasbreuk, gAlarmcontact)
Number Tex_Zone_Z004 "Z04 - Glasbreukdetector berging [MAP(tex_contacts.map):%s]" <error> (gGV_Berging, gGlasbreuk, gAlarmcontact)
Number Tex_Zone_Z014 "Voordeur [MAP(tex_contacts.map):%s]" <deur> (gGV_Inkomhal, gDeur, gAlarmcontact)
Number Tex_Zone_Z015 "Zijdeur [MAP(tex_contacts.map):%s]" <deur> (gGV_Sas, gDeur, gAlarmcontact)
Number Tex_Zone_Z016 "Achterdeur [MAP(tex_contacts.map):%s]" <deur> (gGV_Keuken, gDeur, gAlarmcontact, gAircocontact)
Number Tex_Zone_Z049_W17_1 "Z49 - Raam logeerkamer zijkant open/dicht [MAP(tex_contacts.map):%s]" <window> (gEV_Logeerkamer, gRaamcontact, gAlarmcontact)
Number Tex_Zone_Z050_W17_2 "Z50 - Raam logeerkamer zijkant kipstand [MAP(tex_contacts.map):%s]" <window> (gEV_Logeerkamer, gRaamcontact)
Number Tex_Zone_Z051_W18_1 "Z51 - Raam logeerkamer voorkant (groot) open/dicht [MAP(tex_contacts.map):%s]" <window> (gEV_Logeerkamer, gRaamcontact, gAlarmcontact)
Number Tex_Zone_Z052_W18_2 "Z52 - Raam logeerkamer voorkant (groot) kipstand [MAP(tex_contacts.map):%s]" <window> (gEV_Logeerkamer, gRaamcontact)
Number Tex_Zone_Z066 "Garagepoort [MAP(tex_contacts.map):%s]" <garagepoort> (gGV_Garage, gDeur, gAlarmcontact)
As you can see, I follow a pretty strict naming convention. This will help making my rules concise. Naming convention:
- It starts with Tex_Zone_;
- Then the Zone ID is added (e.g. Z066);
- If it is a window, also a window ID is added (e.g. W17). Most of my windows have 2 contacts. One is positioned higher, and another one is positioned lower. If one contact is open, and the other one is closed, I know if a window is ajar. If interested I could add these rules to this tutorial.
OpenHAB – Rules to process data from Texecom
Most of these rules have been created thanks to @rlkoshak’s Design Patterns and his support on this forum. So he deserves a lot of credit.
The first rule processes incoming data from Texecom. I’ve included most information as a comment in these rules. Read carefully before integration in your setup. Some rules might need to be adjusted, based on the configuration of your Texecom system. E.g. when my alarm is active, my LCD display shows “inbraak alarm”. My rule is based on this message. Your display will probably show something else. You might need to trigger your alarm a few times to test everything. I hope you have a good relationship with your neighbors.
// ***************************
// Process information from UDP interface
// ***************************
rule "Alarm interface via UDP"
when
Item Tex_UDP_Receive received update
then
// We will start with processing the Creston protocol compliant commands (zone updates, armed/disarmed updates and user updates).
// After that, we will try to derive some other information based on the information displayed on the Alarm Keypad.
// We need to trim the incoming UDP package, since that sometimes contains trailing NULL characters.
var in_UDPmsg = Tex_UDP_Receive.state.toString.trim
// To translate UDP zone-updates (Z) to our individual zone-items.
if(in_UDPmsg.startsWith("\"Z0")) {
// val itemName = "Tex_Zone_"+in_UDPmsg.substring(1, 5)
val itemName = gAllecontacten.allMembers.filter[sw|sw.name.contains(in_UDPmsg.substring(1, 5))].head
postUpdate(itemName, in_UDPmsg.substring(5))
}
// When alarm is Armed (A) we register who armed it.
else if(in_UDPmsg.startsWith("\"A0")) {
postUpdate(Tex_Alarm_Status, "1")
postUpdate(Tex_Alarm_Status_Update, new DateTimeType())
postUpdate(Tex_Alarm_Partition, in_UDPmsg.substring(2, 5))
postUpdate(Tex_Alarm_ArmDisarmUser, in_UDPmsg.substring(5, in_UDPmsg.length))
}
// When alarm is Disarmed (D) we register who disarmed it.
else if(in_UDPmsg.startsWith("\"D0")) {
postUpdate(Tex_Alarm_Status, "0")
postUpdate(Tex_Alarm_Status_Update, new DateTimeType())
postUpdate(Tex_Alarm_Partition, in_UDPmsg.substring(2, 5))
postUpdate(Tex_Alarm_ArmDisarmUser, in_UDPmsg.substring(5, in_UDPmsg.length))
}
// When a user is logged on to the keypad (U), we register the user ID.
// This information is also captured in the armed/disarmed message. However, in theory a user could log on to the keypad and do something else than arm/disarm.
else if(in_UDPmsg.startsWith("\"U0")) {
postUpdate(Tex_Alarm_User, in_UDPmsg.substring(4, 5))
}
// We need to process the response to our ASTATUS call. If we receive a "NNNNNNNN", it means the alarm is not armed. If we receive a "YNNNNNN" the alarm is armed.
// This is an optional step. Normally we should have captured this information via the armed/disarmed messages (A...)
else if(in_UDPmsg.startsWith("\"NNNNN")) {
// The alarm is not armed.
if (Tex_Alarm_Status.state == 1) {
postUpdate(Tex_Alarm_Status, "0")
}
}
else if(in_UDPmsg.startsWith("\"YNNNN")) {
// The alarm is armed.
if (Tex_Alarm_Status.state == 0) {
postUpdate(Tex_Alarm_Status, "1")
// Also when the alarm is in exit mode, we should change the status.
} else if (Tex_Alarm_Status.state == 2) {
postUpdate(Tex_Alarm_Status, "1")
}
}
// Messages not related to zone/area/user updates are typically what is displayed on the alarm keypad. They do not adhere a fixed naming convention.
// You will need to finetune this part to your specific alarm-configuration.
else {
// There is no separate UDP message that registers when the alarm has been triggered/activated. However, if the keypad lines contains "inbraak alarm" or "sirene actief", you have unwelcome visitors.
if(in_UDPmsg.startsWith("\"inbraak alarm") && Tex_Alarm_Status.state != 3) {
postUpdate(Tex_Alarm_Status, "3")
postUpdate(Tex_Alarm_Status_Update, new DateTimeType())
}
else if(in_UDPmsg.startsWith("\"sirene actief") && Tex_Alarm_Status.state != 3) {
postUpdate(Tex_Alarm_Status, "3")
postUpdate(Tex_Alarm_Status_Update, new DateTimeType())
}
// There is no separate UDP message that registers when the alarm is in exit mode. However, if the keypad lines contains "Partit.in Uitl.", the alarm is in exit mode.
else if(in_UDPmsg.startsWith("\"Partit.in Uitl.") && Tex_Alarm_Status.state != 2) {
postUpdate(Tex_Alarm_Status, "2")
postUpdate(Tex_Alarm_Status_Update, new DateTimeType())
}
// There is no separate UDP message that registers when the exit mode has failed. However, if the keypad lines contains "wapening gefaald", the alarm has failed to arm after exit mode.
else if(in_UDPmsg.startsWith("\"wapening gefaald") && Tex_Alarm_Status.state != 4) {
postUpdate(Tex_Alarm_Status, "4")
postUpdate(Tex_Alarm_Status_Update, new DateTimeType())
}
// The information displayed on the key pad needs to split back in two (as on the keypad itself).
// Since the length of these messages are unpredicable (between 6 and 32 characters), we need to include some more if statements.
if (in_UDPmsg.length < 17) {
postUpdate(Tex_Alarm_Textline1, in_UDPmsg.substring(1, in_UDPmsg.length))
} else if (in_UDPmsg.length > 16) {
postUpdate(Tex_Alarm_Textline1, in_UDPmsg.substring(1, 17))
postUpdate(Tex_Alarm_Textline2, in_UDPmsg.substring(17, in_UDPmsg.length))
}
}
end
The following rule checks if the alarm is in exit-mode (this is when you turn on the alarm, and the alarm is counting down before it is fully armed).
var Timer exitmode_timer
rule "Process Exit mode"
// The exit mode lasts 90 seconds. After that period, and alarm is normally set to "armed" (status 1).
// However, if the exit mode is cancelled within those 90 seconds, we need to update the alarm back to "disarmed" (status 0) via OpenHAB, since we do not get an update of this cancellation via UDP.
// 5 seconds of extra margin are taken.
when
Item Tex_Alarm_Status changed
then
if (Tex_Alarm_Status.state == 2) {
exitmode_timer = createTimer(now.plusSeconds(95)) [|
postUpdate(Tex_Alarm_Status, "0")
postUpdate(Tex_Alarm_Status_Update, new DateTimeType())
]
} else {
if(exitmode_timer!==null) {
exitmode_timer.cancel
exitmode_timer = null
}
}
end
The following rule is sending a request to the Texecom alarm panel to ask what is currently on the LCD display. This information is needed, since some information is not directly pushed via the Crestron protocol. E.g. the only way to know if an alarm is triggered, is by looking at the LCD display.
I just realized that this code is not optimal. It works with a Thread::sleep(3000), a cron trigger would be better. But that’s for another time.
rule "Get LSTATUS"
// We need to poll for the LCD display on a regular basis (LSTATUS). This information is used to identify if the alarm is triggered.
// We will poll for it every 3 seconds. This implies there is a maximum delay of 3 seconds between the alarm being triggered and OpenHAB being informed.
// You could decrease the time between intervals. However, there is a risk of "flooding" the Texecom.
// Using switch Tex_Get_LSTATUS you could turn on/off the polling. I used it mostly for debugging. Recommended setting is always on, obviously.
when
Item Tex_Get_LSTATUS changed from OFF to ON or
System started
then
Thread::sleep(250)
sendCommand(Tex_Get_LSTATUS, ON)
while(Tex_Get_LSTATUS.state == ON) {
sendCommand(Tex_UDP_Send, "LSTATUS")
Thread::sleep(3000)
}
end
The last rule is to get the “arm status”. When the status of the alarm is changed (from “armed” to “disarmed” or vice versa), this information is pushed. But I noticed that in some rare cases, this information was not picked up by OpenHAB (it happened 4 times the last 8 months). With requesting ASTATUS, the information is updated correctly.
// ***************************
// Get the arm-status
// ***************************
rule "Get ASTATUS"
// We will also poll for the armed status (ASTATUS). This is done every minute. This is to check if the arm status is in line with what we have derived in rule "Alarm interface via UDP"
// This process is completely optional, just an extra check in case OpenHAB "misses" an earlier armed/disarmed message.
when
Time cron "0 0/1 * 1/1 * ? *" or
System started
then
sendCommand(Tex_UDP_Send, "ASTATUS")
end
These are all the rules you need to have a working integration with Texecom.
Beyond that, I have also set up the following functionalities:
- Logging of each contact. So I know when e.g. my front door was opened last time;
- Arm my alarm with switching just one item (instead of punching in a code via OH);
- Implement additional security so you can enter only 5 codes via OH every 5 minutes;
- Send a message via Telegram when the alarm is not armed and none of the residents is at home (“hey, you forgot to turn on the alarm”);
Planned enhancements:
- When nobody is at home and the alarm is disarmed, put the alarm automatically in a “vigilant” mode. Technically the alarm is still off, but residents are informed if certain events happen (e.g. door/window is opened);
Let me know if you have any question!
Dries