feat/solis
needs attentionviewing older commitc3cb64a · fullpre-PRreviewed 2026-07-07 21:29 UTC5H · 9M · 9L · 5I- Purpose
- Port the SolisCloud (91-device brand) into the Batu v2 metrics engine so the universal metrics coordinator can dispatch it alongside eGauge, victron, growatt, helioscope, and the other 7 brands.
- Goal
- Three Lambda workers (metrics + connection + explore) with stateless HMAC-SHA1 signing, CDK stack, and probe-verified granularity decisions — completing SG-1 and SG-2 of the branch plan.
- Sub-goals
- SG-1: SolisCloud API probe + worker package (metrics/connection/explore) + 71 unit tests
- SG-2: CDK IntegrationSolisLambdaStack + main.ts + coordinator SSM reads/invoke grants
- What
- New services/metrics/integrations/solis/ package (3 handlers, HMAC client, translation layer, domain types, 71 tests); CDK IntegrationSolisLambdaStack with IAM and SSM ARN params; solis manifest with corrected key/key_secret field names; seed-metrics-catalog and seed-integration-arns solis entries; SecretProvider union extended with device integration providers.
- Why
- SolisCloud is the largest un-ported brand (91 devices). Porting enables the universal coordinator to collect 5-min native data from all Solis plants via the same SFN/jobType dispatch as other brands.
- Areas
- services/metrics/integrations/solis/+2200−0infra/cdk/+450−11packages/integration-manifests/+750−160packages/database/+224−13scripts/metrics/+1400−0domains/core/+15−1
- Blast
- 53 files, +6461/−196 lines. All new code additive — no existing handlers modified. SecretProvider union is type-only. CDK stack is brand-new CloudFormation stack, no existing resources renamed.
Findings · 22
correctness4
monthRecordsToBatu: fieldSeen set before dateStr validation
services/metrics/integrations/solis/src/translation/points-to-batu.ts:317
fieldSeen=true fires before the dateStr regex check. A month record with valid energy but malformed dateStr sets fieldSeen true while being skipped — returns ok:true with points:[] instead of ok:false. Coverage window marked collected while nothing persists to Tinybird.
powerToWatts: default pec=0.001 coupled to kW — silent 1000x error if vendor sends powerStr=W without powerPec
services/metrics/integrations/solis/src/translation/points-to-batu.ts:86
If vendor sends powerStr=W with no powerPec, defaults compute raw×0.001×1=raw/1000, under-reporting by 1000×. No runtime error; channel passes through silently wrong.
fetchAllStations: extra page fetch when plant count is exact multiple of 100
services/metrics/integrations/solis/src/engine/solis-client.ts:272
Termination: page.records.length < STATION_PAGE_SIZE. Exact-full-page case wastes one vendor call +100ms politeDelay.
31-day cap uses UTC days, not local calendar days
services/metrics/integrations/solis/src/handlers/metrics.lambda.ts:152
UTC-6 site: 31-day window spans 32 local days. 31d+1s window triggers cap on a legitimate collection. Use localDaysInWindow().length.
security3
secretConfigArn forwarded to SM without ARN-format validation
services/metrics/integrations/solis/src/engine/solis-client.ts:295
No format check before GetSecretValueCommand. Blast radius bounded by IAM prefix wildcards (systemic across eGauge/Growatt too).
IAM legacy path {env}/solis/* covers portal secrets
infra/cdk/src/stacks/services/metrics/integrations/solis/iam.ts:43
Prod holds /portal (username/password) variants under the same prefix. Scope to {env}/solis/oid=* for least privilege.
Raw vendor msg in InvocationError.message → CloudWatch Logs
services/metrics/integrations/solis/src/engine/solis-client.ts:174
May include account-identifying strings. Internal boundary only. Acceptable for ops.
conventions4
INFRA_DESIGN.md missing from CDK stack directory
infra/cdk/src/stacks/services/metrics/integrations/solis/
INTEGRATION_STANDARDS.md §8 checklist item 1. eGauge and Growatt both carry this file.
Descriptor not registered in descriptors-catalog.ts
services/metrics/integrations/solis/src/
INTEGRATION_STANDARDS.md §8 item 8. metrics-pipeline.md marks descriptors dead/legacy — decision needs to be explicit.
mapListError uses non-exhaustive default: switch
services/metrics/integrations/solis/src/handlers/explore.lambda.ts:99
Use SolisFetchFailure["reason"] param type + exhaustive switch matching mapFetchError pattern.
probeFailurePayload credentialValid: false for timeout/unreachable
services/metrics/integrations/solis/src/handlers/connection.lambda.ts:96
May trigger false credential-rotation alerts on transient network errors. Fix: reason===auth-failed ? false : true.
tests7
HTTP 408 (clock-skew) classification untested
services/metrics/integrations/solis/src/engine/solis-client.ts:144
>15-min Date skew → HTTP 408 routes to UpstreamUnavailable (infinite SFN retry) not UpstreamAuthFailed. No test pins this.
DST fall-back transition not tested in dayPointsToBatu
services/metrics/integrations/solis/src/__tests__/points-to-batu.test.ts:65
Code handles fall-back dedup; spring-forward tested but fall-back not. Add America/Tijuana 2026-11-01 test.
stationMonth failure path has zero handler-level coverage
services/metrics/integrations/solis/src/__tests__/metrics-handler.test.ts:218
Auth-failure test only exercises 5m/stationDay. The 1d branch failure return path is never tested.
Only one HMAC Python cross-vector
services/metrics/integrations/solis/src/__tests__/signing.test.ts:16
Missing: empty body, non-ASCII body content.
resolveChannels scalar-params fallback untested
services/metrics/integrations/solis/src/__tests__/metrics-handler.test.ts:41
All handler tests use sources[]. Fallback from flat params never exercised.
Inverted-window and NaN-date validation paths not tested
services/metrics/integrations/solis/src/__tests__/metrics-handler.test.ts:92
Only absent from/to case tested. Inverted window and invalid date strings uncovered.
Explore pagination with >100 plants untested
services/metrics/integrations/solis/src/__tests__/explore-handler.test.ts:63
Multi-page loop (pageNo+1, politeDelay) has no dedicated test.
improvement4
windowDays float cap can misfire on fractional windows
services/metrics/integrations/solis/src/handlers/metrics.lambda.ts:152
31d+1s gives 31.000011, rejecting a legitimate collection. Use localDaysInWindow().length > MAX_WINDOW_DAYS[granularity].
resolveChannels scalar-params fallback is vestigial
services/metrics/integrations/solis/src/handlers/metrics.lambda.ts:81
Coordinator never sends the flat-key shape. Remove or comment as test-only.
mapFetchError and mapListError nearly identical
services/metrics/integrations/solis/src/handlers/explore.lambda.ts:99
A shared engine/error-map.ts would enforce exhaustiveness in both call sites.
CfnOutput exportNames are console-only
infra/cdk/src/stacks/services/metrics/integrations/solis/lambda.stack.ts:102
Coordinator reads ARNs via SSM not Fn::ImportValue. Comment would prevent accidental cross-stack coupling.