Summary
The Display settings step's "Timeout duration" field (screen_timeout in config_flow.py, DISPLAY_SCHEMA) is a free-text TextSelector with no format validation. Applying a policy with a malformed value fails with a raw googleapiclient.errors.HttpError traceback instead of a friendly form error.
Reproduction
- Open the integration's options flow → Display settings.
- Enter a value in "Timeout duration" that doesn't match Google's protobuf
Duration string format (e.g. a bare 0 instead of 0s).
- Apply the policy.
Result
googleapiclient.errors.HttpError: <HttpError 400 ...
"Invalid value at 'policy.display_settings.screen_timeout_settings.screen_timeout' ...
Illegal duration format; duration must end with 's'">
Traceback: config_flow.py async_step_apply_policy → api.py async_set_policy → _set_policy → googleapiclient http.execute().
Root cause
I traced the field end-to-end and confirmed there is no string transformation anywhere in the integration — async_step_display does self._options.update(user_input) verbatim, build_policy_from_options does timeout["screenTimeout"] = opts["screen_timeout"] verbatim, and api.py forwards the policy body unchanged to the Google Android Management API. So whatever string is typed into the field is sent to Google exactly as-is, with no local validation first. If the field doesn't end in s (or otherwise doesn't match Google's Duration string format), the failure only surfaces as an unhandled 400 from Google, logged via _LOGGER.exception("Failed to apply policy '%s'", policy_id) and surfaced to the user as a generic apply_failed form error — the underlying "must end with 's'" detail isn't shown in the UI at all.
The field's data_description hint text ("Duration string, e.g. 220s, 60s, 0s (never)") only appears as help text next to the field — it's not a placeholder/prefill, so it's easy to type a bare number by mistake and only find out via a stack trace in the HA log.
Suggested fix
Add a vol.Match (or equivalent) validator to the screen_timeout field in DISPLAY_SCHEMA, e.g.:
vol.Optional("screen_timeout", default="220s"): vol.All(_text(), vol.Match(r"^\d+(\.\d+)?s$")),
so a malformed duration is rejected locally with a clear form error instead of round-tripping to Google's API first.
Happy to send a PR for this if useful — just wanted to confirm the root cause here first.
Summary
The Display settings step's "Timeout duration" field (
screen_timeoutinconfig_flow.py,DISPLAY_SCHEMA) is a free-textTextSelectorwith no format validation. Applying a policy with a malformed value fails with a rawgoogleapiclient.errors.HttpErrortraceback instead of a friendly form error.Reproduction
Durationstring format (e.g. a bare0instead of0s).Result
Traceback:
config_flow.pyasync_step_apply_policy→api.pyasync_set_policy→_set_policy→googleapiclienthttp.execute().Root cause
I traced the field end-to-end and confirmed there is no string transformation anywhere in the integration —
async_step_displaydoesself._options.update(user_input)verbatim,build_policy_from_optionsdoestimeout["screenTimeout"] = opts["screen_timeout"]verbatim, andapi.pyforwards the policy body unchanged to the Google Android Management API. So whatever string is typed into the field is sent to Google exactly as-is, with no local validation first. If the field doesn't end ins(or otherwise doesn't match Google'sDurationstring format), the failure only surfaces as an unhandled 400 from Google, logged via_LOGGER.exception("Failed to apply policy '%s'", policy_id)and surfaced to the user as a genericapply_failedform error — the underlying "must end with 's'" detail isn't shown in the UI at all.The field's
data_descriptionhint text ("Duration string, e.g. 220s, 60s, 0s (never)") only appears as help text next to the field — it's not a placeholder/prefill, so it's easy to type a bare number by mistake and only find out via a stack trace in the HA log.Suggested fix
Add a
vol.Match(or equivalent) validator to thescreen_timeoutfield inDISPLAY_SCHEMA, e.g.:so a malformed duration is rejected locally with a clear form error instead of round-tripping to Google's API first.
Happy to send a PR for this if useful — just wanted to confirm the root cause here first.