Repository navigation
feat(gateway): PR3 — Solplanet adapter, polling, module, README, Docker - #3
Merged
Merged
Conversation
The configuration factory now sets KEY='app' so ConfigService.get() reads land under 'app.modbusHost' etc. validateConfig re-applies defaults because @nestjs/config calls it BEFORE the load factories merge. @nestjs/platform-express is now an explicit dep — NestFactory.create needs it for the HTTP listener to bind /healthz, and pnpm does not auto-hoist transitive @nestjs deps without shamefully-hoist=true.
…hooting User asked to enlarge the README. Added: table of contents, double-buffer section, lifecycle table, prerequisites table, .npmrc rationale, configuration table with security note, pymodbus verification snippet, full register map verified against sunspec/models, operating-states table, scale-factor math walkthrough, stale-data handling, adapter contract, troubleshooting table, observability patterns, security checklist, commit conventions, add-new-brand guide, project layout.
Marked 3.1-3.8 as completed (already merged into this branch) and appended 3.9-3.16 for: config namespace fix, README expansion, Dockerfile + .dockerignore, docker-compose.yml, Docker README section, Docker verification, CI Docker job, and the final PR3 verification gate. Includes the security and IoTech-simulator caveats.
…ervice
The 'KEY = app' namespace hack does not work when validate is also
provided: @nestjs/config v3 silently discards the load-factory
namespace and exposes the validate result flat at the top level.
The earlier 'namespace under app.*' commit therefore left every
config.get('app.*') call returning undefined, which crashed the
SolplanetCgiAdapter constructor on 'inverterBaseUrl.replace'.
Verified end-to-end: app now boots, /healthz returns 200, Modbus
TCP server binds to 0.0.0.0:5020, cron schedules every 5 s.
…ator Dockerfile: node:24-alpine multi-stage. Builder stage compiles TS with pnpm 11 (frozen-lockfile, ignore-scripts to skip husky's .git check). Production stage copies pruned node_modules + dist, runs as non-root 'nestjs' (uid 1001), exposes 3000+5020, ships a HEALTHCHECK against /healthz. docker-compose.yml: 'gateway' service built from the local Dockerfile, .env loaded for INVERTER_SN, host.docker.internal mapping for container→host inverter reach on macOS/Windows. 'modbus-simulator' service uses the IoTech pymodbus-sim:1.0 image behind a '--profile simulator' toggle — it is a GENERIC Modbus TCP server, NOT a SunSpec simulator, so it is useful only for HA smoke-testing without the real hardware. .dockerignore excludes node_modules, dist, coverage, .env, .git, .github, openspec, .atl, test, IDE configs, and dev-only toolchain config (lint/jest/husky/nvmrc). Keeps only what the Dockerfile copies. Verified: docker build succeeds (227MB image), docker compose config validates the resolved YAML, container boots and /healthz returns 200.
Two new sections between Running and Verifying:
## Docker — quick path, port map, Dockerfile highlights,
env-var overrides, resource limits, tear-down.
## IoTech Modbus simulator — profile-gated compose service, NOT a
SunSpec simulator (generic Modbus TCP only),
smoke-test via pymodbus, host port 5021 to
avoid collision with the gateway's 5020.
Plus a 'See also' footer with the IoTech simulator docs link, a
mbpoll CLI pointer, and the HA SunSpec integration source. Table of
contents updated to match.
New 'docker' job runs after 'ci' so it only pays the build cost when lint/build/test/e2e already pass. Steps: docker/setup-buildx-action@v3, 'docker build -t sunspec-gateway:test .', 'docker compose config --quiet' to validate the resolved YAML, then a 10 s retry loop against /healthz on the running container. Adds docker image into CI without slowing the fast feedback loop — the docker job is gated on ci: success and only runs on the same runner (ubuntu-latest), reusing the cached apt/pnpm layers from the first job via actions/cache.
The 'Validate compose stack' step failed in CI because docker-compose.yml references env_file: .env, and docker compose config requires the file to exist. CI doesn't ship a populated .env (it would leak secrets). Touch an empty stub so the YAML validates without exposing any real env vars.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR3 of the
sunspec-modbus-gatewaychange (third and final slice after PR1 and PR2 landed onmain). Wires the Solplanet HTTP CGI transport to the Modbus wire surface from PR2, plus Docker support and an opt-in IoTech simulator profile.What's included
src/config/configuration.tsINVERTER_SNrequired)src/gateway/inverters/solplanet-cgi.adapter.ts@nestjs/axiosHTTP GET, maps CGI JSON toInverterState, never throwssrc/gateway/inverter-polling.service.ts@Cron(EVERY_5_SECONDS)→adapter.read()→bus.publish()→modbus.refreshFromBus()src/gateway/gateway.module.tsHttpModule+ScheduleModule.forRoot(), DI token bindingsrc/health/health.controller.tsGET /healthzsrc/app.module.ts,src/main.tsConfigModule.forRootglobal,enableShutdownHooksREADME.md,.env.exampleopenspec/changes/.../tasks.mdDockerfile,.dockerignorenode:24-alpine, pnpm 11, non-rootnestjs(uid 1001), 227 MB imagedocker-compose.ymlgatewayservice +modbus-simulatorprofile (IoTechiotechsys/pymodbus-sim:1.0).github/workflows/ci.ymldockerjob: build, smoke-test/healthz, validate compose YAMLSecurity notes
MODBUS_HOSTto a private interface in production..env.examplecarries the warning;README.md§ Security checklist repeats it.nestjs(uid 1001), not root..envin image..dockerignoreexcludes it;INVERTER_SNis only injected atdocker runtime.IoTech simulator is generic Modbus, NOT SunSpec
The
modbus-simulatorcompose service usesiotechsys/pymodbus-sim:1.0— a generic Modbus TCP server. It is not a SunSpec simulator, so the gateway's M1/M101 registers will read as zeros from it. It exists for HA smoke-testing without real hardware (does Home Assistant successfully walk the gateway's register map and find the SunS magic at 40000-40001). Behind--profile simulatorso the defaultdocker compose upstays minimal. See IoTech Modbus simulator docs.Verification
pnpm lintcleanpnpm buildcleanpnpm test38/38 passingpnpm test:e2e12/12 passing (FakeAdapter drives the bus, modbus-serial client reads registers)docker build -t sunspec-gateway:test .succeeds, image is 227 MBdocker run …boots,/healthzreturns{"status":"ok"}, Modbus TCP binds0.0.0.0:5020docker compose --profile simulator upstarts the IoTech simulator on host:5021docker compose configvalidates the resolved YAMLdockerjob mirrors the smoke-test on every PRSources of truth
docs/QG0028_ASW6000-10000-S_EN_540-30170-03_V04_0723-2.pdfsrc/modbus/sunspec-registers.tsfrom PR2)Out of scope (deferred)