Skip to content

[CI check only] Integration test: all features on top of main - #13

Draft
mikopp wants to merge 25 commits into
mainfrom
integration/all-features
Draft

mikopp wants to merge 25 commits into
mainfrom
integration/all-features

Conversation

@mikopp

@mikopp mikopp commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Not for merging — this exists only to trigger CI (pull_request has no branch filter, push doesn't fire for this branch name) so the six feature branches can be validated together before any of them go upstream.

Contains, merged on top of main:

  • feat/mode-default-reconciliation
  • feat/docs-attributes-variables
  • feat/legacy-modes-alias
  • feat/ci-pin-actions
  • feat/integration-tests (temporarily carries the name-override fix so its own suite can run)
  • feat/fix-name-override

Close without merging once CI results are checked.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy


Generated by Claude Code

_attr_preset_mode/_attr_fan_mode/_attr_swing_mode were initialised to
hardcoded DEFAULT_* constants (DEFAULT_PRESET_MODE = "comfort",
DEFAULT_FAN_MODE = "low", DEFAULT_SWING_MODE = "off") without ever
validating them against the configured modes lists. When a configured
list didn't include the default, the entity reported -- and on restart
tried to restore -- an attribute value Home Assistant rejects with
"attribute 'X' returned invalid value".

Reconcile each attribute to None when the hardcoded default is absent
from the configured list. hvac_mode is intentionally excluded: it is
the entity STATE and cannot be None. DEFAULT_HVAC_MODE is HVACMode.OFF,
and HA itself requires OFF in hvac_modes (entities omitting OFF are
rejected at registration due to TURN_ON/TURN_OFF feature enforcement),
so the "invalid default" scenario cannot arise for a loadable entity.

Originally found and fixed in mikopp/hass-template-climate while
building an integration test suite against a real Home Assistant;
split out here as its own PR per litinoveweedle#35.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
The modern template-entity schema (make_template_entity_common_schema)
already accepts attributes: and variables:, but the README never
documented either. Add both to the configuration table with worked
examples, and add a Deprecated Keys section listing the legacy
template-entity keys that rewrite_legacy_to_modern_config() already
rewrites on load (availability_template, icon_template,
entity_picture_template, friendly_name, value_template), plus the
removed entity_id option.

Docs only; no code changes. Split out of
litinoveweedle#35.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
jcwillox-era configs used 'modes'; this fork renamed it to 'hvac_modes'
as a breaking change. Accept the old key again and rewrite it inside
rewrite_legacy_to_modern_config(), alongside the other legacy-key
rewrites, with the same startup deprecation warning naming the
affected entity.

hvac_modes' schema default (DEFAULT_HVAC_MODE_LIST) is moved out of
PLATFORM_SCHEMA and applied inside the rewrite instead: with the
default staying on the schema, hvac_modes would always be present by
the time the rewrite runs, so it could never tell a user-supplied
value apart from an applied default -- and 'modes' would never
actually take effect.

Split out of litinoveweedle#35.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
actions/checkout@v2, psf/black@stable, home-assistant/actions/hassfest@master,
and hacs/action@main were all pinned to floating branch/tag refs, so a
compromised or rewritten ref would run in CI and in the release
workflow (which has repo write access via GITHUB_TOKEN) without any
change to this repo. Pin each to its current commit SHA, keeping the
original ref as a trailing comment, and add dependabot.yml to open a
weekly PR bumping any SHA whose ref has moved.

Two of the four SHAs used in litinoveweedle#35
had already moved since that PR (actions/checkout's v2 tag and
home-assistant/actions' master branch); psf/black@stable and
hacs/action@main had not. Re-verified all four against their current
refs before pinning here.

Split out of litinoveweedle#35 as its own PR,
since CI-pipeline changes were flagged there as bundled in with
unrelated work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
Boots a real Home Assistant container (via the HomeAssistant-Test-Harness
pytest plugin) and runs a climate_template scenario suite against it
over the REST/WebSocket API:

- airflow: temperature/humidity control, dynamic min/max/step templates
- presets: preset-profile boiler setup with independent heating circuits
- e2m: self-attribute-read case (state written before the action script
  that reads it back runs)
- mode_init: a static-only / template-only / static+template matrix for
  preset_mode, fan_mode, and swing_mode initialization -- this is the
  regression test for the invalid-default-value bug fixed separately
  ('Fix invalid default preset/fan/swing mode on entity startup')
- roommind: a multi-room setup with per-room overrides

Adds .github/workflows/test-integration.yaml, running the suite as a
matrix against hacs.json's minimum-supported HA version and the latest
stable release, on every push and pull request. Also appends
.pytest_cache/ and /.venv/ to .gitignore (existing entries kept as-is).

Squashed from the incremental commits that built this suite in
mikopp/hass-template-climate, including the intermediate test-only
fixes. Split out of litinoveweedle#35, which
predated this suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
ha-integration-test-harness now requires Python >=3.14.2, but the
workflow was still setting up 3.12, so pip install failed with:
  ERROR: Package 'ha-integration-test-harness' requires a different
  Python: 3.12.14 not in '>=3.14.2'

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRC8Ceijjcdcks6LPTzA6K
TemplateClimate.__init__ set self._attr_name from the legacy
'friendly_name' config key, immediately after super().__init__() had
already correctly resolved it from the modern 'name' key
(TemplateEntity's own __init__ renders CONF_NAME -- static names
immediately, templated names via its tracker -- and sets
self._attr_name from that).

rewrite_legacy_to_modern_config() already rewrites 'friendly_name' to
'name' before the entity is constructed, so config.get(CONF_FRIENDLY_NAME)
is always None by the time this line runs -- for both legacy and modern
configs. Every entity's name, regardless of its configured 'name',
silently collapsed to the hardcoded default "Template Climate".

On a fresh install (no entity-registry history yet) this also produces
colliding entity_ids: Home Assistant slugs the initial entity_id from
the display name, so every climate_template entity in a config lands on
climate.template_climate, climate.template_climate_2, and so on,
instead of the name/unique_id-derived id a user would expect.

Found via mikopp/hass-template-climate's integration test suite: every
one of its 60 tests failed with 'Entity climate.<x> not found' when run
against this code, because none of the expected entity_ids were ever
created.

Removing the overriding line restores TemplateEntity's own correct
handling; verified no other code in __init__ depends on this line
having run (self._attr_name is set by super().__init__() before this
point).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
TemplateClimate.__init__ set self._attr_name from the legacy
'friendly_name' config key, immediately after super().__init__() had
already correctly resolved it from the modern 'name' key
(TemplateEntity's own __init__ renders CONF_NAME -- static names
immediately, templated names via its tracker -- and sets
self._attr_name from that).

rewrite_legacy_to_modern_config() already rewrites 'friendly_name' to
'name' before the entity is constructed, so config.get(CONF_FRIENDLY_NAME)
is always None by the time this line runs -- for both legacy and modern
configs. Every entity's name, regardless of its configured 'name',
silently collapsed to the hardcoded default "Template Climate".

On a fresh install (no entity-registry history yet) this also produces
colliding entity_ids: Home Assistant slugs the initial entity_id from
the display name, so every climate_template entity in a config lands on
climate.template_climate, climate.template_climate_2, and so on,
instead of the name/unique_id-derived id a user would expect.

Found via mikopp/hass-template-climate's integration test suite: every
one of its 60 tests failed with 'Entity climate.<x> not found' when run
against this code, because none of the expected entity_ids were ever
created.

Removing the overriding line restores TemplateEntity's own correct
handling; verified no other code in __init__ depends on this line
having run (self._attr_name is set by super().__init__() before this
point).

Cherry-picked from feat/fix-name-override, which carries this fix as
its own separate PR to litinoveweedle. It is temporarily duplicated
here only so this branch's own CI (test-integration.yaml) can run
end-to-end; drop this commit from this branch once the fix PR merges
upstream, and rebase before opening the integration-tests PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
…into integration/all-features

# Conflicts:
#	custom_components/climate_template/Changelog.md
…tegration/all-features

# Conflicts:
#	custom_components/climate_template/Changelog.md
…ation/all-features

# Conflicts:
#	custom_components/climate_template/Changelog.md
…egration/all-features

# Conflicts:
#	custom_components/climate_template/Changelog.md
…egration/all-features

# Conflicts:
#	custom_components/climate_template/Changelog.md
_update_min_temp/_update_max_temp validated the incoming template value
via _validate_value(..., "target_temperature"), which bound-checks the
value against self._attr_min_temp/self._attr_max_temp -- the very
attribute the callback is trying to update. A new min_temp below the
entity's current min_temp (or a new max_temp above the current
max_temp) was rejected as out of range against its own stale value, so
min_temp_template/max_temp_template could only ever narrow the range
from its initial static/default value, never widen it.
_update_min_humidity/_update_max_humidity had the identical bug via
"target_humidity".

Added dedicated "min_max_temperature" and "min_max_humidity" validate
formats: same type coercion (float / round), no self-referential bound
check, since these templates define the allowed range itself rather
than a setpoint within it. Switched all four callbacks to use them.

Found via mikopp/hass-template-climate's integration test suite:
test_airflow_climate.py::test_min_max_temp_and_step_getters and
test_roommind_climate.py::test_getters_and_min_max_templates both
expected a template-driven range change that never took effect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
_update_min_temp/_update_max_temp validated the incoming template value
via _validate_value(..., "target_temperature"), which bound-checks the
value against self._attr_min_temp/self._attr_max_temp -- the very
attribute the callback is trying to update. A new min_temp below the
entity's current min_temp (or a new max_temp above the current
max_temp) was rejected as out of range against its own stale value, so
min_temp_template/max_temp_template could only ever narrow the range
from its initial static/default value, never widen it.
_update_min_humidity/_update_max_humidity had the identical bug via
"target_humidity".

