├── .gitignore ├── README.md ├── monitor.sh └── support ├── README.md ├── argv ├── btle ├── data ├── init ├── log ├── mqtt └── time /.gitignore: -------------------------------------------------------------------------------- 1 | # mac related files 2 | .DS_Store 3 | 4 | # configuration files 5 | address_blacklist 6 | behavior_preferences 7 | known_beacon_addresses 8 | known_static_addresses 9 | mqtt_preferences 10 | 11 | # caches/temp files 12 | .pids 13 | .previous_version 14 | .public_name_cache 15 | .manufacturer_cache 16 | 17 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 18 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 19 | 20 | # User-specific stuff 21 | .idea/**/workspace.xml 22 | .idea/**/tasks.xml 23 | .idea/**/usage.statistics.xml 24 | .idea/**/dictionaries 25 | .idea/**/shelf 26 | 27 | # Generated files 28 | .idea/**/contentModel.xml 29 | 30 | # Sensitive or high-churn files 31 | .idea/**/dataSources/ 32 | .idea/**/dataSources.ids 33 | .idea/**/dataSources.local.xml 34 | .idea/**/sqlDataSources.xml 35 | .idea/**/dynamic.xml 36 | .idea/**/uiDesigner.xml 37 | .idea/**/dbnavigator.xml 38 | 39 | # Gradle 40 | .idea/**/gradle.xml 41 | .idea/**/libraries 42 | 43 | # Gradle and Maven with auto-import 44 | # When using Gradle or Maven with auto-import, you should exclude module files, 45 | # since they will be recreated, and may cause churn. Uncomment if using 46 | # auto-import. 47 | # .idea/artifacts 48 | # .idea/compiler.xml 49 | # .idea/jarRepositories.xml 50 | # .idea/modules.xml 51 | # .idea/*.iml 52 | # .idea/modules 53 | # *.iml 54 | # *.ipr 55 | 56 | # CMake 57 | cmake-build-*/ 58 | 59 | # Mongo Explorer plugin 60 | .idea/**/mongoSettings.xml 61 | 62 | # File-based project format 63 | *.iws 64 | 65 | # IntelliJ 66 | out/ 67 | 68 | # mpeltonen/sbt-idea plugin 69 | .idea_modules/ 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Cursive Clojure plugin 75 | .idea/replstate.xml 76 | 77 | # Crashlytics plugin (for Android Studio and IntelliJ) 78 | com_crashlytics_export_strings.xml 79 | crashlytics.properties 80 | crashlytics-build.properties 81 | fabric.properties 82 | 83 | # Editor-based Rest Client 84 | .idea/httpRequests 85 | 86 | # Android studio 3.1+ serialized cache file 87 | .idea/caches/build_file_checksums.ser 88 | 89 | # Vim related 90 | # Swap 91 | [._]*.s[a-v][a-z] 92 | !*.svg # comment out if you don't need vector files 93 | [._]*.sw[a-p] 94 | [._]s[a-rt-v][a-z] 95 | [._]ss[a-gi-z] 96 | [._]sw[a-p] 97 | 98 | # Session 99 | Session.vim 100 | Sessionx.vim 101 | 102 | # Temporary 103 | .netrwhist 104 | *~ 105 | # Auto-generated tag files 106 | tags 107 | # Persistent undo 108 | [._]*.un~ 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `monitor` 2 | ======= 3 | ***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. 4 | 5 | ![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) 6 | 7 | [**Frequently Asked Questions**](https://github.com/andrewjfreyer/monitor/blob/master/support/README.md) 8 | 9 |
Installation Instructions 10 | 11 |
12 | 13 |
Set Up Raspberry Pi From Scratch 14 | 15 | 16 | # Installation Instructions for Raspberry Pi Zero W 17 | 18 | ## Setup of SD Card 19 | 20 | 1. Download latest version of **raspbian** [here](https://downloads.raspberrypi.org/raspbian_lite_latest) 21 | 22 | 2. Download etcher from [etcher.io](https://etcher.io) 23 | 24 | 3. Image **raspbian lite buster** to SD card. [Instructions here.](https://www.raspberrypi.org/magpi/pi-sd-etcher/) 25 | 26 | 4. Mount **boot** partition of imaged SD card (unplug it and plug it back in) 27 | 28 | 5. **To enable ssh,** create blank file, without any extension, in the root directory called **ssh** 29 | 30 | 6. **To setup Wi-Fi**, create **wpa_supplicant.conf** file in root directory and add Wi-Fi details for home Wi-Fi: 31 | 32 | ```bash 33 | country=US 34 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 35 | update_config=1 36 | 37 | network={ 38 | ssid="Your Network Name" 39 | psk="Your Network Password" 40 | key_mgmt=WPA-PSK 41 | } 42 | ``` 43 | 44 | 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. 45 | 46 | ## Configuration and Setup 47 | 48 | 1. SSH into the Raspberry Pi (default password: raspberry): 49 | ```bash 50 | ssh pi@theipaddress 51 | ``` 52 | 53 | 2. Change the default password: 54 | ```bash 55 | sudo passwd pi 56 | ``` 57 | 58 | 3. Update and upgrade: 59 | 60 | ```bash 61 | sudo apt-get update 62 | sudo apt-get upgrade -y 63 | sudo apt-get dist-upgrade -y 64 | sudo reboot 65 | ``` 66 | 67 | 5. Install Bluetooth Firmware, if necessary: 68 | ```bash 69 | #install Bluetooth drivers for Pi Zero W 70 | sudo apt-get install pi-bluetooth 71 | 72 | ``` 73 | 74 | 6. Reboot: 75 | ```bash 76 | sudo reboot 77 | ``` 78 | 79 | 7. Install Mosquitto 1.5+ **(important step!)**: 80 | ```bash 81 | 82 | # get repo key 83 | wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key 84 | 85 | #add repo 86 | sudo apt-key add mosquitto-repo.gpg.key 87 | 88 | #download appropriate lists file 89 | cd /etc/apt/sources.list.d/ 90 | sudo wget http://repo.mosquitto.org/debian/mosquitto-buster.list 91 | 92 | #update caches and install 93 | sudo apt-cache search mosquitto 94 | sudo apt-get update 95 | sudo apt-get install -f libmosquitto-dev mosquitto mosquitto-clients libmosquitto1 96 | ``` 97 |
98 | 99 |
Monitor Setup 100 | 101 | ## Setup `monitor` 102 | 103 | 1. Clone `monitor` git: 104 | ```bash 105 | #install git 106 | cd ~ 107 | sudo apt-get install git 108 | 109 | #clone this repo 110 | git clone https://github.com/andrewjfreyer/monitor.git 111 | 112 | #enter `monitor` directory 113 | cd monitor/ 114 | 115 | #(optional) switch to beta branch for latest updates and features (may be unstable) 116 | git checkout beta 117 | 118 | ``` 119 | 120 | 2. Initial run: 121 | 122 | 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 ...` 123 | 124 | ```bash 125 | sudo bash monitor.sh 126 | ``` 127 | 128 | 129 | 3. Edit **mqtt_preferences** file: 130 | 131 | ```bash 132 | sudo nano mqtt_preferences 133 | ``` 134 | 135 | 4. Edit **known_static_addresses** (phones, laptops, some smart watches): 136 | 137 | ```bash 138 | sudo nano known_static_addresses 139 | ``` 140 | 141 | 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: 142 | 143 | 144 | **topic:** `monitor/setup/ADD STATIC DEVICE` 145 | **message:** 00:11:22:33:44:55 alias 146 | 147 | 148 | Use, `monitor/setup/DELETE STATIC DEVICE` with a message containing a mac address to remove a device from all `monitor` nodes. 149 | 150 | 5. Read helpfile: 151 | 152 | ```bash 153 | sudo bash monitor.sh -h 154 | ``` 155 | 156 | 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. 157 | 158 |
159 | 160 | ___ 161 | 162 |
163 | 164 |
Background & Technical Details 165 | 166 | # *Highlights* 167 | 168 | `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). 169 | 170 | Example JSON package: 171 | ``` 172 | topic: monitor/{{name of monitor install}}/{{mac address}} 173 | message: { 174 | "id":"{{mac address}}", 175 | "confidence":"{{ranging from 0-100}}", 176 | "name":"{{if available}}", 177 | "manufacturer":{{if available}}", 178 | "type":"KNOWN_MAC", 179 | "retained":"{{message retained?}}", 180 | "timestamp":"{{formatted date at which message is sent}}", 181 | "version":"{{monitor version}}" 182 | } 183 | ``` 184 | 185 | 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. 186 | 187 | Example JSON package: 188 | ``` 189 | topic: monitor/{{name of monitor install}}/{{mac address or ibeacon uuid}} 190 | message: { 191 | "id":"{{mac address or ibeacon uuid}}", 192 | "report_delay":"{{delay from first detection to this message in seconds}}", 193 | "flags":"{{GAP flags}}", 194 | "movement":"stationary", 195 | "confidence":"{{ranging from 0-100}}", 196 | "name":"{{if available}}", 197 | "power":"{{if available}}", 198 | "rssi":"{{if available}}", 199 | "mac":"{{if ibeacon, the current mac address associated with the uuid}}", 200 | "manufacturer":{{if available}}", 201 | "type":"{{GENERIC_BEACON_PUBLIC or APPLE_IBEACON}}, 202 | "retained":"{{message retained?}}", 203 | "timestamp":"{{formatted date at which message is sent}}", 204 | "version":"{{monitor version}}" 205 | } 206 | ``` 207 | ___ 208 | 209 | # *Oversimplified Analogy of the Bluetooth Presence Problem* 210 | 211 | 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: 212 | 213 | ![First Picture](https://i.imgur.com/FOubz6T.png) 214 | 215 | 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: 216 | 217 | ![Second Picture](https://i.imgur.com/UwPJIMM.png) 218 | 219 | 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?" 220 | 221 | So, you take attendance like in a classroom. Everyone in the room responds **only** when their own name is shouted. 222 | 223 | ![Third Picture](https://i.imgur.com/VCW8AmH.png) 224 | 225 | 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. 226 | 227 | Once a friend stops responding (for some period of time), you presume that he or she has left: 228 | 229 | ![Simple Loop](https://i.imgur.com/ijGw2qb.png) 230 | 231 | 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? 232 | 233 | 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: 234 | 235 | ![Complex Loop](https://i.imgur.com/9Ugn27i.png) 236 | 237 | This way, you're not constantly asking the room for all of your friends. Efficient! 238 | 239 | 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. 240 | 241 | ___ 242 | 243 | # *Oversimplified Technical Background* 244 | 245 | 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. 246 | 247 | ## Name Requests 248 | 249 | 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. 250 | 251 | 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. 252 | 253 | Not all devices respond to `name` requests, however. For example, beacon devices do not respond. 254 | 255 | ## Connectible Devices 256 | 257 | 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. 258 | 259 | ## Beacon/Advertising Devices 260 | 261 | 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). 262 | 263 | 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**. 264 | 265 | _____ 266 | 267 | # *How `monitor` Works* 268 | 269 | 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. 270 | 271 | ##### Known Static Addresses 272 | `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**. 273 | 274 | 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). 275 | 276 | 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. 277 | 278 | More specifically: 279 | 280 | * An ***ARRIVAL*** scan issues a `name` request, sequentially, for each device listed in the `known_static_addresses` file that is known to be **absent**. 281 | 282 | * 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**. 283 | 284 | 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. 285 | 286 | To reduce the number of `name` requests that occur, `monitor` listens for anonymous advertisements and triggers an ***ARRIVAL*** scan for every *new* anonymous address. 287 | 288 | 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. 289 | 290 | 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. 291 | 292 | 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. 293 | 294 | There are two filter types: 295 | 296 | * **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 `.*`. 297 | 298 | * **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 `.*`. 299 | 300 | ##### Beacons & iBeacons 301 | 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. 302 | 303 | Since iBeacons include a UUID and a mac address, two presence messages are reported via mqtt. 304 | 305 | ## Known Beacon Addresses 306 | 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. 307 | ___ 308 | 309 |
310 | 311 |
Home Assistant Example 312 | 313 | # Example with Home Assistant 314 | 315 | 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). 316 | 317 | 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. 318 | 319 | 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): 320 | 321 | ```bash 322 | 00:00:00:00:00:00 alias #comment that is ignored 323 | ``` 324 | 325 | 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. 326 | 327 | 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: 328 | 329 | ``` 330 | - platform: mqtt 331 | state_topic: 'monitor/first floor/alias' 332 | value_template: '{{ value_json.confidence }}' 333 | unit_of_measurement: '%' 334 | name: 'First Floor' 335 | 336 | - platform: mqtt 337 | state_topic: 'monitor/second floor/alias' 338 | value_template: '{{ value_json.confidence }}' 339 | unit_of_measurement: '%' 340 | name: 'Second Floor' 341 | 342 | - platform: mqtt 343 | state_topic: 'monitor/third floor/alias' 344 | value_template: '{{ value_json.confidence }}' 345 | unit_of_measurement: '%' 346 | name: 'Third Floor' 347 | 348 | - platform: mqtt 349 | state_topic: 'monitor/garage/alias' 350 | value_template: '{{ value_json.confidence }}' 351 | unit_of_measurement: '%' 352 | name: 'Garage' 353 | ``` 354 | 355 | These sensors can be combined using a [min_max](https://www.home-assistant.io/components/sensor.min_max/): 356 | 357 | ``` 358 | - platform: min_max 359 | name: "Home Occupancy Confidence" 360 | type: max 361 | round_digits: 0 362 | entity_ids: 363 | - sensor.third_floor 364 | - sensor.second_floor 365 | - sensor.first_floor 366 | - sensor.garage 367 | ``` 368 | 369 | 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. 370 | 371 | As an example: 372 | 373 | ``` 374 | - alias: Occupancy On 375 | trigger: 376 | - platform: numeric_state 377 | entity_id: sensor.home_occupancy_confidence 378 | above: 10 379 | action: 380 | - service: input_boolean.turn_on 381 | data: 382 | entity_id: input_boolean.occupancy 383 | 384 | - alias: Occupancy Off 385 | trigger: 386 | - platform: numeric_state 387 | entity_id: sensor.home_occupancy_confidence 388 | below: 10 389 | action: 390 | - service: input_boolean.turn_off 391 | data: 392 | entity_id: input_boolean.occupancy 393 | ``` 394 | 395 | If you prefer to use the `device_tracker` platform in Home Assistant, a unique solution is to use the undocumented `device_tracker.see` service: 396 | 397 | As an example: 398 | 399 | ``` 400 | - alias: Andrew Occupancy On 401 | trigger: 402 | - platform: numeric_state 403 | entity_id: sensor.andrew_occupancy_confidence 404 | above: 10 405 | action: 406 | - service: device_tracker.see 407 | data: 408 | dev_id: andrew 409 | location_name: home 410 | source_type: bluetooth 411 | 412 | - alias: Andrew Occupancy Off 413 | trigger: 414 | - platform: numeric_state 415 | entity_id: sensor.andrew_occupancy_confidence 416 | below: 10 417 | action: 418 | - service: device_tracker.see 419 | data: 420 | dev_id: andrew 421 | location_name: not_home 422 | source_type: bluetooth 423 | 424 | ``` 425 | 426 | 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). 427 | 428 | If you only have one node, an [add-on](https://github.com/Limych/hassio-addons) by @limych may be an excellent choice for you! 429 | 430 | 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) 431 | 432 |
433 | 434 |
Advanced Configuration Options & Fine Tuning 435 | 436 | 437 | ## Fine Tuning 438 | 439 | 440 | 1. Observe output from `monitor` to tune filters: 441 | 442 | ```bash 443 | sudo bash monitor.sh 444 | ``` 445 | 446 | 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. 447 | 448 | ``` 449 | 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 450 | ``` 451 | 452 | 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: 453 | 454 | ```bash 455 | PREF_PASS_FILTER_MANUFACTURER_ARRIVE="Apple" 456 | ``` 457 | 458 | If you have two phones, and one is **Apple** and the other is **Google**, create a `bash` or statement in the filter like this: 459 | 460 | ```bash 461 | PREF_PASS_FILTER_MANUFACTURER_ARRIVE="Apple|Google" 462 | ``` 463 | 464 | 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: 465 | 466 | ```bash 467 | PREF_PASS_FILTER_MANUFACTURER_ARRIVE=".*" 468 | ``` 469 | 470 | 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: 471 | 472 | ```bash 473 | PREF_FAIL_FILTER_MANUFACTURER_ARRIVE="Google" 474 | ``` 475 | 476 | 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. 477 | 478 | 2. **Standard configuration options:** 479 | 480 | 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: 481 | 482 | | **Option** | **Default Value** | **Description** | 483 | |-|-|-| 484 | | 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. | 485 | | 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. | 486 | | PREF_BEACON_EXPIRATION | 180 | This is the number of seconds without observing an advertisement before a beacon is considered expired. | 487 | | 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. | 488 | | PREF_PASS_FILTER_ADV_FLAGS_ARRIVE | .* | See above. | 489 | | PREF_PASS_FILTER_MANUFACTURER_ARRIVE | .* | See above. | 490 | | PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE | NONE | See above. | 491 | | PREF_FAIL_FILTER_MANUFACTURER_ARRIVE | NONE | See above. | 492 | | PREF_ALIAS_MODE | true | Disable or enable alias mode; if disabled, MQTT messages are sent using a device's mac address. | 493 | 494 | 3. **Advanced configuration options:** 495 | 496 | 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. 497 | 498 | 499 | | **Option** | **Default Value** | **Description** | 500 | |-|-|-| 501 | 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.| 502 | 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.| 503 | 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.| 504 | PREF_RSSI_IGNORE_BELOW|-75|If an anonymous advertisement is "farther" away (lower RSSI), ignore the advertisement 505 | PREF_HCI_DEVICE|hci0|Select which hci device should be used by `monitor`| 506 | 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.| 507 | 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| 508 | PREF_PERCENT_CONFIDENCE_REPORT_THRESHOLD|59|This value defines when a beacon begins reporting a decline in confidence| 509 | PREF_PASS_FILTER_PDU_TYPE|*Various. See FAQ.*|These are the PDU types that should be noticed by `monitor`| 510 | 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. 511 | 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. 512 | 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. 513 | 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. 514 | PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP|15|This is the minimum interval (in seconds) used to estimate advertisement intervals reported in the MQTT message. 515 | PREF_DEPART_SCAN_INTERVAL|30|If using periodic scanning mode, this is the minimum interval (in seconds) at which depart scans are triggered automatically. 516 | PREF_ARRIVE_SCAN_INTERVAL|15|If using periodic scanning mode, this is the minimum interval (in seconds) at which arrive scans are triggered automatically. 517 | 518 | 519 | ## RSSI Tracking 520 | 521 | 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: 522 | 523 | 1. Stop `monitor` service: 524 | 525 | ```bash 526 | sudo systemctl stop monitor 527 | ``` 528 | 529 | 2. Run `monitor` with `-c` flag, followed by the mac address of the known_device to connect: 530 | 531 | ```bash 532 | sudo bash monitor.sh -c 00:11:22:33:44:55 533 | ``` 534 | 535 | 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: 536 | 537 | ```bash 538 | topic: monitor/first floor/00:11:22:33:44:55/rssi 539 | message: -99 through 0 540 | ``` 541 | 542 | If an rssi measurement cannot be obtained, the value of -99 is sent. 543 | 544 | ## Report known states 545 | 546 | 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. 547 | 548 |
549 | 550 | Anything else? Post a [question.](https://github.com/andrewjfreyer/monitor/issues/new) 551 | -------------------------------------------------------------------------------- /monitor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------------------------------------------------------------------------- 4 | # GENERAL INFORMATION 5 | # ---------------------------------------------------------------------------------------- 6 | # 7 | # Written by Andrew J Freyer 8 | # GNU General Public License 9 | # http://github.com/andrewjfreyer/monitor 10 | # 11 | # Credits to: 12 | # Radius Networks iBeacon Script 13 | # • http://developer.radiusnetworks.com/ibeacon/idk/ibeacon_scan 14 | # 15 | # Reely Active advlib 16 | # • https://github.com/reelyactive/advlib 17 | # 18 | # _ _ 19 | # (_) | 20 | # _ __ ___ ___ _ __ _| |_ ___ _ __ 21 | # | '_ ` _ \ / _ \| '_ \| | __/ _ \| '__| 22 | # | | | | | | (_) | | | | | || (_) | | 23 | # |_| |_| |_|\___/|_| |_|_|\__\___/|_| 24 | # 25 | # ---------------------------------------------------------------------------------------- 26 | 27 | #VERSION NUMBER 28 | export version=0.2.200 29 | 30 | if [ -z ${NO_COLOR} -o -z ${NOCOLOR} ] ; then 31 | #COLOR OUTPUT FOR RICH OUTPUT 32 | ORANGE=$'\e[1;33m' 33 | RED=$'\e[1;31m' 34 | NC=$'\e[0m' 35 | GREEN=$'\e[1;32m' 36 | PURPLE=$'\e[1;35m' 37 | BLUE=$'\e[1;34m' 38 | CYAN=$'\e[1;36m' 39 | YELLOW=$'\e[01;33m' 40 | REPEAT=$'\e[1A' 41 | else 42 | : 43 | fi 44 | 45 | # ---------------------------------------------------------------------------------------- 46 | # BETA WARNING ONLY IF ON THE BETA CHANNEL 47 | # ---------------------------------------------------------------------------------------- 48 | 49 | if command -v git >/dev/null && [[ $(git status) =~ .*beta.* ]]; then 50 | 51 | printf "\n%s\n" "${RED}===================================================${NC}" 52 | printf "\n%s\n" "${RED} ${PURPLE}*** BETA/DEV BRANCH ***${NC}" 53 | printf "\n%s\n" "${RED}===================================================${NC}" 54 | 55 | fi 56 | 57 | #CAPTURE ARGS IN VAR TO USE IN SOURCED FILE 58 | export RUNTIME_ARGS=("$@") 59 | 60 | # ---------------------------------------------------------------------------------------- 61 | # SOURCES 62 | # ---------------------------------------------------------------------------------------- 63 | #SOURCE SETUP AND ARGV FILES 64 | source './support/argv' 65 | source './support/init' 66 | 67 | #SOURCE FUNCTIONS 68 | source './support/mqtt' 69 | source './support/log' 70 | source './support/data' 71 | source './support/btle' 72 | source './support/time' 73 | # ---------------------------------------------------------------------------------------- 74 | # CLEANUP ROUTINE 75 | # ---------------------------------------------------------------------------------------- 76 | clean() { 77 | #CLEANUP FOR TRAP 78 | pkill -f monitor.sh 79 | 80 | #REMOVE PIPES 81 | rm main_pipe &>/dev/null 82 | rm log_pipe &>/dev/null 83 | rm packet_pipe &>/dev/null 84 | 85 | #MESSAGE 86 | echo 'Exited.' 87 | } 88 | 89 | trap "clean" EXIT 90 | 91 | # ---------------------------------------------------------------------------------------- 92 | # DEFINE VALUES AND VARIABLES 93 | # ---------------------------------------------------------------------------------------- 94 | 95 | #CYCLE BLUETOOTH INTERFACE 96 | hciconfig "$PREF_HCI_DEVICE" down && sleep 3 && hciconfig "$PREF_HCI_DEVICE" up 97 | 98 | #STOP OTHER INSTANCES OF MONITOR WITHOUT STOPPING THIS ONE 99 | for pid in $(pidof -x "$(basename "$0")"); do 100 | if [ "$pid" != $$ ]; then 101 | kill -9 "$pid" 102 | fi 103 | done 104 | 105 | #SETUP MAIN PIPE 106 | rm main_pipe &>/dev/null 107 | mkfifo main_pipe 108 | 109 | #SETUP LOG PIPE 110 | rm log_pipe &>/dev/null 111 | mkfifo log_pipe 112 | 113 | #SETUP BTLE PIPE 114 | rm packet_pipe &>/dev/null 115 | mkfifo packet_pipe 116 | 117 | 118 | #DEFINE DEVICE TRACKING VARS 119 | declare -A public_device_log 120 | declare -A random_device_log 121 | declare -A rssi_log 122 | 123 | #STATIC DEVICE ASSOCIATIVE ARRAYS 124 | declare -A known_public_device_log 125 | declare -A expiring_device_log 126 | declare -A known_static_device_scan_log 127 | declare -A known_public_device_name 128 | declare -A blacklisted_devices 129 | declare -A beacon_mac_address_log 130 | declare -A mqtt_aliases 131 | declare -A advertisement_interval_observation 132 | 133 | #LAST TIME THIS 134 | scan_pid="" 135 | scan_type="" 136 | 137 | #SCAN VARIABLES 138 | now=$(date +%s) 139 | last_rssi_scan="" 140 | last_arrival_scan=$((now - 25)) 141 | last_depart_scan=$((now - 25)) 142 | first_arrive_scan=true 143 | 144 | # ---------------------------------------------------------------------------------------- 145 | # POPULATE THE ASSOCIATIVE ARRAYS THAT INCLUDE INFORMATION ABOUT THE STATIC DEVICES 146 | # WE WANT TO TRACK 147 | # ---------------------------------------------------------------------------------------- 148 | 149 | #LOAD PUBLIC ADDRESSES TO SCAN INTO ARRAY, IGNORING COMMENTS 150 | mapfile -t known_static_beacons < <(sed 's/#.\{0,\}//gi' < "$BEAC_CONFIG" | awk '{print $1}' | grep -oiE "([0-9a-f]{2}:){5}[0-9a-f]{2}" ) 151 | mapfile -t known_static_addresses < <(sed 's/#.\{0,\}//gi' < "$PUB_CONFIG" | awk '{print $1}' | grep -oiE "([0-9a-f]{2}:){5}[0-9a-f]{2}" ) 152 | mapfile -t address_blacklist < <(sed 's/#.\{0,\}//gi' < "$ADDRESS_BLACKLIST" | awk '{print $1}' | grep -oiE "([0-9a-f]{2}:){5}[0-9a-f]{2}" ) 153 | 154 | #ASSEMBLE COMMENT-CLEANED BLACKLIST INTO BLACKLIST ARRAY 155 | for addr in "${address_blacklist[@]^^}"; do 156 | blacklisted_devices[$addr]=1 157 | printf "%s\n" "> ${RED}blacklisted device:${NC} $addr" 158 | done 159 | 160 | # ---------------------------------------------------------------------------------------- 161 | # POPULATE MAIN DEVICE ARRAY 162 | # ---------------------------------------------------------------------------------------- 163 | 164 | #LIST CONNECTED DEVICES 165 | previously_connected_devices=$(echo "paired-devices" | bluetoothctl | grep -Eio "Device ([0-9A-F]{2}:){5}[0-9A-F]{2}" | sed 's/Device //gi') 166 | 167 | #POPULATE KNOWN DEVICE ADDRESS 168 | for addr in "${known_static_addresses[@]^^}"; do 169 | 170 | #================= SHOULD WE USE AN ALIAS? ===================== 171 | 172 | #WAS THERE A NAME HERE? 173 | known_name=$(grep -i "$addr" "$PUB_CONFIG" | tr "\\t" " " | sed 's/ */ /gi;s/#.\{0,\}//gi' | sed "s/$addr //gi;s/ */ /gi" ) 174 | 175 | #IF THE VALUE DOES NOT EXIST, USE THE KEY (MAC ADDRESS INSTEAD) 176 | alias_value=${known_name//[^A-Za-z0-9]/_} 177 | 178 | #LOWERCASE 179 | alias_value=${alias_value,,} 180 | 181 | #REMOVE FINAL UNDERSCORES SHOUDL THERE BE 182 | alias_value=$(echo "$alias_value" | sed 's/[^0-9a-z]\{1,\}$//gi;s/^[^0-9a-z]\{1,\}//gi;s/__*/_/gi') 183 | 184 | #DEFAULT 185 | alias_value=${alias_value:-$addr} 186 | 187 | #ALIASES 188 | [ -n "$addr" ] && [ -n "$alias_value" ] && mqtt_aliases[$addr]="$alias_value" 189 | 190 | #================= PROCESS THE KNOWN ADDR ===================== 191 | 192 | #IF WE FOUND A NAME, RECORD IT 193 | [ -n "$known_name" ] && known_public_device_name[$addr]="$known_name" 194 | 195 | #CONNECTED? 196 | is_connected="not previously connected" 197 | [[ $previously_connected_devices =~ .*$addr.* ]] && is_connected="previously connected" 198 | 199 | #CORRECT 200 | $PREF_ALIAS_MODE && mqtt_topic_branch=${mqtt_aliases[$addr]:-$addr} || mqtt_topic_branch=$addr 201 | 202 | #PUBLICATION TOPIC 203 | pub_topic="$mqtt_topicpath/$mqtt_publisher_identity/$mqtt_topic_branch" 204 | $PREF_MQTT_SINGLE_TOPIC_MODE && pub_topic="$mqtt_topicpath/$mqtt_publisher_identity { id: $addr ... }" 205 | 206 | #FOR DEBUGGING 207 | printf "%s\n" "> ${GREEN}$addr${NC} confidence topic: $pub_topic (has $is_connected to $PREF_HCI_DEVICE)" 208 | [ "$PREF_DEVICE_TRACKER_REPORT" == 'true' ] && printf "%s\n" "> ${GREEN}$addr${NC} device_tracker topic: $pub_topic/$PREF_DEVICE_TRACKER_TOPIC_BRANCH [$PREF_DEVICE_TRACKER_AWAY_STRING or $PREF_DEVICE_TRACKER_HOME_STRING]" 209 | done 210 | 211 | # ---------------------------------------------------------------------------------------- 212 | # POPULATE BEACON ADDRESS ARRAY 213 | # ---------------------------------------------------------------------------------------- 214 | #POPULATE KNOWN DEVICE ADDRESS 215 | for addr in "${known_static_beacons[@]^^}"; do 216 | 217 | #WAS THERE A NAME HERE? 218 | known_name=$(grep "$addr" "$BEAC_CONFIG" | tr "\\t" " " | sed 's/ */ /gi;s/#.\{0,\}//gi' | sed "s/$addr //gi;s/ */ /gi" ) 219 | 220 | #================= SHOULD WE USE AN ALIAS? ===================== 221 | 222 | #IF THE VALUE DOES NOT EXIST, USE THE KEY (MAC ADDRESS INSTEAD) 223 | alias_value=${known_name//[^A-Za-z0-9]/_} 224 | 225 | #LOWERCASE 226 | alias_value=${alias_value,,} 227 | 228 | #REMOVE FINAL UNDERSCORES SHOUDL THERE BE 229 | alias_value=$(echo "$alias_value" | sed 's/[^0-9a-z]\{1,\}$//gi;s/^[^0-9a-z]\{1,\}//gi;s/__*/_/gi') 230 | 231 | #DEFAULT 232 | alias_value=${alias_value:-$addr} 233 | 234 | #ALIASES 235 | [ -n "$addr" ] && [ -n "$alias_value" ] && mqtt_aliases[$addr]="$alias_value" 236 | 237 | #IF WE FOUND A NAME, RECORD IT 238 | [ -n "$known_name" ] && known_public_device_name[$addr]="$known_name" 239 | 240 | #CORRECT 241 | $PREF_ALIAS_MODE && mqtt_topic_branch=${mqtt_aliases[$addr]:-$addr} || mqtt_topic_branch=$addr 242 | 243 | #PUBLICATION TOPIC 244 | pub_topic="$mqtt_topicpath/$mqtt_publisher_identity/$mqtt_topic_branch" 245 | $PREF_MQTT_SINGLE_TOPIC_MODE && pub_topic="$mqtt_topicpath/$mqtt_publisher_identity { id: $addr ... }" 246 | 247 | #FOR DBUGGING 248 | echo "> known beacon: $addr publishes to: $pub_topic" 249 | done 250 | 251 | # ---------------------------------------------------------------------------------------- 252 | # ASSEMBLE RSSI LISTS 253 | # ---------------------------------------------------------------------------------------- 254 | 255 | connectable_present_devices () { 256 | 257 | #DEFINE LOCAL VARS 258 | local this_state 259 | local known_device_rssi 260 | local avg_total 261 | local scan_result 262 | 263 | #ITERATE THROUGH THE KNOWN DEVICES 264 | local known_addr 265 | for known_addr in "${known_static_addresses[@]^^}"; do 266 | 267 | #GET STATE; ONLY SCAN FOR DEVICES WITH SPECIFIC STATE 268 | this_state="${known_public_device_log[$known_addr]}" 269 | this_state=${this_state:-0} 270 | 271 | #TEST IF THIS DEVICE MATCHES THE TARGET SCAN STATE 272 | if [ "$this_state" == "1" ] && [[ "$previously_connected_devices" =~ .*$known_addr.* ]] ; then 273 | 274 | #CREATE CONNECTION AND DETERMINE RSSI 275 | #AVERAGE OVER THREE CYCLES; IF BLANK GIVE VALUE OF 100 276 | known_device_rssi=$(counter=0; \ 277 | avg_total=0; \ 278 | hcitool cc "$known_addr"; \ 279 | avg_total=""; \ 280 | for i in 1 2 3; \ 281 | do scan_result=$(hcitool rssi "$known_addr" 2>&1); \ 282 | scan_result=${scan_result//[^0-9]/}; \ 283 | scan_result=${scan_result:-99}; \ 284 | [[ "$scan_result" == "0" ]] && scan_result=99; \ 285 | counter=$((counter+1)); \ 286 | avg_total=$((avg_total + scan_result )); \ 287 | sleep 0.5; \ 288 | done; \ 289 | printf "%s" "$(( avg_total / counter ))") 290 | 291 | #PUBLISH MESSAGE TO RSSI SENSOR 292 | publish_rssi_message \ 293 | "$known_addr" \ 294 | "-$known_device_rssi" 295 | 296 | #REPORT 297 | $PREF_VERBOSE_LOGGING && log "${CYAN}[CMD-RSSI] ${NC}$known_addr ${GREEN}$cmd ${NC}RSSI: -$known_device_rssi dBm ${NC}" 298 | 299 | #SET RSSI LOG 300 | rssi_log[$known_addr]="$known_device_rssi" 301 | fi 302 | done 303 | } 304 | 305 | # ---------------------------------------------------------------------------------------- 306 | # ASSEMBLE SCAN LISTS 307 | # ---------------------------------------------------------------------------------------- 308 | 309 | scannable_devices_with_state () { 310 | 311 | #DEFINE LOCAL VARS 312 | local return_list 313 | local timestamp 314 | local scan_state 315 | local scan_type_diff 316 | local this_state 317 | local last_scan 318 | local time_diff 319 | 320 | #SET VALUES AFTER DECLARATION 321 | timestamp=$(date +%s) 322 | scan_state="$1" 323 | 324 | #FIRST, TEST IF WE HAVE DONE THIS TYPE OF SCAN TOO RECENTLY 325 | if [ "$scan_state" == "1" ]; then 326 | #SCAN FOR DEPARTED DEVICES 327 | scan_type_diff=$((timestamp - last_depart_scan)) 328 | elif [ "$scan_state" == "0" ]; then 329 | #SCAN FOR ARRIVED DEVICES 330 | scan_type_diff=$((timestamp - last_arrival_scan)) 331 | fi 332 | 333 | #REJECT IF WE SCANNED TO RECENTLY 334 | [ "$scan_type_diff" -lt "$PREF_MINIMUM_TIME_BETWEEN_SCANS" ] && return 0 335 | 336 | #SCAN ALL? SET THE DEFAULT SCAN STATE TO [X] 337 | scan_state=${scan_state:-2} 338 | 339 | #ITERATE THROUGH THE KNOWN DEVICES 340 | local known_addr 341 | for known_addr in "${known_static_addresses[@]^^}"; do 342 | 343 | #GET STATE; ONLY SCAN FOR DEVICES WITH SPECIFIC STATE 344 | this_state="${known_public_device_log[$known_addr]}" 345 | 346 | #IF WE HAVE NEVER SCANNED THIS DEVICE BEFORE, WE MARK AS 347 | #SCAN STATE [X]; THIS ALLOWS A FIRST SCAN TO PROGRESS TO 348 | #COMPLETION FOR ALL DEVICES 349 | this_state=${this_state:-3} 350 | 351 | #FIND LAST TIME THIS DEVICE WAS SCANNED 352 | last_scan="${known_static_device_scan_log[$known_addr]}" 353 | time_diff=$((timestamp - last_scan)) 354 | 355 | #SCAN IF DEVICE HAS NOT BEEN SCANNED 356 | #WITHIN LAST [X] SECONDS 357 | if [ "$time_diff" -gt "$PREF_MINIMUM_TIME_BETWEEN_SCANS" ]; then 358 | 359 | #TEST IF THIS DEVICE MATCHES THE TARGET SCAN STATE 360 | if [ "$this_state" == "$scan_state" ]; then 361 | #ASSEMBLE LIST OF DEVICES TO SCAN 362 | return_list="$return_list $this_state$known_addr" 363 | 364 | elif [ "$this_state" == "2" ] || [ "$this_state" == "3" ]; then 365 | 366 | #SCAN FOR ALL DEVICES THAT HAVEN'T BEEN RECENTLY SCANNED; 367 | #PRESUME DEVICE IS ABSENT 368 | return_list="$return_list $this_state$known_addr" 369 | fi 370 | fi 371 | done 372 | 373 | #RETURN LIST, CLEANING FOR EXCESS SPACES OR STARTING WITH SPACES 374 | return_list=$(echo "$return_list" | sed 's/^ //gi;s/ $//gi;s/ */ /gi') 375 | 376 | #RETURN THE LIST 377 | echo "$return_list" 378 | } 379 | 380 | # ---------------------------------------------------------------------------------------- 381 | # SCAN FOR DEVICES 382 | # ---------------------------------------------------------------------------------------- 383 | 384 | perform_complete_scan () { 385 | #IF WE DO NOT RECEIVE A SCAN LIST, THEN RETURN 0 386 | if [ -z "$1" ]; then 387 | #log "${GREEN}[CMD-INFO] ${GREEN}**** Rejected group scan. No devices in desired state. **** ${NC}" 388 | return 0 389 | fi 390 | 391 | #REPEAT THROUGH ALL DEVICES THREE TIMES, THEN RETURN 392 | local repetitions=2 393 | [ -n "$2" ] && repetitions="$2" 394 | [ "$repetitions" -lt "1" ] && repetitions=1 395 | 396 | #PRE 397 | local previous_state=0 398 | [ -n "$3" ] && previous_state="$3" 399 | 400 | #SCAN TYPE 401 | local transition_type="arrival" 402 | [ "$previous_state" == "1" ] && transition_type="departure" 403 | 404 | #INTERATION VARIABLES 405 | local devices="$1" 406 | local devices_next="$devices" 407 | local scan_start="" 408 | local scan_duration="" 409 | local should_report="" 410 | local manufacturer="Unknown" 411 | local has_requested_collaborative_depart_scan=false 412 | 413 | #LOG START OF DEVICE SCAN 414 | $PREF_MQTT_REPORT_SCAN_MESSAGES && publish_cooperative_scan_message "$transition_type/start" 415 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INFO] ${GREEN}**** started $transition_type scan [x$repetitions max rep] **** ${NC}" 416 | 417 | #ITERATE THROUGH THE KNOWN DEVICES 418 | local repetition 419 | for repetition in $(seq 1 $repetitions); do 420 | 421 | #SET DONE TO MAIN PIPE 422 | printf "%s\n" "DONE" > main_pipe 423 | 424 | #SET DEVICES 425 | devices="$devices_next" 426 | 427 | #ITERATE THROUGH THESE 428 | local known_addr 429 | local known_addr_stated 430 | local expected_name 431 | local name_raw 432 | local name 433 | local scan_end 434 | local scan_duration 435 | local percent_confidence 436 | local adjusted_delay 437 | 438 | for known_addr_stated in $devices; do 439 | 440 | #EXTRACT KNOWN ADDRESS FROM STATE-PREFIXED KNOWN ADDRESS, IF PRESENT 441 | if [[ "$known_addr_stated" =~ .*[0-9A-Fa-f]{3}.* ]]; then 442 | #SET KNOWN ADDRESS 443 | known_addr=${known_addr_stated:1} 444 | 445 | #SET PREVIOUS STATE 446 | previous_state=${known_addr_stated:0:1} 447 | else 448 | #THIS ELEMENT OF THE ARRAY DOES NOT CONTAIN A STATE PREFIX; GO WITH GLOBAL 449 | #STATE SCAN TYPE 450 | known_addr=$known_addr_stated 451 | fi 452 | 453 | #DETERMINE MANUFACTUERE 454 | manufacturer="$(determine_manufacturer "$known_addr")" 455 | manufacturer=${manufacturer:-Unknown} 456 | 457 | #IN CASE WE HAVE A BLANK ADDRESS, FOR WHATEVER REASON 458 | [ -z "$known_addr" ] && continue 459 | 460 | #DETERMINE START OF SCAN 461 | scan_start="$(date +%s)" 462 | 463 | #GET LOCAL NAME 464 | expected_name="$(determine_name "$known_addr")" 465 | expected_name=${expected_name:-Unknown} 466 | 467 | #DEBUG LOGGING 468 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SCAN] ${GREEN}(No. $repetition)${NC} $known_addr $transition_type? ${NC}" 469 | 470 | #PERFORM NAME SCAN FROM HCI TOOL. THE HCITOOL CMD 0X1 0X0019 IS POSSIBLE, BUT HCITOOL NAME 471 | #SCAN PERFORMS VERIFICATIONS THAT REDUCE FALSE NEGATIVES. 472 | 473 | #L2SCAN MAY INVOLVE LESS INTERFERENCE 474 | name_raw=$(hcitool -i "$PREF_HCI_DEVICE" name "$known_addr" 2>/dev/null) 475 | name=$(echo "$name_raw" | grep -ivE 'input/output error|invalid device|invalid|error|network') 476 | 477 | #COLLECT STATISTICS ABOUT THE SCAN 478 | scan_end="$(date +%s)" 479 | scan_duration=$((scan_end - scan_start)) 480 | 481 | #MARK THE ADDRESS AS SCANNED SO THAT IT CAN BE LOGGED ON THE MAIN PIPE 482 | printf "%s\n" "SCAN$known_addr" > main_pipe & 483 | 484 | #IF STATUS CHANGES TO PRESENT FROM NOT PRESENT, REMOVE FROM VERIFICATIONS 485 | if [ -n "$name" ] && [ "$previous_state" == "0" ]; then 486 | 487 | #PUSH TO MAIN POPE 488 | printf "%s\n" "NAME$known_addr|$name" > main_pipe 489 | 490 | #DEVICE FOUND; IS IT CHANGED? IF SO, REPORT 491 | publish_presence_message \ 492 | "id=$known_addr" \ 493 | "confidence=100" \ 494 | "name=$expected_name" \ 495 | "manufacturer=$manufacturer" \ 496 | "type=KNOWN_MAC" 497 | 498 | #REMOVE FROM SCAN 499 | devices_next=$(echo "$devices_next" | sed "s/$known_addr_stated//gi;s/ */ /gi") 500 | 501 | elif [ -n "$name" ] && [ "$previous_state" == "3" ]; then 502 | #HERE, WE HAVE FOUND A DEVICE FOR THE FIRST TIME 503 | devices_next=$(echo "$devices_next" | sed "s/$known_addr_stated//gi;s/ */ /gi") 504 | 505 | #NEED TO UPDATE STATE TO MAIN THREAD 506 | printf "%s\n" "NAME$known_addr|$name" > main_pipe 507 | 508 | #NEVER SEEN THIS DEVICE; NEED TO PUBLISH STATE MESSAGE 509 | publish_presence_message \ 510 | "id=$known_addr" "confidence=100" \ 511 | "name=$expected_name" \ 512 | "manufacturer=$manufacturer" \ 513 | "type=KNOWN_MAC" 514 | 515 | #COOPERATIVE SCAN ON RESTART 516 | $PREF_TRIGGER_MODE_REPORT_OUT && publish_cooperative_scan_message "arrive" 517 | 518 | 519 | elif [ -n "$name" ] && [ "$previous_state" == "1" ]; then 520 | 521 | #THIS DEVICE IS STILL PRESENT; REMOVE FROM VERIFICATIONS 522 | devices_next=$(echo "$devices_next" | sed "s/$known_addr_stated//gi;s/ */ /gi") 523 | 524 | #NEED TO REPORT? 525 | if [[ $should_report =~ .*$known_addr.* ]] || [ "$PREF_REPORT_ALL_MODE" == true ] ; then 526 | #REPORT PRESENCE 527 | publish_presence_message \ 528 | "id=$known_addr" \ 529 | "confidence=100" \ 530 | "name=$expected_name" \ 531 | "manufacturer=$manufacturer" \ 532 | "type=KNOWN_MAC" 533 | fi 534 | fi 535 | 536 | #SHOULD WE REPORT A DROP IN CONFIDENCE? 537 | if [ -z "$name" ] && [ "$previous_state" == "1" ]; then 538 | 539 | #CALCULATE PERCENT CONFIDENCE 540 | percent_confidence=$(echo "scale=1; ($repetitions - $repetition + 1) / $repetitions * 90" | bc ) 541 | 542 | #FALLBACK TO REMOVE DECIMAL AND PRINT INTEGER ONLY 543 | percent_confidence=${percent_confidence%.*} 544 | 545 | #ONLY PUBLISH COOPERATIVE SCAN MODE IF WE ARE NOT IN TRIGGER MODE 546 | #TRIGGER ONLY MODE DOES NOT SEND COOPERATIVE MESSAGES 547 | if [ "$has_requested_collaborative_depart_scan" == false ]; then 548 | #SEND THE MESSAGE IF APPROPRIATE 549 | if [ "$percent_confidence" -lt "$PREF_COOPERATIVE_SCAN_THRESHOLD" ] && $PREF_TRIGGER_MODE_REPORT_OUT; then 550 | has_requested_collaborative_depart_scan=true 551 | publish_cooperative_scan_message "depart" 552 | fi 553 | fi 554 | 555 | #REPORT PRESENCE OF DEVICE 556 | publish_presence_message \ 557 | "id=$known_addr" \ 558 | "confidence=$percent_confidence" \ 559 | "name=$expected_name" \ 560 | "manufacturer=$manufacturer" \ 561 | "type=KNOWN_MAC" 562 | 563 | #IF WE DO FIND A NAME LATER, WE SHOULD REPORT OUT 564 | should_report="$should_report$known_addr" 565 | 566 | elif [ -z "$name" ] && [ "$previous_state" == "3" ]; then 567 | 568 | #NEVER SEEN THIS DEVICE; NEED TO PUBLISH STATE MESSAGE 569 | publish_presence_message \ 570 | "id=$known_addr" \ 571 | "confidence=0" \ 572 | "name=$expected_name" \ 573 | "manufacturer=$manufacturer" \ 574 | "type=KNOWN_MAC" 575 | 576 | #PUBLISH MESSAGE TO RSSI SENSOR 577 | publish_rssi_message \ 578 | "$known_addr" \ 579 | "-99" 580 | 581 | #NREMOVE FROM THE SCAN LIST TO THE MAIN BECAUSE THIS IS A BOOT UP 582 | devices_next=$(echo "$devices_next" | sed "s/$known_addr_stated//gi;s/ */ /gi") 583 | 584 | #PUBLISH A NOT PRESENT TO THE NAME PIPE 585 | printf "%s\n" "NAME$known_addr|" > main_pipe 586 | 587 | #COOPERATIVE SCAN ON RESTART 588 | $PREF_TRIGGER_MODE_REPORT_OUT && publish_cooperative_scan_message "depart" 589 | 590 | elif [ -z "$name" ] && [ "$previous_state" == "0" ]; then 591 | 592 | if [ "$PREF_REPORT_ALL_MODE" == true ] ; then 593 | #REPORT PRESENCE 594 | publish_presence_message \ 595 | "id=$known_addr" \ 596 | "confidence=0" \ 597 | "name=$expected_name" \ 598 | "manufacturer=$manufacturer" \ 599 | "type=KNOWN_MAC" 600 | 601 | #PUBLISH MESSAGE TO RSSI SENSOR 602 | publish_rssi_message \ 603 | "$known_addr" \ 604 | "-99" 605 | 606 | fi 607 | fi 608 | 609 | #IF WE HAVE NO MORE DEVICES TO SCAN, IMMEDIATELY RETURN 610 | [ -z "$devices_next" ] && break 611 | 612 | #TO PREVENT HARDWARE PROBLEMS 613 | if [ "$scan_duration" -lt "$PREF_INTERSCAN_DELAY" ]; then 614 | adjusted_delay="$((PREF_INTERSCAN_DELAY - scan_duration))" 615 | 616 | if [ "$adjusted_delay" -gt "0" ]; then 617 | sleep "$adjusted_delay" 618 | else 619 | #DEFAULT MINIMUM SLEEP 620 | sleep "$PREF_INTERSCAN_DELAY" 621 | fi 622 | else 623 | #DEFAULT MINIMUM SLEEP 624 | sleep "$PREF_INTERSCAN_DELAY" 625 | fi 626 | done 627 | 628 | #ARE WE DONE WITH ALL DEVICES? 629 | [ -z "$devices_next" ] && break 630 | done 631 | 632 | #ANYHTING LEFT IN THE DEVICES GROUP IS NOT PRESENT 633 | local known_addr_stated 634 | local known_addr 635 | local expected_name 636 | for known_addr_stated in $devices_next; do 637 | #EXTRACT KNOWN ADDRESS FROM STATE-PREFIXED KNOWN ADDRESS, IF PRESENT 638 | if [[ "$known_addr_stated" =~ .*[0-9A-Fa-f]{3}.* ]]; then 639 | #SET KNOWN ADDRESS 640 | known_addr=${known_addr_stated:1} 641 | else 642 | #THIS ELEMENT OF THE ARRAY DOES NOT CONTAIN A STATE PREFIX 643 | known_addr=$known_addr_stated 644 | fi 645 | 646 | #PUBLISH MESSAGE 647 | if [ ! "$previous_state" == "0" ]; then 648 | expected_name="$(determine_name "$known_addr")" 649 | expected_name=${expected_name:-Unknown} 650 | 651 | #DETERMINE MANUFACTUERE 652 | manufacturer="$(determine_manufacturer "$known_addr")" 653 | manufacturer=${manufacturer:-Unknown} 654 | 655 | #PUBLISH PRESENCE METHOD 656 | publish_presence_message \ 657 | "id=$known_addr" \ 658 | "confidence=0" \ 659 | "name=$expected_name" \ 660 | "manufacturer=$manufacturer" \ 661 | "type=KNOWN_MAC" 662 | 663 | #PUBLISH MESSAGE TO RSSI SENSOR 664 | publish_rssi_message \ 665 | "$known_addr" \ 666 | "-99" 667 | fi 668 | 669 | printf "%s\n" "NAME$known_addr|" > main_pipe 670 | done 671 | 672 | 673 | #SET DONE TO MAIN PIPE 674 | printf "%s\n" "DONE" > main_pipe 675 | 676 | #GROUP SCAN FINISHED 677 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INFO] ${GREEN}**** completed $transition_type scan **** ${NC}" 678 | 679 | #PUBLISH END OF COOPERATIVE SCAN 680 | $PREF_MQTT_REPORT_SCAN_MESSAGES && publish_cooperative_scan_message "$transition_type/end" 681 | } 682 | 683 | # ---------------------------------------------------------------------------------------- 684 | # SCAN TYPE FUNCTIONS 685 | # ---------------------------------------------------------------------------------------- 686 | 687 | perform_departure_scan () { 688 | 689 | #SET SCAN TYPE 690 | local depart_list 691 | depart_list=$(scannable_devices_with_state 1) 692 | 693 | #LOCAL SCAN ACTIVE VARIABLE 694 | local scan_active 695 | scan_active=true 696 | 697 | #SCAN ACTIVE? 698 | kill -0 "$scan_pid" >/dev/null 2>&1 && scan_active=true || scan_active=false 699 | 700 | #ONLY ASSEMBLE IF WE NEED TO SCAN FOR ARRIVAL 701 | if [ "$scan_active" == false ] ; then 702 | 703 | #ADD A FLAG TO SCAN FOR 704 | [ -n "$depart_list" ] && printf "%s\n" "BEXP" > main_pipe & 705 | 706 | #ONCE THE LIST IS ESTABLISHED, TRIGGER SCAN OF THESE DEVICES IN THE BACKGROUND 707 | perform_complete_scan "$depart_list" "$PREF_DEPART_SCAN_ATTEMPTS" "1" & 708 | disown "$!" 709 | 710 | scan_pid=$! 711 | scan_type=1 712 | else 713 | #HERE A DEPART SCAN IS ACTIVE; ENQUEUE ANOTHER DEPART SCAN AFTER DELAY 714 | [ "$scan_type" == "0" ] && sleep 5 && printf "%s\n" "ENQUdepart" > main_pipe & 715 | fi 716 | } 717 | 718 | perform_arrival_scan () { 719 | #SET SCAN TYPE 720 | local arrive_list 721 | arrive_list=$(scannable_devices_with_state 0) 722 | 723 | #LOCAL SCAN ACTIVE VARIABLE 724 | local scan_active 725 | scan_active=true 726 | 727 | #SCAN ACTIVE? 728 | kill -0 "$scan_pid" >/dev/null 2>&1 && scan_active=true || scan_active=false 729 | 730 | #ONLY ASSEMBLE IF WE NEED TO SCAN FOR ARRIVAL 731 | if [ "$scan_active" == false ] ; then 732 | 733 | #FIRST SCAN IS DEAD 734 | first_arrive_scan=false 735 | 736 | #ONCE THE LIST IS ESTABLISHED, TRIGGER SCAN OF THESE DEVICES IN THE BACKGROUND 737 | perform_complete_scan "$arrive_list" "$PREF_ARRIVAL_SCAN_ATTEMPTS" "0" & 738 | disown "$!" 739 | 740 | scan_pid=$! 741 | scan_type=0 742 | else 743 | #HERE A DEPART SCAN IS ACTIVE; ENQUEUE ANOTHER DEPART SCAN AFTER DELAY 744 | [ "$scan_type" == "1" ] && sleep 5 && printf "%s\n" "ENQUarrive" > main_pipe & 745 | fi 746 | } 747 | 748 | # ---------------------------------------------------------------------------------------- 749 | # NAME DETERMINATIONS 750 | # ---------------------------------------------------------------------------------------- 751 | 752 | determine_name () { 753 | 754 | #SET DATA 755 | local address 756 | address="$1" 757 | 758 | #RETURN ADDRESS 759 | [ -z "$address" ] && return 0 760 | 761 | #ALTERNATIVE ADDRESS 762 | local alternate_address 763 | alternate_address="$2" 764 | alternate_address=${alternate_address:-Unknown} 765 | 766 | #IF IS NEW AND IS PUBLIC, SHOULD CHECK FOR NAME 767 | local expected_name 768 | expected_name="${known_public_device_name[$address]}" 769 | 770 | #ALTERNATE NAME? 771 | [ -z "$expected_name" ] && expected_name="${known_public_device_name[$alternate_address]}" 772 | 773 | #FIND PERMANENT DEVICE NAME OF PUBLIC DEVICE 774 | if [ -z "$expected_name" ]; then 775 | 776 | #CHECK CACHE 777 | expected_name=$(grep "$address" < "$base_directory/.public_name_cache" | awk -F "\t" '{print $2}') 778 | 779 | #IF CACHE DOES NOT EXIST, TRY TO SCAN 780 | if [ -z "$expected_name" ]; then 781 | 782 | #DOES SCAN PROCESS CURRENTLY EXIST? 783 | kill -0 "$scan_pid" >/dev/null 2>&1 && scan_active=true || scan_active=false 784 | 785 | #ONLY SCAN IF WE ARE NOT OTHERWISE SCANNING; NAME FOR THIS DEVICE IS NOT IMPORTANT 786 | if [ "$scan_active" == false ] ; then 787 | 788 | #FIND NAME OF THIS DEVICE 789 | expected_name=$(hcitool -i "$PREF_HCI_DEVICE" name "$address" 2>/dev/null) 790 | 791 | #IS THE EXPECTED NAME BLANK? 792 | if [ -n "$expected_name" ]; then 793 | 794 | #ADD TO SESSION ARRAY 795 | known_public_device_name[$address]="$expected_name" 796 | 797 | #ADD TO CACHE 798 | echo "$address $expected_name" >> .public_name_cache 799 | else 800 | #ADD TO CACHE TO PREVENT RE-SCANNING 801 | echo "$address Undeterminable" >> .public_name_cache 802 | 803 | fi 804 | fi 805 | else 806 | #WE HAVE A CACHED NAME, ADD IT BACK TO THE PUBLIC DEVICE ARRAY 807 | known_public_device_name[$address]="$expected_name" 808 | fi 809 | fi 810 | 811 | printf "%s\n" "$expected_name" 812 | } 813 | 814 | # ---------------------------------------------------------------------------------------- 815 | # BACKGROUND PROCESSES 816 | # ---------------------------------------------------------------------------------------- 817 | 818 | #SET LOG 819 | (rm .pids) 2>&1 1>/dev/null 820 | 821 | log_listener & 822 | listener_pid="$!" 823 | echo "> log listener pid = $listener_pid" >> .pids 824 | $PREF_VERBOSE_LOGGING && echo "> log listener pid = $listener_pid" 825 | disown "$listener_pid" 826 | 827 | btle_scanner & 828 | btle_scan_pid="$!" 829 | echo "> btle scan pid = $btle_scan_pid" >> .pids 830 | $PREF_VERBOSE_LOGGING && echo "> btle scan pid = $btle_scan_pid" 831 | disown "$btle_scan_pid" 832 | 833 | btle_text_listener & 834 | btle_text_pid="$!" 835 | echo "> btle text pid = $btle_text_pid" >> .pids 836 | $PREF_VERBOSE_LOGGING && echo "> btle text pid = $btle_text_pid" 837 | disown "$btle_text_pid" 838 | 839 | btle_listener & 840 | btle_listener_pid="$!" 841 | echo "> btle listener pid = $btle_listener_pid" >> .pids 842 | $PREF_VERBOSE_LOGGING && echo "> btle listener pid = $btle_listener_pid" 843 | disown "$btle_listener_pid" 844 | 845 | mqtt_listener & 846 | mqtt_pid="$!" 847 | echo "> mqtt listener pid = $mqtt_pid" >> .pids 848 | $PREF_VERBOSE_LOGGING && echo "> mqtt listener pid = $mqtt_pid" 849 | disown "$mqtt_pid" 850 | 851 | btle_packet_listener & 852 | btle_packet_listener_pid="$!" 853 | echo "> packet listener pid = $btle_packet_listener_pid" >> .pids 854 | $PREF_VERBOSE_LOGGING && echo "> packet listener pid = $btle_packet_listener_pid" 855 | disown "$btle_packet_listener_pid" 856 | 857 | beacon_database_expiration_trigger & 858 | beacon_database_expiration_trigger_pid="$!" 859 | echo "> beacon database time trigger pid = $beacon_database_expiration_trigger_pid" >> .pids 860 | $PREF_VERBOSE_LOGGING && echo "> beacon database time trigger pid = $beacon_database_expiration_trigger_pid" 861 | disown "$beacon_database_expiration_trigger_pid" 862 | 863 | # ---------------------------------------------------------------------------------------- 864 | # MAIN LOOPS. INFINITE LOOP CONTINUES, NAMED PIPE IS READ INTO SECONDARY LOOP 865 | # ---------------------------------------------------------------------------------------- 866 | 867 | #MAIN LOOP 868 | while true; do 869 | 870 | #READ FROM THE MAIN PIPE 871 | while read -r event; do 872 | 873 | #DIVIDE EVENT MESSAGE INTO TYPE AND DATA 874 | cmd="${event:0:4}" 875 | data="${event:4}" 876 | timestamp=$(date +%s) 877 | uptime=$((timestamp - now)) 878 | 879 | #FLAGS TO DETERMINE FRESHNESS OF DATA 880 | is_new=false 881 | should_update=false 882 | did_change=false 883 | is_apple_beacon=false 884 | 885 | #CLEAR DATA IN NONLOCAL VARS 886 | manufacturer="unknown" 887 | current_associated_beacon_mac_address="" 888 | name="" 889 | expected_name="" 890 | mac="" 891 | rssi="" 892 | adv_data="" 893 | resolvable="" 894 | pdu_header="" 895 | power="" 896 | major="" 897 | minor="" 898 | uuid="" 899 | beacon_type="GENERIC_BEACON" 900 | beacon_last_seen="" 901 | key_last_seen="" 902 | uuid_reference="" 903 | last_appearance="" 904 | beacon_uuid_key="" 905 | instruction_timestamp="" 906 | instruction_delay="" 907 | observation_made=false 908 | most_recent_beacon="" 909 | observed_max_advertisement_interval="" 910 | temp_observation="" 911 | device_state="" 912 | 913 | #PROCEED BASED ON COMMAND TYPE 914 | if [ "$cmd" == "ENQU" ] && [ "$uptime" -gt "$PREF_STARTUP_SETTLE_TIME" ]; then 915 | 916 | #WE HAVE AN ENQUEUED OPPOSITE SCAN; NEED TO TRIGGER THAT SCAN 917 | if [ "$data" == "arrive" ]; then 918 | 919 | #LOG 920 | $PREF_VERBOSE_LOGGING && log "${GREEN}[ENQ-ARR] ${NC}Enqueued arrival scan triggered.${NC}" 921 | 922 | #WAIT 5 SECONDS 923 | sleep 5 924 | 925 | #TRIGGER 926 | perform_arrival_scan 927 | 928 | elif [ "$data" == "depart" ]; then 929 | #LOG 930 | $PREF_VERBOSE_LOGGING && log "${GREEN}[ENQ-DEP] ${NC}Enqueued depart scan triggered.${NC}" 931 | 932 | #WAIT 5 SECONDS 933 | sleep 5 934 | 935 | #TRIGGER 936 | perform_departure_scan 937 | fi 938 | 939 | elif [ "$cmd" == "RAND" ]; then 940 | #PARSE RECEIVED DATA 941 | mac=$(echo "$data" | awk -F "|" '{print $1}') 942 | pdu_header=$(echo "$data" | awk -F "|" '{print $2}') 943 | name=$(echo "$data" | awk -F "|" '{print $3}') 944 | rssi=$(echo "$data" | awk -F "|" '{print $4}') 945 | adv_data=$(echo "$data" | awk -F "|" '{print $5}') 946 | manufacturer=$(echo "$data" | awk -F "|" '{print $6}') 947 | device_type=$(echo "$data" | awk -F "|" '{print $7}') 948 | flags=$(echo "$data" | awk -F "|" '{print $8}') 949 | oem_data=$(echo "$data" | awk -F "|" '{print $9}') 950 | instruction_timestamp=$(echo "$data" | awk -F "|" '{print $10}') 951 | resolvable=$(echo "$data" | awk -F "|" '{print $11}') 952 | hex_data=$(echo "$data" | awk -F "|" '{print $12}') 953 | 954 | #FIND DELAY BASED ON INSTRUCTINO TIMESTAMP 955 | instruction_delay=$((timestamp - instruction_timestamp)) 956 | 957 | #GET LAST RSSI 958 | rssi_latest="${rssi_log[$mac]}" 959 | 960 | #IF WE HAVE A NAME; UNSEAT FROM RANDOM AND ADD TO STATIC 961 | #THIS IS A BIT OF A FUDGE, A RANDOM DEVICE WITH A LOCAL 962 | #NAME IS TRACKABLE, SO IT'S UNLIKELY THAT ANY CONSUMER 963 | #ELECTRONIC DEVICE OR CELL PHONE IS ASSOCIATED WITH THIS 964 | #ADDRESS. CONSIDER THE ADDRESS AS A STATIC ADDRESS 965 | 966 | #ALSO NEED TO CHECK WHETHER THE RANDOM BROADCAST 967 | #IS INCLUDED IN THE KNOWN DEVICES LOG... 968 | 969 | if [ -n "${public_device_log[$mac]}" ]; then 970 | 971 | #GET INTERVAL SINCE LAST SEEN 972 | last_appearance=${public_device_log[$mac]:-$timestamp} 973 | if [ "$observation_made" == false ]; then 974 | observation_made=true 975 | temp_observation="" && temp_observation=$((((timestamp - last_appearance - 1 + PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) / PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) * PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP)) 976 | [ "$temp_observation" -gt "${advertisement_interval_observation[$mac]:-0}" ] && [ "$temp_observation" -gt "0" ] && [ "$temp_observation" -lt "300" ] && advertisement_interval_observation[$mac]=$temp_observation 977 | 978 | fi 979 | 980 | #IS THIS A NEW STATIC DEVICE? 981 | public_device_log[$mac]="$timestamp" 982 | [ -n "$rssi" ] && rssi_log[$mac]="$rssi" 983 | cmd="PUBL" 984 | 985 | #BEACON TYPE 986 | beacon_type="GENERIC_BEACON_PUBLIC" 987 | 988 | else 989 | #DO WE HAVE A NAME FOR THIS MAC ADDRESSS? 990 | #THAT IS NOT IN THE PUBLIC DEVICE ARRAY? 991 | expected_name="${known_public_device_name[$mac]}" 992 | 993 | #DOES THIS DEVICE HAVE A NAME? 994 | if [ -n "$name" ] || [ -n "$expected_name" ]; then 995 | #RESET COMMAND 996 | cmd="PUBL" 997 | unset "random_device_log[$mac]" 998 | 999 | #BEACON TYPE 1000 | beacon_type="GENERIC_BEACON_RANDOM" 1001 | 1002 | #SAVE THE NAME 1003 | known_public_device_name[$mac]="$name" 1004 | [ -n "$rssi" ] && rssi_log[$mac]="$rssi" 1005 | 1006 | #IS THIS A NEW STATIC DEVICE? 1007 | if [ -n "${public_device_log[$mac]}" ]; then 1008 | #GET INTERVAL SINCE LAST SEEN 1009 | last_appearance=${public_device_log[$mac]:-$timestamp} 1010 | if [ "$observation_made" == false ]; then 1011 | observation_made=true 1012 | temp_observation="" && temp_observation=$((((timestamp - last_appearance - 1 + PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) / PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) * PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP)) 1013 | [ "$temp_observation" -gt "${advertisement_interval_observation[$mac]:-0}" ] && [ "$temp_observation" -gt "0" ] && [ "$temp_observation" -lt "300" ] && advertisement_interval_observation[$mac]=$temp_observation 1014 | 1015 | fi 1016 | 1017 | else 1018 | is_new=true 1019 | fi 1020 | 1021 | public_device_log[$mac]="$timestamp" 1022 | 1023 | else 1024 | 1025 | #DATA IS RANDOM MAC Addr.; ADD TO LOG 1026 | [ -z "${random_device_log[$mac]}" ] && is_new=true 1027 | 1028 | #WHEN DOES THIS RANDOM BEACON EXPIRE? 1029 | last_appearance=${random_device_log[$mac]:-$timestamp} 1030 | if [ "$observation_made" == false ]; then 1031 | observation_made=true 1032 | temp_observation="" && temp_observation=$((((timestamp - last_appearance - 1 + PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) / PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) * PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP)) 1033 | [ "$temp_observation" -gt "${advertisement_interval_observation[$mac]:-0}" ] && [ "$temp_observation" -gt "0" ] && [ "$temp_observation" -lt "300" ] && advertisement_interval_observation[$mac]=$temp_observation 1034 | 1035 | fi 1036 | 1037 | #ONLY ADD THIS TO THE DEVICE LOG 1038 | random_device_log[$mac]="$timestamp" 1039 | [ -n "$rssi" ] && rssi_log[$mac]="$rssi" 1040 | fi 1041 | fi 1042 | 1043 | elif [ "$cmd" == "SCAN" ]; then 1044 | #SET MAC = DATA 1045 | mac=$data 1046 | 1047 | #ADD TO THE SCAN LOG 1048 | known_static_device_scan_log[$mac]=$(date +%s) 1049 | continue 1050 | 1051 | elif [ "$cmd" == "DONE" ]; then 1052 | 1053 | #SCAN MODE IS COMPLETE 1054 | scan_pid="" 1055 | 1056 | #SET LAST ARRIVAL OR DEPARTURE SCAN 1057 | [ "$scan_type" == "0" ] && last_arrival_scan=$(date +%s) 1058 | [ "$scan_type" == "1" ] && last_depart_scan=$(date +%s) 1059 | 1060 | scan_type="" 1061 | continue 1062 | 1063 | elif [ "$cmd" == "MQTT" ] && [ "$uptime" -gt "$PREF_STARTUP_SETTLE_TIME" ]; then 1064 | 1065 | #GET INSTRUCTION 1066 | topic_path_of_instruction="${data%%|*}" 1067 | data_of_instruction="${data##*|}" 1068 | 1069 | #IGNORE INSTRUCTION FROM SELF 1070 | if [[ ${data_of_instruction^^} =~ .*${mqtt_publisher_identity^^}.* ]] || [[ ${topic_path_of_instruction^^} =~ .*${mqtt_publisher_identity^^}.* ]]; then 1071 | continue 1072 | fi 1073 | 1074 | #GET THE TOPIC 1075 | mqtt_topic_branch=$(basename "$topic_path_of_instruction") 1076 | 1077 | #NORMALIZE TO UPPERCASE 1078 | mqtt_topic_branch=${mqtt_topic_branch^^} 1079 | 1080 | if [[ $mqtt_topic_branch =~ .*ARRIVE.* ]]; then 1081 | 1082 | #IGNORE OR PASS MQTT INSTRUCTION? 1083 | scan_type_diff=$((timestamp - last_arrival_scan)) 1084 | if [ "$scan_type_diff" -gt "$PREF_MINIMUM_TIME_BETWEEN_SCANS" ]; then 1085 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] arrive scan requested ${NC}" 1086 | perform_arrival_scan 1087 | else 1088 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${RED}fail mqtt${NC}] arrive scan rejected due to recent scan ${NC}" 1089 | fi 1090 | 1091 | elif [[ $mqtt_topic_branch =~ .*KNOWN\ DEVICE\ STATES.* ]]; then 1092 | 1093 | #SIMPLE STATUS MESSAGE FOR KNOWN 1094 | device_state="" 1095 | for addr in "${known_static_addresses[@]^^}"; do 1096 | #GET STATE; ONLY SCAN FOR DEVICES WITH SPECIFIC STATE 1097 | device_state="${known_public_device_log[$addr]}" 1098 | device_state=${device_state:-0} 1099 | 1100 | #SET TO CONFIDENCE RANGE 1101 | [ "$device_state" == "1" ] && device_state=100 1102 | 1103 | #SEND STATUS UPDATE 1104 | publish_presence_message \ 1105 | "id=$addr" \ 1106 | "confidence=$device_state" \ 1107 | "name=${known_public_device_name[$addr]}" \ 1108 | "type=KNOWN_MAC" 1109 | 1110 | done 1111 | 1112 | elif [[ $mqtt_topic_branch =~ .*ADD\ STATIC\ DEVICE.* ]] || [[ $mqtt_topic_branch =~ .*DELETE\ STATIC\ DEVICE.* ]]; then 1113 | 1114 | if [[ "${data_of_instruction^^}" =~ ([A-F0-9]{2}:){5}[A-F0-9]{2} ]]; then 1115 | #GET MAC ADDRESSES 1116 | mac="${BASH_REMATCH}" 1117 | if [ ! ${known_public_device_name[$mac]+true} ]; then 1118 | 1119 | #HERE, WE KNOW THAT WE HAVE A MAC ADDRESS AND A VALID INSTRUCTION 1120 | if [[ $mqtt_topic_branch =~ .*ADD\ STATIC\ DEVICE.* ]]; then 1121 | #WAS THERE A NAME HERE? 1122 | name=$(echo "$data_of_instruction" | tr "\\t" " " | sed 's/ */ /gi;s/#.\{0,\}//gi' | sed "s/$mac //gi;s/ */ /gi" ) 1123 | 1124 | #IF THE VALUE DOES NOT EXIST, USE THE KEY (MAC ADDRESS INSTEAD) 1125 | alias_value=${name//[^A-Za-z0-9]/_} 1126 | 1127 | #LOWERCASE 1128 | alias_value=${alias_value,,} 1129 | 1130 | #REMOVE FINAL UNDERSCORES SHOUDL THERE BE 1131 | alias_value=$(echo "$alias_value" | sed 's/[^0-9a-z]\{1,\}$//gi;s/^[^0-9a-z]\{1,\}//gi;s/__*/_/gi') 1132 | 1133 | #ADD TO KNOWN PUBLIC DEVICE ARRAY 1134 | known_public_device_name[$mac]="$name" 1135 | 1136 | #ESTABLISH ALIAS 1137 | [ -n "$mac" ] && [ -n "$alias_value" ] && mqtt_aliases[$mac]="$alias_value" 1138 | 1139 | #ADD TO KNOWN_STATIC_ADDRESSES FILE 1140 | echo "$mac ${name:-}" >> $PUB_CONFIG 1141 | 1142 | #UPDATE FROM STATIC ADDRESSES TOO 1143 | mapfile -t known_static_addresses < <(sed 's/#.\{0,\}//gi' < "$PUB_CONFIG" | awk '{print $1}' | grep -oiE "([0-9a-f]{2}:){5}[0-9a-f]{2}" ) 1144 | 1145 | #LOGGING 1146 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] new static device ${GREEN}$mac${NC} added with alias ${GREEN}${name:-none}${NC}" 1147 | 1148 | #PERFORM ARRIVAL SCAN FOR NEW DEVICE 1149 | perform_arrival_scan 1150 | fi 1151 | 1152 | else 1153 | 1154 | #ONLY PERFORM IF WE HAVE A DEVICE TO DELETE 1155 | if [[ $mqtt_topic_branch =~ .*DELETE\ STATIC\ DEVICE.* ]]; then 1156 | 1157 | #HERE, WE NOW THAT WE HAVE TO DELETE THE DEVICE WITH THE MAC ADDRESS 1158 | sed -i '/'"$mac"'/Id' $PUB_CONFIG 1159 | 1160 | #UNSET FROM MEMORY 1161 | unset "known_public_device_name[$mac]" 1162 | unset "mqtt_aliases[$mac]" 1163 | 1164 | #REMOVE FROM STATIC ADDRESSES TOO 1165 | mapfile -t known_static_addresses < <(sed 's/#.\{0,\}//gi' < "$PUB_CONFIG" | awk '{print $1}' | grep -oiE "([0-9a-f]{2}:){5}[0-9a-f]{2}" ) 1166 | 1167 | #LOGGING 1168 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] removed static device ${GREEN}$mac${NC}" 1169 | 1170 | #PERFORM DEPARTURE SCAN TO MAKE SURE THIS DEVICE IS GONE 1171 | perform_departure_scan 1172 | fi 1173 | fi 1174 | else 1175 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${RED}fail mqtt${NC}] new static device request did not contain a device address ${NC}" 1176 | fi 1177 | 1178 | elif [[ $mqtt_topic_branch =~ .*DEPART.* ]]; then 1179 | 1180 | #IGNORE OR PASS MQTT INSTRUCTION? 1181 | scan_type_diff=$((timestamp - last_depart_scan)) 1182 | if [ "$scan_type_diff" -gt "$PREF_MINIMUM_TIME_BETWEEN_SCANS" ]; then 1183 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] depart scan requested ${NC}" 1184 | perform_departure_scan 1185 | else 1186 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${RED}fail mqtt${NC}] depart scan rejected due to recent scan ${NC}" 1187 | fi 1188 | 1189 | elif [[ $mqtt_topic_branch =~ .*RSSI.* ]]; then 1190 | 1191 | #SCAN FOR RSSI 1192 | difference_last_rssi=$((timestamp - last_rssi_scan)) 1193 | 1194 | #ONLY EVER 5 MINUTES 1195 | if [ "$difference_last_rssi" -gt "100" ] || [ -z "$last_rssi_scan" ] ; then 1196 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] rssi update scan requested ${NC}" 1197 | connectable_present_devices 1198 | last_rssi_scan=$(date +%s) 1199 | else 1200 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${RED}fail mqtt${NC}] rssi update scan rejected due to recent scan ${NC}" 1201 | fi 1202 | 1203 | elif [[ $mqtt_topic_branch =~ .*RESTART.* ]]; then 1204 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] service restart requested ${NC}" 1205 | 1206 | #RESTART SYSTEM 1207 | systemctl restart monitor.service 1208 | 1209 | #exit 1210 | exit 0 1211 | 1212 | elif [[ $mqtt_topic_branch =~ .*ECHO.* ]] && [[ -z "$data_of_instruction" ]]; then 1213 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] echo ${NC}" 1214 | 1215 | mqtt_echo 1216 | 1217 | elif [[ $mqtt_topic_branch =~ .*UPDATEBETA.* ]]; then 1218 | 1219 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] beta update requested ${NC}" 1220 | 1221 | #GIT FETCH 1222 | git fetch 1223 | 1224 | #GIT FETCH 1225 | git checkout beta 1226 | 1227 | #GIT PULL 1228 | git pull 1229 | 1230 | #RESTART SYSTEM 1231 | systemctl restart monitor.service 1232 | 1233 | #exit 1234 | exit 0 1235 | 1236 | elif [[ $mqtt_topic_branch =~ .*UPDATE.* ]]; then 1237 | 1238 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-INST] ${NC}[${GREEN}pass mqtt${NC}] update requested ${NC}" 1239 | 1240 | #GIT FETCH 1241 | git fetch 1242 | 1243 | #GIT FETCH 1244 | git checkout master 1245 | 1246 | #GIT PULL 1247 | git pull 1248 | 1249 | #RESTART SYSTEM 1250 | systemctl restart monitor.service 1251 | 1252 | #exit 1253 | exit 0 1254 | 1255 | elif [[ ${mqtt_topic_branch^^} =~ .*START.* ]] || [[ ${mqtt_topic_branch^^} =~ .*END.* ]] || [[ ${mqtt_topic_branch^^} =~ .*STATUS.* ]]; then 1256 | #IGNORE ERRORS 1257 | #$PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SCAN] ${NC}[${RED}ignore mqtt${NC}] ${BLUE}topic:${NC} $topic_path_of_instruction ${BLUE}data:${NC} $data_of_instruction${NC}" 1258 | 1259 | continue 1260 | 1261 | elif [[ ${mqtt_topic_branch^^} =~ .*[0-9A-F:-]{2,}.* ]]; then 1262 | #LOG THE OUTPU 1263 | #log "${GREEN}[CMD-INST] ${NC}[${ORANGE}ignored mqtt${NC}] ${BLUE}topic:${NC} $topic_path_of_instruction ${BLUE}data:${NC} $data_of_instruction${NC}" 1264 | continue 1265 | 1266 | else 1267 | 1268 | #LOG THE OUTPU 1269 | #log "${GREEN}[CMD-INST] ${NC}[${RED}fail mqtt${NC}] ${BLUE}topic:${NC} $topic_path_of_instruction ${BLUE}data:${NC} $data_of_instruction${NC}" 1270 | 1271 | #DO A LITTLE SPELL CHECKING HERE 1272 | if [[ ${mqtt_topic_branch^^} =~ .*ARR.* ]]; then 1273 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SUGG] ${NC}[${RED}fail mqtt${NC}] did you mean .../scan/${RED}arrive${NC}? ${NC}" 1274 | elif [[ ${mqtt_topic_branch^^} =~ .*DEP.* ]]; then 1275 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SUGG] ${NC}[${RED}fail mqtt${NC}] did you mean .../scan/${RED}depart${NC}? ${NC}" 1276 | elif [[ ${mqtt_topic_branch^^} =~ .*BET.* ]]; then 1277 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SUGG] ${NC}[${RED}fail mqtt${NC}] did you mean .../scan/${RED}updatebeta${NC}? ${NC}" 1278 | elif [[ ${mqtt_topic_branch^^} =~ .*RSS.* ]]; then 1279 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SUGG] ${NC}[${RED}fail mqtt${NC}] did you mean .../scan/${RED}rssi${NC}? ${NC}" 1280 | elif [[ ${mqtt_topic_branch^^} =~ .*STAR.* ]]; then 1281 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SUGG] ${NC}[${RED}fail mqtt${NC}] did you mean .../scan/${RED}restart${NC}? ${NC}" 1282 | elif [[ ${mqtt_topic_branch^^} =~ .*DAT.* ]]; then 1283 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SUGG] ${NC}[${RED}fail mqtt${NC}] did you mean .../scan/${RED}update${NC} or .../scan/${RED}updatebeta${NC}? ${NC}" 1284 | elif [[ ${mqtt_topic_branch^^} =~ .*ECH.* ]]; then 1285 | $PREF_VERBOSE_LOGGING && log "${GREEN}[CMD-SUGG] ${NC}[${RED}fail mqtt${NC}] did you mean .../scan/${RED}echo${NC} or .../scan/${RED}updatebeta${NC}? ${NC}" 1286 | fi 1287 | 1288 | fi 1289 | 1290 | elif [ "$cmd" == "BOFF" ] || [ "$cmd" == "BEXP" ]; then 1291 | 1292 | [ "$uptime" -lt "$PREF_STARTUP_SETTLE_TIME" ] && continue 1293 | 1294 | #ONLY WHEN BLUETOOTH IS OFF DO WE ATTEMPT TO SCAN FOR RSSI OF KNOWN/CONNECTED DEVICES 1295 | if [ "$cmd" == "BOFF" ]; then 1296 | #FIND RSSI OF KNOWN DEVICES PREVIOUSLY CONNECTED WHILE HICTOOL IS NOT 1297 | #SCANNING 1298 | difference_last_rssi=$((timestamp - last_rssi_scan)) 1299 | 1300 | #ONLY EVER 5 MINUTES 1301 | if [ "$difference_last_rssi" -gt "90" ] || [ -z "$last_rssi_scan" ] ; then 1302 | connectable_present_devices 1303 | last_rssi_scan=$(date +%s) 1304 | fi 1305 | fi 1306 | 1307 | #RETURN PERIODIC SCAN MODE 1308 | if [ "$PREF_PERIODIC_MODE" == true ]; then 1309 | 1310 | #SCANNED RECENTLY? 1311 | duration_since_arrival_scan=$((timestamp - last_arrival_scan)) 1312 | 1313 | #CALCULATE DEPARTURE 1314 | duration_since_depart_scan=$((timestamp - last_depart_scan)) 1315 | 1316 | if [ "$duration_since_depart_scan" -gt "$PREF_DEPART_SCAN_INTERVAL" ]; then 1317 | 1318 | perform_departure_scan 1319 | 1320 | elif [ "$duration_since_arrival_scan" -gt "$PREF_ARRIVE_SCAN_INTERVAL" ]; then 1321 | 1322 | perform_arrival_scan 1323 | fi 1324 | fi 1325 | 1326 | #********************************************************************** 1327 | # 1328 | # 1329 | # THE FOLLOWING LOOPS CLEAR CACHES OF ALREADY SEEN DEVICES BASED 1330 | # ON APPROPRIATE TIMEOUT PERIODS FOR THOSE DEVICES. 1331 | # 1332 | # 1333 | #********************************************************************** 1334 | 1335 | #DID ANY DEVICE EXPIRE? 1336 | should_scan=false 1337 | last_seen="" 1338 | key="" 1339 | beacon_specific_expiration_interval="" 1340 | 1341 | #PURGE OLD KEYS FROM THE RANDOM DEVICE LOG 1342 | for key in "${!random_device_log[@]}"; do 1343 | 1344 | #FIND WHEN THIS KEYW AS LAST SEEN? 1345 | last_seen="${random_device_log[$key]}" 1346 | 1347 | #DETERMINE THE LAST TIME THIS MAC WAS LOGGED 1348 | difference=$((timestamp - last_seen)) 1349 | 1350 | #FIND THE EXPIRATION INTERVAL FOR THIS PARTICULAR BEACON 1351 | beacon_specific_expiration_interval="${advertisement_interval_observation[$key]}" 1352 | beacon_specific_expiration_interval=$(( beacon_specific_expiration_interval * PREF_DEPART_SCAN_ATTEMPTS )) 1353 | 1354 | #SET EXPIRATION 1355 | beacon_specific_expiration_interval=$(( beacon_specific_expiration_interval > 45 && beacon_specific_expiration_interval < PREF_RANDOM_DEVICE_EXPIRATION_INTERVAL ? beacon_specific_expiration_interval : PREF_RANDOM_DEVICE_EXPIRATION_INTERVAL )) 1356 | 1357 | #CONTINUE IF DEVICE HAS NOT BEEN SEEN OR DATE IS CORRUPT 1358 | [ -z "$last_seen" ] && continue 1359 | 1360 | #IS THIS A BEACON?? 1361 | if [ "$difference" -gt "$beacon_specific_expiration_interval" ]; then 1362 | 1363 | #REMOVE FROM RANDOM DEVICE LOG 1364 | unset "random_device_log[$key]" 1365 | unset "rssi_log[$key]" 1366 | [ -z "${blacklisted_devices[$key]}" ] && log "${BLUE}[DEL-RAND] ${NC}RAND $key expired after $difference seconds ${NC}" 1367 | 1368 | #AT LEAST ONE DEVICE EXPIRED 1369 | should_scan=true 1370 | fi 1371 | done 1372 | 1373 | #RANDOM DEVICE EXPIRATION SHOULD TRIGGER DEPARTURE SCAN 1374 | [ "$should_scan" == true ] && [ "$PREF_TRIGGER_MODE_DEPART" == false ] && perform_departure_scan 1375 | 1376 | #THIS IS A LIST OF ALL DEVIES PURGED FROM THE RECORDS; MAY INCLUDE BEACONS 1377 | notification_sent="____ " 1378 | 1379 | #RESET VARIABLES 1380 | last_seen="" 1381 | key="" 1382 | 1383 | #TEMP VAR 1384 | most_recent_beacon="" 1385 | observed_max_advertisement_interval="" 1386 | 1387 | #PURGE OLD KEYS FROM THE BEACON DEVICE LOG 1388 | for key in "${!public_device_log[@]}"; do 1389 | 1390 | #DETERMINE THE LAST TIME THIS MAC WAS LOGGED 1391 | last_seen="${public_device_log[$key]}" 1392 | 1393 | #RSSI 1394 | latest_rssi="${rssi_log[$key]}" 1395 | 1396 | #ADJUST FOR BEACON? 1397 | is_apple_beacon=false 1398 | 1399 | #RESET BEACON KEY 1400 | observed_max_advertisement_interval="${advertisement_interval_observation[$key]}" 1401 | most_recent_beacon="" 1402 | beacon_uuid_found="" 1403 | beacon_mac_found="" 1404 | 1405 | #THE PROBLEM HEERE IS THAT WE CAN RUN THROUGH THIS AND HIT ONE OR THE OTHER OF MAC OR ADDRESS FIRST; 1406 | #THEN WE EXIT 1407 | 1408 | #IS THIS RANDOM ADDRESS ASSOCIATED WITH A BEACON 1409 | for beacon_uuid_key in "${!beacon_mac_address_log[@]}"; do 1410 | #FIND ASSOCIATED BEACON 1411 | current_associated_beacon_mac_address="${beacon_mac_address_log[$beacon_uuid_key]}" 1412 | 1413 | #COMPARE TO CURRENT KEY 1414 | if [ "$current_associated_beacon_mac_address" == "$key" ]; then 1415 | 1416 | #SET THIS IS A BEACON 1417 | is_apple_beacon=true 1418 | 1419 | #SET VALUES 1420 | beacon_mac_found="$current_associated_beacon_mac_address" 1421 | beacon_uuid_found="$beacon_uuid_key" 1422 | break 1423 | 1424 | elif [ "$beacon_uuid_key" == "$key" ]; then 1425 | 1426 | #SET THIS IS A BEACON 1427 | is_apple_beacon=true 1428 | 1429 | #SET THIS IS A BEACON 1430 | is_apple_beacon=true 1431 | 1432 | #SET VALUES 1433 | beacon_uuid_found="$beacon_uuid_key" 1434 | beacon_mac_found="$current_associated_beacon_mac_address" 1435 | break 1436 | fi 1437 | done 1438 | 1439 | #DETERMINE IF THIS WAS A BEACON AND, IF SO, WHETHER THE BEACON IS SEEN MORE RECENTLY 1440 | if [ "$is_apple_beacon" == true ]; then 1441 | 1442 | #DETERMINE DIFFERENCE SET DEFAULT NON-EXPIRING VALUE FOR DEVUGGING PURPOSES 1443 | [ "${public_device_log[$beacon_mac_found]:--1}" -ge "${public_device_log[$beacon_uuid_found]:--1}" ] && most_recent_beacon=${public_device_log[$beacon_mac_found]} 1444 | [ "${public_device_log[$beacon_uuid_found]:--1}" -ge "${public_device_log[$beacon_mac_found]:--1}" ] && most_recent_beacon=${public_device_log[$beacon_uuid_found]} 1445 | 1446 | last_seen="$most_recent_beacon" 1447 | 1448 | #WHICH PREDICTION SHOULD WE USE? 1449 | [ "${advertisement_interval_observation[$beacon_mac_found]:--1}" -ge "${advertisement_interval_observation[$beacon_uuid_found]:--1}" ] && observed_max_advertisement_interval="${advertisement_interval_observation[$beacon_mac_found]}" 1450 | [ "${advertisement_interval_observation[$beacon_uuid_found]:--1}" -ge "${advertisement_interval_observation[$beacon_mac_found]:--1}" ] && observed_max_advertisement_interval="${advertisement_interval_observation[$beacon_uuid_found]}" 1451 | 1452 | #CALCUALTE DIFFERENCE FOR CONFIDENCE FINDING 1453 | difference=$((timestamp - most_recent_beacon)) 1454 | 1455 | else 1456 | 1457 | #DETERMINE DIFFERENCE 1458 | difference=$((timestamp - last_seen)) 1459 | 1460 | #CONTINUE IF DEVICE HAS NOT BEEN SEEN OR DATE IS CORRUPT 1461 | [ -z "$last_seen" ] && continue 1462 | fi 1463 | 1464 | #FIND THE EXPIRATION INTERVAL FOR THIS PARTICULAR BEACON 1465 | beacon_specific_expiration_interval="${advertisement_interval_observation[$key]}" 1466 | 1467 | #ADJUST TO BUFFER BASED ON USER PREFERENCES 1468 | beacon_specific_expiration_interval=$(( beacon_specific_expiration_interval * PREF_DEPART_SCAN_ATTEMPTS )) 1469 | 1470 | #SET EXPIRATION 1471 | beacon_specific_expiration_interval=$(( beacon_specific_expiration_interval > 45 && beacon_specific_expiration_interval < PREF_BEACON_EXPIRATION ? beacon_specific_expiration_interval : PREF_BEACON_EXPIRATION )) 1472 | 1473 | #TIMEOUT AFTER [XXX] SECONDS; ALL BEACONS HONOR THE SAME EXPRIATION THRESHOLD INCLUDING IBEACONS 1474 | if [ "$difference" -gt "$beacon_specific_expiration_interval" ]; then 1475 | #REMOVE FROM EXPIRING DEVICE LOG 1476 | [ -n "${expiring_device_log[$key]}" ] && unset "expiring_device_log[$key]" 1477 | 1478 | #IS BEACON? 1479 | if [ "$is_apple_beacon" == true ] && [ "$PREF_BEACON_MODE" == true ]; then 1480 | 1481 | #REMOVE FROM LOGS 1482 | unset "rssi_log[$beacon_uuid_found]" 1483 | 1484 | #REMOVE MAC FROM PUBLIC LOG 1485 | unset "public_device_log[$beacon_mac_found]" 1486 | unset "rssi_log[$beacon_mac_found]" 1487 | 1488 | #REMOVE BEACON FROM MAC ADDRESS ARRAY 1489 | unset "beacon_mac_address_log[$beacon_uuid_found]" 1490 | 1491 | #REMOVE FROM BEACON ASSOCIATION 1492 | unset "advertisement_interval_observation[$beacon_uuid_found]" 1493 | unset "advertisement_interval_observation[$beacon_mac_found]" 1494 | 1495 | #PUBLISH EXPIRATION 1496 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$beacon_uuid_found]}" ] && log "${BLUE}[DEL-BEAC] ${NC}BEAC $beacon_uuid_found expired after $difference seconds ${NC}" 1497 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$beacon_mac_found]}" ] && log "${BLUE}[DEL-PUBL] ${NC}BEAC $beacon_mac_found expired after $difference seconds ${NC}" 1498 | 1499 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$beacon_uuid_found]}" ] && publish_presence_message "id=$beacon_uuid_found" "confidence=0" "last_seen=$most_recent_beacon" 1500 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$beacon_mac_found]}" ] && publish_presence_message "id=$beacon_mac_found" "confidence=0" "last_seen=$most_recent_beacon" 1501 | 1502 | else 1503 | 1504 | unset "public_device_log[$key]" 1505 | unset "rssi_log[$key]" 1506 | 1507 | ##REMOVE FROM BEACON ASSOCIATION 1508 | unset "advertisement_interval_observation[$key]" 1509 | 1510 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$key]}" ] && log "${BLUE}[DEL-PUBL] ${NC}PUBL $key expired after $difference seconds ${NC}" 1511 | 1512 | #REPORT PRESENCE OF DEVICE 1513 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$key]}" ] && publish_presence_message "id=$key" "confidence=0" "last_seen=$last_seen" 1514 | 1515 | fi 1516 | 1517 | elif [ "${observed_max_advertisement_interval:-0}" -gt "0" ] && [ "$difference" -gt "$(( (beacon_specific_expiration_interval - observed_max_advertisement_interval) / 2 + observed_max_advertisement_interval))" ]; then 1518 | 1519 | #SHOULD REPORT A DROP IN CONFIDENCE? 1520 | percent_confidence=$(( 100 - (difference - observed_max_advertisement_interval) * 100 / (PREF_BEACON_EXPIRATION - observed_max_advertisement_interval) )) 1521 | [ "$percent_confidence" -lt "5" ] && percent_confidence=0 1522 | 1523 | 1524 | if [ "$PREF_REPORT_ALL_MODE" == true ]; then 1525 | #REPORTING ALL 1526 | if [ "$is_apple_beacon" == true ] && [ "$PREF_BEACON_MODE" == true ]; then 1527 | #DEBUG LOGGING 1528 | [ -z "${blacklisted_devices[$beacon_uuid_found]}" ] && publish_presence_message "id=$beacon_uuid_found" "confidence=$percent_confidence" "mac=$key" "last_seen=$most_recent_beacon" && expiring_device_log[$beacon_uuid_found]='true' 1529 | [ -z "${blacklisted_devices[$beacon_mac_found]}" ] && publish_presence_message "id=$beacon_mac_found" "confidence=$percent_confidence" "last_seen=$most_recent_beacon" && expiring_device_log[$beacon_mac_found]='true' 1530 | 1531 | else 1532 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$key]}" ] && publish_presence_message "id=$key" "confidence=$percent_confidence" "last_seen=$last_seen" && expiring_device_log[$key]='true' 1533 | fi 1534 | else 1535 | #REPORT PRESENCE OF DEVICE ONLY IF IT IS ABOUT TO BE AWAY; ALSO DO NOT REPORT DEVICES THAT WE'VE ALREADY REPORTEDI IN THIS LOOP 1536 | if [ "$is_apple_beacon" == true ]; then 1537 | #IF NOT SEEN AND BELOW THRESHOLD 1538 | if ! [[ $notification_sent =~ $key ]] && [ "$percent_confidence" -lt "$PREF_PERCENT_CONFIDENCE_REPORT_THRESHOLD" ]; then 1539 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$beacon_uuid_found]}" ] && publish_presence_message "id=$beacon_uuid_found" "confidence=$percent_confidence" "mac=$beacon_mac_found" "last_seen=$most_recent_beacon" && expiring_device_log[$beacon_uuid_found]='true' && notification_sent="$notification_sent $beacon_uuid_found" 1540 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$beacon_mac_found]}" ] && publish_presence_message "id=$beacon_mac_found" "confidence=$percent_confidence" "last_seen=$most_recent_beacon" && expiring_device_log[$beacon_mac_found]='true' && notification_sent="$notification_sent $beacon_mac_found" 1541 | fi 1542 | else 1543 | [ "$PREF_BEACON_MODE" == true ] && [ -z "${blacklisted_devices[$key]}" ] && [ "$percent_confidence" -lt "$PREF_PERCENT_CONFIDENCE_REPORT_THRESHOLD" ] && publish_presence_message "id=$key" "confidence=$percent_confidence" "last_seen=$last_seen" && expiring_device_log[$key]='true' && notification_sent="$notification_sent $key" 1544 | notification_sent="$notification_sent $key" 1545 | fi 1546 | fi 1547 | fi 1548 | done 1549 | 1550 | continue 1551 | 1552 | elif [ "$cmd" == "NAME" ]; then 1553 | #DATA IS DELIMITED BY VERTICAL PIPE 1554 | mac=$(echo "$data" | awk -F "|" '{print $1}') 1555 | name=$(echo "$data" | awk -F "|" '{print $2}') 1556 | rssi_latest="${rssi_log[$mac]}" 1557 | 1558 | #PREVIOUS STATE; SET DEFAULT TO UNKNOWN 1559 | previous_state="${known_public_device_log[$mac]}" 1560 | previous_state=${previous_state:--1} 1561 | 1562 | #GET MANUFACTURER INFORMATION 1563 | manufacturer="$(determine_manufacturer "$mac")" 1564 | 1565 | #IF NAME IS DISCOVERED, PRESUME HOME 1566 | if [ -n "$name" ]; then 1567 | known_public_device_log[$mac]=1 1568 | [ "$previous_state" != "1" ] && did_change=true 1569 | else 1570 | known_public_device_log[$mac]=0 1571 | [ "$previous_state" != "0" ] && did_change=true 1572 | fi 1573 | fi 1574 | 1575 | #NEED TO VERIFY WHETHER WE HAVE TO UPDATE INFORMATION FOR A PRIVATE BEACON THAT IS 1576 | #ACTUALLY PUBLIC 1577 | 1578 | if [ "$cmd" == "PUBL" ]; then 1579 | #PARSE RECEIVED DATA 1580 | mac=$(echo "$data" | awk -F "|" '{print $1}') 1581 | pdu_header=$(echo "$data" | awk -F "|" '{print $2}') 1582 | name=$(echo "$data" | awk -F "|" '{print $3}') 1583 | rssi=$(echo "$data" | awk -F "|" '{print $4}') 1584 | adv_data=$(echo "$data" | awk -F "|" '{print $5}') 1585 | manufacturer=$(echo "$data" | awk -F "|" '{print $6}') 1586 | device_type=$(echo "$data" | awk -F "|" '{print $7}') 1587 | flags=$(echo "$data" | awk -F "|" '{print $8}') 1588 | oem_data=$(echo "$data" | awk -F "|" '{print $9}') 1589 | instruction_timestamp=$(echo "$data" | awk -F "|" '{print $10}') 1590 | resolvable=$(echo "$data" | awk -F "|" '{print $11}') 1591 | hex_data=$(echo "$data" | awk -F "|" '{print $12}') 1592 | 1593 | #DEFAULT? 1594 | instruction_timestamp=${instruction_timestamp:-$timestamp} 1595 | instruction_delay=$((timestamp - instruction_timestamp)) 1596 | 1597 | #RESET BEACON UUID 1598 | beacon_uuid_key="" 1599 | 1600 | #SET TYPE 1601 | beacon_type="GENERIC_BEACON_PUBLIC" 1602 | matching_beacon_uuid_key="" 1603 | 1604 | #DETERMINE WHETHER THIS DEVICE IS ASSOCIATED WITH AN IBEACON 1605 | current_associated_beacon_mac_address="" 1606 | for beacon_uuid_key in "${!beacon_mac_address_log[@]}"; do 1607 | current_associated_beacon_mac_address="${beacon_mac_address_log[$beacon_uuid_key]}" 1608 | if [ "$current_associated_beacon_mac_address" == "$mac" ]; then 1609 | matching_beacon_uuid_key="$beacon_uuid_key" 1610 | break 1611 | fi 1612 | done 1613 | 1614 | #SET ADVERTISEMENT INTERVAL OBSERVATION 1615 | last_appearance=${public_device_log[$mac]:-$timestamp} 1616 | if [ "$observation_made" == false ]; then 1617 | observation_made=true 1618 | temp_observation="" && temp_observation=$((((timestamp - last_appearance - 1 + PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) / PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) * PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP)) 1619 | [ "$temp_observation" -gt "${advertisement_interval_observation[$mac]:-0}" ] && [ "$temp_observation" -gt "0" ] && [ "$temp_observation" -lt "300" ] && advertisement_interval_observation[$mac]=$temp_observation 1620 | 1621 | fi 1622 | 1623 | #SET ADVERTISEMENT INTERVAL OBSERVATION 1624 | if [ -n "$matching_beacon_uuid_key" ]; then 1625 | #GET INTERVAL SINCE LAST SEEN 1626 | last_appearance=${public_device_log[$matching_beacon_uuid_key]:-$timestamp} 1627 | temp_observation="" && temp_observation=$((((timestamp - last_appearance - 1 + PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) / PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) * PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP)) 1628 | [ "$temp_observation" -gt "${advertisement_interval_observation[$matching_beacon_uuid_key]:-0}" ] && [ "$temp_observation" -gt "0" ] && [ "$temp_observation" -lt "300" ] && advertisement_interval_observation[$matching_beacon_uuid_key]=$temp_observation 1629 | fi 1630 | 1631 | #SET NAME 1632 | [ -n "$name" ] && known_public_device_name[$mac]="$name" 1633 | [ -z "$name" ] && name="$(determine_name "$mac")" 1634 | 1635 | #DATA IS PUBLIC MAC Addr.; ADD TO LOG 1636 | [ -z "${public_device_log[$mac]}" ] && is_new=true 1637 | 1638 | #HAS THIS DEVICE BEEN MARKED AS EXPIRING SOON? IF SO, SHOULD REPORT 100 AGAIN 1639 | [ -n "${expiring_device_log[$mac]}" ] && should_update=true 1640 | [ -n "$matching_beacon_uuid_key" ] && [ -n "${expiring_device_log[$matching_beacon_uuid_key]}" ] && should_update=true 1641 | 1642 | #GET LAST RSSI 1643 | rssi_latest="${rssi_log[$mac]}" 1644 | [ -z "$rssi_latest" ] && [ -n "$matching_beacon_uuid_key" ] && rssi_latest="${rssi_log[$matching_beacon_uuid_key]}" 1645 | 1646 | #IF NOT IN DATABASE, BUT FOUND HERE 1647 | if [ -n "$name" ]; then 1648 | 1649 | #FIND PUBLIC NAME 1650 | known_public_device_name[$mac]="$name" 1651 | 1652 | #GET NAME FROM CACHE 1653 | cached_name=$(grep "$mac" < ".public_name_cache" | awk -F "\t" '{print $2}') 1654 | 1655 | #ECHO TO CACHE IF DOES NOT EXIST 1656 | [ -z "$cached_name" ] && echo "$mac $name" >> .public_name_cache 1657 | 1658 | #IS THIS ASSOCITED WITH A BEACON? 1659 | if [ -n "$matching_beacon_uuid_key" ]; then 1660 | 1661 | #IF THIS IS AN IBEACON, WE ADD THE NAME TO THAT ARRAY TOO 1662 | known_public_device_name[$matching_beacon_uuid_key]="$name" 1663 | 1664 | #GET NAME FROM CACHE 1665 | cached_name="" 1666 | cached_name=$(grep "$matching_beacon_uuid_key" < ".public_name_cache" | awk -F "\t" '{print $2}') 1667 | 1668 | #ECHO TO CACHE IF DOES NOT EXIST 1669 | [ -z "$cached_name" ] && echo "$matching_beacon_uuid_key $name" >> .public_name_cache 1670 | fi 1671 | fi 1672 | 1673 | #STATIC DEVICE DATABASE AND RSSI DATABASE 1674 | public_device_log[$mac]="$timestamp" 1675 | [ -n "$rssi" ] && rssi_log[$mac]="$rssi" 1676 | 1677 | #MANUFACTURER 1678 | [ -z "$manufacturer" ] && manufacturer="$(determine_manufacturer "$mac")" 1679 | 1680 | elif [ "$cmd" == "BEAC" ]; then 1681 | 1682 | #DATA IS DELIMITED BY VERTICAL PIPE 1683 | uuid=$(echo "$data" | awk -F "|" '{print $1}') 1684 | major=$(echo "$data" | awk -F "|" '{print $2}') 1685 | minor=$(echo "$data" | awk -F "|" '{print $3}') 1686 | rssi=$(echo "$data" | awk -F "|" '{print $4}') 1687 | power=$(echo "$data" | awk -F "|" '{print $5}') 1688 | mac=$(echo "$data" | awk -F "|" '{print $6}') 1689 | beacon_type="APPLE_IBEACON" 1690 | name="" 1691 | 1692 | #FIND INSTRUCTION TIMESTAMP 1693 | instruction_timestamp=$(echo "$data" | awk -F "|" '{print $7}') 1694 | 1695 | #DEFAULT? 1696 | instruction_timestamp=${instruction_timestamp:-$timestamp} 1697 | instruction_delay=$((timestamp - instruction_timestamp)) 1698 | 1699 | #GET MAC AND PDU HEADER 1700 | uuid_reference="$uuid-$major-$minor" 1701 | 1702 | #HAS THIS DEVICE BEEN MARKED AS EXPIRING SOON? IF SO, SHOULD REPORT 100 AGAIN 1703 | [ -n "${expiring_device_log[$uuid_reference]}" ] && should_update=true && unset "expiring_device_log[$uuid_reference]" 1704 | 1705 | #UPDATE MAC ADDRESS OF BEACON 1706 | 1707 | #FIRST FIND PREVIOUS ASSOCIATION OF MAC ADDRESS TO DETERMINE 1708 | #WHETHER THIS ADDRESS HAS BEEN REMOVED BY AN EXPIRATION 1709 | 1710 | if [ -n "${beacon_mac_address_log[$uuid_reference]}" ]; then 1711 | 1712 | #FIND PREVIOUS ASSOCIATION; HAS THIS BEEN REMOVED? 1713 | previous_association=${beacon_mac_address_log[$uuid_reference]} 1714 | 1715 | #IF THE ADDRESS HAS CHANGED, THEN WE NEED TO UPDATE THE ADDRESS 1716 | if [ ! "$previous_association" == "$mac" ]; then 1717 | 1718 | #REMOVE THIS FROM PUBLIC RECORDS 1719 | unset "public_device_log[$previous_association]" 1720 | fi 1721 | else 1722 | #SET THIS AS NEW 1723 | is_new=true 1724 | fi 1725 | 1726 | #SET ADVERTISEMENT INTERVAL OBSERVATION 1727 | last_appearance=${public_device_log[$mac]:-$timestamp} 1728 | [ "$observation_made" == false ] && observation_made=true && advertisement_interval_observation[$mac]=$((((timestamp - last_appearance - 1 + PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) / PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) * PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP)) 1729 | 1730 | #GET INTERVAL SINCE LAST SEEN 1731 | last_appearance=${public_device_log[$mac]:-$timestamp} 1732 | if [ "$observation_made" == false ]; then 1733 | observation_made=true 1734 | temp_observation="" && temp_observation=$((((timestamp - last_appearance - 1 + PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) / PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP) * PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP)) 1735 | [ "$temp_observation" -gt "${advertisement_interval_observation[$mac]:-0}" ] && [ "$temp_observation" -gt "0" ] && [ "$temp_observation" -lt "300" ] && advertisement_interval_observation[$mac]=$temp_observation 1736 | 1737 | fi 1738 | 1739 | #SAVE BEACON ADDRESS LOG 1740 | beacon_mac_address_log[$uuid_reference]="$mac" 1741 | 1742 | #FIND NAME OF BEACON 1743 | [ -z "$name" ] && name="$(determine_name "$mac")" 1744 | 1745 | #GET LAST RSSI 1746 | rssi_latest="${rssi_log[$uuid_reference]}" 1747 | [ -z "$rssi_latest" ] && rssi_latest="${rssi_log[$mac]}" 1748 | 1749 | #IS THIS A NEW DEVICE? 1750 | [ -z "${public_device_log[$uuid_reference]}" ] && is_new=true 1751 | 1752 | #RECORD BASED ON UUID AND MAC ADDRESS 1753 | public_device_log[$uuid_reference]="$timestamp" 1754 | 1755 | #RSSI LOGS 1756 | [ -n "$rssi" ] && rssi_log[$uuid_reference]="$rssi" 1757 | fi 1758 | 1759 | #********************************************************************** 1760 | # 1761 | # 1762 | # THE FOLLOWING REPORTS RSSI CHANGES FOR PUBLIC OR RANDOM DEVICES 1763 | # 1764 | # 1765 | #********************************************************************** 1766 | 1767 | #REPORT RSSI CHANGES 1768 | if [ -n "$rssi" ] && [ "${#rssi}" -lt "5" ] && [ "$uptime" -gt "$PREF_STARTUP_SETTLE_TIME" ]; then 1769 | 1770 | #ONLY FOR PUBLIC OR BEAON DEVICES 1771 | if [ "$cmd" == "PUBL" ] || [ "$cmd" == "BEAC" ]; then 1772 | 1773 | #SET RSSI LATEST IF NOT ALREADY SET 1774 | rssi_latest=${rssi_latest:--200} 1775 | 1776 | #IS RSSI THE SAME? 1777 | rssi_change=$((rssi - rssi_latest)) 1778 | abs_rssi_change=${rssi_change#-} 1779 | 1780 | #DETERMINE MOTION DIRECTION 1781 | motion_direction="depart" 1782 | [ "$rssi_change" == "$abs_rssi_change" ] && motion_direction="approach" 1783 | 1784 | #IF POSITIVE, APPROACHING IF NEGATIVE DEPARTING 1785 | case "1" in 1786 | $(( abs_rssi_change >= 50)) ) 1787 | change_type="fast $motion_direction" 1788 | ;; 1789 | $(( abs_rssi_change >= 30)) ) 1790 | change_type="moderate $motion_direction" 1791 | ;; 1792 | $(( abs_rssi_change >= 10)) ) 1793 | change_type="slow movement $motion_direction" 1794 | ;; 1795 | $(( abs_rssi_change >= 3)) ) 1796 | change_type="drifting" 1797 | ;; 1798 | *) 1799 | change_type="stationary" 1800 | ;; 1801 | esac 1802 | 1803 | #WITHOUT ANY DATA OR INFORMATION, MAKE SURE TO REPORT 1804 | [ "$rssi_latest" == "-200" ] && change_type="initial reading" && should_update=true 1805 | 1806 | #ONLY PRINT IF WE HAVE A CHANCE OF A CERTAIN MAGNITUDE 1807 | [ -z "${blacklisted_devices[$mac]}" ] && [ "$abs_rssi_change" -gt "$PREF_RSSI_CHANGE_THRESHOLD" ] && log "${CYAN}[CMD-RSSI] ${NC}$cmd $mac ${GREEN}${NC}RSSI: ${rssi:-100} dBm ($change_type | $abs_rssi_change dBm) ${NC}" && should_update=true 1808 | fi 1809 | fi 1810 | 1811 | #********************************************************************** 1812 | # 1813 | # 1814 | # THE FOLLOWING CONDITIONS DEFINE BEHAVIOR WHEN A DEVICE ARRIVES 1815 | # OR DEPARTS 1816 | # 1817 | # 1818 | #********************************************************************** 1819 | 1820 | if [ "$cmd" == "NAME" ] ; then 1821 | 1822 | #PRINTING FORMATING 1823 | debug_name="$name" 1824 | expected_name="$(determine_name "$mac")" 1825 | 1826 | 1827 | current_state="${known_public_device_log[$mac]}" 1828 | 1829 | #IF NAME IS NOT PREVIOUSLY SEEN, THEN WE SET THE STATIC DEVICE DATABASE NAME 1830 | [ -z "$expected_name" ] && [ -n "$name" ] && known_public_device_name[$mac]="$name" 1831 | [ -n "$expected_name" ] && [ -z "$name" ] && name="$expected_name" 1832 | 1833 | #OVERWRITE WITH EXPECTED NAME 1834 | [ -n "$expected_name" ] && [ -n "$name" ] && name="$expected_name" 1835 | 1836 | #FOR LOGGING; MAKE SURE THAT AN UNKNOWN NAME IS ADDED 1837 | if [ -z "$debug_name" ]; then 1838 | #SHOW ERROR 1839 | debug_name="Unknown Name" 1840 | 1841 | #CHECK FOR KNOWN NAME 1842 | [ -n "$expected_name" ] && debug_name="$expected_name" 1843 | fi 1844 | 1845 | #IF WE HAVE DEPARTED OR ARRIVED; MAKE A NOTE UNLESS WE ARE ALSO IN THE TRIGGER MODE 1846 | [ "$did_change" == true ] && [ "$current_state" == "1" ] && $PREF_TRIGGER_MODE_REPORT_OUT && publish_cooperative_scan_message "arrive" 1847 | 1848 | #PRINT RAW COMMAND; DEBUGGING 1849 | log "${CYAN}[CMD-$cmd] ${NC}$mac ${GREEN}$debug_name ${NC} $manufacturer${NC}" 1850 | 1851 | elif [ "$cmd" == "BEAC" ] && [ "$PREF_BEACON_MODE" == true ] && ([ "$should_update" == true ] || [ "$is_new" == true ]); then 1852 | 1853 | #PROVIDE USEFUL LOGGING 1854 | if [ -z "${blacklisted_devices[$uuid_reference]}" ] && [ -z "${blacklisted_devices[$mac]}" ]; then 1855 | 1856 | #REMOVE 1857 | [ -n "${expiring_device_log[$uuid_reference]}" ] && unset "expiring_device_log[$uuid_reference]" 1858 | [ -n "${expiring_device_log[$mac]}" ] && unset "expiring_device_log[$mac]" 1859 | 1860 | #LOG 1861 | log "${GREEN}[CMD-$cmd] ${NC}$mac ${GREEN}$uuid $major $minor ${NC}$name${NC}" 1862 | 1863 | publish_presence_message \ 1864 | "id=$uuid_reference" \ 1865 | "confidence=100" \ 1866 | "name=$name" \ 1867 | "type=$beacon_type" \ 1868 | "rssi=$rssi" \ 1869 | "mac=$mac" \ 1870 | "report_delay=$instruction_delay" \ 1871 | "observed_interval=${advertisement_interval_observation[$mac]:--1}" \ 1872 | "power=$power" \ 1873 | "movement=$change_type" 1874 | 1875 | #LOG 1876 | log "${PURPLE}[CMD-PUBL]${NC} $mac ${GREEN}$name${NC} ${BLUE}$manufacturer${NC} $rssi dBm" 1877 | 1878 | publish_presence_message \ 1879 | "id=$mac" \ 1880 | "confidence=100" \ 1881 | "name=$name" \ 1882 | "manufacturer=$manufacturer" \ 1883 | "type=GENERIC_BEACON_PUBLIC" \ 1884 | "report_delay=$instruction_delay" \ 1885 | "observed_interval=${advertisement_interval_observation[$mac]:--1}" \ 1886 | "rssi=$rssi" \ 1887 | "flags=$flags" \ 1888 | "movement=${change_type:-none}" 1889 | fi 1890 | 1891 | elif [ "$cmd" == "PUBL" ] && [ "$PREF_BEACON_MODE" == true ] && ([ "$should_update" == true ] || [ "$is_new" == true ]); then 1892 | 1893 | #PUBLISH PRESENCE MESSAGE FOR BEACON 1894 | if [ -z "${blacklisted_devices[$mac]}" ]; then 1895 | [ -n "${expiring_device_log[$mac]}" ] && unset "expiring_device_log[$mac]" 1896 | 1897 | #FIND NAME 1898 | expected_name="$(determine_name "$mac")" 1899 | 1900 | log "${PURPLE}[CMD-$cmd]${NC} $mac ${GREEN}$name${NC} ${BLUE}$manufacturer${NC} $rssi dBm" 1901 | 1902 | publish_presence_message \ 1903 | "id=$mac" \ 1904 | "confidence=100" \ 1905 | "name=$name" \ 1906 | "manufacturer=$manufacturer" \ 1907 | "type=$beacon_type" \ 1908 | "report_delay=$instruction_delay" \ 1909 | "rssi=$rssi" \ 1910 | "observed_interval=${advertisement_interval_observation[$mac]:--1}" \ 1911 | "flags=${flags:-none}" \ 1912 | "movement=${change_type:-none}" \ 1913 | "oem_data=${oem_data:-not advertised}" \ 1914 | "hex_data=${hex_data:-none}" \ 1915 | "resolvable=${resolvable:-PUBLIC}" 1916 | 1917 | #PERFORM SCAN HERE AS WELL 1918 | if [ "$is_new" == true ]; then 1919 | #REJECTION FILTER 1920 | if [[ ${flags,,} =~ ${PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE,,} ]] || [[ ${manufacturer,,} =~ ${PREF_FAIL_FILTER_MANUFACTURER_ARRIVE,,} ]]; then 1921 | 1922 | $PREF_VERBOSE_LOGGING && log "${RED}[CMD-$cmd]${NC} [${RED}failed filter${NC}] data: ${BLUE}${mac:-none}${NC} pdu: ${BLUE}${pdu_header:-none}${NC} rssi: ${BLUE}${rssi:-UKN} dBm${NC} flags: ${RED}${flags:-none}${NC} man: ${RED}${manufacturer:-unknown}${NC} delay: ${BLUE}${instruction_delay:-UKN}${NC}" 1923 | 1924 | continue 1925 | fi 1926 | 1927 | #FLAG AND MFCG FILTER 1928 | if [[ ${flags,,} =~ ${PREF_PASS_FILTER_ADV_FLAGS_ARRIVE,,} ]] && [[ ${manufacturer,,} =~ ${PREF_PASS_FILTER_MANUFACTURER_ARRIVE,,} ]]; then 1929 | #PROVIDE USEFUL LOGGING 1930 | $PREF_VERBOSE_LOGGING && log "${RED}[CMD-$cmd]${NC} [${GREEN}passed filter${NC}] data: ${BLUE}${mac:-none}${NC} pdu: ${BLUE}${pdu_header:-none}${NC} rssi: ${BLUE}${rssi:-UKN} dBm${NC} flags: ${BLUE}${flags:-none}${NC} man: ${BLUE}${manufacturer:-unknown}${NC} delay: ${BLUE}${instruction_delay:-UKN}${NC}" 1931 | 1932 | #WE ARE PERFORMING THE FIRST ARRIVAL SCAN? 1933 | first_arrive_scan=false 1934 | 1935 | #SCAN ONLY IF WE ARE NOT IN TRIGGER MODE 1936 | perform_arrival_scan 1937 | 1938 | continue 1939 | else 1940 | #PROVIDE USEFUL LOGGING 1941 | $PREF_VERBOSE_LOGGING && log "${RED}[CMD-$cmd]${NC} [${RED}failed filter${NC}] data: ${BLUE}${mac:-none}${NC} pdu: ${BLUE}${pdu_header:-none}${NC} rssi: ${BLUE}${rssi:-UKN} dBm${NC} flags: ${RED}${flags:-none}${NC} man: ${RED}${manufacturer:-unknown}${NC} delay: ${BLUE}${instruction_delay:-UKN}${NC}" 1942 | 1943 | continue 1944 | fi 1945 | fi 1946 | fi 1947 | 1948 | 1949 | elif [ "$cmd" == "RAND" ] && [ "$is_new" == true ] && [ "$PREF_TRIGGER_MODE_ARRIVE" == false ] && [ -z "${blacklisted_devices[$mac]}" ]; then 1950 | 1951 | #REJECTION FILTER 1952 | if [[ ${flags,,} =~ ${PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE,,} ]] || [[ ${manufacturer,,} =~ ${PREF_FAIL_FILTER_MANUFACTURER_ARRIVE,,} ]]; then 1953 | 1954 | $PREF_VERBOSE_LOGGING && log "${RED}[CMD-$cmd]${NC} [${RED}failed filter${NC}] data: ${BLUE}${mac:-none}${NC} pdu: ${BLUE}${pdu_header:-none}${NC} rssi: ${BLUE}${rssi:-UKN} dBm${NC} flags: ${RED}${flags:-none}${NC} man: ${RED}${manufacturer:-unknown}${NC} delay: ${BLUE}${instruction_delay:-UKN}${NC}" 1955 | 1956 | continue 1957 | fi 1958 | 1959 | #FLAG AND MFCG FILTER 1960 | if [[ ${flags,,} =~ ${PREF_PASS_FILTER_ADV_FLAGS_ARRIVE,,} ]] && [[ ${manufacturer,,} =~ ${PREF_PASS_FILTER_MANUFACTURER_ARRIVE,,} ]]; then 1961 | #PROVIDE USEFUL LOGGING 1962 | $PREF_VERBOSE_LOGGING && log "${RED}[CMD-$cmd]${NC} [${GREEN}passed filter${NC}] data: ${BLUE}${mac:-none}${NC} pdu: ${BLUE}${pdu_header:-none}${NC} rssi: ${BLUE}${rssi:-UKN} dBm${NC} flags: ${BLUE}${flags:-none}${NC} man: ${BLUE}${manufacturer:-unknown}${NC} delay: ${BLUE}${instruction_delay:-UKN}${NC}" 1963 | 1964 | #WE ARE PERFORMING THE FIRST ARRIVAL SCAN? 1965 | first_arrive_scan=false 1966 | 1967 | #SCAN ONLY IF WE ARE NOT IN TRIGGER MODE 1968 | perform_arrival_scan 1969 | 1970 | continue 1971 | else 1972 | #PROVIDE USEFUL LOGGING 1973 | $PREF_VERBOSE_LOGGING && log "${RED}[CMD-$cmd]${NC} [${RED}failed filter${NC}] data: ${BLUE}${mac:-none}${NC} pdu: ${BLUE}${pdu_header:-none}${NC} rssi: ${BLUE}${rssi:-UKN} dBm${NC} flags: ${RED}${flags:-none}${NC} man: ${RED}${manufacturer:-unknown}${NC} delay: ${BLUE}${instruction_delay:-UKN}${NC}" 1974 | 1975 | continue 1976 | fi 1977 | fi 1978 | 1979 | #SHOUD WE PERFORM AN ARRIVAL SCAN AFTER THIS FIRST LOOP? 1980 | if [ "$first_arrive_scan" == true ] && [ "$uptime" -lt "$PREF_STARTUP_SETTLE_TIME" ] ; then 1981 | perform_arrival_scan 1982 | fi 1983 | 1984 | done < main_pipe 1985 | 1986 | #SHOUD WE PERFORM AN ARRIVAL SCAN AFTER THIS FIRST LOOP? 1987 | if [ "$first_arrive_scan" == true ] && [ "$uptime" -lt "$PREF_STARTUP_SETTLE_TIME" ] ; then 1988 | perform_arrival_scan 1989 | fi 1990 | done 1991 | -------------------------------------------------------------------------------- /support/README.md: -------------------------------------------------------------------------------- 1 | ## *Basics* 2 | 3 |
Will this be able to track my Apple Watch/Smart Watch?

4 | 5 | Yes, with a caveat. Many users, including myself, have successfully added Apple Watch Bluetooth addresses to the `known_static_addresses` file. In my personal experience, an Apple Watch works just fine [once it has connected to at least one other Bluetooth device, apart from your iPhone](https://github.com/andrewjfreyer/monitor#my-phone-doesnt-seem-to-automatically-broadcast-an-anonymous-bluetooth-advertisement-what-can-i-do). Other users have reported that the Apple Watch will occasionally not respond to this script. Your mileage using the Apple Watch and/or other low-power connectible Bluetooth devices may vary. I strongly recommend tracking phones. 6 | 7 |

8 | 9 |
What special app do I need on my phone to get this to work?

10 | 11 | None, except in rare circumstances. The only requirement is that Bluetooth is left on. Works best with iPhones and Android phones that have peripheral mode enabled. 12 |

13 | 14 |
Does this script reduce battery life for my phone?

15 | 16 | Not noticeable in my several years of using techniques similar to this. 17 |

18 | 19 |
How can I trigger an arrival scan?

20 | 21 | Post a message with blank content to `monitor/scan/arrive` 22 |

23 | 24 |
How can I trigger an depart scan?

25 | 26 | Post a message with blank content to `monitor/scan/depart` 27 |

28 | 29 |
How can I trigger an arrive/depart scan from an automation in Home Assistant?

30 | 31 | For an automation or script (or other service trigger), use: 32 | 33 | ```yaml 34 | service: 'mqtt.publish' 35 | data: 36 | topic: monitor/scan/arrive 37 | ``` 38 | 39 | ```yaml 40 | service: 'mqtt.publish' 41 | data: 42 | topic: monitor/scan/depart 43 | ``` 44 |

45 | 46 |
How can I add a known device without manually entering an address?

47 | 48 | Post a message with the mac address separated from an alias (optional) by a space to: `monitor/setup/add known device` 49 |

50 | 51 |
How can I delete a known device without manually editing an address?

52 | 53 | Post a message with the mac address to: `monitor/setup/delete known device` 54 |

55 | 56 |
How can I upgrade to the latest version without using ssh?

57 | 58 | Post a message with blank content to `monitor/scan/update` or `monitor/scan/updatebeta` 59 |

60 | 61 |
How can I restart a this script node?

62 | 63 | Via command line: 64 | 65 | ```bash 66 | sudo systemctl restart monitor 67 | ``` 68 | 69 | Or, post a message with blank content to `monitor/scan/restart` 70 |

71 | 72 |
Why don't I see RSSI for my device?

73 | 74 | For phones, you'll have to connect to `monitor` first using the `-c` flag. 75 |

76 | 77 |
How do I force an RSSI update for a known device, like my phone?

78 | 79 | Post a message with blank content to `monitor/scan/rssi` 80 | 81 |

82 | 83 | ____ 84 | 85 | ## *Performance* 86 | 87 |
Can't I just issue a name scan every few seconds to get faster arrival and depart detection?

88 | 89 | Yes, use periodic scanning mode with `-r`. 90 |

91 | 92 |
Can I use other Bluetooth services while this script is running?

93 | 94 | No. Monitor needs exclusive use of the Bluetooth radio to function properly. This is why it is designed to run on inexpensive hardware like the Raspberry Pi Zero W. 95 |

96 | 97 |
Can this script run on XYZ hardware or in XYZ container?

98 | 99 | Probably. The script has been designed to minimize dependencies as much as possible. That said, I can't guarantee or provide support to all systems. 100 |

101 | 102 |
Does this script interfere with Wi-Fi, Zigbee, or Zwave?

103 | 104 | It can, if it scans too frequently, especially if you're running this script from internal Raspberry Pi radios. Try to use all techniques for reducing `name` scans, including using trigger-only depart mode `-tdr`. When in this mode, this script will never scan when all devices are home. Instead, this script will wait until a `monitor/scan/depart` message is sent. Personally, I use my front door lock as a depart scan trigger. 105 |

106 | 107 |
How can I check if a this script node is up and hasn't shut down for some reason?

108 | 109 | Post a message to `monitor/scan/echo`, and you'll receive a response at the topic `$mqtt_topicpath/$mqtt_publisher_identity/echo` 110 |

111 | 112 |
I have interference and/or my ssh sessions are really slow and laggy. What gives?

113 | 114 | Cheap Wi-Fi chipsets and cheap Bluetooth chipsets can perform poorly together if operated at the same time, especially on Raspberry Pi devices. If you still experience interference in your network, switching to a Wi-Fi dongle can help. 115 |

116 | 117 |
I use a Bluetooth dongle, and this script seems to become non-responsive after a while - what's going on?

118 | 119 | Many Bluetooth dongles do not properly filter out duplicate advertisements, so this script gets overwhelmed trying to filter out hundreds of reports, when it expects dozens. I'm working on a solution, but for now the best option is to switch to internal Bluetooth or, alternatively, you can try another Bluetooth dongle. 120 |

121 | 122 | ___ 123 | 124 | ## *Debugging* 125 | 126 |
I keep seeing that my Bluetooth hardware is "cycling" in the logs - what does that mean?

127 | 128 | If more than one program or executable try to use the Bluetooth hardware at the same time, your Bluetooth hardware will report an error. To correct this error, the hardware needs to be taken offline, then brought back. 129 |

130 | 131 |
How do I access logs?

132 | 133 | Run via command line and post log output to github. Else, access `journalctl` to show the most recent logs: 134 | 135 | ```bash 136 | journalctl -u monitor -r 137 | ``` 138 |

139 | 140 |
My Android phone doesn't seem to send any anonymous advertisements, no matter what I do. Is there any solution?

141 | 142 | Some phones, like the LG ThinQ G7 include an option in settings to enable file sharing via bluetooth. As resported by Home Assistant forum user @jusdwy, access this option via Settings >Connected Devices > File Sharing > File Sharing ON. For other android phones, an app like [Beacon Simulator](https://play.google.com/store/apps/details?id=net.alea.beaconsimulator&hl=en_US) may be a good option. You may also be able to see more information about Bluetooth on your phone using [nRF Connect](https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&hl=en_US). 143 | 144 | Unfortunately, until Android OS includes at least one service that requires bluetooth peripheral mode to be enabled, Android devices will probably not advertise without an application running in the background. In short, as I understand it, Android/Google has been slow to adopt BTLE peripheral mode as an option in addition to the default central mode. [Here is a decently comprehensive list of phones that support peripheral mode](https://altbeacon.github.io/android-beacon-library/beacon-transmitter-devices.html), should an application choose to leverage the appropriate API. It does not appear as though the native OS has an option (outside of the file sharing option mentioned above on LG phones) to enable this mode. 145 | 146 | Unfortunately, it seems to me that absent an application causing an advertisement to send, Android users will not be able to use monitor in the same way as iOS users or beacon users. 147 |

148 | 149 |
My phone doesn't seem to automatically broadcast an anonymous Bluetooth advertisement ... what can I do?

150 | 151 | Many phones will only broadcast once they have already connected to *at least one* other Bluetooth device. Connect to a speaker, a car, a headset, or `monitor.sh -c [address]` and try again. 152 |

153 | 154 |
Why does my MQTT broker show connection and disconnection so often?

155 | 156 | This is normal behavior for `mosquitto_pub` - nothing to worry about. 157 |

158 | 159 |
I updated and this script is no longer working ... what gives?

160 | 161 | Make sure you've updated `mosquitto` to v1.5 or higher. In order to support a wider userbase, backward compatibility for old versions of `mosquitto` was dropped. It is alos strongly recommended that you upgrade to bash 4.4+. 162 |

163 | 164 |
I keep seeing MQTT Broker Offline messages in the this script log. What's going on?

165 | 166 | mosquitto fails to connect to a broker if your password has certain special characters such as: `@`, `:`,`/` - if this is the case, the easiest solution is to create a new user for this script with a different password. 167 |

168 | 169 |
Can I use a certfile for mosquitto instead of my password?

170 | 171 | Yes, specify a path for `mqtt_certificate_path` in mqtt_preferences. 172 |

173 | 174 | ____ 175 | 176 | ## *Filters* 177 | 178 |
What filters do you personally use?

179 | 180 | ```bash 181 | 182 | #ARRIVE TRIGGER FILTER(S) 183 | PREF_PASS_FILTER_ADV_FLAGS_ARRIVE=\"0x1a|0x1b\" 184 | PREF_PASS_FILTER_MANUFACTURER_ARRIVE=\"Apple\" 185 | 186 | #ARRIVE TRIGGER NEGATIVE FILTER(S) 187 | PREF_FAIL_FILTER_MANUFACTURER_ARRIVE=\"Google|Samsung\" 188 | PREF_FAIL_FILTER_MANUFACTURER_ARRIVE=\"NONE\" 189 | ``` 190 |

191 | 192 |
What are the default filters for the PDU filter option?

193 | 194 | ```ADV_IND|ADV_SCAN_IND|ADV_NONCONN_IND|SCAN_RSP``` 195 |

196 | 197 |
How do I use this as a device_tracker, in addition to the standard confidence messages?

198 | 199 | Set the option `PREF_DEVICE_TRACKER_REPORT` in your `behavior_preferences` file to true. If it's not there, add a line like this: 200 | 201 | ```bash 202 | PREF_DEVICE_TRACKER_REPORT=true 203 | ``` 204 | 205 | Then, an additional mqtt message will be posted to the topic branch ending in `/device_tracker` 206 | 207 | So, as an example for a this script node named "first floor", a device tracker configuration for Home Assistant can look like: 208 | 209 | ```yaml 210 | 211 | device_tracker: 212 | - platform: mqtt 213 | devices: 214 | andrew_first_floor: 'monitor/first floor/[device address or alias]/device_tracker' 215 | ``` 216 | 217 | The standard confidence report will also send. 218 |

219 | 220 |
How do I determine what values to set for filters?

221 | 222 | Try using the verbose logging option `-V` to see what this script sees when a new bluetooth device advertisement is seen. Then, power cycle the bluetooth radio on the device you'd like to track - you'll probably see a pattern develop with flags or manufacturers. Use these values to create your arrival filters! 223 | 224 | Similarly, to set exclude filters, you can observe bluetooth traffic for a period of time to see what devices you simply do not care about seeing. 225 |

226 | 227 | ____ 228 | 229 | ## *Other* 230 | 231 |
It's annoying to have to keep track of mac addresses. Can't I just use a nickname for the mac addresses for MQTT topics?

232 | 233 | Yes, this is default behavior. All you have to do is provide a name next to the address in the `known_static_addresses` file. For example, if you have a known device with the mac address of 00:11:22:33:44:55 that you would like to call "Andrew's Phone": 234 | 235 | ```bash 236 | 00:11:22:33:44:55 Andrew's iPhone 237 | ``` 238 | 239 | Then restart the this script service. The script will now use "andrew_s_iphone" as the final mqtt topic path component. 240 | 241 | ***Important:*** 242 | 243 | * any entry will be made **lowercase** 244 | 245 | * any non-digit or non-decimal character will be replaced with an underscore 246 | 247 | The same is true for beacons in the `known_beacon_addresses` file as well: 248 | 249 | ```bash 250 | 09876543-3333-2222-1111-000000000000-9-10000 Dog 251 | ``` 252 | 253 | To disable this feature, set `PREF_ALIAS_MODE=false` in your `behavior_preferences` file. 254 |

255 | 256 |
I don't care about a few devices that are reporting. Can I block them?

257 | 258 | Yes. Create a file called `address_blacklist` in your configuration directory and add the mac addresses you'd like to block (or uuid-major-minor for iBeacons) one at a time. 259 |

260 | 261 |
I can't use the device_tracker platform with the default status strings of `home` and `not_home` with my home automation software. What can I do?

262 | 263 | Set these options in `behavior_preferences`: 264 | 265 | ```bash 266 | PREF_DEVICE_TRACKER_HOME_STRING='home status string' 267 | PREF_DEVICE_TRACKER_AWAY_STRING='away status string' 268 | PREF_DEVICE_TRACKER_TOPIC_BRANCH='topic path for device tracker/presence tracker' 269 | ``` 270 | 271 | Examples: 272 | 273 | Home Assistant (default): 274 | 275 | ```bash 276 | PREF_DEVICE_TRACKER_HOME_STRING='home' 277 | PREF_DEVICE_TRACKER_AWAY_STRING='not_home' 278 | PREF_DEVICE_TRACKER_TOPIC_BRANCH='device_tracker' 279 | ``` 280 | 281 | SmartThings: 282 | 283 | ```bash 284 | PREF_DEVICE_TRACKER_HOME_STRING='present' 285 | PREF_DEVICE_TRACKER_AWAY_STRING='not present' 286 | PREF_DEVICE_TRACKER_TOPIC_BRANCH='presence' 287 | ``` 288 | 289 | Generic: 290 | 291 | ```bash 292 | PREF_DEVICE_TRACKER_HOME_STRING='home' 293 | PREF_DEVICE_TRACKER_AWAY_STRING='away' 294 | PREF_DEVICE_TRACKER_TOPIC_BRANCH='anything you like' 295 | ``` 296 | 297 |

298 | -------------------------------------------------------------------------------- /support/argv: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------------------------------------------------------------------------- 4 | # GENERAL INFORMATION 5 | # ---------------------------------------------------------------------------------------- 6 | # 7 | # Written by Andrew J Freyer 8 | # GNU General Public License 9 | # http://github.com/andrewjfreyer/monitor 10 | # 11 | # HELP FILE AND GENERAL PREFERENCES 12 | # 13 | # ---------------------------------------------------------------------------------------- 14 | 15 | #---------------------------------------------------------------------------------------- 16 | # REPORT CURRENT VERSION 17 | # ---------------------------------------------------------------------------------------- 18 | 19 | if [ -f ".previous_version" ]; then 20 | previous_version=$(cat ".previous_version") 21 | fi 22 | 23 | #DETERMINE IF UPDATED SINCE LAST RUN 24 | if [ "$previous_version" != "$version" ]; then 25 | 26 | #HAVE WE REPORTED AN UPDATE BEFORE? 27 | [ -z "$previous_version" ] && previous_version="Unknown" 28 | 29 | #UPDATE REPORT 30 | printf "%s\n" "> ${GREEN}updated${NC} $(basename "$0") (v. $previous_version) -> (v. $version)..." 31 | 32 | #RECORD UPDATED VERSION 33 | printf "%s\n" "$version" > ".previous_version" 34 | else 35 | #STANDARD RUN 36 | printf "%s\n" "> ${GREEN}starting${NC} $(basename "$0") (v. $version)..." 37 | fi 38 | 39 | # ---------------------------------------------------------------------------------------- 40 | # HELP TEXT 41 | # ---------------------------------------------------------------------------------------- 42 | 43 | show_help_text() { 44 | #SHOW HELPFULE 45 | printf "%s\n" " 46 | 47 | monitor.sh 48 | 49 | Andrew J Freyer, 2018 50 | https://github.com/andrewjfreyer/monitor 51 | 52 | GNU General Public License 53 | 54 | usage: 55 | 56 | monitor -h show usage information 57 | monitor -R redact private information from logs 58 | monitor -S silent operation (no logging) 59 | monitor -c addr create connection to bluetooth device 60 | monitor -C clean retained messages from MQTT broker 61 | 62 | monitor -V print verbose/debug logging messages 63 | monitor -v print version number 64 | monitor -d restore to default behavior_preferences 65 | monitor -u update 'monitor.service' to current command line settings 66 | (excluding -u,-V, -F, and -d flags) 67 | 68 | monitor -r repeatedly scan for arrival & departure of known devices 69 | monitor -s report all mqtt messages to a single topic with 70 | \$mqtt_topicpath/\$mqtt_publisher_identity (defined in MQTT preferences file) 71 | 72 | monitor -f format MQTT topics with only letters and numbers 73 | monitor -a report all known device scan results, not just changes 74 | monitor -x retain mqtt status messages 75 | monitor -b report bluetooth beacon advertisements (e.g., generic beacons, ibeacons, and so on) 76 | monitor -t[adr] scan for known devices only on mqtt trigger messages: 77 | a \$mqtt_topicpath/scan/ARRIVE (defined in MQTT preferences file) 78 | d \$mqtt_topicpath/scan/DEPART (defined in MQTT preferences file) 79 | r send ARRIVE or DEPART messages to trigger other devices to scan 80 | monitor -D [dir] use alternative directory for configuration files 81 | " 82 | } 83 | 84 | # ---------------------------------------------------------------------------------------- 85 | # PROCESS OPTIONS (technique: https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash) 86 | # ---------------------------------------------------------------------------------------- 87 | 88 | #REMOVE MANUFACTURER CACHE AND PUBLIC NAME CACHE FROM PREVIOUS SESSION 89 | (&>/dev/null rm ".manufacturer_cache") && $PREF_VERBOSE_LOGGING && printf "%s\n" "> removing web request caches" 90 | 91 | #NO LONGER REQUIRED; NAME CACHING SHOULD BE STICKY 92 | #(&>/dev/null rm ".public_name_cache") && printf "%s\n" "> removing public name cache" 93 | 94 | #PROCESS ARGV OPTIONS 95 | OPTIND=1 96 | 97 | #PREFERENCES 98 | PREF_BEACON_MODE=false 99 | PREF_TRIGGER_MODE_ARRIVE=false 100 | PREF_TRIGGER_MODE_DEPART=false 101 | PREF_TRIGGER_MODE_REPORT_OUT=false 102 | PREF_BEACON_MODE=false 103 | PREF_REPORT_ALL_MODE=false 104 | PREF_RESTORE_DEFAULTS=false 105 | PREF_UPDATE_SERVICE=false 106 | PREF_REDACT=false 107 | PREF_SHOULD_RETAIN=false 108 | PREF_CLEAN_MQTT=false 109 | PREF_FORMAT_MQTT=false 110 | PREF_MQTT_REPORT_SCAN_MESSAGES=false 111 | PREF_SERVICE_CHECK=true 112 | PREF_CONFIG_DIR='' 113 | PREF_DISABLE_LOGGING=false 114 | PREF_VERBOSE_LOGGING=false 115 | PREF_FILTER_DEBUG_LOGGING=false 116 | PREF_MQTT_SINGLE_TOPIC_MODE=false 117 | PREF_PERIODIC_MODE=false 118 | 119 | while getopts "h?vfFbut:EgSRCmrsVadxD:c:" opt; do 120 | case "$opt" in 121 | h|\?) 122 | show_help_text 123 | exit 0 124 | ;; 125 | S) 126 | PREF_DISABLE_LOGGING=true && printf "%s\n" "> logging is disabled, although startup messages will still be shown" 127 | ;; 128 | 129 | r) PREF_PERIODIC_MODE=true 130 | printf "%s\n" "> ${ORANGE}warning:${NC} periodic scan mode may cause interference with 2.4GHz networks if run on a Raspberry Pi" 131 | ;; 132 | v) 133 | printf "%s\n" "$VERSION" 134 | exit 0 135 | ;; 136 | F) 137 | PREF_FILTER_DEBUG_LOGGING=true && printf "%s\n" "> ${ORANGE}warning:${NC} filter logging is enabled. this setting is only for informational and debug purposes" 138 | ;; 139 | V) 140 | PREF_VERBOSE_LOGGING=true && printf "%s\n" "> ${ORANGE}warning:${NC} verbose logging is enabled. this setting is only for informational and debugging purposes" 141 | ;; 142 | c) 143 | #GET TARGET MAC ADDRESS 144 | target_mac="$OPTARG" 145 | 146 | #REMOVE PREVIOUS PAIRINGS 147 | printf "%s\n" "> removing previous pairings to $target_mac" 148 | (echo -e "remove $target_mac" | bluetoothctl &>/dev/null) 149 | 150 | #DEBUG ECHO 151 | printf "%s\n" "> creating connection to $target_mac..." 152 | printf "%s\n" "> within 5 seconds, please set $target_mac to discoverable..." 153 | 154 | #WAIT 5 SECONDS FOR DISCOVERABLE MODE 155 | sleep 5 156 | 157 | #CREATE CONNECTION 158 | return_data=$(hcitool cc $target_mac 2>&1 && hcitool auth $target_mac 2>&1 && hcitool dc $target_mac 2>&1) 159 | 160 | #ERROR REPORTING 161 | [[ $return_data =~ .*error.* ]] && printf "%s\n" "> ${RED}error: ${NC}connection to $target_mac failed" || printf "%s\n" "> connection created to $target_mac" 162 | 163 | exit 0 164 | ;; 165 | C) 166 | PREF_CLEAN_MQTT=true && printf "%s\n" "> cleaning retained messages on broker" 167 | ;; 168 | E) PREF_MQTT_REPORT_SCAN_MESSAGES=true && printf "%s\n" "> publishing MQTT .../scan/[arrival|depart]/[start|end]" 169 | ;; 170 | s) PREF_MQTT_SINGLE_TOPIC_MODE=true && printf "%s\n" "> publishing all MQTT presence messages to \$mqtt_topicpath/\$mqtt_publisher_identity" 171 | ;; 172 | x) PREF_SHOULD_RETAIN=true && printf "%s\n" "> retaining mqtt status reports" 173 | ;; 174 | R) PREF_REDACT=true && printf "%s\n" "> private information redacted from logs" 175 | ;; 176 | d) PREF_RESTORE_DEFAULTS=true && printf "%s\n" "> restoring default settings" 177 | ;; 178 | u) PREF_UPDATE_SERVICE=true && printf "%s\n" "> updating monitor.service" 179 | ;; 180 | f) PREF_FORMAT_MQTT=true && $PREF_VERBOSE_LOGGING && printf "%s\n" "> only allow letters, numbers, and spaces in mqtt topic paths" 181 | ;; 182 | b) PREF_BEACON_MODE=true && $PREF_VERBOSE_LOGGING && printf "%s\n" "> generic bluetooth beacon, ibeacon, and known beacon address reporting mode enabled" 183 | ;; 184 | t) #DO WE INCLUDE THE REPORTING TRIGGER? 185 | [[ $OPTARG = *r* ]] && PREF_TRIGGER_MODE_REPORT_OUT=true && $PREF_VERBOSE_LOGGING && printf "%s\n" "> trigger mode: report out MQTT arrive/depart scan triggers to other devices" 186 | #SORT THROUGH REMAINING FILTERS 187 | case "$OPTARG" in 188 | r) PREF_TRIGGER_MODE_REPORT_OUT=true 189 | ;; 190 | d|rd|dr) PREF_TRIGGER_MODE_DEPART=true && $PREF_VERBOSE_LOGGING && printf "%s\n" "> trigger mode: depart scan only on MQTT trigger" 191 | ;; 192 | a|ra|ar) PREF_TRIGGER_MODE_ARRIVE=true && $PREF_VERBOSE_LOGGING && printf "%s\n" "> trigger mode: arrive scan only on MQTT trigger" 193 | ;; 194 | da|ad|rda|rad|dra|ard|dar|adr) PREF_TRIGGER_MODE_ARRIVE=true && PREF_TRIGGER_MODE_DEPART==true && $PREF_VERBOSE_LOGGING && printf "%s\n" "> trigger mode: scan only (both on arrive and depart) on trigger" 195 | ;; 196 | *) printf "%s\n" "> ${ORANGE}warning: ${NC}unknown trigger mode: $OPTARG" 197 | ;; 198 | esac 199 | ;; 200 | a) PREF_REPORT_ALL_MODE=true && $PREF_VERBOSE_LOGGING && printf "%s\n" "> report all scan results mode enabled" 201 | ;; 202 | D) PREF_CONFIG_DIR=$OPTARG && printf "%s\n" "> using custom config directory [$PREF_CONFIG_DIR]" 203 | [ ! -d "$PREF_CONFIG_DIR" ] && printf "%s\n" "> ${RED}error: ${NC}config directory [$PREF_CONFIG_DIR] doesn't exist" && exit 1 204 | ;; 205 | *) printf "%s\n" "> unknown or depreciated argument: $opt" 206 | esac 207 | done 208 | 209 | #RESET OPTION INDEX 210 | shift $((OPTIND-1)) 211 | 212 | #SHIFT IF NECESSARY 213 | [ "$1" = "--" ] && shift 214 | -------------------------------------------------------------------------------- /support/btle: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------------------------------------------------------------------------- 4 | # GENERAL INFORMATION 5 | # ---------------------------------------------------------------------------------------- 6 | # 7 | # Written by Andrew J Freyer 8 | # GNU General Public License 9 | # http://github.com/andrewjfreyer/monitor 10 | # 11 | # BLUETOOTH SCANNING AND PACKET PROCESSING 12 | # 13 | # ---------------------------------------------------------------------------------------- 14 | 15 | 16 | # ---------------------------------------------------------------------------------------- 17 | # BLUETOOTH LE BACKGROUND SCANNING 18 | # ---------------------------------------------------------------------------------------- 19 | btle_scanner () { 20 | 21 | #PREVENT BLUETOOTH SCANNING IF IN TRIGGER-ONLY MODE 22 | if [ "$PREF_TRIGGER_MODE_ARRIVE" == true ] && [ "$PREF_TRIGGER_MODE_DEPART" == true ]; then 23 | return 0 24 | fi 25 | 26 | #IF TRIGGER ONLY MODE FOR ARRIVE AND BEACONS DISABLED 27 | if [ "$PREF_TRIGGER_MODE_ARRIVE" == true ] && [ "$PREF_BEACON_MODE" == false ]; then 28 | return 0 29 | fi 30 | 31 | while true; do 32 | #TIMEOUT THE HCITOOL SCAN TO RESHOW THE DUPLICATES WITHOUT SPAMMING THE MAIN LOOP BY USING THE --DUPLICATES TAG 33 | local hcitool_raw=$(timeout --signal SIGINT 60 hcitool -i $PREF_HCI_DEVICE lescan 2>&1) 34 | 35 | #FIND ERROR VALUES 36 | local error=$(echo "$hcitool_raw" | grep -iE 'input/output error|invalid device|invalid|error|network') 37 | 38 | #PUBLISH ERRORS BACK TO MAIN LOOP 39 | if [ -n "$error" ]; then 40 | 41 | (>&2 log "${GREEN}[CMD-INFO]${NC} cycling bluetooth hardware on $PREF_HCI_DEVICE. please wait a moment...") 42 | 43 | #POWER CYCLE 44 | hciconfig $PREF_HCI_DEVICE down 45 | sleep 5 46 | hciconfig $PREF_HCI_DEVICE up 47 | 48 | continue 49 | fi 50 | 51 | #SEND SIGNAL THAT 52 | printf "BOFF\n" > main_pipe 53 | 54 | #NEED DELAY TO BE SURE WE'RE NOT RESTARTING HARDWARE TOO QUICKLY 55 | sleep 5 56 | done 57 | 58 | #REPORT ERROR 59 | (>&2 echo "error! irrecoverable btle_scanner error") 60 | } 61 | 62 | # ---------------------------------------------------------------------------------------- 63 | # PROCESS BLUETOOTH PACKETS 64 | # ---------------------------------------------------------------------------------------- 65 | 66 | btle_packet_processor () { 67 | #POPULATE PACKET 68 | local packet=$(echo "$1" | sed 's/> *//g;s/ / /gi') 69 | 70 | #BEACON PACKET? 71 | if [[ $packet =~ ^04\ 3E\ 2[AB]\ 02\ 01\ .{26}\ 02\ 01\ .{14}\ 02\ 15 ]]; then 72 | 73 | #HARDARE MAC AND PDU HEADER 74 | local received_mac_address=$(echo "$packet" | awk '{print $13":"$12":"$11":"$10":"$9":"$8}') 75 | 76 | #RAW VALUES 77 | local UUID=$(echo $packet | sed 's/^.\{69\}\(.\{47\}\).*$/\1/') 78 | local MAJOR=$(echo $packet | sed 's/^.\{117\}\(.\{5\}\).*$/\1/') 79 | local MINOR=$(echo $packet | sed 's/^.\{123\}\(.\{5\}\).*$/\1/') 80 | local POWER=$(echo $packet | sed 's/^.\{129\}\(.\{2\}\).*$/\1/') 81 | local UUID=$(echo $UUID | sed -e 's/\ //gi' -e 's/^\(.\{8\}\)\(.\{4\}\)\(.\{4\}\)\(.\{4\}\)\(.\{12\}\)$/\1-\2-\3-\4-\5/') 82 | 83 | #MAJOR CALCULATION 84 | MAJOR=$(echo $MAJOR | sed 's/\ //gi') 85 | MAJOR=$(echo "ibase=16; $MAJOR" | bc) 86 | 87 | #MINOR CALCULATION 88 | MINOR=$(echo $MINOR | sed 's/\ //gi') 89 | MINOR=$(echo "ibase=16; $MINOR" | bc) 90 | 91 | #POWER CALCULATION 92 | POWER=$(echo "ibase=16; $POWER" | bc) 93 | POWER=$((POWER - 256)) 94 | 95 | #RSSI CALCULATION 96 | local RSSI=$(echo $packet | sed 's/^.\{132\}\(.\{2\}\).*$/\1/') 97 | RSSI=$(echo "ibase=16; $RSSI" | bc) 98 | RSSI=$((RSSI - 256)) 99 | 100 | timestamp=$(date +%s) 101 | 102 | #SEND TO MAIN LOOP 103 | [ -n "$UUID" ] && printf "BEAC$UUID|$MAJOR|$MINOR|$RSSI|$POWER|$received_mac_address|$timestamp\n" > main_pipe 104 | fi 105 | } 106 | 107 | 108 | 109 | #---------------------------------------------------------------------------------------- 110 | # PROCESS BLUETOOTH PACKETS 111 | # ---------------------------------------------------------------------------------------- 112 | 113 | trim_whitespace () { 114 | [[ "$1" =~ [^[:space:]](.*[^[:space:]])? ]] && printf "%s" "$BASH_REMATCH" 115 | } 116 | 117 | 118 | btle_text_processor () { 119 | local received_mac_address 120 | local packet 121 | local pdu_type 122 | local rssi_value 123 | local gap_name_str 124 | local should_ignore 125 | local manufacturer 126 | local flags 127 | local device_type 128 | local oem_data 129 | local version 130 | local resolvable 131 | local hex_data 132 | 133 | #FILTER DEBUG STUFF 134 | local mfcg_filter_color 135 | local flag_filter_color 136 | local pdut_filter_color 137 | 138 | local mfcg_pass_filter_color 139 | local flag_pass_filter_color 140 | local full_packet 141 | 142 | #BEACON 143 | local uuid 144 | local tx_power 145 | 146 | 147 | if [ -n "$1" ]; then 148 | #DEFINE PACKET VARIABLE 149 | packet="$1" 150 | 151 | #RETUREN FROM NAME REQUEST 152 | if [[ $packet =~ Event:\ Remote ]]; then 153 | return 0 154 | fi 155 | 156 | #------------ MAC ADDRESS 157 | if [[ $packet =~ Address:\ ([^~\(]*)\ ]]; then 158 | received_mac_address=${BASH_REMATCH[1]} 159 | received_mac_address=${received_mac_address//\|/} 160 | received_mac_address=${received_mac_address^^} 161 | else 162 | return 0 163 | fi 164 | 165 | #------------ PDU TYPE 166 | if [[ $packet =~ Event\ type:\ ([^~\(]*)\ ]]; then 167 | pdu_type=${BASH_REMATCH[1]} 168 | pdu_type=${pdu_type//\|/} 169 | pdu_type=${pdu_type##* } 170 | else 171 | return 0 172 | fi 173 | 174 | #------------ ADVERTISEMENT TYPE 175 | if [[ $packet =~ Random ]] || [[ $packet =~ Public ]]; then 176 | tx_type=${BASH_REMATCH^^} 177 | tx_type=${tx_type//\|/} 178 | tx_type=${tx_type:0:4} 179 | fi 180 | 181 | #------------ RSSI 182 | if [[ $packet =~ RSSI[^:]*?:([^~\(]*) ]]; then 183 | rssi_value=${BASH_REMATCH[1]} 184 | rssi_value=${rssi_value//\|/} 185 | rssi_value=${rssi_value//[^-0-9]/} 186 | fi 187 | 188 | #------------ NAME 189 | if [[ $packet =~ Name[^:]{0,}:\ {0,}([^~\(]*)\ {0,}(~|$|\() ]]; then 190 | gap_name_str=${BASH_REMATCH[1]} 191 | gap_name_str=${gap_name_str//\|/} 192 | gap_name_str=$(trim_whitespace "$gap_name_str") 193 | fi 194 | 195 | #------------ MANUFACTURER OF RADIO 196 | if [[ $packet =~ Company:\ {0,}([^|\(]*)\ {0,}(~|$|\() ]]; then 197 | manufacturer=${BASH_REMATCH[1]} 198 | manufacturer=${manufacturer//\|/} 199 | manufacturer=$(trim_whitespace "$manufacturer") 200 | fi 201 | 202 | #------------ ADVERTISEMENT 203 | if [[ $packet =~ Data:\ {0,}([^~\(]*)\ {0,}(~|$|\() ]]; then 204 | adv_data=${BASH_REMATCH[1]} 205 | adv_data=${adv_data//\|/} 206 | fi 207 | 208 | #------------ DEVICE TYPE 209 | if [[ $packet =~ Type:\ {0,}([^~\(]*)\ (~|$|\() ]]; then 210 | device_type=${BASH_REMATCH[1]} 211 | device_type=${device_type//\|/} 212 | fi 213 | 214 | #------------ FLAGS 215 | if [[ $packet =~ Flags:\ {0,}([^~\(]*)(~|$|\() ]]; then 216 | flags=${BASH_REMATCH[1]} 217 | flags=${flags//\|/} 218 | fi 219 | 220 | #------------ HEX DATA 221 | if [[ $packet =~ [a-f0-9-]{8,} ]]; then 222 | hex_data=$BASH_REMATCH 223 | hex_data=${hex_data//\|/} 224 | fi 225 | 226 | #------------ OEM DATA 227 | if [[ $packet =~ 1\ entry~([^|\(]{1,}) ]]; then 228 | oem_data=${BASH_REMATCH[1]} 229 | oem_data=${oem_data//\|/} 230 | oem_data=$(trim_whitespace "$oem_data") 231 | 232 | #IS THIS A VENDOR-SPECIFIC UUID? 233 | [[ $oem_data =~ Vendor\ specific ]] && oem_data="" 234 | 235 | #OVERWRITE OEMS 236 | [ -n "$oem_data" ] && [ -n "$manufacturer" ] && manufacturer="$manufacturer for $oem_data" 237 | [ -n "$oem_data" ] && manufacturer=${manufacturer:-$oem_data} 238 | fi 239 | 240 | #------------ RESOLVABILITY 241 | if [[ $packet =~ \(Non-Resolvable\) ]] || [[ $packet =~ \(Resolvable\) ]]; then 242 | resolvable=${BASH_REMATCH^^} 243 | resolvable=${resolvable//-/_} 244 | resolvable=${resolvable//\|/} 245 | resolvable=${resolvable//[^A-Z_]/} 246 | fi 247 | 248 | local timestamp 249 | timestamp=$(date +%s) 250 | 251 | #MOVE IGNORE FILTER HERE TO BE SURE 252 | if [ ${PREF_FILTER_DEBUG_LOGGING:-false} == true ]; then 253 | 254 | #FORMATTING FOR FUN AND PROFIT 255 | [[ ${flags,,} =~ ${PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE,,} ]] && flag_filter_color=${RED} || flag_filter_color=${GREEN} 256 | [[ ${manufacturer,,} =~ ${PREF_FAIL_FILTER_MANUFACTURER_ARRIVE,,} ]] && mfcg_filter_color=${RED} || mfcg_filter_color=${GREEN} 257 | [[ ${flags,,} =~ ${PREF_PASS_FILTER_ADV_FLAGS_ARRIVE,,} ]] && flag_pass_filter_color=${GREEN} || flag_pass_filter_color=${RED} 258 | [[ ${manufacturer,,} =~ ${PREF_PASS_FILTER_MANUFACTURER_ARRIVE,,} ]] && mfcg_pass_filter_color=${GREEN} || mfcg_pass_filter_color=${RED} 259 | [[ ${pdu_type,,} =~ ${PREF_PASS_FILTER_PDU_TYPE,,} ]] && pdut_filter_color=${GREEN} || pdut_filter_color=${RED} 260 | 261 | #REASSEMBLE FULL 262 | full_packet=$(echo "$packet" | tr "~" "\n") 263 | 264 | #PRINT DEBUG MESSAGES 265 | printf "\n%s\n" "${PURPLE}***************** ARRIVE/REJECT FILTER DEBUG ********************** ${NC}" 266 | printf "%s\n" "btmon packet:" 267 | printf "%s\n" "$full_packet" 268 | printf "%s\n" "flag pass: ${flag_pass_filter_color}only (${PREF_PASS_FILTER_ADV_FLAGS_ARRIVE,,})${NC}" 269 | printf "%s\n" "mfcg pass: ${mfcg_pass_filter_color}only (${PREF_PASS_FILTER_MANUFACTURER_ARRIVE,,})${NC}" 270 | printf "%s\n" "flag fail: ${flag_filter_color}not (${PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE,,})${NC}" 271 | printf "%s\n" "mfcg fail: ${mfcg_filter_color}not (${PREF_FAIL_FILTER_MANUFACTURER_ARRIVE,,})${NC}" 272 | printf "%s\n" "pdu filter: ${pdut_filter_color}only (${PREF_PASS_FILTER_PDU_TYPE,,})${NC}" 273 | [ -n "${blacklisted_devices["$received_mac_address"]}" ] && printf "%s\n" "blacklist: ${RED}true${NC}" || printf "%s\n" "blacklist: ${GREEN}false${NC}" 274 | printf "%s\n" "" 275 | printf "%s\n" "type: ${CYAN}${tx_type:-???}${NC}" 276 | printf "%s\n" "mac: ${CYAN}${received_mac_address:-???}${NC}" 277 | printf "%s\n" "name: ${CYAN}${gap_name_str:-???}${NC}" 278 | printf "%s\n" "pdu: ${CYAN}${pdu_type:-???}${NC}" 279 | printf "%s\n" "type: ${CYAN}${device_type:-???}${NC}" 280 | printf "%s\n" "oem: ${CYAN}${oem_data:-???}${NC}" 281 | printf "%s\n" "rssi: ${CYAN}${rssi_value:-???} dBm${NC}" 282 | printf "%s\n" "flags: ${CYAN}${flags:-???}${NC}" 283 | printf "%s\n" "hex: ${CYAN}${hex_data:-???}${NC}" 284 | printf "%s\n" "resolvable: ${CYAN}${resolvable:-???}${NC}" 285 | printf "%s\n" "man: ${CYAN} ${manufacturer:-???}${NC}" 286 | printf "%s\n" "time: ${CYAN} ${timestamp:-???}${NC}" 287 | printf "%s\n" "" 288 | fi 289 | 290 | #IF BLACLISTED, PREVENT FROM GOING HOME 291 | [ -n "${blacklisted_devices[$received_mac_address]}" ] && return 0 292 | 293 | if [ "$tx_type" == "PUBL" ]; then 294 | #PRINT ALL PUBLIC BEACONS 295 | printf "%s\n" "$tx_type$received_mac_address|$pdu_type|$gap_name_str|$rssi_value|$adv_data|$manufacturer|$device_type|$flags|$oem_data|$timestamp|$resolvable|$hex_data" > main_pipe 296 | 297 | elif [ "$tx_type" == "RAND" ]; then 298 | 299 | #FAIL FLAGS HERE 300 | if [[ ${flags,,} =~ ${PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE,,} ]] || 301 | [[ ${manufacturer,,} =~ ${PREF_FAIL_FILTER_MANUFACTURER_ARRIVE,,} ]]; then 302 | return 0 303 | fi 304 | 305 | #PDU FILTERING 306 | if [[ ${pdu_type,,} =~ ${PREF_PASS_FILTER_PDU_TYPE,,} ]] ; then 307 | 308 | #IF THIS DOES NOT HAVE A UUID, PROCESS AS GENERIC BEACON 309 | should_ignore=false 310 | [ "$tx_type" == "RAND" ] && [ -n "$rssi_value" ] && [[ "$PREF_RSSI_IGNORE_BELOW" -gt "$rssi_value" ]] && should_ignore=true 311 | 312 | #IF WE HAVE A NAME, DEFINITELY REPORT 313 | [ -n "$gap_name_str" ] && should_ignore=false 314 | 315 | if [ "$should_ignore" == false ]; then 316 | #REPORT TO MAIN LOOP 317 | printf "%s\n" "$tx_type$received_mac_address|$pdu_type|$gap_name_str|$rssi_value|$adv_data|$manufacturer|$device_type|$flags|$oem_data|$timestamp|$resolvable|$hex_data" > main_pipe 318 | fi 319 | fi 320 | fi 321 | fi 322 | 323 | } 324 | 325 | # ---------------------------------------------------------------------------------------- 326 | # BLUETOOTH LE RAW PACKET ANALYSIS 327 | # ---------------------------------------------------------------------------------------- 328 | btle_text_listener () { 329 | 330 | #PREVENT BLUETOOTH SCANNING IF IN TRIGGER-ONLY MODE 331 | if [ "$PREF_TRIGGER_MODE_ARRIVE" == true ] && [ "$PREF_TRIGGER_MODE_DEPART" == true ]; then 332 | return 0 333 | fi 334 | 335 | #IF TRIGGER ONLY MODE FOR ARRIVE AND BEACONS DISABLED 336 | if [ "$PREF_TRIGGER_MODE_ARRIVE" == true ] && [ "$PREF_BEACON_MODE" == false ]; then 337 | return 0 338 | fi 339 | 340 | #LOCAL VALUES 341 | local packet 342 | local line 343 | 344 | while true; do 345 | 346 | #MAINTAIN PACKET PIPE 347 | printf "%s\n" "" > packet_pipe 348 | 349 | #DEFINE VARAIBLES 350 | while IFS= read -r line; do 351 | #maintain the pipe for faster machines. 352 | printf "%s\n" "" > packet_pipe 353 | if [[ $line =~ ^\> ]] || [[ $line =~ ^\< ]] || [[ $line =~ RSSI ]]; then 354 | [[ $line =~ RSSI ]] && packet="$packet~$line" 355 | if [[ $packet =~ ^\> ]]; then 356 | printf "%s\n" "1$packet" > packet_pipe 357 | fi 358 | packet="" 359 | packet=$line 360 | else 361 | packet="$packet~$line" 362 | fi 363 | done < <(timeout --signal SIGINT 90 stdbuf -oL -eL btmon 2>&1) 364 | 365 | #PREVENT LOOPING 366 | sleep 1 367 | done 368 | #REPORT ERROR 369 | (>&2 echo "error! irrecoverable btle_text_listener error") 370 | } 371 | 372 | # ---------------------------------------------------------------------------------------- 373 | # BLUETOOTH LE RAW PACKET ANALYSIS 374 | # ---------------------------------------------------------------------------------------- 375 | btle_listener () { 376 | 377 | #PREVENT BLUETOOTH SCANNING IF IN TRIGGER-ONLY MODE 378 | if [ "$PREF_TRIGGER_MODE_ARRIVE" == true ] && [ "$PREF_TRIGGER_MODE_DEPART" == true ]; then 379 | return 0 380 | fi 381 | 382 | #IF TRIGGER ONLY MODE FOR ARRIVE AND BEACONS DISABLED 383 | if [ "$PREF_TRIGGER_MODE_ARRIVE" == true ] && [ "$PREF_BEACON_MODE" == false ]; then 384 | return 0 385 | fi 386 | 387 | while true; do 388 | #LOCAL VALUES 389 | local packet 390 | local line 391 | local complete_packet_str_length 392 | complete_packet_str_length=0 393 | 394 | while IFS= read -r line; do 395 | # packets start with ">" or "<" 396 | if [[ $line =~ ^\> ]] || [[ $line =~ ^\< ]]; then 397 | # process the completed packet (unless this is the first time through) 398 | if [ "$packet" ] && [[ $packet =~ ^\> ]]; then 399 | printf "%s\n" "0$packet" > packet_pipe 400 | fi 401 | # start the new packet 402 | packet=$line 403 | complete_packet_str_length=0 404 | 405 | if [[ "$packet" =~ ^\>\ 04\ 3E* ]]; then 406 | local packet_len=$(echo $packet | awk '{print "ibase=16;"$4}' | bc ) 407 | complete_packet_str_length=$((( packet_len + 3) * 3 + 1)) 408 | fi 409 | else 410 | # continue building the packet 411 | packet="$packet $line" 412 | 413 | #PROCESS A PACKET WHEN IT IS COMPLETE 414 | if [ "$complete_packet_str_length" == "${#packet}" ]; then 415 | printf "%s\n" "0$packet" > packet_pipe 416 | packet="" 417 | fi 418 | fi 419 | done < <(timeout --signal SIGINT 120 stdbuf -oL -eL hcidump -i $PREF_HCI_DEVICE --raw) 420 | 421 | #PREVENT INFINITE LOOPING 422 | sleep 1 423 | done 424 | } 425 | 426 | 427 | # ---------------------------------------------------------------------------------------- 428 | # BLUETOOTH PACKET PROCESSING 429 | # ---------------------------------------------------------------------------------------- 430 | btle_packet_listener () { 431 | local cmd 432 | local data 433 | local event 434 | 435 | #PREVENT BLUETOOTH SCANNING IF IN TRIGGER-ONLY MODE 436 | if [ "$PREF_TRIGGER_MODE_ARRIVE" == true ] && [ "$PREF_TRIGGER_MODE_DEPART" == true ]; then 437 | return 0 438 | fi 439 | 440 | #IF TRIGGER ONLY MODE FOR ARRIVE AND BEACONS DISABLED 441 | if [ "$PREF_TRIGGER_MODE_ARRIVE" == true ] && [ "$PREF_BEACON_MODE" == false ]; then 442 | return 0 443 | fi 444 | 445 | #LOOPER 446 | while true; do 447 | cmd="" 448 | data="" 449 | 450 | #READ FROM THE MAIN PIPE 451 | while read -r event; do 452 | #DIVIDE EVENT MESSAGE INTO TYPE AND DATA 453 | cmd="${event:0:1}" 454 | data="${event:1}" 455 | 456 | if [ "$cmd" == "0" ]; then 457 | #RAW DATA PROCESSING 458 | btle_packet_processor "$data" 459 | 460 | elif [ "$cmd" == "1" ]; then 461 | 462 | #TEXT DATA PROCESSING 463 | btle_text_processor "$data" 464 | fi 465 | 466 | done < packet_pipe 467 | 468 | #DELAY TO PREVENT LOOPING 469 | sleep 1 470 | done 471 | 472 | #REPORT ERROR 473 | (>&2 echo "error! irrecoverable btle_packet_listener error") 474 | } 475 | 476 | -------------------------------------------------------------------------------- /support/data: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------------------------------------------------------------------------- 4 | # GENERAL INFORMATION 5 | # ---------------------------------------------------------------------------------------- 6 | # 7 | # Written by Andrew J Freyer 8 | # GNU General Public License 9 | # http://github.com/andrewjfreyer/monitor 10 | # 11 | # PACKET INFORMATION GATHERING 12 | # 13 | # ---------------------------------------------------------------------------------------- 14 | 15 | # ---------------------------------------------------------------------------------------- 16 | # OBTAIN MANUFACTURER INFORMATION FOR A PARTICULAR BLUETOOTH MAC ADDRESS 17 | # ---------------------------------------------------------------------------------------- 18 | determine_manufacturer () { 19 | 20 | #IF NO ADDRESS, RETURN BLANK 21 | if [ -n "$1" ]; then 22 | local address="$1" 23 | 24 | #VERIFY THIS IS A MAC ADDRESS 25 | [[ $address =~ ([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2} ]] && manufacturer="Unknown" 26 | 27 | #SET THE FILE IF IT DOESN'T EXIST 28 | [ ! -f ".manufacturer_cache" ] && echo "" > ".manufacturer_cache" 29 | 30 | #CHECK CACHE 31 | local manufacturer=$(grep "${address:0:8}" < ".manufacturer_cache" | awk -F "\t" '{print $2}' | sort -u | head -1) 32 | 33 | #EXCEPTION FOR SUSPECT PRODUCTS 34 | [[ $address =~ ^FF:FF ]] && manufacturer="SIG Unassigned or Noncompliant OEM" 35 | 36 | # If no cache try if we have ieee-data 37 | if [ -z "$manufacturer" ] ; then 38 | _temp=$(echo ${address:0:8} | sed 's/://gi') 39 | _found=false 40 | local remote_result='' 41 | if [ ! ${_found} ] && [ -f /var/lib/ieee-data/oui.csv ] ; then 42 | remote_result=$(egrep ",${_temp}," /var/lib/ieee-data/oui.csv | cut -d\, -f3) 43 | _found=true 44 | fi 45 | if [ ! ${_found} ] && [ -f /usr/share/ieee-data/oui.csv ] ; then 46 | remote_result=$(egrep ",${_temp}," /usr/share/ieee-data/oui.csv | cut -d\, -f3) 47 | _found=true 48 | fi 49 | [ -n "$remote_result" ] && echo "${address:0:8} $remote_result" >> .manufacturer_cache 50 | manufacturer="$remote_result" 51 | fi 52 | 53 | #IF CACHE DOES NOT EXIST, USE MACVENDORS.COM 54 | if [ -z "$manufacturer" ]; then 55 | local remote_result=$(curl -sL https://api.macvendors.com/${address:0:8} | grep -vi "error" | head -1 | sed 's/[^A-Za-z ]//gi') 56 | 57 | #MAKE SURE WE DONT' SCREW UP THE MANUFACTUERE'S FILE 58 | if [[ ! "$remote_result" = *html* ]]; then 59 | [ -n "$remote_result" ] && echo "${address:0:8} $remote_result" >> .manufacturer_cache 60 | manufacturer="$remote_result" 61 | fi 62 | fi 63 | 64 | #SET DEFAULT MANUFACTURER 65 | [ -z "$manufacturer" ] && manufacturer="Unknown" && echo "${address:0:8} Unknown" >> .manufacturer_cache 66 | echo "$manufacturer" 67 | fi 68 | } 69 | -------------------------------------------------------------------------------- /support/init: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------------------------------------------------------------------------- 4 | # GENERAL INFORMATION 5 | # ---------------------------------------------------------------------------------------- 6 | # 7 | # Written by Andrew J Freyer 8 | # GNU General Public License 9 | # http://github.com/andrewjfreyer/monitor 10 | # 11 | # SETUP NECESSARY FILES 12 | # 13 | # ---------------------------------------------------------------------------------------- 14 | 15 | #---------------------------------------------------------------------------------------- 16 | # CHECK DEPENDENCES 17 | # ---------------------------------------------------------------------------------------- 18 | 19 | #FIND DEPENDENCY PATHS, ELSE MANUALLY SET 20 | service_path="/etc/systemd/system/monitor.service" 21 | mosquitto_pub_path=$(which mosquitto_pub) 22 | mosquitto_sub_path=$(which mosquitto_sub) 23 | hcidump_path=$(which hcidump) 24 | btmon_path=$(which btmon) 25 | bc_path=$(which bc) 26 | git_path=$(which git) 27 | 28 | #RUNTYPE 29 | SERVICE_ACTIVE=true 30 | which systemctl >/dev/null && systemctl is-active --quiet monitor && SERVICE_ACTIVE=true || SERVICE_ACTIVE=false 31 | 32 | #VERSION CHECKER 33 | mosquitto_sub_version=$($mosquitto_sub_path --help | grep -Eo "version [0-9.]{3,}" | sed 's/[^0-9.]//gi') 34 | mosquitto_pub_version=$($mosquitto_pub_path --help | grep -Eo "version [0-9.]{3,}" | sed 's/[^0-9.]//gi') 35 | 36 | #error CHECKING FOR MOSQUITTO PUBLICATION 37 | should_exit=false 38 | 39 | #BINARY CHECKS 40 | [ -z "$mosquitto_pub_path" ] && printf "%s\n" ">${RED} error:${NC} required package 'mosquitto_pub' not found. please install 'mosquitto' and 'mosquitto-clients'." && should_exit=true 41 | [ -z "$mosquitto_sub_path" ] && printf "%s\n" ">${RED} error:${NC} required package 'mosquitto_sub' not found. please install 'mosquitto' and 'mosquitto-clients'." && should_exit=true 42 | [ -z "$btmon_path" ] && printf "%s\n" ">${RED} error:${NC} required package 'btmon' not found. please install 'btmon'." && should_exit=true 43 | [ -z "$hcidump_path" ] && printf "%s\n" ">${RED} error:${NC} required package 'hcidump' not found. please install 'bluez-hcidump' (e.g., sudo apt-get install bluez-hcidump)." && should_exit=true 44 | [ -z "$bc_path" ] && printf "%s\n" ">${RED} error:${NC} required package 'bc' not found. please install 'bc' (e.g., apt-get install bc)" && should_exit=true 45 | [ -z "$git_path" ] && printf "%s\n" "${ORANGE}> warning:${NC} Recommended package 'git' not found. please consider installing for regular updates." 46 | [ ! -f /.dockerenv ] && [ ! -z "$(which systemctl)" ] && [ ! -e "$service_path" ] && printf "%s\n" "${ORANGE}> warning:${NC} monitor.service not installed. Install service? (y/n)" && read should_install 47 | 48 | #BASH VERSION 4.4+ 49 | [[ ! ${BASH_VERSION//[^0-9.]/} =~ ^(4.[4-9]{1,}.?|^[5-9].?) ]] && printf "%s\n" ">${ORANGE} warning:${NC} recommended minimum bash version 4.4+ not found. please consider updating." 50 | 51 | #MOSQUITTO V. 1.5+ 52 | [[ ! $mosquitto_sub_version =~ (^1.[5-9]{1,}.?|^[2-9].[0-9]{1,}.?) ]] && printf "%s\n" ">${RED} error:${NC} minimum required mosquitto_sub version 1.5+ not found. please update." && should_exit=true 53 | [[ ! $mosquitto_pub_version =~ (^1.[5-9]{1,}.?|^[2-9].[0-9]{1,}.?) ]] && printf "%s\n" ">${RED} error:${NC} minimum required mosquitto_pub version 1.5+ not found. please update." && should_exit=true 54 | 55 | #BASE DIRECTORY REGARDLESS OF INSTALLATION; ELSE MANUALLY SET HERE 56 | base_directory=$(dirname "$(readlink -f "$0")") 57 | [ ! -z "$PREF_CONFIG_DIR" ] && printf "%s\n" "using $PREF_CONFIG_DIR as config dir" && base_directory="$PREF_CONFIG_DIR" 58 | 59 | #SET THE NAME CACHE IF IT DOESN'T EXIST 60 | [ ! -f "$base_directory/.public_name_cache" ] && printf "%s\n" "" > "$base_directory/.public_name_cache" 61 | [ ! -f "$base_directory/address_blacklist" ] && printf "%s\n" "#LIST MAC ADDRESSES TO IGNORE, ONE PER LINE: 62 | " > "$base_directory/address_blacklist" 63 | 64 | #BLACKLISTED ADDRESSES 65 | ADDRESS_BLACKLIST="$base_directory/address_blacklist" 66 | 67 | #MQTT ALIAS FILE 68 | ALIAS_CONFIG="$base_directory/mqtt_aliases" 69 | 70 | #---------------------------------------------------------------------------------------- 71 | # CHECK MQTT CONFIGURATION FILES 72 | # ---------------------------------------------------------------------------------------- 73 | 74 | #MQTT PREFERENCES 75 | MQTT_CONFIG="$base_directory/mqtt_preferences" 76 | 77 | if [ -f $MQTT_CONFIG ] ; then 78 | source $MQTT_CONFIG 79 | 80 | #errorS 81 | [ "$mqtt_address" == "0.0.0.0" ] && printf "%s\n" ">${RED} error:${NC} please customize mqtt broker address in: mqtt_preferences" && should_exit=true 82 | [ "$mqtt_user" == "username" ] || [ -z "$mqtt_user" ] && printf "%s\n" "${ORANGE}> warning:${NC} for security purposes, please consider changing 'username' in: mqtt_preferences" 83 | [ "$mqtt_password" == "password" ] || [ -z "$mqtt_password" ] && printf "%s\n" "${ORANGE}> warning:${NC} for security purposes, please consider changing 'password' in: mqtt_preferences" 84 | [ ! -f "$mqtt_certificate_path" ] && [ ! -z "$mqtt_certificate_path" ] && printf "%s\n" ">${RED} error:${NC} please check mqtt certificate path in mqtt_preferences" && should_exit=true 85 | 86 | #WARNINGS 87 | [ -z "$mqtt_port" ] && printf "%s\n" "${ORANGE}> warning:${NC} variable mqtt_port does not appear in mqtt_preferences. using default port 1883". && mqtt_port="1883" 88 | [ -z "$mqtt_publisher_identity" ] && printf "%s\n" "${ORANGE}> warning:${NC} variable mqtt_publisher_identity does not appear in: mqtt_preferences. using hostname: $(hostname)". && mqtt_publisher_identity="$(hostname)" 89 | 90 | #DEFINE CA APPEND 91 | if [ -f "$mqtt_certificate_path" ]; then 92 | #DEFINE APPEND TO USE FOR MQTT 93 | mqtt_ca_file_append="--cafile $mqtt_certificate_path" 94 | fi 95 | 96 | #DEFINE CA APPEND 97 | if [ ! -z "$mqtt_version" ]; then 98 | #DEFINE APPEND TO USE FOR MQTT 99 | mqtt_version_append="-V $mqtt_version" 100 | fi 101 | 102 | #DEFINE PASSWORD APPEND 103 | if [ -z "$mqtt_user" ] && [ -z "$mqtt_password" ]; then 104 | #WARNING 105 | printf "%s\n" "${ORANGE}> warning:${NC} operating in anonymous and unprotected mode for mqtt broker at $mqtt_address" 106 | 107 | else 108 | #PREPEND COLON FOR FULL URL PROCESSING 109 | mqtt_password=":$mqtt_password@" 110 | fi 111 | 112 | #SET UP PUB/SUB URL 113 | mqtt_url="mqtt://$mqtt_user$mqtt_password$mqtt_address:$mqtt_port/" 114 | 115 | else 116 | printf "%s\n" "> mosquitto preferences file created. please customize." 117 | 118 | printf "%s\n" "# --------------------------- 119 | # 120 | # MOSQUITTO PREFERENCES 121 | # 122 | # --------------------------- 123 | 124 | # IP ADDRESS OR HOSTNAME OF MQTT BROKER 125 | mqtt_address=0.0.0.0 126 | 127 | # MQTT BROKER USERNAME 128 | mqtt_user=username 129 | 130 | # MQTT BROKER PASSWORD 131 | mqtt_password=password 132 | 133 | # MQTT PUBLISH TOPIC ROOT 134 | mqtt_topicpath=monitor 135 | 136 | # PUBLISHER IDENTITY 137 | mqtt_publisher_identity='' 138 | 139 | # MQTT PORT 140 | mqtt_port='1883' 141 | 142 | # MQTT CERTIFICATE FILE 143 | mqtt_certificate_path='' 144 | 145 | #MQTT VERSION (EXAMPLE: 'mqttv311') 146 | mqtt_version='' 147 | 148 | " > "$MQTT_CONFIG" 149 | 150 | #SET SHOULD EXIT 151 | should_exit=true 152 | fi 153 | 154 | #---------------------------------------------------------------------------------------- 155 | # CHECK STATIC DEVICE CONFIGURATION FILES 156 | # ---------------------------------------------------------------------------------------- 157 | 158 | #STATIC DEVICES PREFERENCES 159 | PUB_CONFIG="$base_directory/known_static_addresses" 160 | 161 | if [ -f "$PUB_CONFIG" ]; then 162 | #DOUBLECHECKS 163 | [ ! -z "$(cat "$PUB_CONFIG" | grep "^00:00:00:00:00:00")" ] && printf "%s\n" " - >${RED} error:${NC} please customize public mac addresses in: known_static_addresses" && should_exit=true 164 | else 165 | printf "%s\n" "> public MAC address list file created. please customize." 166 | #IF NO PUBLIC ADDRESS FILE; LOAD 167 | printf "%s\n" " 168 | # --------------------------- 169 | # 170 | # STATIC MAC ADDRESS LIST 171 | # 172 | # 00:00:00:00:00:00 Alias #comment 173 | # --------------------------- 174 | 175 | " > "$PUB_CONFIG" 176 | 177 | #SET SHOULD EXIT 178 | should_exit=true 179 | fi 180 | 181 | 182 | #---------------------------------------------------------------------------------------- 183 | # CHECK BEACON DEVICE CONFIGURATION FILES 184 | # ---------------------------------------------------------------------------------------- 185 | 186 | #STATIC DEVICES PREFERENCES 187 | BEAC_CONFIG="$base_directory/known_beacon_addresses" 188 | 189 | if [ -f "$BEAC_CONFIG" ]; then 190 | #DOUBLECHECKS 191 | [ ! -z "$(cat "$BEAC_CONFIG" | grep "^00:00:00:00:00:00")" ] && printf "%s\n" " - >${RED} error:${NC} please customize beacon mac addresses in: known_beacon_addresses" && should_exit=true 192 | else 193 | printf "%s\n" "> beacon MAC address list file created. please customize." 194 | #IF NO PUBLIC ADDRESS FILE; LOAD 195 | printf "%s\n" "# --------------------------- 196 | # 197 | # BEACON MAC ADDRESS LIST; REQUIRES NAME 198 | # 199 | # Format: 00:00:00:00:00:00 Nickname #comments 200 | # ---------------------------" > "$BEAC_CONFIG" 201 | fi 202 | 203 | #---------------------------------------------------------------------------------------- 204 | # CHECK MONITOR.SERVICE (IF APPLICABLE) 205 | # 206 | # CREDITS & CONTRIBUTIONS: x99percent 207 | # ---------------------------------------------------------------------------------------- 208 | 209 | #FILTER THE ARGV FROM THE PARENT SCRIPT TO REMOVE ONE-TIME USE VARIABLES 210 | FILTERED_ARGS=$(printf '%s\n' "$(IFS=' '; echo "${RUNTIME_ARGS[*]}")" | sed 's/ \?-d//gi;s/ \?-V//gi;s/ \?-F//gi;s/ \?-u//gi;s/&//g;s/ */ /gi') 211 | 212 | #CHECK FOR CORRECT SERVICE; 213 | if [ "$should_install" == "y" ] || [ "$PREF_UPDATE_SERVICE" == true ] ; then 214 | #REMOVE ALL INSTANCES 215 | if [ -f "$service_path" ]; then 216 | rm "$service_path" 2>&1 >/dev/null 217 | fi 218 | 219 | #CHECK FOR SYSTEMCTL FILE 220 | base_directory=$(dirname "$(readlink -f "$0")") 221 | 222 | printf "%s\n" "[Unit] 223 | Description=Monitor Service 224 | After=network.target 225 | 226 | [Service] 227 | User=root 228 | ExecStart=/bin/bash $base_directory/$(basename $0) $FILTERED_ARGS & 229 | WorkingDirectory=$base_directory 230 | Restart=always 231 | RestartSec=10 232 | 233 | [Install] 234 | WantedBy=multi-user.target network.target" > "$service_path" 235 | 236 | #PRINT RESULTS 237 | [ "$PREF_UPDATE_SERVICE" == true ] && printf "%s\n" "> monitor.service updated with arguments: $FILTERED_ARGS" 238 | 239 | sleep 3 240 | 241 | #RELOAD DAEMON AND ENABLE SERVICE 242 | systemctl daemon-reload 243 | systemctl enable monitor.service 244 | fi 245 | 246 | #---------------------------------------------------------------------------------------- 247 | # BEFORE WE ECHO PREFERENCES, EXIT IF WE NEED TO 248 | # ---------------------------------------------------------------------------------------- 249 | 250 | #ARE REQUIREMENTS MET? 251 | [ "$should_exit" == true ] && exit 1 252 | 253 | #---------------------------------------------------------------------------------------- 254 | # CHECK BEHAVIOR CONFIGURATION FILES 255 | #---------------------------------------------------------------------------------------- 256 | 257 | #MQTT PREFERENCES 258 | PREF_CONFIG="$base_directory/behavior_preferences" 259 | [ "$PREF_RESTORE_DEFAULTS" == true ] && rm "$PREF_CONFIG" 260 | 261 | #RESTORE DEFAULTS? 262 | should_restore_defaults=false 263 | 264 | if [ -f "$PREF_CONFIG" ]; then 265 | source "$PREF_CONFIG" 266 | 267 | preferences=$(printf "%s\n" "> ${GREEN}preference:${NC} minimum time between the same type of scan = $PREF_MINIMUM_TIME_BETWEEN_SCANS 268 | > ${GREEN}preference:${NC} regex filter for flags to accept = $PREF_PASS_FILTER_ADV_FLAGS_ARRIVE 269 | > ${GREEN}preference:${NC} regex filter for flags to reject = $PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE 270 | > ${GREEN}preference:${NC} regex filter for manufacturers to accept = $PREF_PASS_FILTER_MANUFACTURER_ARRIVE 271 | > ${GREEN}preference:${NC} regex filter for manufacturers to reject = $PREF_FAIL_FILTER_MANUFACTURER_ARRIVE 272 | > ${GREEN}preference:${NC} maximum sequential arrive scan attempts = $PREF_ARRIVAL_SCAN_ATTEMPTS 273 | > ${GREEN}preference:${NC} maximum sequential depart scan attempts = $PREF_DEPART_SCAN_ATTEMPTS 274 | > ${GREEN}preference:${NC} selected HCI device = ${PREF_HCI_DEVICE:-hci0} 275 | > ${BLUE}mqtt trigger:${NC} $mqtt_topicpath/scan/ARRIVE 276 | > ${BLUE}mqtt trigger:${NC} $mqtt_topicpath/scan/DEPART" | sed 's/= $/= '${RED}'error'${NC}'\!/gi') 277 | 278 | #SIMPLIFY LOGGING 279 | $PREF_VERBOSE_LOGGING && printf "%s\n" "$preferences" 280 | 281 | #DEFAULTS 282 | PREF_ARRIVAL_SCAN_ATTEMPTS=${PREF_ARRIVAL_SCAN_ATTEMPTS:-2} 283 | PREF_BEACON_EXPIRATION=${PREF_BEACON_EXPIRATION:-180} 284 | PREF_COOPERATIVE_SCAN_THRESHOLD=${PREF_COOPERATIVE_SCAN_THRESHOLD:-60} 285 | PREF_DEPART_SCAN_ATTEMPTS=${PREF_DEPART_SCAN_ATTEMPTS:-3} 286 | PREF_DEVICE_TRACKER_REPORT=${PREF_DEVICE_TRACKER_REPORT:-false} 287 | PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE=${PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE:-REJECTION} 288 | PREF_FAIL_FILTER_MANUFACTURER_ARRIVE=${PREF_FAIL_FILTER_MANUFACTURER_ARRIVE:-REJECTION} 289 | PREF_HCI_DEVICE=${PREF_HCI_DEVICE:-hci0} 290 | PREF_INTERSCAN_DELAY=${PREF_INTERSCAN_DELAY:-3} 291 | PREF_MINIMUM_TIME_BETWEEN_SCANS=${PREF_MINIMUM_TIME_BETWEEN_SCANS:-15} 292 | PREF_MQTT_REPORT_SCAN_MESSAGES=${PREF_MQTT_REPORT_SCAN_MESSAGES:-false} 293 | PREF_PASS_FILTER_ADV_FLAGS_ARRIVE=${PREF_PASS_FILTER_ADV_FLAGS_ARRIVE:-0x1a} 294 | PREF_PASS_FILTER_MANUFACTURER_ARRIVE=${PREF_PASS_FILTER_MANUFACTURER_ARRIVE:-Apple} 295 | PREF_PASS_FILTER_PDU_TYPE=${PREF_PASS_FILTER_PDU_TYPE:-ADV_IND|ADV_SCAN_IND|ADV_NONCONN_IND|SCAN_RSP} 296 | PREF_PERCENT_CONFIDENCE_REPORT_THRESHOLD=${PREF_PERCENT_CONFIDENCE_REPORT_THRESHOLD:-59} 297 | PREF_RANDOM_DEVICE_EXPIRATION_INTERVAL=${PREF_RANDOM_DEVICE_EXPIRATION_INTERVAL:-180} 298 | PREF_RSSI_CHANGE_THRESHOLD=${PREF_RSSI_CHANGE_THRESHOLD:-20} 299 | PREF_RSSI_IGNORE_BELOW=${PREF_RSSI_IGNORE_BELOW:--75} 300 | PREF_STARTUP_SETTLE_TIME=${PREF_STARTUP_SETTLE_TIME:-45} 301 | 302 | #SETTINGS 303 | PREF_DEVICE_TRACKER_AWAY_STRING=${PREF_DEVICE_TRACKER_AWAY_STRING:-not_home} 304 | PREF_DEVICE_TRACKER_HOME_STRING=${PREF_DEVICE_TRACKER_HOME_STRING:-home} 305 | PREF_DEVICE_TRACKER_TOPIC_BRANCH=${PREF_DEVICE_TRACKER_TOPIC_BRANCH:-device_tracker} 306 | PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP=${PREF_ADVERTISEMENT_OBSERVED_INTERVAL_STEP:-15} 307 | 308 | #PERIODIC SCAN MODE(S) 309 | PREF_DEPART_SCAN_INTERVAL=${PREF_DEPART_SCAN_INTERVAL:-30} 310 | PREF_ARRIVE_SCAN_INTERVAL=${PREF_ARRIVE_SCAN_INTERVAL:-15} 311 | 312 | #ALIAS MODE 313 | PREF_ALIAS_MODE=${PREF_ALIAS_MODE:-true} 314 | 315 | 316 | #IF PREFERENCES ARE NOT PROPERLY SET, EXIT 317 | [ "$(printf "%s\n" "$preferences" | grep -c "error")" -gt "0" ] && printf "%s\n" "> ${ORANGE}> warning:${NC} preferences error, using defaults. reset to defaults? [y/n]" && read -t 10 should_restore_defaults 318 | 319 | #CONVERT TYPED VALUE TO TRUE OR FALSE 320 | [[ "${should_restore_defaults,,}" =~ y|yes ]] && should_restore_defaults=true 321 | [[ "${should_restore_defaults,,}" =~ n|no ]] && should_restore_defaults=false 322 | fi 323 | 324 | #NEED TO RESTORE DEFAULTS? 325 | 326 | if [ ! -f "$PREF_CONFIG" ] || [ "$should_restore_defaults" == true ] ; then 327 | 328 | printf "%s\n" "> default behavior preferences file created. please customize to your needs." 329 | 330 | printf "%s\n" "# --------------------------- 331 | # 332 | # BEHAVIOR PREFERENCES 333 | # 334 | # --------------------------- 335 | 336 | #MAX RETRY ATTEMPTS FOR ARRIVAL 337 | PREF_ARRIVAL_SCAN_ATTEMPTS=1 338 | 339 | #MAX RETRY ATTEMPTS FOR DEPART 340 | PREF_DEPART_SCAN_ATTEMPTS=2 341 | 342 | #SECONDS UNTIL A BEACON IS CONSIDERED EXPIRED 343 | PREF_BEACON_EXPIRATION=240 344 | 345 | #MINIMUM TIME BEWTEEN THE SAME TYPE OF SCAN (ARRIVE SCAN, DEPART SCAN) 346 | PREF_MINIMUM_TIME_BETWEEN_SCANS=15 347 | 348 | #ARRIVE TRIGGER FILTER(S) 349 | PREF_PASS_FILTER_ADV_FLAGS_ARRIVE=\".*\" 350 | PREF_PASS_FILTER_MANUFACTURER_ARRIVE=\".*\" 351 | 352 | #ARRIVE TRIGGER NEGATIVE FILTER(S) 353 | PREF_FAIL_FILTER_ADV_FLAGS_ARRIVE=\"NONE\" 354 | PREF_FAIL_FILTER_MANUFACTURER_ARRIVE=\"NONE\" 355 | 356 | " > "$PREF_CONFIG" 357 | 358 | #LOAD DEFAULT PREFERENCES 359 | should_exit=true 360 | fi 361 | 362 | 363 | #ARE REQUIREMENTS MET? 364 | [ "$should_exit" == true ] && exit 1 365 | 366 | -------------------------------------------------------------------------------- /support/log: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------------------------------------------------------------------------- 4 | # GENERAL INFORMATION 5 | # ---------------------------------------------------------------------------------------- 6 | # 7 | # Written by Andrew J Freyer 8 | # GNU General Public License 9 | # http://github.com/andrewjfreyer/monitor 10 | # 11 | # PRINTING AND DEBUGGING 12 | # 13 | # ---------------------------------------------------------------------------------------- 14 | 15 | 16 | # ---------------------------------------------------------------------------------------- 17 | # PRETTY PRINT FOR DEBUG 18 | # ---------------------------------------------------------------------------------------- 19 | 20 | #VARIABLES FOR LOGGING LOGSPAM PREVENTION 21 | last_log_line="" 22 | duplicate_log_count=1 23 | launch_flag="" 24 | 25 | #LOGGING FLAG TO DISTINGUISH SERVICE-LAUNCHED 26 | #LOGGS FROM CONSOLE LOGGED LAUNCHES 27 | $SERVICE_ACTIVE && launch_flag="[+]" || launch_flag="[-]" 28 | 29 | #FORK THE LOG FROM THE MAIN THREAD 30 | log_listener () { 31 | 32 | #LOG PIPE 33 | while true; do 34 | #SHOULD EXIT? 35 | [ "$PREF_DISABLE_LOGGING" == true ] && return 0 36 | 37 | #READ FROM THE MAIN PIPE 38 | local line 39 | while read line; do 40 | 41 | #ECHO TO CONSOLE, REMOVING EXTRA SPACES 42 | should_repeat="" 43 | line_append="" 44 | 45 | #IS THIS LINE DUPLICATED? 46 | if [ "$last_log_line" == "$line" ] ; then 47 | duplicate_log_count=$((duplicate_log_count + 1 )) 48 | else 49 | duplicate_log_count=1 50 | fi 51 | 52 | if [ "$PREF_REDACT" == true ]; then 53 | line=$(echo "$line" | sed "s/\([0-9A-Fa-f]\{2\}:\)\{5\}/ [REDACTED]:/gi;s/\([0-9A-Fa-f-]\{36\}\)/ [REDACTED]/gi" ) 54 | line="$line" 55 | fi 56 | 57 | #ECHO LAST LINE 58 | printf "%s\n" "$launch_flag $version $(date "+%d-%m-%Y %I:%M:%S %p") $line" 59 | last_log_line="$line" 60 | 61 | done < log_pipe 62 | done 63 | } 64 | 65 | 66 | #PRINT TO LOG UNLESS DUPLICATE LINE 67 | log() { 68 | #SEND TO LOG PIPE 69 | [ "$PREF_DISABLE_LOGGING" == false ] && printf "$1\n" | sed 's/ */ /g' > log_pipe 70 | } -------------------------------------------------------------------------------- /support/mqtt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------------------------------------------------------------------------- 4 | # GENERAL INFORMATION 5 | # ---------------------------------------------------------------------------------------- 6 | # 7 | # Written by Andrew J Freyer 8 | # GNU General Public License 9 | # http://github.com/andrewjfreyer/monitor 10 | # 11 | # MQTT SCANNING 12 | # 13 | # ---------------------------------------------------------------------------------------- 14 | 15 | # ---------------------------------------------------------------------------------------- 16 | # UTILITY FUNCTION FOR JOINING STRINGS 17 | # ---------------------------------------------------------------------------------------- 18 | 19 | #GLOBALS 20 | last_error_message="" 21 | duplicate_error_count=1 22 | 23 | #UTILITY FUNCTIONS 24 | function join_string () (IFS=$1; shift; printf "$*"); 25 | function json_keypair () (join_string ":" "\"$1\"" "\"$2\"") 26 | 27 | #UNFORMAT A JASON STRING 28 | function json_unformat (){ 29 | #https://stackoverflow.com/a/38607019/225270 30 | printf "$1" | grep -Eo '"[^"]*" *(: *([0-9]*|"[^"]*")[^{}\["]*|,)?|[^"\]\[\}\{]*|\{|\},?|\[|\],?|[0-9 ]*,?' | awk '{if ($0 ~ /^[}\]]/ ) offset-=4; printf "%*c%s\n", offset, " ", $0; if ($0 ~ /^[{\[]/) offset+=4}' 31 | } 32 | 33 | #FORMAT OF 'KEY=OBJECT' IN ARGV VALUES 34 | function json_format (){ 35 | local concat 36 | local key 37 | local object 38 | for var in "$@" 39 | do 40 | object="${var##*=}" 41 | key="${var%%=*}" 42 | concat="$concat,$(json_keypair "$key" "$object")" 43 | done 44 | printf "{%s}\n" "${concat/,/}" 45 | } 46 | 47 | # ---------------------------------------------------------------------------------------- 48 | # MQTT ANNOUNCE ONLINE 49 | # ---------------------------------------------------------------------------------------- 50 | mqtt_announce_online(){ 51 | 52 | #ANNOUCEE HEALTH 53 | mqtt_error_handler $($mosquitto_pub_path \ 54 | -I "$mqtt_publisher_identity" \ 55 | $mqtt_version_append \ 56 | $mqtt_ca_file_append \ 57 | -L "$mqtt_url$mqtt_topicpath/$mqtt_publisher_identity/status" \ 58 | -m "online" -q "2" 2>&1) 59 | } 60 | 61 | # ---------------------------------------------------------------------------------------- 62 | # MQTT ECHO 63 | # ---------------------------------------------------------------------------------------- 64 | mqtt_echo(){ 65 | #ANNOUCEE HEALTH 66 | mqtt_error_handler $($mosquitto_pub_path \ 67 | -I "$mqtt_publisher_identity" \ 68 | $mqtt_version_append \ 69 | $mqtt_ca_file_append \ 70 | -L "$mqtt_url$mqtt_topicpath/$mqtt_publisher_identity/echo" \ 71 | -m "ok" \ 72 | -q "2" 2>&1) 73 | } 74 | 75 | 76 | # ---------------------------------------------------------------------------------------- 77 | # CLEAR RETAINED 78 | # ---------------------------------------------------------------------------------------- 79 | mqtt_broker_clean (){ 80 | #DEFINE LOCALS 81 | local topic_data 82 | local topic_path 83 | 84 | #MQTT LOOP 85 | while read instruction; do 86 | 87 | #ERROR HANDLING 88 | mqtt_error_handler "$instruction" && break 89 | 90 | #EXTRACT TOPIC PATH FROM FORMATTED MQTT MESSAGE 91 | topic_path_of_instruction="${instruction%%|*}" 92 | 93 | [ -z "$topic_path_of_instruction" ] && continue 94 | 95 | #PUBLISH CLEARING MESSAGE 96 | $mosquitto_pub_path \ 97 | $mqtt_version_append \ 98 | $mqtt_ca_file_append \ 99 | -r \ 100 | -n \ 101 | -L "$mqtt_url$topic_path_of_instruction" 2>&1 102 | 103 | done < <($(which mosquitto_sub) \ 104 | -I "$mqtt_publisher_identity" \ 105 | -v $mqtt_version_append \ 106 | $mqtt_ca_file_append \ 107 | -F '%t|%p' \ 108 | -W 1 \ 109 | -q 2 \ 110 | -L "$mqtt_url$mqtt_topicpath/$mqtt_publisher_identity/#" 2>&1 ) 111 | 112 | printf "%s\n" "> retained messages cleaned from broker" 113 | } 114 | 115 | # ---------------------------------------------------------------------------------------- 116 | # MQTT LISTENER 117 | # ---------------------------------------------------------------------------------------- 118 | mqtt_listener (){ 119 | #ANNOUNCE ONLINE PRESENCE 120 | mqtt_announce_online 121 | 122 | #DEFINE LOCALS 123 | local topic_data 124 | local topic_path 125 | 126 | while true; do 127 | #MQTT LOOP 128 | while read instruction; do 129 | 130 | #ERROR HANDLING 131 | mqtt_error_handler "$instruction" && break 132 | 133 | #PRINT THE INSTRUCTION BACK TO THE MAIN THREAD 134 | printf "MQTT$instruction\n" > main_pipe 135 | 136 | done < <($(which mosquitto_sub) -I \ 137 | "$mqtt_publisher_identity" \ 138 | $mqtt_version_append \ 139 | $mqtt_ca_file_append \ 140 | -v \ 141 | -F '%t|%p' \ 142 | -q 2 \ 143 | -L "$mqtt_url$mqtt_topicpath/#" \ 144 | --will-topic "$mqtt_topicpath/$mqtt_publisher_identity/status" \ 145 | --will-payload "offline" 2>&1 ) 146 | 147 | #NEED TO RESUBSCRIBE 148 | sleep 10 149 | done 150 | } 151 | 152 | 153 | # ---------------------------------------------------------------------------------------- 154 | # MQTT ERROR HANDLER 155 | # ---------------------------------------------------------------------------------------- 156 | mqtt_error_handler () { 157 | local received="$*" 158 | local return_value 159 | local print_message="" 160 | 161 | #SET RETURN VALUE TO TRUE 162 | return_value=1 163 | 164 | if [ -n "$received" ]; then 165 | #ERRORS 166 | [[ ${received^^} =~ .*CONNECTION.* ]] && [[ ${received^^} =~ .*REFUSED.* ]] && return_value=1 && print_message="mqtt broker refused connection - check username, password, and host address" 167 | [[ ${received^^} =~ .*NETWORK.* ]] && [[ ${received^^} =~ .*UNREACHABLE.* ]] && return_value=0 && print_message="network is down. enqueuing command to try again after a delay" 168 | [[ ${received^^} =~ .*LOOKUP.* ]] && [[ ${received^^} =~ .*ERROR.* ]] && return_value=0 && print_message="issue connecting to mqtt server (lookup error). enqueuing command to try again after a delay" 169 | 170 | 171 | if [ -n "$last_error_message" ] && [ "$last_error_message" == "$print_message" ]; then 172 | #HERE, WE HAVE A REPEATED ERROR 173 | duplicate_error_count=$((duplicate_error_count + 1 )) 174 | last_error_message="$print_message" 175 | 176 | if [ "$duplicate_error_count" -gt "3" ]; then 177 | log "${RED}[CMD-ERRO]${NC} ${RED}fatal mqtt error - messages may not be delivered as intended ($print_message / $duplicate_error_count)${NC}" 178 | duplicate_error_count=0 179 | last_error_message="" 180 | fi 181 | 182 | #SET TO TRUE TO CAUSE A LOOP OF 183 | return_value=0 184 | 185 | elif [ -n "$print_message" ]; then 186 | #MESSAGE IS NOT REPEATED, SO FEEL FREE TO LOG IT 187 | log "${YELLOW}[CMD-MQTT]${NC} ${YELLOW}warning: $print_message ${NC}" 188 | duplicate_error_count=0 189 | last_error_message="$print_message" 190 | 191 | #SET TO TRUE TO CAUSE A LOOP OF 192 | return_value=0 193 | fi 194 | else 195 | duplicate_error_count=0 196 | last_error_message="" 197 | fi 198 | 199 | #RETURN VALUE 200 | return $return_value 201 | } 202 | 203 | # ---------------------------------------------------------------------------------------- 204 | # PUBLISH RSSI MESSAGE 205 | # ---------------------------------------------------------------------------------------- 206 | 207 | publish_rssi_message () { 208 | if [ -n "$1" ]; then 209 | #TIMESTAMP 210 | local stamp 211 | local address 212 | local message 213 | local mqtt_topic_branch 214 | 215 | #SET ISOLATED ADDRESS 216 | address="$1" 217 | message="$2" 218 | mqtt_topic_branch="$address" 219 | 220 | #ALIASES? 221 | $PREF_ALIAS_MODE && [ "${mqtt_aliases[$address]+abc}" ] && mqtt_topic_branch=${mqtt_aliases[$address]:-$address} 222 | 223 | local topic="$mqtt_topicpath/$mqtt_publisher_identity/$mqtt_topic_branch/rssi" 224 | 225 | $PREF_VERBOSE_LOGGING && log "${YELLOW}[CMD-MQTT]${NC} ${YELLOW}$topic${NC}" 226 | 227 | #POST TO MQTT 228 | while mqtt_error_handler $($mosquitto_pub_path \ 229 | -I "$mqtt_publisher_identity" \ 230 | $mqtt_version_append \ 231 | $mqtt_ca_file_append \ 232 | -L "$mqtt_url$topic" \ 233 | -m "$message" 2>&1); do 234 | sleep 15 235 | done 236 | fi 237 | } 238 | 239 | # ---------------------------------------------------------------------------------------- 240 | # PUBLISH MESSAGE 241 | # ---------------------------------------------------------------------------------------- 242 | 243 | publish_presence_message () { 244 | if [ -n "$1" ]; then 245 | #TIMESTAMP 246 | local stamp 247 | local should_retain 248 | local isolated_address 249 | local retain_flag 250 | local message 251 | local existing_alias 252 | local mqtt_topic_branch 253 | local confidence_printable 254 | local device_tracker_message 255 | 256 | 257 | #SET ISOLATED ADDRESS 258 | isolated_address="${1##*=}" 259 | mqtt_topic_branch="$isolated_address" 260 | 261 | #ALIASES? 262 | $PREF_ALIAS_MODE && [ "${mqtt_aliases[$isolated_address]+abc}" ] && mqtt_topic_branch=${mqtt_aliases[$isolated_address]:-$isolated_address} 263 | 264 | #SET TIMESTAMP 265 | #stamp=$(date "+%a %b %d %Y %H:%M:%S GMT%z (%Z)") 266 | stamp=$(date +"%Y-%m-%dT%H:%M:%S%z") 267 | 268 | #CLEAR PREVIOUS RETAINED MESSAGE 269 | retain_flag="false" 270 | if [ "$PREF_SHOULD_RETAIN" == true ]; then 271 | should_retain="-r " 272 | retain_flag="true" 273 | fi 274 | 275 | #ASSEMBLE 276 | message=$( \ 277 | json_format "$@" \ 278 | "retained=$retain_flag" \ 279 | "timestamp=$stamp" \ 280 | "version=$version" \ 281 | ) 282 | 283 | #DEFINE THE TOPIC 284 | local topic="$mqtt_topicpath/$mqtt_publisher_identity/$mqtt_topic_branch" 285 | [ "$PREF_MQTT_SINGLE_TOPIC_MODE" == true ] && topic="$mqtt_topicpath/$mqtt_publisher_identity" 286 | 287 | #SHOULD FORMAT AS LETTERS/NUMBERS 288 | [ "$PREF_FORMAT_MQTT" == true ] && topic=$(echo "$topic" | sed 's/[^0-9a-z/]//gi') 289 | 290 | #ANNOUNCE ONLINE 291 | mqtt_announce_online 292 | 293 | #SHOULD WE BE REPORTING AS A DEVICE TRACKER? 294 | if [ "$PREF_DEVICE_TRACKER_REPORT" == true ]; then 295 | 296 | if [[ $message =~ \"confidence\":\"100\" ]]; then 297 | #OVERRIDE MESSAGE 298 | device_tracker_message="$PREF_DEVICE_TRACKER_HOME_STRING" 299 | 300 | elif [[ $message =~ \"confidence\":\"0\" ]]; then 301 | #OVERRIDE MESSAGE 302 | device_tracker_message="$PREF_DEVICE_TRACKER_AWAY_STRING" 303 | else 304 | #NO MQTT MESSAGES IF NOT ZERO OR 100 305 | device_tracker_message="" 306 | fi 307 | 308 | #ONLY POST A MESSAGE TO DEVICE TRACKER BOARD IF WE HAVE 309 | #A COMPLETE CONFIDENCE MESSAGE 310 | if [ -n "$device_tracker_message" ]; then 311 | #POST TO MQTT 312 | while mqtt_error_handler $($mosquitto_pub_path \ 313 | -I "$mqtt_publisher_identity" \ 314 | $should_retain \ 315 | $mqtt_version_append \ 316 | $mqtt_ca_file_append \ 317 | -L "$mqtt_url$topic/$PREF_DEVICE_TRACKER_TOPIC_BRANCH" \ 318 | -m "$device_tracker_message" 2>&1); do 319 | sleep 10 320 | done 321 | 322 | #PRINT FOR DEVICE TRACKER 323 | if [ $PREF_VERBOSE_LOGGING == true ]; then 324 | log "${YELLOW}[CMD-MQTT]${NC} ${YELLOW}$topic/$PREF_DEVICE_TRACKER_TOPIC_BRANCH $device_tracker_message${NC}" 325 | fi 326 | fi 327 | fi 328 | 329 | #POST TO MQTT 330 | while mqtt_error_handler $($mosquitto_pub_path \ 331 | -I "$mqtt_publisher_identity" \ 332 | $should_retain \ 333 | $mqtt_version_append \ 334 | $mqtt_ca_file_append \ 335 | -q 2 \ 336 | -L "$mqtt_url$topic" \ 337 | -m "$message" 2>&1); do 338 | sleep 5 339 | done 340 | 341 | 342 | #MESSAGE PRINTING 343 | if [ $PREF_VERBOSE_LOGGING == true ]; then 344 | 345 | 346 | log "${YELLOW}[CMD-MQTT]${NC} ${YELLOW}$topic${NC}" 347 | #REDACTIONS? 348 | if [ "$PREF_REDACT" == true ]; then 349 | printf "%s\n" "${YELLOW}$(json_unformat "$message" | sed "s/\([0-9A-Fa-f]\{2\}:\)\{5\}/ [REDACTED]:/gi;s/\([0-9A-Fa-f-]\{36\}\)/ [REDACTED]/gi" )${NC}" 350 | else 351 | printf "%s\n" "${YELLOW}$(json_unformat "$message")${NC}" 352 | fi 353 | 354 | #EXTRACT RAW CONFIDENC 355 | elif [[ "${message,,}" =~ \"confidence\":\"([0-9]{1,3})\" ]]; then 356 | 357 | #GET CONFIDENCE VALUE 358 | confidence_printable="${BASH_REMATCH[1]}" 359 | 360 | #EXTRACT CONFIDENCE VALUE TO POST 361 | log "${YELLOW}[CMD-MQTT]${NC} ${YELLOW}$topic { ... confidence : $confidence_printable ... } ${NC}" 362 | fi 363 | fi 364 | } 365 | 366 | publish_cooperative_scan_message () { 367 | 368 | 369 | #ANNOUNCE ONLINE 370 | mqtt_announce_online 371 | 372 | if [ -n "$1" ] && [ -z "$2" ]; then 373 | #POST TO MQTT 374 | $mosquitto_pub_path \ 375 | -I "$mqtt_publisher_identity" \ 376 | $mqtt_version_append \ 377 | $mqtt_ca_file_append \ 378 | -q 2 \ 379 | -L "$mqtt_url$mqtt_topicpath/scan/$1" \ 380 | -m "{\"identity\":\"$mqtt_publisher_identity\"}" 2>&1 381 | 382 | elif [ -n "$1" ] && [ -n "$2" ]; then 383 | #POST TO MQTT 384 | $mosquitto_pub_path \ 385 | -I "$mqtt_publisher_identity" \ 386 | $mqtt_version_append \ 387 | $mqtt_ca_file_append \ 388 | -q 2 \ 389 | -L "$mqtt_url$mqtt_topicpath/$2/$1" \ 390 | -m "{\"identity\":\"$mqtt_publisher_identity\"}" 2>&1 391 | 392 | else 393 | 394 | $mosquitto_pub_path \ 395 | -I "$mqtt_publisher_identity" \ 396 | $mqtt_version_append \ 397 | -q 2 \ 398 | $mqtt_ca_file_append \ 399 | -L "$mqtt_url$mqtt_topicpath/scan" \ 400 | -m "{\"identity\":\"$mqtt_publisher_identity\"}" 2>&1 401 | fi 402 | } 403 | 404 | #SHOULD CLEAN? 405 | [ "$PREF_CLEAN_MQTT" == true ] && mqtt_broker_clean 406 | -------------------------------------------------------------------------------- /support/time: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------------------------------------------------------------------------- 4 | # GENERAL INFORMATION 5 | # ---------------------------------------------------------------------------------------- 6 | # 7 | # Written by Andrew J Freyer 8 | # GNU General Public License 9 | # http://github.com/andrewjfreyer/monitor 10 | # 11 | # TIME BASED TRIGGERS 12 | # 13 | # ---------------------------------------------------------------------------------------- 14 | 15 | # ---------------------------------------------------------------------------------------- 16 | # PERIODIC TRIGGER TO MAIN LOOP 17 | # ---------------------------------------------------------------------------------------- 18 | beacon_database_expiration_trigger () { 19 | while : ; do 20 | sleep "$(( PREF_BEACON_EXPIRATION / 12 ))" 21 | echo "BEXP" > main_pipe 22 | done 23 | } --------------------------------------------------------------------------------