What do I mean by Presence Detection? Presence Detection is adding the ability to your system to detect a presence of someone or something either in your home or on your network. For my purposes I pretty much combine the two. The majority of us carry our smart phones with us everywhere we go so one could almost argue that the phone is just an extension of ourselves. I know this isn’t entirely true and sure someone could be in possession of someone else’s cellphone. For the sake of security you might not want to rely on it to automatically unlock your doors or something of that nature but at the same time… is it really any different than someone being in possession of a physical key to your house?
In my home I only rely on presence detection for a few different tasks. The first one is automating the HVAC system, setting different profiles when my wife or I are home. The second is turning off entertainment devices and lights when no one is home. But how exactly can you do this presence detection?
At the core, this is a super simple task. The first step was setting up a cron job on my Raspberry Pi to run every 5 minutes. This cron task runs an ARP scan on the network and saves the results to a text file.
sudo crontab -e;
Then add a record like this:
*/5 * * * * /usr/bin/arp-scan -l --ignoredups > /home/pi/presence.txt
If your network is anything like mine, you’ll end up with a large list of devices on your network. That’s ok, but to have meaningful presence detection you’ll probably want to identify your permanent devices vs your mobile devices. For example it wouldn’t be a good idea to base presence of someone in your house based on having a smart tv or blu-ray player attached to your network. These devices hopefully aren’t going anywhere.
One nice thing to note about the ARP scan, at least in my case, is that your phone can be connected to your network via VPN and it will not be included in the list.
The second main step of the process is setting a php cron script to parse the info and store it in a database to be used by your application. I setup the cron on the same schedule.
*/5 * * * * /usr/bin/php /path/to/your/script/log_presences.php
Let’s dissect what is in that script.
<?php if(php_sapi_name() == "cli") { $_SERVER['DOCUMENT_ROOT'] = dirname(dirname(__FILE__)) . "/web"; } $sessions['enabled'] = false; require_once("{$_SERVER['DOCUMENT_ROOT']}/../conf/config.php"); sleep(10); $presence->get_data_from_file(); $qid = mysqli_query($dbh, "DELETE FROM `presence_log` WHERE DATE_SUB(NOW(), INTERVAL 24 HOUR) > `datetime`"); $presence->identify_people(); //merge the lists of watch list and mac addresses $merged = array_merge($presence->mac_addresses, $presence->watch); foreach($merged as $name => $mac){ if(array_key_exists($mac, $presence->people)){ //user is present $status = '1'; }else{ //user is not present $status = '0'; } //print_r($name); //echo $status; $query = " INSERT INTO `presence_log` ( `id`, `mac`, `datetime`, `status` ) VALUES ( NULL, '".mysqli_custom_escaper($mac, $dbh)."', NOW(), '".mysqli_custom_escaper($status, $dbh)."' ) "; $qid = mysqli_query($dbh, $query); $presence->poll_status($mac); } ?>
First I import my standard config that holds the database connections, and various libraries. I add a sleep of 10 seconds for this script to offset the time it takes for the arp-scan to complete and log to the file. You could do without the sleep and just add a minute to your cron if you wanted. With the help of the presence class we obtain the data from the text file. We delete any old data from the log table and then parse the mac addresses looking for predefined “mobile” mac addresses.
To help show you what is going on, let’s have a look at the presence class.
<?php class presence{ public $count = 0; private $mac_addresses = array(); public $people = []; public $watch = [ "katie" => "aa:11:bb:22:cc:33", "shawn" => "ab:12:bc:23:cd:34", "chris" => "ee:11:dd:22:cc:33" ]; function __construct(){ $this->who_is_here(); } public function __get($name) { //allow read-access to private/protected properties return isset($this->$name) ? $this->$name : null; } function get_data_from_file(){ $contents = file_get_contents("/home/pi/presence.txt"); $matches = array(); preg_match_all('/([a-fA-F0-9]{2}[:|\-]?){6}/m', $contents, $matches); $this->mac_addresses = $matches[0]; $this->count = count($matches[0]); $this->remove_static_devices(); //$this->identify_people(); } function remove_static_devices(){ $known = [ "34:9d:93:be:d7:6e", //ROUTER "00:d1:60:9e:3d:78", //Chromecast "86:64:91:b8:6d:3e", //Blu-Ray "23:29:f9:a6:8d:3c", //GARAGE DOOR ]; if(is_array($known) && is_array($this->mac_addresses)){ $flipped = array_flip($this->mac_addresses); foreach($known as $key => $value){ if(array_key_exists($value, $flipped)){ unset($flipped["{$value}"]); } } $this->mac_addresses = array_flip($flipped); $this->count = count($this->mac_addresses); } } function who_is_here(){ global $CFG, $dbh; $query = " SELECT `mac` FROM `presence` WHERE `status` = '1' "; $qid = mysqli_query($dbh, $query); $array = []; if($qid && mysqli_num_rows($qid)){ $results = mysqli_custom_fetch_all($qid); foreach($results as $key => $value){ $array[] = $value['mac']; } } $this->mac_addresses = $array; $this->identify_people(); $this->count = count($this->people); } function identify_people(){ $people = $this->watch; $named = array(); if(is_array($this->mac_addresses)){ $lookup = array_flip($people); foreach($this->mac_addresses as $key => $mac){ if(array_key_exists($mac, $lookup)){ $named["{$mac}"] = $lookup["{$mac}"]; }else{ //echo $mac; $named["{$mac}"] = "unknown"; } } } $this->people = $named; } function poll_status($mac){ //check to see current status global $CFG, $dbh; $query = " SELECT * FROM `presence` WHERE `mac` = '".mysqli_custom_escaper($mac, $dbh)."' LIMIT 1 "; $qid = mysqli_query($dbh, $query); if($qid && mysqli_num_rows($qid)){ $current = mysqli_fetch_assoc($qid); } if(!is_array($current)){ $current['status'] = 0; $current['mac'] = $mac; } $query = " SELECT * FROM `presence_log` WHERE `mac` = '".mysqli_custom_escaper($mac, $dbh)."' ORDER BY `datetime` DESC LIMIT 3 "; $qid = mysqli_query($dbh, $query); if($qid && mysqli_num_rows($qid)){ $results = mysqli_custom_fetch_all($qid); }else{ $results[0]['status'] = 0; } //if status = 1: reflect immediately if($results[0]['status'] == 1 && $current['status'] !== $results[0]['status']){ $qid = mysqli_query($dbh, "REPLACE INTO `presence` (`mac`, `datetime`, `status`) VALUES ('".mysqli_custom_escaper($current['mac'], $dbh)."', NOW(), '".mysqli_custom_escaper($results[0]['status'], $dbh)."')"); //$this->trigger_hook($results[0]['status']); } //if status = 0: check to see how many polls were missed $all_missed = true; if(is_array($results)){ foreach($results as $key => $value){ if($value['status'] == 1){ $all_missed = false; } } } if($all_missed == true && $results[0]['status'] == 0 && $current['status'] !== $results[0]['status']){ $qid = mysqli_query($dbh, "REPLACE INTO `presence` (`mac`, `datetime`, `status`) VALUES ('".mysqli_custom_escaper($current['mac'], $dbh)."', NOW(), '".mysqli_custom_escaper($results[0]['status'], $dbh)."')"); //$this->trigger_hook($results[0]['status']); } } function trigger_hook($status){ if($status == 0){ //triggered when no one is here. //Turn off all the lights $client = new \Phue\Client('ip_here', 'secret_key_here'); foreach ($client->getLights() as $lightId => $light) { $light->setOn(false); } }else{ //triggered when someone is here. } } //automate a coffeemaker when activity for a user starts up in the morning? // tie to the person's alarm clock app? can that be done? function likes_coffee($person){ } } ?>
The mac addresses in the above example are all fictitious and randomly generated. When the class is called we run the who_is_here method to find out what mac addresses have a status logged as 1 (for present). We run them by the identify_people method which contains a statically defined list of known mobile mac addresses. In this case you would add your known mobile mac addresses to the array. There are a couple extras I accounted for with this class such as “trigger_hook” and “likes_coffee” as brainstorming some ideas about automatically adding in hooks based on status changes or potentially someday automating a coffee brew when someones alarm goes off on their phone, but only in cases where the person likes coffee, of course.
The get_data_from_file method essentially reads the contents of the text file that arp-scan generates. It parses the mac addresses out of it. Once it has a list of all mac addresses we run the remove_static_devices method to remove known static devices. Any device that isn’t matched to a known person will automatically be logged as an “unknown person”. This lets us better monitor when friends join the network and will give us a chance to identify them/manually add them to our known list.
This will give you the added benefit of knowing when your friends arrive at your house in the event you are gone and want to let them in remotely.