Mobile App add device tracker to person registering app (#30460)
This commit is contained in:
parent
e233dd7cbe
commit
95cd0a2c68
8 changed files with 174 additions and 43 deletions
|
@ -1,7 +1,11 @@
|
|||
"""Config flow for Mobile App."""
|
||||
from homeassistant import config_entries
|
||||
import uuid
|
||||
|
||||
from .const import ATTR_DEVICE_ID, ATTR_DEVICE_NAME, DOMAIN
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import person
|
||||
from homeassistant.helpers import entity_registry
|
||||
|
||||
from .const import ATTR_APP_ID, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
|
@ -23,7 +27,25 @@ class MobileAppFlowHandler(config_entries.ConfigFlow):
|
|||
|
||||
async def async_step_registration(self, user_input=None):
|
||||
"""Handle a flow initialized during registration."""
|
||||
await self.async_set_unique_id(user_input[ATTR_DEVICE_ID])
|
||||
if ATTR_DEVICE_ID in user_input:
|
||||
# Unique ID is combi of app + device ID.
|
||||
await self.async_set_unique_id(
|
||||
f"{user_input[ATTR_APP_ID]}-{user_input[ATTR_DEVICE_ID]}"
|
||||
)
|
||||
else:
|
||||
user_input[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "")
|
||||
|
||||
# Register device tracker entity and add to person registering app
|
||||
ent_reg = await entity_registry.async_get_registry(self.hass)
|
||||
devt_entry = ent_reg.async_get_or_create(
|
||||
"device_tracker",
|
||||
DOMAIN,
|
||||
user_input[ATTR_DEVICE_ID],
|
||||
suggested_object_id=user_input[ATTR_DEVICE_NAME],
|
||||
)
|
||||
await person.async_add_user_device_tracker(
|
||||
self.hass, user_input[CONF_USER_ID], devt_entry.entity_id
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[ATTR_DEVICE_NAME], data=user_input
|
||||
|
|
|
@ -25,7 +25,6 @@ ATTR_DEVICE_ID = "device_id"
|
|||
ATTR_DEVICE_NAME = "device_name"
|
||||
ATTR_MANUFACTURER = "manufacturer"
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_MODEL_ID = "model_id"
|
||||
ATTR_OS_NAME = "os_name"
|
||||
ATTR_OS_VERSION = "os_version"
|
||||
ATTR_PUSH_TOKEN = "push_token"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Provides an HTTP API for mobile_app."""
|
||||
import secrets
|
||||
from typing import Dict
|
||||
import uuid
|
||||
|
||||
from aiohttp.web import Request, Response
|
||||
from nacl.secret import SecretBox
|
||||
|
@ -21,7 +20,6 @@ from .const import (
|
|||
ATTR_DEVICE_NAME,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_MODEL_ID,
|
||||
ATTR_OS_NAME,
|
||||
ATTR_OS_VERSION,
|
||||
ATTR_SUPPORTS_ENCRYPTION,
|
||||
|
@ -50,7 +48,7 @@ class RegistrationsView(HomeAssistantView):
|
|||
vol.Required(ATTR_DEVICE_NAME): cv.string,
|
||||
vol.Required(ATTR_MANUFACTURER): cv.string,
|
||||
vol.Required(ATTR_MODEL): cv.string,
|
||||
vol.Optional(ATTR_MODEL_ID): cv.string, # Added in 0.104
|
||||
vol.Optional(ATTR_DEVICE_ID): cv.string, # Added in 0.104
|
||||
vol.Required(ATTR_OS_NAME): cv.string,
|
||||
vol.Optional(ATTR_OS_VERSION): cv.string,
|
||||
vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean,
|
||||
|
@ -70,14 +68,6 @@ class RegistrationsView(HomeAssistantView):
|
|||
CONF_CLOUDHOOK_URL
|
||||
] = await hass.components.cloud.async_create_cloudhook(webhook_id)
|
||||
|
||||
model_id = data.get(ATTR_MODEL_ID)
|
||||
|
||||
if model_id is None:
|
||||
data[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "")
|
||||
|
||||
else:
|
||||
data[ATTR_DEVICE_ID] = f"{data[ATTR_APP_ID]}-{model_id}"
|
||||
|
||||
data[CONF_WEBHOOK_ID] = webhook_id
|
||||
|
||||
if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption():
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mobile_app",
|
||||
"requirements": ["PyNaCl==1.3.0"],
|
||||
"dependencies": ["http", "webhook"],
|
||||
"dependencies": ["http", "webhook", "person"],
|
||||
"after_dependencies": ["cloud"],
|
||||
"codeowners": ["@robbiet480"]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Support for tracking people."""
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -24,9 +24,8 @@ from homeassistant.const import (
|
|||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import Event, State, callback
|
||||
from homeassistant.helpers import collection, entity_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id
|
||||
from homeassistant.helpers import collection, config_validation as cv, entity_registry
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
@ -77,6 +76,29 @@ async def async_create_person(hass, name, *, user_id=None, device_trackers=None)
|
|||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_add_user_device_tracker(
|
||||
hass: HomeAssistant, user_id: str, device_tracker_entity_id: str
|
||||
):
|
||||
"""Add a device tracker to a person linked to a user."""
|
||||
coll = cast(PersonStorageCollection, hass.data[DOMAIN][1])
|
||||
|
||||
for person in coll.async_items():
|
||||
if person.get(ATTR_USER_ID) != user_id:
|
||||
continue
|
||||
|
||||
device_trackers = person["device_trackers"]
|
||||
|
||||
if device_tracker_entity_id in device_trackers:
|
||||
return
|
||||
|
||||
await coll.async_update_item(
|
||||
person[collection.CONF_ID],
|
||||
{"device_trackers": device_trackers + [device_tracker_entity_id]},
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
CREATE_FIELDS = {
|
||||
vol.Required("name"): vol.All(str, vol.Length(min=1)),
|
||||
vol.Optional("user_id"): vol.Any(str, None),
|
||||
|
@ -124,6 +146,36 @@ class PersonStorageCollection(collection.StorageCollection):
|
|||
self.async_add_listener(self._collection_changed)
|
||||
self.yaml_collection = yaml_collection
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the Storage collection."""
|
||||
await super().async_load()
|
||||
self.hass.bus.async_listen(
|
||||
entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._entity_registry_updated
|
||||
)
|
||||
|
||||
async def _entity_registry_updated(self, event) -> None:
|
||||
"""Handle entity registry updated."""
|
||||
if event.data["action"] != "remove":
|
||||
return
|
||||
|
||||
entity_id = event.data["entity_id"]
|
||||
|
||||
if split_entity_id(entity_id)[0] != "device_tracker":
|
||||
return
|
||||
|
||||
for person in list(self.data.values()):
|
||||
if entity_id not in person["device_trackers"]:
|
||||
continue
|
||||
|
||||
await self.async_update_item(
|
||||
person[collection.CONF_ID],
|
||||
{
|
||||
"device_trackers": [
|
||||
devt for devt in person["device_trackers"] if devt != entity_id
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
async def _process_create_data(self, data: dict) -> dict:
|
||||
"""Validate the config is valid."""
|
||||
data = self.CREATE_SCHEMA(data)
|
||||
|
|
|
@ -17,7 +17,6 @@ REGISTER = {
|
|||
"device_name": "Test 1",
|
||||
"manufacturer": "mobile_app",
|
||||
"model": "Test",
|
||||
"model_id": "mock-model-id",
|
||||
"os_name": "Linux",
|
||||
"os_version": "1.0",
|
||||
"supports_encryption": True,
|
||||
|
@ -31,6 +30,7 @@ REGISTER_CLEARTEXT = {
|
|||
"device_name": "Test 1",
|
||||
"manufacturer": "mobile_app",
|
||||
"model": "Test",
|
||||
"device_id": "mock-device-id",
|
||||
"os_name": "Linux",
|
||||
"os_version": "1.0",
|
||||
"supports_encryption": False,
|
||||
|
|
|
@ -1,15 +1,62 @@
|
|||
"""Tests for the mobile_app HTTP API."""
|
||||
# pylint: disable=redefined-outer-name,unused-import
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import REGISTER, RENDER_TEMPLATE
|
||||
from .const import REGISTER, REGISTER_CLEARTEXT, RENDER_TEMPLATE
|
||||
|
||||
from tests.common import mock_coro
|
||||
|
||||
|
||||
async def test_registration(hass, hass_client):
|
||||
async def test_registration(hass, hass_client, hass_admin_user):
|
||||
"""Test that registrations happen."""
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
api_client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.person.async_add_user_device_tracker",
|
||||
spec=True,
|
||||
return_value=mock_coro(),
|
||||
) as add_user_dev_track:
|
||||
resp = await api_client.post(
|
||||
"/api/mobile_app/registrations", json=REGISTER_CLEARTEXT
|
||||
)
|
||||
|
||||
assert len(add_user_dev_track.mock_calls) == 1
|
||||
assert add_user_dev_track.mock_calls[0][1][1] == hass_admin_user.id
|
||||
assert add_user_dev_track.mock_calls[0][1][2] == "device_tracker.test_1"
|
||||
|
||||
assert resp.status == 201
|
||||
register_json = await resp.json()
|
||||
assert CONF_WEBHOOK_ID in register_json
|
||||
assert CONF_SECRET in register_json
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
assert entries[0].unique_id == "io.homeassistant.mobile_app_test-mock-device-id"
|
||||
assert entries[0].data["device_id"] == REGISTER_CLEARTEXT["device_id"]
|
||||
assert entries[0].data["app_data"] == REGISTER_CLEARTEXT["app_data"]
|
||||
assert entries[0].data["app_id"] == REGISTER_CLEARTEXT["app_id"]
|
||||
assert entries[0].data["app_name"] == REGISTER_CLEARTEXT["app_name"]
|
||||
assert entries[0].data["app_version"] == REGISTER_CLEARTEXT["app_version"]
|
||||
assert entries[0].data["device_name"] == REGISTER_CLEARTEXT["device_name"]
|
||||
assert entries[0].data["manufacturer"] == REGISTER_CLEARTEXT["manufacturer"]
|
||||
assert entries[0].data["model"] == REGISTER_CLEARTEXT["model"]
|
||||
assert entries[0].data["os_name"] == REGISTER_CLEARTEXT["os_name"]
|
||||
assert entries[0].data["os_version"] == REGISTER_CLEARTEXT["os_version"]
|
||||
assert (
|
||||
entries[0].data["supports_encryption"]
|
||||
== REGISTER_CLEARTEXT["supports_encryption"]
|
||||
)
|
||||
|
||||
|
||||
async def test_registration_encryption(hass, hass_client):
|
||||
"""Test that registrations happen."""
|
||||
try:
|
||||
from nacl.secret import SecretBox
|
||||
|
@ -18,8 +65,6 @@ async def test_registration(hass, hass_client):
|
|||
pytest.skip("libnacl/libsodium is not installed")
|
||||
return
|
||||
|
||||
import json
|
||||
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
|
||||
api_client = await hass_client()
|
||||
|
@ -28,22 +73,6 @@ async def test_registration(hass, hass_client):
|
|||
|
||||
assert resp.status == 201
|
||||
register_json = await resp.json()
|
||||
assert CONF_WEBHOOK_ID in register_json
|
||||
assert CONF_SECRET in register_json
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
assert entries[0].unique_id == "io.homeassistant.mobile_app_test-mock-model-id"
|
||||
assert entries[0].data["app_data"] == REGISTER["app_data"]
|
||||
assert entries[0].data["app_id"] == REGISTER["app_id"]
|
||||
assert entries[0].data["app_name"] == REGISTER["app_name"]
|
||||
assert entries[0].data["app_version"] == REGISTER["app_version"]
|
||||
assert entries[0].data["device_name"] == REGISTER["device_name"]
|
||||
assert entries[0].data["manufacturer"] == REGISTER["manufacturer"]
|
||||
assert entries[0].data["model"] == REGISTER["model"]
|
||||
assert entries[0].data["os_name"] == REGISTER["os_name"]
|
||||
assert entries[0].data["os_version"] == REGISTER["os_version"]
|
||||
assert entries[0].data["supports_encryption"] == REGISTER["supports_encryption"]
|
||||
|
||||
keylen = SecretBox.KEY_SIZE
|
||||
key = register_json[CONF_SECRET].encode("utf-8")
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import CoreState, State
|
||||
from homeassistant.helpers import collection
|
||||
from homeassistant.helpers import collection, entity_registry
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import assert_setup_component, mock_component, mock_restore_cache
|
||||
|
@ -664,3 +664,42 @@ async def test_update_person_when_user_removed(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert storage_collection.data[person["id"]]["user_id"] is None
|
||||
|
||||
|
||||
async def test_removing_device_tracker(hass, storage_setup):
|
||||
"""Test we automatically remove removed device trackers."""
|
||||
storage_collection = hass.data[DOMAIN][1]
|
||||
reg = await entity_registry.async_get_registry(hass)
|
||||
entry = reg.async_get_or_create(
|
||||
"device_tracker", "mobile_app", "bla", suggested_object_id="pixel"
|
||||
)
|
||||
|
||||
person = await storage_collection.async_create_item(
|
||||
{"name": "Hello", "device_trackers": [entry.entity_id]}
|
||||
)
|
||||
|
||||
reg.async_remove(entry.entity_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert storage_collection.data[person["id"]]["device_trackers"] == []
|
||||
|
||||
|
||||
async def test_add_user_device_tracker(hass, storage_setup, hass_read_only_user):
|
||||
"""Test adding a device tracker to a person tied to a user."""
|
||||
storage_collection = hass.data[DOMAIN][1]
|
||||
pers = await storage_collection.async_create_item(
|
||||
{
|
||||
"name": "Hello",
|
||||
"user_id": hass_read_only_user.id,
|
||||
"device_trackers": ["device_tracker.on_create"],
|
||||
}
|
||||
)
|
||||
|
||||
await person.async_add_user_device_tracker(
|
||||
hass, hass_read_only_user.id, "device_tracker.added"
|
||||
)
|
||||
|
||||
assert storage_collection.data[pers["id"]]["device_trackers"] == [
|
||||
"device_tracker.on_create",
|
||||
"device_tracker.added",
|
||||
]
|
||||
|
|
Loading…
Add table
Reference in a new issue