Re-introduce webhook to tedee integration (#110247)

* bring webhook over to new branch

* change log levels

* Update homeassistant/components/tedee/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* fix minor version

* ruff

* mock config entry version

* fix

* ruff

* add cleanup during webhook registration

* feedback

* ruff

* Update __init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/tedee/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* add downgrade test

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Josef Zweck 2024-05-14 19:38:58 +02:00 committed by GitHub
parent b684801cae
commit d0e99b62da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 390 additions and 32 deletions

View file

@ -1,13 +1,28 @@
"""Init the tedee component.""" """Init the tedee component."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus
import logging import logging
from typing import Any
from aiohttp.hdrs import METH_POST
from aiohttp.web import Request, Response
from pytedee_async.exception import TedeeDataUpdateException, TedeeWebhookException
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.webhook import (
async_generate_id as webhook_generate_id,
async_generate_path as webhook_generate_path,
async_register as webhook_register,
async_unregister as webhook_unregister,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.network import get_url
from .const import DOMAIN from .const import DOMAIN, NAME
from .coordinator import TedeeApiCoordinator from .coordinator import TedeeApiCoordinator
PLATFORMS = [ PLATFORMS = [
@ -38,6 +53,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
async def unregister_webhook(_: Any) -> None:
await coordinator.async_unregister_webhook()
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
async def register_webhook() -> None:
instance_url = get_url(hass, allow_ip=True, allow_external=False)
# first make sure we don't have leftover callbacks to the same instance
try:
await coordinator.tedee_client.cleanup_webhooks_by_host(instance_url)
except (TedeeDataUpdateException, TedeeWebhookException) as ex:
_LOGGER.warning("Failed to cleanup Tedee webhooks by host: %s", ex)
webhook_url = (
f"{instance_url}{webhook_generate_path(entry.data[CONF_WEBHOOK_ID])}"
)
webhook_name = "Tedee"
if entry.title != NAME:
webhook_name = f"{NAME} {entry.title}"
webhook_register(
hass,
DOMAIN,
webhook_name,
entry.data[CONF_WEBHOOK_ID],
get_webhook_handler(coordinator),
allowed_methods=[METH_POST],
)
_LOGGER.debug("Registered Tedee webhook at hass: %s", webhook_url)
try:
await coordinator.async_register_webhook(webhook_url)
except TedeeWebhookException:
_LOGGER.exception("Failed to register Tedee webhook from bridge")
else:
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
)
entry.async_create_background_task(
hass, register_webhook(), "tedee_register_webhook"
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
@ -46,9 +101,53 @@ 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."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok
def get_webhook_handler(
coordinator: TedeeApiCoordinator,
) -> Callable[[HomeAssistant, str, Request], Awaitable[Response | None]]:
"""Return webhook handler."""
async def async_webhook_handler(
hass: HomeAssistant, webhook_id: str, request: Request
) -> Response | None:
# Handle http post calls to the path.
if not request.body_exists:
return HomeAssistantView.json(
result="No Body", status_code=HTTPStatus.BAD_REQUEST
)
body = await request.json()
try:
coordinator.webhook_received(body)
except TedeeWebhookException as ex:
return HomeAssistantView.json(
result=str(ex), status_code=HTTPStatus.BAD_REQUEST
)
return HomeAssistantView.json(result="OK", status_code=HTTPStatus.OK)
return async_webhook_handler
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
version = config_entry.version
minor_version = config_entry.minor_version
if version == 1 and minor_version == 1:
_LOGGER.debug(
"Migrating Tedee config entry from version %s.%s", version, minor_version
)
data = {**config_entry.data, CONF_WEBHOOK_ID: webhook_generate_id()}
hass.config_entries.async_update_entry(config_entry, data=data, minor_version=2)
_LOGGER.debug("Migration to version 1.2 successful")
return True

View file

@ -13,8 +13,9 @@ from pytedee_async import (
) )
import voluptuous as vol import voluptuous as vol
from homeassistant.components.webhook import async_generate_id as webhook_generate_id
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN, NAME from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN, NAME
@ -25,6 +26,9 @@ _LOGGER = logging.getLogger(__name__)
class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): class TedeeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Tedee.""" """Handle a config flow for Tedee."""
VERSION = 1
MINOR_VERSION = 2
reauth_entry: ConfigEntry | None = None reauth_entry: ConfigEntry | None = None
async def async_step_user( async def async_step_user(
@ -65,7 +69,10 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
await self.async_set_unique_id(local_bridge.serial) await self.async_set_unique_id(local_bridge.serial)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry(title=NAME, data=user_input) return self.async_create_entry(
title=NAME,
data={**user_input, CONF_WEBHOOK_ID: webhook_generate_id()},
)
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",

View file

@ -4,6 +4,7 @@ from collections.abc import Awaitable, Callable
from datetime import timedelta from datetime import timedelta
import logging import logging
import time import time
from typing import Any
from pytedee_async import ( from pytedee_async import (
TedeeClient, TedeeClient,
@ -11,6 +12,7 @@ from pytedee_async import (
TedeeDataUpdateException, TedeeDataUpdateException,
TedeeLocalAuthException, TedeeLocalAuthException,
TedeeLock, TedeeLock,
TedeeWebhookException,
) )
from pytedee_async.bridge import TedeeBridge from pytedee_async.bridge import TedeeBridge
@ -24,7 +26,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN
SCAN_INTERVAL = timedelta(seconds=20) SCAN_INTERVAL = timedelta(seconds=30)
GET_LOCKS_INTERVAL_SECONDS = 3600 GET_LOCKS_INTERVAL_SECONDS = 3600
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -54,6 +56,7 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]):
self._next_get_locks = time.time() self._next_get_locks = time.time()
self._locks_last_update: set[int] = set() self._locks_last_update: set[int] = set()
self.new_lock_callbacks: list[Callable[[int], None]] = [] self.new_lock_callbacks: list[Callable[[int], None]] = []
self.tedee_webhook_id: int | None = None
@property @property
def bridge(self) -> TedeeBridge: def bridge(self) -> TedeeBridge:
@ -104,6 +107,25 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]):
except (TedeeClientException, TimeoutError) as ex: except (TedeeClientException, TimeoutError) as ex:
raise UpdateFailed(f"Querying API failed. Error: {ex!s}") from ex raise UpdateFailed(f"Querying API failed. Error: {ex!s}") from ex
def webhook_received(self, message: dict[str, Any]) -> None:
"""Handle webhook message."""
self.tedee_client.parse_webhook_message(message)
self.async_set_updated_data(self.tedee_client.locks_dict)
async def async_register_webhook(self, webhook_url: str) -> None:
"""Register the webhook at the Tedee bridge."""
self.tedee_webhook_id = await self.tedee_client.register_webhook(webhook_url)
async def async_unregister_webhook(self) -> None:
"""Unregister the webhook at the Tedee bridge."""
if self.tedee_webhook_id is not None:
try:
await self.tedee_client.delete_webhook(self.tedee_webhook_id)
except TedeeWebhookException:
_LOGGER.exception("Failed to unregister Tedee webhook from bridge")
else:
_LOGGER.debug("Unregistered Tedee webhook")
def _async_add_remove_locks(self) -> None: def _async_add_remove_locks(self) -> None:
"""Add new locks, remove non-existing locks.""" """Add new locks, remove non-existing locks."""
if not self._locks_last_update: if not self._locks_last_update:

