From c321d750f49c1c48458628f9e6532516e876714d Mon Sep 17 00:00:00 2001 From: karwosts <karwosts@gmail.com> Date: Tue, 29 Oct 2024 11:24:57 -0700 Subject: [PATCH 1/8] Allow disabled selectors in config flows. Show hidden options for history_stats. --- .../components/history_stats/config_flow.py | 11 +++++++++++ homeassistant/components/history_stats/strings.json | 12 ++++++++++++ homeassistant/data_entry_flow.py | 5 ++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index 8dbca3b19393f..6e09dedd4c1a9 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -66,6 +66,17 @@ async def validate_options( ) DATA_SCHEMA_OPTIONS = vol.Schema( { + vol.Required(CONF_ENTITY_ID, description={"disabled": True}): EntitySelector(), + vol.Required(CONF_STATE, description={"disabled": True}): TextSelector( + TextSelectorConfig(multiple=True) + ), + vol.Required(CONF_TYPE, description={"disabled": True}): SelectSelector( + SelectSelectorConfig( + options=CONF_TYPE_KEYS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_TYPE, + ) + ), vol.Optional(CONF_START): TemplateSelector(), vol.Optional(CONF_END): TemplateSelector(), vol.Optional(CONF_DURATION): DurationSelector( diff --git a/homeassistant/components/history_stats/strings.json b/homeassistant/components/history_stats/strings.json index 8961d66118d28..54674ce10c2bb 100644 --- a/homeassistant/components/history_stats/strings.json +++ b/homeassistant/components/history_stats/strings.json @@ -26,11 +26,17 @@ "options": { "description": "Read the documention for further details on how to configure the history stats sensor using these options.", "data": { + "entity_id": "[%key:component::history_stats::config::step::user::data::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data::state%]", + "type": "[%key:component::history_stats::config::step::user::data::type%]", "start": "Start", "end": "End", "duration": "Duration" }, "data_description": { + "entity_id": "[%key:component::history_stats::config::step::user::data_description::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data_description::state%]", + "type": "[%key:component::history_stats::config::step::user::data_description::type%]", "start": "When to start the measure (timestamp or datetime). Can be a template.", "end": "When to stop the measure (timestamp or datetime). Can be a template", "duration": "Duration of the measure." @@ -49,11 +55,17 @@ "init": { "description": "[%key:component::history_stats::config::step::options::description%]", "data": { + "entity_id": "[%key:component::history_stats::config::step::user::data::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data::state%]", + "type": "[%key:component::history_stats::config::step::user::data::type%]", "start": "[%key:component::history_stats::config::step::options::data::start%]", "end": "[%key:component::history_stats::config::step::options::data::end%]", "duration": "[%key:component::history_stats::config::step::options::data::duration%]" }, "data_description": { + "entity_id": "[%key:component::history_stats::config::step::user::data_description::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data_description::state%]", + "type": "[%key:component::history_stats::config::step::user::data_description::type%]", "start": "[%key:component::history_stats::config::step::options::data_description::start%]", "end": "[%key:component::history_stats::config::step::options::data_description::end%]", "duration": "[%key:component::history_stats::config::step::options::data_description::duration%]" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 63baca56aebd3..55e6f8e5747b0 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -694,7 +694,10 @@ def add_suggested_values_to_schema( ): # Copy the marker to not modify the flow schema new_key = copy.copy(key) - new_key.description = {"suggested_value": suggested_values[key.schema]} + new_key.description = { + **(new_key.description or {}), + "suggested_value": suggested_values[key.schema], + } schema[new_key] = val return vol.Schema(schema) From e5ef5817e83a377684b7f05b6bc1becca70465b8 Mon Sep 17 00:00:00 2001 From: karwosts <karwosts@gmail.com> Date: Tue, 29 Oct 2024 12:20:18 -0700 Subject: [PATCH 2/8] fix tests --- .../components/history_stats/test_config_flow.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/components/history_stats/test_config_flow.py b/tests/components/history_stats/test_config_flow.py index a695a06995e96..3d06f3f9a853b 100644 --- a/tests/components/history_stats/test_config_flow.py +++ b/tests/components/history_stats/test_config_flow.py @@ -44,6 +44,9 @@ async def test_form( result = await hass.config_entries.flow.async_configure( result["flow_id"], { + CONF_ENTITY_ID: "binary_sensor.test_monitored", + CONF_STATE: ["on"], + CONF_TYPE: "count", CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", CONF_END: "{{ utcnow() }}", }, @@ -77,6 +80,9 @@ async def test_options_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ + CONF_ENTITY_ID: "binary_sensor.test_monitored", + CONF_STATE: ["on"], + CONF_TYPE: "count", CONF_END: "{{ utcnow() }}", CONF_DURATION: {"hours": 8, "minutes": 0, "seconds": 0, "days": 20}, }, @@ -126,6 +132,9 @@ async def test_validation_options( result = await hass.config_entries.flow.async_configure( result["flow_id"], { + CONF_ENTITY_ID: "binary_sensor.test_monitored", + CONF_STATE: ["on"], + CONF_TYPE: "count", CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", CONF_END: "{{ utcnow() }}", CONF_DURATION: {"hours": 8, "minutes": 0, "seconds": 0, "days": 20}, @@ -140,6 +149,9 @@ async def test_validation_options( result = await hass.config_entries.flow.async_configure( result["flow_id"], { + CONF_ENTITY_ID: "binary_sensor.test_monitored", + CONF_STATE: ["on"], + CONF_TYPE: "count", CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", CONF_END: "{{ utcnow() }}", }, @@ -185,6 +197,9 @@ async def test_entry_already_exist( result = await hass.config_entries.flow.async_configure( result["flow_id"], { + CONF_ENTITY_ID: "binary_sensor.test_monitored", + CONF_STATE: ["on"], + CONF_TYPE: "count", CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", CONF_END: "{{ utcnow() }}", }, From 9407b787bddaad42a87ab282a777bee59479c579 Mon Sep 17 00:00:00 2001 From: karwosts <karwosts@gmail.com> Date: Tue, 29 Oct 2024 14:43:20 -0700 Subject: [PATCH 3/8] use optional instead of required --- .../components/history_stats/config_flow.py | 6 +++--- homeassistant/helpers/schema_config_entry_flow.py | 4 ++++ .../components/history_stats/test_config_flow.py | 15 --------------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index 6e09dedd4c1a9..c25421bbbca6e 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -66,11 +66,11 @@ async def validate_options( ) DATA_SCHEMA_OPTIONS = vol.Schema( { - vol.Required(CONF_ENTITY_ID, description={"disabled": True}): EntitySelector(), - vol.Required(CONF_STATE, description={"disabled": True}): TextSelector( + vol.Optional(CONF_ENTITY_ID, description={"disabled": True}): EntitySelector(), + vol.Optional(CONF_STATE, description={"disabled": True}): TextSelector( TextSelectorConfig(multiple=True) ), - vol.Required(CONF_TYPE, description={"disabled": True}): SelectSelector( + vol.Optional(CONF_TYPE, description={"disabled": True}): SelectSelector( SelectSelectorConfig( options=CONF_TYPE_KEYS, mode=SelectSelectorMode.DROPDOWN, diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index af8c4c6402df5..a08d2a59493d7 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -214,6 +214,10 @@ def _update_and_remove_omitted_optional_keys( and key.description.get("advanced") and not self._handler.show_advanced_options ) + and not ( + # don't remove readonly keys + key.description and key.description.get("disabled") + ) ): # Key not present, delete keys old value (if present) too values.pop(key.schema, None) diff --git a/tests/components/history_stats/test_config_flow.py b/tests/components/history_stats/test_config_flow.py index 3d06f3f9a853b..a695a06995e96 100644 --- a/tests/components/history_stats/test_config_flow.py +++ b/tests/components/history_stats/test_config_flow.py @@ -44,9 +44,6 @@ async def test_form( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_ENTITY_ID: "binary_sensor.test_monitored", - CONF_STATE: ["on"], - CONF_TYPE: "count", CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", CONF_END: "{{ utcnow() }}", }, @@ -80,9 +77,6 @@ async def test_options_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_ENTITY_ID: "binary_sensor.test_monitored", - CONF_STATE: ["on"], - CONF_TYPE: "count", CONF_END: "{{ utcnow() }}", CONF_DURATION: {"hours": 8, "minutes": 0, "seconds": 0, "days": 20}, }, @@ -132,9 +126,6 @@ async def test_validation_options( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_ENTITY_ID: "binary_sensor.test_monitored", - CONF_STATE: ["on"], - CONF_TYPE: "count", CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", CONF_END: "{{ utcnow() }}", CONF_DURATION: {"hours": 8, "minutes": 0, "seconds": 0, "days": 20}, @@ -149,9 +140,6 @@ async def test_validation_options( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_ENTITY_ID: "binary_sensor.test_monitored", - CONF_STATE: ["on"], - CONF_TYPE: "count", CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", CONF_END: "{{ utcnow() }}", }, @@ -197,9 +185,6 @@ async def test_entry_already_exist( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_ENTITY_ID: "binary_sensor.test_monitored", - CONF_STATE: ["on"], - CONF_TYPE: "count", CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", CONF_END: "{{ utcnow() }}", }, From a1ce4a5090a154150e274bec7d6cdf588979ee65 Mon Sep 17 00:00:00 2001 From: karwosts <karwosts@gmail.com> Date: Wed, 30 Oct 2024 05:09:32 -0700 Subject: [PATCH 4/8] rename flag to readonly --- homeassistant/components/history_stats/config_flow.py | 6 +++--- homeassistant/helpers/schema_config_entry_flow.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index c25421bbbca6e..d0d4fbe60d72c 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -66,11 +66,11 @@ async def validate_options( ) DATA_SCHEMA_OPTIONS = vol.Schema( { - vol.Optional(CONF_ENTITY_ID, description={"disabled": True}): EntitySelector(), - vol.Optional(CONF_STATE, description={"disabled": True}): TextSelector( + vol.Optional(CONF_ENTITY_ID, description={"readonly": True}): EntitySelector(), + vol.Optional(CONF_STATE, description={"readonly": True}): TextSelector( TextSelectorConfig(multiple=True) ), - vol.Optional(CONF_TYPE, description={"disabled": True}): SelectSelector( + vol.Optional(CONF_TYPE, description={"readonly": True}): SelectSelector( SelectSelectorConfig( options=CONF_TYPE_KEYS, mode=SelectSelectorMode.DROPDOWN, diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index a08d2a59493d7..f4a9d79ab4b32 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -216,7 +216,7 @@ def _update_and_remove_omitted_optional_keys( ) and not ( # don't remove readonly keys - key.description and key.description.get("disabled") + key.description and key.description.get("readonly") ) ): # Key not present, delete keys old value (if present) too From 4d3aa45b65d48ba4a5e4ad918ef7423014e1013c Mon Sep 17 00:00:00 2001 From: karwosts <karwosts@gmail.com> Date: Sat, 23 Nov 2024 06:03:27 -0800 Subject: [PATCH 5/8] rename to read_only --- homeassistant/components/history_stats/config_flow.py | 6 +++--- homeassistant/helpers/schema_config_entry_flow.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index d0d4fbe60d72c..1c3c2e20f9185 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -66,11 +66,11 @@ async def validate_options( ) DATA_SCHEMA_OPTIONS = vol.Schema( { - vol.Optional(CONF_ENTITY_ID, description={"readonly": True}): EntitySelector(), - vol.Optional(CONF_STATE, description={"readonly": True}): TextSelector( + vol.Optional(CONF_ENTITY_ID, description={"read_only": True}): EntitySelector(), + vol.Optional(CONF_STATE, description={"read_only": True}): TextSelector( TextSelectorConfig(multiple=True) ), - vol.Optional(CONF_TYPE, description={"readonly": True}): SelectSelector( + vol.Optional(CONF_TYPE, description={"read_only": True}): SelectSelector( SelectSelectorConfig( options=CONF_TYPE_KEYS, mode=SelectSelectorMode.DROPDOWN, diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index f4a9d79ab4b32..565d379393e82 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -215,8 +215,8 @@ def _update_and_remove_omitted_optional_keys( and not self._handler.show_advanced_options ) and not ( - # don't remove readonly keys - key.description and key.description.get("readonly") + # don't remove read_only keys + key.description and key.description.get("read_only") ) ): # Key not present, delete keys old value (if present) too From 958d76ff39c3cf1dd605b82b44080d75fda610d8 Mon Sep 17 00:00:00 2001 From: karwosts <karwosts@gmail.com> Date: Sat, 18 Jan 2025 17:23:25 +0000 Subject: [PATCH 6/8] Update to use read_only field as part of selector definition --- .../components/history_stats/config_flow.py | 12 ++++++--- homeassistant/data_entry_flow.py | 5 +--- .../helpers/schema_config_entry_flow.py | 2 +- homeassistant/helpers/selector.py | 25 ++++++++++++++----- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index 1c3c2e20f9185..96c8f319fbcac 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -18,6 +18,7 @@ DurationSelector, DurationSelectorConfig, EntitySelector, + EntitySelectorConfig, SelectSelector, SelectSelectorConfig, SelectSelectorMode, @@ -66,15 +67,18 @@ async def validate_options( ) DATA_SCHEMA_OPTIONS = vol.Schema( { - vol.Optional(CONF_ENTITY_ID, description={"read_only": True}): EntitySelector(), - vol.Optional(CONF_STATE, description={"read_only": True}): TextSelector( - TextSelectorConfig(multiple=True) + vol.Optional(CONF_ENTITY_ID): EntitySelector( + EntitySelectorConfig(read_only=True) ), - vol.Optional(CONF_TYPE, description={"read_only": True}): SelectSelector( + vol.Optional(CONF_STATE): TextSelector( + TextSelectorConfig(multiple=True, read_only=True) + ), + vol.Optional(CONF_TYPE): SelectSelector( SelectSelectorConfig( options=CONF_TYPE_KEYS, mode=SelectSelectorMode.DROPDOWN, translation_key=CONF_TYPE, + read_only=True, ) ), vol.Optional(CONF_START): TemplateSelector(), diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 55e6f8e5747b0..63baca56aebd3 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -694,10 +694,7 @@ def add_suggested_values_to_schema( ): # Copy the marker to not modify the flow schema new_key = copy.copy(key) - new_key.description = { - **(new_key.description or {}), - "suggested_value": suggested_values[key.schema], - } + new_key.description = {"suggested_value": suggested_values[key.schema]} schema[new_key] = val return vol.Schema(schema) diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 565d379393e82..331b3cad8829f 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -216,7 +216,7 @@ def _update_and_remove_omitted_optional_keys( ) and not ( # don't remove read_only keys - key.description and key.description.get("read_only") + data_schema.schema[key].config.get("read_only") ) ): # Key not present, delete keys old value (if present) too diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 025b8de88963c..3ee3b0c9c1237 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -131,7 +131,20 @@ def _validate_supported_features(supported_features: int | list[str]) -> int: return feature_mask -ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema( +BASE_SELECTOR_CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("read_only"): bool, + } +) + + +class BaseSelectorConfig(TypedDict, total=False): + """Class to common options of all selectors.""" + + read_only: bool + + +ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { # Integration that provided the entity vol.Optional("integration"): str, @@ -760,7 +773,7 @@ def __call__(self, data: Any) -> dict[str, float]: return cast(dict[str, float], data) -class EntitySelectorConfig(EntityFilterSelectorConfig, total=False): +class EntitySelectorConfig(BaseSelectorConfig, total=False): """Class to represent an entity selector config.""" exclude_entities: list[str] @@ -1139,7 +1152,7 @@ class SelectSelectorMode(StrEnum): DROPDOWN = "dropdown" -class SelectSelectorConfig(TypedDict, total=False): +class SelectSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a select selector config.""" options: Required[Sequence[SelectOptionDict] | Sequence[str]] @@ -1156,7 +1169,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): selector_type = "select" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Required("options"): vol.All(vol.Any([str], [select_option])), vol.Optional("multiple", default=False): cv.boolean, @@ -1292,7 +1305,7 @@ def __call__(self, data: Any) -> str: return template.template -class TextSelectorConfig(TypedDict, total=False): +class TextSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a text selector config.""" multiline: bool @@ -1327,7 +1340,7 @@ class TextSelector(Selector[TextSelectorConfig]): selector_type = "text" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("multiline", default=False): bool, vol.Optional("prefix"): str, From 4915f25576a91d9957dd501c6ba7ea72d998d403 Mon Sep 17 00:00:00 2001 From: karwosts <karwosts@gmail.com> Date: Sat, 18 Jan 2025 18:42:10 +0000 Subject: [PATCH 7/8] lint fix --- homeassistant/helpers/selector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 3ee3b0c9c1237..96bc962f0ff7c 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -160,7 +160,7 @@ class BaseSelectorConfig(TypedDict, total=False): ) -class EntityFilterSelectorConfig(TypedDict, total=False): +class EntityFilterSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a single entity selector config.""" integration: str @@ -773,7 +773,7 @@ def __call__(self, data: Any) -> dict[str, float]: return cast(dict[str, float], data) -class EntitySelectorConfig(BaseSelectorConfig, total=False): +class EntitySelectorConfig(EntityFilterSelectorConfig, total=False): """Class to represent an entity selector config.""" exclude_entities: list[str] From 58489d522e2897bd122697cec4f1402d210b0e83 Mon Sep 17 00:00:00 2001 From: karwosts <karwosts@gmail.com> Date: Sat, 18 Jan 2025 19:38:45 +0000 Subject: [PATCH 8/8] Fix test --- homeassistant/helpers/schema_config_entry_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 331b3cad8829f..93d9a3d06f11c 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -216,7 +216,8 @@ def _update_and_remove_omitted_optional_keys( ) and not ( # don't remove read_only keys - data_schema.schema[key].config.get("read_only") + isinstance(data_schema.schema[key], selector.Selector) + and data_schema.schema[key].config.get("read_only") ) ): # Key not present, delete keys old value (if present) too