1
mirror of https://github.com/home-assistant/core synced 2024-10-01 05:30:36 +02:00

Allow mobile app registrations only supporting websocket push (#63208)

This commit is contained in:
Paulus Schoutsen 2022-01-03 11:02:41 -08:00 committed by GitHub
parent 9f0805f512
commit ad8af5fc7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 124 additions and 20 deletions

View File

@ -551,8 +551,8 @@ homeassistant/components/minecraft_server/* @elmurato
tests/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan
tests/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480
tests/components/mobile_app/* @robbiet480
homeassistant/components/mobile_app/* @home-assistant/core
tests/components/mobile_app/* @home-assistant/core
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
tests/components/modbus/* @adamchengtkc @janiversen @vzahradnik
homeassistant/components/modem_callerid/* @tkdrob

View File

@ -1,4 +1,8 @@
"""Constants for mobile_app."""
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
DOMAIN = "mobile_app"
STORAGE_KEY = DOMAIN
@ -26,6 +30,7 @@ ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL = "model"
ATTR_OS_NAME = "os_name"
ATTR_OS_VERSION = "os_version"
ATTR_PUSH_WEBSOCKET_CHANNEL = "push_websocket_channel"
ATTR_PUSH_TOKEN = "push_token"
ATTR_PUSH_URL = "push_url"
ATTR_PUSH_RATE_LIMITS = "rateLimits"
@ -76,3 +81,14 @@ SIGNAL_SENSOR_UPDATE = f"{DOMAIN}_sensor_update"
SIGNAL_LOCATION_UPDATE = DOMAIN + "_location_update_{}"
ATTR_CAMERA_ENTITY_ID = "camera_entity_id"
SCHEMA_APP_DATA = vol.Schema(
{
vol.Inclusive(ATTR_PUSH_TOKEN, "push_cloud"): cv.string,
vol.Inclusive(ATTR_PUSH_URL, "push_cloud"): cv.url,
# Set to True to indicate that this registration will connect via websocket channel
# to receive push notifications.
vol.Optional(ATTR_PUSH_WEBSOCKET_CHANNEL): cv.boolean,
},
extra=vol.ALLOW_EXTRA,
)

View File

@ -32,6 +32,7 @@ from .const import (
CONF_SECRET,
CONF_USER_ID,
DOMAIN,
SCHEMA_APP_DATA,
)
from .helpers import supports_encryption
@ -45,7 +46,7 @@ class RegistrationsView(HomeAssistantView):
@RequestDataValidator(
vol.Schema(
{
vol.Optional(ATTR_APP_DATA, default={}): dict,
vol.Optional(ATTR_APP_DATA, default={}): SCHEMA_APP_DATA,
vol.Required(ATTR_APP_ID): cv.string,
vol.Required(ATTR_APP_NAME): cv.string,
vol.Required(ATTR_APP_VERSION): cv.string,

View File

@ -6,7 +6,7 @@
"requirements": ["PyNaCl==1.4.0", "emoji==1.5.0"],
"dependencies": ["http", "webhook", "person", "tag", "websocket_api"],
"after_dependencies": ["cloud", "camera", "notify"],
"codeowners": ["@robbiet480"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal",
"iot_class": "local_push"
}

View File

@ -15,6 +15,7 @@ from homeassistant.components.notify import (
ATTR_TITLE_DEFAULT,
BaseNotificationService,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.util.dt as dt_util
@ -118,20 +119,28 @@ class MobileAppNotificationService(BaseNotificationService):
local_push_channels = self.hass.data[DOMAIN][DATA_PUSH_CHANNEL]
for target in targets:
registration = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target].data
if target in local_push_channels:
local_push_channels[target].async_send_notification(
data, partial(self._async_send_remote_message_target, target)
data,
partial(
self._async_send_remote_message_target, target, registration
),
)
continue
await self._async_send_remote_message_target(target, data)
# Test if local push only.
if ATTR_PUSH_URL not in registration[ATTR_APP_DATA]:
raise HomeAssistantError(
"Device not connected to local push notifications"
)
async def _async_send_remote_message_target(self, target, data):
await self._async_send_remote_message_target(target, registration, data)
async def _async_send_remote_message_target(self, target, registration, data):
"""Send a message to a target."""
entry = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target]
entry_data = entry.data
app_data = entry_data[ATTR_APP_DATA]
app_data = registration[ATTR_APP_DATA]
push_token = app_data[ATTR_PUSH_TOKEN]
push_url = app_data[ATTR_PUSH_URL]
@ -139,12 +148,12 @@ class MobileAppNotificationService(BaseNotificationService):
target_data[ATTR_PUSH_TOKEN] = push_token
reg_info = {
ATTR_APP_ID: entry_data[ATTR_APP_ID],
ATTR_APP_VERSION: entry_data[ATTR_APP_VERSION],
ATTR_APP_ID: registration[ATTR_APP_ID],
ATTR_APP_VERSION: registration[ATTR_APP_VERSION],
ATTR_WEBHOOK_ID: target,
}
if ATTR_OS_VERSION in entry_data:
reg_info[ATTR_OS_VERSION] = entry_data[ATTR_OS_VERSION]
if ATTR_OS_VERSION in registration:
reg_info[ATTR_OS_VERSION] = registration[ATTR_OS_VERSION]
target_data["registration_info"] = reg_info
@ -160,7 +169,7 @@ class MobileAppNotificationService(BaseNotificationService):
HTTPStatus.CREATED,
HTTPStatus.ACCEPTED,
):
log_rate_limits(self.hass, entry_data[ATTR_DEVICE_NAME], result)
log_rate_limits(self.hass, registration[ATTR_DEVICE_NAME], result)
return
fallback_error = result.get("errorMessage", "Unknown error")
@ -177,7 +186,7 @@ class MobileAppNotificationService(BaseNotificationService):
if response.status == HTTPStatus.TOO_MANY_REQUESTS:
_LOGGER.warning(message)
log_rate_limits(
self.hass, entry_data[ATTR_DEVICE_NAME], result, logging.WARNING
self.hass, registration[ATTR_DEVICE_NAME], result, logging.WARNING
)
else:
_LOGGER.error(message)

View File

@ -9,6 +9,7 @@ from .const import (
ATTR_APP_DATA,
ATTR_PUSH_TOKEN,
ATTR_PUSH_URL,
ATTR_PUSH_WEBSOCKET_CHANNEL,
DATA_CONFIG_ENTRIES,
DATA_DEVICES,
DATA_NOTIFY,
@ -37,7 +38,9 @@ def supports_push(hass, webhook_id: str) -> bool:
"""Return if push notifications is supported."""
config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id]
app_data = config_entry.data[ATTR_APP_DATA]
return ATTR_PUSH_TOKEN in app_data and ATTR_PUSH_URL in app_data
return (
ATTR_PUSH_TOKEN in app_data and ATTR_PUSH_URL in app_data
) or ATTR_PUSH_WEBSOCKET_CHANNEL in app_data
@callback

View File

@ -91,6 +91,7 @@ from .const import (
ERR_ENCRYPTION_REQUIRED,
ERR_INVALID_FORMAT,
ERR_SENSOR_NOT_REGISTERED,
SCHEMA_APP_DATA,
SIGNAL_LOCATION_UPDATE,
SIGNAL_SENSOR_UPDATE,
)
@ -332,7 +333,7 @@ async def webhook_update_location(hass, config_entry, data):
@WEBHOOK_COMMANDS.register("update_registration")
@validate_schema(
{
vol.Optional(ATTR_APP_DATA, default={}): dict,
vol.Optional(ATTR_APP_DATA): SCHEMA_APP_DATA,
vol.Required(ATTR_APP_VERSION): cv.string,
vol.Required(ATTR_DEVICE_NAME): cv.string,
vol.Required(ATTR_MANUFACTURER): cv.string,

View File

@ -286,7 +286,12 @@ async def async_test_home_assistant(loop, load_registries=True):
hass.config.media_dirs = {"local": get_test_config_dir("media")}
hass.config.skip_pip = True
hass.config_entries = config_entries.ConfigEntries(hass, {})
hass.config_entries = config_entries.ConfigEntries(
hass,
{
"_": "Not empty or else some bad checks for hass config in discovery.py breaks"
},
)
hass.config_entries._entries = {}
hass.config_entries._store._async_ensure_stop_listener = lambda: None

View File

@ -5,6 +5,7 @@ from unittest.mock import patch
import pytest
from homeassistant.components.mobile_app.const import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@ -102,6 +103,38 @@ async def setup_push_receiver(hass, aioclient_mock, hass_admin_user):
assert hass.services.has_service("notify", "mobile_app_loaded_late")
@pytest.fixture
async def setup_websocket_channel_only_push(hass, hass_admin_user):
"""Set up local push."""
entry = MockConfigEntry(
data={
"app_data": {"push_websocket_channel": True},
"app_id": "io.homeassistant.mobile_app",
"app_name": "mobile_app tests",
"app_version": "1.0",
"device_id": "websocket-push-device-id",
"device_name": "Websocket Push Name",
"manufacturer": "Home Assistant",
"model": "mobile_app",
"os_name": "Linux",
"os_version": "5.0.6",
"secret": "123abc2",
"supports_encryption": False,
"user_id": hass_admin_user.id,
"webhook_id": "websocket-push-webhook-id",
},
domain=DOMAIN,
source="registration",
title="websocket push test entry",
version=1,
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert hass.services.has_service("notify", "mobile_app_websocket_push_name")
async def test_notify_works(hass, aioclient_mock, setup_push_receiver):
"""Test notify works."""
assert hass.services.has_service("notify", "mobile_app_test") is True
@ -333,3 +366,39 @@ async def test_notify_ws_not_confirming(
)
assert len(aioclient_mock.mock_calls) == 3
async def test_local_push_only(hass, hass_ws_client, setup_websocket_channel_only_push):
"""Test a local only push registration."""
with pytest.raises(HomeAssistantError) as e_info:
assert await hass.services.async_call(
"notify",
"mobile_app_websocket_push_name",
{"message": "Not connected"},
blocking=True,
)
assert str(e_info.value) == "Device not connected to local push notifications"
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 5,
"type": "mobile_app/push_notification_channel",
"webhook_id": "websocket-push-webhook-id",
}
)
sub_result = await client.receive_json()
assert sub_result["success"]
assert await hass.services.async_call(
"notify",
"mobile_app_websocket_push_name",
{"message": "Hello world 1"},
blocking=True,
)
msg = await client.receive_json()
assert msg == {"id": 5, "type": "event", "event": {"message": "Hello world 1"}}