Migrate Ecowitt to webhooks (#77610)

This commit is contained in:
Paulus Schoutsen 2022-08-31 12:41:04 -04:00 committed by GitHub
parent 4b24370549
commit 708e614823
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 58 additions and 170 deletions

View File

@ -2,32 +2,38 @@
from __future__ import annotations from __future__ import annotations
from aioecowitt import EcoWittListener from aioecowitt import EcoWittListener
from aiohttp import web
from homeassistant.components import webhook
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant from homeassistant.core import Event, HomeAssistant, callback
from .const import CONF_PATH, DOMAIN from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Ecowitt component from UI.""" """Set up the Ecowitt component from UI."""
hass.data.setdefault(DOMAIN, {}) ecowitt = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EcoWittListener()
ecowitt = hass.data[DOMAIN][entry.entry_id] = EcoWittListener(
port=entry.data[CONF_PORT], path=entry.data[CONF_PATH]
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await ecowitt.start() async def handle_webhook(
hass: HomeAssistant, webhook_id: str, request: web.Request
) -> web.Response:
"""Handle webhook callback."""
return await ecowitt.handler(request)
# Close on shutdown webhook.async_register(
async def _stop_ecowitt(_: Event): hass, DOMAIN, entry.title, entry.data[CONF_WEBHOOK_ID], handle_webhook
)
@callback
def _stop_ecowitt(_: Event):
"""Stop the Ecowitt listener.""" """Stop the Ecowitt listener."""
await ecowitt.stop() webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
entry.async_on_unload( entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_ecowitt) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_ecowitt)
@ -38,9 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
ecowitt = hass.data[DOMAIN][entry.entry_id]
await ecowitt.stop()
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)

View File

@ -1,77 +1,50 @@
"""Config flow for ecowitt.""" """Config flow for ecowitt."""
from __future__ import annotations from __future__ import annotations
import logging
import secrets import secrets
from typing import Any from typing import Any
from aioecowitt import EcoWittListener from yarl import URL
import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_PORT from homeassistant.components import webhook
from homeassistant.core import HomeAssistant from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import get_url
from .const import CONF_PATH, DEFAULT_PORT, DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PATH, default=f"/{secrets.token_urlsafe(16)}"): cv.string,
}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
"""Validate user input."""
# Check if the port is in use
try:
listener = EcoWittListener(port=data[CONF_PORT])
await listener.start()
await listener.stop()
except OSError:
raise InvalidPort from None
return {"title": f"Ecowitt on port {data[CONF_PORT]}"}
class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for the Ecowitt.""" """Config flow for the Ecowitt."""
VERSION = 1 VERSION = 1
_webhook_id: str
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
if user_input is None: if user_input is None:
self._webhook_id = secrets.token_hex(16)
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA step_id="user",
) )
errors = {} base_url = URL(get_url(self.hass))
assert base_url.host
# Check if the port is in use by another config entry return self.async_create_entry(
self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]}) title="Ecowitt",
data={
try: CONF_WEBHOOK_ID: self._webhook_id,
info = await validate_input(self.hass, user_input) },
except InvalidPort: description_placeholders={
errors["base"] = "invalid_port" "path": webhook.async_generate_path(self._webhook_id),
except Exception: # pylint: disable=broad-except "server": base_url.host,
_LOGGER.exception("Unexpected exception") "port": str(base_url.port),
errors["base"] = "unknown" },
else:
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
) )

View File

@ -1,7 +1,3 @@
"""Constants used by ecowitt component.""" """Constants used by ecowitt component."""
DOMAIN = "ecowitt" DOMAIN = "ecowitt"
DEFAULT_PORT = 49199
CONF_PATH = "path"

View File