Added dedicated "min_max_temperature" and "min_max_humidity" validate
formats: same type coercion (float / round), no self-referential bound
check, since these templates define the allowed range itself rather
than a setpoint within it. Switched all four callbacks to use them.

Found via mikopp/hass-template-climate's integration test suite:
test_airflow_climate.py::test_min_max_temp_and_step_getters and
test_roommind_climate.py::test_getters_and_min_max_templates both
expected a template-driven range change that never took effect.

Cherry-picked from feat/fix-min-max-validation, which carries this fix
as its own separate PR to litinoveweedle. It is temporarily duplicated
here only so this branch's own CI (test-integration.yaml) can run
end-to-end; drop this commit from this branch once the fix PR merges
upstream, and rebase before opening the integration-tests PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
…o integration/all-features

# Conflicts:
#	custom_components/climate_template/Changelog.md
…egration/all-features

# Conflicts:
#	custom_components/climate_template/Changelog.md
Two details worth calling out explicitly about the min/max validation
fix that weren't spelled out in the original entry:

- The old (buggy) target_temperature-reusing code also snapped
  min_temp/max_temp to the nearest temp_step multiple before its bound
  check. The new dedicated format doesn't: bounds aren't setpoints and
  have no reason to sit on a step grid, so min_temp/max_temp now
  reflects the template's value exactly. This is the one intentional
  behavior difference from what narrowing-only configs saw before.
