feat/powerradar
needs attentionviewing older commit563252b · incrementalpre-PRreviewed 2026-07-21 18:32 UTC4H · 9M · 8L · 4I- Purpose
- Add automatic API-based PowerRadar (Centrica / Panoramic Power) metrics integration, replacing manual-CSV path with native 5/15/60-min collection via headless OAuth + /eigw chart-data pull
- Goal
- Production-ready powerradar-api worker fitting the existing Site Collection SFN model: manifest registered, CDK deployed, DB seeded, live-proven end-to-end
- Sub-goals
- SG-1: Auth + transport engine + live probe (OAuth broker cracked; chart-data contract locked; browser headers defeat WAF; retention ≥12mo)
- SG-2: Manifest + catalog + provider registration (powerradar-api manifest, seed rows, provider enum)
- SG-3: Pure translation + unit tests (chart-to-batu.ts, 13 tests)
- SG-4: Metrics handler + handler tests (§9 multi-gran, 18 tests; exploration deferred — metrics-only like solark)
- SG-5: CDK stack + wiring (lambda.stack, iam, paths, main.ts dependency, coordinator invoke grant)
- SG-6: Live worker e2e (191 demand points @ 15m; solar skipped on load-only device; full path proven headless)
- SG-7: Framework fold-back (CLAUDE.md PowerRadar-API section)
- What
- This incremental window (from beced587) adds: pure translation layer (chart-to-batu.ts), metrics handler (metrics.lambda.ts), all 31 unit tests, CDK lambda+IAM stack, coordinator invoke grant, and integrations CLAUDE.md docs
- Why
- Gives Energía Real's 31 PowerRadar devices native 5/15-min granularity without manual export — prerequisite for accurate real-time CFE-basis max-demand billing; CSV worker stays as deep-history fallback
- Areas
- services/metrics/integrations/powerradar-api+1003−0infra/cdk/src/stacks/services/metrics/integrations/powerradar-api+157−0infra/cdk/src (app/main.ts + lib/paths.ts + engine/coordinator)+29−0packages/integration-manifests/src/manifests+100−0packages/database/src (schema + seeds)+17−0services/metrics/integrations/CLAUDE.md+42−0
- Blast
- ~1350 net additions. No existing code paths modified beyond coordinator SSM read + addDependency; CSV powerradar worker untouched.
Findings · 23
correctness2
Fetch error masks grain-mismatch diagnosis in multi-group total-failure path
services/metrics/integrations/powerradar-api/src/handlers/metrics.lambda.ts:219
When `payloads.length === 0`, error returned is `fetchErrors[0] ?? translationFailed(details)`. If a grain-mismatch stored in `grainMismatch` plus a fetch failure in `fetchErrors` coexist, the operator sees UpstreamAuthFailed and fixes auth only to hit the non-retryable grain-mismatch on retry. The `details` variable is computed but discarded when fetchErrors[0] exists.
Single-point window bypasses grain guard
services/metrics/integrations/powerradar-api/src/translation/chart-to-batu.ts:165
`dominantSpacingSeconds` returns null for <2 points. The grain guard `spacing !== null && spacing !== slot` never fires for a 1-point window — a wrong-cadence single point can be stored. Known gap, belt-and-suspenders only fires at 2+ points.
security3
IAM centrica/* wildcard broader than necessary
infra/cdk/src/stacks/services/metrics/integrations/powerradar-api/iam.ts:44
Grants `secretsmanager:GetSecretValue` on `{env}/centrica/*` to reuse the legacy `{env}/centrica/portal/oid={oid}` secrets. Any future secret under `centrica/` gains ambient read access. Narrow to `{env}/centrica/portal/*`.
Spring OAuth error_description reflected into structured logs
services/metrics/integrations/powerradar-api/src/engine/eigw-client.ts
Auth failure embeds `body?.error_description` into the error details string, which the handler logs. Standard Spring value is 'Bad credentials' — harmless — but this is a vendor contract, not a code guarantee. Log only `body?.error` (the short code) and drop error_description.
OAuth client constant hardcoded — correctly a public SPA client
services/metrics/integrations/powerradar-api/src/engine/eigw-client.ts
OAUTH_CLIENT_ID ('webapi') / OAUTH_CLIENT_SECRET ('webapisecret') are code constants from the browser bundle — observable by any browser session. Per-org credential is {username,password} in Secrets Manager. No action needed.
conventions7
dynamicVariables: false missing from manifest object
packages/integration-manifests/src/manifests/powerradar-api.ts:49
The manifest comment documents `dynamicVariables: false` but the field is ABSENT from the exported object. The provisioning wizard defaults via `manifest.dynamicVariables ?? false` so routing is correct today — but the adjacent CSV `powerradar` manifest explicitly declares `dynamicVariables: true`, making the implicit false a maintenance trap. The missing assertion in device-integrations.test.ts is what let this slip through. Add `dynamicVariables: false` to the manifest object.
grainMismatch variable name misleads — captures unit-unknown failures too
services/metrics/integrations/powerradar-api/src/handlers/metrics.lambda.ts:143
Variable is set for both `grain-mismatch` AND `unit-unknown` (lines 203-205). Used as primary error detail at line 217. A unit error surfaced under a grain-mismatch name is confusing to debug. Rename to `vendorDataError` or `hardChannelFailure`. Same pattern in solark's handler — fix both.
No manifest test block for powerradar-api in device-integrations.test.ts
packages/integration-manifests/src/__tests__/device-integrations.test.ts
Every other manifest has a dedicated `it` block in this file. The absence is the proximate reason `dynamicVariables: false` was not caught. Add a block asserting: `dynamicVariables: false`, `requiresAuth: true`, `assetConfigSchema.required` includes `powerradarSiteId`, both variables declare `granularities: ['5m','15m','1h']`.
ProjectionResult deviates from @batu/result — justification not documented
services/metrics/integrations/powerradar-api/src/translation/chart-to-batu.ts:99
ADR-016 mandates `Result<T,E>` from `@batu/result`. The custom `ProjectionResult` discriminated union is a consistent pattern across vendor worker translation layers (solark, enphase, hoymiles) — valid for worker-internal pure functions that never cross the domain boundary. But the deviation is undocumented. Add a comment explaining the boundary (translated to InvocationError by the handler).
§9 resilient semantics fully correct
services/metrics/integrations/powerradar-api/src/handlers/metrics.lambda.ts:154
Group-by-gran → ONE fetch per group → channel projection → zero-point drop → fail-only-if-nothing all implemented correctly. Real transport tag surfaces on total failure.
units: W label always correct after kW/MW scaling
services/metrics/integrations/powerradar-api/src/translation/chart-to-batu.ts:179
powerUnitFactor scales kW/MW to W; payload labeled 'W'. Matches site-metrics-seam rule. Confirmed correct.
addDependency CDK wiring complete
infra/cdk/src/app/main.ts:571
coordinatorLambdaStack.addDependency(powerradarApiLambdaStack) present; no additional edges needed. Infra checklist satisfied.
tests9
Missing test: partial success when one granularity group fails but another succeeds
services/metrics/integrations/powerradar-api/src/__tests__/metrics-handler.test.ts
§9: a failing group skips its channels while a succeeding group still lands in `payloads`. The existing mixed-15m+1h test asserts BOTH succeed. No test exercises: 15m fetch → UpstreamRateLimited, 1h fetch → ok → status: 'success' with only the 1h payload. Without this, a bug where fetchErrors[0] contaminates a partial success is undetected.
Missing test: unit-unknown at handler level (grainMismatch sentinel unexercised for unit errors)
services/metrics/integrations/powerradar-api/src/__tests__/metrics-handler.test.ts
`grainMismatch` is set for both `grain-mismatch` AND `unit-unknown` (lines 203-205), but unit-unknown is never triggered at the handler test level. The code path setting grainMismatch for a unit error and using it as the preferred failure detail at line 217 is untested. A test where the vendor returns `unitSymbol: 'Wh'` (energy, not power) for the only channel would cover this.
Missing test: empty dataRecord array (not null) → ok:true with zero points → handler drops it
services/metrics/integrations/powerradar-api/src/__tests__/chart-to-batu.test.ts
The channel-not-found guard checks `records == null` (falsy for null, but NOT for `[]`). With `dataRecord: []`, the function returns `ok: true` with `points: []`; the handler drops zero-point payloads. This correct behavior is untested — a future regression changing `== null` to `=== null` would silently break empty-window handling.
Missing test: dominantSpacingSeconds tie-breaking (equal count → smaller spacing wins)
services/metrics/integrations/powerradar-api/src/__tests__/chart-to-batu.test.ts
The tie-break branch `count === bestCount && delta < best` is untested. A test with equal-count 300s and 3600s deltas should return 300. Without it a Map iteration order change could silently flip the grain-guard decision.
Missing test: MW unit scaling (×1_000_000 branch in powerUnitFactor)
services/metrics/integrations/powerradar-api/src/__tests__/chart-to-batu.test.ts
Only W and kW are tested. The MW branch is dead code from the test's perspective — a typo in the multiplier (1_000 instead of 1_000_000) would go undetected.
Missing test: 5m granularity happy path in the handler
services/metrics/integrations/powerradar-api/src/__tests__/metrics-handler.test.ts
The window-cap test exercises 5m but the fetch is mocked to never be called. A happy-path test with `granularity: '5m'` checking `resolution: 'FIVE_MIN'` is sent would verify the SLOT_SECONDS['5m'] and GRANULARITY_RESOLUTION['5m'] wiring.
Missing test: catch branch (uncaught exception → UpstreamUnavailable 502)
services/metrics/integrations/powerradar-api/src/__tests__/metrics-handler.test.ts
No test forces loadCredentialFromArn or fetchChartDataWithRetry to throw (vs return a failure object). If the failure() call inside catch were replaced with throw, the SFN would receive an unstructured Lambda error undetected.
Missing test: dedupe last-wins with 3+ duplicates
services/metrics/integrations/powerradar-api/src/__tests__/chart-to-batu.test.ts
Existing dedupe test uses exactly 2 records. A first-wins regression would pass the 2-duplicate test if records were in the right order. A 3-entry test (values 10, 11, 12 at same ts expecting 12) would be definitive.
Missing test: non-finite timestamp records (NaN, Infinity) silently dropped
services/metrics/integrations/powerradar-api/src/__tests__/chart-to-batu.test.ts
Line 155 guards `!Number.isFinite(rec.timestamp)`. A refactor dropping the isFinite check would go undetected. Covers realistic API schema drift.
improvement2
dominantSpacingSeconds duplicated from solark — candidate for @batu/metrics-engine
services/metrics/integrations/powerradar-api/src/translation/chart-to-batu.ts:80
Byte-for-byte copy of `services/metrics/integrations/solark/src/translation/points-to-batu.ts`. Will repeat for every new integration needing a cadence guard. Belongs in `@batu/metrics-engine` alongside `groupChannelsByGranularity`.
CDK stack may be missing batu:env and batu:owner tags
infra/cdk/src/stacks/services/metrics/integrations/powerradar-api/lambda.stack.ts:49
Sets 5 tags (domain/subdomain/provider/service/dataClass) but not batu:env or batu:owner. Matches egauge/solark pattern — may be covered by an aspect, but worth verifying against the infra tagging standard for cost allocation.
History · 15 commits
- f34e4ceneeds attentionincremental0H · 1M · 3L2026-07-23 00:32
- 1bc8aedneeds attentionincremental0H · 2M · 3L2026-07-22 21:34
- 31e63bbneeds attentionincremental0H · 2M · 5L2026-07-22 20:53
- 482cb88safeincremental0H · 0M · 1L2026-07-22 17:56
- 9a73b79safeincremental0H · 0M · 0L2026-07-22 17:34
- 87f8298needs attentionincremental0H · 1M · 3L2026-07-22 16:59
- 1e80096safeincremental0H · 0M · 1L2026-07-22 16:42
- 6ed65b4safeincremental0H · 1M · 3L2026-07-22 00:00
- 74a1131needs attentionfull0H · 7M · 12L2026-07-21 19:06
- 563252bneeds attentionincremental4H · 9M · 8L2026-07-21 18:32current
- beced58needs attentionincremental0H · 3M · 3L2026-07-21 01:13
- 7fa8684needs attentionincremental4H · 9M · 7L2026-07-20 22:56
- 230784fneeds attentionfull2H · 11M · 14L2026-07-10 00:15
- b876b54needs attentionincremental2H · 2M · 5L2026-07-08 04:44
- ec4847fneeds attentionfull2H · 11M · 8L2026-07-08 03:32