Merge pull request #1 from WillCodeForCats/first-upload

First upload
This commit is contained in:
Seth 2023-07-30 14:03:28 -07:00 committed by GitHub
commit a733442c33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 686 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.DS_Store

651
asteriskcontactid.pl Executable file
View file

@ -0,0 +1,651 @@
#!/usr/bin/perl
#
# asteriskcontactid.pl
# https://github.com/WillCodeForCats/asterisk-contact-id
#
# A Contact ID handler for Asterisk's AlarmReceiver() application.
# https://wiki.asterisk.org
#
# Uses rcell-smsclient for direct SMS notifications
# https://github.com/WillCodeForCats/rcell-smsclient
#
#
use strict;
use IO::Dir;
use DBI;
use DateTime::Format::Strptime;
use DateTime::Format::MySQL;
use MIME::Lite;
my $spoolDir = '/var/spool/asterisk/alarmreceiver';
my $dbi = "DBI:mysql:host=localhost;database=asterisk";
my $dbuIser = "asterisk";
my $dbiPassword = "";
my $emailFrom = 'alarmreceiver@example.com';
my $timezone = 'America/Los_Angeles';
# account names
my %accts = (
1111 => "Account 1111",
2222 => "Another Account 2222",
3333 => "Third Account 3333",
);
# who to notify per account
# 10-digit cell number for SMS or email address only
my %notify = (
1111 => [
'user@example.com', '5555551234',
],
2222 => [
'user@example.com', '5555551234',
'user2@example.com', '5555551234',
],
3333 => [
'user@example.com', '5555551234',
'user3@example.com', '5555551234',
],
);
# Contact ID Event Codes
my %events = (
# 100 - Medical Alarms
100 => "Medical",
101 => "Personal Emergency",
102 => "Fail To Report In",
# 110 - Fire Alarms
110 => "Fire",
111 => "Smoke",
112 => "Combustion",
113 => "Water Flow",
114 => "Heat",
115 => "Pull Station",
116 => "Duct",
117 => "Flame",
118 => "Near Alarm",
# 120 - Panic Alarms
120 => "Panic",
121 => "Duress",
122 => "Silent",
123 => "Audible",
124 => "Duress - Access Granted",
125 => "Diress - Egress Granted",
# 130 - Burglar Alarms
130 => "Burglary",
131 => "Perimeter",
132 => "Interior",
133 => "24 Hour",
134 => "Entry/Exit",
135 => "Day/Night",
136 => "Outdoor",
137 => "Tamper",
138 => "Near Alarm",
139 => "Intrusion Verifier",
# 140 - General Alarm
140 => "General Alarm",
141 => "Polling Loop Open",
142 => "Polling Loop Short",
143 => "Expansion Module Failure",
144 => "Sensor Tamper",
145 => "Expansion Module Tamper",
146 => "Silent Burglary",
147 => "Sensor Supervision Failure",
# 150 and 160 - 24 Hour Non-Burglary
150 => "24 Hour Non-Burglary",
151 => "Gas Detected",
152 => "Refrigeration",
153 => "Loss of Heat",
154 => "Water Leak",
155 => "Foil Break",
156 => "Day Trouble",
157 => "Low Bottled Gas Level",
158 => "High temp",
159 => "Low temp",
161 => "Loss of air flow",
162 => "Carbon Monoxide detected",
163 => "Tank level",
# 200 and 210 - Fire Supervisory
200 => "Fire Supervisory",
201 => "Low Water Pressure",
202 => "Low CO2",
203 => "Gate Valve Sensor",
204 => "Low Water Level",
205 => "Pump Activated",
206 => "Pump Failure",
# 300 and 310 - System Troubles
300 => "System Trouble",
301 => "AC Loss",
302 => "Low System Battery",
303 => "RAM Checksum Bad",
304 => "ROM Checksum Bad",
305 => "System Reset",
306 => "Panel Programming Changed",
307 => "Self-Test Failure",
308 => "System Shutdown",
309 => "Battery Test Failure",
310 => "Ground Fault",
311 => "Battery Missing/Dead",
312 => "Power Supply Overcurrent",
313 => "Engineer Reset",
# 320 - Sounder / Relay Troubles
320 => "Sounder/Relay Trouble",
321 => "Bell 1 Trouble",
322 => "Bell 2 Trouble",
323 => "Alarm Relay Trouble",
324 => "Trouble Relay",
325 => "Reversing Relay Trouble",
326 => "Notification Appliance Ckt. #3 Trouble",
327 => "Notification Appliance Ckt. #4 Trouble",
# 330 and 340 - System Peripheral Trouble
330 => "System Peripheral Trouble",
331 => "Polling Loop Open",
332 => "Polling Loop Short",
333 => "Expansion Module Failure",
334 => "Repeater Failure",
335 => "Local Printer Out Of Paper",
336 => "Local Printer Failure",
337 => "Exp. Module DC Loss",
338 => "Exp. Module Low Batt.",
339 => "Exp. Module Reset",
341 => "Exp. Module Tamper",
342 => "Exp. Module AC Loss",
343 => "Exp. Module Self-Test Fail",
344 => "RF Receiver Jam Detect",
# 350 and 360 - Communication Troubles
350 => "Communication Trouble",
351 => "Telco 1 Fault",
352 => "Telco 2 Fault",
353 => "Long Range Radio Xmitter Fault",
354 => "Failure To Communicate Event",
355 => "Loss Of Radio Supervision",
356 => "Loss Of Central Polling",
357 => "Long Range Radio Vswr Problem",
# 370 - Protection Loop
370 => "Protection Loop",
371 => "Protection Loop Open",
372 => "Protection Loop Short",
373 => "Fire Trouble",
374 => "Exit Error Alarm (Zone)",
375 => "Panic Zone Trouble",
376 => "Hold-Up Zone Trouble",
377 => "Swinger Trouble",
378 => "Cross-Zone Trouble",
# 380 - Sensor Trouble
380 => "Sensor Trouble",
381 => "Loss Of Supervision - RF",
382 => "Loss Of Supervision - RPM",
383 => "Sensor Tamper",
384 => "RF Low Battery",
385 => "Smoke Detector Hi Sensitivity",
386 => "Smoke Detector Low Sensitivity",
387 => "Intrusion Detector Hi Sensitivity",
388 => "Intrusion Detector Low Sensitivity",
389 => "Sensor Self-Test Failure",
391 => "Sensor Watch Trouble",
392 => "Drift Compensation Error",
393 => "Maintenance Alert",
# 400 and 440 and 450 - Open/Close
400 => "Open/Close",
401 => "O/C By User",
402 => "Group O/C",
403 => "Automatic O/C",
404 => "Late To O/C ",
405 => "Deferred O/C",
406 => "Cancel",
407 => "Remote Arm/Disarm",
408 => "Quick Arm",
409 => "Keyswitch O/C",
441 => "Armed Stay",
442 => "Keyswitch Armed Stay",
450 => "Exception O/C",
451 => "Early O/C",
452 => "Late O/C",
453 => "Failed To Open",
454 => "Failed To Close",
455 => "Auto-Arm Failed",
456 => "Partial Arm",
457 => "Exit Error (User)",
458 => "User On Premises",
459 => "Recent Close",
462 => "Legal Code Entry",
463 => "Re-Arm After Alarm",
464 => "Auto-Arm Time Extended",
465 => "Panic Alarm Reset",
466 => "Service On/Off Premises",
# 410 - Remote Access
411 => "Callback Request Made",
412 => "Successful Download/Access",
413 => "Unsuccessful Access",
414 => "System Shutdown Command Received",
415 => "Dialer Shutdown Command Received",
416 => "Successful Upload",
# 420 and 430 - Access Control
421 => "Access Denied",
422 => "Access Report By User",
423 => "Forced Access",
424 => "Egress Denied",
425 => "Egress Granted",
426 => "Access Door Propped Open",
427 => "Access Point Door Status Monitor Trouble",
428 => "Access Point Request To Exit Trouble",
429 => "Access Program Mode Entry",
430 => "Access Program Mode Exit",
431 => "Access Threat Level Change",
432 => "Access Relay/Trigger Fail",
433 => "Access Rte Shunt",
434 => "Access Dsm Shunt",
# 500 and 510 - System Disables
501 => "Access Reader Disable",
# 520 - Sounder / Relay Disables
520 => "Sounder/Relay Disable",
521 => "Bell 1 Disable",
522 => "Bell 2 Disable",
523 => "Alarm Relay Disable",
524 => "Trouble Relay Disable",
525 => "Reversing Relay Disable",
526 => "Notification Appliance Ckt. # 3 Disable",
527 => "Notification Appliance Ckt. # 4 Disable",
# 530 and 540 - System Peripheral Disables
531 => "Module Added",
532 => "Module Removed",
# 550 and 560 - Communication Disables -
551 => "Dialer Disabled",
552 => "Radio Transmitter Disabled",
553 => "Remote Upload/Download Disabled",
# 570 - Bypasses
570 => "Zone/Sensor Bypass",
571 => "Fire Bypass",
572 => "24 Hour Zone Bypass",
573 => "Burg. Bypass",
574 => "Group Bypass",
575 => "Swinger Bypass",
576 => "Access Zone Shunt",
577 => "Access Point Bypass",
# 600 and 610 - Test/Misc.
601 => "Manual Trigger Test Report",
602 => "Periodic Test Report",
603 => "Periodic RF Transmission",
604 => "Fire Test",
605 => "Status Report To Follow",
606 => "Listen-In To Follow",
607 => "Walk Test Mode",
608 => "Periodic Test - System Trouble Present",
609 => "Video Xmitter Active",
611 => "Point Tested OK",
612 => "Point Not Tested",
613 => "Intrusion Zone Walk Tested",
614 => "Fire Zone Walk Tested",
615 => "Panic Zone Walk Tested",
616 => "Service Request",
# 620 - Event Log
621 => "Event Log Reset",
622 => "Event Log 50% Full",
623 => "Event Log 90% Full",
624 => "Event Log Overflow",
625 => "Time/Date Reset",
626 => "Time/Date Inaccurate",
627 => "Program Mode Entry",
628 => "Program Mode Exit",
629 => "32 Hour Event Log Marker",
# 630 - Scheduling
630 => "Schedule Change",
631 => "Exception Schedule Change",
632 => "Access Schedule Change",
# 640 - Personnel Monitoring
641 => "Senior Watch Trouble",
642 => "Latch-Key Supervision",
# 650 - Misc.
651 => "Reserved For Ademco Use",
652 => "Reserved For Ademco Use",
653 => "Reserved For Ademco Use",
654 => "System Inactivity",
);
# Contact ID Event Qualifiers
my %eventQual = (
1 => "New Event or Opening",
3 => "New Restore or Closing",
6 => "Previously Reported",
);
my %eventQualAlarm = (
1 => "New",
3 => "Restored",
6 => "Previously Reported",
);
my %eventQualOC = (
1 => "Opening",
3 => "Closing",
6 => "Previously Reported",
);
# Contact ID digit value map
my %map = (
'0' => 10,
'1' => 1,
'2' => 2,
'3' => 3,
'4' => 4,
'5' => 5,
'6' => 6,
'7' => 7,
'8' => 8,
'9' => 9,
'B' => 11,
'C' => 12,
'D' => 13,
'E' => 14,
'F' => 15,
);
my %rmap = reverse %map;
my $dbh = DBI->connect($dbi, $dbiUser, $dbiPassword)
or die($DBI::errstr);
my $dir = IO::Dir->new($spoolDir);
if (defined $dir) {
while (defined($_ = $dir->read)) {
next unless /^event/;
print "Processing event file: $_\n";
processEvents($_);
}
}
else {
print "Failed to open $spoolDir\n";
}
$dbh->disconnect;
sub processEvents {
my $eventFile = shift;
my $meta = 0;
my $events = 0;
my %metadata;
# delete after processing (disable for testing)
my $deleteFile = 1;
# open the file
open(my $fh, '<', "$spoolDir/$eventFile")
or die "Could not open file '$eventFile' $!";
# process lines in file
while (<$fh>) {
next if /^\n/;
s/\n//;
# file has two sections: [metadata] and [events]
if ($_ =~ /^\[metadata\]$/) {
print "Begin metadata...\n";
$meta = 1;
$events = 0;
next;
}
if ($_ =~ /^\[events\]$/) {
print "Begin events...\n";
$meta = 0;
$events = 1;
next;
}
if ($meta) {
s/\r?\n$//;
if (/([^=]+)=(.*)/) {
$metadata{substr(lc($1), 0, 80)} = substr($2, 0, 80);
}
}
elsif ($events) {
# Translate DTMF into Contact ID values
s/B/E/; # DTMF B is Contact ID E
s/C/F/; # DTMF C is Contact ID F
s/\*/B/; # DTMF * is Contact ID B
s/#/C/; # DTMF # is Contact ID C
s/A/D/; # DTMF A is Contact ID D
# Contact ID event format
# ACCT MT QXYZ GG CCC S
#
# ACCT = 4 Digit Account number (0-9, B-F)
# MT = Message Type. either 18 (preferred) or 98 (optional)
# Q = Event qualifier
# XYZ = Event code (3 Hex digits 0-9,B-F)
# GG = Group or Partition number (2 Hex digits 0-9, B-F).
# 00 to indicate that no specific group or partition information applies.
# CCC = Zone number (Event reports) or User # (Open / Close reports ) (3 Hex digits 0-9,B-F ).
# 000 to indicate that no specific zone or user information applies
# S = 1 Digit Hex checksum
# (Sum of all message digits + S) MOD 15 = 0
if ($_ =~ /^([0-9B-F]{4})(18|98)(1|3|6)([0-9B-F]{3})([0-9B-F]{2})([0-9B-F]{3})([0-9B-F]{1})$/) {
# skip if checksum failed
if (!checksum($7)) {
print "Skipping event $_: checksum failed!\n";
next;
}
# skip unknown events from unknown accounts
if (!defined($accts{$1})) {
print "Skipping event $_: unknown account $1\n";
next;
}
# insert event into database
storeEvent(\%metadata, $1, $4, $_);
# process notifications for event
notifyEvent(\%metadata, $1, $3, $4, $5, $6);
print "Account: $accts{$1}\n";
print "Qual: $3 ".$eventQual{$3}."\n";
print "Event: $4 ".$events{$4}."\n";
print "Group: $5 Zone: $6\n";
print "\n";
}
else {
print "Bad data: $_\n";
}
}
else {
next;
}
}
close $fh;
if ($deleteFile) {
print "delete $spoolDir/$eventFile\n";
unlink "$spoolDir/$eventFile";
}
}
sub notifyEvent {
my $metadata = shift;
my $account = shift;
my $qual = shift;
my $event = shift;
my $group = shift;
my $zone = shift;
# don't notify for these events
return if ($event == '602'); # routine test
# start with undefined message
my $notifyString = undef;
# 100 series - Alarms
if ($event =~ /1[0-9]{2}/) {
$notifyString = sprintf("%s\nAlarm: %s \nZone: %s (%s)",
$accts{$account}, $events{$event}, $zone, $eventQualAlarm{$qual});
}
# 200 Series - Fire Supervisory
if ($event =~ /2[0-9]{2}/) {
$notifyString = sprintf("%s\nAlarm: %s \nZone: %s (%s)",
$accts{$account}, $events{$event}, $zone, $eventQualAlarm{$qual});
}
# 300 Series - Troubles
if ($event =~ /3[0-9]{2}/) {
if ($event == '350') {
#350 => "Communication Trouble"
$notifyString = sprintf("%s %s Line %s (%s)",
$accts{$account}, $events{$event}, $zone, $eventQualAlarm{$qual});
}
elsif ($event == '354') {
#354 => "Failure To Communicate Event"
$notifyString = sprintf("%s %s Account %s (%s)",
$accts{$account}, $events{$event}, $zone, $eventQualAlarm{$qual});
}
elsif ($zone != 000) {
$notifyString = sprintf("%s\nAlarm: %s \nZone: %s (%s)",
$accts{$account}, $events{$event}, $zone, $eventQualAlarm{$qual});
}
elsif ($group != 00) {
$notifyString = sprintf("%s\nAlarm: %s \nModule: %s (%s)",
$accts{$account}, $events{$event}, $group, $eventQualAlarm{$qual});
}
else {
$notifyString = sprintf("%s\n%s (%s)", $accts{$account}, $events{$event}, $eventQualAlarm{$qual});
}
}
# 400 Series - Open/Close and Access
if ($event =~ /4[0-9]{2}/) {
if ($zone ne '000' && $group ne '00') {
$notifyString = sprintf("%s\n%s: %s %s Partition %s", $accts{$account}, $eventQualOC{$qual}, $events{$event}, $zone, $group);
}
else {
$notifyString = sprintf("%s\n%s: %s", $accts{$account}, $eventQualOC{$qual}, $events{$event});
}
}
# 601 Manual Trigger Test Report
# 608 Periodic Test - System Trouble Present
if ($event == '608' || $event == '601') {
$notifyString = sprintf("%s\n%s", $accts{$account}, $events{$event});
}
# append timestamp
if (defined($notifyString)) {
$notifyString .= "\n$$metadata{'timestamp'}";
}
if (defined($notifyString)) {
#print "NOTIFY: $notifyString\n";
foreach (@{$notify{$account}}) {
if (/^[0-9]{10}$/) {
print "Notify SMS: $_\n";
open(my $sms, '|-', "/usr/local/bin/smsclient.pl -p $_")
or die "Could not open smsclient.pl' $!";
print $sms $notifyString;
close $sms;
}
else {
print "Notify Email $_\n";
my $msg = MIME::Lite->new(
From => $emailFrom,
To => $_,
Subject => "Alarm Event for $accts{$account} at $$metadata{'timestamp'}",
Type => 'text/plain; charset=utf-8',
Data => $notifyString
);
$msg->add("Auto-Submitted" => "auto-generated");
$msg->send;
}
}
}
}
# stores an event in the database
sub storeEvent {
my $metadata = shift;
my $account = shift;
my $cidevent = shift;
my $event = shift;
print $$metadata{'timestamp'}."\n";
# parse timestamp from file metadata
# Tue May 02, 2017 @ 21:00:01 PDT
my $strp = DateTime::Format::Strptime->new(
pattern => '%a %b %d, %Y @ %H:%M:%S',
time_zone => $timezone
);
my $dt = $strp->parse_datetime($$metadata{'timestamp'});
# format timestamp for mysql
my $timestamp = DateTime::Format::MySQL->format_datetime($dt);
# insert data
$dbh->do(q{
INSERT INTO alarmreceiver
(timestamp, account, event, protocol, callingfrom, callername)
VALUES (?, ?, ?, ?, ?, ?)
},
undef,
$timestamp, $account, $event, $$metadata{'protocol'}, $$metadata{'callingfrom'}, $$metadata{'callername'}
) or die($DBI::errstr);
# 601 Manual Trigger Test Report
# 602 Periodic Test Report
if ($cidevent == '602' || $cidevent == '601') {
$dbh->do(q{
UPDATE alarmreceiver_test
SET timestamp = ?
WHERE account = ?
},
undef,
$timestamp, $account
) or die($DBI::errstr);
}
}
# Contact ID Checksum
sub checksum {
# (Sum of all message digits + S) MOD 15 = 0
my $sum = 0;
foreach my $c (split //) {
$sum += $map{$c};
}
# if result is 0, use digit F for checksum.
if ($sum == 0) { $sum = $map{'F'}; }
# return 1 if checksum ok, 0 if not
return ($sum % 15) ? 0 : 1;
}

3
schema/.sqlfluff Normal file
View file

@ -0,0 +1,3 @@
[sqlfluff]
dialect = mysql
exclude_rules = LT01, CP01

View file

@ -0,0 +1,30 @@
--
-- Table structure for table `alarmreceiver`
--
DROP TABLE IF EXISTS `alarmreceiver`;
CREATE TABLE `alarmreceiver` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`timestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`account` smallint(5) unsigned NOT NULL,
`event` varchar(16) NOT NULL,
`protocol` enum('ADEMCO_CONTACT_ID') NOT NULL,
`callingfrom` varchar(80) NOT NULL DEFAULT '',
`callername` varchar(80) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `account` (`account`),
KEY `callingfrom` (`callingfrom`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--
-- Table structure for table `alarmreceiver_test`
--
DROP TABLE IF EXISTS `alarmreceiver_test`;
CREATE TABLE `alarmreceiver_test` (
`account` smallint(5) unsigned NOT NULL,
`test_interval` smallint(5) unsigned NOT NULL DEFAULT '24',
`timestamp` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`account`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;