Skip to content

Commit

Permalink
Add OwnTracks Friends via person integration (home-assistant#27303)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Linting and tid generation

* Fix test

Co-authored-by: Paulus Schoutsen <[email protected]>
  • Loading branch information
balloob and balloob authored Mar 5, 2020
1 parent b5022f5 commit 873bf88
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 3 deletions.
34 changes: 31 additions & 3 deletions homeassistant/components/owntracks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

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__)

Expand Down Expand Up @@ -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()
Expand All @@ -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":
Expand All @@ -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:
Expand Down
31 changes: 31 additions & 0 deletions homeassistant/components/owntracks/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
69 changes: 69 additions & 0 deletions tests/components/owntracks/test_device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit 873bf88

Please sign in to comment.