Skip to content

Add TimescaleDB (PostgreSQL) as an alternative/parallel datastore - #830

Open
youzer-name wants to merge 45 commits into
jasonacox:mainfrom
youzer-name:feature/timescaledb-support
Open

Add TimescaleDB (PostgreSQL) as an alternative/parallel datastore#830
youzer-name wants to merge 45 commits into
jasonacox:mainfrom
youzer-name:feature/timescaledb-support

Conversation

@youzer-name

@youzer-name youzer-name commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds TimescaleDB (PostgreSQL) as an opt-in, experimental alternative/parallel datastore to InfluxDB. powerwall.yml, setup.sh, and compose.env.sample are untouched — byte-identical to main. Everything lives under tools/timescaledb/, enabled via tools/timescaledb/setup.sh and the stack's existing powerwall.extend.yml add-on mechanism (the same pattern tools/tesla-history and tools/pgadmin already use), not the main ./setup.sh. InfluxDB remains the default, tested datastore and is never disabled or modified.

Note on how this PR has changed shape: earlier revisions of this branch wired TimescaleDB directly into powerwall.yml/setup.sh via Compose profiles. Per review feedback, that broke existing InfluxDB installs on upgrade in several independent ways — profiles silently stopped influxdb/telegraf for pre-existing installs since upgrade.sh never set COMPOSE_PROFILES; Grafana's env_file unconditionally required timescaledb.env, which only setup.sh creates, so the whole stack failed to start without it; weather411 became a mandatory local build for every install, including InfluxDB-only ones, risking ARM build failures; and none of it was labeled EXPERIMENTAL despite being the first prompt every new user saw. This revision reverts the core files to stock and moves the whole feature to tools/timescaledb/ as a self-contained, fully opt-in extension instead — see timescaledb/README.md for the design writeup.

  • Ingestion: a second, independent Telegraf instance (telegraf-timescale) polls pypowerwall the same way the stock telegraf does, writing raw data into TimescaleDB via outputs.postgresql. weather411 writes pw_weather_log directly (its TimescaleDB write path is entirely optional — an absent/disabled [TimescaleDB] config section skips it). A lightweight aggregate-cron sidecar runs the SQL scripts in timescaledb/aggregate/ on a 60s schedule — the TimescaleDB equivalent of InfluxDB's continuous queries — producing pw_autogen_1m, pw_kwh_1h, pw_grid_1m, pw_vitals_log, pw_pod_log, pw_pwtemps_log, pw_alerts_log, pw_strings_log, and pw_fans_log.
  • Server options: a bundled container (default) or an existing PostgreSQL/TimescaleDB server you already run — tools/timescaledb/setup.sh prompts for host/port/credentials either way and writes them to a gitignored timescaledb.env; re-run it any time to switch modes or update connection details.
  • Migration: timescaledb/migrate/ can backfill full InfluxDB history into TimescaleDB (including from a different/external InfluxDB source), with per-source checkpointing so re-runs are safe, and an option to permanently skip the prompt.
  • Dashboard: a new dashboard-timescaledb.json, and a Grafana datasource template (grafana/timescaledb-template.yml) that only gets provisioned when the extension is actually set up — so an InfluxDB-only install never sees a broken TimescaleDB datasource.
  • Tooling: tools/tesla-history gained multi-target support (--target influxdb|timescaledb|both); an optional pgAdmin service (tools/pgadmin/) for browsing TimescaleDB directly, added the same opt-in way; sample backup/restore scripts and instructions for pg_dump/pg_restore (including the --clean caveat against hypertables); an experimental MCP server (below).
  • MCP server: tools/timescaledb/mcp/, an extension-of-the-extension exposing an MCP (Model Context Protocol) server so an AI agent (Claude, etc.) can explore the TimescaleDB schema and run read-only SQL against real Powerwall history conversationally. Ported from tools/powerwall-mcp's InfluxDB version, but not a 1:1 translation — this schema has no retention-policy concept and a mix of "wide" (one column per field) and narrow/EAV (time, metric_name, value) tables that need different schema-exploration tools (get_columns vs. get_metric_names), and being real Postgres, read-only access is enforced with an actual least-privilege database role (readonly_role.sqlSELECT-only, no write/DDL grants, search_path pinned to public) rather than relying on query-string validation alone the way the InfluxDB version has to. Not wired into tools/timescaledb/setup.sh — a fully manual, opt-in add-on on top of an already-opt-in extension.
  • Docs: timescaledb/README.md covering architecture, setup, and gotchas (host-side psycopg2-binary requirement, the transient "no default database" Grafana race on first boot, the wide-vs-narrow table shape rationale, etc.), plus new tools/README.md entries for the TimescaleDB, pgAdmin, and MCP extensions.

Also included — a bug found and fixed while dogfooding this on a real install:

  • weather411's TimescaleDB write path used datetime.utcfromtimestamp(), which returns a naive datetime; psycopg2 sends naive datetimes to Postgres with no offset, so they get reinterpreted using the session's TimeZone setting instead of UTC — silently shifting every stored weather row by the local UTC offset. Fixed to use a timezone-aware UTC datetime instead.

Test plan

  • Fresh install with TimescaleDB (via tools/timescaledb/setup.sh) alongside InfluxDB, and InfluxDB-only (regression check) — confirmed powerwall.yml/setup.sh/compose.env.sample are unmodified and the core stack behaves identically without the extension
  • Both bundled-container and existing-external-server modes, and switching between them
  • Full InfluxDB → TimescaleDB historical migration, including from an external InfluxDB source
  • Verified aggregate-cron's output against the equivalent InfluxDB continuous queries for accuracy
  • Verified the weather timezone fix end-to-end (new writes land with zero offset; corrected historical rows written under the bug)
  • dashboard-timescaledb.json renders correctly against a populated TimescaleDB instance
  • Multiple powerwall.extend.yml add-ons manually merged and coexisting on a live external-TimescaleDB-server install (TimescaleDB extension + MCP server together in one file)
  • MCP server validated end-to-end on a live install, twice: built the image, ran it as a standalone container, drove all 5 tools through a real MCP client, confirmed the read-only role blocks writes/DDL while allowing SELECT, confirmed search_path pinning holds, confirmed LIMIT auto-cap/clamp and the comment/multi-statement/keyword rejections, confirmed bearer-token auth. Then a second, full clean-room pass through the actual documented setup steps end-to-end via docker compose (not docker run, which is what let the first issue below slip through) — copy the env template, run readonly_role.sql, merge the real .sample service block, build, start, exercise it. Found and fixed three real gotchas this way: a depends_on: timescaledb that broke docker compose up outright for external-server installs, a password-rotation footgun in readonly_role.sql (re-running it after changing the password silently kept the old one), and a hardcoded database name in a GRANT that would've misfired for anyone not using the default POSTGRES_DB value

youzer-name and others added 26 commits July 14, 2026 09:58
…afana config)

Adds an alternative TimescaleDB backend alongside InfluxDB: raw/aggregate
schema, per-metric migration scripts, continuous-aggregate SQL, a
telegraf output config, and a Grafana datasource + dashboard.
Rebuild after the timezone-file hook was lost in the main resync reset.
Rebuild after this was lost in the main resync reset. Weather411 can now
write straight to pw_weather_log (bypassing Telegraf polling), toggled
independently of InfluxDB via [TimescaleDB] ENABLE in weather411.conf.
Rebuild after this was lost in the main resync reset. influxdb/telegraf
gated behind profile "influxdb"; new timescaledb/telegraf-timescale/
aggregate-cron services gated behind profile "timescaledb". grafana and
weather411 no longer hard-depend on influxdb (both now tolerate either
datastore being absent). weather411 switches to a local build so it can
pick up psycopg2-binary. Validated with `docker compose config` under
--profile influxdb, --profile timescaledb, and both together.
Rebuild after this was lost in the main resync reset. Adds a "Select
datastore" prompt (InfluxDB / TimescaleDB / Both) that drives
COMPOSE_PROFILES, always provisions timescaledb.env with a random
password, toggles weather411's per-datastore ENABLE flags, applies
timescaledb/schema.sql and offers the InfluxDB->TimescaleDB migration
via an ephemeral container, and gates the InfluxDB-only setup steps
and final Grafana instructions behind the active profile(s).
Rebuild after this was lost in the main resync reset. tesla-history.py
can now import into InfluxDB, TimescaleDB, or both, selectable via the
config file's [InfluxDB]/[TimescaleDB] ENABLE settings or --target
{influxdb,timescaledb,both} on the command line.

