Repository: andrewjfreyer/presence Branch: master Commit: f14d3d7c5e32 Files: 2 Total size: 20.4 KB Directory structure: gitextract_zicas_p1/ ├── README.md └── presence.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ presence ======= ***Note: as of `presence` 0.5.1, triggered scans, guest scanning, and beacon scanning are removed from `presence.sh` for simplification. Please consider using [monitor](http://github.com/andrewjfreyer/monitor) instead for beacon scanning and detection and for generic device detection. It is unlikely that `presence` will receive substantive updates after version 0.5.1.*** ____ ***TL;DR***: *Bluetooth-based presence detection useful for [mqtt-based](http://mqtt.org) home automation. More granular, responsive, and reliable than device-reported GPS. Cheaper, more reliable, more configurable, and less mqtt-spammy than Happy Bubbles. Does not require any app to be running or installed. Does not require device pairing. Designed to run as service on a [Raspberry Pi Zero W](https://www.raspberrypi.org/products/raspberry-pi-zero-w/).* Note that the more frequently you scan for devices, the more 2.4GHz bandwidth you will use. This script may cause interference with Wi-Fi or other bluetooth devices for particularly short delays between scans.

Summary

A JSON-formatted MQTT message is reported to a broker whenever a specified bluetooth device responds to a **name** query. If the device responds, the JSON message includes the name of the device and a **confidence** of 100. After a delay, another **name** query is sent and, if the device does not respond, a verification-of-absence loop begins that queries for the device (on a shorter interval) a set number of times. Each time, the device does not respond, the **confidence** is reduced, eventually to 0. A configuration file defines 'owner devices' that contains the mac addresses of the devices you'd like to regularly ping to determine presence. Topics are formatted like this: location/pi_zero_location/00:00:00:00:00:00 Messages are JSON formatted and contain **name** and **confidence** fields, including a javascript-formatted timestamp and a duration of a particular scan (in ms): { confidence : 100, name : Andrew’s iPhone, scan_duration_ms: 500, timestamp : Sat Apr 21 2018 11:52:04 GMT-0600 (MDT)} { confidence : 0, name : Andrew’s iPhone or Unknown, scan_duration_ms: 5000, timestamp : Sat Apr 21 2018 11:52:04 GMT-0600 (MDT)} ___

Example Use with Home Assistant

The presence script can be used as an input to a number of [mqtt sensors](https://www.home-assistant.io/components/sensor.mqtt/) in [Home Assistant.](https://www.home-assistant.io). Output from these sensors can be averaged to give a highly-accurate numerical occupancy confidence. In order to detect presence in a home that has three floors and a garage, we might include one Raspberry Pi per floor. For average houses, a single well-placed sensor can probably work, but for more reliability at the edges of the house, more sensors are better. ``` - platform: mqtt state_topic: 'location/first floor/00:00:00:00:00:00' value_template: '{{ value_json.confidence }}' unit_of_measurement: '%' name: 'Andrew First Floor' - platform: mqtt state_topic: 'location/second floor/00:00:00:00:00:00' value_template: '{{ value_json.confidence }}' unit_of_measurement: '%' name: 'Andrew Second Floor' - platform: mqtt state_topic: 'location/third floor/00:00:00:00:00:00' value_template: '{{ value_json.confidence }}' unit_of_measurement: '%' name: 'Andrew Third Floor' - platform: mqtt state_topic: 'location/garage/00:00:00:00:00:00' value_template: '{{ value_json.confidence }}' unit_of_measurement: '%' name: 'Andrew Garage' ``` These sensors can be combined/averaged using a [min_max](https://www.home-assistant.io/components/sensor.min_max/): ``` - platform: min_max name: "Andrew Home Occupancy Confidence" type: mean round_digits: 0 entity_ids: - sensor.andrew_garage - sensor.andrew_third_floor - sensor.andrew_second_floor - sensor.andrew_first_floor ``` So, as a result of this combination, we use the entity **sensor.andrew_home_occupancy_confidence** in automations to control the state of an **input_boolean** that represents a very high confidence of a user being home or not. As an example: ``` - alias: Andrew Occupancy hide_entity: true trigger: - platform: numeric_state entity_id: sensor.andrew_home_occupancy_confidence above: 10 action: - service: homeassistant.turn_on data: entity_id: input_boolean.andrew_occupancy ``` ___

Installation Instructions (Raspbian Jessie Lite Stretch):

Setup of SD Card

1. Download latest version of **jessie lite stretch** [here](https://downloads.raspberrypi.org/raspbian_lite_latest) 2. Download etcher from [etcher.io](https://etcher.io) 3. Image **jessie lite stretch** to SD card. [Instructions here.](https://www.raspberrypi.org/magpi/pi-sd-etcher/) 4. Mount **boot** partition of imaged SD card (unplug it and plug it back in) 5. **[ENABLE SSH]** Create blank file, without any extension, in the root directory called **ssh** 6. **[SETUP WIFI]** Create **wpa_supplicant.conf** file in root directory and add Wi-Fi details for home Wi-Fi: ``` country=US ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 network={ ssid="Your Network Name" psk="Your Network Password" key_mgmt=WPA-PSK } ``` 7. **[FIRST STARTUP]** Insert SD card and power on Raspberry Pi Zero W. On first boot, the newly-created **wpa_supplicant.conf** file and **ssh** will be moved to appropriate directories. Find the IP address of the Pi via your router. One method is scanning for open ssh ports (port 22) on your local network: ``` nmap 192.168.1.0/24 -p 22 ```

Configuration and Setup of Raspberry Pi Zero W

1. SSH into the Raspberry Pi (password: raspberry): ``` ssh pi@theipaddress ``` 2. Change the default password: ``` sudo passwd pi ``` 3. **[PREPARATION]** Update and upgrade: ``` sudo apt-get update sudo apt-get upgrade -y sudo apt-get dist-upgrade -y sudo rpi-update sudo reboot ``` 5. **[BLUETOOTH]** Install Bluetooth Firmware: ``` #install bluetooth drivers for Pi Zero W sudo apt-get install pi-bluetooth #verify that bluetooth is working sudo service bluetooth start sudo service bluetooth status ``` 6. **[OPTIONAL: BLUEZ UPGRADE]** Compile/install latest **bluez** This is not strictly required anymore, as **pi-bluetooth** appears to have been updated to support bluez 0.5.43. ``` #purge old bluez sudo apt-get --purge remove bluez #get latest version number from: https://www.kernel.org/pub/linux/bluetooth/ #current version as of this writing is 5.49 cd ~; wget https://www.kernel.org/pub/linux/bluetooth/bluez-5.49.tar.xz tar xvf bluez-5.49.tar.xz #update errythang again sudo apt-get update #install necessary packages sudo apt-get install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev #move into new unpacked directory cd bluez-5.49 #set exports export LDFLAGS=-lrt #configure ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-library -disable-systemd #make & install make sudo make install #cleanup cd ~ rm -r bluez-5.49/ rm bluez-5.49.tar.xz #update again sudo apt-get update sudo apt-get upgrade #verify bluez version bluetoothd -v ``` 7. **[REBOOT]** ``` sudo reboot ``` 8. **[INSTALL MOSQUITTO]** ``` # get repo key wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key #add repo sudo apt-key add mosquitto-repo.gpg.key #download appropriate lists file cd /etc/apt/sources.list.d/ sudo wget http://repo.mosquitto.org/debian/mosquitto-stretch.list #update caches and install apt-cache search mosquitto sudo apt-get update sudo aptitude install libmosquitto-dev mosquitto mosquitto-clients ``` 9. **[INSTALL PRESENCE]** ``` #install git cd ~ sudo apt-get install git #clone this repo git clone git://github.com/andrewjfreyer/presence #enter presence directory cd presence/ ``` 10. **[CONFIGURE PRESENCE]** create file named **mqtt_preferences** and include content: ``` nano mqtt_preferences ``` Then... ``` mqtt_address="ip.address.of.broker" mqtt_port="optional broker network port number. Defaults to 1883" mqtt_user="your broker username" mqtt_password="your broker password" mqtt_topicpath="location" mqtt_room="your pi's location" ``` 11. **[CONFIGURE PRESENCE]** create file named **owner_devices** and include mac addresses of devices on separate lines. ``` nano owner_devices ``` Then... ``` 00:00:00:00:00 #comments 00:00:00:00:00 ``` 12. **[CONFIGURE SERVICE]** Create file at **/etc/systemd/system/presence.service** and include content: ``` sudo nano /etc/systemd/system/presence.service ``` Then... ``` [Unit] Description=Presence service [Service] User=root ExecStart=/bin/bash /home/pi/presence/presence.sh & WorkingDirectory=/home/pi/presence Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ``` 13. **[CONFIGURE SERVICE]** Enable service by: ``` sudo systemctl enable presence.service sudo systemctl start presence.service ``` That's it. Your broker should be receiving messages and the presence service will restart each time the Raspberry Pi boots. ================================================ FILE: presence.sh ================================================ #!/bin/bash # ---------------------------------------------------------------------------------------- # GENERAL INFORMATION # ---------------------------------------------------------------------------------------- # # Written by Andrew J Freyer # GNU General Public License # http://github.com/andrewjfreyer/presence # # ---------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------- # INCLUDES & VARIABLES # ---------------------------------------------------------------------------------------- #VERSION NUMBER VERSION=0.5.1 #COLOR OUTPUT FOR RICH DEBUG ORANGE='\033[0;33m' RED='\033[0;31m' NC='\033[0m' GREEN='\033[0;32m' PURPLE='\033[1;35m' #BASE DIRECTORY REGARDLESS OF INSTALLATION; ELSE MANUALLY SET HERE base_directory=$(dirname "$(readlink -f "$0")") #FIND MQTT PATH, ELSE MANUALLY SET HERE mosquitto_pub_path=$(which mosquitto_pub) mosquitto_sub_path=$(which mosquitto_sub) #ERROR CHECKING FOR MOSQUITTO PUBLICATION [ -z "$mosquitto_pub_path" ] && echo "Required package 'mosquitto_pub' not found. Please install." && exit 1 [ -z "$mosquitto_sub_path" ] && echo "Required package 'mosquitto_sub' not found. Please install." && exit 1 # ---------------------------------------------------------------------------------------- # LOAD PREFERENCES # ---------------------------------------------------------------------------------------- #OR LOAD FROM A SOURCE FILE if [ ! -f "$base_directory/behavior_preferences" ]; then echo -e "${GREEN}presence $VERSION ${RED}WARNING: ${NC}Behavior preferences are not defined:${NC}" echo -e "/behavior_preferences. Creating file and setting default values.${NC}" echo -e "" #DEFAULT VALUES echo " #DELAY BETWEEN SCANS OF OWNER DEVICES WHEN AWAY FROM HOME delay_between_owner_scans_away=6 #DELAY BETWEEN SCANS OF OWNER DEVICES WHEN HOME delay_between_owner_scans_present=30 #HOW MANY VERIFICATIONS ARE REQUIRED TO DETERMINE A DEVICE IS AWAY verification_of_away_loop_size=6 #HOW LONG TO DELAY BETWEEN VERIFICATIONS THAT A DEVICE IS AWAY verification_of_away_loop_delay=3 #PREFERRED HCI DEVICE hci_device='hci0'" > "$base_directory/behavior_preferences" fi # ---------------------------------------------------------------------------------------- # VARIABLE DEFINITIONS # ---------------------------------------------------------------------------------------- #SET PREFERENCES FROM FILE DELAY_CONFIG="$base_directory/behavior_preferences" ; [ -f $DELAY_CONFIG ] && source $DELAY_CONFIG #LOAD DEFAULT VALUES IF NOT PRESENT [ -z "$hci_device" ] && hci_device='hci0' [ -z "$name_scan_timeout" ] && name_scan_timeout=5 [ -z "$delay_between_owner_scans_away" ] && delay_between_owner_scans_away=6 [ -z "$delay_between_owner_scans_present" ] && delay_between_owner_scans_present=30 [ -z "$verification_of_away_loop_size" ] && verification_of_away_loop_size=6 [ -z "$verification_of_away_loop_delay" ] && verification_of_away_loop_delay=3 #LOAD PREFERENCES IF PRESENT MQTT_CONFIG=$base_directory/mqtt_preferences ; [ -f $MQTT_CONFIG ] && source $MQTT_CONFIG [ ! -f "$MQTT_CONFIG" ] && echo "warning: please configure mqtt preferences file. exiting." && echo "" > "$MQTT_CONFIG" && exit 1 #FILL ADDRESS ARRAY WITH SUPPORT FOR COMMENTS [ ! -f "$base_directory/owner_devices" ] && "" > "$base_directory/owner_devices" macaddress_owners=($(cat "$base_directory/owner_devices" | grep -oiE "([0-9a-f]{2}:){5}[0-9a-f]{2}" )) [ -z "$macaddress_owners" ] && echo "warning: no owner devices are specified. exiting." && exit 1 #NUMBER OF CLIENTS THAT ARE MONITORED number_of_owners=$((${#macaddress_owners[@]})) # ---------------------------------------------------------------------------------------- # HELP TEXT # ---------------------------------------------------------------------------------------- show_help_text() { echo "Usage:" echo " presence -h show usage information" echo " presence -d print debug messages and mqtt messages" echo " presence -b binary output only; either 100 or 0 confidence" echo " presence -c only post confidence status changes for owners/guests" echo " presence -V print version" } # ---------------------------------------------------------------------------------------- # PROCESS OPTIONS (technique: https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash) # ---------------------------------------------------------------------------------------- OPTIND=1 # INITIALIZE OUR OWN VARIABLES: debug=0 binary_only=0 changes_only=0 while getopts "h?Vdbct:" opt; do case "$opt" in h|\?) show_help_text exit 0 ;; V) echo "$VERSION" exit 0 ;; d) debug=1 ;; b) binary_only=1 ;; c) changes_only=1 ;; *) echo "warning: unknown or depreciated option: $opt" esac done #RESET OPTION INDEX shift $((OPTIND-1)) #SHIFT IF NECESSARY [ "$1" = "--" ] && shift # ---------------------------------------------------------------------------------------- # DEBUG FUNCTION # ---------------------------------------------------------------------------------------- debug_echo () { if [ "$debug" == "1" ]; then (>&2 echo -e "${ORANGE}DEBUG MSG: $1${NC}") fi } # ---------------------------------------------------------------------------------------- # SCAN # ---------------------------------------------------------------------------------------- scan () { if [ ! -z "$1" ]; then local result=$(hcitool -i $hci_device name "$1" 2>&1 | grep -v 'not available' | grep -vE "hcitool|timeout|invalid|error" ) debug_echo "Scan result: [$result]" echo "$result" fi } # ---------------------------------------------------------------------------------------- # PUBLISH MESSAGE # ---------------------------------------------------------------------------------------- publish () { if [ ! -z "$1" ]; then #SET NAME FOR 'UNKONWN' local name="$3" #IF NO NAME, RETURN "UNKNOWN" if [ -z "$3" ]; then name="Unknown" fi #TIMESTAMP stamp=$(date "+%a %b %d %Y %H:%M:%S GMT%z (%Z)") #DEBUGGING [ "$debug" == "1" ] && (>&2 echo -e "${PURPLE}$mqtt_topicpath$1 { confidence : $2, name : $name, scan_duration_ms: $4, timestamp : $stamp} ${NC}") #POST TO MQTT $mosquitto_pub_path -h "$mqtt_address" -p "${mqtt_port:=1883}" -u "$mqtt_user" -P "$mqtt_password" -t "$mqtt_topicpath$1" -m "{\"confidence\":\"$2\",\"name\":\"$name\",\"scan_duration_ms\":\"$4\",\"timestamp\":\"$stamp\"}" fi } # ---------------------------------------------------------------------------------------- # MAIN LOOP # ---------------------------------------------------------------------------------------- device_statuses=() #STORES STATUS FOR EACH BLUETOOTH DEVICES device_names=() #STORES DEVICE NAMES FOR BOTH BEACONS AND BLUETOOTH DEVICES one_owner_home=0 #FLAG FOR AT LEAST ONE OWNER BEING HOME # ---------------------------------------------------------------------------------------- # START THE OPERATIONAL LOOP # ---------------------------------------------------------------------------------------- #MAIN LOOP while true; do #RESET AT LEAST ONE DEVICE HOME one_owner_home=0 #-------------------------------------- # UPDATE STATUS OF ALL USERS WITH NAME QUERY #-------------------------------------- for ((index=0; index<${#macaddress_owners[*]}; index++)); do #CLEAR PER-LOOP VARIABLES name_scan_result="" name_scan_result_verify="" ok_to_publish=1 #OBTAIN INDIVIDUAL ADDRESS current_device_address="${macaddress_owners[$index]}" #CHECK FOR ADDITIONAL BLANK LINES IN ADDRESS FILE if [ -z "$current_device_address" ]; then continue fi #MARK BEGINNING OF SCAN OPERATION start_timer=$(date +%s%N) #OBTAIN RESULTS AND APPEND EACH TO THE SAME name_scan_result=$(scan $current_device_address) #MARK END OF SCAN OPERATION end_time=$(date +%s%N) #CALCULATE DIFFERENCE duration_timer=$(( (end_time - start_timer) / 1000000 )) #THIS DEVICE NAME IS PRESENT if [ "$name_scan_result" != "" ]; then #STATE IS SAME && ONLY REPORT CHANGES THEN DISABLE PUBLICATION [ "${device_statuses[$index]}" == '100' ] && [ "$changes_only" == 1 ] && ok_to_publish=0 #NO DUPLICATE MESSAGES [ "$ok_to_publish" == "1" ] && publish "/$mqtt_room/$current_device_address" '100' "$name_scan_result" "$duration_timer" #USER STATUS device_statuses[$index]="100" #SET AT LEAST ONE DEVICE HOME one_owner_home=1 #SET NAME ARRAY device_names[$index]="$name_scan_result" else #USER STATUS status="${device_statuses[$index]}" if [ -z "$status" ]; then status="0" fi #BY DEFAULT, SET REPETITION TO PREFERENCE repetitions="$verification_of_away_loop_size" #IF WE ARE JUST STARTING OR, ALTERNATIVELY, WE HAVE RECORDED THE STATUS #OF NOT HOME ALREADY, ONLY SCAN ONE MORE TIME. if [ "$status" == 0 ];then repetitions=1 fi #SHOULD VERIFY ABSENSE for repetition in $(seq 1 $repetitions); do #RESET OK TO PUBLISH ok_to_publish=1 #VERIFICATION LOOP DELAY sleep "$verification_of_away_loop_delay" #GET PERCENTAGE percentage=$(($status * ( $repetitions - $repetition) / $repetitions)) #ONLY SCAN IF OUR STATUS IS NOT ALREADY 0 if [ "$status" != 0 ];then #MARK BEGINNING OF SCAN OPERATION start_timer=$(date +%s%N) #PERFORM SCAN name_scan_result_verify=$(scan $current_device_address) #MARK END OF SCAN OPERATION end_time=$(date +%s%N) #CALCULATE DIFFERENCE duration_timer=$(( (end_time - start_timer) / 1000000 )) #CHECK SCAN if [ "$name_scan_result_verify" != "" ]; then #STATE IS SAME && ONLY REPORT CHANGES THEN DISABLE PUBLICATION [ "${device_statuses[$index]}" == '100' ] && [ "$changes_only" == 1 ] && ok_to_publish=0 #PUBLISH [ "$ok_to_publish" == "1" ] && publish "/$mqtt_room/$current_device_address" '100' "$name_scan_result_verify" "$duration_timer" #SET AT LEAST ONE DEVICE HOME one_owner_home=1 #WE KNOW THAT WE MUST HAVE BEEN AT A PREVIOUSLY-SEEN USER STATUS device_statuses[$index]="100" #UPDATE NAME ARRAY device_names[$index]="$name_scan_result_verify" #MUST BREAK CONFIDENCE SCANNING LOOP; 100' ISCOVERED break fi fi #RETREIVE LAST-KNOWN NAME FOR PUBLICATION; SINCE WE OBVIOUSLY DIDN'T RECEIVE A NAME SCAN RESULT expectedName="${device_names[$index]}" if [ "$percentage" == "0" ]; then #STATE IS SAME && ONLY REPORT CHANGES THEN DISABLE PUBLICATION [ "${device_statuses[$index]}" == '0' ] && [ "$changes_only" == 1 ] && ok_to_publish=0 #PRINT ZERO CONFIDENCE OF A DEVICE AT HOME [ "$ok_to_publish" == "1" ] && publish "/$mqtt_room/$current_device_address" "0" "$expectedName" "$duration_timer" else #STATE IS SAME && ONLY REPORT CHANGES THEN DISABLE PUBLICATION [ "${device_statuses[$index]}" == '$percentage' ] && [ "$changes_only" == 1 ] && ok_to_publish=0 #IF BINARY ONLY, THEN DISABLE PUBLICATION [ "$binary_only" == "1" ] && ok_to_publish=0 #REPORT CONFIDENCE DROP [ "$ok_to_publish" == "1" ] && publish "/$mqtt_room/$current_device_address" "$percentage" "$expectedName" "$duration_timer" fi #UPDATE STATUS ARRAY device_statuses[$index]="$percentage" done fi done #CHECK STATUS ARRAY FOR ANY DEVICE MARKED AS 'HOME' wait_duration=0 #DETERMINE APPROPRIATE DELAY if [ "$one_owner_home" == 1 ]; then wait_duration=$delay_between_owner_scans_present else wait_duration=$delay_between_owner_scans_away fi sleep "$wait_duration" done