Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
176667c
Fix invalid default preset/fan/swing mode on entity startup
claude Sep 9, 2026
540d76d
Docs: document attributes/variables config options and legacy keys
claude Sep 9, 2026
2227cf5
Restore deprecated 'modes' alias for 'hvac_modes'
claude Sep 9, 2026
b49244b
Pin GitHub Actions to commit SHAs; add Dependabot
claude Sep 9, 2026
9b11f7d
Add integration test suite running against a real Home Assistant
claude Sep 9, 2026
7ea54db
Bump CI Python to 3.14 for integration tests
claude Sep 9, 2026
ed294c0
Fix every entity's name collapsing to 'Template Climate'
claude Sep 9, 2026
7c65758
Fix every entity's name collapsing to 'Template Climate'
claude Sep 9, 2026
8915cc1
Merge remote-tracking branch 'origin/feat/mode-default-reconciliation…
claude Sep 10, 2026
4a609d8
Merge remote-tracking branch 'origin/feat/docs-attributes-variables' …
claude Sep 10, 2026
2d10564
Merge remote-tracking branch 'origin/feat/legacy-modes-alias' into in…
claude Sep 10, 2026
f7d25fd
Merge remote-tracking branch 'origin/feat/ci-pin-actions' into integr…
claude Sep 10, 2026
3b7934c
Merge remote-tracking branch 'origin/feat/integration-tests' into int…
claude Sep 10, 2026
6ffe174
Merge remote-tracking branch 'origin/feat/fix-name-override' into int…
claude Sep 10, 2026
33e4448
Fix min/max temp and humidity templates unable to widen the range
claude Sep 10, 2026
b0713a6
Fix min/max temp and humidity templates unable to widen the range
claude Sep 10, 2026
9de555f
Merge remote-tracking branch 'origin/feat/fix-min-max-validation' int…
claude Sep 10, 2026
35fde85
Merge remote-tracking branch 'origin/feat/integration-tests' into int…
claude Sep 10, 2026
6d20898
Document temp_step-snapping and single-sided behavior in changelog
claude Sep 10, 2026
6fcdde3
Document temp_step-snapping and single-sided behavior in changelog
claude Sep 10, 2026
e81be87
Add single-sided max_temp_template regression test
claude Sep 10, 2026
a9fb0cf
Merge remote-tracking branch 'origin/feat/fix-min-max-validation' int…
claude Sep 10, 2026
3daad58
Merge remote-tracking branch 'origin/feat/integration-tests' into int…
claude Sep 10, 2026
25c478d
Fix stale HA min-version in integration test matrix
claude Sep 10, 2026
1b5db87
Merge remote-tracking branch 'origin/feat/integration-tests' into int…
claude Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Checkout the repository"
uses: actions/checkout@v2
uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2

- name: "Check format"
uses: psf/black@stable
uses: psf/black@87928e6d6761a4a6d22250e1fee5601b3998086e # stable
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Checkout the repository"
uses: actions/checkout@v2
uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2

- name: "Update version"
run: |
Expand Down
108 changes: 108 additions & 0 deletions .github/workflows/test-integration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: "Integration tests"

# Boots a real Home Assistant container (via the ha_integration_test_harness pytest
# plugin) and runs the climate_template scenario suite under tests/ against it.
# Mirrors the harness-based approach used in mikopp/homeassistent-config.

on:
push:
branches:
- "main"
- "feat**"
tags-ignore:
- "**"
pull_request:
workflow_dispatch:

concurrency:
group: integration-${{ github.ref }}
cancel-in-progress: true

jobs:
integration:
name: "HA ${{ matrix.ha_label }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# hacs.json "homeassistant" minimum supported version.
- ha_label: "2026.9.0 (min)"
ha_image_tag: "2026.9.0"
# Latest published stable release.
- ha_label: "stable (latest)"
ha_image_tag: "stable"

env:
# The harness boots HA with this directory mounted as /config.
HOME_ASSISTANT_CONFIG_ROOT: ${{ github.workspace }}/tests/ha_config

steps:
- name: "Checkout the repository"
uses: actions/checkout@v4

- name: "Set up Python"
uses: actions/setup-python@v5
with:
python-version: "3.14"

- name: "Install test dependencies"
run: pip install -r requirements_test.txt

