Skip to content

Model the REST fields the cloud has started returning - #124

Merged
eman merged 2 commits into
mainfrom
feat/rest-device-list-fields
Aug 29, 2026
Merged

Model the REST fields the cloud has started returning#124
eman merged 2 commits into
mainfrom
feat/rest-device-list-fields

Conversation

@eman

@eman eman commented Aug 29, 2026

Copy link
Copy Markdown
Owner

What changed upstream

Comparing today's live REST responses against the HAR captures in reference/ (September 2025), /device/list has grown:

Field Then Now
error: {errorCode, errorOccuredTime} absent present — in no HAR, not in openapi.yaml
descaling: {descalingStartTime, descalingEndTime} /device/info only also on /device/list
deviceInfo.modelTypeCode absent present (null on this unit)
deviceInfo.installerId absent present

NavienBaseModel sets extra="ignore", so all four were being discarded silently. The error block is the useful one: it makes the device's last recorded fault readable without an MQTT connection, and it stays readable while the device is offline.

Found while diffing live MQTT and REST traffic against the models after a controller firmware update (11.1.0.0 → 11.5.0.0).

Changes

  • Device gains optional error (DeviceErrorSummary) and descaling (DescalingInfo) sections.
  • DeviceInfo gains model_type_code and installer_id.
  • Every new field is optional, so a response that omits them parses exactly as before — /device/info returns no error block, and that path is covered by a test.
  • error_code is typed ErrorCode | int, following the existing device_type pattern, so a code the enum doesn't know degrades to a plain int rather than making an entire device listing unparseable. ErrorCode is a bare IntEnum with no _missing_, so without this an unrecognised code would raise out of list_devices().
  • error_occurred_time is spelled correctly in Python; the API's errorOccuredTime is handled by the alias.
  • docs/openapi.yaml and docs/reference/python_api/models.rst updated to match.

Not included: /device/info also returns installer and alarmInfo sections that no model declares. Those are visible in the 2025 HARs too, so they aren't new, and I left them out of this change.

Tests

tests/test_device_rest_models.py validates trimmed real payloads from both endpoints: the error and descaling blocks parse, the new deviceInfo fields parse, known error codes become enum members, an unknown code stays an int, and a /device/info payload with no error block still parses. Full suite: 716 passed.

https://claude.ai/code/session_01XVj9BYvuLj7Th3iFUVoeCn

@eman
eman requested a balanced review from Copilot August 29, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Smart-union validation currently leaves known error codes as integers, and the documented example fails for intentionally supported unknown codes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Extends REST models for newly observed device metadata, fault summaries, and descaling data.

Changes:

  • Adds and exports REST error/descaling models.
  • Models new DeviceInfo fields.
  • Updates tests, OpenAPI documentation, and changelog.

Validation: PR reports 716 passing tests.

File summaries
File Description
src/nwp500/models/device.py Adds REST fields and models.
src/nwp500/models/__init__.py Exports new models.
src/nwp500/__init__.py Adds top-level exports.
tests/test_device_rest_models.py Tests REST payload parsing.
docs/reference/python_api/models.rst Documents new models.
docs/openapi.yaml Updates REST schemas.
CHANGELOG.rst Records the additions.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/test_device_rest_models.py Outdated

device = Device.model_validate(payload)

assert device.error.error_code == ErrorCode(code)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — the assertion passed from either union branch, so it could not have caught the bug above. Fixed in ae3f1c0: the test now asserts isinstance(error_code, ErrorCode) and identity against the member, and the unknown-code test asserts not isinstance(..., ErrorCode). Also extended the parametrization to cover 0/NO_ERROR.

Comment on lines +110 to +113
for device in devices:
if device.error and device.error.error_code != ErrorCode.NO_ERROR:
print(f"{device.device_info.device_name}: {device.error.error_code.name}"
f" at {device.error.error_occurred_time}")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae3f1c0 — the example now branches on isinstance(code, ErrorCode) and prints code {n} for an unknown value instead of reaching for .name. With the union fix a known code is an enum member, but the int fallback is the whole point of the type, so the example should show handling it.

Comment thread src/nwp500/models/device.py Outdated
#: ``NO_ERROR`` when the device has no recorded fault. Typed to accept a
#: bare int as well, following ``device_type``, so a code the enum does
#: not know cannot make a whole ``/device/list`` response unparseable.
error_code: ErrorCode | int = ErrorCode.NO_ERROR

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in ae3f1c0. Verified directly:

smart union      -> 96 stays int
left_to_right    -> 96 becomes ErrorCode.E096_UPPER_HEATER, 9999 stays int

error_code now uses Field(union_mode="left_to_right"), with a comment recording why, so a known code becomes an enum member and only an unknown one falls through to int.

Worth noting the same class of bug exists on the pre-existing device_type: DeviceType | int (and FirmwareInfo.device_type) — those also stay plain ints today. Out of scope here since changing them alters the type callers already receive, but it should probably be a follow-up.

eman added 2 commits August 29, 2026 07:32
A live /device/list response carries more than it did when these models
were written. Comparing today's responses against the HAR captures in
reference/ (September 2025), the cloud has added:

  - an `error` block: errorCode, errorOccuredTime
  - `descaling`, previously only on /device/info
  - deviceInfo.modelTypeCode and deviceInfo.installerId

NavienBaseModel sets extra="ignore", so every one of these was being
discarded without a trace. The `error` block is the useful one: it makes
the device's last recorded fault readable without an MQTT connection,
including while the device is offline.

Device gains optional `error` and `descaling` sections and DeviceInfo
gains `model_type_code`/`installer_id`. All are optional, so a response
that omits them - /device/info returns no `error` block - parses exactly
as before. `error_code` is typed `ErrorCode | int`, following
`device_type`, so a code the enum does not know degrades to a plain int
instead of making a whole device listing unparseable.

docs/openapi.yaml and the model reference are updated to match.

Claude-Session: https://claude.ai/code/session_01XVj9BYvuLj7Th3iFUVoeCn
Pydantic's default smart union matches an incoming int against the int
branch exactly and never reaches the enum, so every code - known or not -
stayed a plain int, and the .name access in the documented example would
have raised AttributeError on a code the enum does know.

union_mode="left_to_right" makes a known code an ErrorCode member while
an unknown one still falls back to int. The test asserted only equality,
which an IntEnum satisfies from either branch; it now asserts the type.
The doc example handles the int fallback.

Claude-Session: https://claude.ai/code/session_01XVj9BYvuLj7Th3iFUVoeCn
@eman
eman force-pushed the feat/rest-device-list-fields branch from ae3f1c0 to 1fde090 Compare August 29, 2026 14:32
@eman
eman merged commit 14e246e into main Aug 29, 2026
7 checks passed
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