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"