TimescaleDB writes mirror the existing InfluxDB "never overwrite
existing data" semantics via ON CONFLICT ... COALESCE upserts against
pw_autogen_1m/pw_grid_1m/pw_pod_log, with a new source column to track
cloud-imported vs. live-ingested rows for --remove. That column is
deliberately excluded from every UPDATE SET clause: including it would
flip an existing live row's source to 'cloud' the moment any other
column on that row got a cloud-fill (COALESCE treats an existing NULL
as unset), which would make --remove delete live gateway data -
verified by seeding a live NULL-source row, merging in a cloud value,
and confirming source stayed NULL after the fix.

When --target both is active, a gap found in either datastore is
filled from a single Tesla cloud fetch so the two datastores can't
drift apart (search_databases/merge_periods). Also updates the
Dockerfile (psycopg2-binary + postgresql-client for the psql-based
kwh_backfill.sql call), powerwall.extend.yml.sample (local build,
timescaledb.env/PGHOST/PGPORT wiring), and README.

Verified: --help argparse output, full interactive config creation for
both InfluxDB-only and TimescaleDB-only paths (correct ENABLE flags
and prompts), and config-file read-back with --target override, all
via direct script execution.
Closes a gap flagged (but deliberately deferred) in the original
TimescaleDB port: the version stat panel had no TimescaleDB equivalent
because ver.sh's inputs.exec output wasn't being written anywhere.

telegraf-timescale.conf now runs the same inputs.exec/ver.sh block as
the stock telegraf.conf, mounted into the telegraf-timescale service
the same way. Output lands in a lazily-created powerwall_dashboard
table (hypertabled/3-day-retained by bootstrap_raw_tables.sql, same
pattern as http/alerts). Dashboard row + stat panel added back to
dashboard-timescaledb.json, querying last version by time.

Verified against an isolated disposable TimescaleDB + Telegraf pair
(not the host's real production stack): schema applies, create_templates
auto-hypertables powerwall_dashboard on first write with the correct
version/file_ts columns, bootstrap_raw_tables.sql adds the 3-day
retention policy, and the panel's exact query returns the right value.
…tory)

Fixes "pull access denied for weather411-timescaledb, repository does
not exist" on setup: when both build: and image: are set, Compose
tries to pull the image tag first and only falls back to building on
failure. Since these images are never published to a registry, that
pull always fails - cosmetic/non-fatal on some Compose versions, hard
error on others. pull_policy: build skips the pull attempt entirely.