# The harness's bundled docker-compose always launches the
# homeassistant/home-assistant:stable image. Pull the version under test and
# re-tag it as :stable so the matrix selects the HA version (no env override
# for the image tag exists in the harness compose file).
- name: "Pull Home Assistant image (${{ matrix.ha_image_tag }})"
run: |
docker pull "homeassistant/home-assistant:${{ matrix.ha_image_tag }}"
docker tag "homeassistant/home-assistant:${{ matrix.ha_image_tag }}" \
"homeassistant/home-assistant:stable"

# The harness mounts the config root as /config, so the custom integration
# under test must live underneath it. The mount cannot follow a symlink out of
# the tree, so copy the component in (tests/ha_config/custom_components is
# git-ignored).
- name: "Stage the integration under test"
run: |
mkdir -p tests/ha_config/custom_components
cp -r custom_components/climate_template \
tests/ha_config/custom_components/climate_template

- name: "Home Assistant config check"
run: |
output=$(docker run --rm \
-v "${HOME_ASSISTANT_CONFIG_ROOT}:/config" \
homeassistant/home-assistant:stable \
python -m homeassistant --config /config --script check_config 2>&1)
echo "$output"
if echo "$output" | grep -qE "Failed config|Invalid config"; then
echo "::error::Home Assistant reported configuration errors (see above)"
exit 1
fi

- name: "Run integration tests"
id: run_tests
continue-on-error: true
run: |
set -o pipefail
pytest tests/ -v 2>&1 | tee /tmp/pytest_output.txt

- name: "Write test results to job summary"
if: always()
run: |
{
echo "## climate_template integration tests β€” HA ${{ matrix.ha_label }}"
echo ""
if [ -f /tmp/pytest_output.txt ]; then
echo '```'
cat /tmp/pytest_output.txt
echo '```'
else
echo "No pytest output produced (container or setup failure β€” see step logs)."
fi
} >> "$GITHUB_STEP_SUMMARY"

- name: "Fail if tests failed"
if: steps.run_tests.outcome == 'failure'
run: exit 1
8 changes: 4 additions & 4 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@ jobs:
name: "Hassfest"
steps:
- name: "Checkout the repository"
uses: actions/checkout@v2
uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2

- name: "Validate Hassfest"
uses: home-assistant/actions/hassfest@master
uses: home-assistant/actions/hassfest@a7c616ce81ccda50150bf1595786c71b1883fabb # master

validate-hacs:
runs-on: ubuntu-latest
name: "HACS"
steps:
- name: "Checkout the repository"
uses: actions/checkout@v2
uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2

