Skip to content

Commit

Permalink
Migrate File notify entity platform (#117215)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jbouwh committed May 13, 2024
1 parent 0b47bfc commit 548eb35
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 25 deletions.
30 changes: 16 additions & 14 deletions homeassistant/components/file/__init__.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -22,9 +23,7 @@

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:
Expand All @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions homeassistant/components/file/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@

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,
}
Expand Down Expand Up @@ -79,8 +78,7 @@ async def async_step_notify(
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(
Expand Down
70 changes: 68 additions & 2 deletions homeassistant/components/file/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,15 +15,20 @@
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__)

Expand Down Expand Up @@ -58,6 +65,15 @@ def __init__(self, file_path: str, add_timestamp: bool) -> None:
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
Expand All @@ -82,3 +98,53 @@ def send_message(self, message: str = "", **kwargs: Any) -> None:
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
2 changes: 0 additions & 2 deletions homeassistant/components/file/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down
1 change: 0 additions & 1 deletion tests/components/file/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"platform": "notify",
"file_path": "some_file",
"timestamp": True,
"name": "File",
}
MOCK_CONFIG_SENSOR = {
"platform": "sensor",
Expand Down
22 changes: 19 additions & 3 deletions tests/components/file/test_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -46,6 +51,7 @@ async def test_bad_config(hass: HomeAssistant) -> None:
"name": "test",
"platform": "file",
"filename": "mock_file",
"timestamp": False,
}
]
},
Expand Down Expand Up @@ -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"),
[
Expand All @@ -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']}]"
Expand Down

0 comments on commit 548eb35

Please sign in to comment.