Skip to content

feat: add viaGateway mode for shared per-gateway service - #777

Merged
CFenner merged 5 commits into
openviess:masterfrom
lackas:feature/via-gateway-service
Jul 14, 2026
Merged

CFenner merged 5 commits into
openviess:masterfrom
lackas:feature/via-gateway-service

Conversation

@lackas

@lackas lackas commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in service mode where ONE ViCareCachedServiceViaGateway instance serves all devices on a gateway from a single bulk API call, instead of N per-device calls per refresh.

vicare = PyViCare()
vicare.loadViaGateway(True)   # default False; must precede initWith*
vicare.initWithCredentials(...)

Per-device entry points (device.getProperty, DeviceConfig.as*) are unchanged. Default behavior is unchanged.

Why

The Viessmann API exposes a per-gateway bulk endpoint:

GET /features/installations/{id}/gateways/{serial}/features?includeDevicesFeatures=true

It returns all isEnabled=true features for every device on the gateway in one call. For overlapping features the property values + timestamps are byte-identical to per-device fetches.

For free-tier users (1450 API calls/day) this is meaningful headroom: a 4-device gateway drops from 4 calls per refresh to 1.

Architecture

ViCareService                       (per-device, transport)
└── ViCareCachedService             (per-device, with cache; default)

ViCareServiceViaGateway             (per-gateway, transport)
└── ViCareCachedServiceViaGateway   (per-gateway, with cache; opt-in)

The shared service is wired in PyViCare.__extract_all_devices: one service per gateway in viaGateway mode, one per device otherwise. setProperty still uses the per-device URL in both modes (writes target one feature on one device; no bulk write endpoint exists).

Both getProperty and fetch_all_features are cached against the same payload. The fetch_all_features cache is the key optimization for HA Core's DataUpdateCoordinator pattern (per-device coordinators call service.clear_cache + service.fetch_all_features per refresh — without caching that path, N coordinators on a shared service would still trigger N bulk fetches per cycle).

Defensive error handling (PACKAGE_NOT_PAID_FOR, DeviceCommunicationError, InternalServerError) mirrors ViCareCachedService.

Precursor refactor

Commit 1 moves role-based decisions (hasRoles, isGateway, isLegacyDevice, isE3Device, get_heat_curve_formular) from Service to Device/DeviceConfig. In shared-service mode the service cannot answer per-device role questions because its roles list would be whichever device was processed first. Service-side hasRoles/_isGateway are kept for backwards compatibility (still used by the per-device service for URL shape selection).

Public API unchanged. Test mocks updated to pass roles via the PyViCareDeviceConfig constructor instead of patching service.hasRoles.

Credit

Carries the design intent of #626 by @CFenner forward. The bulk response test fixtures (heatbox1/heatbox2/tcu1) are lifted from that draft. Implementation is fresh on current master because #774 made the previous sketch incompatible (accessor lives on Device now, service is stateless), and the original draft did not actually share the service instance across devices.

Tests

20 new tests covering URL building, per-device filtering of the bulk response, shared-cache behavior across devices, TTL respect, setProperty cache invalidation, stale-cache fallback on transient errors, end-to-end PyViCare wiring with mocked OAuth.

749 → 750 total tests passing, ruff clean, pylint 10/10 on changed modules.

Refs: #626, #774, home-assistant/core#173776

lackas added 3 commits June 18, 2026 12:43
Preparation for the viaGateway shared-service mode (next commit): when one
ViCareServiceViaGateway instance serves all devices on a gateway, the service
cannot answer per-device role questions because its roles list would be
whichever device was processed first. Move role-based decisions onto the
device-identity side of the architecture, where they belong.

- DeviceConfig.hasRoles(roles) + DeviceConfig.isGateway() use self.roles
  instead of self.service.hasRoles / self.service._isGateway.
- Device base class now takes optional roles list; all DeviceConfig.as*
  factories pass it through. Device.isLegacyDevice / isE3Device use
  self.roles.
- HeatingDevice.get_heat_curve_formular uses self.roles for the
  heatpump/E3 check.
- Service-side hasRoles / _isGateway are kept untouched for backwards
  compatibility (still used internally by the per-device service for URL
  shape selection).

No public API removals. Test mocks updated to pass roles via the
PyViCareDeviceConfig constructor instead of patching service.hasRoles.
Adds an opt-in service mode where ONE ViCareCachedServiceViaGateway
instance serves all devices on a gateway from a single bulk API call,
instead of N per-device calls per refresh.

Endpoint used (already verified byte-identical for overlapping features):
  GET /features/installations/{id}/gateways/{serial}/features?includeDevicesFeatures=true

Public API:
  vicare = PyViCare()
  vicare.loadViaGateway(True)   # default False; must precede initWith*
  vicare.initWithCredentials(...)

