Present the heater as a climate entity #121

Merged
rob merged 4 commits from feat/heater-climate into main 2026-10-10 14:33:00 +00:00
Collaborator

The heater endpoint now gets a climate entity (off/heat, target temperature, heating/idle action) driven by CMD_HEATER, instead of a switch. The run state and coolant temperature sensors stay.

Behaviour change: the heater switch from #119 is gone; the climate entity takes its unique_id (60-00) and the switch platform gives the endpoint up. No automations to protect, as the switch landed today.

  • Current temperature is the temperature readout on the same node whose label contains "cabin" (case-insensitive); None if there is none. The node's other temperature readouts share its type and class, so the label is the only way to tell them apart. STATE_HEATER's measured field is the coolant and is not used.
  • Min/max come from the endpoint descriptor range when it has one, otherwise 5 and 30 °C.
  • Mode requests carry the current setpoint (the unknown sentinel if none is known); power is 0, since the spec reads it only in fixed power mode.
  • Added a frame_bench example for the command.

Ticket: CCS-HA-56

The heater endpoint now gets a climate entity (off/heat, target temperature, heating/idle action) driven by `CMD_HEATER`, instead of a switch. The run state and coolant temperature sensors stay. Behaviour change: the heater switch from #119 is gone; the climate entity takes its `unique_id` (`60-00`) and the switch platform gives the endpoint up. No automations to protect, as the switch landed today. - Current temperature is the temperature readout on the same node whose label contains "cabin" (case-insensitive); None if there is none. The node's other temperature readouts share its type and class, so the label is the only way to tell them apart. `STATE_HEATER`'s measured field is the coolant and is not used. - Min/max come from the endpoint descriptor range when it has one, otherwise 5 and 30 °C. - Mode requests carry the current setpoint (the unknown sentinel if none is known); power is 0, since the spec reads it only in fixed power mode. - Added a frame_bench example for the command. Ticket: CCS-HA-56
Fix the heater example file name
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 13s
Lint, type check and test / release (pull_request) Has been cancelled
Lint, type check and test / quality (pull_request) Has been cancelled
94844f37f3
Claude left a comment

Three things to fix, two smaller ones below. I did not run the suite or ruff: the worktree has no python on PATH.

Three things to fix, two smaller ones below. I did not run the suite or ruff: the worktree has no python on PATH.
@ -0,0 +26,4 @@
from .protocol.enums import HeaterRunState
from .protocol.presentation import PresentationKind, SemanticClass, kind_of
UNKNOWN_SETPOINT = 0x7FFF
Author
Collaborator

UNKNOWN_SETPOINT duplicates SIGNED_SENTINELS[Sentinel.UNKNOWN] in protocol/units.py. Reuse that. The test file also redefines it as UNKNOWN_TEMPERATURE and again as the 0x7FFF default in heater_reports.

`UNKNOWN_SETPOINT` duplicates `SIGNED_SENTINELS[Sentinel.UNKNOWN]` in protocol/units.py. Reuse that. The test file also redefines it as `UNKNOWN_TEMPERATURE` and again as the `0x7FFF` default in `heater_reports`.
@ -0,0 +47,4 @@
class CampervanHeater(CampervanCommandEntity, ClimateEntity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
Author
Collaborator

_cabin and the module docstring say room or cabin temperature. Per the spec's heater table the lowest-index temperature on NODE_HEATER is index 1, the heat exchanger hot sensor. It is not a room reading. The owner's rule is fine, but the naming and the docstring claim something false. Rename (e.g. _temperature_endpoint) and fix the docstring. CLAUDE.md's wording is accurate ("lowest-index temperature readout").

`_cabin` and the module docstring say room or cabin temperature. Per the spec's heater table the lowest-index temperature on NODE_HEATER is index 1, the heat exchanger hot sensor. It is not a room reading. The owner's rule is fine, but the naming and the docstring claim something false. Rename (e.g. `_temperature_endpoint`) and fix the docstring. CLAUDE.md's wording is accurate ("lowest-index temperature readout").
@ -0,0 +85,4 @@
def max_temp(self) -> float:
return self._limit(self.endpoint.maximum, DEFAULT_MAX_TEMP)
@property
Author
Collaborator

is_on is true for any non-zero mode, so mode 2 (fixed power) or 3 (ventilate) shows as HEAT with HEATING or IDLE. Spec 8.3: the node honours modes 0 and 1 only. Unlikely on the bus today, but the entity claims heat when the mode is not heat. Either accept this on purpose or map mode 1 only.

`is_on` is true for any non-zero mode, so mode 2 (fixed power) or 3 (ventilate) shows as HEAT with HEATING or IDLE. Spec 8.3: the node honours modes 0 and 1 only. Unlikely on the bus today, but the entity claims heat when the mode is not heat. Either accept this on purpose or map mode 1 only.
@ -0,0 +140,4 @@
self.endpoint.index,
heat,
UNKNOWN_SETPOINT if setpoint is None else setpoint,
),
Author
Collaborator

