From 50a26218666b33930a0e68dc4baeff17e764d4df Mon Sep 17 00:00:00 2001 From: Jb Date: Tue, 23 Dec 2025 19:15:40 -0500 Subject: [PATCH 1/6] fixing mqtt env var error --- entrypoint.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 entrypoint.sh diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..4560fc5 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +PREF_FILE="/opt/monitor/mqtt_preferences" + +if [ ! -f "$PREF_FILE" ]; then + echo "[entrypoint] Generating mqtt_preferences from env" + + cat > "$PREF_FILE" < Date: Tue, 23 Dec 2025 22:22:38 -0500 Subject: [PATCH 2/6] adding kaniki workflow --- .github/workflows/kaniko-dev-build.yml | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/kaniko-dev-build.yml diff --git a/.github/workflows/kaniko-dev-build.yml b/.github/workflows/kaniko-dev-build.yml new file mode 100644 index 0000000..11ad797 --- /dev/null +++ b/.github/workflows/kaniko-dev-build.yml @@ -0,0 +1,55 @@ +name: Kaniko Build (Dev → GHCR) + +on: + push: + branches: ["dev"] + workflow_dispatch: {} + +permissions: + contents: read + packages: write # required to push to GHCR + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} # owner/repo + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Compute tags + id: meta + shell: bash + run: | + SHORT_SHA="${GITHUB_SHA:0:7}" + echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" + echo "image=${REGISTRY}/${IMAGE_NAME}" >> "$GITHUB_OUTPUT" + + - name: Write Docker auth for GHCR + shell: bash + run: | + mkdir -p .docker + AUTH="$(printf "%s:%s" "${{ github.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 -w0)" + cat > .docker/config.json < + --context=dir:///github/workspace + --dockerfile=Dockerfile + --docker-config=/github/workspace/.docker + --destination=${{ steps.meta.outputs.image }}:dev-${{ steps.meta.outputs.short_sha }} + --destination=${{ steps.meta.outputs.image }}:dev-latest + --cache=true + --cache-ttl=48h From 2d31f5c61248b7b11fbda19b2ef8fb1c1f1f78c9 Mon Sep 17 00:00:00 2001 From: Jb Date: Tue, 23 Dec 2025 22:48:51 -0500 Subject: [PATCH 3/6] adding makefile and grc push --- .dockerignore | 53 ++++++++++++++ Makefile | 173 ++++++++++++++++++++++++++++++++++++++++++++++ docker/Dockerfile | 14 ++++ 3 files changed, 240 insertions(+) create mode 100644 .dockerignore create mode 100644 Makefile create mode 100644 docker/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d1048b9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,53 @@ +# ============================ +# VCS +# ============================ +.git +.gitignore +.github + +# ============================ +# CI / build artifacts +# ============================ +docker-compose*.yml +*.log +*.pid +*.sock + +# ============================ +# Editor / OS junk +# ============================ +.DS_Store +Thumbs.db +.idea +.vscode + +# ============================ +# Python (future-proofing) +# ============================ +__pycache__/ +*.pyc +*.pyo +*.pyd +.venv/ +venv/ + +# ============================ +# Node / frontend (just in case) +# ============================ +node_modules/ + +# ============================ +# Secrets / runtime state +# ============================ +.env +.env.* +mqtt_preferences +secrets/ +config/ + +# ============================ +# Docs & misc +# ============================ +docs/ +README.md +CHANGELOG.md diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f26f4a7 --- /dev/null +++ b/Makefile @@ -0,0 +1,173 @@ +# ============================================================ +# BLE Monitor — Docker Build Makefile +# ============================================================ + +# ---- Config ------------------------------------------------ +REGISTRY ?= ghcr.io + +# Remote URL (may be https://github.com/OWNER/REPO(.git) or git@github.com:OWNER/REPO(.git)) +GIT_REMOTE_URL := $(shell git config --get remote.origin.url 2>/dev/null) + +# Parse OWNER +OWNER_FROM_HTTPS := $(shell echo "$(GIT_REMOTE_URL)" | cut -d/ -f4) +OWNER_FROM_SSH := $(shell echo "$(GIT_REMOTE_URL)" | cut -d: -f2 | cut -d/ -f1) + +# Parse REPO +REPO_FROM_HTTPS := $(shell echo "$(GIT_REMOTE_URL)" | cut -d/ -f5 | sed 's/\.git$$//') +REPO_FROM_SSH := $(shell echo "$(GIT_REMOTE_URL)" | cut -d/ -f2 | sed 's/\.git$$//') + +# Allow manual override, otherwise detect +OWNER ?= $(if $(OWNER_FROM_HTTPS),$(OWNER_FROM_HTTPS),$(OWNER_FROM_SSH)) +REPO ?= $(if $(REPO_FROM_HTTPS),$(REPO_FROM_HTTPS),$(REPO_FROM_SSH)) + +# Final fallbacks +REPO := $(if $(REPO),$(REPO),$(shell basename $(CURDIR))) +OWNER := $(if $(OWNER),$(OWNER),unknown) + +IMAGE := $(REGISTRY)/$(OWNER)/$(REPO) + +SOURCE_URL ?= $(shell \ + git config --get remote.origin.url 2>/dev/null | \ + sed -e 's|^git@github.com:|https://github.com/|' -e 's|\.git$$||' \ +) + +BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") + + +GIT_SHA := $(shell git rev-parse --short HEAD) +TAG_DEV := dev-$(GIT_SHA) +TAG_LATEST := dev-latest + +DOCKER ?= docker + +# ---- Paths ------------------------------------------------ +DOCKERFILE ?= docker/Dockerfile +CONTEXT ?= . + + +# ---- Runtime ------------------------------------------------ +CONTAINER_NAME ?= ble_monitor + +# Host paths +LOCALTIME_MOUNT ?= /etc/localtime:/etc/localtime:ro +DBUS_MOUNT ?= /run/dbus:/run/dbus:ro + +# Repo-relative path to preferences (override if different) +MQTT_PREFS_PATH ?= ./ble-monitor/mqtt_preferences +MQTT_PREFS_MOUNT ?= $(MQTT_PREFS_PATH):/opt/monitor/mqtt_preferences:ro + +# Env (override at runtime: `MQTT_ADDRESS=... make run`) +NODE_NAME ?= $(shell hostname) +MQTT_ADDRESS ?= 127.0.0.1 +MQTT_PORT ?= 1883 +MQTT_USERNAME ?= +MQTT_PASSWORD ?= +MQTT_TOPIC_PREFIX ?= presence/ble/raw + +.PHONY: run stop logs sh ps rm + +run: + $(DOCKER) run -d --rm \ + --name $(CONTAINER_NAME) \ + --network host \ + --privileged \ + -v $(LOCALTIME_MOUNT) \ + -v $(DBUS_MOUNT) \ + -v $(MQTT_PREFS_MOUNT) \ + -e MQTT_ADDRESS="$(MQTT_ADDRESS)" \ + -e MQTT_PORT="$(MQTT_PORT)" \ + -e MQTT_USERNAME="$(MQTT_USERNAME)" \ + -e MQTT_PASSWORD="$(MQTT_PASSWORD)" \ + -e MQTT_PUBLISHER_IDENTITY="$(NODE_NAME)" \ + -e MQTT_TOPIC_PREFIX="$(MQTT_TOPIC_PREFIX)" \ + $(IMAGE):$(TAG_LATEST) + +stop: + -$(DOCKER) stop $(CONTAINER_NAME) 2>/dev/null || true + +logs: + $(DOCKER) logs -f $(CONTAINER_NAME) + +sh: + $(DOCKER) exec -it $(CONTAINER_NAME) sh + +ps: + $(DOCKER) ps --filter "name=$(CONTAINER_NAME)" + +rm: + -$(DOCKER) rm -f $(CONTAINER_NAME) 2>/dev/null || true + +# ---- Targets ---------------------------------------------- + +.PHONY: help build build-dev build-latest build-all tag push login clean + +help: + @echo "" + @echo "BLE Monitor — Docker Build" + @echo "" + @echo "Targets:" + @echo " build Build image with dev- tag" + @echo " build-latest Build image with dev-latest tag" + @echo " build-all Build both dev- and dev-latest" + @echo " tag Tag dev- as dev-latest" + @echo " push Push both tags to registry" + @echo " login Docker login to GHCR" + @echo " clean Remove local images" + @echo "" + +.PHONY: info +info: + @echo "IMAGE=$(IMAGE)" + @echo "TAG_DEV=$(TAG_DEV)" + @echo "TAG_LATEST=$(TAG_LATEST)" + @echo "SOURCE_URL=$(SOURCE_URL)" + +# ---- Build ------------------------------------------------ + +build: + $(DOCKER) build \ + -f $(DOCKERFILE) \ + -t $(IMAGE):$(TAG_DEV) \ + --label org.opencontainers.image.title="$(REPO)" \ + --label org.opencontainers.image.source="$(SOURCE_URL)" \ + --label org.opencontainers.image.revision="$(GIT_SHA)" \ + --label org.opencontainers.image.created="$(BUILD_DATE)" \ + $(CONTEXT) + +build-latest: + $(DOCKER) build \ + -f $(DOCKERFILE) \ + -t $(IMAGE):$(TAG_LATEST) \ + --label org.opencontainers.image.title="$(REPO)" \ + --label org.opencontainers.image.source="$(SOURCE_URL)" \ + --label org.opencontainers.image.revision="$(GIT_SHA)" \ + --label org.opencontainers.image.created="$(BUILD_DATE)" \ + $(CONTEXT) + + + +build-all: build build-latest + +# ---- Tagging ---------------------------------------------- + +tag: + $(DOCKER) tag \ + $(IMAGE):$(TAG_DEV) \ + $(IMAGE):$(TAG_LATEST) + +# ---- Push ------------------------------------------------- + +push: + $(DOCKER) push $(IMAGE):$(TAG_DEV) + $(DOCKER) push $(IMAGE):$(TAG_LATEST) + +# ---- Auth ------------------------------------------------- + +login: + @echo "Logging in to GHCR..." + @echo $$GITHUB_TOKEN | $(DOCKER) login ghcr.io -u $$GITHUB_USER --password-stdin + +# ---- Cleanup ---------------------------------------------- + +clean: + -$(DOCKER) rmi $(IMAGE):$(TAG_DEV) $(IMAGE):$(TAG_LATEST) 2>/dev/null || true diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..05b3ec2 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,14 @@ +FROM debian:12-slim +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bluetooth bluez bluez-hcidump bc curl procps mosquitto-clients ca-certificates tini \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/monitor +COPY . /opt/monitor + +RUN chmod +x /opt/monitor/monitor.sh + +ENTRYPOINT ["/usr/bin/tini","--"] +CMD ["./monitor.sh"] From 36b7d7da4dd49fc3a0a9af1897ae2f68efbfb1de Mon Sep 17 00:00:00 2001 From: Jb Date: Tue, 23 Dec 2025 22:52:38 -0500 Subject: [PATCH 4/6] Container-first refactor: Makefile, GHCR build, OCI labels, updated README --- README.md | 595 ++++++++++++------------------------------------------ 1 file changed, 133 insertions(+), 462 deletions(-) diff --git a/README.md b/README.md index 468ad85..a4d9cc9 100644 --- a/README.md +++ b/README.md @@ -1,550 +1,221 @@ -`monitor` -======= -***TL;DR***: Passive Bluetooth presence detection of beacons, cell phones, and other Bluetooth devices. Useful for [mqtt-based](http://mqtt.org) home automation, especially when the script runs on multiple devices, distributed throughout a property. +# BLE Monitor (Containerized) -![version](https://img.shields.io/badge/version-0.2-green.svg?maxAge=2592000) ![mosquitto](https://img.shields.io/badge/mosquitto-1.5+-blue.svg?maxAge=2592000) +**BLE Monitor** is a **container-first Bluetooth Low Energy (BLE) presence signal publisher**. -[**Frequently Asked Questions**](https://github.com/andrewjfreyer/monitor/blob/master/support/README.md) +It scans BLE advertisements and publishes **raw presence facts** to MQTT. +It does **not** perform automations, aggregation, or decision-making. -
Installation Instructions +This project is designed to be consumed by **Home Assistant** (or any other system) where higher-level logic such as Wi-Fi correlation, Bayesian sensors, and `person` entities belong. -
+--- -
Set Up Raspberry Pi From Scratch +## 🔑 Core Principles +### 1. BLE Monitor is stateless -# Installation Instructions for Raspberry Pi Zero W +* No Home Assistant logic +* No automations +* No zone or presence fusion +* BLE is a **signal**, not a decision -## Setup of SD Card +### 2. MQTT is the contract -1. Download latest version of **raspbian** [here](https://downloads.raspberrypi.org/raspbian_lite_latest) +* Stable topic structure +* Idempotent, retained messages +* HA restarts must not break state recovery -2. Download etcher from [etcher.io](https://etcher.io) +### 3. Container-first -3. Image **raspbian lite buster** to SD card. [Instructions here.](https://www.raspberrypi.org/magpi/pi-sd-etcher/) +* No systemd +* No interactive setup +* All configuration via env vars or mounted files +* Designed to run unattended long-term -4. Mount **boot** partition of imaged SD card (unplug it and plug it back in) +--- -5. **To enable ssh,** create blank file, without any extension, in the root directory called **ssh** +## 📡 MQTT Topic Contract -6. **To setup Wi-Fi**, create **wpa_supplicant.conf** file in root directory and add Wi-Fi details for home Wi-Fi: +BLE Monitor publishes **raw facts only**. -```bash -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. **On the 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. - -## Configuration and Setup - -1. SSH into the Raspberry Pi (default password: raspberry): -```bash -ssh pi@theipaddress -``` - -2. Change the default password: -```bash -sudo passwd pi -``` - -3. Update and upgrade: - -```bash -sudo apt-get update -sudo apt-get upgrade -y -sudo apt-get dist-upgrade -y -sudo reboot -``` - -5. Install Bluetooth Firmware, if necessary: -```bash -#install Bluetooth drivers for Pi Zero W -sudo apt-get install pi-bluetooth - -``` - -6. Reboot: -```bash -sudo reboot +presence/ble/raw//status online | offline +presence/ble/raw// home | not_home +presence/ble/raw///rssi ``` -7. Install Mosquitto 1.5+ **(important step!)**: -```bash +### ❌ BLE Monitor will NOT publish -# get repo key -wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key +* `person.*` +* `device_tracker.*` +* `binary_sensor.*` +* Zone logic or presence fusion -#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-buster.list +## 🧩 What BLE Monitor Does (and Does Not Do) -#update caches and install -sudo apt-cache search mosquitto -sudo apt-get update -sudo apt-get install -f libmosquitto-dev mosquitto mosquitto-clients libmosquitto1 -``` -
+### ✅ BLE Monitor -
Monitor Setup +* Scans BLE advertisements +* Tracks device proximity +* Publishes raw presence + RSSI to MQTT -## Setup `monitor` +### ❌ BLE Monitor does NOT -1. Clone `monitor` git: -```bash -#install git -cd ~ -sudo apt-get install git +* Decide “who is home” +* Handle Wi-Fi presence +* Define Home Assistant entities +* Trigger automations or notifications -#clone this repo -git clone https://github.com/andrewjfreyer/monitor.git +Those responsibilities belong to **Home Assistant** (or downstream consumers). -#enter `monitor` directory -cd monitor/ +--- -#(optional) switch to beta branch for latest updates and features (may be unstable) -git checkout beta +## 🐳 Container Runtime Model -``` +BLE Monitor runs as a **privileged container** using the **host Bluetooth stack (BlueZ + D-Bus)**. -2. Initial run: +Typical runtime requirements: -Configuration files will be created with default preferences. Any executables that are not installed will be reported. All can be installed via `apt-get install ...` +* Host Bluetooth hardware +* Access to `/run/dbus` +* Host networking +* MQTT broker reachable from the container -```bash -sudo bash monitor.sh -``` +--- +## 🚀 Quick Start (Local) -3. Edit **mqtt_preferences** file: +### Build the image ```bash -sudo nano mqtt_preferences +make build-latest ``` -4. Edit **known_static_addresses** (phones, laptops, some smart watches): +### Run the container ```bash -sudo nano known_static_addresses +make run ``` -Alternatively, send an mqtt message to `monitor/setup/ADD STATIC DEVICE` with a message including a mac address and an alias separated by a space: - - -**topic:** `monitor/setup/ADD STATIC DEVICE` -**message:** 00:11:22:33:44:55 alias - - -Use, `monitor/setup/DELETE STATIC DEVICE` with a message containing a mac address to remove a device from all `monitor` nodes. - -5. Read helpfile: +### View logs ```bash -sudo bash monitor.sh -h -``` - -Now the basic setup is complete. Your broker should be receiving messages and the `monitor` service will restart each time the Raspberry Pi boots. As currently configured, you should run `sudo bash monitor.sh` a few times from your command line to get a sense of how the script works. - -
- -___ - -
- -
Background & Technical Details - -# *Highlights* - -`monitor` sends a JSON-formatted MQTT message including a confidence value from 0 to 100 to a specified broker when a specified Bluetooth device responds to a `name` query. By default, `name` queries are triggered after receiving an anonymous advertisement from a previously-unseen device (e.g., a device in peripheral mode advertising an ability to connect). - -Example JSON package: +make logs ``` -topic: monitor/{{name of monitor install}}/{{mac address}} -message: { - "id":"{{mac address}}", - "confidence":"{{ranging from 0-100}}", - "name":"{{if available}}", - "manufacturer":{{if available}}", - "type":"KNOWN_MAC", - "retained":"{{message retained?}}", - "timestamp":"{{formatted date at which message is sent}}", - "version":"{{monitor version}}" - } -``` - -In addition, optionally, a JSON-formatted MQTT message can be reported to the same broker whenever a publicly-advertising beacon device or an iBeacon device advertises. - -Example JSON package: -``` -topic: monitor/{{name of monitor install}}/{{mac address or ibeacon uuid}} -message: { - "id":"{{mac address or ibeacon uuid}}", - "report_delay":"{{delay from first detection to this message in seconds}}", - "flags":"{{GAP flags}}", - "movement":"stationary", - "confidence":"{{ranging from 0-100}}", - "name":"{{if available}}", - "power":"{{if available}}", - "rssi":"{{if available}}", - "mac":"{{if ibeacon, the current mac address associated with the uuid}}", - "manufacturer":{{if available}}", - "type":"{{GENERIC_BEACON_PUBLIC or APPLE_IBEACON}}, - "retained":"{{message retained?}}", - "timestamp":"{{formatted date at which message is sent}}", - "version":"{{monitor version}}" - } - ``` -___ - -# *Oversimplified Analogy of the Bluetooth Presence Problem* - -Imagine you're blindfolded in a large room with other people. We want to find out who of your friends **is** present and who of your friends **isn't** present: - -![First Picture](https://i.imgur.com/FOubz6T.png) - -Some of the people in the room periodically make sounds (e.g., eating a chip, sneeze, cough, etc.), others sit quietly and don’t make a sound unless you specifically ask for them by name, and still others periodically announce their own name out loud at regular intervals whether or not you want them to do that: - -![Second Picture](https://i.imgur.com/UwPJIMM.png) - -Here's the problem. You can’t just shout “WHO’S HERE” because then everyone would say their name at the same time and you couldn’t tell anything apart. Similarly, for obvious reasons, you can't simply ask "WHO ISN'T HERE?" - -So, you take attendance like in a classroom. Everyone in the room responds **only** when their own name is shouted. - -![Third Picture](https://i.imgur.com/VCW8AmH.png) - -So, one way to take attendance is to shout for each friend on a list by name, one at a time, repeatedly. Ask for someone, get a response, wait for a moment, and ask again. - -Once a friend stops responding (for some period of time), you presume that he or she has left: - -![Simple Loop](https://i.imgur.com/ijGw2qb.png) - -This technique should work just fine, but there's a minor problem. You're constantly shouting into the room, which means that it's difficult for you to hear quiet responses and it's difficult for other people to carry on conversations. What else can we do? Can we use those random sounds for anything? - -Yes! A smarter approach is to wait for an anonymous sound, *then* start asking whether a friend *you know isn't present* has just arrived: - -![Complex Loop](https://i.imgur.com/9Ugn27i.png) - -This way, you're not constantly asking the room for all of your friends. Efficient! - -This technique is a very simplified description of how `monitor` works for devices like cell phones (friends on a list) and beacons (announce a name out loud). This also gives an idea of how `monitor` uses anonymous sounds to reduce the number of times that it has to send inquiries into the Bluetooth environment. - -___ - -# *Oversimplified Technical Background* - -The Bluetooth Low Energy spec was designed to make connecting Bluetooth devices simpler for the user. No more pin codes, no more code verifications, no more “discovery mode” - for the most part. It was also designed to be more private than previous Bluetooth implementations. That said, it’s hard to maintain privacy when you want to be able to connect to an unknown device without intervention. - -## Name Requests - -A part of the Blueooth spec is a special function called a `name` request that asks another Bluetooth device to send back a human-readable name of itself. In order to send a `name` request, however, we need to know a private (unchanging) address of the target device. - -Issuing a `name` request to the same private mac address every few seconds is a reliable - albeit rudimentary - way of detecting whether that device is "**present**" (it responds to the `name` request) or "**absent**" (no response to the `name` request is received). However, issuing `name` requests too frequently (*e.g.*, every few seconds) uses quite a bit of 2.4GHz spectrum, which can cause interference with Wi-Fi or other wireless communications. - -Not all devices respond to `name` requests, however. For example, beacon devices do not respond. - -## Connectible Devices - -Blueooth devices that can exchange information with other devices (almost always) advertise a random/anonymous address that other devices can use to negotiate a secure connection and receive the first device's real, private, Bluetooth address. Using a random address in this way when publicly advertising prevents bad actors from tracking via passive Bluetooth monitoring. - -## Beacon/Advertising Devices - -The Bluetooth spec has been used by Apple, Google, and others to create additional standards (e.g., iBeacon, Eddystone, and so on). These devices generally don't care to connect to other devices, so use of random/anonymous addresses doesn't really matter. Instead, these devices encode additional information into each advertisement of an anonymous address. For example, iBeacon devices will broadcast a UUID that conforms to the 8-4-4-4-12 format defined by [IETC RFC4122](http://www.ietf.org/rfc/rfc4122.txt). -As noted above, most beacons do not respond to `name` requests, even if made to the device's private Bluetooth address. So, issuing periodic `name` requests to beacons is not a good way to detect whether a beacon device is **present** or **absent**. However, monitoring for beacon advertisement is a reliable way to detect whether a beacon device is **present** or **absent**. - -_____ - -# *How `monitor` Works* - -This script combines `name` requests, anonymous advertisements, and beacon advertisements to logically determine (1) *when* to issue a `name` request to determine whether a device is **present** and (2) *when* to issue a `name` request to determine whether a device is **absent**. The script also listens for beacons. - -##### Known Static Addresses -`monitor` uses unchanging/static mac addresses for your devices that you have added to a file called `known_static_addresses`. These are the addresses for which `monitor` will issue `name` requests to determine whether or not these devices are **present** or **absent**. - -Once a determination of presence is made, the script posts to an mqtt topic path defined in a file called `mqtt_preferences` that includes a JSON-formatted message with a confidence value that corresponds to a confidence of presence. For example, a confidence of 100 means that `monitor` is 100% sure the device is present. Similarly, a confidence of 0 means that `monitor` is 0% sure the device is present (*i.e.*, the `monitor` is 100% sure the device is absent). - -To minimize the number of times that `monitor` issues `name` requests (thereby reducing 2.4GHz interference), the script performs either an ***ARRIVAL*** scan or a ***DEPART*** scan, instead of scanning all devices listed in the `known_static_addresses` each time. - -More specifically: - -* An ***ARRIVAL*** scan issues a `name` request, sequentially, for each device listed in the `known_static_addresses` file that is known to be **absent**. - -* Similarly, a ***DEPART*** scan issues a `name` request, sequentially, for each device listed in the `known_static_addresses` file that is known to be **present**. - -For example, if there are two phone addresses listed in the `known_static_addresses` file, and both of those devices are **present**, an ***ARRIVAL*** scan will never occur. Similarly, if both of these addresses are **absent** then a ***DEPART*** scan will never occur. If only one device is present, an **ARRIVAL** scan will only scan for the device that is currently away. - -To reduce the number of `name` requests that occur, `monitor` listens for anonymous advertisements and triggers an ***ARRIVAL*** scan for every *new* anonymous address. - -The script will also trigger an ***ARRIVE*** scan in response to an mqtt message posted to the topic of `monitor/scan/arrive`. Advertisement-triggered scanning can be disabled by using the trigger argument if `-ta`, which causes `monitor` to *only* trigger ***ARRIVAL*** scans in response to mqtt messages. - -If `monitor` has not heard from a particular anonymous address in a long time, `monitor` triggers a ***DEPART*** scan. The script will also trigger a ***DEPART*** scan in response to an mqtt message posted to the topic of `monitor/scan/depart`. Expiration-triggered scanning can be disabled by using the trigger argument if `-td`, which causes `monitor` to *only* trigger ***DEPART*** scans in response to mqtt messages. - -To reduce scanning even further, `monitor` can filter which types of anonymous advertisements are used for ***ARRIVE*** scans. These are called "filters" and are defined in a file called `behavior_preferences`. The filters are bash RegEx strings that either pass or reject anonymous advertisements that match the filter. - -There are two filter types: - -* **Manufacturer Filter** - filters based on data in an advertisement that is connected to a particular device manufacturer. This is almost always the OEM of the device that is transmitting the anonymous advertisement. By default, because of the prevalence of iPhones, Apple is the only manufacturer that triggers an ***ARRIVAL*** scan. Multiple manufacturers can be appended together by a pipe: `|`. An example filter for Apple and Samsung looks like: `Apple|Samsung`. To disable the manufacturer filter, use `.*`. - -* **Flag Filter:** filters based on flags contained in an advertisement. This varies by device type. By default, because of the prevalence of iPhones, the flag of `0x1b` triggers an ***ARRIVAL*** scan. Like with the manufacturer filter, multiple flags can be appended together by a pipe: `|`. To disable the manufacturer filter, use `.*`. - -##### Beacons & iBeacons -In addition, when run with the `-b` beacon argument, `monitor` listens for beacon advertisements that report themselves as "public", meaning that their addresses will not change. The script can track these by default; these addresses do not have to be added anywhere - after all, `monitor` will obtain them just by listening. - -Since iBeacons include a UUID and a mac address, two presence messages are reported via mqtt. - -## Known Beacon Addresses -In some cases, manufacturers try to get sneaky and cause their beacons to advertise as "anonymous" (or "random") devices, despite that their addresses do not change at all. By default, `monitor` does not report presence of anonymous advertisement devices, so to force `monitor` to recognize these devices, we add the "random" address to a file called `known_static_beacons`. After restarting, `monitor` will know that these addresses should be treated like a normal beacon. -___ - -
- -
Home Assistant Example - -# Example with Home Assistant - -Personally, I have four **raspberry pi zero w**s throughout the house and garage. My family spends most of our time on the first floor, so our main `monitor` node or sensor is on the first floor. Our other 'nodes' on the second and third floor and garage are set up for triggered use only - these will scan for ***ARRIVAL*** and ***DEPART*** only in response to mqtt messages, with option ```-tad```. The first floor node is set up to send mqtt arrive/depart scan instructions to these nodes by including the `-tr` flag ("report" to other nodes when an arrival or depart scan is triggered). - -The first floor constantly monitors for beacons (`-b`) advertisements and anonymous advertisements, which may be sent by our phones listed in the `known_static_addresses` file. In response to a new anonymous advertisement, `monitor` will initiate an ***ARRIVAL*** scan for whichever of our phones is not present. If one of those devices is seen, an mqtt message is sent to Home Assistant reporting that the scanned phone is "home" with a confidence of 100%. In addition, an mqtt message is sent to the second and third floor and garage to trigger a scan on those floors as well. As a result of this configuration, when we leave the house, we use either the front door or the garage door to trigger an mqtt trigger of ```monitor/scan/depart``` after a ten second delay to trigger a departure scan of our devices that were previously known to be present. The ten second delay gives us a chance to get out of Bluetooth range before a "departure" scan is triggered. Different houses/apartments will probably need different delays. - -More specifically, each of these `monitor` nodes uses the same name for each device so that states can be tracked easily by Home Assistant. For example, on each node, my `known_static_addresses` file looks like this (note that 00:00:00:00:00:00 is an example address - this should be your phone's private, static, Bluetooth address): +### Stop it ```bash -00:00:00:00:00:00 alias #comment that is ignored +make stop ``` -The address I want to track is separated by a space from the *alias* that I want to use to refer to this device in Home Assistant. If you prefer to use the address instead of an alias, set the value `PREF_ALIAS_MODE=false` in your `behavior_preferences` file. +--- -In this manner, [Home Assistant](https://www.home-assistant.io) receives mqtt messages and stores the values as input to a number of [mqtt sensors](https://www.home-assistant.io/components/sensor.mqtt/). Output from these sensors is combined to give an accurate numerical occupancy confidence: +## 🔧 Configuration -``` -- platform: mqtt - state_topic: 'monitor/first floor/alias' - value_template: '{{ value_json.confidence }}' - unit_of_measurement: '%' - name: 'First Floor' - -- platform: mqtt - state_topic: 'monitor/second floor/alias' - value_template: '{{ value_json.confidence }}' - unit_of_measurement: '%' - name: 'Second Floor' - -- platform: mqtt - state_topic: 'monitor/third floor/alias' - value_template: '{{ value_json.confidence }}' - unit_of_measurement: '%' - name: 'Third Floor' - -- platform: mqtt - state_topic: 'monitor/garage/alias' - value_template: '{{ value_json.confidence }}' - unit_of_measurement: '%' - name: 'Garage' -``` +### Environment Variables -These sensors can be combined using a [min_max](https://www.home-assistant.io/components/sensor.min_max/): - -``` -- platform: min_max - name: "Home Occupancy Confidence" - type: max - round_digits: 0 - entity_ids: - - sensor.third_floor - - sensor.second_floor - - sensor.first_floor - - sensor.garage -``` +| Variable | Description | Default | +| ------------------------- | -------------------- | ------------------ | +| `MQTT_ADDRESS` | MQTT broker host | `127.0.0.1` | +| `MQTT_PORT` | MQTT broker port | `1883` | +| `MQTT_USERNAME` | MQTT username | empty | +| `MQTT_PASSWORD` | MQTT password | empty | +| `MQTT_PUBLISHER_IDENTITY` | Node name (``) | hostname | +| `MQTT_TOPIC_PREFIX` | Topic prefix | `presence/ble/raw` | -Thereafter, I use the entity **sensor.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. +### MQTT Preferences File -As an example: +The container expects a preferences file mounted at: ``` -- alias: Occupancy On - trigger: - - platform: numeric_state - entity_id: sensor.home_occupancy_confidence - above: 10 - action: - - service: input_boolean.turn_on - data: - entity_id: input_boolean.occupancy - -- alias: Occupancy Off - trigger: - - platform: numeric_state - entity_id: sensor.home_occupancy_confidence - below: 10 - action: - - service: input_boolean.turn_off - data: - entity_id: input_boolean.occupancy +/opt/monitor/mqtt_preferences ``` -If you prefer to use the `device_tracker` platform in Home Assistant, a unique solution is to use the undocumented `device_tracker.see` service: - -As an example: +This file is **not baked into the image** and should be mounted at runtime. -``` -- alias: Andrew Occupancy On - trigger: - - platform: numeric_state - entity_id: sensor.andrew_occupancy_confidence - above: 10 - action: - - service: device_tracker.see - data: - dev_id: andrew - location_name: home - source_type: bluetooth - -- alias: Andrew Occupancy Off - trigger: - - platform: numeric_state - entity_id: sensor.andrew_occupancy_confidence - below: 10 - action: - - service: device_tracker.see - data: - dev_id: andrew - location_name: not_home - source_type: bluetooth +--- -``` +## 🛠 Makefile Targets -For more information, see [here](https://community.home-assistant.io/t/device-tracker-from-script/97295/7) and [here](https://github.com/andrewjfreyer/monitor/issues/138). +| Target | Description | +| ------------------- | ------------------------------------ | +| `make build` | Build image tagged `dev-` | +| `make build-latest` | Build image tagged `dev-latest` | +| `make build-all` | Build both tags | +| `make run` | Run container (host net, privileged) | +| `make stop` | Stop container | +| `make logs` | Tail logs | +| `make push` | Push `dev-latest` to GHCR | +| `make publish` | Push both tags | +| `make info` | Show image metadata | -If you only have one node, an [add-on](https://github.com/Limych/hassio-addons) by @limych may be an excellent choice for you! +--- -If having several nodes and lots of users, there is an AppDaemon App that can easily manage the integration of of this system into Home Assistant automatically for you by @Odianosen25. More information can be found [here](https://github.com/Odianosen25/Monitor-App) +## 📦 Image Registry -
- -
Advanced Configuration Options & Fine Tuning - - -## Fine Tuning - - -1. Observe output from `monitor` to tune filters: - -```bash -sudo bash monitor.sh -``` - -Observe the output of the script for debug log [CMD-RAND] lines including [failed filter] or [passed filter]. These lines show what anonymous advertisement `monitor` sees and how `monitor` filters those advertisements. In particular, cycle the Bluetooth power on your phone or another device and look at the `flags` value, the `pdu` value, and the `man` (manufacturer) value that appears after you turn Bluetooth power back on. Remember, the address you see in the log will be an anonymous address - ignore it, we're only focused on the values referenced above. +Images are published to **GitHub Container Registry (GHCR)**: ``` -0.1.xxx 03:25:39 pm [CMD-RAND] [passed filter] data: 00:11:22:33:44:55 pdu: ADV_NONCONN_IND rssi: -73 dBm flags: 0x1b man: Apple, Inc. delay: 4 +ghcr.io/bageera/ble-monitor ``` -If you repeatedly see the same values in one or more of these fields, consider adding a PASS filter condition to the `behavior_preferences` file. This will cause `monitor` to *only* scan in response to an anonymous advertisement that passes the filter condition that you define. For example, if you notice that Apple always shows up as the manufacturer when you cycle the power on you phone, you can create an Apple filter: +Tags: -```bash -PREF_PASS_FILTER_MANUFACTURER_ARRIVE="Apple" -``` +* `dev-` +* `dev-latest` -If you have two phones, and one is **Apple** and the other is **Google**, create a `bash` or statement in the filter like this: - -```bash -PREF_PASS_FILTER_MANUFACTURER_ARRIVE="Apple|Google" -``` +Each image includes OCI metadata: -If your phone shows as **Unknown**, then it is best to disable the filter entirely - some phones will report a blank manufacturer, others will report a null value... it's much easier to try and filter with another value: +* source repository +* git revision +* build timestamp -```bash -PREF_PASS_FILTER_MANUFACTURER_ARRIVE=".*" -``` +--- -Similarly, we can create a negative filter. If you or your neighbors use Google Home, it is likely that you'll see at least some devices manufactured by **Google**. Create a fail filter condition to ignore these advertisements: +## 🏗 Architecture (High-Level) -```bash -PREF_FAIL_FILTER_MANUFACTURER_ARRIVE="Google" +``` +BLE Devices + ↓ +BLE Monitor (container) + ↓ MQTT (raw facts) +MQTT Broker + ↓ +Home Assistant + - binary_sensor + - device_tracker + - Wi-Fi correlation + - Bayesian sensors + - person entities + - automations ``` -Filters are a great way to minimize the frequency of `name` requestning, which causes 2.4GHz interference and can, if your values are too aggressive, dramatically interfere with Wi-Fi and other services. - -2. **Standard configuration options:** - -When `monitor` is first run, default preferences are created in the `behavior_preferences` file. These preferences can be changed, and in many cases should be changed depending on your Bluetooth environment (how many devices you have around you at any given time). A table below describes what these default variables are: - -| **Option** | **Default Value** | **Description** | -|-|-|-| -| PREF_ARRIVAL_SCAN_ATTEMPTS | 1 | This is the number of times that `monitor` will send a name request before deciding that a device has not yet arrived. The higher the number, the fewer errors on arrival detection but also the longer it may take to recognize all devices are home in a multi-device installation. | -| PREF_DEPART_SCAN_ATTEMPTS | 2 | This is the number of times that `monitor` will send a name request before deciding that a device has not yet departed. The higher the number, the fewer errors on departure detection but also the longer it may take to recognize all devices are away in a multi-device installation. | -| PREF_BEACON_EXPIRATION | 180 | This is the number of seconds without observing an advertisement before a beacon is considered expired. | -| PREF_MINIMUM_TIME_BETWEEN_SCANS | 15 | This is the minimum number of seconds required between "arrival" scans or between "departure" scans. Increasing the value will decrease interference, but will also increase arrival and departure detection time. | -| PREF_PASS_FILTER_ADV_FLAGS_ARRIVE | .* | See above. | -| PREF_PASS_FILTER_MANUFACTURER_ARRIVE | .* | See above. | -| PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE | NONE | See above. | -| PREF_FAIL_FILTER_MANUFACTURER_ARRIVE | NONE | See above. | -| PREF_ALIAS_MODE | true | Disable or enable alias mode; if disabled, MQTT messages are sent using a device's mac address. | - -3. **Advanced configuration options:** - -In addition to the options described above, there are a number of advanced options that can be set by the user. To modify any of these options, add a line to the `behavior_preferences` file. - - -| **Option** | **Default Value** | **Description** | -|-|-|-| -PREF_INTERSCAN_DELAY|3|This is a fixed delay between `name` requests. Increasing the value will decrease interference, but will decrease responsiveness. Decreasing the value will risk a Bluetooth hardware fault.| -PREF_RANDOM_DEVICE_EXPIRATION_INTERVAL|75|This is the interval after which an anonymous advertisement mac address is considered expired. Increasing this value will reduce arrival scan frequency, but will also increase memory footprint (minimal) and will decrease the frequency of depart scans.| -PREF_RSSI_CHANGE_THRESHOLD|-20|If a beacon's rssi changes by at least this value, then the beacon will be reported again via mqtt.| -PREF_RSSI_IGNORE_BELOW|-75|If an anonymous advertisement is "farther" away (lower RSSI), ignore the advertisement -PREF_HCI_DEVICE|hci0|Select which hci device should be used by `monitor`| -PREF_COOPERATIVE_SCAN_THRESHOLD|60|Once confidence of a known device falls below this value, send an mqtt message to other `monitor` nodes to begin an arrival scan or a departure scan.| -PREF_MQTT_REPORT_SCAN_MESSAGES|false|This value is either true or false and determines whether `monitor` publishes when a scan begins and when a scan ends| -PREF_PERCENT_CONFIDENCE_REPORT_THRESHOLD|59|This value defines when a beacon begins reporting a decline in confidence| -PREF_PASS_FILTER_PDU_TYPE|*Various. See FAQ.*|These are the PDU types that should be noticed by `monitor`| -PREF_DEVICE_TRACKER_REPORT|false|If true, this value will cause `monitor` to report a 'home' or 'not_home' message to `... /device_tracker` conforming to device_tracker mqtt protocol. -PREF_DEVICE_TRACKER_HOME_STRING|home|If `PREF_DEVICE_TRACKER_REPORT` is true, this is the string that is reported to the device_tracker when the device is home. -PREF_DEVICE_TRACKER_AWAY_STRING|not_home|If `PREF_DEVICE_TRACKER_REPORT` is true, this is the string that is reported to the device_tracker when the device is not home. -PREF_DEVICE_TRACKER_TOPIC_BRANCH|device_tracker|If `PREF_DEVICE_TRACKER_REPORT` is true, this is last path element of the mqtt topic path that will be used to publish the device tracker message. -PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP|15|This is the minimum interval (in seconds) used to estimate advertisement intervals reported in the MQTT message. -PREF_DEPART_SCAN_INTERVAL|30|If using periodic scanning mode, this is the minimum interval (in seconds) at which depart scans are triggered automatically. -PREF_ARRIVE_SCAN_INTERVAL|15|If using periodic scanning mode, this is the minimum interval (in seconds) at which arrive scans are triggered automatically. - - -## RSSI Tracking - -This script can also track RSSI changes throughout the day. This can be used for very rudimentary room- or floor-level tracking. Only devices in `known_static_addresses` that have been paired to a `monitor` node can have their RSSI tracked. Here's how to pair: +--- -1. Stop `monitor` service: +## 🧭 Project Direction -```bash -sudo systemctl stop monitor -``` +This repository is intentionally focused on **one responsibility**: -2. Run `monitor` with `-c` flag, followed by the mac address of the known_device to connect: +> **Publish raw BLE presence facts over MQTT, reliably and predictably.** -```bash -sudo bash monitor.sh -c 00:11:22:33:44:55 -``` +Future evolution may include: -After this, follow the prompts given by `monitor` and your device will be connected. That's it. After you restart monitor will periodically (once every ~1.5 minutes) connect to your phone and take three RSSI samples, average the samples, and report a string message to the same path as a confidence report, with the additional path component of */rssi*. So, if a `monitor` node is named 'first floor', an rssi message is reported to: +* Native Python BLE scanner (replacing shell scripts) +* Improved resolvable MAC handling +* Better beacon vs phone identity mapping -```bash -topic: monitor/first floor/00:11:22:33:44:55/rssi -message: -99 through 0 -``` +What will **not** be added: -If an rssi measurement cannot be obtained, the value of -99 is sent. +* Home Assistant YAML +* Presence logic +* Automations +* UI configuration -## Report known states +--- -It is also possible tell monitor to report all currently known device states by sending an MQTT message to something like `monitor/first floor/KNOWN DEVICE STATES`. monitor.sh will then iterate over all known static addresses and report the current confidence level. This may be useful in home assistant to get the current state after a home assistant restart. +## 📜 License -
+MIT (inherited from upstream work). -Anything else? Post a [question.](https://github.com/andrewjfreyer/monitor/issues/new) From 773ae160b857dbbe3ec81e8f0b6ab223859cac2a Mon Sep 17 00:00:00 2001 From: Jb Date: Tue, 23 Dec 2025 22:56:22 -0500 Subject: [PATCH 5/6] adding kaniko on push --- ...aniko-dev-build.yml => kaniko-release.yml} | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) rename .github/workflows/{kaniko-dev-build.yml => kaniko-release.yml} (59%) diff --git a/.github/workflows/kaniko-dev-build.yml b/.github/workflows/kaniko-release.yml similarity index 59% rename from .github/workflows/kaniko-dev-build.yml rename to .github/workflows/kaniko-release.yml index 11ad797..4a2a9ca 100644 --- a/.github/workflows/kaniko-dev-build.yml +++ b/.github/workflows/kaniko-release.yml @@ -1,17 +1,18 @@ -name: Kaniko Build (Dev → GHCR) +name: Kaniko Build (Release → GHCR) on: push: - branches: ["dev"] + tags: + - "v*.*.*" workflow_dispatch: {} permissions: contents: read - packages: write # required to push to GHCR + packages: write env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} # owner/repo + IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: @@ -25,8 +26,8 @@ jobs: id: meta shell: bash run: | - SHORT_SHA="${GITHUB_SHA:0:7}" - echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" + VERSION="${GITHUB_REF_NAME}" # e.g. v0.1.0 + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "image=${REGISTRY}/${IMAGE_NAME}" >> "$GITHUB_OUTPUT" - name: Write Docker auth for GHCR @@ -35,11 +36,7 @@ jobs: mkdir -p .docker AUTH="$(printf "%s:%s" "${{ github.actor }}" "${{ secrets.GITHUB_TOKEN }}" | base64 -w0)" cat > .docker/config.json < --context=dir:///github/workspace - --dockerfile=Dockerfile + --dockerfile=docker/Dockerfile --docker-config=/github/workspace/.docker - --destination=${{ steps.meta.outputs.image }}:dev-${{ steps.meta.outputs.short_sha }} - --destination=${{ steps.meta.outputs.image }}:dev-latest + --destination=${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.version }} + --destination=${{ steps.meta.outputs.image }}:latest --cache=true - --cache-ttl=48h + --cache-ttl=168h From 08fcbfe155a6a51ab6d79c736371db40987739a6 Mon Sep 17 00:00:00 2001 From: Jb Date: Tue, 23 Dec 2025 22:58:35 -0500 Subject: [PATCH 6/6] adding readme release --- Makefile | 11 +++++++++++ README.md | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Makefile b/Makefile index f26f4a7..e1db23e 100644 --- a/Makefile +++ b/Makefile @@ -149,6 +149,17 @@ build-latest: build-all: build build-latest # ---- Tagging ---------------------------------------------- +.PHONY: tag-release push-release + +VERSION ?= v0.1.0 + +tag-release: + git tag $(VERSION) + git push origin $(VERSION) + +push-release: + @echo "Release is CI-driven. Push a tag: make tag-release VERSION=vX.Y.Z" + tag: $(DOCKER) tag \ diff --git a/README.md b/README.md index a4d9cc9..9114583 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,25 @@ What will **not** be added: * Automations * UI configuration +--- +## 🚢 Releases + +Development images: +- `dev-` +- `dev-latest` + +Release images (tagged): +- `vX.Y.Z` +- `latest` + +To cut a release: +1. Merge `dev` → `main/master` +2. Tag a version: + +```bash +make tag-release VERSION=v0.1.0 + + --- ## 📜 License