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.
<h1>Summary</h1>
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)}
___
<h1>Example Use with Home Assistant</h1>
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
```
___
<h1>Installation Instructions (Raspbian Jessie Lite Stretch):</h1>
<h2>Setup of SD Card</h2>
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
```
<h2>Configuration and Setup of Raspberry Pi Zero W</h2>
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
gitextract_zicas_p1/ ├── README.md └── presence.sh
Condensed preview — 2 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (22K chars).
[
{
"path": "README.md",
"chars": 9153,
"preview": "presence\n=======\n\n***Note: as of `presence` 0.5.1, triggered scans, guest scanning, and beacon scanning are removed from"
},
{
"path": "presence.sh",
"chars": 11734,
"preview": "\n#!/bin/bash\n\n# ----------------------------------------------------------------------------------------\n# GENERAL INFOR"
}
]
About this extraction
This page contains the full source code of the andrewjfreyer/presence GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 2 files (20.4 KB), approximately 5.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.