Repository: home-assistant/core
Pull request #129456: Support readonly selectors in config_flows
Author: login: karwosts
Labels: name: Hacktoberfest
name: core
name: cla-signed
name: small-pr
description: PRs with less than 30 lines.
name: new-feature
name: integration: history_stats
name: has-tests
name: no-stale
name: Quality Scale: internal
Created: 2024-10-29T19:42:23Z | Merged: 2025-05-23T15:05:43Z

<!--
  You are amazing! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
-->
## Proposed change

A common complaint for a UI configured entity is that after the initial config flow, the inputs to that config flow are no longer visible, and you can't review how you configured it in the first place. 

As a solution to this I propose supporting readonly/disabled selectors in options flows, which can show the values chosen during config flow, but freeze them so that they cannot be changed. 

I've configured history_stats in this PR as a proof of concept of this flow.

A small [frontend change](https://github.com/home-assistant/frontend/pull/22592) is also required to support the way the disabled flag is passed to the form.

![image](https://github.com/user-attachments/assets/9cba9d73-3357-4035-870c-993edccc57fa)



## Type of change
<!--
  What type of change does your PR introduce to Home Assistant?
  NOTE: Please, check only 1! box!
  If your PR requires multiple boxes to be checked, you'll most likely need to
  split it into multiple PRs. This makes things easier and faster to code review.
-->

- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New integration (thank you!)
- [x] New feature (which adds functionality to an existing integration)
- [ ] Deprecation (breaking change to happen in the future)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests

## Additional information
<!--
  Details are important, and help maintainers processing your PR.
  Please be sure to fill out additional details, if applicable.
-->

- This PR fixes or closes issue: fixes #
- This PR is related to issue: 
- Link to documentation pull request: 

## Checklist
<!--
  Put an `x` in the boxes that apply. You can also fill these out after
  creating the PR. If you're unsure about any of them, don't hesitate to ask.
  We're here to help! This is simply a reminder of what we are going to look
  for before merging your code.
-->

- [x] The code change is tested and works locally.
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
- [x] There is no commented out code in this PR.
- [x] I have followed the [development checklist][dev-checklist]
- [x] I have followed the [perfect PR recommendations][perfect-pr]
- [x] The code has been formatted using Ruff (`ruff format homeassistant tests`)
- [ ] Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]

If the code communicates with devices, web services, or third-party tools:

- [ ] The [manifest file][manifest-docs] has all fields filled out correctly.  
      Updated and included derived files by running: `python3 -m script.hassfest`.
- [ ] New or updated dependencies have been added to `requirements_all.txt`.  
      Updated by running `python3 -m script.gen_requirements_all`.
- [ ] For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

<!--
  This project is very active and we have a high turnover of pull requests.

  Unfortunately, the number of incoming pull requests is higher than what our
  reviewers can review and merge so there is a long backlog of pull requests
  waiting for review. You can help here!
  
  By reviewing another pull request, you will help raise the code quality of
  that pull request and the final review will be faster. This way the general
  pace of pull request reviews will go up and your wait time will go down.
  
  When picking a pull request to review, try to choose one that hasn't yet
  been reviewed.

  Thanks for helping out!
-->

To help with the load of incoming pull requests:

- [ ] I have reviewed two other [open pull requests][prs] in this repository.

[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-desc+review%3Anone+-status%3Afailure

<!--
  Thank you for contributing <3

  Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr



Diff:
diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py
index 8dbca3b19393f0..96c8f319fbcac1 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,6 +67,20 @@ async def validate_options(
 )
 DATA_SCHEMA_OPTIONS = vol.Schema(
     {
+        vol.Optional(CONF_ENTITY_ID): EntitySelector(
+            EntitySelectorConfig(read_only=True)
+        ),
+        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(),
         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 e10a72f67422ad..7a33099cf9928e 100644
--- a/homeassistant/components/history_stats/strings.json
+++ b/homeassistant/components/history_stats/strings.json
@@ -26,11 +26,17 @@
       "options": {
         "description": "Read the documentation 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/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py
index af8c4c6402df5f..93d9a3d06f11c0 100644
--- a/homeassistant/helpers/schema_config_entry_flow.py
+++ b/homeassistant/helpers/schema_config_entry_flow.py
@@ -214,6 +214,11 @@ def _update_and_remove_omitted_optional_keys(
                         and key.description.get("advanced")
                         and not self._handler.show_advanced_options
                     )
+                    and not (
+                        # don't remove read_only keys
+                        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
                     values.pop(key.schema, None)
diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py
index f2c76d1d019548..2d7fd51cac7aeb 100644
--- a/homeassistant/helpers/selector.py
+++ b/homeassistant/helpers/selector.py
@@ -131,6 +131,19 @@ def _validate_supported_features(supported_features: int | list[str]) -> int:
     return feature_mask
 
 
+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 = vol.Schema(
     {
         # Integration that provided the entity
@@ -183,7 +196,7 @@ class DeviceFilterSelectorConfig(TypedDict, total=False):
     model_id: str
 
 
-class ActionSelectorConfig(TypedDict):
+class ActionSelectorConfig(BaseSelectorConfig):
     """Class to represent an action selector config."""
 
 
@@ -193,7 +206,7 @@ class ActionSelector(Selector[ActionSelectorConfig]):
 
     selector_type = "action"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: ActionSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -204,7 +217,7 @@ def __call__(self, data: Any) -> Any:
         return data
 
 
-class AddonSelectorConfig(TypedDict, total=False):
+class AddonSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent an addon selector config."""
 
     name: str
@@ -217,7 +230,7 @@ class AddonSelector(Selector[AddonSelectorConfig]):
 
     selector_type = "addon"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("name"): str,
             vol.Optional("slug"): str,
@@ -234,7 +247,7 @@ def __call__(self, data: Any) -> str:
         return addon
 
 
-class AreaSelectorConfig(TypedDict, total=False):
+class AreaSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent an area selector config."""
 
     entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
@@ -248,7 +261,7 @@ class AreaSelector(Selector[AreaSelectorConfig]):
 
     selector_type = "area"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("entity"): vol.All(
                 cv.ensure_list,
@@ -276,7 +289,7 @@ def __call__(self, data: Any) -> str | list[str]:
         return [vol.Schema(str)(val) for val in data]
 
 
-class AssistPipelineSelectorConfig(TypedDict, total=False):
+class AssistPipelineSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent an assist pipeline selector config."""
 
 
@@ -286,7 +299,7 @@ class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]):
 
     selector_type = "assist_pipeline"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: AssistPipelineSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -298,7 +311,7 @@ def __call__(self, data: Any) -> str:
         return pipeline
 
 
-class AttributeSelectorConfig(TypedDict, total=False):
+class AttributeSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent an attribute selector config."""
 
     entity_id: Required[str]
@@ -311,7 +324,7 @@ class AttributeSelector(Selector[AttributeSelectorConfig]):
 
     selector_type = "attribute"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Required("entity_id"): cv.entity_id,
             # hide_attributes is used to hide attributes in the frontend.
@@ -330,7 +343,7 @@ def __call__(self, data: Any) -> str:
         return attribute
 
 
-class BackupLocationSelectorConfig(TypedDict, total=False):
+class BackupLocationSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a backup location selector config."""
 
 
@@ -340,7 +353,7 @@ class BackupLocationSelector(Selector[BackupLocationSelectorConfig]):
 
     selector_type = "backup_location"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: BackupLocationSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -352,7 +365,7 @@ def __call__(self, data: Any) -> str:
         return name
 
 
-class BooleanSelectorConfig(TypedDict):
+class BooleanSelectorConfig(BaseSelectorConfig):
     """Class to represent a boolean selector config."""
 
 
@@ -362,7 +375,7 @@ class BooleanSelector(Selector[BooleanSelectorConfig]):
 
     selector_type = "boolean"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: BooleanSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -374,7 +387,7 @@ def __call__(self, data: Any) -> bool:
         return value
 
 
-class ColorRGBSelectorConfig(TypedDict):
+class ColorRGBSelectorConfig(BaseSelectorConfig):
     """Class to represent a color RGB selector config."""
 
 
@@ -384,7 +397,7 @@ class ColorRGBSelector(Selector[ColorRGBSelectorConfig]):
 
     selector_type = "color_rgb"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: ColorRGBSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -396,7 +409,7 @@ def __call__(self, data: Any) -> list[int]:
         return value
 
 
-class ColorTempSelectorConfig(TypedDict, total=False):
+class ColorTempSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a color temp selector config."""
 
     unit: ColorTempSelectorUnit
@@ -419,7 +432,7 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]):
 
     selector_type = "color_temp"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("unit", default=ColorTempSelectorUnit.MIRED): vol.All(
                 vol.Coerce(ColorTempSelectorUnit), lambda val: val.value
@@ -456,7 +469,7 @@ def __call__(self, data: Any) -> int:
         return value
 
 
-class ConditionSelectorConfig(TypedDict):
+class ConditionSelectorConfig(BaseSelectorConfig):
     """Class to represent an condition selector config."""
 
 
@@ -466,7 +479,7 @@ class ConditionSelector(Selector[ConditionSelectorConfig]):
 
     selector_type = "condition"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: ConditionSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -477,7 +490,7 @@ def __call__(self, data: Any) -> Any:
         return vol.Schema(cv.CONDITIONS_SCHEMA)(data)
 
 
-class ConfigEntrySelectorConfig(TypedDict, total=False):
+class ConfigEntrySelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a config entry selector config."""
 
     integration: str
@@ -489,7 +502,7 @@ class ConfigEntrySelector(Selector[ConfigEntrySelectorConfig]):
 
     selector_type = "config_entry"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("integration"): str,
         }
@@ -505,7 +518,7 @@ def __call__(self, data: Any) -> str:
         return config
 
 
-class ConstantSelectorConfig(TypedDict, total=False):
+class ConstantSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a constant selector config."""
 
     label: str
@@ -519,7 +532,7 @@ class ConstantSelector(Selector[ConstantSelectorConfig]):
 
     selector_type = "constant"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("label"): str,
             vol.Optional("translation_key"): cv.string,
@@ -546,7 +559,7 @@ class QrErrorCorrectionLevel(StrEnum):
     HIGH = "high"
 
 
-class QrCodeSelectorConfig(TypedDict, total=False):
+class QrCodeSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a QR code selector config."""
 
     data: str
@@ -560,7 +573,7 @@ class QrCodeSelector(Selector[QrCodeSelectorConfig]):
 
     selector_type = "qr_code"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Required("data"): str,
             vol.Optional("scale"): int,
@@ -580,7 +593,7 @@ def __call__(self, data: Any) -> Any:
         return self.config["data"]
 
 
-class ConversationAgentSelectorConfig(TypedDict, total=False):
+class ConversationAgentSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a conversation agent selector config."""
 
     language: str
@@ -592,7 +605,7 @@ class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]):
 
     selector_type = "conversation_agent"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("language"): str,
         }
@@ -608,7 +621,7 @@ def __call__(self, data: Any) -> str:
         return agent
 
 
-class CountrySelectorConfig(TypedDict, total=False):
+class CountrySelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a country selector config."""
 
     countries: list[str]
@@ -621,7 +634,7 @@ class CountrySelector(Selector[CountrySelectorConfig]):
 
     selector_type = "country"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("countries"): [str],
             vol.Optional("no_sort", default=False): cv.boolean,
@@ -642,7 +655,7 @@ def __call__(self, data: Any) -> Any:
         return country
 
 
-class DateSelectorConfig(TypedDict):
+class DateSelectorConfig(BaseSelectorConfig):
     """Class to represent a date selector config."""
 
 
@@ -652,7 +665,7 @@ class DateSelector(Selector[DateSelectorConfig]):
 
     selector_type = "date"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: DateSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -664,7 +677,7 @@ def __call__(self, data: Any) -> Any:
         return data
 
 
-class DateTimeSelectorConfig(TypedDict):
+class DateTimeSelectorConfig(BaseSelectorConfig):
     """Class to represent a date time selector config."""
 
 
@@ -674,7 +687,7 @@ class DateTimeSelector(Selector[DateTimeSelectorConfig]):
 
     selector_type = "datetime"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: DateTimeSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -686,7 +699,7 @@ def __call__(self, data: Any) -> Any:
         return data
 
 
-class DeviceSelectorConfig(DeviceFilterSelectorConfig, total=False):
+class DeviceSelectorConfig(BaseSelectorConfig, DeviceFilterSelectorConfig, total=False):
     """Class to represent a device selector config."""
 
     entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
@@ -700,7 +713,9 @@ class DeviceSelector(Selector[DeviceSelectorConfig]):
 
     selector_type = "device"
 
-    CONFIG_SCHEMA = DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA.extend(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
+        DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA.schema
+    ).extend(
         {
             vol.Optional("multiple", default=False): cv.boolean,
             vol.Optional("filter"): vol.All(
@@ -724,7 +739,7 @@ def __call__(self, data: Any) -> str | list[str]:
         return [vol.Schema(str)(val) for val in data]
 
 
-class DurationSelectorConfig(TypedDict, total=False):
+class DurationSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a duration selector config."""
 
     enable_day: bool
@@ -738,7 +753,7 @@ class DurationSelector(Selector[DurationSelectorConfig]):
 
     selector_type = "duration"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             # Enable day field in frontend. A selection with `days` set is allowed
             # even if `enable_day` is not set
@@ -763,7 +778,7 @@ def __call__(self, data: Any) -> dict[str, float]:
         return cast(dict[str, float], data)
 
 
-class EntitySelectorConfig(EntityFilterSelectorConfig, total=False):
+class EntitySelectorConfig(BaseSelectorConfig, EntityFilterSelectorConfig, total=False):
     """Class to represent an entity selector config."""
 
     exclude_entities: list[str]
@@ -778,7 +793,9 @@ class EntitySelector(Selector[EntitySelectorConfig]):
 
     selector_type = "entity"
 
-    CONFIG_SCHEMA = ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.extend(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
+        ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.schema
+    ).extend(
         {
             vol.Optional("exclude_entities"): [str],
             vol.Optional("include_entities"): [str],
@@ -824,7 +841,7 @@ def validate(e_or_u: str) -> str:
         return cast(list, vol.Schema([validate])(data))  # Output is a list
 
 
-class FloorSelectorConfig(TypedDict, total=False):
+class FloorSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent an floor selector config."""
 
     entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
@@ -838,7 +855,7 @@ class FloorSelector(Selector[FloorSelectorConfig]):
 
     selector_type = "floor"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("entity"): vol.All(
                 cv.ensure_list,
@@ -866,7 +883,7 @@ def __call__(self, data: Any) -> str | list[str]:
         return [vol.Schema(str)(val) for val in data]
 
 
-class IconSelectorConfig(TypedDict, total=False):
+class IconSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent an icon selector config."""
 
     placeholder: str
@@ -878,7 +895,7 @@ class IconSelector(Selector[IconSelectorConfig]):
 
     selector_type = "icon"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {vol.Optional("placeholder"): str}
         # Frontend also has a fallbackPath option, this is not used by core
     )
@@ -893,7 +910,7 @@ def __call__(self, data: Any) -> str:
         return icon
 
 
-class LabelSelectorConfig(TypedDict, total=False):
+class LabelSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a label selector config."""
 
     multiple: bool
@@ -905,7 +922,7 @@ class LabelSelector(Selector[LabelSelectorConfig]):
 
     selector_type = "label"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("multiple", default=False): cv.boolean,
         }
@@ -925,7 +942,7 @@ def __call__(self, data: Any) -> str | list[str]:
         return [vol.Schema(str)(val) for val in data]
 
 
-class LanguageSelectorConfig(TypedDict, total=False):
+class LanguageSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent an language selector config."""
 
     languages: list[str]
@@ -939,7 +956,7 @@ class LanguageSelector(Selector[LanguageSelectorConfig]):
 
     selector_type = "language"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("languages"): [str],
             vol.Optional("native_name", default=False): cv.boolean,
@@ -959,7 +976,7 @@ def __call__(self, data: Any) -> str:
         return language
 
 
-class LocationSelectorConfig(TypedDict, total=False):
+class LocationSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a location selector config."""
 
     radius: bool
@@ -972,7 +989,7 @@ class LocationSelector(Selector[LocationSelectorConfig]):
 
     selector_type = "location"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {vol.Optional("radius"): bool, vol.Optional("icon"): str}
     )
     DATA_SCHEMA = vol.Schema(
@@ -993,7 +1010,7 @@ def __call__(self, data: Any) -> dict[str, float]:
         return location
 
 
-class MediaSelectorConfig(TypedDict):
+class MediaSelectorConfig(BaseSelectorConfig):
     """Class to represent a media selector config."""
 
 
@@ -1003,7 +1020,7 @@ class MediaSelector(Selector[MediaSelectorConfig]):
 
     selector_type = "media"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
     DATA_SCHEMA = vol.Schema(
         {
             # Although marked as optional in frontend, this field is required
@@ -1026,7 +1043,7 @@ def __call__(self, data: Any) -> dict[str, float]:
         return media
 
 
-class NumberSelectorConfig(TypedDict, total=False):
+class NumberSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a number selector config."""
 
     min: float
@@ -1061,7 +1078,7 @@ class NumberSelector(Selector[NumberSelectorConfig]):
     selector_type = "number"
 
     CONFIG_SCHEMA = vol.All(
-        vol.Schema(
+        BASE_SELECTOR_CONFIG_SCHEMA.extend(
             {
                 vol.Optional("min"): vol.Coerce(float),
                 vol.Optional("max"): vol.Coerce(float),
@@ -1096,7 +1113,7 @@ def __call__(self, data: Any) -> float:
         return value
 
 
-class ObjectSelectorConfig(TypedDict):
+class ObjectSelectorConfig(BaseSelectorConfig):
     """Class to represent an object selector config."""
 
 
@@ -1106,7 +1123,7 @@ class ObjectSelector(Selector[ObjectSelectorConfig]):
 
     selector_type = "object"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: ObjectSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -1142,7 +1159,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]]
@@ -1159,7 +1176,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,
@@ -1199,14 +1216,14 @@ def __call__(self, data: Any) -> Any:
         return [parent_schema(vol.Schema(str)(val)) for val in data]
 
 
-class TargetSelectorConfig(TypedDict, total=False):
+class TargetSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent a target selector config."""
 
     entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
     device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
 
 
-class StateSelectorConfig(TypedDict, total=False):
+class StateSelectorConfig(BaseSelectorConfig, total=False):
     """Class to represent an state selector config."""
 
     entity_id: Required[str]
@@ -1218,7 +1235,7 @@ class StateSelector(Selector[StateSelectorConfig]):
 
     selector_type = "state"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Required("entity_id"): cv.entity_id,
             # The attribute to filter on, is currently deliberately not
@@ -1248,7 +1265,7 @@ class TargetSelector(Selector[TargetSelectorConfig]):
 
     selector_type = "target"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("entity"): vol.All(
                 cv.ensure_list,
@@ -1273,7 +1290,7 @@ def __call__(self, data: Any) -> dict[str, list[str]]:
         return target
 
 
-class TemplateSelectorConfig(TypedDict):
+class TemplateSelectorConfig(BaseSelectorConfig):
     """Class to represent an template selector config."""
 
 
@@ -1283,7 +1300,7 @@ class TemplateSelector(Selector[TemplateSelectorConfig]):
 
     selector_type = "template"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: TemplateSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -1295,7 +1312,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
@@ -1330,7 +1347,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,
@@ -1359,7 +1376,7 @@ def __call__(self, data: Any) -> str | list[str]:
         return [vol.Schema(str)(val) for val in data]
 
 
-class ThemeSelectorConfig(TypedDict):
+class ThemeSelectorConfig(BaseSelectorConfig):
     """Class to represent a theme selector config."""
 
 
@@ -1369,7 +1386,7 @@ class ThemeSelector(Selector[ThemeSelectorConfig]):
 
     selector_type = "theme"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             vol.Optional("include_default", default=False): cv.boolean,
         }
@@ -1385,7 +1402,7 @@ def __call__(self, data: Any) -> str:
         return theme
 
 
-class TimeSelectorConfig(TypedDict):
+class TimeSelectorConfig(BaseSelectorConfig):
     """Class to represent a time selector config."""
 
 
@@ -1395,7 +1412,7 @@ class TimeSelector(Selector[TimeSelectorConfig]):
 
     selector_type = "time"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: TimeSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -1407,7 +1424,7 @@ def __call__(self, data: Any) -> str:
         return cast(str, data)
 
 
-class TriggerSelectorConfig(TypedDict):
+class TriggerSelectorConfig(BaseSelectorConfig):
     """Class to represent an trigger selector config."""
 
 
@@ -1417,7 +1434,7 @@ class TriggerSelector(Selector[TriggerSelectorConfig]):
 
     selector_type = "trigger"
 
-    CONFIG_SCHEMA = vol.Schema({})
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
 
     def __init__(self, config: TriggerSelectorConfig | None = None) -> None:
         """Instantiate a selector."""
@@ -1428,7 +1445,7 @@ def __call__(self, data: Any) -> Any:
         return vol.Schema(cv.TRIGGER_SCHEMA)(data)
 
 
-class FileSelectorConfig(TypedDict):
+class FileSelectorConfig(BaseSelectorConfig):
     """Class to represent a file selector config."""
 
     accept: str  # required
@@ -1440,7 +1457,7 @@ class FileSelector(Selector[FileSelectorConfig]):
 
     selector_type = "file"
 
-    CONFIG_SCHEMA = vol.Schema(
+    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
         {
             # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept
             vol.Required("accept"): str,


Reviews:
user: login: gjohansson-ST
body: While I didn't receive any response on my proposal for a read only selector (which is guess should close as this is likely a better suggestion anyhow), that and this proposal share the same issue eg. They should be only for display and not sent back to core (the helper should not need to consider fields which are there only for display purpose I think).
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-29T20:01:52Z
user: login: karwosts
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-29T20:26:51Z
user: login: karwosts
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-29T21:45:37Z
user: login: MindFreeze
state: APPROVED
author_association: MEMBER
submitted_at: 2024-11-23T05:15:06Z
user: login: gjohansson-ST
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-23T11:09:19Z
user: login: karwosts
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-23T14:54:55Z
user: login: MartinHjelmare
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T14:04:20Z
user: login: gjohansson-ST
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T12:17:06Z
user: login: karwosts
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T12:23:47Z
user: login: gjohansson-ST
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T13:21:10Z
user: login: gjohansson-ST
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T14:09:50Z
user: login: MartinHjelmare
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T14:22:47Z
user: login: karwosts
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T14:24:58Z
user: login: MartinHjelmare
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T15:04:08Z

Review comments:
diff_hunk: @@ -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",
path: tests/components/history_stats/test_config_flow.py
user: login: gjohansson-ST
body: Read only fields should not be set.
created_at: 2024-10-29T19:59:04Z
author_association: MEMBER
diff_hunk: @@ -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",
path: tests/components/history_stats/test_config_flow.py
user: login: karwosts
body: Yeah I was kind of disappointed I had to do this to pass the testing, was initially hoping it would be less invasive.

If I mark the disabled fields as vol.Required, I have to pass them into this function. 

If I mark the disabled fields as vol.Optional, (and don't pass them in here), they don't get passed through the config+options flow and the assert fails at the end. 

Will have to think about this some more. 


created_at: 2024-10-29T20:26:51Z
author_association: MEMBER
diff_hunk: @@ -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",
path: tests/components/history_stats/test_config_flow.py
user: login: karwosts
body: Ok trying a bit different approach.

- Make readonly fields vol.Optional instead of vol.Required.
- Change frontend to delete the readonly fields before sending data back to backend.
- Change backend to not delete optional/readonly fields when they are not provided by the step flow (which they never will be provided).

With this can revert all the changes to the test. 
created_at: 2024-10-29T21:45:37Z
author_association: MEMBER
diff_hunk: @@ -66,6 +66,17 @@ async def validate_options(
 )
 DATA_SCHEMA_OPTIONS = vol.Schema(
     {
+        vol.Optional(CONF_ENTITY_ID, description={"readonly": True}): EntitySelector(),
path: homeassistant/components/history_stats/config_flow.py
user: login: gjohansson-ST
body: See https://github.com/home-assistant/frontend/pull/22592#discussion_r1855173099
created_at: 2024-11-23T11:09:19Z
author_association: MEMBER
diff_hunk: @@ -66,6 +66,17 @@ async def validate_options(
 )
 DATA_SCHEMA_OPTIONS = vol.Schema(
     {
+        vol.Optional(CONF_ENTITY_ID, description={"readonly": True}): EntitySelector(),
path: homeassistant/components/history_stats/config_flow.py
user: login: karwosts
body: renamed to `read_only`
created_at: 2024-11-23T14:54:55Z
author_association: MEMBER
diff_hunk: @@ -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
+
+
path: homeassistant/helpers/selector.py
user: login: gjohansson-ST
body: Needs to be added to all the selectors I guess, not only the few below already done.
created_at: 2025-05-23T12:17:06Z
author_association: MEMBER
diff_hunk: @@ -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
+
+
path: homeassistant/helpers/selector.py
user: login: karwosts
body: Yes this was limited to just the requirements of this specific PR. I can add the rest in a follow-up?
created_at: 2025-05-23T12:23:47Z
author_association: MEMBER
diff_hunk: @@ -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
+
+
path: homeassistant/helpers/selector.py
user: login: gjohansson-ST
body: No, I think we should add all in the same PR
created_at: 2025-05-23T13:21:10Z
author_association: MEMBER
diff_hunk: @@ -131,6 +131,19 @@ def _validate_supported_features(supported_features: int | list[str]) -> int:
     return feature_mask
 
 
+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 = vol.Schema(
path: homeassistant/helpers/selector.py
user: login: MartinHjelmare
body: Is there a reason we don't update `ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA`?
created_at: 2025-05-23T14:22:40Z
author_association: MEMBER
diff_hunk: @@ -131,6 +131,19 @@ def _validate_supported_features(supported_features: int | list[str]) -> int:
     return feature_mask
 
 
+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 = vol.Schema(
path: homeassistant/helpers/selector.py
user: login: karwosts
body: ENTITY_FILTER is not a selector. It's a schema from which other selectors are partially derived from. 

To add read_only here would also add it to all the children of EntitySelector.filters, where it wouldn't make sense to exist. So I want to add read_only to the EntitySelector itself, but not EntityFilterSelector.

Probably due to the way that entity filter has multiple legacy formats.

See entity selector definition:

```

class EntitySelectorConfig(BaseSelectorConfig, EntityFilterSelectorConfig, total=False):
    """Class to represent an entity selector config."""

    exclude_entities: list[str]
    include_entities: list[str]
    multiple: bool
    filter: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
 ```
 
 ```
 @SELECTORS.register("entity")
class EntitySelector(Selector[EntitySelectorConfig]):
    """Selector of a single or list of entities."""

    selector_type = "entity"

    CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(  <-- add read_only to EntitySelector
        ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.schema
    ).extend(
        {
            vol.Optional("exclude_entities"): [str],
            vol.Optional("include_entities"): [str],
            vol.Optional("multiple", default=False): cv.boolean,
            vol.Optional("filter"): vol.All(
                cv.ensure_list,
                [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA], <-- but don't want read_only here
            ),
        }
    )
 ```
created_at: 2025-05-23T14:24:58Z
author_association: MEMBER
diff_hunk: @@ -131,6 +131,19 @@ def _validate_supported_features(supported_features: int | list[str]) -> int:
     return feature_mask
 
 
+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 = vol.Schema(
path: homeassistant/helpers/selector.py
user: login: MartinHjelmare
body: Ok
created_at: 2025-05-23T15:04:08Z
author_association: MEMBER

PR comments:
user: login: MindFreeze
created_at: 2024-10-30T08:13:21Z
body: > While I didn't receive any response on my proposal for a read only selector (which is guess should close as this is likely a better suggestion anyhow), that and this proposal share the same issue eg. They should be only for display and not sent back to core (the helper should not need to consider fields which are there only for display purpose I think).

Yeah, `readonly` sounds better. `disabled` is a consequence of the fact that the field is readonly and is usually something you toggle. Read only makes it clear that we don't need to process this data on save
author_association: MEMBER
user: login: epenet
created_at: 2024-10-30T13:16:56Z
body: > > While I didn't receive any response on my proposal for a read only selector (which is guess should close as this is likely a better suggestion anyhow), that and this proposal share the same issue eg. They should be only for display and not sent back to core (the helper should not need to consider fields which are there only for display purpose I think).
> 
> Yeah, `readonly` sounds better. `disabled` is a consequence of the fact that the field is readonly and is usually something you toggle. Read only makes it clear that we don't need to process this data on save

Small nit: I would rename `readonly` to `displayonly` as with `readonly` I would expect to have them to make the round trip and be sent back to core.
author_association: CONTRIBUTOR
user: login: karwosts
created_at: 2024-10-30T14:55:15Z
body: > Small nit: I would rename `readonly` to `displayonly` as with `readonly` I would expect to have them to make the round trip and be sent back to core.

I'll let a little more time to accumulate for further opinions, then I will change this if no other objections.
author_association: MEMBER
user: login: tetele
created_at: 2024-10-30T14:59:01Z
body: > Small nit: I would rename `readonly` to `displayonly` as with `readonly` I would expect to have them to make the round trip and be sent back to core.

I've never heard the term "display only", whereas I know exactly what "read only" means. That said, this option is only in the code so maybe from a core perspective it makes more sense.
author_association: CONTRIBUTOR
user: login: MindFreeze
created_at: 2024-10-30T14:59:42Z
body: > > > While I didn't receive any response on my proposal for a read only selector (which is guess should close as this is likely a better suggestion anyhow), that and this proposal share the same issue eg. They should be only for display and not sent back to core (the helper should not need to consider fields which are there only for display purpose I think).
> > 
> > 
> > Yeah, `readonly` sounds better. `disabled` is a consequence of the fact that the field is readonly and is usually something you toggle. Read only makes it clear that we don't need to process this data on save
> 
> Small nit: I would rename `readonly` to `displayonly` as with `readonly` I would expect to have them to make the round trip and be sent back to core.

I don't agree as `readonly` seems clear enough to me but `displayonly` is also fine. I just wanted it to be differentiated from the existing disabled option on the frontend.
author_association: MEMBER
user: login: joostlek
created_at: 2025-01-16T12:25:24Z
body: So we discussed this PR in the core team and we had the following remarks:
- Concept is good
- Developer documentation should make it clear with examples when a read only selector is useful
- The PR doesn't do what the title suggests, the PR adds a flag to voluptuous. Ideally we add the read_only flag to the SelectorConfig
- Either frontend should not be allowed to send data for a read_only selector, or the data must match. In either case, this must be checked. We think frontend should decide which option is better. Ideally we align with what the const selector does.
author_association: MEMBER
user: login: MindFreeze
created_at: 2025-01-16T12:43:07Z
body: Frontend shouldn't be allowed to send read only or core can just discard it. Checking if it matches is not worth it.
author_association: MEMBER
user: login: karwosts
created_at: 2025-01-18T20:25:16Z
body: > The PR doesn't do what the title suggests, the PR adds a flag to voluptuous. Ideally we add the read_only flag to the SelectorConfig

I've changed such that read_only is a flag to the selector config. Hope I have understood what you were asking for. 

```
        vol.Optional(CONF_STATE): TextSelector(
            TextSelectorConfig(multiple=True, read_only=True)
        ),
```

> Either frontend should not be allowed to send data for a read_only selector, or the data must match. In either case, this must be checked. We think frontend should decide which option is better. Ideally we align with what the const selector does.

Current behavior of frontend is that it does not send data for read_only selector. 
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-19T22:05:26Z
body: There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want to keep it open. Also, please rebase your PR onto the latest dev branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
author_association: NONE
user: login: karwosts
created_at: 2025-03-19T22:38:16Z
body: Unstale (waiting for review)
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-05-19T00:28:35Z
body: There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want to keep it open. Also, please rebase your PR onto the latest dev branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
author_association: NONE
user: login: MindFreeze
created_at: 2025-05-19T04:56:55Z
body: @karwosts the frontend PR was closed. Should we close this one too?

author_association: MEMBER
user: login: karwosts
created_at: 2025-05-19T04:59:56Z
body: I'd still like to pursue this. The core side keeps changing so I stopped trying to keep frontend up to date for now. 

I'll just wait to see if this settles, and then make a new frontend that works with whatever solution this ends up at (if any). 
author_association: MEMBER
user: login: karwosts
created_at: 2025-05-20T15:00:23Z
body: @MindFreeze - I think since you added the no-stale label this is now permanently labelled as stale 😅 
author_association: MEMBER
user: login: MartinHjelmare
created_at: 2025-05-22T06:16:25Z
body: The linked frontend PR was closed. What's the status here?

Please link appropriate PRs for frontend, if that is still needed, and documentation. Our selectors are documented in the user docs, but we should also add something to the dev docs eg an example of a config flow form with this feature used.
author_association: MEMBER
user: login: karwosts
created_at: 2025-05-22T13:48:57Z
body: > The linked frontend PR was closed. What's the status here?

Sorry I let it get closed while waiting for the review to finish here. I have reopened/refreshed it. 
https://github.com/home-assistant/frontend/pull/22592

This is still awaiting an approving review. 

I will write docs once someone gives me a nod that this is an acceptable implementation.

I don't think I want to document this in the user-facing blueprint selector documentation, as this has no relevance to blueprints, and picking up this read_only key is only done in the config flow step. But the config flow docs could be updated. 
author_association: MEMBER
user: login: MartinHjelmare
created_at: 2025-05-22T14:02:30Z
body: I think the nod of approval was already given here:
https://github.com/home-assistant/core/pull/129456#issuecomment-2595445135
author_association: MEMBER
user: login: MartinHjelmare
created_at: 2025-05-22T14:05:00Z
body: Can be merged when docs PR is linked and frontend PR is linked and approved.
author_association: MEMBER
user: login: karwosts
created_at: 2025-05-23T12:11:25Z
body: Docs linked:  https://github.com/home-assistant/developers.home-assistant/pull/2680
Frontend linked and approved: https://github.com/home-assistant/frontend/pull/22592

Looks like this is ready to go. 
author_association: MEMBER