Migrate KNX notify service to entity platform (#115665)

This commit is contained in:
Matthias Alphart 2024-04-24 07:51:02 +02:00 committed by GitHub
parent b37f7b1ff0
commit f115525137
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 281 additions and 49 deletions

View File

@ -197,11 +197,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
[
platform
for platform in SUPPORTED_PLATFORMS
if platform in config and platform not in (Platform.SENSOR, Platform.NOTIFY)
if platform in config and platform is not Platform.SENSOR
],
)
# set up notify platform, no entry support for notify component yet
# set up notify service for backwards compatibility - remove 2024.11
if NotifySchema.PLATFORM in config:
hass.async_create_task(
discovery.async_load_platform(
@ -232,7 +232,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
platform
for platform in SUPPORTED_PLATFORMS
if platform in hass.data[DATA_KNX_CONFIG]
and platform not in (Platform.SENSOR, Platform.NOTIFY)
and platform is not Platform.SENSOR
],
],
)

View File

@ -4,7 +4,7 @@
"after_dependencies": ["panel_custom"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"config_flow": true,
"dependencies": ["file_upload", "websocket_api"],
"dependencies": ["file_upload", "repairs", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/knx",
"integration_type": "hub",
"iot_class": "local_push",

View File

@ -1,4 +1,4 @@
"""Support for KNX/IP notification services."""
"""Support for KNX/IP notifications."""
from __future__ import annotations
@ -7,13 +7,16 @@ from typing import Any
from xknx import XKNX
from xknx.devices import Notification as XknxNotification
from homeassistant.components.notify import BaseNotificationService
from homeassistant.const import CONF_NAME, CONF_TYPE
from homeassistant import config_entries
from homeassistant.components.notify import BaseNotificationService, NotifyEntity
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
from .schema import NotifySchema
from .knx_entity import KnxEntity
from .repairs import migrate_notify_issue
async def async_get_service(
@ -25,16 +28,11 @@ async def async_get_service(
if discovery_info is None:
return None
if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM):
if platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.NOTIFY):
xknx: XKNX = hass.data[DOMAIN].xknx
notification_devices = [
XknxNotification(
xknx,
name=device_config[CONF_NAME],
group_address=device_config[KNX_ADDRESS],
value_type=device_config[CONF_TYPE],
)
_create_notification_instance(xknx, device_config)
for device_config in platform_config
]
return KNXNotificationService(notification_devices)
@ -59,6 +57,7 @@ class KNXNotificationService(BaseNotificationService):
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a notification to knx bus."""
migrate_notify_issue(self.hass)
if "target" in kwargs:
await self._async_send_to_device(message, kwargs["target"])
else:
@ -74,3 +73,41 @@ class KNXNotificationService(BaseNotificationService):
for device in self.devices:
if device.name in names:
await device.set(message)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up notify(s) for KNX platform."""
xknx: XKNX = hass.data[DOMAIN].xknx
config: list[ConfigType] = hass.data[DATA_KNX_CONFIG][Platform.NOTIFY]
async_add_entities(KNXNotify(xknx, entity_config) for entity_config in config)
def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotification:
"""Return a KNX Notification to be used within XKNX."""
return XknxNotification(
xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
value_type=config[CONF_TYPE],
)
class KNXNotify(NotifyEntity, KnxEntity):
"""Representation of a KNX notification entity."""
_device: XknxNotification
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
"""Initialize a KNX notification."""
super().__init__(_create_notification_instance(xknx, config))
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
async def async_send_message(self, message: str) -> None:
"""Send a notification to knx bus."""
await self._device.set(message)

View File

@ -0,0 +1,36 @@
"""Repairs support for KNX."""
from __future__ import annotations
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
from .const import DOMAIN
@callback
def migrate_notify_issue(hass: HomeAssistant) -> None:
"""Create issue for notify service deprecation."""
ir.async_create_issue(
hass,
DOMAIN,
"migrate_notify",
breaks_in_ha_version="2024.11.0",
issue_domain=Platform.NOTIFY.value,
is_fixable=True,
is_persistent=True,
translation_key="migrate_notify",
severity=ir.IssueSeverity.WARNING,
)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
assert issue_id == "migrate_notify"
return ConfirmRepairFlow()

View File

@ -750,6 +750,7 @@ class NotifySchema(KNXPlatformSchema):
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator,
vol.Required(KNX_ADDRESS): ga_validator,
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
}
)

View File

@ -384,5 +384,18 @@
"name": "[%key:common::action::reload%]",
"description": "Reloads the KNX integration."
}
},
"issues": {
"migrate_notify": {
"title": "Migration of KNX notify service",
"fix_flow": {
"step": {
"confirm": {
"description": "The KNX `notify` service has been migrated. New `notify` entities are available now.\n\nUpdate any automations to use the new `notify.send_message` exposed by these new entities. When this is done, fix this issue and restart Home Assistant.",
"title": "Disable legacy KNX notify service"
}
}
}
}
}
}

View File

@ -1,5 +1,6 @@
"""Test KNX notify."""
from homeassistant.components import notify
from homeassistant.components.knx.const import KNX_ADDRESS
from homeassistant.components.knx.schema import NotifySchema
from homeassistant.const import CONF_NAME, CONF_TYPE
@ -8,7 +9,9 @@ from homeassistant.core import HomeAssistant
from .conftest import KNXTestKit
async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None:
async def test_legacy_notify_service_simple(
hass: HomeAssistant, knx: KNXTestKit
) -> None:
"""Test KNX notify can send to one device."""
await knx.setup_integration(
{
@ -26,22 +29,7 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None:
await knx.assert_write(
"1/0/0",
(
0x49,
0x20,
0x6C,
0x6F,
0x76,
0x65,
0x20,
0x4B,
0x4E,
0x58,
0x0,
0x0,
0x0,
0x0,
),
(73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0),
)
await hass.services.async_call(
@ -56,26 +44,11 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None:
await knx.assert_write(
"1/0/0",
(
0x49,
0x20,
0x6C,
0x6F,
0x76,
0x65,
0x20,
0x4B,
0x4E,
0x58,
0x2C,
0x20,
0x62,
0x75,
),
(73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117),
)
async def test_notify_multiple_sends_to_all_with_different_encodings(
async def test_legacy_notify_service_multiple_sends_to_all_with_different_encodings(
hass: HomeAssistant, knx: KNXTestKit
) -> None:
"""Test KNX notify `type` configuration."""
@ -110,3 +83,91 @@ async def test_notify_multiple_sends_to_all_with_different_encodings(
"1/0/1",
(71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0),
)
async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None:
"""Test KNX notify can send to one device."""
await knx.setup_integration(
{
NotifySchema.PLATFORM: {
CONF_NAME: "test",
KNX_ADDRESS: "1/0/0",
}
}
)
await hass.services.async_call(
notify.DOMAIN,
notify.SERVICE_SEND_MESSAGE,
{
"entity_id": "notify.test",
notify.ATTR_MESSAGE: "I love KNX",
},
)
await knx.assert_write(
"1/0/0",
(73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0),
)
await hass.services.async_call(
notify.DOMAIN,
notify.SERVICE_SEND_MESSAGE,
{
"entity_id": "notify.test",
notify.ATTR_MESSAGE: "I love KNX, but this text is too long for KNX, poor KNX",
},
)
await knx.assert_write(
"1/0/0",
(73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117),
)
async def test_notify_multiple_sends_with_different_encodings(
hass: HomeAssistant, knx: KNXTestKit
) -> None:
"""Test KNX notify `type` configuration."""
await knx.setup_integration(
{
NotifySchema.PLATFORM: [
{
CONF_NAME: "ASCII",
KNX_ADDRESS: "1/0/0",
CONF_TYPE: "string",
},
{
CONF_NAME: "Latin-1",
KNX_ADDRESS: "1/0/1",
CONF_TYPE: "latin_1",
},
]
}
)
message = {notify.ATTR_MESSAGE: "Gänsefüßchen"}
await hass.services.async_call(
notify.DOMAIN,
notify.SERVICE_SEND_MESSAGE,
{
"entity_id": "notify.ascii",
**message,
},
)
await knx.assert_write(
"1/0/0",
# "G?nsef??chen"
(71, 63, 110, 115, 101, 102, 63, 63, 99, 104, 101, 110, 0, 0),
)
await hass.services.async_call(
notify.DOMAIN,
notify.SERVICE_SEND_MESSAGE,
{
"entity_id": "notify.latin_1",
**message,
},
)
await knx.assert_write(
"1/0/1",
(71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0),
)

View File

@ -0,0 +1,84 @@
"""Test repairs for KNX integration."""
from http import HTTPStatus
from homeassistant.components.knx.const import DOMAIN, KNX_ADDRESS
from homeassistant.components.knx.schema import NotifySchema
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.components.repairs.websocket_api import (
RepairsFlowIndexView,
RepairsFlowResourceView,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.issue_registry as ir
from .conftest import KNXTestKit
from tests.typing import ClientSessionGenerator
async def test_knx_notify_service_issue(
hass: HomeAssistant,
knx: KNXTestKit,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test the legacy notify service still works before migration and repair flow is triggered."""
await knx.setup_integration(
{
NotifySchema.PLATFORM: {
CONF_NAME: "test",
KNX_ADDRESS: "1/0/0",
}
}
)
http_client = await hass_client()
# Assert no issue is present
assert len(issue_registry.issues) == 0
# Simulate legacy service being used
assert hass.services.has_service(NOTIFY_DOMAIN, NOTIFY_DOMAIN)
await hass.services.async_call(
NOTIFY_DOMAIN,
NOTIFY_DOMAIN,
service_data={"message": "It is too cold!", "target": "test"},
blocking=True,
)
await knx.assert_write(
"1/0/0",
(73, 116, 32, 105, 115, 32, 116, 111, 111, 32, 99, 111, 108, 100),
)
# Assert the issue is present
assert len(issue_registry.issues) == 1
assert issue_registry.async_get_issue(
domain=DOMAIN,
issue_id="migrate_notify",
)
# Test confirm step in repair flow
resp = await http_client.post(
RepairsFlowIndexView.url,
json={"handler": DOMAIN, "issue_id": "migrate_notify"},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data["step_id"] == "confirm"
resp = await http_client.post(
RepairsFlowResourceView.url.format(flow_id=flow_id),
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["type"] == "create_entry"
# Assert the issue is no longer present
assert not issue_registry.async_get_issue(
domain=DOMAIN,
issue_id="migrate_notify",
)
assert len(issue_registry.issues) == 0