- name: "Validate HACS"
uses: hacs/action@main
uses: hacs/action@1ebf01c408f29afcb6406bd431bc98fd8cbb15aa # main
with:
category: integration
ignore: brands
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ __pycache__/
/custom_components/climate_template/icons.json
/custom_components/climate_template/translations/*.json
!/custom_components/climate_template/translations/en.json

# Test-only virtualenvs and pytest cache (tests/)
.pytest_cache/
/.venv/
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ This is a fork of the original repository jcwillox/hass-template-climate, which

## Breaking changes from jcwillox versions

- Config parameter `modes` renamed to `hvac_modes`
- Config parameter `modes` renamed to `hvac_modes`. The old `modes` key still
works (with a startup deprecation warning naming the affected entity) and
is automatically mapped to `hvac_modes` on load.
- `hvac_modes` list is set only to `["off", "heat"]` by default.
- `preset_modes`, `fan_modes` and `swing_modes` are now not set by default and shall be configured **only** if being used and set to the used miminum list of modes.

Expand Down Expand Up @@ -87,6 +89,8 @@ All configuration variables are optional. If you do not define a `template` or i
| ------------------------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| name | `string` | The name of the climate device. | "Template Climate" |
| unique_id | `string` | The [unique id](https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) of the climate entity. | None |
| attributes | `dict` | Dictionary of `name: template` pairs defining extra state attributes to expose on the entity. Each value is rendered as a template. See [example](#example-extra-state-attributes). | |
| variables | `dict` | Additional variables available in all action scripts (e.g. `set_hvac_mode`). See [example](#example-variables-in-actions). | |
| mode_action | `string` | Possible values: `parallel`, `queued`, `restart`, `single`. For explanation, see the [`script`](https://www.home-assistant.io/integrations/script/#script-modes) documentation. | single |
| max_action | `positive_int` | Limits the number of concurrent runs of actions. Used together with `parallel` and `queued` `mode_action`, set to a positive number greater than 1. For explanation, see the [`script`](https://www.home-assistant.io/integrations/script/#max) documentation. | 1 |
| presets_features | `positive_int` | Define the feature flags supported by the `preset_mode` feature as bit flags. See [example](#presets_features) for options. Default value `0` means presets are disabled. | 0 |
Expand Down Expand Up @@ -228,6 +232,21 @@ climate:
> [!WARNING]
> **Known limitation:** Home Assistant preloads an integration's `translations/<lang>.json` in the background *before* this integration's own startup code runs, so there is a narrow window where Home Assistant can read the file before it has been regenerated for the current configuration. Once loaded, translations are cached in memory for the rest of that Home Assistant session and are not re-read even after we finish rewriting the file. In practice this only becomes visible right after the on-disk file was reset to a stale/placeholder state just before that particular restart (for example, immediately after updating this integration via HACS, which reinstalls the file shipped in the release). When it happens, entity/preset/state text falls back to the raw, untranslated value for that session only β€” icons are unaffected, since they are loaded on demand rather than preloaded at startup. **Restarting Home Assistant a second time resolves it**, since the file already holds the correct, current content by then.

## Deprecated Keys

The following config keys still work but log a deprecation warning naming the
affected entity and the migration target. They are automatically rewritten to
their replacement on load.

| Deprecated Key | Replacement | Notes |
| -------------------------- | -------------- | ----------------------------------------------------------- |
| `availability_template` | `availability` | HA template-entity standard key. Rewritten automatically. |
| `icon_template` | `icon` | HA template-entity standard key. Rewritten automatically. |
| `entity_picture_template` | `picture` | HA template-entity standard key. Rewritten automatically. |
| `friendly_name` | `name` | HA template-entity standard key. Rewritten automatically. |
| `value_template` | `state` | HA template-entity standard key. Rewritten automatically. |
| `entity_id` | *(removed)* | No longer used; remove it from the configuration. |

## Example Configuration

```yaml
Expand Down Expand Up @@ -275,6 +294,35 @@ climate:
# could also send IR command via broadlink service calls etc.
```

### Example: extra state attributes

```yaml
climate:
- platform: climate_template
name: Airflow Controller
hvac_modes:
- "off"
- "cool"
attributes:
outdoor_temp: "{{ states('sensor.outdoor_temp') | float(none) }}"
indoor_dew: "{{ states('sensor.indoor_dew') | float(none) }}"
free_cooling_available: "{{ is_state('binary_sensor.free_cooling_available', 'on') }}"
```

### Example: variables in actions

```yaml
climate:
- platform: climate_template
name: My Climate
variables:
device_id: "my_esphome_device"
set_hvac_mode:
- service: esphome.{{ device_id }}_set_mode
data:
mode: "{{ hvac_mode }}"
```

### Example action to control existing Home Assistant devices

```yaml
Expand Down
42 changes: 42 additions & 0 deletions custom_components/climate_template/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@ All notable changes across all fork generations are hopefully documented here.

---

### 2026-09-10 β€” Fix min_temp_template/max_temp_template (and humidity) unable to widen the range

- **Author:** [@mikopp](https://github.com/mikopp)
- `_update_min_temp`/`_update_max_temp` validated the incoming template value with the same `"target_temperature"` format used for actual setpoints, which bound-checks the value against `self._attr_min_temp`/`self._attr_max_temp` β€” the very attribute being updated. 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"`/`"min_max_humidity"` validation formats that parse the value without bound-checking it against itself, and switched all four callbacks to use them.
- **Behavior change:** the old `"target_temperature"` format also snapped the value to the nearest `temp_step` multiple before the (buggy) bound check, so a narrowing `min_temp`/`max_temp` update was previously silently rounded (e.g. `7.3` β†’ `7.5` with `temp_step: 0.5`). The new format does not snap to `temp_step` β€” bounds are not setpoints and have no reason to sit on the step grid β€” so `min_temp`/`max_temp` now reports the template's value exactly. `min_temp_template` and `max_temp_template` (likewise `min_humidity_template`/`max_humidity_template`) are also fully independent of each other: each is validated and applied on its own, so configuring only one side (e.g. `max_temp_template` with a static `min_temp`) works identically to configuring both.
### 2026-09-09 β€” Fix invalid default preset/fan/swing mode on startup

- **Author:** [@mikopp](https://github.com/mikopp)
- `preset_mode`, `fan_mode`, and `swing_mode` were initialised to hardcoded defaults (`"comfort"`, `"low"`, `"off"`) without checking them against the configured `preset_modes`/`fan_modes`/`swing_modes` lists. When a configured list omitted the default, Home Assistant rejected the entity's state with `attribute 'X' returned invalid value`, and restoring it after a restart failed the same way. Each attribute now falls back to `None` when its hardcoded default isn't one of the configured values. `hvac_mode` is intentionally not reconciled: it is the entity's state, its default is always `HVACMode.OFF`, and Home Assistant already requires `off` in `hvac_modes`.


### 2026-09-09 β€” Document `attributes`, `variables`, and deprecated keys

- **Author:** [@mikopp](https://github.com/mikopp)
- Documented the existing `attributes` and `variables` config options in the configuration table, with worked examples. Added a "Deprecated Keys" section listing the legacy template-entity keys (`availability_template`, `icon_template`, `entity_picture_template`, `friendly_name`, `value_template`) that are rewritten automatically, and the removed `entity_id` option.


### 2026-09-09 β€” Deprecated `modes` alias for `hvac_modes`

- **Author:** [@mikopp](https://github.com/mikopp)
- Restored back-compat for the jcwillox-era `modes` config key: it is now accepted again and automatically rewritten to `hvac_modes` on load, alongside the other legacy keys in `rewrite_legacy_to_modern_config()`, with the same startup deprecation warning naming the affected entity.


### 2026-09-09 β€” Pin GitHub Actions to commit SHAs

- **Author:** [@mikopp](https://github.com/mikopp)
- Pinned `actions/checkout`, `psf/black`, `home-assistant/actions/hassfest`, and `hacs/action` in the CI/release/validate workflows to their current commit SHA (each ref's own comment preserved), and added `.github/dependabot.yml` to bump those SHAs weekly. Reduces exposure to a compromised or rewritten tag/branch on any of these actions.


### 2026-09-09 β€” Fix every entity's name collapsing to "Template Climate"

- **Author:** [@mikopp](https://github.com/mikopp)
- `TemplateClimate.__init__` set `self._attr_name` from the legacy `friendly_name` config key, overriding the name `TemplateEntity`'s base `__init__` had already correctly resolved from `name` (`CONF_NAME`) a few lines earlier. Since `rewrite_legacy_to_modern_config()` already rewrites `friendly_name` to `name` before the entity is constructed, `config.get(CONF_FRIENDLY_NAME)` is always `None` by this point β€” so every entity, regardless of its configured `name`, silently fell back to the hardcoded default `"Template Climate"`. On a fresh install this also means colliding `entity_id`s (`climate.template_climate`, `climate.template_climate_2`, ...), since Home Assistant slugs the initial `entity_id` from the display name. Removed the overriding line; `TemplateEntity.__init__` already handles both static and templated `name` correctly on its own.


### 2026-09-09 β€” Add integration test suite against a real Home Assistant

- **Author:** [@mikopp](https://github.com/mikopp)
- Added `tests/`: a pytest suite that boots a real Home Assistant container (via the [`HomeAssistant-Test-Harness`](https://github.com/HeadlessTarry/HomeAssistant-Test-Harness) pytest plugin) and exercises five `climate_template` scenarios end-to-end over its REST/WebSocket API β€” airflow control, a preset-profile boiler setup, an "E2M" self-attribute-read case, a fan/swing/preset mode-init matrix, and a multi-room ("RoomMind") setup. Added `.github/workflows/test-integration.yaml`, running the suite as a matrix against the `hacs.json` minimum-supported HA version and the latest stable release on every push and pull request.
- The `mode_init` suite reproduces the `preset_mode`/`fan_mode`/`swing_mode` invalid-default-value bug fixed separately in this changelog's "Fix invalid default preset/fan/swing mode on entity startup" entry β€” it fails against unpatched `climate.py` and passes with that fix applied.


### 2026-09-03 β€” Document translations startup race limitation

- **Author:** [@litinoveweedle](https://github.com/litinoveweedle)
Expand Down
Loading
Loading