View file

@ -3,7 +3,7 @@
"name": "Tedee", "name": "Tedee",
"codeowners": ["@patrickhilker", "@zweckj"], "codeowners": ["@patrickhilker", "@zweckj"],
"config_flow": true, "config_flow": true,
"dependencies": ["http"], "dependencies": ["http", "webhook"],
"documentation": "https://www.home-assistant.io/integrations/tedee", "documentation": "https://www.home-assistant.io/integrations/tedee",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pytedee_async"], "loggers": ["pytedee_async"],

View file

@ -11,11 +11,13 @@ from pytedee_async.lock import TedeeLock
import pytest import pytest
from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, load_fixture
WEBHOOK_ID = "bq33efxmdi3vxy55q2wbnudbra7iv8mjrq9x0gea33g4zqtd87093pwveg8xcb33"
@pytest.fixture @pytest.fixture
def mock_config_entry() -> MockConfigEntry: def mock_config_entry() -> MockConfigEntry:
@ -26,8 +28,11 @@ def mock_config_entry() -> MockConfigEntry:
data={ data={
CONF_LOCAL_ACCESS_TOKEN: "api_token", CONF_LOCAL_ACCESS_TOKEN: "api_token",
CONF_HOST: "192.168.1.42", CONF_HOST: "192.168.1.42",
CONF_WEBHOOK_ID: WEBHOOK_ID,
}, },
unique_id="0000-0000", unique_id="0000-0000",
version=1,
minor_version=2,
) )
@ -63,6 +68,8 @@ def mock_tedee(request) -> Generator[MagicMock, None, None]:
tedee.get_local_bridge.return_value = TedeeBridge(0, "0000-0000", "Bridge-AB1C") tedee.get_local_bridge.return_value = TedeeBridge(0, "0000-0000", "Bridge-AB1C")
tedee.parse_webhook_message.return_value = None tedee.parse_webhook_message.return_value = None
tedee.register_webhook.return_value = 1
tedee.delete_webhooks.return_value = None
locks_json = json.loads(load_fixture("locks.json", DOMAIN)) locks_json = json.loads(load_fixture("locks.json", DOMAIN))
@ -78,7 +85,6 @@ async def init_integration(
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up the Tedee integration for testing.""" """Set up the Tedee integration for testing."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -1,6 +1,6 @@
"""Test the Tedee config flow.""" """Test the Tedee config flow."""
from unittest.mock import MagicMock from unittest.mock import MagicMock, patch
from pytedee_async import ( from pytedee_async import (
TedeeClientException, TedeeClientException,
@ -11,10 +11,12 @@ import pytest
from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID
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 .conftest import WEBHOOK_ID
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
FLOW_UNIQUE_ID = "112233445566778899" FLOW_UNIQUE_ID = "112233445566778899"
@ -23,25 +25,30 @@ LOCAL_ACCESS_TOKEN = "api_token"
async def test_flow(hass: HomeAssistant, mock_tedee: MagicMock) -> None: async def test_flow(hass: HomeAssistant, mock_tedee: MagicMock) -> None:
"""Test config flow with one bridge.""" """Test config flow with one bridge."""
result = await hass.config_entries.flow.async_init( with patch(
DOMAIN, context={"source": SOURCE_USER} "homeassistant.components.tedee.config_flow.webhook_generate_id",
) return_value=WEBHOOK_ID,
await hass.async_block_till_done() ):
assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
CONF_HOST: "192.168.1.62",
CONF_LOCAL_ACCESS_TOKEN: "token",
},
)
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["data"] == {
CONF_HOST: "192.168.1.62", CONF_HOST: "192.168.1.62",
CONF_LOCAL_ACCESS_TOKEN: "token", CONF_LOCAL_ACCESS_TOKEN: "token",
}, CONF_WEBHOOK_ID: WEBHOOK_ID,
) }
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["data"] == {
CONF_HOST: "192.168.1.62",
CONF_LOCAL_ACCESS_TOKEN: "token",
}
async def test_flow_already_configured( async def test_flow_already_configured(

View file

@ -1,16 +1,29 @@
"""Test initialization of tedee.""" """Test initialization of tedee."""
from unittest.mock import MagicMock from http import HTTPStatus
from typing import Any
from unittest.mock import MagicMock, patch
from urllib.parse import urlparse
from pytedee_async.exception import TedeeAuthException, TedeeClientException from pytedee_async.exception import (
TedeeAuthException,
TedeeClientException,
TedeeWebhookException,
)
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN
from homeassistant.components.webhook import async_generate_url
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .conftest import WEBHOOK_ID
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator
async def test_load_unload_config_entry( async def test_load_unload_config_entry(
@ -51,6 +64,80 @@ async def test_config_entry_not_ready(
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_cleanup_on_shutdown(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_tedee: MagicMock,
) -> None:
"""Test the webhook is cleaned up on shutdown."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
mock_tedee.delete_webhook.assert_called_once()
async def test_webhook_cleanup_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_tedee: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the webhook is cleaned up on shutdown."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_tedee.delete_webhook.side_effect = TedeeWebhookException("")
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
mock_tedee.delete_webhook.assert_called_once()
assert "Failed to unregister Tedee webhook from bridge" in caplog.text
async def test_webhook_registration_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_tedee: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the webhook is cleaned up on shutdown."""
mock_tedee.register_webhook.side_effect = TedeeWebhookException("")
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_tedee.register_webhook.assert_called_once()
assert "Failed to register Tedee webhook from bridge" in caplog.text
async def test_webhook_registration_cleanup_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_tedee: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the errors during webhook cleanup during registration."""
mock_tedee.cleanup_webhooks_by_host.side_effect = TedeeWebhookException("")
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_tedee.cleanup_webhooks_by_host.assert_called_once()
assert "Failed to cleanup Tedee webhooks by host:" in caplog.text
async def test_bridge_device( async def test_bridge_device(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
@ -68,3 +155,97 @@ async def test_bridge_device(
) )
assert device assert device
assert device == snapshot assert device == snapshot
@pytest.mark.parametrize(
(
"body",
"expected_code",
"side_effect",
),
[
(
{"hello": "world"},
HTTPStatus.OK,
None,
), # Success
(
None,
HTTPStatus.BAD_REQUEST,
None,
), # Missing data
(
{},
HTTPStatus.BAD_REQUEST,
TedeeWebhookException,
), # Error
],
)
async def test_webhook_post(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_tedee: MagicMock,
hass_client_no_auth: ClientSessionGenerator,
body: dict[str, Any],
expected_code: HTTPStatus,
side_effect: Exception,
) -> None:
"""Test webhook callback."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
client = await hass_client_no_auth()
webhook_url = async_generate_url(hass, WEBHOOK_ID)
mock_tedee.parse_webhook_message.side_effect = side_effect
resp = await client.post(urlparse(webhook_url).path, json=body)
# Wait for remaining tasks to complete.
await hass.async_block_till_done()
assert resp.status == expected_code
async def test_config_flow_entry_migrate_2_1(hass: HomeAssistant) -> None:
"""Test that config entry fails setup if the version is from the future."""
entry = MockConfigEntry(
domain=DOMAIN,
version=2,
minor_version=1,
)
entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry.entry_id)
async def test_migration(
hass: HomeAssistant,
mock_tedee: MagicMock,
) -> None:
"""Test migration of the config entry."""
mock_config_entry = MockConfigEntry(
title="My Tedee",
domain=DOMAIN,
data={
CONF_LOCAL_ACCESS_TOKEN: "api_token",
CONF_HOST: "192.168.1.42",
},
version=1,
minor_version=1,
unique_id="0000-0000",
)
with patch(
"homeassistant.components.tedee.webhook_generate_id",
return_value=WEBHOOK_ID,
):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.version == 1
assert mock_config_entry.minor_version == 2
assert mock_config_entry.data[CONF_WEBHOOK_ID] == WEBHOOK_ID
assert mock_config_entry.state is ConfigEntryState.LOADED

View file

@ -2,9 +2,10 @@
from datetime import timedelta from datetime import timedelta
from unittest.mock import MagicMock from unittest.mock import MagicMock
from urllib.parse import urlparse
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from pytedee_async import TedeeLock from pytedee_async import TedeeLock, TedeeLockState
from pytedee_async.exception import ( from pytedee_async.exception import (
TedeeClientException, TedeeClientException,
TedeeDataUpdateException, TedeeDataUpdateException,
@ -18,15 +19,21 @@ from homeassistant.components.lock import (
SERVICE_LOCK, SERVICE_LOCK,
SERVICE_OPEN, SERVICE_OPEN,
SERVICE_UNLOCK, SERVICE_UNLOCK,
STATE_LOCKED,
STATE_LOCKING, STATE_LOCKING,
STATE_UNLOCKED,
STATE_UNLOCKING, STATE_UNLOCKING,
) )
from homeassistant.components.webhook import async_generate_url
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import WEBHOOK_ID
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import ClientSessionGenerator
pytestmark = pytest.mark.usefixtures("init_integration") pytestmark = pytest.mark.usefixtures("init_integration")
@ -267,3 +274,32 @@ async def test_new_lock(
assert state assert state
state = hass.states.get("lock.lock_6g7h") state = hass.states.get("lock.lock_6g7h")
assert state assert state
async def test_webhook_update(
hass: HomeAssistant,
mock_tedee: MagicMock,
hass_client_no_auth: ClientSessionGenerator,
) -> None:
"""Test updated data set through webhook."""
state = hass.states.get("lock.lock_1a2b")
assert state
assert state.state == STATE_UNLOCKED
webhook_data = {"dummystate": 6}
mock_tedee.locks_dict[
12345
].state = TedeeLockState.LOCKED # is updated in the lib, so mock and assert in L296
client = await hass_client_no_auth()
webhook_url = async_generate_url(hass, WEBHOOK_ID)
await client.post(
urlparse(webhook_url).path,
json=webhook_data,
)
mock_tedee.parse_webhook_message.assert_called_once_with(webhook_data)
state = hass.states.get("lock.lock_1a2b")
assert state
assert state.state == STATE_LOCKED