Per-device entry points (device.getProperty, DeviceConfig.as*) are
unchanged. The shared service is wired in PyViCare.__extract_all_devices:
one service per gateway in viaGateway mode, one per device otherwise.

Architecture:
- ViCareServiceViaGateway: transport, hits the bulk URL for fetch_all_features
  and getProperty; setProperty still uses the per-device URL (writes target
  one specific feature on one specific device).
- ViCareCachedServiceViaGateway: caches both getProperty and
  fetch_all_features against the same cached payload. The fetch_all_features
  cache is the key optimization for HA Core's DataUpdateCoordinator pattern,
  where each per-device coordinator calls service.clear_cache +
  service.fetch_all_features per refresh -- without it, N coordinators on a
  shared service would still trigger N bulk fetches per cycle.
- Defensive error handling (PACKAGE_NOT_PAID_FOR, DeviceCommunicationError,
  InternalServerError) mirrors ViCareCachedService: serve stale cache on
  transient failure, raise on first-fetch failure.

Carries the design intent of CFenner's stalled openviess#626 forward. The bulk
response test fixtures (heatbox1/heatbox2/tcu1) are lifted from that draft.
Implementation is fresh on current master because openviess#774 made the previous
sketch incompatible (accessor lives on Device now, service is stateless),
and the original draft did not actually share the service instance across
devices.

Tests: 20 new ones across three files covering URL building, per-device
filtering of the bulk response, shared-cache behavior across devices,
TTL respect, setProperty cache invalidation, stale-cache fallback on
transient errors, and end-to-end PyViCare wiring with mocked OAuth.

Refs: openviess#626 (CFenner draft)
Refs: home-assistant/core#173776 (raised the question)
CI pylint failed with C0415 (import-outside-toplevel).
lackas added a commit to lackas/ha-core that referenced this pull request Jul 9, 2026
viaGateway mode (PyViCare openviess/PyViCare#777) shares one feature
fetch across all devices behind a gateway. Group devices by gateway
serial and run one coordinator per gateway, so N devices behind one
gateway cost one API call per cycle instead of N. Cache duration now
scales with gateway count.
lackas added a commit to lackas/ha-core that referenced this pull request Jul 9, 2026
viaGateway mode (PyViCare openviess/PyViCare#777) shares one feature
fetch across all devices behind a gateway. Group devices by gateway
serial and run one coordinator per gateway, so N devices behind one
gateway cost one API call per cycle instead of N. Cache duration now
scales with gateway count.
Comment thread PyViCare/PyViCare.py Outdated
Comment thread PyViCare/PyViCareServiceViaGateway.py Outdated
Comment thread PyViCare/PyViCareCachedServiceViaGateway.py Outdated
Comment thread PyViCare/PyViCareDeviceConfig.py
lackas added 2 commits July 14, 2026 11:44
- extract ViCareCachedServiceBase (TTL, locking, stale-cache fallback,
  write invalidation) shared by ViCareCachedService and
  ViCareCachedServiceViaGateway via _fetch_uncached/_extract_entities hooks
- replace ViCareService._isGateway with module-level GATEWAY_ROLES /
  is_gateway_role, also used by PyViCareDeviceConfig.isGateway
- move PyViCare.viaGateway to a class attribute
- annotate filter_features_for_device

Addresses review feedback on openviess#777.
Avoids the mixin super() lint/type noise; the concrete gateway __init__
keeps its explicit parent init (pylint super-init-not-called silenced).

@CFenner CFenner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

@CFenner
CFenner merged commit aff06e9 into openviess:master Jul 14, 2026
11 checks passed
lackas added a commit to lackas/ha-core that referenced this pull request Aug 26, 2026
viaGateway mode (PyViCare openviess/PyViCare#777) shares one feature
fetch across all devices behind a gateway. Group devices by gateway
serial and run one coordinator per gateway, so N devices behind one
gateway cost one API call per cycle instead of N. Cache duration now
scales with gateway count.
lackas added a commit to lackas/ha-core that referenced this pull request Aug 29, 2026
viaGateway mode (PyViCare openviess/PyViCare#777) shares one feature
fetch across all devices behind a gateway. Group devices by gateway
serial and run one coordinator per gateway, so N devices behind one
gateway cost one API call per cycle instead of N. Cache duration now
scales with gateway count.
lackas added a commit to lackas/ha-core that referenced this pull request Aug 29, 2026
viaGateway mode (PyViCare openviess/PyViCare#777) shares one feature
fetch across all devices behind a gateway. Group devices by gateway
serial and run one coordinator per gateway, so N devices behind one
gateway cost one API call per cycle instead of N. Cache duration now
scales with gateway count.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants