Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate File notify entity platform #117215

Merged
merged 8 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]}]"
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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