Reproduced the exact reported error locally, confirmed the fix with a
fresh (uncached) build showing straight to "Building" with no pull
step or error message.
Investigated a user report of "you do not currently have a default
database configured" on Energy Usage/Grid Status right after a fresh
setup. Reproduced the exact provisioning (same datasource YAML, same
dashboard-timescaledb.json import) against an isolated Grafana +
TimescaleDB pair and ran every affected panel's queries directly via
/api/ds/query -- all returned 200 with no errors. The user also
confirmed it cleared on its own after a container restart. Root cause
is Grafana serving requests before the postgres datasource has
finished settling against a freshly-started TimescaleDB, not a dashboard
or provisioning defect. Documented in the Gotchas section so it's not
mistaken for a real bug on the next fresh install.
Running timescaledb/migrate/*.py or tesla-history.py --target
timescaledb/both directly on the host (rather than via setup.sh's
ephemeral container or the tesla-history Docker image) needs
psycopg2-binary installed separately - not obvious since neither
script's error message points at it directly.
A user hit "Failed to backfill pw_kwh_1h: FileNotFoundError(2, 'No
such file or directory')" running tesla-history.py --target
timescaledb directly on a host without the postgresql-client package.
Reproduced exactly (repr(FileNotFoundError(2, 'No such file or
directory')) matches character-for-character) - subprocess.run(["psql",
...]) raises this when the psql binary itself isn't on PATH, not when
the SQL file argument is missing.

update_timescaledb() now catches FileNotFoundError specifically and
explains what's missing, that the main write already succeeded (no
data lost), and that re-running the same date range once psql is
installed will backfill the gap. Documented the psql/psycopg2-binary
host dependency in both READMEs.
The interactive TimescaleDB Setup prompt has no default to fall back
on for the password field, unlike host/port/database/user. Document
that it's safe to leave blank when running via docker (POSTGRES_PASSWORD
from timescaledb.env overrides the config file value on every run -
verified against tesla-history.py's os.getenv('POSTGRES_PASSWORD', ...)
precedence), but running the script bare on the host requires manually
looking up the auto-generated value in ../../timescaledb.env.
A user asked whether the InfluxDB->TimescaleDB migration tool is safe
to re-run later (e.g. skip migration on first install, come back
weeks after TimescaleDB has been live-ingesting on its own). It
wasn't, for pw_autogen_1m/pw_grid_1m: insert_wide() used a plain
ON CONFLICT (time) DO UPDATE SET col = EXCLUDED.col, which
unconditionally overwrites. Since the walk-back migration always
processes every month from InfluxDB's true earliest data up to now on
a first run, a delayed migration would silently clobber whatever
TimescaleDB had already live-ingested for the overlap period.

The other seven migrate_*.py scripts already used ON CONFLICT DO
NOTHING (safe). insert_wide() now uses the same
COALESCE(dest_table.col, EXCLUDED.col) fill-gaps-only pattern already
established in tesla-history.py's write_timescaledb(), making the
migration tool safe to run at any time, not just during initial setup.

Verified live: seeded a row with real home/solar values and NULL
from_pw, ran insert_wide() with different home/solar plus a real
from_pw at the same timestamp, confirmed home/solar were preserved
and only from_pw (genuinely missing) was filled in.
Modeled after backup.sh.sample and the InfluxDB section of
backups/README.md, but using pg_dump/pg_restore instead of a raw
directory copy -- TimescaleDB has an active WAL, so copying
timescaledb/data while the container is running risks a torn,
inconsistent snapshot (same reasoning the InfluxDB script already
applies by using `influxd backup` instead of copying influxdb/
directly).

Verified end-to-end against an isolated TimescaleDB pair: pg_dump -Fc
from a live database with real data, pg_restore into a fresh database,
confirmed identical rows and hypertable/compression metadata. Also
discovered and documented that pg_restore's usual --clean flag does
NOT work against hypertables (TimescaleDB rejects the `ALTER TABLE
ONLY ... DROP CONSTRAINT` it generates) -- the restore instructions
use drop-and-recreate-the-database instead, which was confirmed to
restore cleanly with zero errors.
… selected

setup.sh previously hardcoded the InfluxDB migration source to this
stack's own influxdb:8086 whenever InfluxDB was part of the run, and
never prompted for credentials at all. That breaks a real scenario:
standing up a fresh dual-database stack to evaluate TimescaleDB while
backfilling it from an existing InfluxDB history running elsewhere.

Now the host/port/database/credentials prompt always runs (defaulting
to the local container so the common case is just hitting enter), and
credentials pass through a temp --env-file instead of -e so the
password isn't left sitting in docker inspect/ps output.
migration_progress previously keyed checkpoints only on
(source_measurement, year, month), which was fine while the migration
source was always this stack's own InfluxDB container. Now that
setup.sh can point the migration at an external InfluxDB server, a
checkpoint recorded against one source (e.g. an earlier run against
the local/empty container) was silently being read as "done" for a
completely different source, skipping real data with no error.

Add a "source" column to the checkpoint's primary key and thread a
source_key (host:port/db) from get_config() through already_done(),
mark_progress(), run_migration(), and all migrate_*.py call sites, so
a checkpoint is only ever considered valid for the source it actually
came from.
setup.sh previously re-asked whether to migrate InfluxDB history into
TimescaleDB on every run, which gets annoying once a user has already
decided. Answering "never" now sets PWD_SKIP_INFLUX_MIGRATE_PROMPT in
compose.env to suppress future prompts; plain "no" still asks again
next time. Removing/resetting the flag re-enables the prompt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…o default database" error

grafana has no depends_on for timescaledb (it must start regardless of
which datastore profile is active), so it was starting in parallel with
TimescaleDB and its postgres datasource plugin's first connection attempt
could land before Postgres was accepting connections. That failure doesn't
get retried until the datasource's settings change, so dashboards kept
showing "you do not currently have a default database configured for this
data source" on every restart until manually hitting Save & Test -- despite
an earlier (incorrect) assumption in timescaledb/README.md that this
cleared on its own.

Fixed with depends_on: timescaledb: condition: service_healthy, required:
false -- required: false keeps this a no-op when timescaledb isn't in the
active profile set, but gates Grafana's startup on TimescaleDB's
pg_isready healthcheck when it is. Verified with an isolated Grafana +
TimescaleDB compose stack: the first query after a fresh `docker compose
up`, with no restart or manual Save & Test, now succeeds immediately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ana's documented format

The provisioning file only set the top-level "database" field. Grafana's
own docs provision the postgres datasource with the database name under
jsonData.database instead, and there's a documented Grafana 12.x
regression around exactly this placement producing "you do not currently
have a default database configured for this data source" -- the same
error being chased on a real install where the earlier depends_on/startup-
ordering fix (03749a1) turned out NOT to be the actual cause (confirmed:
TimescaleDB's healthcheck was passing well before Grafana started, thanks
to Docker's 5s start_interval default -- Grafana was correctly waiting).

Keeping the top-level field too rather than replacing it, since it's
unconfirmed whether this alone resolves the real-world symptom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…er generated env files

Both are generated with real secrets/personal data (POSTGRES_PASSWORD,
and lat/long) by setup.sh, the same as compose.env/grafana.env/etc., but
never got added to .gitignore when TimescaleDB support was built. They'd
stayed untracked so far only because no one had run `git add`, not
because .gitignore actually protected them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…base), remove dangling CLAUDE.md references

The depends_on fix (03749a1) was real but not what fixed the reported
"no default database" error; the actual cause was Grafana's frontend
validation checking jsonData.database while provisioning only set the
legacy top-level database field (701bf69). Also drops two leftover
references to CLAUDE.md, a working-notes file that was folded into this
README and deleted before the tsdb_files/ cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Opt-in via powerwall.extend.yml, same pattern tesla-history already
uses -- not wired into setup.sh. Pre-populates a server pointing at
the stack's own timescaledb service; the extend file's ports: entry
only adds the new pgadmin service and deliberately doesn't touch
timescaledb's own port mapping, since Compose concatenates ports:
lists across files rather than replacing them (TIMESCALEDB_PORTS in
compose.env remains the right way to expose TimescaleDB itself).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ndled container

Adds a setup.sh prompt (bundled vs. existing PostgreSQL/TimescaleDB server)
for users who already run TimescaleDB elsewhere, e.g. in a homelab. The
bundled timescaledb container moves to its own "timescaledb-local" Compose
profile so it can be omitted; telegraf-timescale, aggregate-cron, grafana's
datasource, and weather411 all read TIMESCALEDB_HOST/PORT/SSLMODE from
timescaledb.env instead of assuming the "timescaledb" service name, and
setup.sh's wait/schema-apply and migration steps go through aggregate-cron
(always present) instead of exec'ing into the bundled container directly.
Documented prerequisites and the new option in timescaledb/README.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Grafana computes a default UID for provisioned datasources that don't
specify one, independent of the name field. The Sep 2024 "auto
provisioned" rename (bba3e60) changed the InfluxDB and Sun-and-Moon
datasource names without adding explicit uids, so any install with a
pre-rename copy of either datasource already in Grafana's DB hits a
UID collision -- and a datasource provisioning failure is fatal to
Grafana's startup -- the next time setup.sh regenerates these files
and Grafana restarts. Giving all three provisioned datasources
(InfluxDB, Sun and Moon, TimescaleDB) a fixed uid makes provisioning
idempotent regardless of future name changes.
…C offset

datetime.utcfromtimestamp() returns a naive datetime -- psycopg2 sends
naive datetimes to Postgres with no offset, so they get interpreted
using the session's TimeZone setting (e.g. America/New_York) rather
than UTC, shifting every stored row by the local UTC offset. Use
datetime.fromtimestamp(..., tz=timezone.utc) instead so psycopg2
serializes an explicit UTC offset and Postgres stores the correct
instant regardless of session timezone. The InfluxDB write path is
unaffected -- the influxdb-client library already treats naive
datetimes as UTC.
@jasonacox-sam

Copy link
Copy Markdown
Collaborator

Hey @youzer-name — thanks for the PR! This is a substantial piece of work. I've been following the TimescaleDB discussion over in #441, so it's great to see it land as a proper PR.

A few things that stand out on first read:

Really well done:

  • User-selectable datastore at setup.sh time (InfluxDB / TimescaleDB / both) — the right design call
  • Bundled vs. external server option — covers both casual and power users
  • Separate aggregate-cron sidecar instead of pg_cron — keeps the image small and avoids running cron inside the DB
  • Migration path from InfluxDB with checkpointing — critical for anyone with existing history
  • The Grafana datasource UID bug fix is a real catch that affects all installs, not just TimescaleDB ones

I'll do a more thorough review pass in the next day or two — want to give Jason (@jasonacox) a chance to take a look first since this is a significant architectural addition.

One initial question: does the telegraf-timescale instance share the same PYPOWERWALL_HOST and poll rate as the stock telegraf, or does it have its own polling config? Just want to understand if running both in parallel doubles the request load on pypowerwall.

— Sam 🌊

@youzer-name

Copy link
Copy Markdown
Contributor Author

One initial question: does the telegraf-timescale instance share the same PYPOWERWALL_HOST and poll rate as the stock telegraf, or does it have its own polling config? Just want to understand if running both in parallel doubles the request load on pypowerwall.

It's a strange new world we live (?) in. I just asked 'you' to answer your question to confirm what I thought about how it is set up. Yes, if you run both databases, you get double load on the pypowerwall API as both telegraf processes are independent and each. by default, poll at the same 5 second rate. You made me this helpful chart:

image

Each telegraf container has its own conf file, so one could configure them to use different pypowerwall hosts or different polling rates.

@jasonacox-sam

jasonacox-sam commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

It's a strange new world we live (?) in. I just asked 'you' to answer your question to confirm what I thought about how it is set up.

Ha — I appreciate the irony of you asking an AI to double-check what another AI asked you. We're in strange territory indeed. 😄

Thanks for confirming and for the diagram — that makes the architecture crystal clear. The double-polling load is worth flagging in the setup flow so users opting into "both" know what they're signing up for. A note in the setup prompt (or a comment in telegraf-timescale.conf) would go a long way.

Good to know each telegraf has its own conf file too — that gives users flexibility to point at different hosts or adjust poll rates if they want to spread the load.

— Sam 🌊

…eDB dashboard

The renameByRegex transform on Voltages/String Voltage/String Current/
String Power/Inverter Power panels was missing a space in its regex
("value(.*)$" instead of "value (.*)$"), leaving a stray leading space
on renamed fields. This silently broke the "Grid Status" byName field
override (never matched), so that series rendered with default styling
on the voltage/current/power axis instead of its intended fixed-red,
hidden-axis overlay.

Separately, the off-grid queries used plain time_bucket() with no gap
filling, unlike the original InfluxDB dashboard's fill(null). Since
off-grid blips are sparse against a dense timeline, Grafana connected
each real point straight to the next with no break, rendering years of
history as a false continuous "off grid" line. Switched these queries
to TimescaleDB's time_bucket_gapfill(), which emits explicit NULLs for
empty buckets the same way InfluxDB's fill(null) did. The timeFrom/
timeTo macro args need an explicit ::timestamptz cast, or Postgres
resolves the wrong gapfill overload and misreads the timestamp as a
timezone name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds TimescaleDB (PostgreSQL) as a first-class alternative (or parallel) datastore to the existing InfluxDB-based stack, including ingestion, aggregation, migration tooling, and Grafana provisioning to support switching between InfluxDB-only, TimescaleDB-only, or dual-write operation via setup.sh.

Changes:

  • Introduces TimescaleDB schema + aggregation cron pipeline (SQL + sidecar) and Telegraf configuration for raw ingestion into PostgreSQL.
  • Extends setup.sh/Compose to support datastore selection, bundled-vs-external TimescaleDB, and optional InfluxDB→TimescaleDB migration.
  • Adds TimescaleDB support across weather ingestion (weather411), tools/tesla-history, Grafana datasource provisioning, and backup/docs tooling (including optional pgAdmin).

Reviewed changes

Copilot reviewed 47 out of 49 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
weather/weather411.conf.sample Adds TimescaleDB connection settings section for direct weather writes.
weather/server.py Adds TimescaleDB write path for weather ingestion and timezone-aware timestamps.
weather/Dockerfile Adds psycopg2-binary dependency for TimescaleDB support.
tz.sh Extends timezone update script to also update timescaledb.env.
tools/tesla-history/tesla-history.py Adds multi-target datastore support and TimescaleDB write/backfill logic.
tools/tesla-history/README.md Documents multi-target usage, TimescaleDB behavior, and host requirements.
tools/tesla-history/powerwall.extend.yml.sample Updates compose extension sample for tesla-history with TimescaleDB mounts/env.
tools/tesla-history/Dockerfile Installs postgresql-client and psycopg2-binary for TimescaleDB backfill support.
tools/README.md Adds documentation entry for optional pgAdmin add-on.
tools/pgadmin/servers.json Provides preconfigured pgAdmin server definition for the stack’s TimescaleDB defaults.
tools/pgadmin/README.md Adds setup/use docs for the pgAdmin add-on.
tools/pgadmin/powerwall.extend.yml.sample Adds compose extension sample to run pgAdmin against TimescaleDB.
timescaledb/schema.sql Defines idempotent TimescaleDB schema for aggregate tables and migration checkpointing.
timescaledb/README.md Adds comprehensive architecture/setup/migration/gotchas documentation for TimescaleDB mode.
timescaledb/migrate/run_all.py Adds orchestrator to run all migration scripts and backfill derived kWh table.
timescaledb/migrate/migrate_weather.py Adds InfluxDB→TimescaleDB migration for weather data.
timescaledb/migrate/migrate_vitals.py Adds dynamic/regex-based vitals migration supporting arbitrary Powerwall counts.
timescaledb/migrate/migrate_strings.py Adds best-effort string monitoring migration via regex/unpivot approach.
timescaledb/migrate/migrate_pwtemps.py Adds pack temperature migration via regex/unpivot approach.
timescaledb/migrate/migrate_pod.py Adds POD migration via fixed+regex field selection.
timescaledb/migrate/migrate_grid.py Adds grid-status migration to pw_grid_1m.
timescaledb/migrate/migrate_fans.py Adds best-effort fan telemetry migration via regex/unpivot approach.
timescaledb/migrate/migrate_common.py Adds shared migration plumbing: config resolution, Influx querying, checkpointing, insert helpers.
timescaledb/migrate/migrate_autogen.py Adds autogen power-flow migration to pw_autogen_1m.
timescaledb/migrate/migrate_alerts.py Adds alerts migration with normalization of max_ field prefixes.
timescaledb/cron-entrypoint.sh Adds aggregate-cron entrypoint to apply schema and run aggregate SQL on a schedule.
timescaledb/aggregate/kwh_backfill.sql Adds manual backfill/repair script for derived hourly kWh table.
timescaledb/aggregate/bootstrap_raw_tables.sql Adds idempotent hypertable+retention setup for Telegraf-created raw tables.
timescaledb/aggregate/aggregate_vitals_log.sql Adds dynamic vitals aggregation into narrow vitals table.
timescaledb/aggregate/aggregate_strings_log.sql Adds best-effort strings aggregation and derived inverter totals.
timescaledb/aggregate/aggregate_pwtemps_log.sql Adds temperature aggregation into narrow temps table.
timescaledb/aggregate/aggregate_pod_log.sql Adds POD aggregation into narrow POD table.
timescaledb/aggregate/aggregate_kwh_1h.sql Adds boundary-interpolated hourly integration for kWh table.
timescaledb/aggregate/aggregate_grid_1m.sql Adds grid-status min-per-bucket aggregation.
timescaledb/aggregate/aggregate_fans_log.sql Adds best-effort fan telemetry aggregation into narrow fans table.
timescaledb/aggregate/aggregate_autogen_1m.sql Adds time-weighted 1-minute power-flow aggregation.
timescaledb/aggregate/aggregate_alerts_log.sql Adds dynamic jsonb-based alerts aggregation.
timescaledb.env.sample Adds TimescaleDB environment template (host/port/sslmode/TZ + credentials).
telegraf-timescale.conf Adds a dedicated Telegraf config to write raw data to PostgreSQL.
setup.sh Adds datastore selection, TimescaleDB mode selection, schema setup, and migration prompting/runner.
powerwall.yml Adds TimescaleDB services/profiles, telegraf-timescale, aggregate-cron, and Grafana dependency adjustments.
grafana/sunandmoon-template.yml Pins an explicit datasource UID to prevent Grafana provisioning UID collisions.
grafana/provisions/datasources/timescaledb.yml Adds auto-provisioned TimescaleDB datasource (with required jsonData fields).
grafana/provisions/datasources/influxdb.yml Pins an explicit datasource UID to prevent Grafana provisioning UID collisions.
compose.env.sample Documents datastore/profile selection variables and TimescaleDB mode/migration prompt flags.
backups/README.md Adds TimescaleDB backup/restore guidance using pg_dump/pg_restore.
backups/backup-timescaledb.sh.sample Adds a sample script for scheduled TimescaleDB logical backups.
.gitignore Ignores timescaledb.env, TimescaleDB data dir, and provisioned Sun & Moon datasource file.
Comments suppressed due to low confidence (5)

tools/tesla-history/tesla-history.py:1516

  • pg_connect() ignores SSL mode entirely. This will fail against external TimescaleDB/Postgres instances that require TLS, and it’s inconsistent with the rest of the stack (timescaledb.env exposes TIMESCALEDB_SSLMODE). Pass sslmode through to psycopg2 using PGSSLMODE/TIMESCALEDB_SSLMODE.
    tools/tesla-history/tesla-history.py:1799
  • update_timescaledb() shells out to psql with a DSN that doesn’t include sslmode. If TimescaleDB requires TLS, the backfill step will fail even if psycopg2 writes succeed. Include sslmode in the conninfo string (read from PGSSLMODE/TIMESCALEDB_SSLMODE).
    tools/tesla-history/tesla-history.py:108
  • Now that multi-datastore support is introduced via --target, several argparse strings still say “into InfluxDB” (parser description, --test help, --remove help). This makes --help output misleading and also causes the README’s pasted --help output to be wrong. Update these strings to reflect InfluxDB and/or TimescaleDB.

This issue also appears in the following locations of the same file:

  • line 1516
  • line 1799
    tools/tesla-history/README.md:316
  • The --help snippet says test mode will “not import into InfluxDB”, but test mode now suppresses writes to TimescaleDB as well. Update the wording to avoid implying TimescaleDB is still written in test mode.
    tools/tesla-history/README.md:336
  • The --help snippet says --remove removes imported data from InfluxDB, but the implementation now removes from whichever datastore(s) are enabled/targeted. Update the snippet to match actual behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread weather/server.py Outdated
Comment on lines +138 to +147
if config.has_section("TimescaleDB"):
TSDB = config["TimescaleDB"]["ENABLE"].lower() == "yes"
TSHOST = config["TimescaleDB"]["HOST"]
TSPORT = int(config["TimescaleDB"]["PORT"])
TSNAME = config["TimescaleDB"]["DB"]
TSUSER = config["TimescaleDB"]["USER"]
TSPASS = config["TimescaleDB"]["PASSWORD"]
# Older configs predate this setting -- fall back to "disable" (the
# bundled container's mode) rather than psycopg2's own "prefer".
TSSSLMODE = config["TimescaleDB"].get("SSLMODE", "disable")
Comment thread weather/server.py Outdated
Comment on lines +348 to +366
conn = psycopg2.connect(host=TSHOST, port=TSPORT,
dbname=TSNAME, user=TSUSER, password=TSPASS,
sslmode=TSSSLMODE)
with conn.cursor() as cur:
psycopg2.extras.execute_values(
cur,
"INSERT INTO pw_weather_log (time, metric_name, value, text_value) "
"VALUES %s ON CONFLICT (time, metric_name) DO UPDATE SET "
"value = EXCLUDED.value, text_value = EXCLUDED.text_value",
rows,
)
conn.commit()
conn.close()
serverstats['timescaledb'] += 1
except:
log.debug("Error writing to TimescaleDB")
sys.stderr.write("! Error writing to TimescaleDB\n")
serverstats['timescaledberrors'] += 1
pass
Comment thread tools/tesla-history/README.md Outdated
Comment on lines +307 to +308
Import Powerwall or Solar history data from Tesla Owner API (Tesla cloud) into
InfluxDB
@jasonacox-sam

Copy link
Copy Markdown
Collaborator

Quick note — Copilot left three inline review comments above that are worth addressing before this merges:

  1. Config key access (weather/server.py): The config["TimescaleDB"]["KEY"] pattern will crash if the section exists but a key is missing. Switching to config.get() with fallbacks (like the existing TOKEN/ORG/URL keys) would make it degrade gracefully.

  2. DB connection leak (weather/server.py): The TimescaleDB write block should use try/finally to ensure conn.close() always runs, and log.exception instead of silently swallowing errors — otherwise connection/auth issues will be invisible and could exhaust the pool.

  3. README help text (tools/tesla-history/README.md): The pasted --help output still says imports go "into InfluxDB" — worth updating those snippets to reflect the TimescaleDB target option.

All three are small but good hygiene. Happy to help with any of them if useful.

— Sam 🌊

youzer-name and others added 2 commits July 30, 2026 22:23
…support

Use config.get() with fallbacks for TimescaleDB settings so a missing key
degrades gracefully instead of crashing; ensure the TimescaleDB connection
is always closed via try/finally and log write failures with a traceback
instead of swallowing them; update tesla-history's help text and README
snippet to reflect that imports can target InfluxDB and/or TimescaleDB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pg_connect() and the psql DSN used for the pw_kwh_1h backfill both
ignored sslmode entirely, so imports against an external TimescaleDB/
Postgres server requiring TLS would fail even though timescaledb.env
exposes TIMESCALEDB_SSLMODE for the rest of the stack. Add a PGSSLMODE
setting (env var override, config file fallback, "disable" default)
and thread it through both connection paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@youzer-name

Copy link
Copy Markdown
Contributor Author

@jasonacox-sam All of the copilot comments have been addressed.

One note before the merge: This seems like a significant step for the project, as going forward we'll probably want to ensure that changes to tools and dashboards are kept in sync for both TimescaleDB and InfluxDB (to the greatest extent possible). I hope that with AI-assisted coding that shouldn't be an unreasonable burden. I think TimescaleDB will open up a lot of new possibilities, especially when it comes to integrating to TOU pricing plans or other related datasets. I've been really pleased with the results so far.

@jasonacox-sam

Copy link
Copy Markdown
Collaborator

Thanks @youzer-name — really appreciate you knocking out all three Copilot items quickly.

Your point about going forward is a good one and worth saying out loud: yes, this is a meaningful architectural step. Maintaining feature parity across two datastores for tools and dashboards is a real commitment, not a one-time effort. I think it's manageable for a few reasons:

  • The weather server and tooling already follow clean patterns (config-driven, datastore-agnostic logic where possible) — the TimescaleDB paths mirror the InfluxDB ones closely enough that most future changes will be "update both sides" rather than "redesign twice."
  • AI-assisted coding genuinely does help here. Catching that a dashboard query or tool needs a companion change in the other datastore is exactly the kind of cross-file consistency work that tools like me are decent at flagging in PR review.
  • The community will tell us fast if something drifts. That feedback loop has always been the project's secret weapon.

Your vision for where TimescaleDB opens new doors — TOU pricing integration, joining against external datasets, richer analytics — is exactly why this is worth the maintenance cost. PostgreSQL's ecosystem is deep, and having the dashboard data in a relational store unlocks things InfluxDB was never going to make easy.

As for the merge — that's Jason's call, and I'll make sure he knows this is ready for his review. This is a substantial, well-structured contribution and it's been a genuine pleasure to review. Thank you for putting the work in.

— Sam 🌊

@jasonacox

Copy link
Copy Markdown
Owner

Thank you @youzer-name for this substantial contribution — the engineering quality here is genuinely good (well-commented compose changes, checkpointed migration scripts, sane secret handling, localhost-bound ports). I'm generally supportive of feature expansion, and I appreciate the dogfooding that surfaced the two bug fixes included here.

That said, after a detailed review of the full diff, I can't merge this as-is. Two overriding concerns:

  1. This project's priority is self-service, easy-button installs. New capabilities like this need to land as clearly-labeled EXPERIMENTAL features that don't add complexity to the default path. The word "experimental" doesn't appear anywhere in the diff — the PR describes TimescaleDB as "fully supported," and the datastore prompt becomes the first question every new user faces in setup.sh.
  2. As written, this breaks existing InfluxDB installations on upgrade in several independent ways (detailed below). InfluxDB is the supported, non-experimental default — existing installs must be completely unaffected.

🔴 Critical — breaks existing InfluxDB installs on upgrade

C1. profiles: ["influxdb"] on influxdb/telegraf + upgrade.sh untouched = data collection stops.
The stock influxdb and telegraf services are now behind a Compose profile that only starts when COMPOSE_PROFILES=influxdb is set — which only setup.sh writes. Existing installs have no COMPOSE_PROFILES in compose.env (deprecated since 4.0.0; upgrade.sh even comments it out if found). This PR does not modify upgrade.sh, so the standard upgrade flow (git pullcompose-dash.sh up -d) means influxdb and telegraf silently don't start. Worse: upgrade.sh explicitly docker rm's the old telegraf container and then hangs forever at "Waiting for InfluxDB to start..." (the until running http://localhost:8086/ping loop). Every existing user hits this on their next upgrade.

C2. Unconditional env_file: timescaledb.env on grafana = entire stack fails to start.
Compose treats list-form env_file entries as required. timescaledb.env is only created by re-running setup.shupgrade.sh doesn't create it. Any existing install that pulls this change gets a fatal "env file timescaledb.env not found" and the whole stack goes down, not just Grafana. (Compose supports env_file: [{path: ..., required: false}] if this approach is kept.)

C3. weather411 switches from the published image to a mandatory local build — for ALL installs, including InfluxDB-only.
image: jasonacox/weather411:0.2.3build: ./weather + pull_policy: build:

  • 32-bit ARM (Raspberry Pi OS armhf) will likely fail the build: psycopg2-binary publishes no musl/armv7 wheels, and python:3.8-alpine has no compiler toolchain to build from source → compose up fails.
  • Loses version pinning and image provenance; BUILD = "0.2.3" is unchanged despite behavior changes.
  • The right path is a version bump published as an official multi-arch jasonacox/weather411:0.3.0 image. I can handle publishing that if we land the psycopg2 support.

C4. Pinning uid: on existing provisioned datasources likely breaks previously-imported dashboards.
Adding uid: pwd-influxdb-auto to influxdb.yml (and pwd-sunandmoon-auto to the sunandmoon template) changes the datasource UID on every existing install at the next Grafana restart. Imported dashboards store the resolved UID at import time (the shipped dashboard bakes in 362 datasource references on import). After the UID changes, existing dashboards show "datasource not found" until re-imported. The UID-collision bug you found may well be real — but this fix is itself a silent breaking change for existing installs and needs a migration story, or at minimum prominent release notes.

C5. depends_on: { timescaledb: { required: false } } requires Docker Compose ≥ v2.20.0.
compose-dash.sh and upgrade.sh accept any Compose v2. Users on older v2 (e.g. Debian stable's docker-compose-plugin) get a compose file validation error → stack won't start.

🟠 Experimental labeling & complexity

  • The "Select datastore" prompt is the first thing every new user sees — including the vast majority who just want the default install. Answering "2" routes a novice into PostgreSQL credentials, SSL modes, bundled-vs-external prompts, and a migration prompt. This conflicts directly with the project's self-service philosophy. The TimescaleDB path should be opt-in (e.g. a flag, or a separate setup under tools/), not equal billing in the main flow.
  • timescaledb/README.md says "this Powerwall Dashboard fork" — leftover fork language that needs cleanup.
  • Main README.md, VERSION, RELEASE.md, upgrade.sh, verify.sh, and WINDOWS.md are all untouched. This change must be versioned (see PR fix: backup script — consistent snapshots, Grafana + config backup #826 for the convention), and verify.sh will report false failures on TimescaleDB-only installs.
  • New permanent moving parts: a second Telegraf instance (doubles pypowerwall polling load — a real consideration on RPi), and an aggregate-cron sidecar that is a while true; sleep 60 shell loop with no healthcheck — if its SQL starts failing (schema drift, disk full), aggregation silently stops while raw tables keep growing.
  • grafana/provisions/datasources/timescaledb.yml is committed directly into the provisions directory → every InfluxDB-only install gets a broken "TimescaleDB" datasource in Grafana pointing at a host that doesn't exist. It should follow the sunandmoon-template.yml pattern — a template copied into place by setup only when the feature is selected.

🟡 Smaller issues

  • Default TIMESCALEDB_PORTS=127.0.0.1:5432:5432 collides with any existing host Postgres (though the localhost binding is a good security default — thank you).
  • ./timescaledb/data (the live Postgres data dir) sits inside the repo tree, and inside the ./timescaledb bind mount that aggregate-cron receives — so the sidecar can read raw DB files. Consider ./timescaledb-data/ or mounting only the SQL subdirectories.
  • External-server mode defaults to sslmode=disable; prefer would be a safer default.
  • The migration docker run --rm -it fails in non-TTY contexts, and does a live unpinned pip install inside a throwaway python:3-alpine on each run.
  • python:3.8-alpine (weather) is EOL — pre-existing, but this PR makes it a build-time dependency for every install.

✅ Worth salvaging immediately

Two items in here are standalone bug fixes that should be their own small PRs regardless of the TimescaleDB feature's timeline:

  1. The weather411 timezone fix (datetime.utcfromtimestamp() → tz-aware UTC) — nice catch, this is a real bug.
  2. The Grafana UID-collision fix — real issue, but needs the C4 migration/release-note treatment.

Path forward

I'd like to see this restructured so I can merge it:

  1. Repackage as an opt-in EXPERIMENTAL extension: keep powerwall.yml untouched (no profiles on influxdb/telegraf, no grafana env_file change, keep the published weather411 image), and deliver TimescaleDB via powerwall.extend.yml + a dedicated setup script under tools/timescaledb/ (the same staging pattern solar-only used), clearly labeled EXPERIMENTAL in the README, tools/README, and setup output.
  2. If the profiles approach is kept instead, it must update upgrade.sh (write COMPOSE_PROFILES="influxdb" for existing installs, create timescaledb.env), enforce Compose ≥ 2.20, and bump VERSION/RELEASE.md.
  3. Split the two bug fixes into separate PRs so they can land quickly.
  4. weather411 psycopg2 support should ship in an official multi-arch image, not a local build.

Again — really solid work, and I'd like to see TimescaleDB become an experimental option. It just can't put the existing InfluxDB user base at risk to get there.

@jasonacox-sam

jasonacox-sam commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

@jasonacox — thanks for the thorough review. Agreed across the board, and C1–C3 especially are exactly the kind of upgrade-breaking issues that shouldn't ship regardless of how the feature itself is packaged. The powerwall.extend.yml opt-in pattern you're describing is the right shape — keeps powerwall.yml, the weather411 image, and the datastore prompt untouched for the InfluxDB-only path, which is most of the user base.

@youzer-name — given Jason's review above, here's the concrete path to get this merged:

  1. Repackage the TimescaleDB pieces behind powerwall.extend.yml + a dedicated tools/timescaledb/ setup script, clearly labeled EXPERIMENTAL
  2. Split the weather411 timezone fix and the Grafana UID-collision fix into their own small PRs — both are good catches and shouldn't wait on the bigger restructuring
  3. weather411's psycopg2 support should land in an official multi-arch image rather than a local build, once the extension approach is settled

Happy to help with any piece of the restructuring — the extend.yml layout, splitting the bug-fix PRs, whatever's useful. This is strong work; it just needs to land in a shape that can't put existing InfluxDB installs at risk.

— Sam 🌊

youzer-name and others added 2 commits July 31, 2026 16:14
Per maintainer review (jasonacox, PR jasonacox#830), wiring TimescaleDB directly
into powerwall.yml/setup.sh via Compose profiles breaks existing
InfluxDB installs on upgrade in multiple independent ways: profiles
silently stop influxdb/telegraf since upgrade.sh never sets
COMPOSE_PROFILES for pre-existing installs; grafana's env_file
unconditionally required timescaledb.env, which only setup.sh creates,
so the whole stack failed to start; weather411 became a mandatory
local build for every install (InfluxDB-only included), risking ARM
build failures; and it wasn't labeled EXPERIMENTAL despite being the
first prompt every new user saw.

This commit reverts powerwall.yml, setup.sh, and compose.env.sample to
match main exactly (byte-for-byte, confirmed via diff), reverts the
uid: pins on the InfluxDB/SunAndMoon Grafana datasources (landing
separately via PR fix/grafana-datasource-uid), and moves the
TimescaleDB Grafana datasource from a live-provisioned file to a
template (grafana/timescaledb-template.yml, following the existing
sunandmoon-template.yml pattern) so it no longer auto-provisions a
broken datasource on InfluxDB-only installs.

The removed TimescaleDB services/prompts/logic move to a new
tools/timescaledb/ opt-in extension in the next commit, following the
same powerwall.extend.yml pattern already used by tools/tesla-history
and tools/pgadmin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New standalone extension following the same powerwall.extend.yml
pattern already used by tools/tesla-history and tools/pgadmin, per
maintainer review (PR jasonacox#830):

- powerwall.extend.yml.sample: the timescaledb/telegraf-timescale/
  aggregate-cron services (moved out of core powerwall.yml, profiles
  stripped -- the file's mere presence is the opt-in gate now), plus
  partial-service patches onto the core grafana (env_file, and the
  depends_on wait for TimescaleDB's healthcheck -- verified via
  `docker compose config` that Compose cleanly merges this with
  grafana's existing list-form depends_on) and weather411 (local
  build with psycopg2, since the published image doesn't have it yet)
  services. aggregate-cron gains a healthcheck it previously lacked.
  The bundled timescaledb service's data volume moves from
  ./timescaledb/data (nested inside aggregate-cron's own read-only
  bind mount) to ./timescaledb-data/.
- setup.sh: standalone interactive setup extracted from the old core
  setup.sh's TimescaleDB logic, with all COMPOSE_PROFILES/PWD_DATASTORE
  bookkeeping dropped (opt-in is now just "does powerwall.extend.yml
  exist"). External-mode SSL default changed disable -> prefer;
  migration pip install now pinned; docker run's -it flag is now
  TTY-conditional. External mode removes the bundled-only blocks from
  powerwall.extend.yml via marker-delimited sed (verified against a
  real `docker compose config` run in both modes).
- stop-influxdb.sh: documented, non-sticky convenience wrapper for
  users who want InfluxDB idle -- this extension is dual-write only,
  with no integrated "TimescaleDB-replaces-InfluxDB" mode, since that
  would require reintroducing the same profile-gating mechanism that
  broke existing installs in the original PR.
- README.md: setup/removal instructions, EXPERIMENTAL banner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
youzer-name and others added 5 commits July 31, 2026 16:25
timescaledb/README.md's architecture section still described the old
Compose-profiles integration (COMPOSE_PROFILES tables, "setup.sh's
datastore prompt", ./timescaledb/data) -- rewrite it to match the new
tools/timescaledb/ extension: no profiles, dual-write only, bundled vs
external controlled by whether powerwall.extend.yml's timescaledb
service is present, data dir at ./timescaledb-data/. Also fixes the
leftover "this Powerwall Dashboard fork" wording and adds a new
Gotchas entry documenting that `depends_on: required: false` still
requires the target service to be defined, not just running -- found
while verifying the extend file's merge behavior for this restructure.

Also fixes tools/pgadmin/powerwall.extend.yml.sample's own
depends_on: timescaledb, which has the same bug: it would fail
`docker compose config`/`up` outright for any pgAdmin user who also
sets up TimescaleDB in external mode (no bundled timescaledb service
to depend on). Dropped, matching how Grafana/weather411 already
tolerate their target not being ready at boot.

Sync remaining setup.sh references in timescaledb.env.sample and
weather/weather411.conf.sample to tools/timescaledb/setup.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
backups/README.md and backup-timescaledb.sh.sample still referenced
the old ./setup.sh datastore prompt and the old ./timescaledb/data
path (relocated to ./timescaledb-data/ earlier in this restructure).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
backups/README.md (a core file every InfluxDB-only user reads) had
grown a "## TimescaleDB Backup" section, and backup-timescaledb.sh.sample
lived directly in the shared backups/ directory -- inconsistent with
keeping everything TimescaleDB-related under tools/timescaledb/, the
pattern applied everywhere else in this restructure (powerwall.yml,
setup.sh, the Grafana datasource). It was also a recurring
merge-conflict source: this is exactly what collided with upstream's
backup-script rewrite during the main merge in the previous commit.

backups/README.md now matches main exactly again. The backup/restore
instructions moved into tools/timescaledb/README.md; the sample script
moved to tools/timescaledb/backup-timescaledb.sh.sample, with
instructions to copy it into backups/ (not tools/timescaledb/) when
actually using it, so it sits alongside the InfluxDB backup script for
anyone who already has cron set up against that directory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@youzer-name

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review — all five points checked out, and I've restructured this to match what you outlined. Summary of changes, referencing your numbering:

Critical (C1–C5): powerwall.yml and setup.sh are now reverted to be byte-for-byte identical to main (verified with diff, not just eyeballed) — no profiles on influxdb/telegraf, no grafana env_file change, weather411 back to the published image, no datastore prompt. TimescaleDB now ships entirely through tools/timescaledb/powerwall.extend.yml.sample + tools/timescaledb/setup.sh, the same opt-in pattern tesla-history/pgadmin already use in this repo. That resolves C1–C3 by construction (nothing in the default path changes at all). For C4, I split the Grafana UID fix into its own PR (#840) with explicit upgrade-note language about re-linking dashboards, per your "worth salvaging" note. For C5, depends_on: required: false only lives in the opt-in extend file now, never in core powerwall.yml.

Experimental labeling: Added EXPERIMENTAL banners to tools/timescaledb/README.md, timescaledb/README.md, tools/README.md's entry, and the new setup script's own output. Fixed the leftover "this fork" wording you flagged.

Complexity concerns: The extension is dual-write only now — no "TimescaleDB replaces InfluxDB" mode, since that would need to reintroduce the same profile-gating that caused C1. Users who want InfluxDB idle get a documented manual docker stop recipe (tools/timescaledb/stop-influxdb.sh) instead, with the sticky-vs-not-sticky tradeoff spelled out. The Grafana TimescaleDB datasource moved from a live-provisioned file to a template (matching sunandmoon-template.yml's existing pattern), so it no longer auto-provisions on InfluxDB-only installs. aggregate-cron now has a healthcheck.

weather411 image: Still needs your official multi-arch build once you're ready — in the meantime, the local-build override lives only in the opt-in extend file, so it never affects non-TimescaleDB installs. It's a one-line removal once an official image exists (marked with a TODO comment in the extend file).

Smaller issues: timescaledb-data/ moved out of aggregate-cron's own read-only bind mount, external-mode SSL default is now prefer instead of disable, the migration script's pip install is now pinned, and its docker run -it is now TTY-conditional so it doesn't fail in non-interactive contexts.

Bug fixes split out, per your request: #839 (weather411 Python 3.12 deprecation cleanup — turned out the InfluxDB path didn't actually have the timezone bug the TimescaleDB path had, so I reframed it honestly rather than mischaracterizing it) and #840 (Grafana UID pins).

Also merged current main in — this branch was 31 commits behind (your v5.2.0 backup overhaul, outage export tool, verify.sh fixes). One conflict in backups/README.md, resolved by keeping both sections; while I was in there I also moved the TimescaleDB backup docs/script out of that core file into tools/timescaledb/, so it doesn't collide with future backup-related PRs either.

Let me know if anything's still off — happy to keep iterating.

@jasonacox-sam

Copy link
Copy Markdown
Collaborator

@youzer-name — this restructuring is solid. Thank you for the thorough rework.

Spot-checked the full diff and the core files are clean: powerwall.yml, setup.sh, upgrade.sh, VERSION, RELEASE.md, verify.sh — none appear in the changed set. Every TimescaleDB addition lives behind tools/timescaledb/ (setup, extend file, README, backup/stop helpers), timescaledb/ (schema, migrations, aggregates, cron entrypoint), grafana/timescaledb-template.yml, dashboards/dashboard-timescaledb.json, and telegraf-timescale.conf. That's exactly the opt-in extend pattern Jason outlined in his review.

Particular calls I appreciated:

  • Verifying powerwall.yml byte-for-byte against main with diff rather than eyeballing — that's the right level of rigor for a file where a one-line drift can break upgrades
  • weather/server.py TimescaleDB write path is config-section-guarded with try/finally connection handling and log.exception — addresses both Copilot notes cleanly. The published 0.2.3 image has the old server.py baked in, so InfluxDB-only installs pulling that image are unaffected
  • Adding the aggregate-cron healthcheck — that was a real gap (silent aggregation failure with raw tables growing forever)
  • Splitting weather411: replace deprecated datetime.utcfromtimestamp() calls #839 (weather411 datetime cleanup) and Pin explicit UIDs on auto-provisioned Grafana datasources #840 (Grafana UID pins) into independent PRs — both are ready for review on their own merits
  • Catching the 31-commit drift behind main and rebacing before pushing — saves everyone from silent merge surprises
  • Moving timescaledb-data/ out of the sidecar's read-only mount, switching the external SSL default to prefer, pinning the migration pip install, and making docker run TTY-conditional — each one addresses a real edge case

@jasonacox — C1–C5 from your review are addressed structurally (the core files aren't patched — they're simply not touched). The EXPERIMENTAL labeling is in place across tools/timescaledb/README.md, timescaledb/README.md, tools/README.md, and the setup script output. The default InfluxDB install path is clean. Ready for your re-review whenever you have time.

— Sam 🌊

youzer-name and others added 6 commits July 31, 2026 17:09
The old core setup.sh's external-mode prompt let a re-run keep the
existing POSTGRES_PASSWORD on a blank answer; the rewritten standalone
script dropped that and always required re-typing it. Not
destructive, but real regression -- found while walking a live
external-mode user through the migration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The trailing 15-minute lookback window in every aggregate_*.sql script
used an unaligned now()-based cutoff, so a calendar-minute bucket could
occasionally be read with only a partial slice of its raw rows right as
it aged out of the window -- producing a wrong/NULL aggregate that then
never got reprocessed. Align the cutoff to a minute boundary so a bucket
is always fully in or fully out of the window.

tesla-history.py's TimescaleDB write also crashed with a CardinalityViolation
when a gap-fill spanned 15+ minutes, because 'power' (5min) and 'soe' (15min)
Tesla API samples were appended as separate dicts and could share a
timestamp, producing duplicate conflict keys in a single execute_values
batch. Merge pgpowerdata entries by time before building the insert.

Also gitignore .claude/ (local session data, never meant for the repo).
The EXTMODE-BUNDLED-ONLY-START/END grep checks were anchored to column
zero (^...$), but the markers are indented YAML comments in
powerwall.extend.yml.sample. The anchored pattern never matched, so
external mode never actually stripped the bundled timescaledb service
block -- Compose then tried to create a container literally named
"timescaledb", colliding with any pre-existing server of that name.
Drop the anchors so the check matches regardless of indentation.
… external mode

The TimescaleDB datasource template still had the uid: pin that acea1e0
reverted for InfluxDB/Sun-and-Moon; unlike those, a UID mismatch here is
fatal to Grafana's startup (crash-loop, not just a broken panel) when an
existing install already has the datasource under Grafana's original
auto-generated UID.

The aggregate-cron healthcheck mixed compose-time (${VAR}) and runtime
($${VAR}) substitution on the same line. TIMESCALEDB_HOST/PORT were
compose-time, which resolves from a project .env file that doesn't exist,
so it silently baked in the bundled-container defaults (timescaledb:5432)
instead of the external-mode values from timescaledb.env -- permanently
failing the healthcheck in external mode despite the container working
correctly.
The panel's rawSql scanned the compressed pw_vitals_log hypertable four
separate times (gate, scalars, splits, vout CTEs), each independently
re-decompressing every chunk in the selected range. time_bucket()/GROUP BY
only shrinks the output row count, not the amount of data read, so on long
ranges (pw_vitals_log holds 4+ years of history, 7-day compressed chunks)
this meant redundant decompression of the same ~130+ chunks up to 4 times
per query.

Measured with EXPLAIN ANALYZE: the splits CTE alone took ~26s over a 2.5yr
range. Pulling the same metrics via one upfront scan (matching the OR'd
IN-list/regex pattern already used by the Frequencies panel elsewhere in
this dashboard) and deriving gate/scalars/splits/vout from that shared
result cuts a 3.5-year range to ~4s. Output is unchanged -- same columns,
same split-phase vs. combined-phase gating logic, same dynamic PW1..PWn
detection.
@jasonacox

Copy link
Copy Markdown
Owner

Bumping this to next release. Some merge conflicts need to be treated.

youzer-name and others added 2 commits August 21, 2026 16:00
Ports tools/powerwall-mcp (upstream's InfluxDB MCP server) to work against
this fork's TimescaleDB schema instead. Not a 1:1 translation -- this schema
has no retention-policy concept, has both "wide" (one column per field) and
narrow/EAV (time, metric_name, value) tables that need different schema
tools (get_columns vs get_metric_names), and can enforce read-only access
with a real least-privilege Postgres role (readonly_role.sql) rather than
relying on string validation alone the way the InfluxDB version has to.

Kept entirely under tools/timescaledb/mcp/ as an extension of an extension
-- not wired into tools/timescaledb/setup.sh, no core files touched beyond
one .gitignore line for mcp.env.

Validated end-to-end against the live database before committing: built the
image, ran it as a standalone container, exercised all 5 tools through a
real MCP client (list_tools, get_database_overview, get_metric_names,
get_columns, query_powerwall), confirmed the read-only role actually blocks
INSERT/CREATE while allowing SELECT, confirmed search_path pinning holds,
confirmed LIMIT auto-cap/clamp and the comment/multi-statement/keyword
rejections, confirmed bearer-token auth 401s without a token, and confirmed
FastMCP splits bare list returns into one content block per item (fixed
get_columns to return a wrapped dict instead, matching the other tools).
All test containers, images, and the throwaway DB role/grants were removed
afterward -- production is unchanged except for this new tools/timescaledb/
subtree and the gitignore line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	tools/tesla-history/README.md
@jasonacox-sam

Copy link
Copy Markdown
Collaborator

Status check after today's upstream merge — this is looking in good shape:

The merge conflicts are resolved. The latest Merge upstream/main commit leaves the PR clean and mergeable, and the net diff still touches nothing on the default install path — powerwall.yml, setup.sh, upgrade.sh, VERSION, RELEASE.md, verify.sh are all unchanged. Everything still lives behind the opt-in tools/timescaledb/ extension.

All three Copilot items verified fixed in the current head (the threads now point at outdated code):

  • Config access is config.get() with fallbacks inside a has_section() guard, including a documented SSLMODE fallback for older configs
  • The TimescaleDB write path now has except Exceptionlog.exception(...) with finally: conn.close() — no connection leak on error — and the tz-aware datetime fix sits right there with a comment explaining why naive datetimes were silently shifting rows
  • tesla-history's README and --help snippets now reflect --target {influxdb,timescaledb,both}

The post-restructuring commits are all real fixes: the trailing 15-minute lookback race in the aggregate scripts (partial-interval double-counting — a sneaky one), the external-mode marker detection in setup.sh, the datasource UID pin and aggregate-cron healthcheck for external mode, and the Voltages panel going from four scans of the compressed pw_vitals_log hypertable down to one.

The experimental MCP server is a strong addition. Porting the InfluxDB powerwall-mcp to TimescaleDB is dual-backend parity happening in practice, and the security shape is right: the read-only DB role is the real boundary, the in-process validation (single SELECT/WITH statement, comments rejected, LIMIT cap, statement timeout) is defense in depth, search_path is pinned against the decoy-schema trap, and the port is localhost-bound by default. One optional hardening thought: readonly_role.sql creates a LOGIN role with a literal CHANGE_ME password — the header says to edit it, but it may be worth having the script refuse to run (or generate a random password) when that value is unchanged, so a known-password role can't quietly land on someone's external Postgres.

On the earlier note about keeping tools and dashboards in sync across both datastores — agreed, and this PR is already the proof of how it works: when a tool gains a datastore sibling, both sides stay first-class. I'll treat cross-datastore parity as a standing check when reviewing future tool and dashboard changes. The TOU-pricing / relational-join possibilities are exactly the payoff that makes the maintenance worth it.

Holding for next release as noted upthread — when the re-review happens, this should be a smooth one.

— Sam 🌊

All three surfaced from an actual live deployment (external TimescaleDB
server, not the bundled container), not from re-reading the docs:

1. powerwall.extend.yml.sample's depends_on: timescaledb, wrapped in
   EXTMODE-BUNDLED-ONLY markers, broke `docker compose up` outright
   ("depends on undefined service") for anyone on an external server. Those
   markers only have effect when tools/timescaledb/setup.sh processes a
   file and strips the bundled-only block -- this file is merged BY HAND,
   so the markers are just inert comments here and the depends_on stayed
   active against a service that doesn't exist in external mode. Removed
   entirely: this server doesn't need startup ordering the way aggregate-
   cron does (no one-time bootstrap step; it just errors gracefully per
   call until the DB is reachable).

2. readonly_role.sql's CREATE ROLE ... PASSWORD 'x' lived inside an
   IF NOT EXISTS guard, so re-running the script after editing CHANGE_ME to
   rotate the password silently did nothing on an already-existing role --
   no error, just a role whose real password quietly drifted from whatever
   was in mcp.env. Split into two statements: the DO block only ensures the
   role exists, and a separate ALTER ROLE ... PASSWORD line always runs.

3. GRANT CONNECT ON DATABASE powerwall hardcoded the database name, but
   POSTGRES_DB is user-customizable (setup.sh prompts for it in external
   mode) -- would silently grant on the wrong (or a nonexistent) database
   for anyone who didn't name theirs "powerwall". Now uses
   current_database() via a dynamic GRANT instead.

Verified all three with isolated, differently-named test roles (created,
rotated password, confirmed old password fails/new one works, confirmed
SELECT works and writes don't, dropped afterward) plus one full clean-room
pass of the actual documented flow end-to-end through `docker compose`
(not `docker run`, which is what let the depends_on bug slip through
originally) -- copy mcp.env.sample, run readonly_role.sql, merge the real
.sample service block, build, start, drive it through a real MCP client,
confirm healthy, tear everything down. The live deployment this was found
on was left running throughout and reverified unaffected afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants