Skip to content

Commit

Permalink
Enable config flow for html5 (#112806)
Browse files Browse the repository at this point in the history
* 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. <[email protected]>

* Update homeassistant/components/html5/issues.py

Co-authored-by: Steven B. <[email protected]>

* 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 <[email protected]>

* Update tests/components/html5/test_config_flow.py

Co-authored-by: J. Nick Koston <[email protected]>

* Update tests/components/html5/test_config_flow.py

Co-authored-by: J. Nick Koston <[email protected]>

* 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 <[email protected]>
Co-authored-by: Steven B. <[email protected]>
Co-authored-by: J. Nick Koston <[email protected]>
Co-authored-by: Joostlek <[email protected]>
  • Loading branch information
5 people authored Aug 30, 2024
1 parent ac39bf9 commit 2628166
Show file tree
Hide file tree
Showing 13 changed files with 497 additions and 37 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions homeassistant/components/html5/__init__.py
Original file line number Diff line number Diff line change
@@ -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
103 changes: 103 additions & 0 deletions homeassistant/components/html5/config_flow.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions homeassistant/components/html5/const.py
Original file line number Diff line number Diff line change
@@ -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"
50 changes: 50 additions & 0 deletions homeassistant/components/html5/issues.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
6 changes: 4 additions & 2 deletions homeassistant/components/html5/manifest.json
Original file line number Diff line number Diff line change
@@ -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
}
51 changes: 29 additions & 22 deletions homeassistant/components/html5/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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
Expand All @@ -38,32 +39,23 @@
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,
Expand Down Expand Up @@ -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(
Expand Down
27 changes: 27 additions & 0 deletions homeassistant/components/html5/strings.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
"homewizard",
"homeworks",
"honeywell",
"html5",
"huawei_lte",
"hue",
"huisbaasje",
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 2628166

Please sign in to comment.