A containerized data scraper for APSolar ECU's written in node.js by Ryan Jacobs. It has the following features:
-
Prometheus metrics exporter
-
JSON data endpoint
-
MQTT data export
-
Data export to MQTT that is follows the topic format required for auto-discovery in Home Assistant.
Since most of us that have installed APSolar ECU's in the home don't have access to APSolar's monitoring site or API to get information and may be curious about exporting generation and all other data provided by the ECU into something a bit more query-able (at the very least), this data scraper scrapes the data from the ECU's local web page and provides a number of methods to ingest that data.
This project started life as an unofficial Prometheus exporter for the data provided by the APSolar ECU as Prometheus was the time-series database of choice for me at the time (and it's a little easier to get started with for something simple like this vs. InfluxDB). It became apparent quickly that having the scraper provide a number of different data formats for whatever someone might want to do with it was the way to go, and so it grew a JSON endpoint and not one, but two MQTT data formats.
At this time, providing support for InfluxDB is being considered for a future release.
NOTE: The APSolar ECU (at this time) only updates the statistics every 5 minutes. As a result, the scraper will only scrape every 5 minutes. Repeated hits to the scraper will produce a cached copy for those 5 minutes. Please set any downstream ingestion to respect this time limit.
For local development, use Node.js 24 or newer.
You will need to define at least the environment variable ECUHOST in order to run the image and ensure that port 3000 is forwarded somehow. Further configuration information is available below.
docker run --env ECUHOST=<your-ecu-host-or-ip> -p 3000:3000 djryanj/solar-scraper:latest
This repo includes a VS Code devcontainer in .devcontainer/devcontainer.json.
Use VS Code's Reopen in Container action to start a Node 24 development environment with port 3000 forwarded. The container runs npm install after creation.
Once the container is ready, start the app with npm run dev.
Run the automated test suite with npm test.
Print a terminal coverage summary with npm run test:coverage.
Use npm run test:watch while iterating locally.
The file test/integration/live.test.js contains tests that talk to a real ECU and a real MQTT broker. They are skipped automatically when the required environment variables are absent, so npm test always passes in CI and on machines without hardware access.
To run them, set the relevant variables before invoking the test runner:
ECU_HOST=192.168.100.2 MQTT_HOST=mqtt.mosquitto node --test test/integration/live.test.js| Variable | Required for | Default | Description |
|---|---|---|---|
ECU_HOST |
ECU test | — | IP address or hostname of the real APSolar ECU |
MQTT_HOST |
MQTT test | — | Hostname or IP of the MQTT broker |
MQTT_PORT |
MQTT test | 1883 |
Port of the MQTT broker |
MQTT_USERNAME |
MQTT test | — | Username if the broker requires authentication |
MQTT_PASSWORD |
MQTT test | — | Password if the broker requires authentication |
The ECU test scrapes both ECU endpoints and validates that the parsed panel data is structurally correct — in particular that gridFrequency values fall in the Hz range and are not accidentally populated with voltage values (regression for the dual-channel rowspan parsing bug).
The MQTT test opens a dedicated publisher and subscriber client, publishes a JSON payload to solar-scraper/integration-test at QoS 1, and asserts the subscriber receives the identical message. The topic uses retain: false so no state is left on the broker after the test completes.
- CHANGELOG.md tracks notable releases and points to Release Please as the long-term source of generated release history.
- CONTRIBUTING.md describes contribution expectations and validation commands.
- SECURITY.md explains how to report security issues.
- AI_NOTICE.md describes expectations for AI-assisted changes.
- .github/CODEOWNERS defines repository ownership for review routing.
- renovate.json configures automated dependency updates for npm, GitHub Actions, and container base images.
This repo now includes GitHub Actions workflows for CI, PR containers, release creation, and release container publishing.
CIruns on pull requests and pushes tomain.- It regenerates a clean
package-lock.json, installs dependencies withnpm ci, runsnpm run check, and runs the test suite with coverage.
PR Containerbuilds a multi-architecture image for non-draft pull requests.- For pull requests opened from branches in the same repository, it pushes the image to GitHub Container Registry as
ghcr.io/<owner>/solar-scraper:pr-<number>andghcr.io/<owner>/solar-scraper:pr-<number>-<sha>. - For pull requests opened from forks, it still performs the build but skips the push for safety.
Cleanup PR Containerdeletes the PR container versions after the pull request is merged.
Release Pleaseruns on pushes tomainand maintains a release PR plus GitHub Releases.- Releases are driven by Conventional Commits so the generated changelog and version bump are correct.
- When a GitHub Release is published,
Release Containerbuilds and pushes the final multi-architecture image to GitHub Container Registry. - Release container tags include the exact GitHub release tag, the plain version without the
vprefix, themajor.minorline, themajorline, andlatest. - The container build workflows bake
RELEASE_VERSION,GIT_REF, andGIT_SHAinto the image so runtime metadata reflects the actual GitHub release or PR build.
For a release such as v0.3.1, the published tags are expected to include:
ghcr.io/<owner>/solar-scraper:v0.3.1ghcr.io/<owner>/solar-scraper:0.3.1ghcr.io/<owner>/solar-scraper:0.3ghcr.io/<owner>/solar-scraper:0ghcr.io/<owner>/solar-scraper:latest
Preferred path:
Use this for normal releases. It keeps package.json, package-lock.json, the Release Please manifest, the GitHub Release, and the container tags aligned automatically.
- Merge feature and fix branches into
mainusing Conventional Commit messages. - Wait for the
Release Pleaseworkflow to open or update the release PR. - Review the release PR. It will include the version bump and changelog updates.
- Merge the release PR into
main. - Release Please will create and publish the GitHub Release.
- The
Release Containerworkflow will build and publish the multi-arch image for that release.
Manual fallback: Use this only when you need to force a specific version bump yourself.
- Choose the next version:
npm run release:bump:patchnpm run release:bump:minornpm run release:bump:major
- Review the resulting changes in
package.json,package-lock.json, and.release-please-manifest.json. - Commit the version bump and merge it to
main. - From a clean
maincheckout with GitHub CLI authenticated, create the tag and GitHub release draft:npm run release:draft- or
npm run release:draft -- v0.4.0to force a specific tag name
- Publish the GitHub release draft.
- The
Release Containerworkflow will publish the final container image for that release tag.
Notes:
- The normal Release Please flow is the source of truth and is the recommended release path.
- The manual bump commands only update repository version files; they do not create git tags or GitHub Releases for you.
npm run release:draftrequires a clean working tree, themainbranch,git, and the GitHub CLI (gh) authenticated against the target repository.- If you use the manual fallback, keep the GitHub Release tag format consistent so the release container workflow produces the tags you expect.
- GitHub Actions package permissions must be enabled so workflows can publish to GitHub Container Registry.
- If package deletion via the default
GITHUB_TOKENis restricted in your repository or organization, add aGHCR_CLEANUP_TOKENrepository secret with permission to delete package versions.
Configuration is done via environment variables only.
The following environment variables directly affect the operation of the scraper. Only one is required to be set; the rest are optional depending on what features you want to do (and the configuration required for them). There are defaults for all.
| Variable | Required | Description | Default |
|---|---|---|---|
ECUHOST |
Yes | IP or hostname of the APSolar ECU to scrape. | 192.168.1.1 |
PORT |
No | The port that the container listens on (internally). Be sure to change the port Docker forwards if you change this. | 3000 |
SCRAPE_INTERVAL_MS |
No | Poll interval for scraping the ECU and publishing MQTT state. | 300000 |
REQUEST_TIMEOUT_MS |
No | HTTP timeout for ECU page fetches in milliseconds. | 15000 |
USE_MQTT |
No | Set to true if you want to use generic MQTT. See the MQTT section for more information. Be sure to also set MQTT_HOST!! | False |
USE_HA_MQTT |
No | Set to true if you want to use Home Assistant to auto-discover solar information via MQTT. Note that USE_MQTT and USE_HA_MQTT are independent; you do not need to set USE_MQTT to true if USE_HA_MQTT is true. Be sure to also set MQTT_HOST!! | False |
MQTT_HOST |
No | Set to the IP address or DNS name of your MQTT server. Both USE_MQTT and USE_HA_MQTT use the same host. IMPORTANT! The default host could potentially expose information to a public server you don't want exposed. Be sure to set this if you set USE_MQTT or USE_HA_MQTT to true! NOTE: At this time, providing a self-signed certificate file for TLS communications is not supported. If your MQTT server uses PKI from a well-known certificate authority such as letsencrypt (be sure that the WHOLE chain is sent), it should work. At this time, self-signed cert behaviour is untested. | test.mosquitto.org |
MQTT_PORT |
No | The TCP port on which to connect to the MQTT server. | 1883 |
MQTT_USERNAME |
No | If you need to supply a username for your MQTT server, set it here. | null/unset (works for anonymous servers) |
MQTT_PASSWORD |
No | If you need to supply a password for your MQTT server, set it here. IMPORTANT! This will be visible in the logs and if you get container information from the Docker host. You could provide this via Docker secrets. | null/unset (works for anonymous servers) |
MQTT_TOPIC |
No | Base topic for standard MQTT data. See MQTT section for more information. | home/solar |
HA_MQTT_TOPIC |
No | Base topic for Home Assistant auto-discovery. This must match the base topic in your Home Assistant configuration.yaml. Note: this can only be a single label (e.g. homeassistant or hass) or auto-discovery will fail. |
homeassistant |
The following environment variables provide information into the scraper but do not affect operation beyond the cosmetic (they are exported with metrics and are visible on the browseable home page). None of these are required.
| Variable | Description | Default |
|---|---|---|
SITENAME |
A friendly site name. Mostly visible on the home page of the scraper but also exported with metrics. | Your House! |
GIT_SHA |
Short or full git commit SHA to expose in the app, metrics, and MQTT metadata. The GitHub container workflows set this automatically. | local git short SHA or missingGitSha |
GIT_REF |
Git ref associated with the running build, such as a branch name, tag name, or PR identifier. The GitHub container workflows set this automatically. | NODE_ENV or local |
RELEASE_VERSION |
Release identifier to expose in the UI and metadata. For release builds this should match the GitHub Release tag. | package.json version |
Exposes port tcp/3000 by default. Change the PORT environment variable if you want to change it. Leverage something like jwilder/nginx-proxy to automatically forward a hostname from the Docker host's port 80 or 443 to this container.
There is a default cosmetic endpoint at / (e.g. http://docker-host:3000/) that shows all data in a human-readable form.
Prometheus expects an endpoint at /metrics, which is where it is. In addition to the custom metrics provided by this scraper, there are node.js-specific metrics exported as well that are not presented here. You can browse the endpoint if you wish.
All metrics are prefixed by solar_. The list of metrics is below (including the prefix for clarity).
| Metric | Description |
|---|---|
solar_total_generated |
Total solar power generated over the lifetime of the array in kilowatt hours (kWh). |
solar_daily_generated |
Total solar power generated today in kilowatt hours (kWh). |
solar_current_total_power |
Current solar power output in watts (W). |
solar_carbon_offset |
Estimated carbon offset in kg of CO2 saved over the lifetime of the solar array. |
solar_trees_planted |
Estimated carbon offset by the equivalent number of trees that have been planted over the lifetime of the solar array. |
solar_gallons_offset |
Estimated carbon offset in gallons of gasoline saved over the lifetime of the solar array. |
solar_scraper_info{version="<version>",hostname="<hostname>",gitSha="<gitSha>",ecuHost="<ecu-host>"} |
Version and other information. |
The following metrics will be one per panel in your array, labelled by inverter ID (see the Administration->ID Management or the Real Time Data links on the ECU to see what inverters are configured). Inverters that have multiple panels connected (e.g. the YC500/600 or YC1000) will have an -A, -B etc. appended to the end.
| Metric | Description |
|---|---|
solar_panel_power{inverterId=<inverterId>} |
Current power output of the panel in watts (W). |
solar_panel_grid_frequency{inverterId=<inverterId>} |
Current grid frequency measured by the panel in hertz (Hz). |
solar_inverter_temperature{inverterId=<inverterId>} |
Current inverter temperature in °C. |
solar_inverter_grid_voltage{inverterId=<inverterId>} |
Current grid voltage output by the inverter in volts A/C (V). |
Although only one MQTT host is allowed, you can output 2 kinds of MQTT data. One is more generically formatted and published when USE_MQTT is true; the other is formatted for use by the MQTT auto-discovery function in Home Assistant, enabled by setting the USE_HA_MQTT environment variable to true.
The generically-formatted data could be suitable for consumption into Prometheus through something like MQTTGateway for Prometheus. This would achieve a decoupling of the scraper and Prometheus, which could be useful in large systems with many ECU's (though I doubt such a thing would need this software ;) ).
The MQTT_TOPIC and HA_MQTT_TOPIC environment variables, set to home/solar and homeassistant respectively by default, are the base topics upon which the rest of the topic labels are built.
IMPORTANT: Do not use more than a single label (e.g. homeassistant or hass) for the Home Assistant auto-discovery base topic or auto-discovery will fail.
The following metrics are exported to the MQTT_TOPIC (e.g. home/solar):
| Metric | Description |
|---|---|
/dailyGen |
Total solar power generated today in kilowatt hours (kWh). |
/totalGen |
Total solar power generated over the lifetime of the array in kilowatt hours (kWh). |
/currentSystemPower |
Current solar power output in watts (W). |
/treesPlanted |
Estimated carbon offset by the equivalent number of trees that have been planted over the lifetime of the solar array. |
/gallonsSaved |
Estimated carbon offset in gallons of gasoline saved over the lifetime of the solar array. |
/carbonOffset |
Estimated carbon offset in kg of CO2 saved over the lifetime of the solar array. |
/scraper/version |
Running version number of the scraper software. |
/scraper/hostname |
Hostname on which the software is running. |
/scraper/releaseVersion |
Release identifier of the running image or local build. |
/scraper/gitSha |
Short git SHA of the running image or local build. |
/scraper/ecuHost |
Configured ECU IP address or hostname. |
The following topics will be one per panel in your array, labelled by inverter ID (see the Administration->ID Management or the Real Time Data links on the ECU to see what inverters are configured). Inverters that have multiple panels connected (e.g. the YC500/600 or YC1000) will have an -A, -B etc. appended to the end.
| Metric | Description |
|---|---|
/panels/<inverterId>/currentPower |
Current power output of the panel in watts (W). |
/panels/<inverterId>/gridVoltage |
Current grid voltage output by the inverter in volts A/C (V). |
/panels/<inverterId>/temperature |
Current inverter temperature in °C. |
/panels/<inverterId>/gridFrequency |
Current grid frequency measured by the panel in hertz (Hz). |
/ping |
Unix-timestamp for the last published MQTT data. |
The following metrics are exported to the HA_MQTT_TOPIC (e.g. homeassistant).
All of these will be discovered as sensor within Home Assistant, due to the topics' full format e.g. homeassistant/sensor/totalGen/state. For more information, see MQTT auto-discovery function in Home Assistant. Therefore, all of the below are relative to HA_MQTT_TOPIC/sensor.
Note: For brevity, the /state suffix for each topic is not added on to the topic name (this is usually not seen in Home Assistant anyway). /config topics are not presented.
| Topic | Data | Home Assistant Sensor Name |
|---|---|---|
/carbonOffset/carbon |
Estimated carbon offset in kg of CO2 saved over the lifetime of the solar array. | sensor.carbon_offset_kg_of_co2 |
/carbonOffset/trees |
Estimated carbon offset by the equivalent number of trees that have been planted over the lifetime of the solar array. | sensor.carbon_offset_trees_planted |
/carbonOffset/gallons |
Estimated carbon offset in gallons of gasoline saved over the lifetime of the solar array. | sensor.carbon_offset_gallons_of_gasoline_saved |
/dailyGen |
Total solar power generated today in kilowatt hours (kWh). | sensor.power_generated_today |
/currGen |
Current solar power output in watts (W). | sensor.current_solar_power_output |
/totalGen |
Total solar power generated over the lifetime of the array in kilowatt hours (kWh). | sensor.lifetime_solar_power_generated |
Each of the below metrics will exist one per panel/inverter ID. The panel # for sensors should be the same every run, unless additional inverters are added.
| Topic | Data | Home Assistant Sensor Name |
|---|---|---|
/solar_panel_<inverterID>/power |
Current power output of the panel in watts (V). | sensor.solar_panel_<#>_power |
/solar_panel_<inverterID>/temperature |
Current inverter temperature in °C. | sensor.solar_panel_<#>_temperature |
/solar_panel_<inverterID>/frequency |
Current grid frequency measured by the panel in hertz (Hz). | sensor.solar_panel_<#>_frequency |
/solar_panel_<inverterID>/voltage |
Current grid voltage output by the inverter in volts A/C (V). | sensor.solar_panel_<#>_voltage |
The following additional topics are published:
- A JSON-formatted string of all data is published to
HA_MQTT_TOPIC/json(e.g not atHA_MQTT_TOPIC/sensor/json. Unsure at this time how useful that is, but Home Assistant can read JSON-formatted MQTT topics. MQTT_TOPIC/pingis always published if eitherUSE_MQTTorUSE_HA_MQTTistrue
NOTE: Scraper versioning info is not currently published to a Home Assistant topic. This is a decision I made because at this time, I can't see a reason to include it. It's mostly useless information for most users, more so for Home Assistant users because of the way the data is surfaced and likely to be used in Home Assistant, and if it's desired it's available on the other topic for manual addition anyway.
The scraper will return a JSON-formatted string by browsing /json, e.g. http://docker-host:3000/json. This is useful for consumption into a wide variety of downstream things.
There is no other API available.
Here is a sample config block for Prometheus:
- job_name: solar_ecu
scrape_interval: 5m
static_configs:
- targets:
- <hostname>