Skip to content

Type events by name (discriminated Event subtypes)#2127

Merged
iMicknl merged 18 commits into
mainfrom
feature/typed-events
Jun 5, 2026
Merged

Type events by name (discriminated Event subtypes)#2127
iMicknl merged 18 commits into
mainfrom
feature/typed-events

Conversation

@iMicknl

@iMicknl iMicknl commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Turns the flat Event model into a discriminated union keyed on the name field. A slim base Event carries only the fields common to every event; concrete events structure into typed subtypes with their documented, precisely-typed fields. Unknown / unmodeled event names fall back to the base Event, so forward-compatibility is preserved.

This replaces the previous single ~24-field Event where every field was optional, which forced consumers (notably Home Assistant) to write defensive if not event.device_url / if event.exec_id guards even when the API guarantees those fields for a given event type.

Warning

Breaking change (intended for the v2 release). Fields like device_url, device_states, new_state, exec_id, etc. are no longer on the base Event — they live on the subtypes. Consumers must narrow with isinstance / match before accessing them. client.fetch_events() still returns list[Event].

What's added

  • Base Event: name, timestamp, setup_oid, owning_partners (the last is newly surfaced; it appears on ~95 documented event types and was previously dropped).
  • Typed subtypes:
    • DeviceStateChangedEventdevice_url, device_states (non-optional)
    • ExecutionRegisteredEvent, ExecutionStateChangedEvent
    • FailureEvent — base for the *FailedEvent names; carries failure_type + failure_type_code together
    • Device lifecycle: DeviceAvailableEvent, DeviceUnavailableEvent, DeviceDisabledEvent, DeviceCreatedEvent, DeviceUpdatedEvent, DeviceRemovedEvent (created/updated/removed add controllable_name, previously dropped)
    • Zone events: ZoneCreatedEvent, ZoneUpdatedEvent, ZoneDeletedEvent (zone_oid, device_urls, place_oids — previously dropped)
  • Discriminator: EVENT_TYPE_BY_NAME in models.py + a cattrs structure hook on Event in converter.py. The hook resolves the subtype from name and delegates to each class's camelCase-rename-aware hook. Subtype hooks are pre-built and called directly to avoid infinite recursion (cattrs would otherwise re-dispatch a subclass back through the base Event hook). All *FailedEvent names are mapped to FailureEvent, derived from the EventName enum by suffix so new failure events are covered automatically.

Testing

  • New TestEvent coverage: direct subtype construction, converter dispatch per family, base fallback for known-unmodeled names, fallback for genuinely-unknown names, and *FailedEventFailureEvent.
  • Wires up the previously-orphaned tests/fixtures/event/local_events.json fixture end-to-end (local-API typed values incl. a nested object value).
  • Full suite green (531 tests), plus ruff check, ruff format, mypy, and ty all clean.

Follow-up (separate PR)

The Home Assistant overkiz coordinator can drop its EventName handler registry and the redundant None-guards in favour of a match event: block over the subtypes — cleaner and statically checked. Not included here; pyoverkiz only exposes the subtypes + discriminator.

Copilot AI review requested due to automatic review settings June 5, 2026 08:29
@iMicknl iMicknl requested a review from tetienne as a code owner June 5, 2026 08:29

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.

Pull request overview

This PR refactors the flat Event model into a discriminated union of typed subtypes, keyed on the name field. This enables consumers to use isinstance checks or pattern matching instead of defensive None-guards when accessing event-specific fields. Unknown/unmodeled event names gracefully fall back to the base Event class for forward compatibility.

Changes:

  • Splits the monolithic Event class into a base class (with only universal fields) plus concrete subtypes (DeviceStateChangedEvent, ExecutionStateChangedEvent, FailureEvent, device lifecycle events, zone events), each with precisely-typed required fields.
  • Adds a cattrs discriminator hook in converter.py that inspects the "name" field to dispatch structuring to the appropriate subtype, with pre-built hooks to avoid infinite recursion.
  • Comprehensive test coverage for subtype construction, converter dispatch, fallback behavior, and integration with both cloud and local API event fixtures.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
pyoverkiz/models.py Splits Event into base + typed subtypes; adds EVENT_TYPE_BY_NAME discriminator map with auto-registration of *FailedEvent names.
pyoverkiz/converter.py Registers a discriminated-union structure hook for Event that resolves subtypes by name and delegates to pre-built rename-aware hooks.
tests/test_models.py Adds extensive tests: subtype construction, converter dispatch per event family, base fallback, and local fixture integration.
tests/test_client.py Guards device_states access behind isinstance(event, DeviceStateChangedEvent) since the field no longer exists on the base class.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyoverkiz/models.py Outdated
iMicknl added 4 commits June 5, 2026 10:47
Model all Gateway* events via a single GatewayEvent carrying gateway_id,
enrich FailureEvent with gateway_id/device_url/protocol_type so failure
payloads keep their scope, and drop the never-sent failure_type_code from
failures (it stays on ExecutionStateChangedEvent, where the API sends it).

Make identity fields the API reliably sends required (device_url, gateway_id,
zone_oid, exec_id/new_state/old_state) so consumers get non-None guarantees
after isinstance narrowing. To keep strictness from making batches fragile,
the discriminator now degrades a single malformed event to the base Event
with a logged warning instead of failing the whole fetch_events() list.

Also: tolerate null deviceStates, share _DeviceMetadataEvent/_ZoneEvent bases,
and document the event changes in the v2 migration guide.
Gateway events now mirror device events: a private _GatewayEvent base plus one
public class per name (GatewayDownEvent, GatewayAliveEvent, ...), with the three
documented extra-field events carrying their payload (timeout, firmware_type,
function_type/enabled). This drops the GatewayEvent grab-bag that collapsed 22
distinct events a consumer would want to tell apart into one type.

Failures stay as the single shared FailureEvent by design — consumers branch on
"did it fail, and why" (failure_type), not on which operation failed; the
docstring and migration guide now state this explicitly. The event model is
three rules: each modeled event is its own class, all *FailedEvent -> FailureEvent,
anything unmodeled -> base Event.
Promote the per-category event bases to public (DeviceEvent, GatewayEvent,
ZoneEvent) and add ExecutionEvent, each carrying that category's identity
field (device_url / gateway_id / zone_oid / exec_id). Consumers can now narrow
broadly (any gateway event) or to a leaf (GatewayDownEvent) for full payload.
The Created/Updated DRY helpers stay private (_DeviceMetadataEvent,
_ZoneMutationEvent) — they are not a category anyone narrows to.

Fix a latent camelize bug surfaced by the now-required zone_oid: the API sends
zoneOID / deviceURLs / placeOIDs, but camelize produced zoneOid / deviceUrls /
placeOids, so zone events silently lost those fields (and now degraded to base
Event). Added the missing acronym overrides.

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

iMicknl added 2 commits June 5, 2026 13:11
Lift per-item resilience to the list[Event] level so a single
unstructurable event (missing name, non-dict) is dropped and logged
instead of failing the whole fetch. Remove the speculative deviceStates
null coercion, which never occurs in real data and is now redundant
with subtype-degradation.
@iMicknl iMicknl merged commit 6f03353 into main Jun 5, 2026
11 checks passed
@iMicknl iMicknl deleted the feature/typed-events branch June 5, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants