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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Tibber notify service #116893

Merged
merged 8 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
48 changes: 42 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 ServiceValidationError
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()])


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

Expand All @@ -38,8 +50,32 @@ 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 ServiceValidationError(
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_unique_id = f"{TIBBER_DOMAIN}_notify"
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
_attr_name = TIBBER_DOMAIN
_attr_icon = "mdi:message-flash"

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 ServiceValidationError(
translation_domain=TIBBER_DOMAIN, translation_key="send_message_timeout"
) from exc
18 changes: 18 additions & 0 deletions homeassistant/components/tibber/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,23 @@
"description": "Enter your access token from {url}"
}
}
},
"exceptions": {
"send_message_timeout": {
"message": "Timeout sending message with Tibber"
}
},
"issues": {
"migrate_notify": {
"title": "Migration of Tibber notify service",
"fix_flow": {
"step": {
"confirm": {
"description": "The Tibber `notify` service has been migrated. A new `notify` entity is available now.\n\nUpdate any automations to use the new `notify.send_message` service exposed by this new entity. When this is done, fix this issue and restart Home Assistant.",
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
"title": "Migrate legacy Tibber notify service"
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}
}
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
17 changes: 17 additions & 0 deletions tests/components/tibber/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""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.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")
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)
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
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 ServiceValidationError


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
jbouwh marked this conversation as resolved.
Show resolved Hide resolved

with pytest.raises(ServiceValidationError):
# 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(ServiceValidationError):
# 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"

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"
# Test confirm step in repair flow
jbouwh marked this conversation as resolved.
Show resolved Hide resolved
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