Migrate File notify entity platform (#117215)
* Migrate File notify entity platform * Do not load legacy notify service for new config entries * Follow up comment * mypy * Correct typing * Only use the name when importing notify services * Make sure a name is set on new entires
This commit is contained in:
parent
0b47bfc823
commit
548eb35b79
6 changed files with 104 additions and 25 deletions
|
@ -1,7 +1,8 @@
|
||||||
"""The file component."""
|
"""The file component."""
|
||||||
|
|
||||||
|
from homeassistant.components.notify import migrate_notify_issue
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_FILE_PATH, CONF_PLATFORM, Platform
|
from homeassistant.const import CONF_FILE_PATH, CONF_NAME, CONF_PLATFORM, Platform
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
|
@ -22,9 +23,7 @@ IMPORT_SCHEMA = {
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
|
||||||
|
|
||||||
YAML_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
@ -34,6 +33,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
if hass.config_entries.async_entries(DOMAIN):
|
if hass.config_entries.async_entries(DOMAIN):
|
||||||
# We skip import in case we already have config entries
|
# We skip import in case we already have config entries
|
||||||
return True
|
return True
|
||||||
|
# The use of the legacy notify service was deprecated with HA Core 2024.6.0
|
||||||
|
# and will be removed with HA Core 2024.12
|
||||||
|
migrate_notify_issue(hass, DOMAIN, "File", "2024.12.0")
|
||||||
# The YAML config was imported with HA Core 2024.6.0 and will be removed with
|
# The YAML config was imported with HA Core 2024.6.0 and will be removed with
|
||||||
# HA Core 2024.12
|
# HA Core 2024.12
|
||||||
ir.async_create_issue(
|
ir.async_create_issue(
|
||||||
|
@ -53,8 +55,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import the YAML config into separate config entries
|
# Import the YAML config into separate config entries
|
||||||
platforms_config = {
|
platforms_config: dict[Platform, list[ConfigType]] = {
|
||||||
domain: config[domain] for domain in YAML_PLATFORMS if domain in config
|
domain: config[domain] for domain in PLATFORMS if domain in config
|
||||||
}
|
}
|
||||||
for domain, items in platforms_config.items():
|
for domain, items in platforms_config.items():
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -85,14 +87,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
translation_placeholders={"filename": filepath},
|
translation_placeholders={"filename": filepath},
|
||||||
)
|
)
|
||||||
|
|
||||||
if entry.data[CONF_PLATFORM] in PLATFORMS:
|
await hass.config_entries.async_forward_entry_setups(
|
||||||
await hass.config_entries.async_forward_entry_setups(
|
entry, [Platform(entry.data[CONF_PLATFORM])]
|
||||||
entry, [Platform(entry.data[CONF_PLATFORM])]
|
)
|
||||||
)
|
if entry.data[CONF_PLATFORM] == Platform.NOTIFY and CONF_NAME in entry.data:
|
||||||
else:
|
# New notify entities are being setup through the config entry,
|
||||||
# The notify platform is not yet set up as entry, so
|
# but during the deprecation period we want to keep the legacy notify platform,
|
||||||
# forward setup config through discovery to ensure setup notify service.
|
# so we forward the setup config through discovery.
|
||||||
# This is needed as long as the legacy service is not migrated
|
# Only the entities from yaml will still be available as legacy service.
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
discovery.async_load_platform(
|
discovery.async_load_platform(
|
||||||
hass,
|
hass,
|
||||||
|
|
|
@ -41,7 +41,6 @@ FILE_SENSOR_SCHEMA = vol.Schema(
|
||||||
|
|
||||||
FILE_NOTIFY_SCHEMA = vol.Schema(
|
FILE_NOTIFY_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): TEXT_SELECTOR,
|
|
||||||
vol.Required(CONF_FILE_PATH): TEXT_SELECTOR,
|
vol.Required(CONF_FILE_PATH): TEXT_SELECTOR,
|
||||||
vol.Optional(CONF_TIMESTAMP, default=False): BOOLEAN_SELECTOR,
|
vol.Optional(CONF_TIMESTAMP, default=False): BOOLEAN_SELECTOR,
|
||||||
}
|
}
|
||||||
|
@ -79,8 +78,7 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
if not await self.validate_file_path(user_input[CONF_FILE_PATH]):
|
if not await self.validate_file_path(user_input[CONF_FILE_PATH]):
|
||||||
errors[CONF_FILE_PATH] = "not_allowed"
|
errors[CONF_FILE_PATH] = "not_allowed"
|
||||||
else:
|
else:
|
||||||
name: str = user_input.get(CONF_NAME, DEFAULT_NAME)
|
title = f"{DEFAULT_NAME} [{user_input[CONF_FILE_PATH]}]"
|
||||||
title = f"{name} [{user_input[CONF_FILE_PATH]}]"
|
|
||||||
return self.async_create_entry(data=user_input, title=title)
|
return self.async_create_entry(data=user_input, title=title)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from types import MappingProxyType
|
||||||
from typing import Any, TextIO
|
from typing import Any, TextIO
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -13,15 +15,20 @@ from homeassistant.components.notify import (
|
||||||
ATTR_TITLE_DEFAULT,
|
ATTR_TITLE_DEFAULT,
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
BaseNotificationService,
|
BaseNotificationService,
|
||||||
|
NotifyEntity,
|
||||||
|
NotifyEntityFeature,
|
||||||
|
migrate_notify_issue,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import CONF_TIMESTAMP, DOMAIN
|
from .const import CONF_TIMESTAMP, DEFAULT_NAME, DOMAIN, FILE_ICON
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -58,6 +65,15 @@ class FileNotificationService(BaseNotificationService):
|
||||||
self._file_path = file_path
|
self._file_path = file_path
|
||||||
self.add_timestamp = add_timestamp
|
self.add_timestamp = add_timestamp
|
||||||
|
|
||||||
|
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||||
|
"""Send a message to a file."""
|
||||||
|
# The use of the legacy notify service was deprecated with HA Core 2024.6.0
|
||||||
|
# and will be removed with HA Core 2024.12
|
||||||
|
migrate_notify_issue(self.hass, DOMAIN, "File", "2024.12.0")
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
partial(self.send_message, message, **kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||||
"""Send a message to a file."""
|
"""Send a message to a file."""
|
||||||
file: TextIO
|
file: TextIO
|
||||||
|
@ -82,3 +98,53 @@ class FileNotificationService(BaseNotificationService):
|
||||||
translation_key="write_access_failed",
|
translation_key="write_access_failed",
|
||||||
translation_placeholders={"filename": filepath, "exc": f"{exc!r}"},
|
translation_placeholders={"filename": filepath, "exc": f"{exc!r}"},
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up notify entity."""
|
||||||
|
unique_id = entry.entry_id
|
||||||
|
async_add_entities([FileNotifyEntity(unique_id, entry.data)])
|
||||||
|
|
||||||
|
|
||||||
|
class FileNotifyEntity(NotifyEntity):
|
||||||
|
"""Implement the notification entity platform for the File service."""
|
||||||
|
|
||||||
|
_attr_icon = FILE_ICON
|
||||||
|
_attr_supported_features = NotifyEntityFeature.TITLE
|
||||||
|
|
||||||
|
def __init__(self, unique_id: str, config: MappingProxyType[str, Any]) -> None:
|
||||||
|
"""Initialize the service."""
|
||||||
|
self._file_path: str = config[CONF_FILE_PATH]
|
||||||
|
self._add_timestamp: bool = config.get(CONF_TIMESTAMP, False)
|
||||||
|
# Only import a name from an imported entity
|
||||||
|
self._attr_name = config.get(CONF_NAME, DEFAULT_NAME)
|
||||||
|
self._attr_unique_id = unique_id
|
||||||
|
|
||||||
|
def send_message(self, message: str, title: str | None = None) -> None:
|
||||||
|
"""Send a message to a file."""
|
||||||
|
file: TextIO
|
||||||
|
filepath = self._file_path
|
||||||
|
try:
|
||||||
|
with open(filepath, "a", encoding="utf8") as file:
|
||||||
|
if os.stat(filepath).st_size == 0:
|
||||||
|
title = (
|
||||||
|
f"{title or ATTR_TITLE_DEFAULT} notifications (Log"
|
||||||
|
f" started: {dt_util.utcnow().isoformat()})\n{'-' * 80}\n"
|
||||||
|
)
|
||||||
|
file.write(title)
|
||||||
|
|
||||||
|
if self._add_timestamp:
|
||||||
|
text = f"{dt_util.utcnow().isoformat()} {message}\n"
|
||||||
|
else:
|
||||||
|
text = f"{message}\n"
|
||||||
|
file.write(text)
|
||||||
|
except OSError as exc:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="write_access_failed",
|
||||||
|
translation_placeholders={"filename": filepath, "exc": f"{exc!r}"},
|
||||||
|
) from exc
|
||||||
|
|
|
@ -27,12 +27,10 @@
|
||||||
"description": "Set up a service that allows to write notification to a file.",
|
"description": "Set up a service that allows to write notification to a file.",
|
||||||
"data": {
|
"data": {
|
||||||
"file_path": "[%key:component::file::config::step::sensor::data::file_path%]",
|
"file_path": "[%key:component::file::config::step::sensor::data::file_path%]",
|
||||||
"name": "Name",
|
|
||||||
"timestamp": "Timestamp"
|
"timestamp": "Timestamp"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"file_path": "A local file path to write the notification to",
|
"file_path": "A local file path to write the notification to",
|
||||||
"name": "Name of the notify service",
|
|
||||||
"timestamp": "Add a timestamp to the notification"
|
"timestamp": "Add a timestamp to the notification"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ MOCK_CONFIG_NOTIFY = {
|
||||||
"platform": "notify",
|
"platform": "notify",
|
||||||
"file_path": "some_file",
|
"file_path": "some_file",
|
||||||
"timestamp": True,
|
"timestamp": True,
|
||||||
"name": "File",
|
|
||||||
}
|
}
|
||||||
MOCK_CONFIG_SENSOR = {
|
MOCK_CONFIG_SENSOR = {
|
||||||
"platform": "sensor",
|
"platform": "sensor",
|
||||||
|
|
|
@ -32,8 +32,13 @@ async def test_bad_config(hass: HomeAssistant) -> None:
|
||||||
("domain", "service", "params"),
|
("domain", "service", "params"),
|
||||||
[
|
[
|
||||||
(notify.DOMAIN, "test", {"message": "one, two, testing, testing"}),
|
(notify.DOMAIN, "test", {"message": "one, two, testing, testing"}),
|
||||||
|
(
|
||||||
|
notify.DOMAIN,
|
||||||
|
"send_message",
|
||||||
|
{"entity_id": "notify.test", "message": "one, two, testing, testing"},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
ids=["legacy"],
|
ids=["legacy", "entity"],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("timestamp", "config"),
|
("timestamp", "config"),
|
||||||
|
@ -46,6 +51,7 @@ async def test_bad_config(hass: HomeAssistant) -> None:
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"platform": "file",
|
"platform": "file",
|
||||||
"filename": "mock_file",
|
"filename": "mock_file",
|
||||||
|
"timestamp": False,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -276,6 +282,16 @@ async def test_legacy_notify_file_not_allowed(
|
||||||
assert "is not allowed" in caplog.text
|
assert "is not allowed" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("service", "params"),
|
||||||
|
[
|
||||||
|
("test", {"message": "one, two, testing, testing"}),
|
||||||
|
(
|
||||||
|
"send_message",
|
||||||
|
{"entity_id": "notify.test", "message": "one, two, testing, testing"},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("data", "is_allowed"),
|
("data", "is_allowed"),
|
||||||
[
|
[
|
||||||
|
@ -295,12 +311,12 @@ async def test_notify_file_write_access_failed(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
mock_is_allowed_path: MagicMock,
|
mock_is_allowed_path: MagicMock,
|
||||||
|
service: str,
|
||||||
|
params: dict[str, Any],
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the notify file fails."""
|
"""Test the notify file fails."""
|
||||||
domain = notify.DOMAIN
|
domain = notify.DOMAIN
|
||||||
service = "test"
|
|
||||||
params = {"message": "one, two, testing, testing"}
|
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
||||||
|
|
Loading…
Add table
Reference in a new issue