From 873bf887a5b9c64ee96871ece531d02cce3a0daf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Mar 2020 12:55:48 -0800 Subject: [PATCH] Add OwnTracks Friends via person integration (#27303) * Returns an unencrypted location of all persons with device trackers * Handle encrypted messages and exclude the poster's location * Friends is by default False. Reformats with Black * Updates the context init to account for the Friends option * Fix Linter error * Remove as a config option * No longer imports encyrption-related functions in encrypt_message * Fix initialization in test * Test the friends functionality * Bugfix for persons not having a location * Better way to return the timestamp * Update homeassistant/components/owntracks/__init__.py Co-Authored-By: Paulus Schoutsen * Linting and tid generation * Fix test Co-authored-by: Paulus Schoutsen --- .../components/owntracks/__init__.py | 34 ++++++++- .../components/owntracks/messages.py | 31 +++++++++ .../owntracks/test_device_tracker.py | 69 +++++++++++++++++++ 3 files changed, 131 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 71494e9e805..cf034950154 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -16,7 +16,7 @@ from homeassistant.setup import async_when_setup from .config_flow import CONF_SECRET from .const import DOMAIN -from .messages import async_handle_message +from .messages import async_handle_message, encrypt_message _LOGGER = logging.getLogger(__name__) @@ -154,6 +154,7 @@ async def handle_webhook(hass, webhook_id, request): Android does not set a topic but adds headers to the request. """ context = hass.data[DOMAIN]["context"] + topic_base = re.sub("/#$", "", context.mqtt_topic) try: message = await request.json() @@ -168,7 +169,6 @@ async def handle_webhook(hass, webhook_id, request): device = headers.get("X-Limit-D", user) if user: - topic_base = re.sub("/#$", "", context.mqtt_topic) message["topic"] = f"{topic_base}/{user}/{device}" elif message["_type"] != "encrypted": @@ -180,7 +180,35 @@ async def handle_webhook(hass, webhook_id, request): return json_response([]) hass.helpers.dispatcher.async_dispatcher_send(DOMAIN, hass, context, message) - return json_response([]) + + response = [] + + for person in hass.states.async_all(): + if person.domain != "person": + continue + + if "latitude" in person.attributes and "longitude" in person.attributes: + response.append( + { + "_type": "location", + "lat": person.attributes["latitude"], + "lon": person.attributes["longitude"], + "tid": "".join(p[0] for p in person.name.split(" ")[:2]), + "tst": int(person.last_updated.timestamp()), + } + ) + + if message["_type"] == "encrypted" and context.secret: + return json_response( + { + "_type": "encrypted", + "data": encrypt_message( + context.secret, message["topic"], json.dumps(response) + ), + } + ) + + return json_response(response) class OwnTracksContext: diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 7fab391efc1..42f1f62d10a 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -144,6 +144,37 @@ def _decrypt_payload(secret, topic, ciphertext): return None +def encrypt_message(secret, topic, message): + """Encrypt message.""" + + keylen = SecretBox.KEY_SIZE + + if isinstance(secret, dict): + key = secret.get(topic) + else: + key = secret + + if key is None: + _LOGGER.warning( + "Unable to encrypt payload because no decryption key known " "for topic %s", + topic, + ) + return None + + key = key.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b"\0") + + try: + message = message.encode("utf-8") + payload = SecretBox(key).encrypt(message, encoder=Base64Encoder) + _LOGGER.debug("Encrypted message: %s to %s", message, payload) + return payload.decode("utf-8") + except ValueError: + _LOGGER.warning("Unable to encrypt message for topic %s", topic) + return None + + @HANDLERS.register("location") async def async_handle_location_message(hass, context, message): """Handle a location message.""" diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 730da4bc7b2..ae9fb65c615 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1565,3 +1565,72 @@ async def test_restore_state(hass, hass_client): assert state_1.attributes["longitude"] == state_2.attributes["longitude"] assert state_1.attributes["battery_level"] == state_2.attributes["battery_level"] assert state_1.attributes["source_type"] == state_2.attributes["source_type"] + + +async def test_returns_empty_friends(hass, hass_client): + """Test that an empty list of persons' locations is returned.""" + entry = MockConfigEntry( + domain="owntracks", data={"webhook_id": "owntracks_test", "secret": "abcd"} + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + client = await hass_client() + resp = await client.post( + "/api/webhook/owntracks_test", + json=LOCATION_MESSAGE, + headers={"X-Limit-u": "Paulus", "X-Limit-d": "Pixel"}, + ) + + assert resp.status == 200 + assert await resp.text() == "[]" + + +async def test_returns_array_friends(hass, hass_client): + """Test that a list of persons' current locations is returned.""" + otracks = MockConfigEntry( + domain="owntracks", data={"webhook_id": "owntracks_test", "secret": "abcd"} + ) + otracks.add_to_hass(hass) + + await hass.config_entries.async_setup(otracks.entry_id) + await hass.async_block_till_done() + + # Setup device_trackers + assert await async_setup_component( + hass, + "person", + { + "person": [ + { + "name": "person 1", + "id": "person1", + "device_trackers": ["device_tracker.person_1_tracker_1"], + }, + { + "name": "person2", + "id": "person2", + "device_trackers": ["device_tracker.person_2_tracker_1"], + }, + ] + }, + ) + hass.states.async_set( + "device_tracker.person_1_tracker_1", "home", {"latitude": 10, "longitude": 20} + ) + + client = await hass_client() + resp = await client.post( + "/api/webhook/owntracks_test", + json=LOCATION_MESSAGE, + headers={"X-Limit-u": "Paulus", "X-Limit-d": "Pixel"}, + ) + + assert resp.status == 200 + response_json = json.loads(await resp.text()) + + assert response_json[0]["lat"] == 10 + assert response_json[0]["lon"] == 20 + assert response_json[0]["tid"] == "p1"