├── .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 |  
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 | 
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 | 
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 | 
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 | 
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 | 
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 | }
--------------------------------------------------------------------------------