@ -1,6 +1,6 @@
{ {
"domain": "ecowitt", "domain": "ecowitt",
"name": "Ecowitt Weather Station", "name": "Ecowitt",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecowitt", "documentation": "https://www.home-assistant.io/integrations/ecowitt",
"requirements": ["aioecowitt==2022.08.3"], "requirements": ["aioecowitt==2022.08.3"],

View File

@ -1,17 +1,12 @@
{ {
"config": { "config": {
"error": {
"invalid_port": "Port is already used.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": { "step": {
"user": { "user": {
"description": "The following steps must be performed to set up this integration.\n\nUse the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\nPick your station -> Menu Others -> DIY Upload Servers.\nHit next and select 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath have to match, you can copy with secure token /.\nSave configuration. The Ecowitt should then start attempting to send data to your server.", "description": "Are you sure you want to set up Ecowitt?"
"data": { }
"port": "Listening port", },
"path": "Path with Security token" "create_entry": {
} "default": "To finish setting up the integration, use the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\n\nPick your station -> Menu Others -> DIY Upload Servers. Hit next and select 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nClick on 'Save'."
}
} }
} }
} }

View File

@ -1,16 +1,11 @@
{ {
"config": { "config": {
"error": { "create_entry": {
"invalid_port": "Port is already used.", "default": "To finish setting up the integration, use the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\n\nPick your station -> Menu Others -> DIY Upload Servers. Hit next and select 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nClick on 'Save'."
"unknown": "Unknown error."
}, },
"step": { "step": {
"user": { "user": {
"description": "The following steps must be performed to set up this integration.\n\nUse the Ecowitt App (on your phone) or your Ecowitt WebUI over the station IP address.\nPick your station -> Menu Others -> DIY Upload Servers.\nHit next and select 'Customized'\n\nPick the protocol Ecowitt, and put in the ip/hostname of your hass server.\nPath have to match, you can copy with secure token /.\nSave configuration. The Ecowitt should then start attempting to send data to your server.", "description": "Are you sure you want to set up Ecowitt?"
"data": {
"port": "Listening port",
"path": "Path with Security token"
}
} }
} }
} }

View File

@ -5,12 +5,13 @@ from homeassistant import config_entries
from homeassistant.components.ecowitt.const import DOMAIN from homeassistant.components.ecowitt.const import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def test_create_entry(hass: HomeAssistant) -> None: async def test_create_entry(hass: HomeAssistant) -> None:
"""Test we can create a config entry.""" """Test we can create a config entry."""
await async_setup_component(hass, "http", {})
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
@ -18,93 +19,18 @@ async def test_create_entry(hass: HomeAssistant) -> None:
assert result["errors"] is None assert result["errors"] is None
with patch( with patch(
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start"
), patch(
"homeassistant.components.ecowitt.config_flow.EcoWittListener.stop"
), patch(
"homeassistant.components.ecowitt.async_setup_entry", "homeassistant.components.ecowitt.async_setup_entry",
return_value=True, return_value=True,
) as mock_setup_entry: ) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {},
"port": 49911,
"path": "/ecowitt-station",
},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Ecowitt on port 49911" assert result2["title"] == "Ecowitt"
assert result2["data"] == { assert result2["data"] == {
"port": 49911, "webhook_id": result2["description_placeholders"]["path"].split("/")[-1],
"path": "/ecowitt-station",
} }
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_port(hass: HomeAssistant) -> None:
"""Test we handle invalid port."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start",
side_effect=OSError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"port": 49911,
"path": "/ecowitt-station",
},
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "invalid_port"}
async def test_already_configured_port(hass: HomeAssistant) -> None:
"""Test already configured port."""
MockConfigEntry(domain=DOMAIN, data={"port": 49911}).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start",
side_effect=OSError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"port": 49911,
"path": "/ecowitt-station",
},
)
assert result2["type"] == FlowResultType.ABORT
async def test_unknown_error(hass: HomeAssistant) -> None:
"""Test we handle unknown error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start",
side_effect=Exception(),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"port": 49911,
"path": "/ecowitt-station",
},
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "unknown"}