Skip to content

Commit

Permalink
Migrate Tibber notify service (#116893)
Browse files Browse the repository at this point in the history
* Migrate tibber notify service

* Tests and repair flow

* Use notify repair flow helper

* Cleanup strings after using helper, use HomeAssistantError

* Add entry state assertions to unload test

* Update comment

* Update comment
  • Loading branch information
jbouwh committed May 12, 2024
1 parent 07061b1 commit a1bc929
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 11 deletions.
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1440,7 +1440,6 @@ omit =
homeassistant/components/thinkingcleaner/*
homeassistant/components/thomson/device_tracker.py
homeassistant/components/tibber/__init__.py
homeassistant/components/tibber/notify.py
homeassistant/components/tibber/sensor.py
homeassistant/components/tikteck/light.py
homeassistant/components/tile/__init__.py
Expand Down
8 changes: 5 additions & 3 deletions homeassistant/components/tibber/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from .const import DATA_HASS_CONFIG, DOMAIN

PLATFORMS = [Platform.SENSOR]
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]

CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)

Expand Down Expand Up @@ -68,8 +68,9 @@ async def _close(event: Event) -> None:

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
# Use discovery to load platform legacy notify platform
# The use of the legacy notify service was deprecated with HA Core 2024.6
# Support will be removed with HA Core 2024.12
hass.async_create_task(
discovery.async_load_platform(
hass,
Expand All @@ -79,6 +80,7 @@ async def _close(event: Event) -> None:
hass.data[DATA_HASS_CONFIG],
)
)

return True


Expand Down
51 changes: 45 additions & 6 deletions homeassistant/components/tibber/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,44 @@
from __future__ import annotations

from collections.abc import Callable
import logging
from typing import Any

from tibber import Tibber

from homeassistant.components.notify import (
ATTR_TITLE,
ATTR_TITLE_DEFAULT,
BaseNotificationService,
NotifyEntity,
NotifyEntityFeature,
migrate_notify_issue,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from . import DOMAIN as TIBBER_DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> TibberNotificationService:
"""Get the Tibber notification service."""
tibber_connection = hass.data[TIBBER_DOMAIN]
tibber_connection: Tibber = hass.data[TIBBER_DOMAIN]
return TibberNotificationService(tibber_connection.send_notification)


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Tibber notification entity."""
async_add_entities([TibberNotificationEntity(entry.entry_id)])


class TibberNotificationService(BaseNotificationService):
"""Implement the notification service for Tibber."""

Expand All @@ -38,8 +50,35 @@ def __init__(self, notify: Callable) -> None:

async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to Tibber devices."""
migrate_notify_issue(self.hass, TIBBER_DOMAIN, "Tibber", "2024.12.0")
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
try:
await self._notify(title=title, message=message)
except TimeoutError:
_LOGGER.error("Timeout sending message with Tibber")
except TimeoutError as exc:
raise HomeAssistantError(
translation_domain=TIBBER_DOMAIN, translation_key="send_message_timeout"
) from exc


class TibberNotificationEntity(NotifyEntity):
"""Implement the notification entity service for Tibber."""

_attr_supported_features = NotifyEntityFeature.TITLE
_attr_name = TIBBER_DOMAIN
_attr_icon = "mdi:message-flash"

def __init__(self, unique_id: str) -> None:
"""Initialize Tibber notify entity."""
self._attr_unique_id = unique_id

async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to Tibber devices."""
tibber_connection: Tibber = self.hass.data[TIBBER_DOMAIN]
try:
await tibber_connection.send_notification(
title or ATTR_TITLE_DEFAULT, message
)
except TimeoutError as exc:
raise HomeAssistantError(
translation_domain=TIBBER_DOMAIN, translation_key="send_message_timeout"
) from exc
5 changes: 5 additions & 0 deletions homeassistant/components/tibber/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,10 @@
"description": "Enter your access token from {url}"
}
}
},
"exceptions": {
"send_message_timeout": {
"message": "Timeout sending message with Tibber"
}
}
}
27 changes: 26 additions & 1 deletion tests/components/tibber/conftest.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
"""Test helpers for Tibber."""

from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch

import pytest

from homeassistant.components.tibber.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant

from tests.common import MockConfigEntry


@pytest.fixture
def config_entry(hass):
def config_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Tibber config entry."""
config_entry = MockConfigEntry(
domain=DOMAIN,
Expand All @@ -18,3 +22,24 @@ def config_entry(hass):
)
config_entry.add_to_hass(hass)
return config_entry


@pytest.fixture
async def mock_tibber_setup(
config_entry: MockConfigEntry, hass: HomeAssistant
) -> AsyncGenerator[None, MagicMock]:
"""Mock tibber entry setup."""
unique_user_id = "unique_user_id"
title = "title"

tibber_mock = MagicMock()
tibber_mock.update_info = AsyncMock(return_value=True)
tibber_mock.user_id = PropertyMock(return_value=unique_user_id)
tibber_mock.name = PropertyMock(return_value=title)
tibber_mock.send_notification = AsyncMock()
tibber_mock.rt_disconnect = AsyncMock()

with patch("tibber.Tibber", return_value=tibber_mock):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
yield tibber_mock
21 changes: 21 additions & 0 deletions tests/components/tibber/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Test loading of the Tibber config entry."""

from unittest.mock import MagicMock

from homeassistant.components.recorder import Recorder
from homeassistant.components.tibber import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant


async def test_entry_unload(
recorder_mock: Recorder, hass: HomeAssistant, mock_tibber_setup: MagicMock
) -> None:
"""Test unloading the entry."""
entry = hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, "tibber")
assert entry.state == ConfigEntryState.LOADED

await hass.config_entries.async_unload(entry.entry_id)
mock_tibber_setup.rt_disconnect.assert_called_once()
await hass.async_block_till_done(wait_background_tasks=True)
assert entry.state == ConfigEntryState.NOT_LOADED
61 changes: 61 additions & 0 deletions tests/components/tibber/test_notify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Tests for tibber notification service."""

from asyncio import TimeoutError
from unittest.mock import MagicMock

import pytest

from homeassistant.components.recorder import Recorder
from homeassistant.components.tibber import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError


async def test_notification_services(
recorder_mock: Recorder, hass: HomeAssistant, mock_tibber_setup: MagicMock
) -> None:
"""Test create entry from user input."""
# Assert notify entity has been added
notify_state = hass.states.get("notify.tibber")
assert notify_state is not None

# Assert legacy notify service hass been added
assert hass.services.has_service("notify", DOMAIN)

# Test legacy notify service
service = "tibber"
service_data = {"message": "The message", "title": "A title"}
await hass.services.async_call("notify", service, service_data, blocking=True)
calls: MagicMock = mock_tibber_setup.send_notification

calls.assert_called_once_with(message="The message", title="A title")
calls.reset_mock()

# Test notify entity service
service = "send_message"
service_data = {
"entity_id": "notify.tibber",
"message": "The message",
"title": "A title",
}
await hass.services.async_call("notify", service, service_data, blocking=True)
calls.assert_called_once_with("A title", "The message")
calls.reset_mock()

calls.side_effect = TimeoutError

with pytest.raises(HomeAssistantError):
# Test legacy notify service
service = "tibber"
service_data = {"message": "The message", "title": "A title"}
await hass.services.async_call("notify", service, service_data, blocking=True)

with pytest.raises(HomeAssistantError):
# Test notify entity service
service = "send_message"
service_data = {
"entity_id": "notify.tibber",
"message": "The message",
"title": "A title",
}
await hass.services.async_call("notify", service, service_data, blocking=True)
66 changes: 66 additions & 0 deletions tests/components/tibber/test_repairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Test loading of the Tibber config entry."""

from http import HTTPStatus
from unittest.mock import MagicMock

from homeassistant.components.recorder import Recorder
from homeassistant.components.repairs.websocket_api import (
RepairsFlowIndexView,
RepairsFlowResourceView,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir

from tests.typing import ClientSessionGenerator


async def test_repair_flow(
recorder_mock: Recorder,
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
mock_tibber_setup: MagicMock,
hass_client: ClientSessionGenerator,
) -> None:
"""Test unloading the entry."""

# Test legacy notify service
service = "tibber"
service_data = {"message": "The message", "title": "A title"}
await hass.services.async_call("notify", service, service_data, blocking=True)
calls: MagicMock = mock_tibber_setup.send_notification

calls.assert_called_once_with(message="The message", title="A title")
calls.reset_mock()

http_client = await hass_client()
# Assert the issue is present
assert issue_registry.async_get_issue(
domain="notify",
issue_id="migrate_notify_tibber",
)
assert len(issue_registry.issues) == 1

url = RepairsFlowIndexView.url
resp = await http_client.post(
url, json={"handler": "notify", "issue_id": "migrate_notify_tibber"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()

flow_id = data["flow_id"]
assert data["step_id"] == "confirm"

# Simulate the users confirmed the repair flow
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
resp = await http_client.post(url)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["type"] == "create_entry"
await hass.async_block_till_done()

# Assert the issue is no longer present
assert not issue_registry.async_get_issue(
domain="notify",
issue_id="migrate_notify_tibber",
)
assert len(issue_registry.issues) == 0

0 comments on commit a1bc929

Please sign in to comment.