- min_temp_template/max_temp_template (and the humidity equivalents)
  are fully independent of each other post-fix, so templating only one
  side works identically to templating both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
Two details worth calling out explicitly about the min/max validation
fix that weren't spelled out in the original entry:

- The old (buggy) target_temperature-reusing code also snapped
  min_temp/max_temp to the nearest temp_step multiple before its bound
  check. The new dedicated format doesn't: bounds aren't setpoints and
  have no reason to sit on a step grid, so min_temp/max_temp now
  reflects the template's value exactly. This is the one intentional
  behavior difference from what narrowing-only configs saw before.
- min_temp_template/max_temp_template (and the humidity equivalents)
  are fully independent of each other post-fix, so templating only one
  side works identically to templating both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
Adds a second, minimal entity to the airflow fixture (climate.airflow_max_only)
that configures only max_temp_template, with a static min_temp and no
min_temp_template at all, plus a test asserting max_temp widens
correctly on its own while min_temp stays at its static value.

Closes the one gap in the min/max validation fix's test coverage:
airflow_climate and roommind_climate (the only existing fixtures using
these templates) both template both sides together, so neither proved
the two callbacks are actually independent of each other -- only that
they don't regress when used in combination.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
…egration/all-features

# Conflicts:
#	custom_components/climate_template/Changelog.md
The matrix's 'min' leg still hardcoded ha_image_tag: 2025.9.0, carried
over verbatim from the fork's pre-reset test suite, when hacs.json's
declared minimum was still 2025.9.0. Since main was reset onto
upstream, hacs.json's minimum is 2026.9.0 (upstream bumped it as part
of their own 2026.9 compatibility work), but this workflow was never
updated to match.

Result: HA 2025.9.0 is now well below what the current climate.py
actually requires -- make_template_entity_common_schema doesn't exist
in that version's homeassistant.components.template.schemas, so every
climate_template entity fails ImportError at platform setup and every
test fails with 'Entity ... not found'. This is a stale test-matrix
value, not a real HA compatibility gap: the declared minimum (2026.9.0)
works fine, as the 'stable (latest)' leg (which happens to be the same
version right now) already showed passing 61/61.

Verified the homeassistant/home-assistant:2026.9.0 image tag exists on
Docker Hub before pushing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants