Enable config flow for html5 (#112806)

* html5: Enable config flow

* Add tests

* attempt check create_issue

* replace len with call_count

* fix config flow tests

* test user config

* more tests

* remove whitespace

* Update homeassistant/components/html5/issues.py

Co-authored-by: Steven B. <51370195+sdb9696@users.noreply.github.com>

* Update homeassistant/components/html5/issues.py

Co-authored-by: Steven B. <51370195+sdb9696@users.noreply.github.com>

* fix config

* Adjust issues log

* lint

* lint

* rename create issue

* fix typing

* update codeowners

* fix test

* fix tests

* Update issues.py

* Update tests/components/html5/test_config_flow.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update tests/components/html5/test_config_flow.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update tests/components/html5/test_config_flow.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* update from review

* remove ternary

* fix

* fix missing service

* fix tests

* updates

* adress review comments

* fix indent

* fix

* fix format

* cleanup from review

* Restore config schema and use HA issue

* Restore config schema and use HA issue

---------

Co-authored-by: alexyao2015 <alexyao2015@users.noreply.github.com>
Co-authored-by: Steven B. <51370195+sdb9696@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Joostlek <joostlek@outlook.com>
pull/124749/head^2
Alex Yao 2024-08-30 16:22:14 -05:00 committed by GitHub
parent ac39bf991f
commit 26281662b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 497 additions and 37 deletions

View File

@ -633,6 +633,8 @@ build.json @home-assistant/supervisor
/tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer
/tests/components/honeywell/ @rdfurman @mkmer
/homeassistant/components/html5/ @alexyao2015
/tests/components/html5/ @alexyao2015
/homeassistant/components/http/ @home-assistant/core
/tests/components/http/ @home-assistant/core
/homeassistant/components/huawei_lte/ @scop @fphammerle

View File

@ -1 +1,16 @@
"""The html5 component."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from .const import DOMAIN
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HTML5 from a config entry."""
await discovery.async_load_platform(
hass, Platform.NOTIFY, DOMAIN, dict(entry.data), {}
)
return True

View File

@ -0,0 +1,103 @@
"""Config flow for the html5 component."""
import binascii
from typing import Any, cast
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from py_vapid import Vapid
from py_vapid.utils import b64urlencode
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
from .const import ATTR_VAPID_EMAIL, ATTR_VAPID_PRV_KEY, ATTR_VAPID_PUB_KEY, DOMAIN
from .issues import async_create_html5_issue
def vapid_generate_private_key() -> str:
"""Generate a VAPID private key."""
private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
return b64urlencode(
binascii.unhexlify(f"{private_key.private_numbers().private_value:x}".zfill(64))
)
def vapid_get_public_key(private_key: str) -> str:
"""Get the VAPID public key from a private key."""
vapid = Vapid.from_string(private_key)
public_key = cast(ec.EllipticCurvePublicKey, vapid.public_key)
return b64urlencode(
public_key.public_bytes(
serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
)
)
class HTML5ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HTML5."""
@callback
def _async_create_html5_entry(
self: "HTML5ConfigFlow", data: dict[str, str]
) -> tuple[dict[str, str], ConfigFlowResult | None]:
"""Create an HTML5 entry."""
errors = {}
flow_result = None
if not data.get(ATTR_VAPID_PRV_KEY):
data[ATTR_VAPID_PRV_KEY] = vapid_generate_private_key()
# we will always generate the corresponding public key
try:
data[ATTR_VAPID_PUB_KEY] = vapid_get_public_key(data[ATTR_VAPID_PRV_KEY])
except (ValueError, binascii.Error):
errors[ATTR_VAPID_PRV_KEY] = "invalid_prv_key"
if not errors:
config = {
ATTR_VAPID_EMAIL: data[ATTR_VAPID_EMAIL],
ATTR_VAPID_PRV_KEY: data[ATTR_VAPID_PRV_KEY],
ATTR_VAPID_PUB_KEY: data[ATTR_VAPID_PUB_KEY],
CONF_NAME: DOMAIN,
}
flow_result = self.async_create_entry(title="HTML5", data=config)
return errors, flow_result
async def async_step_user(
self: "HTML5ConfigFlow", user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input:
errors, flow_result = self._async_create_html5_entry(user_input)
if flow_result:
return flow_result
else:
user_input = {}
return self.async_show_form(
data_schema=vol.Schema(
{
vol.Required(
ATTR_VAPID_EMAIL, default=user_input.get(ATTR_VAPID_EMAIL, "")
): str,
vol.Optional(ATTR_VAPID_PRV_KEY): str,
}
),
errors=errors,
)
async def async_step_import(
self: "HTML5ConfigFlow", import_config: dict
) -> ConfigFlowResult:
"""Handle config import from yaml."""
_, flow_result = self._async_create_html5_entry(import_config)
if not flow_result:
async_create_html5_issue(self.hass, False)
return self.async_abort(reason="invalid_config")
async_create_html5_issue(self.hass, True)
return flow_result

View File

@ -1,4 +1,9 @@
"""Constants for the HTML5 component."""
DOMAIN = "html5"
DATA_HASS_CONFIG = "html5_hass_config"
SERVICE_DISMISS = "dismiss"
ATTR_VAPID_PUB_KEY = "vapid_pub_key"
ATTR_VAPID_PRV_KEY = "vapid_prv_key"
ATTR_VAPID_EMAIL = "vapid_email"

View File

@ -0,0 +1,50 @@
"""Issues utility for HTML5."""
import logging
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SUCCESSFUL_IMPORT_TRANSLATION_KEY = "deprecated_yaml"
FAILED_IMPORT_TRANSLATION_KEY = "deprecated_yaml_import_issue"
INTEGRATION_TITLE = "HTML5 Push Notifications"
@callback
def async_create_html5_issue(hass: HomeAssistant, import_success: bool) -> None:
"""Create issues for HTML5."""
if import_success:
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2025.4.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": INTEGRATION_TITLE,
},
)
else:
async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2025.4.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_issue",
translation_placeholders={
"domain": DOMAIN,
"integration_title": INTEGRATION_TITLE,
},
)

View File

@ -1,10 +1,12 @@
{
"domain": "html5",
"name": "HTML5 Push Notifications",
"codeowners": [],
"codeowners": ["@alexyao2015"],
"config_flow": true,
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/html5",
"iot_class": "cloud_push",
"loggers": ["http_ece", "py_vapid", "pywebpush"],
"requirements": ["pywebpush==1.14.1"]
"requirements": ["pywebpush==1.14.1"],
"single_config_entry": true
}

View File

@ -29,6 +29,7 @@ from homeassistant.components.notify import (
PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA,
BaseNotificationService,
)
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ATTR_NAME, URL_ROOT
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
@ -38,32 +39,23 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import ensure_unique_string
from homeassistant.util.json import JsonObjectType, load_json_object
from .const import DOMAIN, SERVICE_DISMISS
from .const import (
ATTR_VAPID_EMAIL,
ATTR_VAPID_PRV_KEY,
ATTR_VAPID_PUB_KEY,
DOMAIN,
SERVICE_DISMISS,
)
from .issues import async_create_html5_issue
_LOGGER = logging.getLogger(__name__)
REGISTRATIONS_FILE = "html5_push_registrations.conf"
ATTR_VAPID_PUB_KEY = "vapid_pub_key"
ATTR_VAPID_PRV_KEY = "vapid_prv_key"
ATTR_VAPID_EMAIL = "vapid_email"
def gcm_api_deprecated(value):
"""Warn user that GCM API config is deprecated."""
if value:
_LOGGER.warning(
"Configuring html5_push_notifications via the GCM api"
" has been deprecated and stopped working since May 29,"
" 2019. Use the VAPID configuration instead. For instructions,"
" see https://www.home-assistant.io/integrations/html5/"
)
return value
PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
{
vol.Optional("gcm_sender_id"): vol.All(cv.string, gcm_api_deprecated),
vol.Optional("gcm_sender_id"): cv.string,
vol.Optional("gcm_api_key"): cv.string,
vol.Required(ATTR_VAPID_PUB_KEY): cv.string,
vol.Required(ATTR_VAPID_PRV_KEY): cv.string,
@ -171,15 +163,30 @@ async def async_get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> HTML5NotificationService | None:
"""Get the HTML5 push notification service."""
if config:
existing_config_entry = hass.config_entries.async_entries(DOMAIN)
if existing_config_entry:
async_create_html5_issue(hass, True)
return None
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)
return None
if discovery_info is None:
return None
json_path = hass.config.path(REGISTRATIONS_FILE)
registrations = await hass.async_add_executor_job(_load_config, json_path)
vapid_pub_key = config[ATTR_VAPID_PUB_KEY]
vapid_prv_key = config[ATTR_VAPID_PRV_KEY]
vapid_email = config[ATTR_VAPID_EMAIL]
vapid_pub_key = discovery_info[ATTR_VAPID_PUB_KEY]
vapid_prv_key = discovery_info[ATTR_VAPID_PRV_KEY]
vapid_email = discovery_info[ATTR_VAPID_EMAIL]
def websocket_appkey(hass, connection, msg):
def websocket_appkey(_hass, connection, msg):
connection.send_message(websocket_api.result_message(msg["id"], vapid_pub_key))
websocket_api.async_register_command(

View File

@ -1,4 +1,31 @@
{
"config": {
"step": {
"user": {
"data": {
"vapid_email": "[%key:common::config_flow::data::email%]",
"vapid_prv_key": "VAPID private key"
},
"data_description": {
"vapid_email": "Email to use for html5 push notifications.",
"vapid_prv_key": "If not specified, one will be automatically generated."
}
}
},
"error": {
"unknown": "Unknown error",
"invalid_prv_key": "Invalid private key"
},
"abort": {
"invalid_config": "Invalid configuration"
}
},
"issues": {
"deprecated_yaml_import_issue": {
"title": "HTML5 YAML configuration import failed",
"description": "Configuring HTML5 push notification using YAML has been deprecated. An automatic import of your existing configuration was attempted, but it failed.\n\nPlease remove the HTML5 push notification YAML configuration from your configuration.yaml file and reconfigure HTML5 push notification again manually."
}
},
"services": {
"dismiss": {
"name": "Dismiss",

View File

@ -253,6 +253,7 @@ FLOWS = {
"homewizard",
"homeworks",
"honeywell",
"html5",
"huawei_lte",
"hue",
"huisbaasje",

View File

@ -2633,8 +2633,9 @@
"html5": {
"name": "HTML5 Push Notifications",
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_push"
"config_flow": true,
"iot_class": "cloud_push",
"single_config_entry": true
},
"huawei_lte": {
"name": "Huawei LTE",

View File

@ -0,0 +1,203 @@
"""Test the HTML5 config flow."""
from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.html5.const import (
ATTR_VAPID_EMAIL,
ATTR_VAPID_PRV_KEY,
ATTR_VAPID_PUB_KEY,
DOMAIN,
)
from homeassistant.components.html5.issues import (
FAILED_IMPORT_TRANSLATION_KEY,
SUCCESSFUL_IMPORT_TRANSLATION_KEY,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
import homeassistant.helpers.issue_registry as ir
MOCK_CONF = {
ATTR_VAPID_EMAIL: "test@example.com",
ATTR_VAPID_PRV_KEY: "h6acSRds8_KR8hT9djD8WucTL06Gfe29XXyZ1KcUjN8",
}
MOCK_CONF_PUB_KEY = "BIUtPN7Rq_8U7RBEqClZrfZ5dR9zPCfvxYPtLpWtRVZTJEc7lzv2dhzDU6Aw1m29Ao0-UA1Uq6XO9Df8KALBKqA"
async def test_step_user_success(hass: HomeAssistant) -> None:
"""Test a successful user config flow."""
with patch(
"homeassistant.components.html5.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=MOCK_CONF.copy(),
)
await hass.async_block_till_done()
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"] == {
ATTR_VAPID_PRV_KEY: MOCK_CONF[ATTR_VAPID_PRV_KEY],
ATTR_VAPID_PUB_KEY: MOCK_CONF_PUB_KEY,
ATTR_VAPID_EMAIL: MOCK_CONF[ATTR_VAPID_EMAIL],
CONF_NAME: DOMAIN,
}
assert mock_setup_entry.call_count == 1
async def test_step_user_success_generate(hass: HomeAssistant) -> None:
"""Test a successful user config flow, generating a key pair."""
with patch(
"homeassistant.components.html5.async_setup_entry",
return_value=True,
) as mock_setup_entry:
conf = {ATTR_VAPID_EMAIL: MOCK_CONF[ATTR_VAPID_EMAIL]}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
)
await hass.async_block_till_done()
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"][ATTR_VAPID_EMAIL] == MOCK_CONF[ATTR_VAPID_EMAIL]
assert mock_setup_entry.call_count == 1
async def test_step_user_new_form(hass: HomeAssistant) -> None:
"""Test new user input."""
with patch(
"homeassistant.components.html5.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
)
await hass.async_block_till_done()
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert mock_setup_entry.call_count == 0
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MOCK_CONF
)
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert mock_setup_entry.call_count == 1
@pytest.mark.parametrize(
("key", "value"),
[
(ATTR_VAPID_PRV_KEY, "invalid"),
],
)
async def test_step_user_form_invalid_key(
hass: HomeAssistant, key: str, value: str
) -> None:
"""Test invalid user input."""
with patch(
"homeassistant.components.html5.async_setup_entry",
return_value=True,
) as mock_setup_entry:
bad_conf = MOCK_CONF.copy()
bad_conf[key] = value
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=bad_conf
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert mock_setup_entry.call_count == 0
result = await hass.config_entries.flow.async_configure(
result["flow_id"], MOCK_CONF
)
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert mock_setup_entry.call_count == 1
async def test_step_import_good(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test valid import input."""
with (
patch(
"homeassistant.components.html5.async_setup_entry",
return_value=True,
) as mock_setup_entry,
):
conf = MOCK_CONF.copy()
conf[ATTR_VAPID_PUB_KEY] = MOCK_CONF_PUB_KEY
conf["random_key"] = "random_value"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"] == {
ATTR_VAPID_PRV_KEY: conf[ATTR_VAPID_PRV_KEY],
ATTR_VAPID_PUB_KEY: MOCK_CONF_PUB_KEY,
ATTR_VAPID_EMAIL: conf[ATTR_VAPID_EMAIL],
CONF_NAME: DOMAIN,
}
assert mock_setup_entry.call_count == 1
assert len(issue_registry.issues) == 1
issue = issue_registry.async_get_issue(
HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}"
)
assert issue
assert issue.translation_key == SUCCESSFUL_IMPORT_TRANSLATION_KEY
@pytest.mark.parametrize(
("key", "value"),
[
(ATTR_VAPID_PRV_KEY, "invalid"),
],
)
async def test_step_import_bad(
hass: HomeAssistant, issue_registry: ir.IssueRegistry, key: str, value: str
) -> None:
"""Test invalid import input."""
with (
patch(
"homeassistant.components.html5.async_setup_entry",
return_value=True,
) as mock_setup_entry,
):
bad_conf = MOCK_CONF.copy()
bad_conf[key] = value
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=bad_conf
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert mock_setup_entry.call_count == 0
assert len(issue_registry.issues) == 1
issue = issue_registry.async_get_issue(DOMAIN, f"deprecated_yaml_{DOMAIN}")
assert issue
assert issue.translation_key == FAILED_IMPORT_TRANSLATION_KEY

