Don't allow partial update of input_select settings (#78376)
parent
abf8b59831
commit
19e853dbb0
|
@ -56,7 +56,7 @@ def _unique(options: Any) -> Any:
|
||||||
raise HomeAssistantError("Duplicate options are not allowed") from exc
|
raise HomeAssistantError("Duplicate options are not allowed") from exc
|
||||||
|
|
||||||
|
|
||||||
CREATE_FIELDS = {
|
STORAGE_FIELDS = {
|
||||||
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
||||||
vol.Required(CONF_OPTIONS): vol.All(
|
vol.Required(CONF_OPTIONS): vol.All(
|
||||||
cv.ensure_list, vol.Length(min=1), _unique, [cv.string]
|
cv.ensure_list, vol.Length(min=1), _unique, [cv.string]
|
||||||
|
@ -64,14 +64,6 @@ CREATE_FIELDS = {
|
||||||
vol.Optional(CONF_INITIAL): cv.string,
|
vol.Optional(CONF_INITIAL): cv.string,
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
}
|
}
|
||||||
UPDATE_FIELDS = {
|
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
|
||||||
vol.Optional(CONF_OPTIONS): vol.All(
|
|
||||||
cv.ensure_list, vol.Length(min=1), _unique, [cv.string]
|
|
||||||
),
|
|
||||||
vol.Optional(CONF_INITIAL): cv.string,
|
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_duplicates(options: list[str], name: str | None) -> list[str]:
|
def _remove_duplicates(options: list[str], name: str | None) -> list[str]:
|
||||||
|
@ -172,7 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
await storage_collection.async_load()
|
await storage_collection.async_load()
|
||||||
|
|
||||||
collection.StorageCollectionWebsocket(
|
collection.StorageCollectionWebsocket(
|
||||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||||
).async_setup(hass)
|
).async_setup(hass)
|
||||||
|
|
||||||
async def reload_service_handler(service_call: ServiceCall) -> None:
|
async def reload_service_handler(service_call: ServiceCall) -> None:
|
||||||
|
@ -238,12 +230,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
class InputSelectStorageCollection(collection.StorageCollection):
|
class InputSelectStorageCollection(collection.StorageCollection):
|
||||||
"""Input storage based collection."""
|
"""Input storage based collection."""
|
||||||
|
|
||||||
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select))
|
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_select))
|
||||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
|
||||||
|
|
||||||
async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]:
|
async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Validate the config is valid."""
|
"""Validate the config is valid."""
|
||||||
return cast(dict[str, Any], self.CREATE_SCHEMA(data))
|
return cast(dict[str, Any], self.CREATE_UPDATE_SCHEMA(data))
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_suggested_id(self, info: dict[str, Any]) -> str:
|
def _get_suggested_id(self, info: dict[str, Any]) -> str:
|
||||||
|
@ -254,8 +245,8 @@ class InputSelectStorageCollection(collection.StorageCollection):
|
||||||
self, data: dict[str, Any], update_data: dict[str, Any]
|
self, data: dict[str, Any], update_data: dict[str, Any]
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return a new updated data object."""
|
"""Return a new updated data object."""
|
||||||
update_data = self.UPDATE_SCHEMA(update_data)
|
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
|
||||||
return _cv_input_select({**data, **update_data})
|
return {CONF_ID: data[CONF_ID]} | update_data
|
||||||
|
|
||||||
|
|
||||||
class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity):
|
class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity):
|
||||||
|
|
|
@ -628,13 +628,11 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
|
||||||
async def test_update(hass, hass_ws_client, storage_setup):
|
async def test_update(hass, hass_ws_client, storage_setup):
|
||||||
"""Test updating options updates the state."""
|
"""Test updating options updates the state."""
|
||||||
|
|
||||||
items = [
|
settings = {
|
||||||
{
|
|
||||||
"id": "from_storage",
|
|
||||||
"name": "from storage",
|
"name": "from storage",
|
||||||
"options": ["yaml update 1", "yaml update 2"],
|
"options": ["yaml update 1", "yaml update 2"],
|
||||||
}
|
}
|
||||||
]
|
items = [{"id": "from_storage"} | settings]
|
||||||
assert await storage_setup(items)
|
assert await storage_setup(items)
|
||||||
|
|
||||||
input_id = "from_storage"
|
input_id = "from_storage"
|
||||||
|
@ -647,28 +645,36 @@ async def test_update(hass, hass_ws_client, storage_setup):
|
||||||
|
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
updated_settings = settings | {
|
||||||
|
"options": ["new option", "newer option"],
|
||||||
|
CONF_INITIAL: "newer option",
|
||||||
|
}
|
||||||
await client.send_json(
|
await client.send_json(
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
"type": f"{DOMAIN}/update",
|
"type": f"{DOMAIN}/update",
|
||||||
f"{DOMAIN}_id": f"{input_id}",
|
f"{DOMAIN}_id": f"{input_id}",
|
||||||
"options": ["new option", "newer option"],
|
**updated_settings,
|
||||||
CONF_INITIAL: "newer option",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
resp = await client.receive_json()
|
resp = await client.receive_json()
|
||||||
assert resp["success"]
|
assert resp["success"]
|
||||||
|
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||||
|
|
||||||
state = hass.states.get(input_entity_id)
|
state = hass.states.get(input_entity_id)
|
||||||
assert state.attributes[ATTR_OPTIONS] == ["new option", "newer option"]
|
assert state.attributes[ATTR_OPTIONS] == ["new option", "newer option"]
|
||||||
|
|
||||||
# Should fail because the initial state is now invalid
|
# Should fail because the initial state is now invalid
|
||||||
|
updated_settings = settings | {
|
||||||
|
"options": ["new option", "no newer option"],
|
||||||
|
CONF_INITIAL: "newer option",
|
||||||
|
}
|
||||||
await client.send_json(
|
await client.send_json(
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
"type": f"{DOMAIN}/update",
|
"type": f"{DOMAIN}/update",
|
||||||
f"{DOMAIN}_id": f"{input_id}",
|
f"{DOMAIN}_id": f"{input_id}",
|
||||||
"options": ["new option", "no newer option"],
|
**updated_settings,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
resp = await client.receive_json()
|
resp = await client.receive_json()
|
||||||
|
@ -678,13 +684,11 @@ async def test_update(hass, hass_ws_client, storage_setup):
|
||||||
async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog):
|
async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog):
|
||||||
"""Test updating options updates the state."""
|
"""Test updating options updates the state."""
|
||||||
|
|
||||||
items = [
|
settings = {
|
||||||
{
|
|
||||||
"id": "from_storage",
|
|
||||||
"name": "from storage",
|
"name": "from storage",
|
||||||
"options": ["yaml update 1", "yaml update 2"],
|
"options": ["yaml update 1", "yaml update 2"],
|
||||||
}
|
}
|
||||||
]
|
items = [{"id": "from_storage"} | settings]
|
||||||
assert await storage_setup(items)
|
assert await storage_setup(items)
|
||||||
|
|
||||||
input_id = "from_storage"
|
input_id = "from_storage"
|
||||||
|
@ -697,13 +701,16 @@ async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog):
|
||||||
|
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
updated_settings = settings | {
|
||||||
|
"options": ["new option", "newer option", "newer option"],
|
||||||
|
CONF_INITIAL: "newer option",
|
||||||
|
}
|
||||||
await client.send_json(
|
await client.send_json(
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
"type": f"{DOMAIN}/update",
|
"type": f"{DOMAIN}/update",
|
||||||
f"{DOMAIN}_id": f"{input_id}",
|
f"{DOMAIN}_id": f"{input_id}",
|
||||||
"options": ["new option", "newer option", "newer option"],
|
**updated_settings,
|
||||||
CONF_INITIAL: "newer option",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
resp = await client.receive_json()
|
resp = await client.receive_json()
|
||||||
|
|
Loading…
Reference in New Issue