Skip to content

Commit

Permalink
Merge pull request #11 from bramstroker/feat/retry
Browse files Browse the repository at this point in the history
feat: implement retry
  • Loading branch information
bramstroker authored Dec 22, 2024
2 parents 863ff2a + ca30d08 commit efeebb2
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 10 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: Release Drafter

permissions:
contents: read

on:
push:
branches:
Expand All @@ -9,6 +12,9 @@ on:

jobs:
update_release_draft:
permissions:
contents: write
pull-requests: write
name: Update release draft
runs-on: ubuntu-latest
steps:
Expand All @@ -19,6 +25,6 @@ jobs:
- name: Create Release
uses: release-drafter/release-drafter@v6
with:
disable-releaser: github.ref != 'refs/heads/main'
commitish: refs/heads/main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 changes: 17 additions & 6 deletions custom_components/state_webhook/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import fnmatch
import logging
from collections.abc import Mapping
Expand All @@ -19,14 +20,18 @@
CONF_FILTER_MODE,
CONF_PAYLOAD_ATTRIBUTES,
CONF_PAYLOAD_OLD_STATE,
CONF_RETRY_LIMIT,
CONF_WEBHOOK_AUTH_HEADER,
CONF_WEBHOOK_HEADERS,
CONF_WEBHOOK_URL,
DEFAULT_RETRY_LIMIT,
FilterMode,
)

_LOGGER = logging.getLogger(__name__)

RETRY_DELAY = 5


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _register_webhook(_: Any) -> None: # noqa ANN401
Expand All @@ -47,6 +52,7 @@ async def register_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None:

webhook_url = str(entry.options.get(CONF_WEBHOOK_URL))
headers = prepare_headers(entry.options)
retry_limit = int(entry.options.get(CONF_RETRY_LIMIT, DEFAULT_RETRY_LIMIT))

_LOGGER.debug("Start webhook tracking using URL: %s", webhook_url)
_LOGGER.debug("Tracking the following entities: %s", entities_to_track)
Expand Down Expand Up @@ -76,12 +82,17 @@ async def handle_state_change(event: Event[EventStateChangedData]) -> None:
new_state.state,
)

await call_webhook(
session,
webhook_url,
headers,
build_payload(entry.options, entity_id, old_state, new_state),
)
result = False
retry_count = 0
while not result and retry_count < retry_limit:
result = await call_webhook(
session,
webhook_url,
headers,
build_payload(entry.options, entity_id, old_state, new_state),
)
retry_count += 1
await asyncio.sleep(RETRY_DELAY)

async_track_state_change_event(hass, entities_to_track, handle_state_change)

Expand Down
6 changes: 6 additions & 0 deletions custom_components/state_webhook/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
EntitySelectorConfig,
LabelSelector,
LabelSelectorConfig,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
ObjectSelector,
SelectSelector,
SelectSelectorConfig,
Expand All @@ -35,9 +38,11 @@
CONF_ENTITY_LABELS,
CONF_PAYLOAD_ATTRIBUTES,
CONF_PAYLOAD_OLD_STATE,
CONF_RETRY_LIMIT,
CONF_WEBHOOK_AUTH_HEADER,
CONF_WEBHOOK_HEADERS,
CONF_WEBHOOK_URL,
DEFAULT_RETRY_LIMIT,
DOMAIN,
FilterMode,
)
Expand All @@ -47,6 +52,7 @@
vol.Required(CONF_WEBHOOK_URL): TextSelector(),
vol.Optional(CONF_WEBHOOK_AUTH_HEADER): TextSelector(),
vol.Optional(CONF_WEBHOOK_HEADERS): ObjectSelector(),
vol.Optional(CONF_RETRY_LIMIT, default=DEFAULT_RETRY_LIMIT): NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX)),
},
)

Expand Down
3 changes: 3 additions & 0 deletions custom_components/state_webhook/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
CONF_FILTER_MODE = "filter_mode"
CONF_PAYLOAD_ATTRIBUTES = "payload_attributes"
CONF_PAYLOAD_OLD_STATE = "payload_old_state"
CONF_RETRY_LIMIT = "retry_limit"
CONF_WEBHOOK_URL = "webhook_url"
CONF_WEBHOOK_HEADERS = "webhook_headers"
CONF_WEBHOOK_AUTH_HEADER = "webhook_auth_header"

DEFAULT_RETRY_LIMIT = 3