View File

@ -0,0 +1,44 @@
"""Test the HTML5 setup."""
from homeassistant.core import HomeAssistant
import homeassistant.helpers.issue_registry as ir
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
NOTIFY_CONF = {
"notify": [
{
"platform": "html5",
"name": "html5",
"vapid_pub_key": "BIUtPN7Rq_8U7RBEqClZrfZ5dR9zPCfvxYPtLpWtRVZTJEc7lzv2dhzDU6Aw1m29Ao0-UA1Uq6XO9Df8KALBKqA",
"vapid_prv_key": "h6acSRds8_KR8hT9djD8WucTL06Gfe29XXyZ1KcUjN8",
"vapid_email": "test@example.com",
}
]
}
async def test_setup_entry(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test setup of a good config entry."""
config_entry = MockConfigEntry(domain="html5", data={})
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, "html5", {})
assert len(issue_registry.issues) == 0
async def test_setup_entry_issue(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test setup of an imported config entry with deprecated YAML."""
config_entry = MockConfigEntry(domain="html5", data={})
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, "notify", NOTIFY_CONF)
assert await async_setup_component(hass, "html5", NOTIFY_CONF)
assert len(issue_registry.issues) == 1

View File

@ -94,7 +94,7 @@ async def test_get_service_with_no_json(hass: HomeAssistant) -> None:
await async_setup_component(hass, "http", {})
m = mock_open()
with patch("homeassistant.util.json.open", m, create=True):
service = await html5.async_get_service(hass, VAPID_CONF)
service = await html5.async_get_service(hass, {}, VAPID_CONF)
assert service is not None
@ -109,7 +109,7 @@ async def test_dismissing_message(mock_wp, hass: HomeAssistant) -> None:
m = mock_open(read_data=json.dumps(data))
with patch("homeassistant.util.json.open", m, create=True):
service = await html5.async_get_service(hass, VAPID_CONF)
service = await html5.async_get_service(hass, {}, VAPID_CONF)
service.hass = hass
assert service is not None
@ -138,7 +138,7 @@ async def test_sending_message(mock_wp, hass: HomeAssistant) -> None:
m = mock_open(read_data=json.dumps(data))
with patch("homeassistant.util.json.open", m, create=True):
service = await html5.async_get_service(hass, VAPID_CONF)
service = await html5.async_get_service(hass, {}, VAPID_CONF)
service.hass = hass
assert service is not None
@ -169,7 +169,7 @@ async def test_fcm_key_include(mock_wp, hass: HomeAssistant) -> None:
m = mock_open(read_data=json.dumps(data))
with patch("homeassistant.util.json.open", m, create=True):
service = await html5.async_get_service(hass, VAPID_CONF)
service = await html5.async_get_service(hass, {}, VAPID_CONF)
service.hass = hass
assert service is not None
@ -194,7 +194,7 @@ async def test_fcm_send_with_unknown_priority(mock_wp, hass: HomeAssistant) -> N
m = mock_open(read_data=json.dumps(data))
with patch("homeassistant.util.json.open", m, create=True):
service = await html5.async_get_service(hass, VAPID_CONF)
service = await html5.async_get_service(hass, {}, VAPID_CONF)
service.hass = hass
assert service is not None
@ -219,7 +219,7 @@ async def test_fcm_no_targets(mock_wp, hass: HomeAssistant) -> None:
m = mock_open(read_data=json.dumps(data))
with patch("homeassistant.util.json.open", m, create=True):
service = await html5.async_get_service(hass, VAPID_CONF)
service = await html5.async_get_service(hass, {}, VAPID_CONF)
service.hass = hass
assert service is not None
@ -244,7 +244,7 @@ async def test_fcm_additional_data(mock_wp, hass: HomeAssistant) -> None:
m = mock_open(read_data=json.dumps(data))
with patch("homeassistant.util.json.open", m, create=True):
service = await html5.async_get_service(hass, VAPID_CONF)
service = await html5.async_get_service(hass, {}, VAPID_CONF)
service.hass = hass
assert service is not None
@ -479,7 +479,7 @@ async def test_callback_view_with_jwt(
mock_wp().send().status_code = 201
await hass.services.async_call(
"notify",
"notify",
"html5",
{"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}},
blocking=True,
)
@ -516,7 +516,7 @@ async def test_send_fcm_without_targets(
mock_wp().send().status_code = 201
await hass.services.async_call(
"notify",
"notify",
"html5",
{"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}},
blocking=True,
)
@ -541,7 +541,7 @@ async def test_send_fcm_expired(
mock_wp().send().status_code = 410
await hass.services.async_call(
"notify",
"notify",
"html5",
{"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}},
blocking=True,
)
@ -566,7 +566,7 @@ async def test_send_fcm_expired_save_fails(
mock_wp().send().status_code = 410
await hass.services.async_call(
"notify",
"notify",
"html5",
{"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}},
blocking=True,
)