Allow mobile app registrations only supporting websocket push (#63208)
parent
9f0805f512
commit
ad8af5fc7a
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"}}
|
||||
|
|
Loading…
Reference in New Issue