class FilterMode(StrEnum):
OR = "or"
AND = "and"
4 changes: 4 additions & 0 deletions custom_components/state_webhook/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
"user": {
"data": {
"name": "Name",
"retry_limit": "Retry limit",
"webhook_url": "Webhook URL",
"webhook_auth_header": "Authorization header",
"webhook_headers": "Additional headers"
},
"data_description": {
"retry_limit": "Number of times to retry sending the webhook, with a delay of 5 seconds between each retry",
"webhook_url": "URL where state changes will be sent to using POST request",
"webhook_auth_header": "Authorization header to be sent with the request",
"webhook_headers": "Additional headers to be sent. Format: key1: value1 on each line"
Expand Down Expand Up @@ -48,11 +50,13 @@
"webhook": {
"data": {
"name": "Name",
"retry_limit": "Retry limit",
"webhook_url": "Webhook URL",
"webhook_auth_header": "Authorization header",
"webhook_headers": "Additional headers"
},
"data_description": {
"retry_limit": "Number of times to retry sending the webhook, with a delay of 5 seconds between each retry",
"webhook_url": "URL where state changes will be sent to using POST request",
"webhook_auth_header": "Authorization header to be sent with the request",
"webhook_headers": "Additional headers to be sent. Format: key1: value1 on each line"
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
from collections.abc import Generator
from unittest.mock import patch

import pytest
from homeassistant import loader
Expand Down Expand Up @@ -50,3 +51,9 @@ def device_reg(hass: HomeAssistant) -> DeviceRegistry:
def entity_reg(hass: HomeAssistant) -> EntityRegistry:
"""Return an empty, loaded, registry."""
return mock_registry(hass)


@pytest.fixture(autouse=True)
def patch_retry_delay() -> Generator:
with patch("custom_components.state_webhook.RETRY_DELAY", 0):
yield
12 changes: 10 additions & 2 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError
from pytest_homeassistant_custom_component.common import MockConfigEntry

from custom_components.state_webhook import CONF_ENTITY_DOMAIN, CONF_ENTITY_ID, CONF_FILTER_MODE, CONF_PAYLOAD_OLD_STATE, CONF_WEBHOOK_URL
from custom_components.state_webhook import (
CONF_ENTITY_DOMAIN,
CONF_ENTITY_ID,
CONF_FILTER_MODE,
CONF_PAYLOAD_OLD_STATE,
CONF_RETRY_LIMIT,
CONF_WEBHOOK_URL,
)
from custom_components.state_webhook.config_flow import validate_webhook
from custom_components.state_webhook.const import CONF_PAYLOAD_ATTRIBUTES, DOMAIN, FilterMode
from custom_components.state_webhook.const import CONF_PAYLOAD_ATTRIBUTES, DEFAULT_RETRY_LIMIT, DOMAIN, FilterMode


async def test_validate_url() -> None:
Expand Down Expand Up @@ -85,6 +92,7 @@ async def test_config_flow(hass: HomeAssistant) -> None:
assert result["options"] == {
CONF_PAYLOAD_ATTRIBUTES: True,
CONF_PAYLOAD_OLD_STATE: True,
CONF_RETRY_LIMIT: DEFAULT_RETRY_LIMIT,
CONF_NAME: "Test",
CONF_WEBHOOK_URL: "http://example.com",
CONF_FILTER_MODE: FilterMode.OR,
Expand Down
26 changes: 25 additions & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from homeassistant.helpers.entity_registry import RegistryEntry
from pytest_homeassistant_custom_component.common import MockConfigEntry, mock_registry

from custom_components.state_webhook import CONF_ENTITY_DOMAIN, CONF_WEBHOOK_URL, async_setup_entry, build_payload
from custom_components.state_webhook import CONF_ENTITY_DOMAIN, CONF_ENTITY_ID, CONF_WEBHOOK_URL, async_setup_entry, build_payload
from custom_components.state_webhook.const import CONF_PAYLOAD_ATTRIBUTES, CONF_PAYLOAD_OLD_STATE, DOMAIN

DEFAULT_WEBHOOK_URL = "https://example.com/webhook"
Expand Down Expand Up @@ -59,6 +59,30 @@ async def test_state_webhook_triggered_successfully(hass: HomeAssistant) -> None
headers={},
)

async def test_retry_when_webhook_unavailable(hass: HomeAssistant) -> None:
hass.states.async_set("input_boolean.test", "off")
await hass.async_block_till_done()

entry = MockConfigEntry(
domain=DOMAIN,
options={
CONF_WEBHOOK_URL: DEFAULT_WEBHOOK_URL,
CONF_ENTITY_ID: ["input_boolean.test"],
},
)

await async_setup_entry(hass, entry)

with aioresponses() as http_mock:
http_mock.post(DEFAULT_WEBHOOK_URL, status=500, repeat=2)
http_mock.post(DEFAULT_WEBHOOK_URL, status=200)

hass.states.async_set("input_boolean.test", "on")
await hass.async_block_till_done()

total_calls = sum(len(calls) for calls in http_mock.requests.values())
assert total_calls == 3


@pytest.mark.parametrize(
"options,expected_payload",
Expand Down

0 comments on commit efeebb2

Please sign in to comment.