Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Ecowitt to webhooks #77610

Merged
merged 3 commits into from Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 18 additions & 15 deletions homeassistant/components/ecowitt/__init__.py
Expand Up @@ -2,32 +2,38 @@
from __future__ import annotations

from aioecowitt import EcoWittListener
from aiohttp import web

from homeassistant.components import webhook
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP, Platform
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]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Ecowitt component from UI."""
hass.data.setdefault(DOMAIN, {})

ecowitt = hass.data[DOMAIN][entry.entry_id] = EcoWittListener(
port=entry.data[CONF_PORT], path=entry.data[CONF_PATH]
)
ecowitt = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EcoWittListener()

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
async def _stop_ecowitt(_: Event):
webhook.async_register(
hass, DOMAIN, entry.title, entry.data[CONF_WEBHOOK_ID], handle_webhook
)

@callback
def _stop_ecowitt(_: Event):
"""Stop the Ecowitt listener."""
await ecowitt.stop()
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])

entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_ecowitt)
Expand All @@ -38,9 +44,6 @@ async def _stop_ecowitt(_: Event):

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""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):
hass.data[DOMAIN].pop(entry.entry_id)

Expand Down
69 changes: 21 additions & 48 deletions homeassistant/components/ecowitt/config_flow.py
@@ -1,77 +1,50 @@
"""Config flow for ecowitt."""
from __future__ import annotations

import logging
import secrets
from typing import Any

from aioecowitt import EcoWittListener
import voluptuous as vol
from yarl import URL

from homeassistant import config_entries
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.components import webhook
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.data_entry_flow import FlowResult
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

_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]}"}
from .const import DOMAIN


class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for the Ecowitt."""

VERSION = 1
_webhook_id: str

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
self._webhook_id = secrets.token_hex(16)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
step_id="user",
)

errors = {}

# Check if the port is in use by another config entry
self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]})

try:
info = await validate_input(self.hass, user_input)
except InvalidPort:
errors["base"] = "invalid_port"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
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
base_url = URL(get_url(self.hass))
assert base_url.host

return self.async_create_entry(
title="Ecowitt",
data={
CONF_WEBHOOK_ID: self._webhook_id,
},
description_placeholders={
"path": webhook.async_generate_path(self._webhook_id),
"server": base_url.host,
"port": str(base_url.port),
},
)


Expand Down
4 changes: 0 additions & 4 deletions homeassistant/components/ecowitt/const.py
@@ -1,7 +1,3 @@
"""Constants used by ecowitt component."""

DOMAIN = "ecowitt"

DEFAULT_PORT = 49199

CONF_PATH = "path"
2 changes: 1 addition & 1 deletion homeassistant/components/ecowitt/manifest.json
@@ -1,6 +1,6 @@
{
"domain": "ecowitt",
"name": "Ecowitt Weather Station",
"name": "Ecowitt",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
"requirements": ["aioecowitt==2022.08.3"],
Expand Down
13 changes: 4 additions & 9 deletions homeassistant/components/ecowitt/strings.json
@@ -1,17 +1,12 @@
{
"config": {
"error": {
"invalid_port": "Port is already used.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"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.",
"data": {
"port": "Listening port",
"path": "Path with Security token"
}
"description": "Are you sure you want to set up Ecowitt?"
}
},
"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'."
}
}
}
23 changes: 9 additions & 14 deletions homeassistant/components/ecowitt/translations/en.json
@@ -1,17 +1,12 @@
{
"config": {
"error": {
"invalid_port": "Port is already used.",
"unknown": "Unknown error."
},
"step": {
"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.",
"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'."
},
"step": {
"user": {
"description": "Are you sure you want to set up Ecowitt?"
}
}
}
}
}
86 changes: 6 additions & 80 deletions tests/components/ecowitt/test_config_flow.py
Expand Up @@ -5,106 +5,32 @@
from homeassistant.components.ecowitt.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType

from tests.common import MockConfigEntry
from homeassistant.setup import async_setup_component


async def test_create_entry(hass: HomeAssistant) -> None:
"""Test we can create a config entry."""
await async_setup_component(hass, "http", {})

result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["errors"] is None

with patch(
"homeassistant.components.ecowitt.config_flow.EcoWittListener.start"
), patch(
"homeassistant.components.ecowitt.config_flow.EcoWittListener.stop"
), patch(
"homeassistant.components.ecowitt.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"port": 49911,
"path": "/ecowitt-station",
},
{},
)
await hass.async_block_till_done()

assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == "Ecowitt on port 49911"
assert result2["title"] == "Ecowitt"
assert result2["data"] == {
"port": 49911,
"path": "/ecowitt-station",
"webhook_id": result2["description_placeholders"]["path"].split("/")[-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"}