From cea779c61de122229172205678e23a4a2a6b1a13 Mon Sep 17 00:00:00 2001 From: PatrickR <> Date: Thu, 2 Mar 2017 19:41:49 +0000 Subject: [PATCH] lepresenced: Update to V0.8, added rssi, added V0.8 deb package git-svn-id: https://svn.fhem.de/fhem/trunk@13581 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- .../PRESENCE/deb/lepresenced-0.8-1.deb | Bin 0 -> 6620 bytes fhem/contrib/PRESENCE/lepresenced | 577 +++++++++++------- 2 files changed, 354 insertions(+), 223 deletions(-) create mode 100644 fhem/contrib/PRESENCE/deb/lepresenced-0.8-1.deb diff --git a/fhem/contrib/PRESENCE/deb/lepresenced-0.8-1.deb b/fhem/contrib/PRESENCE/deb/lepresenced-0.8-1.deb new file mode 100644 index 0000000000000000000000000000000000000000..c1f0b9192a21b812920c20fb4cdbf708bc94a562 GIT binary patch literal 6620 zcmai&RZts_y2R1o#flT0;ts(=aCc~Nr$}&z;83h+ard@Z(c%!?-L(|=QrzwS&$%-9 z?d*KBGdthx+wW;gc%M=}*8?&e{v#^p3FulhAdh+)MCY6br?%UgI znn3rdonH6-o2y@45LBWapA%wYk6zzWi7TZml~h^$uvgyXY2;-IVoM{?EvkT9viN4Q zR{Cpw_eO41pZXc~l)N7_RP0D~^_)PG{CGVXNnid-xY%jXEl3L9ME1czj)_~|KmAh4@rzsan=x+5fDC_dzk+lCZB&``bYnX zM*$&W(0>DEhWRY$t7m|KfU>txFNe}Kz!7|gNQ+4PZ^RIgN@Z9!ykChsj1fq%+9?AN zQ@eml5ExO<*t=pQ1=*KP8>7zxWF>E56I>&iBIgB3&gXR1q#(k&>byq7;Wz<~=OWQt z4N>gtv9t+mM#wRzX)fS+3Q83IOKeDU{^IpO*~#H!I9yQ3d_3;wrytPa5{8E#vUDXs zW5d7!s_@~kG;L0srrtq=JFuJxSj6F7RK>M+cNxs49&hyAb)^uIFaeif$P*U5jM2OB z6q)U0OtOW?+B-t?)NE2^A!u<&H!LU<_6ueDq{{*k=CnaIKMbfIoCv6 z$jaBY;0o$=#~*S0ghNg+v182qB3zU5cQ&x*Y-ZwF7w^p-A}qK;&6HO518tU?UkQ4C zXoTg6^dLSl_lt>2iB3PU?CG?I=SsA9Jl1(u3I&?okgbQmZ3-;uSMTl1;K~>xFOwvK ziFrZJnn#_-N5jmKGc=^y9_)lV0~7_DA0_Ox0Vo)vRC*jkoJ!ey8fDoYNzHKB%rG?P z$BOPQGkL{ozYhw*8Z+6-rnZzt?H};uUG&wQilj&0SKO-}%t5y4Io0|i~8 zk^|*(KvWoN7H#bNBxJXaYTYj)tWqoeW-bte_iq_ovwz>thvX}FUmw2AUats?` z6n2{b5kr3Vi~4YCw9OcQufSg(akwAyc(9KX?_ z{6}Z=k5N(O`>&;;_?F(KE$y}be`V6zk&Uj};~;C2^(W{m`G5W2w>#8DS~{-3#(-?9 zt2vFG67(*%DZb?Ny0G-=-M%(7Cf6@}rg{f3?)-FUmoUs)yxBC({&6Hk4# zBe`n$RnsLbXhP_-y+(%Ju#xM6SyxMt^edyq)bAS~2fbGG5f=}Fp9RKiA5HP=2wYLaFXiapBJ zKdb&-PK^eGRn`g1uPV0RUX6Fg}NJgf*Pfu27pdwLQH&->lredswT;ydh(i;5NWeztqLd*VK zG0le1R5DAtZHPb@@xV0;r(jPY_BC`G%P=xfuo57D%)bu%+IS~D6cUQnuo}0`2W>o2 zThJp$)|l&jJn)XgfbOO_^@zm#8(TPbYOiIBwu)1we9HLf@G0>iV9~h|J>96}o z^N8clRS%UsLrAn2qnK-gX>-;8!m*LtM4ZSPzZZM05R%^WE@Fcc`dR#W_ih<9r;p)TCm;UNISJTvz1&n#36AspxjP7E8rg|I>p@(HV~tU$10cjCsw;F(*?o zxI+epBQ!NRRni-Fp~6yoalJB^c$6U%KRzYPFguxR1Db?trNxf)6kBP42`^P!7Rpw4 z`O#I)XKS{T%@Bups2)aa+9vHvxA_-q*(Atd=Wk*hrbX7#{@!|3qI5b1n!ry<3MHe~ ztI9xAW%Ba@{EfKWn6J6Jdskd2@Bj*6#qyZV%%RG#K1~`%pyY`_E;9KwFMe)ilplpZ z)DO=m+%sQvFSuW8wVLAwBEk0Eb~if0d7KC#*UA}|qh8W+qtk(;DdzVK>m`!`JlnOc z(>^#qu0*$>w3ug&4&&1y=n&rR$8@&3c2dcxS@{^}76+w3B1{N)(g6=|g2yjEa=!Ye zX})M2A<29sO)S4>&H9w9^q7{QaO8ABO;N}An<`1SZv$0ektk$@h0uTAHPL#%Ub-fF zwLK1Ma!CY{no3txkVAxrvJ$!>stwQ>y=#<67g;A1u&B8<(13Bf??l7%zA>@?yDr9 zdSQ(X-fzqE3a7`R@} Qk#sHNyuv34h75tnG2H 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 literal 0 HcmV?d00001 diff --git a/fhem/contrib/PRESENCE/lepresenced b/fhem/contrib/PRESENCE/lepresenced index 8c4f8a16c..72bcc991f 100755 --- a/fhem/contrib/PRESENCE/lepresenced +++ b/fhem/contrib/PRESENCE/lepresenced @@ -1,31 +1,31 @@ #!/usr/bin/perl ############################################################################## -# $Id$ +# $Id$ ############################################################################## # -# lepresenced +# lepresenced # -# checks for one or multiple bluetooth *low energy* devices for their -# presence state and reports it to the 73_PRESENCE.pm module. +# checks for one or multiple bluetooth *low energy* devices for their +# presence state and reports it to the 73_PRESENCE.pm module. # -# Copyright (C) 2015-2016 P. Reinhardt, pr-fhem (at) reinhardtweb (dot) de +# Copyright (C) 2015-2016 P. Reinhardt, pr-fhem (at) reinhardtweb (dot) de # -# 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. +# 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. +# 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. +# 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. # ############################################################################## @@ -43,261 +43,392 @@ use Sys::Syslog qw(:standard :macros); use Time::HiRes qw(usleep); use Net::Server::Daemonize qw(daemonize); -use constant RETRY_SLEEP => 1; -use constant INET_RECV_BUFFER => 1024; -use constant MAINLOOP_SLEEP_US => 100 * 1000; +use constant RETRY_SLEEP => 1; +use constant INET_RECV_BUFFER => 1024; +use constant MAINLOOP_SLEEP_US => 250 * 1000; -use constant CLEANUP_INTERVAL => 15 * 60; -use constant CLEANUP_MAX_AGE => 30 * 60; -use constant STATS_INTERVAL => 5 * 60; +use constant CLEANUP_INTERVAL => 15 * 60; +use constant CLEANUP_MAX_AGE => 30 * 60; +use constant STATS_INTERVAL => 5 * 60; -use constant ME => 'lepresenced'; -use constant VERSION => '0.6'; +use constant DEFAULT_RSSI_THRESHOLD => 10; +use constant RSSI_WINDOW => 10; -use constant PIDFILE => '/var/run/' . ME . '.pid'; +use constant ME => 'lepresenced'; +use constant VERSION => '0.8'; + +use constant PIDFILE => '/var/run/' . ME . '.pid'; + +use constant { + HCIDUMP_STATE_NONE => 0, + HCIDUMP_STATE_LE_META_EVENT => 1, + HCIDUMP_STATE_LE_ADVERTISING_REPORT => 2, + HCIDUMP_STATE_ADV_INT => 3, + HCIDUMP_STATE_SCAN_RSP => 4, +}; my %devices :shared; my @clients = (); my $syslog_level; sub syslogw { - return if (scalar(@_) < 2); - if (scalar(@_)==2) { - my ($priority, $message) = @_; - syslog($priority, "[tid:%i] %s: $message", threads->self()->tid(), (caller(1))[3] // 'main') if ($syslog_level >= $priority); - } else { - my ($priority, $format, @args) = @_; - syslog($priority, "[tid:%i] %s: $format", threads->self()->tid(), (caller(1))[3] // 'main', @args) if ($syslog_level >= $priority); - } + return if (scalar(@_) < 2); + if (scalar(@_)==2) { + my ($priority, $message) = @_; + syslog($priority, "[tid:%i] %s: $message", threads->self()->tid(), (caller(1))[3] // 'main') if ($syslog_level >= $priority); + } else { + my ($priority, $format, @args) = @_; + syslog($priority, "[tid:%i] %s: $format", threads->self()->tid(), (caller(1))[3] // 'main', @args) if ($syslog_level >= $priority); + } } sub error_exit { - my $exit_code = shift(); - syslogw(LOG_ERR, @_); - foreach my $thread (threads->list()) { - $thread->exit(0); - } - exit ($exit_code); + my $exit_code = shift(); + syslogw(LOG_ERR, @_); + foreach my $thread (threads->list()) { + $thread->exit(0); + } + exit ($exit_code); } sub usage_exit() { - print("usage:\n"); - printf("\t%s --bluetoothdevice --listenaddress --listenport --loglevel --daemon\n", ME); - printf("\t%s -b -a -p -l -d\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("examples:\n"); - printf("\t%s --bluetoothdevice hci0 --listenaddress 127.0.0.1 --listenport 5333 --daemon\n", ME); - printf("\t%s --loglevel LOG_DEBUG --daemon\n", ME); - closelog(); - exit(1); + print("usage:\n"); + printf("\t%s --bluetoothdevice --listenaddress --listenport --loglevel --daemon\n", ME); + printf("\t%s -b -a -p -l -d\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("optional arguments:\n"); + print("--legacymode - legacy mode without rssi detection. Use if you do not have hcidump installed.\n"); + printf("--rssithreshold - rssi deviation to trigger an update. Minimum value: 5, default: %s\n", DEFAULT_RSSI_THRESHOLD); + print("examples:\n"); + printf("\t%s --bluetoothdevice hci0 --listenaddress 127.0.0.1 --listenport 5333 --daemon\n", ME); + printf("\t%s --loglevel LOG_DEBUG --daemon\n", ME); + closelog(); + exit(1); } sub parse_options() { - my $device = "hci0"; - my $daemonize = 0; - my $listen_address = "0.0.0.0"; - my $listen_port = "5333"; - my $syslog_level = "LOG_INFO"; - - GetOptions( - 'bluetoothdevice|device|b=s' => \$device, - 'daemon|daemonize|d!' => \$daemonize, - 'listenaddress|address|a=s' => \$listen_address, - 'listenport|port|p=i' => \$listen_port, - 'loglevel|l=s' => \$syslog_level - ) or usage_exit(); - $listen_address =~ m/^\d+\.\d+\.\d+\.\d+$/ or usage_exit(); - $syslog_level =~ m/^LOG_(EMERG|ALERT|CRIT|ERR|WARNING|NOTICE|INFO|DEBUG)$/ or usage_exit(); - $syslog_level = eval($syslog_level); - - return ($device, $daemonize, $listen_address, $listen_port, $syslog_level); + my $device = "hci0"; + my $daemonize = 0; + my $listen_address = "0.0.0.0"; + my $listen_port = "5333"; + my $syslog_level = "LOG_INFO"; + my $legacy_mode = 0; + my $rssi_threshold = DEFAULT_RSSI_THRESHOLD; + + GetOptions( + 'bluetoothdevice|device|b=s' => \$device, + 'daemon|daemonize|d!' => \$daemonize, + 'listenaddress|address|a=s' => \$listen_address, + 'listenport|port|p=i' => \$listen_port, + 'loglevel|l=s' => \$syslog_level, + 'legacymode|legacy!' => \$legacy_mode, + 'rssithreshold=i' => \$rssi_threshold, + ) or usage_exit(); + + usage_exit() if ($rssi_threshold < 5); + + $listen_address =~ m/^\d+\.\d+\.\d+\.\d+$/ or usage_exit(); + $syslog_level =~ m/^LOG_(EMERG|ALERT|CRIT|ERR|WARNING|NOTICE|INFO|DEBUG)$/ or usage_exit(); + $syslog_level = eval($syslog_level); + + return ($device, $daemonize, $listen_address, $listen_port, $syslog_level, $legacy_mode, $rssi_threshold); } -sub update_device($$) { - my ($mac, $name) = @_; - $mac = lc($mac); - { - lock(%devices); - unless (exists $devices{$mac}) { - my %device :shared; - $devices{$mac} = \%device; - } - $devices{$mac}{'name'} = $name unless ($name eq '(unknown)' && defined($devices{$mac}{'name'})); - $devices{$mac}{'timestamp'} = time(); - } +sub update_device($$$) { + my ($mac, $name, $rssi) = @_; + $mac = lc($mac); + { + lock(%devices); + unless (exists $devices{$mac}) { + my %device :shared; + $devices{$mac} = \%device; + } + $name = '(unknown)' if ($name eq ''); + if (!defined($devices{$mac}{'name'}) || $name ne '(unknown)') { + $devices{$mac}{'name'} = $name + } + $devices{$mac}{'rssi'} = $rssi; + $devices{$mac}{'reported_rssi'} = $rssi if (!defined($devices{$mac}{'reported_rssi'})); + $devices{$mac}{'timestamp'} = time(); + } + #dump_devices(); } -sub bluetooth_thread($) { - my ($device) = @_; - my $hcitool; - for(;;) { - my $pid = open($hcitool, "-|", "stdbuf -oL hcitool -i " . $device . " lescan --duplicates 2>&1") || die('Unable to start scanning. Please make sure hcitool and stdbuf are installed!'); - while (<$hcitool>) { - chomp($_); - if ($_ eq 'LE Scan ...') { - syslogw(LOG_INFO, "Received '%s'.", $_); - } elsif (my ($fbmac, $fbname) = $_ =~ /^([\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2})\s(.*)$/i) { - #syslogw(LOG_DEBUG, "Received advertisement from bluetooth mac address '%s' with name '%s'.", $fbmac, $fbname); - update_device($fbmac, $fbname); - } elsif ( - $_ =~ m/^Set scan parameters failed: Input\/output error$/ || - $_ =~ m/^Invalid device: Network is down$/ - ) { - syslogw(LOG_WARNING, "Received '%s', resetting...", $_); - system(sprintf('hciconfig %s reset', $device)); - } else { - syslogw(LOG_WARNING, "Received unknown output: '%s'!", $_); - } - } - syslogw(LOG_WARNING, "hcitool exited, retrying..."); - close($hcitool); - sleep(RETRY_SLEEP); - } +sub dump_devices() { + foreach my $mac (keys(%devices)) { + printf("mac: %s, timestamp: %s, rssi: %s, name: %s\n", $mac, $devices{$mac}{'timestamp'}, $devices{$mac}{'rssi'}, $devices{$mac}{'name'}); + } + print("\n"); +} + +sub bluetooth_scan_thread($$) { + my ($device, $legacy_mode) = @_; + my $hcitool; + for(;;) { + my $pid = open($hcitool, "-|", "stdbuf -oL hcitool -i " . $device . " lescan --duplicates 2>&1") || die('Unable to start scanning. Please make sure hcitool and stdbuf are installed!'); + while (<$hcitool>) { + chomp($_); + if ($_ eq 'LE Scan ...') { + syslogw(LOG_INFO, "Received '%s'.", $_); + } elsif (my ($fbmac, $fbname) = $_ =~ /^([\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2})\s(.*)$/i) { + if ($legacy_mode) { + #syslogw(LOG_DEBUG, "Received advertisement from bluetooth mac address '%s' with name '%s'.", $fbmac, $fbname); + update_device($fbmac, $fbname, 'unknown'); + } + } elsif ( + $_ =~ m/^Set scan parameters failed: Input\/output error$/ || + $_ =~ m/^Invalid device: Network is down$/ + ) { + syslogw(LOG_WARNING, "Received '%s', resetting...", $_); + system(sprintf('hciconfig %s reset', $device)); + } else { + syslogw(LOG_WARNING, "Received unknown output: '%s'!", $_); + } + } + syslogw(LOG_WARNING, "hcitool exited, retrying..."); + close($hcitool); + sleep(RETRY_SLEEP); + } +} + +sub bluetooth_dump_thread($) { + my ($device) = @_; + my $hcidump; + my %rssitable; + + for(;;) { + my $pid = open($hcidump, "-|", "hcidump -i " . $device) || die('Unable to start scanning. Please make sure hcidump is installed or use legacy mode (--legacymode)!'); + my $state = HCIDUMP_STATE_NONE; + my $current_mac = ''; + my $current_rssi = ''; + my $current_name = ''; + + while (<$hcidump>) { + chomp($_); + if ($_ =~ m/^>/) { + if ($current_mac) { + #printf("DEBUG: mac: %s, name: '%s', rssi: %s\n", $current_mac, $current_name, $current_rssi); + + # update rssi queue + unless (exists $rssitable{$current_mac}) { + $rssitable{$current_mac} = []; + } + if ($current_rssi) { + shift(@{$rssitable{$current_mac}}) if(scalar(@{$rssitable{$current_mac}}) >= RSSI_WINDOW); + push(@{$rssitable{$current_mac}}, $current_rssi); + } + my $mean_rssi = 0; + foreach my $rssi (@{$rssitable{$current_mac}}) { + $mean_rssi += $rssi; + } + $mean_rssi = int($mean_rssi / scalar(@{$rssitable{$current_mac}})); + #printf("DEBUG: mac: %s, rssi count: %i, rssis: %s, mean: %s\n", $current_mac, scalar(@{$rssitable{$current_mac}}), join(',', @{$rssitable{$current_mac}}), $mean_rssi); + + update_device($current_mac, $current_name, $mean_rssi); + } + $current_mac = ''; + $current_rssi = ''; + $current_name = ''; + if ($_ =~ m/^> HCI Event: LE Meta Event \(0x3e\) plen \d+$/) { + $state = HCIDUMP_STATE_LE_META_EVENT; + } else { + $state = HCIDUMP_STATE_NONE; + } + } elsif ( + $state == HCIDUMP_STATE_LE_META_EVENT && + $_ eq ' LE Advertising Report' + ) { + $state = HCIDUMP_STATE_LE_ADVERTISING_REPORT; + } elsif ($state == HCIDUMP_STATE_LE_ADVERTISING_REPORT) { + if ( + $_ eq ' ADV_IND - Connectable undirected advertising (0)' || + $_ eq ' ADV_NONCONN_IND - Non connectable undirected advertising (3)' + ) { + $state = HCIDUMP_STATE_ADV_INT; + } elsif ($_ eq ' SCAN_RSP - Scan Response (4)') { + $state = HCIDUMP_STATE_SCAN_RSP; + } + } elsif ($state == HCIDUMP_STATE_SCAN_RSP || $state == HCIDUMP_STATE_ADV_INT) { + if ($_ =~ m/^ bdaddr ([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}) \((Public|Random)\)$/) { + $current_mac = $1; + } elsif ($_ =~ m/^ Complete local name: '(.*)'$/) { + $current_name = $1; + } elsif ($_ =~ m/^ RSSI: (-\d+)$/) { + $current_rssi = $1; + } + } + } + syslogw(LOG_WARNING, "hcidump exited, retrying..."); + close($hcidump); + sleep(RETRY_SLEEP); + } } sub handle_command($$) { - my ($buf, $current_client) = @_; - if (my ($mac, undef, $interval) = $buf =~ m/^\s*(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})\s*\|\s*(\d+)\s*$/) { - $mac = lc($mac); - if (my ($client) = grep { $current_client == $_->{'handle'} } @clients) { - syslogw(LOG_INFO, "Received query update for mac address %s, interval: %i by client %s:%i.", $mac, $interval, $current_client->peerhost(), $current_client->peerport()); - $client->{'mac'} = $mac; - $client->{'interval'} = $interval; - $client->{'next_check'} = 0; #now - } else { - syslogw(LOG_INFO, "Received query for mac address %s, interval: %i. Adding client %s:%i to clients list.", $mac, $interval, $current_client->peerhost(), $current_client->peerport()); - my %new_client; - $new_client{'handle'} = $current_client; - $new_client{'mac'} = $mac; - $new_client{'interval'} = $interval; - $new_client{'next_check'} = 0; #now - push(@clients, \%new_client); - } - print $current_client "command accepted\n" - } elsif ($buf =~ m/^\s*now\s*$/) { - syslogw(LOG_DEBUG, "Received now command from client %s:%i. Scheduling update...", $current_client->peerhost(), $current_client->peerport()); - foreach my $client (grep { $_->{'handle'} == $current_client } @clients) { - $client->{'next_check'} = 0; #now - } - print $current_client "command accepted\n" - } elsif ($buf =~ m/^\s*stop\s*$/) { - # Stop does not make sense when scanning permanently - syslogw(LOG_DEBUG, "Received stop command from client %s:%i. Pretending to care and ignoring...", $current_client->peerhost(), $current_client->peerport()); - print $current_client "no command running\n" # ToDo: Does the FHEM module even care? - } else { - syslogw(LOG_WARNING, "Received unknown command: '%s'.", $buf); - } + my ($buf, $current_client) = @_; + if (my ($mac, undef, $interval) = $buf =~ m/^\s*(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})\s*\|\s*(\d+)\s*$/) { + $mac = lc($mac); + if (my ($client) = grep { $current_client == $_->{'handle'} } @clients) { + syslogw(LOG_INFO, "Received query update for mac address %s, interval: %i by client %s:%i.", $mac, $interval, $current_client->peerhost(), $current_client->peerport()); + $client->{'mac'} = $mac; + $client->{'interval'} = $interval; + $client->{'next_check'} = 0; #now + } else { + syslogw(LOG_INFO, "Received query for mac address %s, interval: %i. Adding client %s:%i to clients list.", $mac, $interval, $current_client->peerhost(), $current_client->peerport()); + my %new_client; + $new_client{'handle'} = $current_client; + $new_client{'mac'} = $mac; + $new_client{'interval'} = $interval; + $new_client{'next_check'} = 0; #now + push(@clients, \%new_client); + } + print $current_client "command accepted\n" + } elsif ($buf =~ m/^\s*now\s*$/) { + syslogw(LOG_DEBUG, "Received now command from client %s:%i. Scheduling update...", $current_client->peerhost(), $current_client->peerport()); + foreach my $client (grep { $_->{'handle'} == $current_client } @clients) { + $client->{'next_check'} = 0; #now + } + print $current_client "command accepted\n" + } elsif ($buf =~ m/^\s*stop\s*$/) { + # Stop does not make sense when scanning permanently + syslogw(LOG_DEBUG, "Received stop command from client %s:%i. Pretending to care and ignoring...", $current_client->peerhost(), $current_client->peerport()); + print $current_client "no command running\n" # ToDo: Does the FHEM module even care? + } else { + syslogw(LOG_WARNING, "Received unknown command: '%s'.", $buf); + } } sub stats_task() { - my ($min_age, $max_age, $devices); - { - lock(%devices); - $devices = scalar(keys(%devices)); - foreach my $mac (keys(%devices)) { - my $age = time() - $devices{$mac}{'timestamp'}; - $min_age = $age if (!defined($min_age) || $age < $min_age); - $max_age = $age if (!defined($max_age) || $age > $max_age); - } - } - syslogw(LOG_INFO, "Active clients: %i, known devices: %i (min/max age: %s/%s)", scalar(@clients), $devices, $min_age // '%', $max_age // '%'); + my ($min_age, $max_age, $devices); + { + lock(%devices); + $devices = scalar(keys(%devices)); + foreach my $mac (keys(%devices)) { + my $age = time() - $devices{$mac}{'timestamp'}; + $min_age = $age if (!defined($min_age) || $age < $min_age); + $max_age = $age if (!defined($max_age) || $age > $max_age); + } + } + syslogw(LOG_INFO, "Active clients: %i, known devices: %i (min/max age: %s/%s)", scalar(@clients), $devices, $min_age // '%', $max_age // '%'); } sub cleanup_task() { - my $start_time = time(); - my $deleted_items = 0; - { - lock(%devices); - foreach my $mac (keys(%devices)) { - my $age = time() - $devices{$mac}{'timestamp'}; - 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); + my $start_time = time(); + my $deleted_items = 0; + { + lock(%devices); + foreach my $mac (keys(%devices)) { + my $age = time() - $devices{$mac}{'timestamp'}; + 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); } openlog(ME, 'pid', LOG_USER); -(my $device, my $daemonize, my $listen_address, my $listen_port, $syslog_level) = parse_options(); +(my $device, my $daemonize, my $listen_address, my $listen_port, $syslog_level, my $legacy_mode, my $rssi_threshold) = parse_options(); - local $SIG{INT} = local $SIG{TERM} = local $SIG{HUP} = sub { - syslogw(LOG_NOTICE, "Caught signal, cleaning up and exiting..."); - unlink(PIDFILE) if (-e PIDFILE); - closelog(); - exit(1); - }; + local $SIG{INT} = local $SIG{TERM} = local $SIG{HUP} = sub { + syslogw(LOG_NOTICE, "Caught signal, cleaning up and exiting..."); + unlink(PIDFILE) if (-e PIDFILE); + closelog(); + exit(1); + }; -syslogw(LOG_NOTICE, "Version %s started (device: %s, listen addr: %s, listen port: %s, daemonize: %i, log level: %i).", - VERSION, $device, $listen_address, $listen_port, $daemonize, $syslog_level); +syslogw(LOG_NOTICE, "Version %s started (device: %s, listen addr: %s, listen port: %s, daemonize: %i, legacy mode: %i, rssi threshold: %i, log level: %i).", + VERSION, $device, $listen_address, $listen_port, $daemonize, $legacy_mode, $rssi_threshold, $syslog_level); daemonize('root', 'root', PIDFILE) if $daemonize; -my $bluetooth_thread = threads->new(\&bluetooth_thread, $device)->detach(); +my $bluetooth_scan_thread = threads->new(\&bluetooth_scan_thread, $device, $legacy_mode)->detach(); +my $bluetooth_dump_thread = threads->new(\&bluetooth_dump_thread, $device)->detach() if (!$legacy_mode); my $current_client; $| = 1; my $server_socket = new IO::Socket::INET ( - LocalHost => $listen_address, - LocalPort => $listen_port, - Proto => 'tcp', - Listen => 5, - ReuseAddr => 1, + LocalHost => $listen_address, + LocalPort => $listen_port, + Proto => 'tcp', + Listen => 5, + ReuseAddr => 1, ); $server_socket or error_exit(1, "ERROR: Unable to create TCP server: $!, Exiting."); my $select = IO::Select->new($server_socket) or error_exit(1, "ERROR: Unable to select: $!, Exiting."); -my $next_stats_time = 0; -my $next_cleanup_time = 0; +my $next_stats_time = time() + STATS_INTERVAL; +my $next_cleanup_time = time() + CLEANUP_INTERVAL; + +$SIG{PIPE} = sub { + syslogw(LOG_INFO, "SIGPIPE received!"); +}; for(;;) { - # Process INET socket - foreach my $current_client ($select->can_read(0)) { - if($current_client == $server_socket) { - my $client_socket = $server_socket->accept(); - $select->add($client_socket); - syslogw(LOG_INFO, "Connection from %s:%s. Connected clients: %i.", $client_socket->peerhost(), $client_socket->peerport(), $select->count()-1); - } else { - sysread ($current_client, my $buf, INET_RECV_BUFFER); - if ($buf) { - chomp($buf); - handle_command($buf, $current_client); - } else { - $select->remove($current_client); - @clients = grep {$_->{'handle'} != $current_client} @clients; - syslogw(LOG_INFO, "Client %s:%s disconnected. Connected clients: %i.", $current_client->peerhost(), $current_client->peerport(), $select->count()-1); - $current_client->close(); - } - } - } + # Process INET socket + foreach my $current_client ($select->can_read(0)) { + if($current_client == $server_socket) { + my $client_socket = $server_socket->accept(); + $select->add($client_socket); + syslogw(LOG_INFO, "Connection from %s:%s. Connected clients: %i.", $client_socket->peerhost(), $client_socket->peerport(), $select->count()-1); + } else { + sysread ($current_client, my $buf, INET_RECV_BUFFER); + if ($buf) { + chomp($buf); + handle_command($buf, $current_client); + } else { + $select->remove($current_client); + @clients = grep {$_->{'handle'} != $current_client} @clients; + syslogw(LOG_INFO, "Client %s:%s disconnected. Connected clients: %i.", $current_client->peerhost(), $current_client->peerport(), $select->count()-1); + $current_client->close(); + } + } + } - # Check for due client updates, cleanup, stats - # For performance reasons, a maximum of one task is performed per loop - if (my @due_clients = grep { time() >= $_->{'next_check'} } @clients) { - foreach my $client (@due_clients) { - if ( - defined($devices{$client->{'mac'}}) && - time()-$devices{$client->{'mac'}}{timestamp} <= $client->{'interval'} - ) { - syslogw(LOG_DEBUG, "Sending update for mac address %s, age: %i, max age: %i, result: present.", $client->{'mac'}, time()-$devices{$client->{'mac'}}{timestamp}, $client->{'interval'}); - printf {$client->{'handle'}} "present;%s\n", $devices{$client->{'mac'}}{name} - } else { - syslogw(LOG_DEBUG, "Sending update for mac address %s, max age: %i, result: absence.", $client->{'mac'}, $client->{'interval'}); - print {$client->{'handle'}} "absence\n" - } - $client->{'next_check'} = time() + $client->{'interval'}; - } - } elsif (time() > $next_cleanup_time) { - cleanup_task(); - $next_cleanup_time = time() + CLEANUP_INTERVAL; - } elsif (time() > $next_stats_time) { - stats_task(); - $next_stats_time = time() + STATS_INTERVAL; - } + # Check for updates due to a changed rssi + if (!$legacy_mode) { + lock(%devices); + my $devices = scalar(keys(%devices)); + foreach my $mac (keys(%devices)) { + if (abs($devices{$mac}{'reported_rssi'} - $devices{$mac}{'rssi'}) > $rssi_threshold) { + if (my @due_clients = grep { $_->{'mac'} eq $mac } @clients) { + syslogw(LOG_DEBUG, "Mac address %s needs update due to changed rssi. Old/new rssi: %i/%i, difference: %i, affected clients: %i.", $mac, $devices{$mac}{'reported_rssi'}, $devices{$mac}{'rssi'}, abs($devices{$mac}{'reported_rssi'} - $devices{$mac}{'rssi'}), scalar(@due_clients)); + foreach my $client (@due_clients) { + $client->{'next_check'} = 0; #now + } + } + } + } + } + + # Check for due client updates, cleanup, stats + # For performance reasons, a maximum of one task is performed per loop + if (my @due_clients = grep { time() >= $_->{'next_check'} } @clients) { + foreach my $client (@due_clients) { + if (defined($devices{$client->{'mac'}}) && time()-$devices{$client->{'mac'}}{timestamp} <= $client->{'interval'}) { + syslogw(LOG_DEBUG, "Sending update for mac address %s, age: %i, max age: %i, rssi: %i, result: present.", $client->{'mac'}, time()-$devices{$client->{'mac'}}{'timestamp'}, $client->{'interval'}, $devices{$client->{'mac'}}{'rssi'}); + printf {$client->{'handle'}} "present;device_name=%s;rssi=%s;daemon=%s V%s\n", $devices{$client->{'mac'}}{name}, $devices{$client->{'mac'}}{'rssi'}, ME, VERSION; + } else { + syslogw(LOG_DEBUG, "Sending update for mac address %s, max age: %i, result: absence.", $client->{'mac'}, $client->{'interval'}); + printf {$client->{'handle'}} "absence;rssi=unreachable;daemon=%s V%s\n", ME, VERSION; + } + if (defined($devices{$client->{'mac'}})) { + lock(%devices); + $devices{$client->{'mac'}}{'reported_rssi'} = $devices{$client->{'mac'}}{'rssi'}; + } + $client->{'next_check'} = time() + $client->{'interval'}; + } + } elsif (time() > $next_cleanup_time) { + cleanup_task(); + $next_cleanup_time = time() + CLEANUP_INTERVAL; + } elsif (time() > $next_stats_time) { + stats_task(); + $next_stats_time = time() + STATS_INTERVAL; + } - usleep(MAINLOOP_SLEEP_US); + usleep(MAINLOOP_SLEEP_US); } $server_socket->close();