Tracking issue: pypowerwall/issues#254 (to be ported to pypowerwall-server repo)
This plan covers four features to support sites that have one or more standalone Tesla inverters (no Powerwall batteries) connected alongside — or instead of — a full gateway.
Problem: The server treats all configured gateways identically. An inverter-only
gateway has no battery data — soe, system_status.battery_blocks, and reserve will
always be empty — but the console still renders Powerwall Status and battery graphics for
it, showing blank panels.
Approach: Add an explicit type field to the gateway configuration. Auto-detection
from polling data is brittle (empty battery data could also mean a temporarily offline
unit); explicit declaration is cleaner and more reliable.
app/models/gateway.py — Gateway model:
# New field with default so existing configs need no changes
type: str = "powerwall" # "powerwall" | "inverter"GatewayStatus should surface this field so API consumers and the console can read it.
gateways.yaml — add type: inverter example:
# Example: Standalone inverter (no Powerwall batteries)
- id: inverter-south
name: South Array Inverter
host: 192.168.91.1 # or travel-router IP
gw_pwd: your_gateway_password
type: inverter # Omit Powerwall battery panels in console
timezone: America/Los_Angelesapp/core/gateway_manager.py — pass type through to GatewayStatus so it is
visible in /api/gateways responses.
Tests: Add a type: inverter fixture gateway and assert that /api/gateways/{id}
returns the correct type.
Problem: When multiple gateways are configured, the Alerts, Solar Strings, and Powerwall Status panels show data from the "default" gateway only. Users with an inverter on a separate gateway lose visibility into its strings and alerts.
Approach: Add new aggregate API endpoints that return per-gateway data. The console detects multiple gateways at load time and conditionally renders labeled sub-sections. Single-gateway installs see no visual change.
GET /api/aggregate/strings → { "home": {"A": {...}, "B": {...}}, "inverter-south": {"A": {...}} }
GET /api/aggregate/alerts → { "home": ["FWUpdateSucceeded", ...], "inverter-south": [...] }
GET /api/aggregate/vitals → { "home": {...}, "inverter-south": {...} }
Each endpoint iterates gateway_manager.list_gateways() and returns data keyed by
gateway ID, skipping offline gateways (or including them with null data, TBD by design
preference). These read from the existing per-gateway cache — no new polling needed.
The console already fetches /api/gateways on load to build the System Health panel.
Extend startup to also load the gateway list and count.
When gateways.length === 1: Current behavior, no labels.
When gateways.length > 1:
- Alerts panel: Render one
<div class="gateway-section">per gateway with a bold gateway name header, then the existing color-coded alert list inside it. - Solar Strings panel: One labeled section per gateway with its own strings table.
- Powerwall Status panel: One labeled section per gateway that has
type === "powerwall". Inverter-only gateways are entirely omitted from this panel (they have no batteries). The Total Energy / Backup Reserve / Time Remaining block at the top of this panel aggregates only across powerwall-type gateways.
Battery graphics and the System Health panel are unaffected (they already aggregate or are gateway-aware).
- New tests in
tests/test_api_aggregates.pyfor the three new endpoints. - Mock a two-gateway fixture (one
powerwall, oneinverter) and assert correct shape.
Problem: Both a full gateway and a standalone inverter may return a solar string named
"A". When two gateways both report "A", the aggregate /strings endpoint (or a
Telegraf/InfluxDB pipeline that merges them) will silently discard one of them.
Approach: The new /api/aggregate/strings endpoint (Feature 2) returns strings
nested under the gateway ID key, which is enough disambiguation for API consumers.
For the legacy /strings endpoint, nothing changes (it serves only the default gateway,
no collision possible).
For users who push data to InfluxDB via the aggregate endpoint, the gateway-keyed
structure (home/A, inverter-south/A) makes it trivial to add a gateway tag in
Telegraf without needing [[processor.rename.replace]] workarounds.
Console: The Solar Strings section (Feature 2) already solves this for the UI by showing strings inside a per-gateway labeled section.
No changes needed to legacy.py /strings. Changes are entirely in
app/api/aggregates.py (the new endpoint from Feature 2).
Problem: Users who bridge their Powerwall's Tesla Wi-Fi using a travel router with
port-forwarding (e.g., 192.168.1.50:8443 → 192.168.91.1:443) cannot configure
pypowerwall-server to reach the gateway at a non-standard port because the host field
accepts only a hostname/IP and pypowerwall.Powerwall() has no separate port kwarg.
URL construction in both pypowerwall/local/pypowerwall_local.py and
pypowerwall/tedapi/__init__.py interpolates self.host / self.gw_ip directly into
HTTPS URLs ("https://%s%s" % (self.host, api)), so host:port strings produce valid
URLs like https://192.168.1.50:8443/api/... without any changes to those modules.
The only blocker was input validation in pypowerwall/__init__.py
_validate_init_configuration(): IPV4_6_REGEX and HOST_REGEX only match bare
hosts. Fixed in the local copy by stripping any :<digits> port suffix before the
regex check. This change has already been applied to pypowerwall/__init__.py.
app/models/gateway.py — Gateway model:
port: Optional[int] = None # Non-standard port (default: 443 / pypowerwall default)gateways.yaml — add example:
# Example: Gateway behind a travel router on a custom port
- id: garage
name: Garage Gateway (travel router)
host: 192.168.1.50
port: 8443
gw_pwd: your_gateway_password
timezone: America/Los_Angelesapp/core/gateway_manager.py — when building TEDAPI kwargs:
effective_host = f"{config.host}:{config.port}" if config.port else config.host
tedapi_kwargs = {"host": effective_host, "gw_pwd": config.gw_pwd, ...}Tests: Mock gateway with port: 8443 and assert the correct host string is passed
to pypowerwall.Powerwall().
If host:port does not work inside pypowerwall, document the limitation in the README
with a note that users can configure their travel router to forward port 443 to the
gateway, making a custom port unnecessary at the pypowerwall level.
| Step | Feature | Files | Status |
|---|---|---|---|
| 1 | Gateway type field | models/gateway.py, gateways.yaml, gateway_manager.py |
✅ Done (v0.1.13) |
| 2 | Investigate host:port in pypowerwall |
pypowerwall/__init__.py |
✅ Done — stripped port suffix before regex validation |
| 3 | /api/aggregate/strings, /alerts, /vitals endpoints |
api/aggregates.py |
✅ Done (v0.1.13) |
| 4 | Per-gateway labeled console sections | static/index.html |
✅ Done (v0.1.13) |
| 5 | Non-standard port config | models/gateway.py, gateway_manager.py, gateways.yaml |
✅ Done (v0.1.13) |
- mDNS/auto-discovery of gateways on the local network
- Built-in NAT/iptables management for multiple
192.168.91.1addresses - Inverter start/stop control buttons (separate feature, tracked elsewhere)