Only 0x7FFF is treated as no setpoint. 0x7FFE (fault), 0x7FFD (over range) and 0x8000 (under range) would show as 3276.6, 3276.5 or -3276.8 degrees. Spec section 6.1 says sentinels are real values. Use the existing sentinel handling (units.TEMPERATURE / measurement_of) instead of a hand-rolled check.

Only 0x7FFF is treated as no setpoint. 0x7FFE (fault), 0x7FFD (over range) and 0x8000 (under range) would show as 3276.6, 3276.5 or -3276.8 degrees. Spec section 6.1 says sentinels are real values. Use the existing sentinel handling (`units.TEMPERATURE` / `measurement_of`) instead of a hand-rolled check.
@ -0,0 +141,4 @@
heat,
UNKNOWN_SETPOINT if setpoint is None else setpoint,
),
on=heat,
Author
Collaborator

set_temperature while off or unknown sends mode=0 (spec: off sends shutdown) to an already-off heater. Unknown state counts as off. A setpoint change should not carry a shutdown. Confirm with the spec owner what the node does with mode 0 plus a setpoint, and say so in a test.

`set_temperature` while off or unknown sends `mode=0` (spec: off sends shutdown) to an already-off heater. Unknown state counts as off. A setpoint change should not carry a shutdown. Confirm with the spec owner what the node does with mode 0 plus a setpoint, and say so in a test.
Author
Collaborator

Fixed 1-3 in the latest commit: all reserved setpoint patterns read as unknown, the constant comes from units.SIGNED_SENTINELS, and _cabin is now _temperature_readout with a docstring that says what it is.

4: left. is_on treating any non-zero mode as on is the existing, tested rule, and the spec says NODE_HEATER reports 0 or 1 only.

5: left. Set-temperature sends the current mode as designed. Off plus a setpoint to an already-off heater is the same request the node sees from any other remote.

Fixed 1-3 in the latest commit: all reserved setpoint patterns read as unknown, the constant comes from `units.SIGNED_SENTINELS`, and `_cabin` is now `_temperature_readout` with a docstring that says what it is. 4: left. `is_on` treating any non-zero mode as on is the existing, tested rule, and the spec says NODE_HEATER reports 0 or 1 only. 5: left. Set-temperature sends the current mode as designed. Off plus a setpoint to an already-off heater is the same request the node sees from any other remote.
Treat every reserved setpoint as unknown and rename the readout lookup
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 1m38s
Lint, type check and test / quality (pull_request) Successful in 3m19s
Lint, type check and test / release (pull_request) Has been skipped
a7cd683f20
Read the heater's current temperature from the readout labelled cabin
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 1m23s
Lint, type check and test / quality (pull_request) Successful in 3m38s
Lint, type check and test / release (pull_request) Has been skipped
e28e70405c
rob force-pushed feat/heater-climate from e28e70405c
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 1m23s
Lint, type check and test / quality (pull_request) Successful in 3m38s
Lint, type check and test / release (pull_request) Has been skipped
to c4f0ecd320
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 27s
Lint, type check and test / quality (pull_request) Successful in 2m14s
Lint, type check and test / release (pull_request) Has been skipped
2026-10-10 14:28:16 +00:00
Compare
rob merged commit 3f2ff8b2d6 into main 2026-10-10 14:33:00 +00:00
rob deleted branch feat/heater-climate 2026-10-10 14:33:00 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/CampervanHomeAssistant!121
No description provided.