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 <paulus@home-assistant.io>

* Linting and tid generation

* Fix test

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
pull/32508/head
Paulus Schoutsen 2020-03-05 12:55:48 -08:00 committed by GitHub
parent b5022f5bcb
commit 873bf887a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 131 additions and 3 deletions

View File

@ -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:

View File

@ -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."""

View File

@ -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"