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."""
|
||||
|
||||
from homeassistant.components.notify import migrate_notify_issue
|
||||
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.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
|
@ -22,9 +23,7 @@ IMPORT_SCHEMA = {
|
|||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
YAML_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
|
||||
|
||||
|
||||
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):
|
||||
# We skip import in case we already have config entries
|
||||
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
|
||||
# HA Core 2024.12
|
||||
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
|
||||
platforms_config = {
|
||||
domain: config[domain] for domain in YAML_PLATFORMS if domain in config
|
||||
platforms_config: dict[Platform, list[ConfigType]] = {
|
||||
domain: config[domain] for domain in PLATFORMS if domain in config
|
||||
}
|
||||
for domain, items in platforms_config.items():
|
||||
for item in items:
|
||||
|
@ -85,14 +87,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
translation_placeholders={"filename": filepath},
|
||||
)
|
||||
|
||||
if entry.data[CONF_PLATFORM] in PLATFORMS:
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, [Platform(entry.data[CONF_PLATFORM])]
|
||||
)
|
||||
else:
|
||||
# The notify platform is not yet set up as entry, so
|
||||
# forward setup config through discovery to ensure setup notify service.
|
||||
# This is needed as long as the legacy service is not migrated
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, [Platform(entry.data[CONF_PLATFORM])]
|
||||
)
|
||||
if entry.data[CONF_PLATFORM] == Platform.NOTIFY and CONF_NAME in entry.data:
|
||||
# New notify entities are being setup through the config entry,
|
||||
# but during the deprecation period we want to keep the legacy notify platform,
|
||||
# so we forward the setup config through discovery.
|
||||
# Only the entities from yaml will still be available as legacy service.
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
|
|
|
@ -41,7 +41,6 @@ FILE_SENSOR_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.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]):
|
||||
errors[CONF_FILE_PATH] = "not_allowed"
|
||||
else:
|
||||
name: str = user_input.get(CONF_NAME, DEFAULT_NAME)
|
||||
title = f"{name} [{user_input[CONF_FILE_PATH]}]"
|
||||
title = f"{DEFAULT_NAME} [{user_input[CONF_FILE_PATH]}]"
|
||||
return self.async_create_entry(data=user_input, title=title)
|
||||
|
||||
return self.async_show_form(
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
from types import MappingProxyType
|
||||
from typing import Any, TextIO
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -13,15 +15,20 @@ from homeassistant.components.notify import (
|
|||
ATTR_TITLE_DEFAULT,
|
||||
PLATFORM_SCHEMA,
|
||||
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.exceptions import ServiceValidationError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
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__)
|
||||
|
||||
|
@ -58,6 +65,15 @@ class FileNotificationService(BaseNotificationService):
|
|||
self._file_path = file_path
|
||||
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:
|
||||
"""Send a message to a file."""
|
||||
file: TextIO
|
||||
|
@ -82,3 +98,53 @@ class FileNotificationService(BaseNotificationService):
|
|||
translation_key="write_access_failed",
|
||||
translation_placeholders={"filename": filepath, "exc": f"{exc!r}"},
|
||||
) 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.",
|
||||
"data": {
|
||||
"file_path": "[%key:component::file::config::step::sensor::data::file_path%]",
|
||||
"name": "Name",
|
||||
"timestamp": "Timestamp"
|
||||
},
|
||||
"data_description": {
|
||||
"file_path": "A local file path to write the notification to",
|
||||
"name": "Name of the notify service",
|
||||
"timestamp": "Add a timestamp to the notification"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ MOCK_CONFIG_NOTIFY = {
|
|||
"platform": "notify",
|
||||
"file_path": "some_file",
|
||||
"timestamp": True,
|
||||
"name": "File",
|
||||
}
|
||||
MOCK_CONFIG_SENSOR = {
|
||||
"platform": "sensor",
|
||||
|
|
|
@ -32,8 +32,13 @@ async def test_bad_config(hass: HomeAssistant) -> None:
|
|||
("domain", "service", "params"),
|
||||
[
|
||||
(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(
|
||||
("timestamp", "config"),
|
||||
|
@ -46,6 +51,7 @@ async def test_bad_config(hass: HomeAssistant) -> None:
|
|||
"name": "test",
|
||||
"platform": "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
|
||||
|
||||
|
||||
@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(
|
||||
("data", "is_allowed"),
|
||||
[
|
||||
|
@ -295,12 +311,12 @@ async def test_notify_file_write_access_failed(
|
|||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_is_allowed_path: MagicMock,
|
||||
service: str,
|
||||
params: dict[str, Any],
|
||||
data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the notify file fails."""
|
||||
domain = notify.DOMAIN
|
||||
service = "test"
|
||||
params = {"message": "one, two, testing, testing"}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
||||
|
|
Loading…
Add table
Reference in a new issue