Conversation
_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
…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
…o integration/all-features
…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
…egration/all-features
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Not for merging — this exists only to trigger CI (
pull_requesthas no branch filter,pushdoesn'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-reconciliationfeat/docs-attributes-variablesfeat/legacy-modes-aliasfeat/ci-pin-actionsfeat/integration-tests(temporarily carries the name-override fix so its own suite can run)feat/fix-name-overrideClose without merging once CI results are checked.
🤖 Generated with Claude Code
https://claude.ai/code/session_01EG6nBkBsv5jNAzXpiBewUy
Generated by Claude Code