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 platform
for platform in SUPPORTED_PLATFORMS 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: if NotifySchema.PLATFORM in config:
hass.async_create_task( hass.async_create_task(
discovery.async_load_platform( discovery.async_load_platform(
@ -232,7 +232,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
platform platform
for platform in SUPPORTED_PLATFORMS for platform in SUPPORTED_PLATFORMS
if platform in hass.data[DATA_KNX_CONFIG] 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"], "after_dependencies": ["panel_custom"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"config_flow": true, "config_flow": true,
"dependencies": ["file_upload", "websocket_api"], "dependencies": ["file_upload", "repairs", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/knx", "documentation": "https://www.home-assistant.io/integrations/knx",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_push", "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 from __future__ import annotations
@ -7,13 +7,16 @@ from typing import Any
from xknx import XKNX from xknx import XKNX
from xknx.devices import Notification as XknxNotification from xknx.devices import Notification as XknxNotification
from homeassistant.components.notify import BaseNotificationService from homeassistant import config_entries
from homeassistant.const import CONF_NAME, CONF_TYPE 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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS 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( async def async_get_service(
@ -25,16 +28,11 @@ async def async_get_service(
if discovery_info is None: if discovery_info is None:
return 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 xknx: XKNX = hass.data[DOMAIN].xknx
notification_devices = [ notification_devices = [
XknxNotification( _create_notification_instance(xknx, device_config)
xknx,
name=device_config[CONF_NAME],
group_address=device_config[KNX_ADDRESS],
value_type=device_config[CONF_TYPE],
)
for device_config in platform_config for device_config in platform_config
] ]
return KNXNotificationService(notification_devices) return KNXNotificationService(notification_devices)
@ -59,6 +57,7 @@ class KNXNotificationService(BaseNotificationService):
async def async_send_message(self, message: str = "", **kwargs: Any) -> None: async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a notification to knx bus.""" """Send a notification to knx bus."""
migrate_notify_issue(self.hass)
if "target" in kwargs: if "target" in kwargs:
await self._async_send_to_device(message, kwargs["target"]) await self._async_send_to_device(message, kwargs["target"])
else: else:
@ -74,3 +73,41 @@ class KNXNotificationService(BaseNotificationService):
for device in self.devices: for device in self.devices:
if device.name in names: if device.name in names:
await device.set(message) 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_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator, vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator,
vol.Required(KNX_ADDRESS): ga_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%]", "name": "[%key:common::action::reload%]",
"description": "Reloads the KNX integration." "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.""" """Test KNX notify."""
from homeassistant.components import notify
from homeassistant.components.knx.const import KNX_ADDRESS from homeassistant.components.knx.const import KNX_ADDRESS
from homeassistant.components.knx.schema import NotifySchema from homeassistant.components.knx.schema import NotifySchema
from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.const import CONF_NAME, CONF_TYPE
@ -8,7 +9,9 @@ from homeassistant.core import HomeAssistant
from .conftest import KNXTestKit 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.""" """Test KNX notify can send to one device."""
await knx.setup_integration( await knx.setup_integration(
{ {
@ -26,22 +29,7 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None:
await knx.assert_write( await knx.assert_write(
"1/0/0", "1/0/0",
( (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 0, 0, 0, 0),
0x49,
0x20,
0x6C,
0x6F,
0x76,
0x65,
0x20,
0x4B,
0x4E,
0x58,
0x0,
0x0,
0x0,
0x0,
),
) )
await hass.services.async_call( await hass.services.async_call(
@ -56,26 +44,11 @@ async def test_notify_simple(hass: HomeAssistant, knx: KNXTestKit) -> None:
await knx.assert_write( await knx.assert_write(
"1/0/0", "1/0/0",
( (73, 32, 108, 111, 118, 101, 32, 75, 78, 88, 44, 32, 98, 117),
0x49,
0x20,
0x6C,
0x6F,
0x76,
0x65,
0x20,
0x4B,
0x4E,
0x58,
0x2C,
0x20,
0x62,
0x75,
),
) )
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 hass: HomeAssistant, knx: KNXTestKit
) -> None: ) -> None:
"""Test KNX notify `type` configuration.""" """Test KNX notify `type` configuration."""
@ -110,3 +83,91 @@ async def test_notify_multiple_sends_to_all_with_different_encodings(
"1/0/1", "1/0/1",
(71, 228, 110, 115, 101, 102, 252, 223, 99, 104, 101, 110, 0, 0), (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