feat: measurement templates with per-device readings, transforms, and web UI#19
feat: measurement templates with per-device readings, transforms, and web UI#19derek-miller wants to merge 19 commits into
Conversation
- src/constants.lua: LOG_LEVELS, LOG_MODES, InfluxDB defaults (write interval, precision, batch/buffer sizes, retry intervals), VALUE_TYPES for line protocol, WRITE_INTERVALS and PRECISIONS maps - drivers/influxdb/driver.xml: full property set (Cloud, Driver, InfluxDB Settings, Measurements sections), 4 events, INFLUXDB_CONNECTED conditional, Test Connection / Add Measurement / Flush Buffer actions - drivers/influxdb/driver.lua: complete skeleton with OnDriverLateInit, OPC handlers, EC action handlers, updateConnectionStatus, testConnection via C4:urlGet /health, measurement add/remove stubs with persistence, write buffer with flush logic, proper InfluxDB v3 HTTP write client - drivers/influxdb/driver.c4zproj, squishy, www/ scaffold - Build verified: make init && make build-nodocs passes cleanly
Remove drivercentral from distributions list per Derek's direction. Only the OSS build target is needed for this driver.
* DRV-6: Fix CI for OSS-only distribution - Add missing documentation/images dir (docs-readme cp) - Remove drivercentral artifact upload from CI workflow - Fix PDF verification to only check OSS distribution * DRV-6: Add generated README and ignore images dir - Commit pandoc-generated README.md for clean tree check - Add /images to .gitignore (temp dir used by docs-readme) * DRV-6: Normalize README for CI pandoc compatibility Regenerate README.md with prettier normalization to match CI output (different pandoc --columns default between local and CI). --------- Co-authored-by: OpenClaw <openclaw@dmiller.me>
gen-squishy generates this at build time into build/. No other driver repo commits drivers/*/squishy. Co-authored-by: OpenClaw <openclaw@dmiller.me>
Co-authored-by: OpenClaw <openclaw@dmiller.me>
* DRV-12: Implement offline buffering and retry with exponential backoff
- Add src/lib/offline_buffer.lua: OfflineBuffer module with:
- Persistent storage via C4:PersistSetValue (through lib/persist)
- FIFO eviction when buffer exceeds max_points (default 10,000) or max_bytes (default 1MB)
- Exponential backoff retry schedule: 5s, 15s, 30s, 1m, 5m, 15m
- Distinguishes retriable errors (5xx, 429, network) from permanent errors (401, 422)
- Reconnection drain: oldest-first delivery when connection restored
- Extended outage notification via configurable threshold (default 5m)
- Connection state machine: Connected / Disconnected / Reconnecting
- State change logging and callbacks
- Update drivers/influxdb/driver.lua:
- Integrate OfflineBuffer into write path (flushBuffer, writeBatch)
- Route failed retriable writes to offline buffer instead of dropping
- Drain offline buffer on reconnection via callback
- Add drainOfflineBuffer() and drainInFlight guard to prevent overlapping drain attempts
- Add EC.DrainOfflineBuffer and EC.ClearOfflineBuffer action handlers
- Add OPC.Max_Buffer_Size and OPC.Outage_Notification_Threshold property handlers
- Initialize OfflineBuffer in OnDriverLateInit; destroy in OnDriverDestroyed
- Update Connection State and Offline Buffer Size read-only properties on state change
- Update drivers/influxdb/driver.xml:
- Add Extended Outage event (id=5)
- Add Offline Buffer Settings property section (Max Buffer Size, Outage Threshold,
Offline Buffer Size read-only, Connection State read-only)
- Add Drain Offline Buffer and Clear Offline Buffer actions
- Update src/constants.lua:
- Correct MAX_BUFFER_SIZE to 10,000 (was 50,000)
- Add MAX_BUFFER_BYTES (1MB)
- Update RETRY_INTERVALS to DRV-12 schedule [5,15,30,60,300,900]
- Add DEFAULT_OUTAGE_THRESHOLD and OUTAGE_THRESHOLDS map
- Update test/c4_shim.lua:
- Expose persist_store as global for test reset
- Add C4:KillTimer stub to both socket and non-socket code paths
- Add test/test_offline_buffer.lua:
- 18 unit tests covering buffer push/eviction, backoff, state transitions,
outage threshold, drain success, and destroy cleanup
* style: apply StyLua formatting
---------
Co-authored-by: OpenClaw <openclaw@dmiller.me>
Co-authored-by: svc-finitelabs[bot] <svc-finitelabs[bot]@users.noreply.github.com>
* DRV-8: Implement measurement add/remove/configure UI pattern Active measurement context pattern using DYNAMIC_LIST for measurement selection (following Home Connect's Configure Camera pattern): - Select Measurement: DYNAMIC_LIST, shown/hidden based on measurement count - Per-measurement config properties (variable selector, write interval, enabled) shown/hidden based on selected measurement context - Add/remove variables via VARIABLE_SELECTOR + action buttons - Remove Variable: DYNAMIC_LIST populated from configured variables - Configured Variables: read-only display of fields and tags - All measurement config persisted via C4:PersistSetValue - gInitialized guard on OPC handlers (HC pattern) - Constants: SELECT_OPTION, NONE_OPTION added for consistency * DRV-8: Convert measurement actions to property-driven OPC handlers Address PR review feedback: - Remove EC.AddAsField, EC.AddAsTag, EC.RemoveSelectedVariable, EC.RemoveMeasurement action handlers - Add OPC.Add_Variable_As: LIST property with (Select)/Field/Tag options, auto-resets after selection (matches HC property-driven pattern) - Add OPC.Remove_Variable: triggers removal on DYNAMIC_LIST selection instead of requiring a separate action button - Add OPC.Remove_Measurement: LIST property with (Select)/Remove options, auto-resets after selection - Remove corresponding <action> entries from driver.xml - Add new properties to refreshMeasurementUI show/hide list - All configuration now flows through OPC handlers, no ExecuteCommand actions needed for measurement config (consistent with Home Connect) * Address review comments: remove images/.gitkeep, delete extra newline in driver.xml --------- Co-authored-by: OpenClaw <openclaw@dmiller.me>
* feat: DRV-10/DRV-11 — InfluxDB write client and batch engine DRV-10: InfluxDB HTTP write client (src/lib/influx_writer.lua) - Full line protocol builder: measurement, tags, fields, timestamp - Value type coercion: integer (i suffix), float, string (quoted), boolean - Proper escaping per InfluxDB spec (commas, spaces, equals in measurement/tag/field keys+values) - POST to /api/v2/write?db=<database>&precision=<precision> - Authorization: Token <token> header - Response handling: 200/204 ok, 401 permanent auth error, 422 permanent parse error, 429 rate-limited (Retry-After header), 5xx retriable server errors - Clear retriable vs permanent error classification DRV-11: Per-measurement batch engine (InfluxWriter:new()) - Per-measurement write buffers and independent flush timers - Configurable flush interval per measurement - Dedup: skip enqueue if all field values unchanged since last flush (configurable) - Max buffer size with FIFO eviction, fires Buffer Full event - Force-flush action (ForceFlushAll) and per-measurement forceFlush() - Flush on driver shutdown via influxWriter:shutdown() - Metrics as driver variables: INFLUX_POINTS_BUFFERED, INFLUX_POINTS_WRITTEN, INFLUX_POINTS_DROPPED, INFLUX_WRITE_ERRORS, INFLUX_LAST_WRITE_TS driver.lua: wire in InfluxWriter, update flushBuffer to use postBatch, add ForceFlushAll action handler, init/shutdown lifecycle hooks. driver.xml: add INFLUX_* variables and Force Flush All action. * style: apply StyLua formatting to fix CI * refactor: replace callback patterns with deferred lib in influx_writer Address review feedback: use vendor/deferred.lua instead of callback patterns for postBatch and all callers. * fix: correct deferred module require path The module is in vendor/deferred.lua and the vendor/ directory is already in the Lua package path, so the correct require is 'deferred' not 'vendor.deferred'. * style: format long reject lines to pass dirty-tree check * chore: remove ticket comments from influx_writer.lua * refactor: use values lib for metric variables Replace raw C4:SetVariable() calls in _updateMetricVariables with values:update() from the values lib, which handles variable registration, type coercion, and persistence. Remove static <variables> block from driver.xml since the values lib registers variables dynamically via C4:AddVariable(). --------- Co-authored-by: OpenClaw <openclaw@finitelabs.com> Co-authored-by: OpenClaw <openclaw@dmiller.me>
Co-authored-by: OpenClaw <openclaw@dmiller.me>
Refactors the monolithic driver.lua into a modular architecture matching the ESPHome/Home Connect driver patterns, with bug fixes and UX improvements found during end-to-end testing on a live controller. ## Architecture Extracts three new modules from driver.lua (1719 → 480 lines): - `src/lib/subscriptions.lua` — Variable subscription engine - `src/lib/measurements.lua` — Measurement CRUD and UI management - `src/lib/influx_client.lua` — Connection config and health check ## Bug Fixes - Flush timers never fired (C4:AddTimer → SetTimer/CancelTimer) - Tags missing from initial data points (_subscribing flag) - Deleted 349 lines of dead duplicate code - InfluxDB 3: `db=` → `bucket=`, health endpoint → write probe - persist dot/colon syntax, log method names, dedup override logic - Events/conditionals wired correctly for XML-defined elements ## UX Improvements - Auto-connect on URL/Token/Database change - Home Connect add/remove/configure pattern for measurements - Separate Add Field / Add Tag selectors with Remove dropdowns - Auto-hide properties when empty, readable variable labels - Removed redundant actions (kept Update Drivers + Clear Offline Buffer) ## Other - Removed legacy write path, all writes via Deferred-based InfluxWriter - GitHub updater wired up, consistent trace logging, LuaDoc annotations - Complete documentation rewrite matching style guide
…vers) (#13) The XML command was UpdateDrivers but the EC handler is EC.Update_Drivers. C4 maps commands by exact name, so the action never fired.
…#14) The Test Connection action was removed in DRV-21. The driver now auto-connects when InfluxDB Settings are configured. Updated both DriverCentral and GitHub installation steps to reflect this.
…te check (#15) - Add OnDriverInit with C4:AllowExecute, cloud-client-byte require, and log:setLogName matching the ESPHome/Home Connect pattern - Add C4:FileSetDir magic init call in OnDriverLateInit - Add leader election (lowest device ID) for update checks with recomputation on each cycle (DRV-22 fix) - Add syncPropertyToOtherInstances for Automatic Updates and Update Channel - Add periodic 30-minute update check timer (leader instance only)
* DRV-22: Consistent property sync and release workflow - Use SetDeviceProperties utility for property sync (consistent with all drivers) - Glob zip files in release workflow (no hardcoded repo name) * fix: update changelog wording per review feedback Property sync already works in the released version — this change is about consistency with the other drivers, not a bug fix. * fix: pre-wrap changelog line to pass dirty-tree check The build regenerates README.md via pandoc, which wraps lines at ~72 columns. The changelog entry exceeded that limit, causing a dirty-tree diff in CI. * fix: reset README.md (auto-generated by build) * chore: regenerate README after build * fix: revert changelog entry per review — user-facing changes only * fix: remove Unreleased changelog section — no user-facing changes --------- Co-authored-by: OpenClaw <openclaw@dmiller.me> Co-authored-by: svc-finitelabs[bot] <svc-finitelabs[bot]@users.noreply.github.com>
Co-authored-by: OpenClaw <openclaw@dmiller.me>
Remove module-level isLeaderInstance variable and its check from OPC guards. The leader check should only gate the periodic update timer, not property syncing between instances. Co-authored-by: OpenClaw <openclaw@dmiller.me>
There was a problem hiding this comment.
Code Review — PR #19
Solid architectural upgrade. Moving from flat property-based config to schema-based measurements with a web UI is the right call for this level of complexity. The transform engine, per-device readings, and global interval timers are all well-designed. A few things worth addressing before merge:
🔴 Issues
1. UIR._GET_CONFIG() restarts interval timers as a side effect
GET_CONFIG is semantically a read operation, but it calls subEngine:restartIntervalTimers(). This means every config fetch (initial load, after every CRUD mutation, the JS calls it after every action) stops and restarts all interval timers, causing timing drift. The timer restart should live in the mutation handlers that actually change config (add/remove measurement, update settings, add/remove reading, save mapping, etc.), not in the read path.
2. transform.lua — setfenv on cached function object
compile() caches the function, and eval() calls setfenv(fn, env) on the cached instance before pcall. In DriverWorks' single-threaded Lua this works, but setfenv mutates the function in-place. If a future C4 callback interrupts between setfenv and pcall (unlikely but possible with timer callbacks), the env could be wrong. Safer to either:
- Clone per-call:
local sandboxed = function() return fn() end; setfenv(sandboxed, env); pcall(sandboxed) - Or compile fresh each call and only cache the source validation result
Given DriverWorks is cooperative single-threaded, this is low risk, but worth a comment at minimum.
3. utils.lua displayName format change affects all callers
The GetDevice() change (displayName = string.format("%s (%d)", displayName, deviceIdInt) and the room name dedup) affects every caller across the codebase, not just the web UI. If anything else parses or displays displayName, this is a breaking change. Should this be scoped to the web UI's device list instead?
🟡 Suggestions
4. Transform cache eviction is all-or-nothing
When _cache hits MAX_CACHE_SIZE, the entire cache is cleared. This causes a compile storm for all active expressions. Consider evicting the oldest half, or just bumping MAX_CACHE_SIZE higher (expressions are tiny, 200-500 would be fine).
5. MAPPING_SOURCES constant appears unused
constants.MAPPING_SOURCES = { "Variable", "Literal" } is added but never referenced in the diff. Dead code?
6. Missing newline at end of index.html
Minor, but \ No newline at end of file in the diff.
✅ What looks good
- Schema-based model (fieldDefs, tagDefs, per-device readings) is a clean upgrade from flat variable lists
- Transform engine sandboxing via
setfenvwith a minimal allowlist is solid for this context - Global interval timers grouped by write interval is more efficient than per-measurement timers
- Web UI is well-structured with real-time socket.io updates, searchable dropdowns, live transform preview
- InfluxWriter
dedupKeycleanly separates per-reading dedup state without changing the measurement name written to InfluxDB - Old-format migration in
MeasurementManager:new()gracefully discards pre-schema configs - Reading-level subscription refs are a good granularity improvement over measurement-level refs
- Clean separation of concerns: measurements.lua owns data, subscriptions.lua owns variable lifecycle, transform.lua owns expression eval, web UI owns presentation
| end | ||
| local displayName = device.deviceName | ||
| if not IsEmpty(device.roomName) then | ||
| if not IsEmpty(device.roomName) and device.roomName ~= device.deviceName then |
There was a problem hiding this comment.
Do not touch this file. This is a fix specific to this driver. This file is a template managed file.
There was a problem hiding this comment.
Already reverted — utils.lua changes were dropped in the last push.
There was a problem hiding this comment.
Took a thorough look at this. Overall it's a really clean redesign: moving from the property-based UI to a schema-driven web UI with per-device readings is a big step forward. The code is well structured and the web UI is solid vanilla JS. A few things I noticed:
Architecture
Immediate flush on enqueue bypasses batching. Each enqueue() now calls _flushMeasurement(stateKey) immediately (influx_writer.lua L380). Combined with the global interval timers calling _enqueueReadingPoint() for each reading individually, this means every reading at each interval tick produces its own HTTP POST with a single point. If someone has 50 readings on the same interval, that's 50 individual writes per tick.
Previously the per-measurement flush timers allowed multiple enqueues to accumulate in the buffer before flushing. Consider either:
- Batching all readings at the same interval into a single flush (the interval timer could collect points, then flush once)
- Or reintroducing a short debounce/buffer window after the interval timer fires
Not necessarily a blocker for a typical setup with a handful of readings, but worth thinking about for scale.
Interval timer restart on every config mutation. The UIR._GET_CONFIG() handler (driver.lua L289) calls subEngine:restartIntervalTimers() every time. Since every config-mutating UI action returns UIR._GET_CONFIG(), every add/remove/update operation restarts all interval timers, resetting their phase. Probably fine since config changes are infrequent, but worth noting.
Code
UIR._VALIDATE_TRANSFORM(driver.lua L460) usesC4:SendDataToUI()directly instead ofuiRespond(). The socket push works fine for the web UI, but for consistency and so REST callers also get a response, might want to useuiRespond()here too.- Missing newline at EOF in
drivers/influxdb/www/html/index.html. - Old config migration silently discards old-format measurements. The
log:info("Discarding old-format measurement '%s'", name)is good, but worth bumping tolog:warnsince it's data loss on upgrade.
Web UI
Clean implementation. The searchable device/variable dropdowns, real-time socket updates, and client-side transform preview are nice touches. Hash-based deep linking for measurement editing is a good UX detail.
One thought: the PROP_GROUPS object in the JS hardcodes which properties go in which group. If properties are added/removed in driver.xml later, the JS needs updating too. Not a problem now, just a maintenance note.
No merge from me (your PR), but looks solid overall.
Address anything relevant from this review. |
There was a problem hiding this comment.
Reverted — restored src/lib/utils.lua to match main. Both changes (the roomName dedup fix and the device ID suffix) have been removed from this branch. If the roomName fix is worth having across all repos, I can open a PR against the template instead.
Review Fixes —
|
|
🔴 CI failed on The formatter rewrote two files that differ from what was committed:
Quick fix: run make build
git add src/constants.lua src/lib/transform.lua
git commit -m "style: apply formatter"
git push |
|
Build failed on the dirty tree check —
Quick fix: run |
|
Can you fix the build? |
|
Fixed — the build was failing on the "Check for dirty tree" step. The formatter (stylua + prettier) was producing diffs that weren't committed:
Pushed a formatting commit ( |
|
CI is failing on the dirty tree check — prettier is reformatting The diff is all cosmetic: line-wrap changes, markdown table column padding, and Quick fix: run |
|
Build failure: dirty tree (formatting drift) The
All in Fix: Run the build locally ( |
|
you need to run |
|
Fixed — pushed |
| local devices = {} | ||
| local seen = {} | ||
| local xml = C4:GetProjectItems("LIMIT_DEVICE_DRIVERS", "NO_ROOT_TAGS") or "" | ||
| for idStr in xml:gmatch("<id>(%d+)</id>") do |
There was a problem hiding this comment.
We should be using ParseXml for all xml parsing
There was a problem hiding this comment.
Good call. Replaced the gmatch with ParseXml + Select + IsList, matching the pattern from utils.lua. Pushed in d7e328d.
0f19685 to
d24ff2e
Compare
… web UI Replaces the flat variable-to-measurement mapping with a schema-based approach: measurements define field/tag names, readings map N devices to that schema with optional value transforms. Backend: - New data model: MeasurementConfig with fieldDefs, tagDefs, readings - Transform engine (loadstring + setfenv sandbox) with device_name(), room_name(), map() built-in functions - Per-reading dedup in InfluxWriter via dedupKey - Global interval timers grouped by write interval (bounded count) - InfluxWriter flushes immediately on enqueue (no per-reading timers) - ParseXml for device list XML parsing (replaces raw gmatch) Web UI (Composer Pro <tabs> pattern): - Status, Settings, Measurements panels with deep-linked views - Dynamic property rendering via REST API - Socket.io subscription for real-time SendDataToUI events - Searchable device picker with text filter - Live transform preview with error feedback - Per-reading device tags dropdown - Per-measurement dedup toggle - Form validation with inline error states - Auto-refresh status every 30s Communication: REST POST /commands for UIRequest, socket.io for SendDataToUI. Commands prefixed with _ to avoid C4 reserved names. Properties remain source of truth for connection/driver settings.
d24ff2e to
c3d3ce7
Compare
9faf9f1 to
b974185
Compare
Summary
device_name(),room_name(),map()built-in functions<tabs>pattern (REST + socket.io communication)Test plan
value * 100,device_name(value),map({...})