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
parent
b5022f5bcb
commit
873bf887a5
|
@ -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:
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue