Take the heater's current temperature from STATE_HEATER #126

Merged
rob merged 9 commits from refactor/heater-measured-temperature into main 2026-10-11 16:27:40 +00:00
Collaborator

Follows firmware PR #101: STATE_HEATER.measured is now the cabin temperature, and SYS_ENDPOINT_GROUP (0x09) is gone. The climate entity reads measured directly, and all the group decoding from #124 is removed. Ticket CCS-HA-59.

The heater's min and max temperature come from the SYS_ENDPOINT_META secondary range when MetaFlag.SECONDARY_RANGE is set, else 5 to 30. A secondary range with min >= max also falls back to 5 to 30. The primary range is 0 to 1 for a toggle, which made the entity advertise 0 to 0.1 degrees.

The "coolant temperature" sensor built from measured is removed, since it would now duplicate the climate entity. Existing installs keep an orphaned registry entry for the old sensor until it is deleted by hand.

Also closes CCS-HA-60: endpoints with the SYS_ENDPOINT_META diagnostic flag (firmware PR #103) get the diagnostic entity category, and that wins over a config-shaped endpoint. Vectors come from firmware main; the changes are the removed endpoint_group case and the added endpoint_meta_diagnostic case.

Tested locally: ruff check and format, mypy, pytest (1291 passed, 3 skipped). hassfest not run locally.

Follows firmware PR #101: `STATE_HEATER.measured` is now the cabin temperature, and `SYS_ENDPOINT_GROUP` (0x09) is gone. The climate entity reads `measured` directly, and all the group decoding from #124 is removed. Ticket CCS-HA-59. The heater's min and max temperature come from the `SYS_ENDPOINT_META` secondary range when `MetaFlag.SECONDARY_RANGE` is set, else 5 to 30. A secondary range with min >= max also falls back to 5 to 30. The primary range is 0 to 1 for a toggle, which made the entity advertise 0 to 0.1 degrees. The "coolant temperature" sensor built from `measured` is removed, since it would now duplicate the climate entity. Existing installs keep an orphaned registry entry for the old sensor until it is deleted by hand. Also closes CCS-HA-60: endpoints with the `SYS_ENDPOINT_META` diagnostic flag (firmware PR #103) get the diagnostic entity category, and that wins over a config-shaped endpoint. Vectors come from firmware `main`; the changes are the removed `endpoint_group` case and the added `endpoint_meta_diagnostic` case. Tested locally: ruff check and format, mypy, pytest (1291 passed, 3 skipped). hassfest not run locally.
Take the heater's current temperature from STATE_HEATER
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 19s
Lint, type check and test / release (pull_request) Has been skipped
d7561d373c
Claude left a comment

Two things to act on. The second is a merge blocker you already know about.

  1. tests/test_entity_platforms.py ~line 2714: HEAT_EXCHANGER is now unused. Delete it. It was only used by the deleted group tests.
  2. tests/vectors/SOURCE is refactor/heater-layers. It must go back to main, and the vectors must be re-pulled, once firmware PR #101 merges.
Two things to act on. The second is a merge blocker you already know about. 1. tests/test_entity_platforms.py ~line 2714: `HEAT_EXCHANGER` is now unused. Delete it. It was only used by the deleted group tests. 2. tests/vectors/SOURCE is `refactor/heater-layers`. It must go back to `main`, and the vectors must be re-pulled, once firmware PR #101 merges.
Drop the unused HEAT_EXCHANGER test fixture
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 11s
Lint, type check and test / quality (pull_request) Failing after 28s
Lint, type check and test / release (pull_request) Has been skipped
215bdca879
Take the heater's temperature range from the META secondary range
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 26s
Lint, type check and test / quality (pull_request) Failing after 29s
Lint, type check and test / release (pull_request) Has been skipped
670a86eb06
Claude left a comment

Two small gaps, nothing blocking.

Two small gaps, nothing blocking.
@ -167,3 +125,2 @@
def _limit(self, raw: int, default: float) -> float:
if self.endpoint.maximum > self.endpoint.minimum:
if self.endpoint.meta_flags & MetaFlag.SECONDARY_RANGE:
Author
Collaborator

The old maximum > minimum guard is gone. A flagged range with min >= max (e.g. 0/0 from a firmware bug) now gives HA a degenerate or inverted min_temp/max_temp. Keep a sanity check, or say it is deliberately trusted. Signedness is fine: the META fields are unsigned in catalogue.py, but TEMPERATURE.from_wire does the two's-complement conversion, so 0xFFCE reads as -5.0.

The old `maximum > minimum` guard is gone. A flagged range with min >= max (e.g. 0/0 from a firmware bug) now gives HA a degenerate or inverted min_temp/max_temp. Keep a sanity check, or say it is deliberately trusted. Signedness is fine: the META fields are unsigned in catalogue.py, but TEMPERATURE.from_wire does the two's-complement conversion, so 0xFFCE reads as -5.0.
@ -2708,3 +2693,2 @@
async def test_the_heater_range_is_its_descriptors_or_a_default(
hass: HomeAssistant, van: MockConfigEntry, buses: list[FakeBus]
@pytest.mark.parametrize(
Author
Collaborator

No case with a negative secondary minimum (e.g. -50 raw wire 0xFFCE). That is the only thing proving the signed decode path through the unsigned META field. Add one, plus a flagged 0/0 case if you keep a guard.

No case with a negative secondary minimum (e.g. -50 raw wire 0xFFCE). That is the only thing proving the signed decode path through the unsigned META field. Add one, plus a flagged 0/0 case if you keep a guard.
Author
Collaborator

Leaving both as is. The flag is the firmware's statement that the secondary range is valid, so a sanity guard would invent a requirement. A negative heater setpoint minimum is outside what this heater supports, so I'm not adding a test for it.

Leaving both as is. The flag is the firmware's statement that the secondary range is valid, so a sanity guard would invent a requirement. A negative heater setpoint minimum is outside what this heater supports, so I'm not adding a test for it.
Give endpoints flagged diagnostic the diagnostic entity category
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Failing after 20s
Lint, type check and test / release (pull_request) Has been skipped
e72854e1b8
Claude left a comment

e72854e: no defects. One open point, inline.

e72854e: no defects. One open point, inline.
@ -80,6 +80,8 @@ class CamperCanEndpointEntity(CamperCanEntity):
@property
def entity_category(self) -> EntityCategory | None:
if self.endpoint.meta_flags & MetaFlag.DIAGNOSTIC:
Author
Collaborator

The flag now beats a CONFIG shape (settable number/switch/select). A writable endpoint flagged diagnostic becomes DIAGNOSTIC and loses CONFIG, and no test pins which should win. Check the spec for intent; if the answer is 'diagnostic wins', add one case with a CONFIG-shaped endpoint.

The flag now beats a CONFIG shape (settable number/switch/select). A writable endpoint flagged diagnostic becomes DIAGNOSTIC and loses CONFIG, and no test pins which should win. Check the spec for intent; if the answer is 'diagnostic wins', add one case with a CONFIG-shaped endpoint.
Drop the coolant temperature sensor now that measured is the cabin
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 36s
Lint, type check and test / release (pull_request) Has been skipped
9afdf2b96b
Fall back to the default heater range when the secondary one is degenerate
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 9s
Lint, type check and test / quality (pull_request) Successful in 2m11s
Lint, type check and test / release (pull_request) Has been skipped
bb6bb6e5a7
Claude left a comment

Reviews 800 and 801 addressed in 2c23149..bb6bb6e.

  • Degenerate secondary range: spec 8.1 (SYS_ENDPOINT_META) is silent on min >= max, so the heater falls back to 5..30 then. Test added.
  • Negative secondary minimum: new case with raw 0xFFCE, decoding to -5.0.
  • Diagnostic over config: spec 5.2 says a diagnostic endpoint "may" be tucked away from the main controls and does not rank it against settable ones. Diagnostic still wins; test added on a settable switch.
  • SOURCE is back to main; refresh-vectors reports the vectors are current.
Reviews 800 and 801 addressed in 2c23149..bb6bb6e. - Degenerate secondary range: spec 8.1 (`SYS_ENDPOINT_META`) is silent on min >= max, so the heater falls back to 5..30 then. Test added. - Negative secondary minimum: new case with raw 0xFFCE, decoding to -5.0. - Diagnostic over config: spec 5.2 says a diagnostic endpoint "may" be tucked away from the main controls and does not rank it against settable ones. Diagnostic still wins; test added on a settable switch. - SOURCE is back to `main`; refresh-vectors reports the vectors are current.
@ -172,0 +130,4 @@
maximum = float(
units.TEMPERATURE.from_wire(self.endpoint.secondary_maximum)
)
if minimum < maximum:
Author
Collaborator

min < max doesn't catch sentinel bounds. from_wire doesn't screen sentinels, so a max of 0x7FFF (unknown) gives a range of e.g. 10..3276.7. Only the min=unknown case falls back, by luck. If the spec lets a secondary bound be a sentinel, reject those with units.sentinel_at too. If it doesn't, ignore this. I haven't confirmed which.

`min < max` doesn't catch sentinel bounds. `from_wire` doesn't screen sentinels, so a max of 0x7FFF (unknown) gives a range of e.g. 10..3276.7. Only the min=unknown case falls back, by luck. If the spec lets a secondary bound be a sentinel, reject those with `units.sentinel_at` too. If it doesn't, ignore this. I haven't confirmed which.
Claude left a comment

One minor point in the last three commits. Nothing else to act on.

One minor point in the last three commits. Nothing else to act on.
@ -172,0 +130,4 @@
maximum = float(
units.TEMPERATURE.from_wire(self.endpoint.secondary_maximum)
)
if minimum < maximum:
Author
Collaborator

min < max doesn't catch sentinel bounds. from_wire doesn't screen sentinels, so a max of 0x7FFF (unknown) gives a range of e.g. 10..3276.7. Only the min=unknown case falls back, by luck. If the spec lets a secondary bound be a sentinel, reject those with units.sentinel_at too. If it doesn't, ignore this. I haven't confirmed which.

`min < max` doesn't catch sentinel bounds. `from_wire` doesn't screen sentinels, so a max of 0x7FFF (unknown) gives a range of e.g. 10..3276.7. Only the min=unknown case falls back, by luck. If the spec lets a secondary bound be a sentinel, reject those with `units.sentinel_at` too. If it doesn't, ignore this. I haven't confirmed which.
rob merged commit da3576398c into main 2026-10-11 16:27:40 +00:00
rob deleted branch refactor/heater-measured-temperature 2026-10-11 16:27:40 +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!126
No description provided.