mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add options flow to File (#120269)
* Add options flow to File * Review comments
This commit is contained in:
parent
e39bfeac08
commit
24a20c75eb
@ -1,5 +1,8 @@
|
|||||||
"""The file component."""
|
"""The file component."""
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.notify import migrate_notify_issue
|
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 (
|
from homeassistant.const import (
|
||||||
@ -84,7 +87,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up a file component entry."""
|
"""Set up a file component entry."""
|
||||||
config = dict(entry.data)
|
config = {**entry.data, **entry.options}
|
||||||
filepath: str = config[CONF_FILE_PATH]
|
filepath: str = config[CONF_FILE_PATH]
|
||||||
if filepath and not await hass.async_add_executor_job(
|
if filepath and not await hass.async_add_executor_job(
|
||||||
hass.config.is_allowed_path, filepath
|
hass.config.is_allowed_path, filepath
|
||||||
@ -98,6 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
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])]
|
||||||
)
|
)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
if entry.data[CONF_PLATFORM] == Platform.NOTIFY and CONF_NAME in entry.data:
|
if entry.data[CONF_PLATFORM] == Platform.NOTIFY and CONF_NAME in entry.data:
|
||||||
# New notify entities are being setup through the config entry,
|
# New notify entities are being setup through the config entry,
|
||||||
# but during the deprecation period we want to keep the legacy notify platform,
|
# but during the deprecation period we want to keep the legacy notify platform,
|
||||||
@ -121,3 +125,29 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return await hass.config_entries.async_unload_platforms(
|
return await hass.config_entries.async_unload_platforms(
|
||||||
entry, [entry.data[CONF_PLATFORM]]
|
entry, [entry.data[CONF_PLATFORM]]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Migrate config entry."""
|
||||||
|
if config_entry.version > 2:
|
||||||
|
# Downgraded from future
|
||||||
|
return False
|
||||||
|
|
||||||
|
if config_entry.version < 2:
|
||||||
|
# Move optional fields from data to options in config entry
|
||||||
|
data: dict[str, Any] = deepcopy(dict(config_entry.data))
|
||||||
|
options = {}
|
||||||
|
for key, value in config_entry.data.items():
|
||||||
|
if key not in (CONF_FILE_PATH, CONF_PLATFORM, CONF_NAME):
|
||||||
|
data.pop(key)
|
||||||
|
options[key] = value
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
config_entry, version=2, data=data, options=options
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
"""Config flow for file integration."""
|
"""Config flow for file integration."""
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import (
|
||||||
|
ConfigEntry,
|
||||||
|
ConfigFlow,
|
||||||
|
ConfigFlowResult,
|
||||||
|
OptionsFlow,
|
||||||
|
OptionsFlowWithConfigEntry,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_FILE_PATH,
|
CONF_FILE_PATH,
|
||||||
CONF_FILENAME,
|
CONF_FILENAME,
|
||||||
@ -15,6 +22,7 @@ from homeassistant.const import (
|
|||||||
CONF_VALUE_TEMPLATE,
|
CONF_VALUE_TEMPLATE,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.selector import (
|
from homeassistant.helpers.selector import (
|
||||||
BooleanSelector,
|
BooleanSelector,
|
||||||
BooleanSelectorConfig,
|
BooleanSelectorConfig,
|
||||||
@ -31,27 +39,44 @@ BOOLEAN_SELECTOR = BooleanSelector(BooleanSelectorConfig())
|
|||||||
TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig())
|
TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig())
|
||||||
TEXT_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
|
TEXT_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
|
||||||
|
|
||||||
FILE_FLOW_SCHEMAS = {
|
FILE_OPTIONS_SCHEMAS = {
|
||||||
Platform.SENSOR.value: vol.Schema(
|
Platform.SENSOR.value: vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_FILE_PATH): TEXT_SELECTOR,
|
|
||||||
vol.Optional(CONF_VALUE_TEMPLATE): TEMPLATE_SELECTOR,
|
vol.Optional(CONF_VALUE_TEMPLATE): TEMPLATE_SELECTOR,
|
||||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): TEXT_SELECTOR,
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT): TEXT_SELECTOR,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Platform.NOTIFY.value: vol.Schema(
|
Platform.NOTIFY.value: vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_FILE_PATH): TEXT_SELECTOR,
|
|
||||||
vol.Optional(CONF_TIMESTAMP, default=False): BOOLEAN_SELECTOR,
|
vol.Optional(CONF_TIMESTAMP, default=False): BOOLEAN_SELECTOR,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FILE_FLOW_SCHEMAS = {
|
||||||
|
Platform.SENSOR.value: vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_FILE_PATH): TEXT_SELECTOR,
|
||||||
|
}
|
||||||
|
).extend(FILE_OPTIONS_SCHEMAS[Platform.SENSOR.value].schema),
|
||||||
|
Platform.NOTIFY.value: vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_FILE_PATH): TEXT_SELECTOR,
|
||||||
|
}
|
||||||
|
).extend(FILE_OPTIONS_SCHEMAS[Platform.NOTIFY.value].schema),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a file config flow."""
|
"""Handle a file config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 2
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return FileOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
async def validate_file_path(self, file_path: str) -> bool:
|
async def validate_file_path(self, file_path: str) -> bool:
|
||||||
"""Ensure the file path is valid."""
|
"""Ensure the file path is valid."""
|
||||||
@ -80,7 +105,13 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
errors[CONF_FILE_PATH] = "not_allowed"
|
errors[CONF_FILE_PATH] = "not_allowed"
|
||||||
else:
|
else:
|
||||||
title = f"{DEFAULT_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)
|
data = deepcopy(user_input)
|
||||||
|
options = {}
|
||||||
|
for key, value in user_input.items():
|
||||||
|
if key not in (CONF_FILE_PATH, CONF_PLATFORM, CONF_NAME):
|
||||||
|
data.pop(key)
|
||||||
|
options[key] = value
|
||||||
|
return self.async_create_entry(data=data, title=title, options=options)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id=platform, data_schema=FILE_FLOW_SCHEMAS[platform], errors=errors
|
step_id=platform, data_schema=FILE_FLOW_SCHEMAS[platform], errors=errors
|
||||||
@ -114,4 +145,29 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
file_path = import_data[CONF_FILE_PATH]
|
file_path = import_data[CONF_FILE_PATH]
|
||||||
title = f"{name} [{file_path}]"
|
title = f"{name} [{file_path}]"
|
||||||
return self.async_create_entry(title=title, data=import_data)
|
data = deepcopy(import_data)
|
||||||
|
options = {}
|
||||||
|
for key, value in import_data.items():
|
||||||
|
if key not in (CONF_FILE_PATH, CONF_PLATFORM, CONF_NAME):
|
||||||
|
data.pop(key)
|
||||||
|
options[key] = value
|
||||||
|
return self.async_create_entry(title=title, data=data, options=options)
|
||||||
|
|
||||||
|
|
||||||
|
class FileOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
|
"""Handle File options."""
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Manage File options."""
|
||||||
|
if user_input:
|
||||||
|
return self.async_create_entry(data=user_input)
|
||||||
|
|
||||||
|
platform = self.config_entry.data[CONF_PLATFORM]
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="init",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
FILE_OPTIONS_SCHEMAS[platform], self.config_entry.options or {}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
from functools import partial
|
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
|
||||||
@ -109,7 +108,7 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up notify entity."""
|
"""Set up notify entity."""
|
||||||
unique_id = entry.entry_id
|
unique_id = entry.entry_id
|
||||||
async_add_entities([FileNotifyEntity(unique_id, entry.data)])
|
async_add_entities([FileNotifyEntity(unique_id, {**entry.data, **entry.options})])
|
||||||
|
|
||||||
|
|
||||||
class FileNotifyEntity(NotifyEntity):
|
class FileNotifyEntity(NotifyEntity):
|
||||||
@ -118,7 +117,7 @@ class FileNotifyEntity(NotifyEntity):
|
|||||||
_attr_icon = FILE_ICON
|
_attr_icon = FILE_ICON
|
||||||
_attr_supported_features = NotifyEntityFeature.TITLE
|
_attr_supported_features = NotifyEntityFeature.TITLE
|
||||||
|
|
||||||
def __init__(self, unique_id: str, config: MappingProxyType[str, Any]) -> None:
|
def __init__(self, unique_id: str, config: dict[str, Any]) -> None:
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
self._file_path: str = config[CONF_FILE_PATH]
|
self._file_path: str = config[CONF_FILE_PATH]
|
||||||
self._add_timestamp: bool = config.get(CONF_TIMESTAMP, False)
|
self._add_timestamp: bool = config.get(CONF_TIMESTAMP, False)
|
||||||
|
@ -60,14 +60,15 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the file sensor."""
|
"""Set up the file sensor."""
|
||||||
config = dict(entry.data)
|
config = dict(entry.data)
|
||||||
|
options = dict(entry.options)
|
||||||
file_path: str = config[CONF_FILE_PATH]
|
file_path: str = config[CONF_FILE_PATH]
|
||||||
unique_id: str = entry.entry_id
|
unique_id: str = entry.entry_id
|
||||||
name: str = config.get(CONF_NAME, DEFAULT_NAME)
|
name: str = config.get(CONF_NAME, DEFAULT_NAME)
|
||||||
unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
|
unit: str | None = options.get(CONF_UNIT_OF_MEASUREMENT)
|
||||||
value_template: Template | None = None
|
value_template: Template | None = None
|
||||||
|
|
||||||
if CONF_VALUE_TEMPLATE in config:
|
if CONF_VALUE_TEMPLATE in options:
|
||||||
value_template = Template(config[CONF_VALUE_TEMPLATE], hass)
|
value_template = Template(options[CONF_VALUE_TEMPLATE], hass)
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[FileSensor(unique_id, name, file_path, unit, value_template)], True
|
[FileSensor(unique_id, name, file_path, unit, value_template)], True
|
||||||
|
@ -42,6 +42,22 @@
|
|||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"value_template": "[%key:component::file::config::step::sensor::data::value_template%]",
|
||||||
|
"unit_of_measurement": "[%key:component::file::config::step::sensor::data::unit_of_measurement%]",
|
||||||
|
"timestamp": "[%key:component::file::config::step::notify::data::timestamp%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"value_template": "[%key:component::file::config::step::sensor::data_description::value_template%]",
|
||||||
|
"unit_of_measurement": "[%key:component::file::config::step::sensor::data_description::unit_of_measurement%]",
|
||||||
|
"timestamp": "[%key:component::file::config::step::notify::data_description::timestamp%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"dir_not_allowed": {
|
"dir_not_allowed": {
|
||||||
"message": "Access to {filename} is not allowed."
|
"message": "Access to {filename} is not allowed."
|
||||||
|
@ -7,6 +7,7 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.file import DOMAIN
|
from homeassistant.components.file import DOMAIN
|
||||||
|
from homeassistant.const import CONF_UNIT_OF_MEASUREMENT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
@ -15,20 +16,22 @@ from tests.common import MockConfigEntry
|
|||||||
MOCK_CONFIG_NOTIFY = {
|
MOCK_CONFIG_NOTIFY = {
|
||||||
"platform": "notify",
|
"platform": "notify",
|
||||||
"file_path": "some_file",
|
"file_path": "some_file",
|
||||||
"timestamp": True,
|
|
||||||
}
|
}
|
||||||
|
MOCK_OPTIONS_NOTIFY = {"timestamp": True}
|
||||||
MOCK_CONFIG_SENSOR = {
|
MOCK_CONFIG_SENSOR = {
|
||||||
"platform": "sensor",
|
"platform": "sensor",
|
||||||
"file_path": "some/path",
|
"file_path": "some/path",
|
||||||
"value_template": "{{ value | round(1) }}",
|
|
||||||
}
|
}
|
||||||
|
MOCK_OPTIONS_SENSOR = {"value_template": "{{ value | round(1) }}"}
|
||||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_setup_entry")
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("platform", "data"),
|
("platform", "data", "options"),
|
||||||
[("sensor", MOCK_CONFIG_SENSOR), ("notify", MOCK_CONFIG_NOTIFY)],
|
[
|
||||||
|
("sensor", MOCK_CONFIG_SENSOR, MOCK_OPTIONS_SENSOR),
|
||||||
|
("notify", MOCK_CONFIG_NOTIFY, MOCK_OPTIONS_NOTIFY),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
async def test_form(
|
async def test_form(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -36,6 +39,7 @@ async def test_form(
|
|||||||
mock_is_allowed_path: bool,
|
mock_is_allowed_path: bool,
|
||||||
platform: str,
|
platform: str,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
|
options: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we get the form."""
|
"""Test we get the form."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -50,7 +54,7 @@ async def test_form(
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
user_input = dict(data)
|
user_input = {**data, **options}
|
||||||
user_input.pop("platform")
|
user_input.pop("platform")
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], user_input=user_input
|
result["flow_id"], user_input=user_input
|
||||||
@ -59,12 +63,17 @@ async def test_form(
|
|||||||
|
|
||||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert result2["data"] == data
|
assert result2["data"] == data
|
||||||
|
assert result2["options"] == options
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_setup_entry")
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("platform", "data"),
|
("platform", "data", "options"),
|
||||||
[("sensor", MOCK_CONFIG_SENSOR), ("notify", MOCK_CONFIG_NOTIFY)],
|
[
|
||||||
|
("sensor", MOCK_CONFIG_SENSOR, MOCK_OPTIONS_SENSOR),
|
||||||
|
("notify", MOCK_CONFIG_NOTIFY, MOCK_OPTIONS_NOTIFY),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
async def test_already_configured(
|
async def test_already_configured(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -72,9 +81,10 @@ async def test_already_configured(
|
|||||||
mock_is_allowed_path: bool,
|
mock_is_allowed_path: bool,
|
||||||
platform: str,
|
platform: str,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
|
options: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test aborting if the entry is already configured."""
|
"""Test aborting if the entry is already configured."""
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data=data)
|
entry = MockConfigEntry(domain=DOMAIN, data=data, options=options)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -91,7 +101,7 @@ async def test_already_configured(
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == platform
|
assert result["step_id"] == platform
|
||||||
|
|
||||||
user_input = dict(data)
|
user_input = {**data, **options}
|
||||||
user_input.pop("platform")
|
user_input.pop("platform")
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
@ -103,10 +113,14 @@ async def test_already_configured(
|
|||||||
assert result2["reason"] == "already_configured"
|
assert result2["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_setup_entry")
|
||||||
@pytest.mark.parametrize("is_allowed", [False], ids=["not_allowed"])
|
@pytest.mark.parametrize("is_allowed", [False], ids=["not_allowed"])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("platform", "data"),
|
("platform", "data", "options"),
|
||||||
[("sensor", MOCK_CONFIG_SENSOR), ("notify", MOCK_CONFIG_NOTIFY)],
|
[
|
||||||
|
("sensor", MOCK_CONFIG_SENSOR, MOCK_OPTIONS_SENSOR),
|
||||||
|
("notify", MOCK_CONFIG_NOTIFY, MOCK_OPTIONS_NOTIFY),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
async def test_not_allowed(
|
async def test_not_allowed(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -114,6 +128,7 @@ async def test_not_allowed(
|
|||||||
mock_is_allowed_path: bool,
|
mock_is_allowed_path: bool,
|
||||||
platform: str,
|
platform: str,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
|
options: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test aborting if the file path is not allowed."""
|
"""Test aborting if the file path is not allowed."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -130,7 +145,7 @@ async def test_not_allowed(
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == platform
|
assert result["step_id"] == platform
|
||||||
|
|
||||||
user_input = dict(data)
|
user_input = {**data, **options}
|
||||||
user_input.pop("platform")
|
user_input.pop("platform")
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
@ -140,3 +155,49 @@ async def test_not_allowed(
|
|||||||
|
|
||||||
assert result2["type"] is FlowResultType.FORM
|
assert result2["type"] is FlowResultType.FORM
|
||||||
assert result2["errors"] == {"file_path": "not_allowed"}
|
assert result2["errors"] == {"file_path": "not_allowed"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("platform", "data", "options", "new_options"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"sensor",
|
||||||
|
MOCK_CONFIG_SENSOR,
|
||||||
|
MOCK_OPTIONS_SENSOR,
|
||||||
|
{CONF_UNIT_OF_MEASUREMENT: "mm"},
|
||||||
|
),
|
||||||
|
("notify", MOCK_CONFIG_NOTIFY, MOCK_OPTIONS_NOTIFY, {"timestamp": False}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_options_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_is_allowed_path: bool,
|
||||||
|
platform: str,
|
||||||
|
data: dict[str, Any],
|
||||||
|
options: dict[str, Any],
|
||||||
|
new_options: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test options config flow."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data=data, options=options, version=2)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=new_options,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == new_options
|
||||||
|
|
||||||
|
entry = hass.config_entries.async_get_entry(entry.entry_id)
|
||||||
|
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||||
|
assert entry.options == new_options
|
||||||
|
65
tests/components/file/test_init.py
Normal file
65
tests/components/file/test_init.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""The tests for local file init."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
|
from homeassistant.components.file import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, get_fixture_path
|
||||||
|
|
||||||
|
|
||||||
|
@patch("os.path.isfile", Mock(return_value=True))
|
||||||
|
@patch("os.access", Mock(return_value=True))
|
||||||
|
async def test_migration_to_version_2(
|
||||||
|
hass: HomeAssistant, mock_is_allowed_path: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test the File sensor with JSON entries."""
|
||||||
|
data = {
|
||||||
|
"platform": "sensor",
|
||||||
|
"name": "file2",
|
||||||
|
"file_path": get_fixture_path("file_value_template.txt", "file"),
|
||||||
|
"value_template": "{{ value_json.temperature }}",
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
version=1,
|
||||||
|
data=data,
|
||||||
|
title=f"test [{data['file_path']}]",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
assert entry.version == 2
|
||||||
|
assert entry.data == {
|
||||||
|
"platform": "sensor",
|
||||||
|
"name": "file2",
|
||||||
|
"file_path": get_fixture_path("file_value_template.txt", "file"),
|
||||||
|
}
|
||||||
|
assert entry.options == {
|
||||||
|
"value_template": "{{ value_json.temperature }}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@patch("os.path.isfile", Mock(return_value=True))
|
||||||
|
@patch("os.access", Mock(return_value=True))
|
||||||
|
async def test_migration_from_future_version(
|
||||||
|
hass: HomeAssistant, mock_is_allowed_path: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test the File sensor with JSON entries."""
|
||||||
|
data = {
|
||||||
|
"platform": "sensor",
|
||||||
|
"name": "file2",
|
||||||
|
"file_path": get_fixture_path("file_value_template.txt", "file"),
|
||||||
|
"value_template": "{{ value_json.temperature }}",
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, version=3, data=data, title=f"test [{data['file_path']}]"
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.MIGRATION_ERROR
|
@ -174,7 +174,7 @@ async def test_legacy_notify_file_exception(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("timestamp", "data"),
|
("timestamp", "data", "options"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
False,
|
False,
|
||||||
@ -182,6 +182,8 @@ async def test_legacy_notify_file_exception(
|
|||||||
"name": "test",
|
"name": "test",
|
||||||
"platform": "notify",
|
"platform": "notify",
|
||||||
"file_path": "mock_file",
|
"file_path": "mock_file",
|
||||||
|
},
|
||||||
|
{
|
||||||
"timestamp": False,
|
"timestamp": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -191,6 +193,8 @@ async def test_legacy_notify_file_exception(
|
|||||||
"name": "test",
|
"name": "test",
|
||||||
"platform": "notify",
|
"platform": "notify",
|
||||||
"file_path": "mock_file",
|
"file_path": "mock_file",
|
||||||
|
},
|
||||||
|
{
|
||||||
"timestamp": True,
|
"timestamp": True,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -203,6 +207,7 @@ async def test_legacy_notify_file_entry_only_setup(
|
|||||||
timestamp: bool,
|
timestamp: bool,
|
||||||
mock_is_allowed_path: MagicMock,
|
mock_is_allowed_path: MagicMock,
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
|
options: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the legacy notify file output in entry only setup."""
|
"""Test the legacy notify file output in entry only setup."""
|
||||||
filename = "mock_file"
|
filename = "mock_file"
|
||||||
@ -213,7 +218,11 @@ async def test_legacy_notify_file_entry_only_setup(
|
|||||||
message = params["message"]
|
message = params["message"]
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
domain=DOMAIN,
|
||||||
|
data=data,
|
||||||
|
version=2,
|
||||||
|
options=options,
|
||||||
|
title=f"test [{data['file_path']}]",
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
@ -252,7 +261,7 @@ async def test_legacy_notify_file_entry_only_setup(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("is_allowed", "config"),
|
("is_allowed", "config", "options"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
False,
|
False,
|
||||||
@ -260,6 +269,8 @@ async def test_legacy_notify_file_entry_only_setup(
|
|||||||
"name": "test",
|
"name": "test",
|
||||||
"platform": "notify",
|
"platform": "notify",
|
||||||
"file_path": "mock_file",
|
"file_path": "mock_file",
|
||||||
|
},
|
||||||
|
{
|
||||||
"timestamp": False,
|
"timestamp": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -271,10 +282,15 @@ async def test_legacy_notify_file_not_allowed(
|
|||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
mock_is_allowed_path: MagicMock,
|
mock_is_allowed_path: MagicMock,
|
||||||
config: dict[str, Any],
|
config: dict[str, Any],
|
||||||
|
options: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test legacy notify file output not allowed."""
|
"""Test legacy notify file output not allowed."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=config, title=f"test [{config['file_path']}]"
|
domain=DOMAIN,
|
||||||
|
data=config,
|
||||||
|
version=2,
|
||||||
|
options=options,
|
||||||
|
title=f"test [{config['file_path']}]",
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
assert not await hass.config_entries.async_setup(entry.entry_id)
|
assert not await hass.config_entries.async_setup(entry.entry_id)
|
||||||
@ -293,13 +309,15 @@ async def test_legacy_notify_file_not_allowed(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("data", "is_allowed"),
|
("data", "options", "is_allowed"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"platform": "notify",
|
"platform": "notify",
|
||||||
"file_path": "mock_file",
|
"file_path": "mock_file",
|
||||||
|
},
|
||||||
|
{
|
||||||
"timestamp": False,
|
"timestamp": False,
|
||||||
},
|
},
|
||||||
True,
|
True,
|
||||||
@ -314,12 +332,17 @@ async def test_notify_file_write_access_failed(
|
|||||||
service: str,
|
service: str,
|
||||||
params: dict[str, Any],
|
params: dict[str, Any],
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
|
options: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the notify file fails."""
|
"""Test the notify file fails."""
|
||||||
domain = notify.DOMAIN
|
domain = notify.DOMAIN
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
domain=DOMAIN,
|
||||||
|
data=data,
|
||||||
|
version=2,
|
||||||
|
options=options,
|
||||||
|
title=f"test [{data['file_path']}]",
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
@ -47,7 +47,11 @@ async def test_file_value_entry_setup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
domain=DOMAIN,
|
||||||
|
data=data,
|
||||||
|
version=2,
|
||||||
|
options={},
|
||||||
|
title=f"test [{data['file_path']}]",
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
@ -66,11 +70,17 @@ async def test_file_value_template(
|
|||||||
"platform": "sensor",
|
"platform": "sensor",
|
||||||
"name": "file2",
|
"name": "file2",
|
||||||
"file_path": get_fixture_path("file_value_template.txt", "file"),
|
"file_path": get_fixture_path("file_value_template.txt", "file"),
|
||||||
|
}
|
||||||
|
options = {
|
||||||
"value_template": "{{ value_json.temperature }}",
|
"value_template": "{{ value_json.temperature }}",
|
||||||
}
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
domain=DOMAIN,
|
||||||
|
data=data,
|
||||||
|
version=2,
|
||||||
|
options=options,
|
||||||
|
title=f"test [{data['file_path']}]",
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
@ -90,7 +100,11 @@ async def test_file_empty(hass: HomeAssistant, mock_is_allowed_path: MagicMock)
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
domain=DOMAIN,
|
||||||
|
data=data,
|
||||||
|
version=2,
|
||||||
|
options={},
|
||||||
|
title=f"test [{data['file_path']}]",
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
@ -113,7 +127,11 @@ async def test_file_path_invalid(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data=data, title=f"test [{data['file_path']}]"
|
domain=DOMAIN,
|
||||||
|
data=data,
|
||||||
|
version=2,
|
||||||
|
options={},
|
||||||
|
title=f"test [{data['file_path']}]",
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user