mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Migrate KNX notify service to entity platform (#115665)
This commit is contained in:
parent
b37f7b1ff0
commit
f115525137
@ -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
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
36
homeassistant/components/knx/repairs.py
Normal file
36
homeassistant/components/knx/repairs.py
Normal 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()
|
@ -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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
)
|
||||||
|
84
tests/components/knx/test_repairs.py
Normal file
84
tests/components/knx/test_repairs.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user