Skip to content

Commit c337b0c

Browse files
committedNov 25, 2023
Initial commit
0 parents  commit c337b0c

17 files changed

+710
-0
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.swp

‎README.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Tracker
2+
3+
Device tracking for home automation (e.g home assistant), running as an external
4+
service, and reporting status to a MQTT broker.
5+
6+
Tracking is configured per device, using a set of pre-defined modules. As of
7+
now, only the *deepsleep* module is implemented.
8+
9+
![Image of Tracker from Paw Patrol](images/tracker.png)
10+
11+
*(Image source: https://pawpatrol.fandom.com/wiki/Tracker)*
12+
13+
## Modules
14+
15+
### Deepsleep
16+
17+
This module works by sending a UDP packet on port 5353 (used by multicast DNS)
18+
with whatever payload to a device, and then checking the host machines ARP table
19+
to see if it contains an entry for the device we are tracking.
20+
21+
This module enables us to track newer iPhones, which are hard to track, since
22+
they disconnect from WiFi when the screen is locked. This method probably works
23+
for other devices as well.
24+
25+
This module basically uses the same detection strategy as implemented by Magnus
26+
Nyström (mudape) in [iphonedetect](https://github.com/mudape/iphonedetect).
27+
I had to do my own implementation, since I wanted to track devices on an other
28+
subnet then the Home Assistant server, which would not work with iphonedetect,
29+
since the server was not getting ARP table entries for the devices.
30+
31+
## Use together with Home Assistant
32+
33+
### Install Mosquitto broker
34+
In this example, we use the Mosquitto MQTT broker from the Home Assistant add-on
35+
store. Feel free to use any MQTT broker you like.
36+
37+
Start by installing the Mosquitto broker from the add-on store and add a local
38+
user to the configuration file (Supervisor → Mosquitto broker → Configuration),
39+
e.g:
40+
41+
```yaml
42+
logins:
43+
- username: tracker
44+
password: some-awesome-random-generated-password
45+
```
46+
47+
The broker needs to be restarted before we are able to log in with the new user.
48+
49+
### Add device_tracker config to configuration.yaml
50+
51+
To actually track the device, we have to add some configuration to
52+
/config/configuration.yaml (or maybe in another location if HASSOS is not used).
53+
This can be done using SSH or using the *File Editor* add-on.
54+
55+
The following configuration tracks the device *my_iphone* using the
56+
*tracker/location/my_iphone* MQTT topic:
57+
58+
```yaml
59+
device_tracker:
60+
- platform: mqtt
61+
devices:
62+
my_iphone: tracker/location/my_iphone
63+
```
64+
65+
Home Assistant should be restarted after modifying *configuration.yaml*.
66+
67+
This means that every time Tracker publishes something on that MQTT topic, it is
68+
picked up by Home Assistant.
69+
70+
### Configure Tracker
71+
72+
The following should be added to the configuration file of Tracker (default
73+
path: */etc/tracker.yaml*) to track the device configured in the previous step:
74+
75+
```yaml
76+
device_track:
77+
- my_iphone:
78+
module: deepsleep
79+
ip: 1.2.3.4 # add the IP address of your device here
80+
```
81+
82+
Now start Tracker to start tracking your device.

‎example.yaml

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Example configuration file for Tracker
2+
---
3+
4+
# Configure logging (uses logging.config.dictConfig)
5+
logging:
6+
version: 1
7+
formatters:
8+
standard:
9+
format: '%(asctime)s (%(levelname)s) %(message)s'
10+
handlers:
11+
default:
12+
class: logging.StreamHandler
13+
level: INFO
14+
formatter: standard
15+
loggers:
16+
'':
17+
level: INFO
18+
handlers:
19+
- default
20+
21+
# Default state
22+
state:
23+
home: "home"
24+
not_home: "not_home"
25+
26+
# Default module config
27+
module:
28+
deepsleep:
29+
# How often to check device (in seconds)
30+
interval: 5
31+
32+
# When a iPhone is locked we often have to try multiple times before
33+
# we get an answer.
34+
max_retries: 10
35+
36+
# How to long to wait between the retries (in seconds)
37+
retry_interval: 2
38+
39+
# MQTT config
40+
mqtt:
41+
broker: "mqtt.local"
42+
port: 1883
43+
username: "tracker"
44+
password: "some-awesome-random-generated-password"
45+
topic_prefix: "tracker/location/"
46+
47+
# List of devices to track
48+
device_track:
49+
- my_iphone:
50+
module: deepsleep
51+
ip: 1.2.3.4

‎images/tracker.png

49.8 KB
Loading

‎requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-r requirements/prod.txt

‎requirements/dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-r prod.txt

‎requirements/prod.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
paho-mqtt
2+
PyYAML
3+
pyroute2

‎requirements/test.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-r prod.txt
2+
3+
pytest

‎setup.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
try:
2+
from setuptools import setup
3+
except ImportError:
4+
from distutils.core import setup
5+
6+
7+
setup(
8+
name='tracker',
9+
version='1.0.0',
10+
description="Device tracking for home automation",
11+
author="Mats Klepsland",
12+
author_email='mats.klepsland@gmail.com',
13+
packages=[
14+
'tracker',
15+
],
16+
package_dir={'tracker':
17+
'tracker'},
18+
include_package_data=True,
19+
install_requires=[],
20+
zip_safe=False,
21+
keywords='tracker',
22+
test_suite='tests',
23+
)

‎tracker/__init__.py

Whitespace-only changes.

‎tracker/args.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
Copyright (C) 2020 Mats Klepsland <mats.klepsland@gmail.com>
3+
4+
This file is part of Tracker.
5+
6+
This program is free software: you can redistribute it and/or modify it under
7+
the terms of the GNU General Public License as published by the Free Software
8+
Foundation, either version 3 of the License, or (at your option) any later
9+
version.
10+
11+
This program is distributed in the hope that it will be useful, but WITHOUT
12+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13+
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License along with
16+
this program. If not, see <http://www.gnu.org/licenses/>.
17+
"""
18+
19+
import argparse
20+
21+
DEFAULT_CONFIG_FILE = "/etc/tracker.yaml"
22+
23+
24+
def read_args():
25+
"""Read command-line arguments.
26+
27+
Returns:
28+
Parsed arguments.
29+
30+
"""
31+
parser = argparse.ArgumentParser()
32+
parser.add_argument("-c", "--config-file",
33+
default=DEFAULT_CONFIG_FILE, metavar="FILE")
34+
args = parser.parse_args()
35+
36+
return args

‎tracker/config.py

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Copyright (C) 2020 Mats Klepsland <mats.klepsland@gmail.com>
3+
4+
This file is part of Tracker.
5+
6+
This program is free software: you can redistribute it and/or modify it under
7+
the terms of the GNU General Public License as published by the Free Software
8+
Foundation, either version 3 of the License, or (at your option) any later
9+
version.
10+
11+
This program is distributed in the hope that it will be useful, but WITHOUT
12+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13+
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License along with
16+
this program. If not, see <http://www.gnu.org/licenses/>.
17+
"""
18+
19+
import sys
20+
import yaml
21+
22+
DEFAULT_STATE_HOME = "home"
23+
DEFAULT_STATE_NOT_HOME = "not_home"
24+
DEFAULT_MQTT_BROKER = "localhost"
25+
DEFAULT_MQTT_PORT = "1833"
26+
DEFAULT_MQTT_TOPIC_PREFIX = "tracker/location/"
27+
28+
29+
def _dict_nested_set(d, keys, value):
30+
"""Set a nested key in a dictionary.
31+
32+
Args:
33+
d (dict): The dictionary.
34+
keys (list): The nested key.
35+
value: The value to set the key to.
36+
37+
"""
38+
for key in keys[:-1]:
39+
d = d.setdefault(key, {})
40+
d[keys[-1]] = value
41+
42+
43+
def _dict_nested_isset(d, keys):
44+
"""Check if nested key is set in dictionary.
45+
46+
Args:
47+
d (dict): The dictionary.
48+
keys (list): The nested key.
49+
50+
Returns:
51+
True if key is set, False otherwise.
52+
53+
"""
54+
_d = d
55+
56+
for key in keys:
57+
try:
58+
_d = _d[key]
59+
except KeyError:
60+
return False
61+
62+
return True
63+
64+
65+
def config_apply_defaults(conf):
66+
"""Apply defaults to the config where key is not set.
67+
68+
Args:
69+
conf (dict): The loaded config.
70+
71+
Returns:
72+
Dictionary containing config, with added defaults.
73+
74+
"""
75+
76+
if not _dict_nested_isset(conf, ["state", "home"]):
77+
_dict_nested_set(conf, ["state", "home"], DEFAULT_STATE_HOME)
78+
79+
if not _dict_nested_isset(conf, ["state", "not_home"]):
80+
_dict_nested_set(conf, ["state", "not_home"], DEFAULT_STATE_NOT_HOME)
81+
82+
if not _dict_nested_isset(conf, ["mqtt", "broker"]):
83+
_dict_nested_set(conf, ["mqtt", "broker"], DEFAULT_MQTT_BROKER)
84+
85+
if not _dict_nested_isset(conf, ["mqtt", "port"]):
86+
_dict_nested_set(conf, ["mqtt", "port"], DEFAULT_MQTT_PORT)
87+
88+
if not _dict_nested_isset(conf, ["mqtt", "topic_prefix"]):
89+
_dict_nested_set(conf, ["mqtt", "topic_prefix"],
90+
DEFAULT_MQTT_TOPIC_PREFIX)
91+
92+
return conf
93+
94+
95+
def config_load(config_file):
96+
"""Load configuration file (YAML).
97+
98+
Args:
99+
config_file (str): Configuration file.
100+
101+
Returns:
102+
Dictionary containing config.
103+
104+
"""
105+
try:
106+
with open(config_file, "r") as f:
107+
conf = yaml.safe_load(f)
108+
except yaml.YAMLError as ex:
109+
sys.exit("ERROR: could not parse config file '%s': %s" %
110+
(config_file, ex))
111+
except FileNotFoundError:
112+
sys.exit("ERROR: could not find config file '%s'" % config_file)
113+
114+
config_apply_defaults(conf)
115+
116+
if "device_track" not in conf:
117+
sys.exit("Config does not contain any devices to track. Exiting.")
118+
119+
return conf

0 commit comments

Comments
 (0)
Please sign in to comment.