Migrate Tibber notify service (#116893)

* 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
pull/117314/head
Jan Bouwhuis 2024-05-12 19:52:08 +02:00 committed by GitHub
parent 07061b14d0
commit a1bc929421
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 229 additions and 11 deletions

View File

@ -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

View File

@ -22,7 +22,7 @@ from homeassistant.util import dt as dt_util
from .const import DATA_HASS_CONFIG, DOMAIN
PLATFORMS = [Platform.SENSOR]
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@ -68,8 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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,
@ -79,6 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DATA_HASS_CONFIG],
)
)
return True

View File

@ -3,21 +3,26 @@
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,
@ -25,10 +30,17 @@ async def async_get_service(
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."""
@ -38,8 +50,35 @@ class TibberNotificationService(BaseNotificationService):
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

View File

@ -101,5 +101,10 @@
"description": "Enter your access token from {url}"
}
}
},
"exceptions": {
"send_message_timeout": {
"message": "Timeout sending message with Tibber"
}
}
}

View File

@ -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,
@ -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

View File

@ -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

View File

@ -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)

View File

@ -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