2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2024-11-22 02:59:49 +00:00
fhem-mirror/fhem/contrib/ble2mqttd
2024-07-24 19:44:31 +00:00

605 lines
24 KiB
Perl

#!/usr/bin/perl
# vim: set softtabstop=2 shiftwidth=2 expandtab :
##############################################################################
# $Id$
##############################################################################
#
# ble2mqttd
#
# checks for one or multiple bluetooth *low energy* devices for their
# presence state and reports it to an mqtt server.
#
# Copyright (C) 2015-2018 P. Reinhardt, pr-fhem (at) reinhardtweb (dot) de
# Contributions by (in chronological order): decaflo
#
# This script free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# The GNU General Public License can be found at
# http://www.gnu.org/copyleft/gpl.html.
# A copy is found in the textfile GPL.txt and important notices to the
# license from the author is found in LICENSE.txt distributed with these
# scripts.
#
# This script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
##############################################################################
# cpan Net::MQTT::Simple
# apt-get install bluez libexpect-perl libnet-server-perl libreadonly-perl
use strict;
use warnings;
no warnings 'redefine';
use Readonly;
use Expect;
use Digest::MD5;
use Net::MQTT::Simple;
use Net::MQTT::Simple::SSL;
use IO::Socket::SSL;
use Fcntl 'LOCK_EX', 'LOCK_NB';
use Sys::Syslog qw(:standard :macros);
use Sys::Hostname;
use Time::HiRes qw(usleep);
use POSIX qw(strftime);
use Getopt::Long;
use Net::Server::Daemonize qw(daemonize);
use Data::Dumper;
Readonly my $EXPECT_TIMEOUT => 10;
Readonly my $BLUETOOTHCTL_RESTART_SLEEP => 1;
Readonly my $MAINLOOP_SLEEP_US => 250 * 1000;
Readonly my $HEARTBEAT_INTERVAL => 60;
Readonly my $DUMP_INTERVAL => 15;
Readonly my $EVALUATION_INTERVAL => 5;
Readonly my $PUBLISH_FILTER_INTERVAL => 15;
Readonly my $CLEANUP_INTERVAL => 15 * 60;
Readonly my $CLEANUP_MAX_AGE => 30 * 60;
Readonly my $QUERY_INTERVAL => 2;
Readonly my $BATTERY_INTERVAL => 1 * 60 * 60;
Readonly my $INITIAL_BATTERY_INTERVAL => 2 * 60;
Readonly my $MQTT_MAX_CLIENTID_LENGTH => 23;
Readonly my $BATTERY_LEVEL_CHARACTERISTIC_UUID => '00002a19-0000-1000-8000-00805f9b34fb';
Readonly my $DEFAULT_BATTERY_MAX_AGE => 48;
Readonly my $DEFAULT_RSSI_THRESHOLD => 10;
Readonly my $DEFAULT_ABSENT_INTERVAL => 60;
Readonly my $ME => 'ble2mqttd';
Readonly my $VERSION => '0.16';
Readonly my $PIDFILE => "/var/run/$ME.pid";
my ($mqtt, $bluetoothctl, $loglevel, $logtarget, $debug);
my ($nextheartbeat, $nextdump, $nextevaluation, $nextcleanup, $nextbattery, $lastquery, $lastevent, $lastconnect);
my %devices;
*Net::MQTT::Simple::_client_identifier = sub {
return(
substr(sprintf("%s-%s", $ME, hostname()), 0, $MQTT_MAX_CLIENTID_LENGTH)
);
};
sub tsprefix {
return(sprintf('[%s] ', strftime('%F %H:%M:%S', localtime())));
}
sub syslogw {
my ($priority, @args) = @_;
return if (scalar(@args) < 1);
my $logmessage;
if (scalar(@args)==1) {
my ($message) = @args;
$logmessage = sprintf("%s: $message", (caller(1))[3] // 'main');
} else {
my ($format, @args) = @args;
$logmessage = sprintf("%s: $format", (caller(1))[3] // 'main', @args);
}
if ($loglevel >= $priority) {
if ($logtarget eq 'syslog') {
syslog($priority, $logmessage) if ($loglevel >= $priority);
} elsif ($logtarget eq 'stdout' && !$debug) {
printf("%s%s\n", tsprefix(),$logmessage);
}
}
printf("%s%s\n", tsprefix(),$logmessage) if ($debug);
return();
}
sub error_exit {
my ($exit_code, @args) = @_;
syslogw(LOG_ERR, @args);
$bluetoothctl->hard_close() if ($bluetoothctl);
closelog();
exit ($exit_code);
}
sub usage_exit {
printf("%s --mqttserver <mqtt server:mqtt port> --mqttfingerprint <ssl fingerprint> --mqttuser <mqtt user> --mqttpass <mqtt pass> --loglevel <log level> --logtarget <log target> --daemon --mac <regex>\n", $ME);
print("valid log levels:\n");
print("\tLOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG. Default: LOG_INFO\n");
print("valid log targets:\n");
print("\tsyslog, stdout. Default: syslog\n");
print("optional arguments:\n");
print("\t--mqtttopic - Set custom mqtt topic, especially necessary if more than one instance is used.\n");
print("\t--daemonize - Run as daemon.\n");
printf("\t--absentinterval - Seconds after which device is considered absent. Minimum value: 30, default: %i.\n", $DEFAULT_ABSENT_INTERVAL);
printf("\t--rssithreshold - rssi deviation to trigger an update. Minimum value: 5, default: %i.\n", $DEFAULT_RSSI_THRESHOLD);
print("\t--watchdogthreshold - if (any) last beacon was received before the set watchdog threshold, bluetoothctl is restarted. Minimum value: 30 seconds, default: disabled.\n");
print("\t--debug <number> - print extensive debug output to stdout (mutually exclusive with --daemon). A higher number results in more debug output.\n");
print("\t--mac <regex> - filter handled MAC Adresses with regular expression.\n");
printf("\t--batterymaxage <number> - Maximum time (in hours) after which the battery reading is considered outdated and updated, default: %i. A value of 0 disables all battery checks.\n", $DEFAULT_BATTERY_MAX_AGE);
print("\t--forcebattery - Forces the battery check on known devices even though the battery service is not recognized. Requires setting --mac.\n");
print("\t--retain - Publishes the topics present, lastseen as retained.\n");
print("examples:\n");
printf("\t%s --mqttserver mqtt.domain:1883\n", $ME);
printf("\t%s --mqttserver mqtt.domain:1883 --mqtttopic ble2mqtt/livingroom\n", $ME);
printf("\t%s --mqttserver mqtt.domain:1883 --mqttuser azurediamond --mqttpass hunter2\n", $ME);
printf("\t%s --mqttserver mqtt.domain:8883 --getmqttfingerprint -> Displays the mqtt fingerprint of the server.\n", $ME);
printf("\t%s --mqttserver mqtt.domain:8883 --mqttfingerprint 'sha256\$11e20bbb668da517a0ae8536f3c79887d06ddec945fab60401deab92dc7e7c1f'\n", $ME);
printf("\t%s --mqttserver mqtt.domain:8883 --mqttfingerprint 'sha256\$11e20bbb668da517a0ae8536f3c79887d06ddec945fab60401deab92dc7e7c1f' --mqttuser user --mqttpass pass\n", $ME);
printf("\t%s --mqttserver mqtt.domain:8883 --mqttfingerprint 'sha256\$11e20bbb668da517a0ae8536f3c79887d06ddec945fab60401deab92dc7e7c1f' --mqttuser user --mqttpass pass --mac 'AB:CD:EF:.*'\n", $ME);
closelog();
exit(2);
}
sub parse_loglevel {
my ($loglevel_str) = @_;
$loglevel_str = uc($loglevel_str);
return ( $loglevel_str eq 'LOG_EMERG' ? LOG_EMERG
: $loglevel_str eq 'LOG_ALERT' ? LOG_ALERT
: $loglevel_str eq 'LOG_CRIT' ? LOG_CRIT
: $loglevel_str eq 'LOG_ERR' ? LOG_ERR
: $loglevel_str eq 'LOG_WARNING' ? LOG_WARNING
: $loglevel_str eq 'LOG_NOTICE' ? LOG_NOTICE
: $loglevel_str eq 'LOG_INFO' ? LOG_INFO
: $loglevel_str eq 'LOG_DEBUG' ? LOG_DEBUG
: usage_exit()
);
}
sub parse_options {
my $device = "hci0";
my $mqttserver;
my $mqttfingerprint;
my $mqttuser;
my $mqttpass;
my $mqtttopic = "ble2mqtt";
my $daemonize = 0;
my $logtarget = "syslog";
my $loglevel = "LOG_INFO";
my $absentinterval = $DEFAULT_ABSENT_INTERVAL;
my $rssithreshold = $DEFAULT_RSSI_THRESHOLD;
my $watchdogthreshold = 0;
my $debug = 0;
my $debugbluetoothctl = 0;
my $help = 0;
my $getmqttfingerprint = 0;
my $macregex;
my $forcebattery = 0;
my $retain = 0;
my $batterymaxage = $DEFAULT_BATTERY_MAX_AGE;
GetOptions(
#'bluetoothdevice|device|b=s' => \$device,
'mqttserver|m=s' => \$mqttserver,
'mqttfingerprint|f=s' => \$mqttfingerprint,
'mqttuser|u=s' => \$mqttuser,
'mqttpass|p=s' => \$mqttpass,
'mqtttopic|t=s' => \$mqtttopic,
'getmqttfingerprint|g' => \$getmqttfingerprint,
'daemon|daemonize|d!' => \$daemonize,
'loglevel|l=s' => \$loglevel,
'logtarget|t=s' => \$logtarget,
'absentinterval|a=i' => \$absentinterval,
'rssithreshold=i' => \$rssithreshold,
'watchdogthreshold|w=i' => \$watchdogthreshold,
'debug=i' => \$debug,
'debugbluetoothctl!' => \$debugbluetoothctl,
'help|h' => \$help,
'forcebattery!' => \$forcebattery,
'retain!' => \$retain,
'batterymaxage=i' => \$batterymaxage,
'mac=s' => \$macregex,
) or usage_exit();
usage_exit() if ($help);
usage_exit() unless ($mqttserver);
usage_exit() unless ($absentinterval >= 30);
usage_exit() unless ($rssithreshold >= 5);
usage_exit() unless ($batterymaxage >= 0);
usage_exit() if ($watchdogthreshold && $watchdogthreshold < 30);
usage_exit() if ($forcebattery && !$macregex);
$logtarget =~ m/^(syslog|stdout)$/ or usage_exit();
$loglevel = parse_loglevel($loglevel);
$daemonize = 0 if ($debug);
return ($device, $mqttserver, $mqttuser, $mqttpass, $mqttfingerprint, $getmqttfingerprint, $mqtttopic, $daemonize, $loglevel, $logtarget, $absentinterval, $rssithreshold, $watchdogthreshold, $debug, $debugbluetoothctl, $macregex, $forcebattery, $retain, $batterymaxage);
}
sub sanity_check {
my ($batterymaxage) = @_;
error_exit(3, "ERROR: $ME is already running. Exiting.") if (!flock DATA, LOCK_EX | LOCK_NB);
# log md5 digest
open (my $me, '<', $0);
binmode ($me);
syslogw(LOG_INFO, "md5 digest of '%s' is: '%s'.", $0, Digest::MD5->new->addfile($me)->hexdigest());
close($me);
# check if necessary external binaries exist
my $ok = 1;
foreach my $binary ($batterymaxage > 0 ? qw/bluetoothctl gatttool/ : qw/bluetoothctl/) {
my $binpath = `which $binary 2>/dev/null`;
chomp($binpath);
if ($? == 0) {
syslogw(LOG_INFO, "%s found at '%s'.", $binary, $binpath);
} else {
syslogw(LOG_ERR, "ERROR: %s not found!", $binary);
$ok = 0;
}
}
error_exit(4, "ERROR: Exiting due to missing binaries.") if (!$ok);
return();
}
sub mqtt_check_reconnect {
my ($mqtt, $mqtttopic) = @_;
if (!defined($lastconnect) || $lastconnect != $mqtt->{last_connect}) {
syslogw(LOG_INFO, '(Re-)connect detected, handling...');
$mqtt->retain($mqtttopic . '/state', 'connected');
$lastconnect = $mqtt->{last_connect};
}
}
sub mqtt_connect {
my ($mqttserver, $mqttuser, $mqttpass, $mqttfingerprint, $mqtttopic) = @_;
my $mqtt;
if (!defined($mqttfingerprint)) {
if ($mqttuser && $mqttpass) {
$ENV{MQTT_SIMPLE_ALLOW_INSECURE_LOGIN} = 1;
$mqtt = Net::MQTT::Simple->new($mqttserver);
$mqtt->login($mqttuser,$mqttpass);
} else {
$mqtt = Net::MQTT::Simple->new($mqttserver);
}
} else {
$mqtt = Net::MQTT::Simple::SSL->new($mqttserver, {SSL_fingerprint => $mqttfingerprint});
if ($mqttuser && $mqttpass) {
$mqtt->login($mqttuser, $mqttpass);
}
}
$mqtt->last_will($mqtttopic . '/state', 'disconnected', 1);
mqtt_check_reconnect($mqtt, $mqtttopic);
return($mqtt);
}
sub dump_task {
syslogw(LOG_INFO, 'Executing dump task.');
printf("%sKnown devices (%i):\n", tsprefix(), scalar(keys(%devices)));
foreach my $mac (sort keys(%devices)) {
printf("\tmac: %s, present: %i, name: %s, address type: %s, first seen: %i, last seen: %i, last published: %i, rssi: %s, published rssi: %s, query count: %s, battery service: %i, battery: %s (%s)\n",
$mac,
$devices{$mac}{'present'} // 'unknown',
$devices{$mac}{'name'} // 'unknown',
$devices{$mac}{'address_type'} // 'unknown',
time() - $devices{$mac}{'firstseen'},
time() - $devices{$mac}{'lastseen'},
time() - $devices{$mac}{'lastpublished'},
$devices{$mac}{'rssi'} // 'unknown',
$devices{$mac}{'rssipublished'} // 'unknown',
$devices{$mac}{'query_count'} // '%',
$devices{$mac}{'batteryservice'} // 0,
$devices{$mac}{'battery'} // 'unknown',
$devices{$mac}{'batterytime'} // 'never'
);
}
return();
}
sub evaluation_task {
my ($absentinterval, $mqtttopic, $retain) = @_;
syslogw(LOG_DEBUG, 'Executing evaluation task.');
foreach my $mac (sort keys(%devices)) {
if ($devices{$mac}{'present'} && time() - $devices{$mac}{'lastseen'} > $absentinterval) {
syslogw(LOG_INFO, 'Device %s is now absent.', $mac);
if ($retain) {
$mqtt->retain($mqtttopic . '/' . $mac . '/present', 0);
} else {
$mqtt->publish($mqtttopic . '/' . $mac . '/present', 0);
}
$devices{$mac}{'present'} = 0;
}
}
}
sub cleanup_task {
my $start_time = time();
my $deleted_items = 0;
foreach my $mac (keys(%devices)) {
next if ($devices{$mac}{'present'});
my $age = time() - $devices{$mac}{'lastseen'};
if ($age > $CLEANUP_MAX_AGE) {
$deleted_items++;
syslogw(LOG_DEBUG, "Deleting device %s.", $mac);
delete($devices{$mac});
}
}
syslogw(LOG_INFO, "Cleanup finished, deleted %i devices in %i seconds.", $deleted_items, time() - $start_time);
return();
}
sub get_battery_level {
my ($mac) = @_;
my $address_type = $devices{$mac}{'address_type'} // 'public';
#-i $device
open(my $gatttool, "-|", "gatttool -b $mac -t $address_type --char-read --uuid=$BATTERY_LEVEL_CHARACTERISTIC_UUID 2>&1") || die('Error executing gatttool!');
my $result = 'unknown';
while (<$gatttool>) {
chomp($_);
syslogw(LOG_DEBUG, "gatttool (mac: %s, address type: '%s'): '%s'", $mac, $address_type, $_);
if ($_ =~ m/^handle:\s[0-9A-Fa-fx]+\s+value:\s([0-9a-f]+)\s*$/) {
# Success: 'handle: 0x0028 value: 64'
$result = hex($1);
} elsif ($_ =~ m/^Read characteristics by UUID failed: No attribute found within the given range$/) {
# Unsupported: 'Read characteristics by UUID failed: No attribute found within the given range'
$result = 'unknown (unsupported)';
} elsif ($_ =~ m/^connect error: Connection refused \(111\)$/) {
# Unreachable (after 40s): 'connect error: Connection refused (111)'
# Shouldn't happen very often because we try to query only reachable clients that offer battery service
$result = 'unknown (timeout)';
}
}
close($gatttool);
return($result);
}
sub battery_task {
my ($mqtttopic, $forcebattery, $batterymaxage) = @_;
syslogw(LOG_INFO, 'Executing battery task.');
foreach my $mac (sort keys(%devices)) {
if (
$devices{$mac}{'present'} &&
($devices{$mac}{'batteryservice'} || $forcebattery) &&
(!$devices{$mac}{'batterytime'} || (time()-$devices{$mac}{'batterytime'}) >= $batterymaxage * 60 * 60)
) {
my $battery = get_battery_level($mac);
if ($battery !~ m/^unknown/) {
syslogw(LOG_INFO, 'Battery level of device %s is %s, updating reading...', $mac, $battery);
$devices{$mac}{'battery'} = $battery;
$devices{$mac}{'batterytime'} = time();
$mqtt->publish($mqtttopic . '/' . $mac . '/battery', $devices{$mac}{'battery'});
$mqtt->publish($mqtttopic . '/' . $mac . '/batterytime', $devices{$mac}{'batterytime'});
} else {
syslogw(LOG_INFO, 'Battery level of device %s is %s, skipping reading update...', $mac, $battery);
}
}
}
$nextbattery = time() + $BATTERY_INTERVAL;
return();
}
sub query_task {
my ($bluetoothctl) = @_;
foreach my $mac (sort keys(%devices)) {
if (
$devices{$mac}{'present'} &&
!$devices{$mac}{'address_type'} &&
(!$devices{$mac}{'next_query_time'} || time() > $devices{$mac}{'next_query_time'})
) {
$devices{$mac}{'query_count'}++;
my $backoff = 2**$devices{$mac}{'query_count'}; # exponential backoff
$devices{$mac}{'next_query_time'} = time() + $backoff;
$bluetoothctl->send(sprintf("info %s\n", $mac));
syslogw(LOG_DEBUG, 'Executing query task. Querying %s (count: %i, backoff: %i)...', $mac, $devices{$mac}{'query_count'}, $backoff);
return(); # only one query per interval
}
}
}
sub handle_match {
my ($mqtttopic, $rssithreshold, $type, $mac, $info, $bluetoothctl, $macregex, $retain) = @_;
$info =~ s/\s+$//;
$mac = uc($mac);
$lastevent = time();
if($macregex && $mac!~/$macregex/i) {
printf("%s### Match (filtered): [%s] %s (%s)\n", tsprefix(), $type, $mac, $info) if ($debug >= 2);
return();
}
if (my ($rssi) = ($info =~ m/^RSSI: (-?[0-9]+)$/)) {
printf("%s### Match RSSI: [%s] %s -> %s\n", tsprefix(), $type, $mac, $rssi) if ($debug >= 2);
$devices{$mac}{'rssi'}=$rssi;
if (!$devices{$mac}{'rssipublished'} || abs($devices{$mac}{'rssipublished'} - $rssi) > $rssithreshold) {
$mqtt->publish($mqtttopic . '/' . $mac . '/rssi', $rssi);
$devices{$mac}{'rssipublished'} = $rssi;
}
}
if($devices{$mac}{'firstseen'}) {
printf("%s### Match: [%s] %s (%s)\n", tsprefix(), $type, $mac, $info) if ($debug >= 2);
} else {
printf("%s### Match (new): [%s] %s (%s)\n", tsprefix(), $type, $mac, $info) if ($debug >= 2);
$devices{$mac}{'firstseen'} = time();
$devices{$mac}{'query_count'} = 0;
}
$devices{$mac}{'lastseen'} = time();
$devices{$mac}{'present'} = 1;
if (!$devices{$mac}{'present'} || !$devices{$mac}{'lastpublished'} || time() - $devices{$mac}{'lastpublished'} > $PUBLISH_FILTER_INTERVAL) {
if ($retain) {
$mqtt->retain($mqtttopic . '/' . $mac . '/lastseen', time());
$mqtt->retain($mqtttopic . '/' . $mac . '/present', 1);
} else {
$mqtt->publish($mqtttopic . '/' . $mac . '/lastseen', time());
$mqtt->publish($mqtttopic . '/' . $mac . '/present', 1);
}
$devices{$mac}{'lastpublished'} = time();
syslogw(LOG_INFO, 'Device %s is now present.', $mac) unless ($devices{$mac}{'present'});
}
}
(my $device, my $mqttserver, my $mqttuser, my $mqttpass, my $mqttfingerprint, my $getmqttfingerprint, my $mqtttopic, my $daemonize, $loglevel, $logtarget, my $absentinterval, my $rssithreshold, my $watchdogthreshold, $debug, my $debugbluetoothctl, my $macregex, my $forcebattery, my $retain, my $batterymaxage) = parse_options();
syslogw(LOG_NOTICE, "Version %s started (device: %s, mqtt server: %s, mqtt user: %s, mqtt fingerprint: %s, get mqtt fingerprint: %s, mqtt topic: %s, daemonize: %i, log level: %i, log target: %s, absent interval: %s, rssi threshold: %s, watchdog threshold: %s, mac regex: %s, debug: %i, debug bluetoothctl: %i, force battery: %i, battery max age: %i).",
$VERSION, $device, $mqttserver, $mqttuser // '-none-', $mqttfingerprint // '-none-', $getmqttfingerprint, $mqtttopic // '-none-', $daemonize, $loglevel, $logtarget, $absentinterval, $rssithreshold, $watchdogthreshold, $macregex // '-none-', $debug, $debugbluetoothctl, $forcebattery, $batterymaxage);
sanity_check($batterymaxage);
daemonize('root', 'root', $PIDFILE) if $daemonize;
local $SIG{INT} = local $SIG{TERM} = local $SIG{HUP} = sub {
syslogw(LOG_NOTICE, "Caught signal, cleaning up and exiting...");
unlink($PIDFILE) if (-e $PIDFILE);
if ($mqtt) {
$mqtt->retain($mqtttopic . '/state', 'disconnected');
$mqtt->disconnect();
}
$bluetoothctl->hard_close() if ($bluetoothctl);
closelog();
exit(1);
};
if ($getmqttfingerprint) {
printf("Fingerprint for server '%s': '%s'.\n", $mqttserver, IO::Socket::SSL->new(PeerHost => $mqttserver, SSL_verify_mode => SSL_VERIFY_NONE)->get_fingerprint);
exit(0);
}
$mqtt = mqtt_connect($mqttserver, $mqttuser, $mqttpass, $mqttfingerprint, $mqtttopic);
$lastquery = 0;
$nextheartbeat = 0;
$nextdump = time() + $DUMP_INTERVAL;
$nextevaluation = time() + $EVALUATION_INTERVAL;
$nextbattery = time() + $INITIAL_BATTERY_INTERVAL;
$nextcleanup = time() + $CLEANUP_INTERVAL;
for(;;) {
$bluetoothctl=Expect->new();
$bluetoothctl->spawn('bluetoothctl') or error_exit(10, "Cannot spawn bluetoothctl: $!");
$bluetoothctl->log_stdout(0);
$bluetoothctl->log_file("/tmp/$ME.log", "w") if ($debugbluetoothctl);
# Requires bluez >=~5.50
for my $cmd (
"menu scan",
"clear",
"duplicate-data on",
"transport le",
"back",
"scan on",
"version"
) {
$bluetoothctl->expect($EXPECT_TIMEOUT, '#') or error_exit(11, "ERROR expecting bluetoothctl prompt while waiting to send '$cmd'.");
syslogw(LOG_DEBUG, "Sending '%s'...", $cmd);
$bluetoothctl->send($cmd."\n");
}
$bluetoothctl->expect($EXPECT_TIMEOUT, '#') or error_exit(12, "ERROR expecting bluetoothctl prompt.");
syslogw(LOG_INFO, "Scan started.");
my $current_mac;
$lastevent = time();
do {
$bluetoothctl->expect(0,
# [<lovely control characters>NEW|CHG<lovely control characters>] Device 63:A7:11:06:7D:8B 63-A7-11-06-7D-8B
[qr/\[.*(NEW|CHG).*\] Device ([0-9A-Fa-f:]+) (.*)/ => sub {
handle_match($mqtttopic, $rssithreshold, $bluetoothctl->matchlist(), $bluetoothctl, $macregex, $retain);
exp_continue();
}],
[qr/Device ([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}) not available/ => sub {
printf("%s+++ Device info: %s not available\n", tsprefix(), $bluetoothctl->matchlist()) if ($debug >= 2);
$current_mac='';
exp_continue();
}],
[qr/Device ([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}) \((random|public)\)/ => sub {
my ($mac, $type) = $bluetoothctl->matchlist();
printf("%s+++ Device info: %s %s\n", tsprefix(), $mac, $type) if ($debug >= 2);
$current_mac = $mac;
$devices{$mac}{'address_type'} = $type;
delete($devices{$current_mac}{'query_count'});
delete($devices{$current_mac}{'next_query_time'});
exp_continue();
}],
[qr/\s+Name:\s*([\w -]+)/ => sub {
printf("%s+++ Device name: %s '%s'\n", tsprefix(), $current_mac // 'NONE', $bluetoothctl->matchlist()) if ($debug >= 2);
if ($current_mac) {
$devices{$current_mac}{'name'}=$bluetoothctl->matchlist()->[0];
} else {
syslogw(LOG_INFO, "WARNING: Current mac is not set, ignoring!\n") if ($debug >= 2);
}
exp_continue();
}],
[qr/\s+UUID:\s*Battery Service/ => sub {
printf("%s+++ Device battery %s\n", tsprefix(), $current_mac // 'NONE') if ($debug >= 2);
if ($current_mac) {
$devices{$current_mac}{'batteryservice'}=1;
} else {
syslogw(LOG_INFO, "WARNING: Current mac is not set, ignoring!\n") if ($debug >= 2);
}
exp_continue();
}],
[qr/Version\s*([\w\.]+)/ => sub {
chomp($bluetoothctl->matchlist()->[0]);
syslogw(LOG_INFO, "Found bluetoothctl version %s.", $bluetoothctl->matchlist()->[0]);
exp_continue();
}],
);
mqtt_check_reconnect($mqtt, $mqtttopic);
# For performance reasons, a maximum of one task is performed per loop
if (time() >= $nextheartbeat) {
$mqtt->publish($mqtttopic . '/heartbeat', time());
$nextheartbeat = time() + $HEARTBEAT_INTERVAL;
} elsif (time() > $nextevaluation) {
evaluation_task($absentinterval, $mqtttopic, $retain);
$nextevaluation = time() + $EVALUATION_INTERVAL;
} elsif ((time() - $lastquery > $QUERY_INTERVAL)) {
query_task($bluetoothctl);
$lastquery=time();
} elsif (time() > $nextcleanup) {
cleanup_task();
$nextcleanup = time() + $CLEANUP_INTERVAL;
} elsif ($debug >= 1 && time() >= $nextdump) {
dump_task();
$nextdump = time() + $DUMP_INTERVAL;
}
usleep($MAINLOOP_SLEEP_US);
} while ((!$watchdogthreshold || (time() - $lastevent < $watchdogthreshold)) && ($batterymaxage == 0 || time() < $nextbattery));
$bluetoothctl->send("exit\n");
$bluetoothctl->hard_close();
if ($batterymaxage > 0 && time() >= $nextbattery) {
battery_task($mqtttopic, $forcebattery);
} else {
syslogw(LOG_INFO, "Watchdog timeout! Didn't receive any beacon for %i seconds. Restarting bluetoothctl handling in %i second(s).", $watchdogthreshold, $BLUETOOTHCTL_RESTART_SLEEP);
sleep($BLUETOOTHCTL_RESTART_SLEEP);
}
}
__DATA__
This exists to allow the locking code at the beginning of the file to work and to praise adamk's wisdom.