From 42bfab6952180a6182f27b1d725db060845293bc Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Tue, 27 Oct 2026 17:27:30 +0000 Subject: [PATCH 1/4] Add myenergi plugin Monitors myenergi devices through the director digest-auth cloud API: the hub plus every Zappi, Eddi, Harvi and Libbi is imported, with streams for live power flow, device status, CT readings, energy history (per minute up to 24 hours, hourly beyond) and boost schedules. Out-of-the-box dashboards cover the whole site and each device type, using smartviz charts: live and period energy-flow sankeys, self-sufficiency rings, daily balance bars, hour-by-day heatmaps, and per-device gauges, donuts and rich tables. Tested live against a Zappi and an Eddi; the Libbi and Harvi content is untested. Co-Authored-By: Claude Opus 5.5 --- plugins/MyEnergi/v1/configValidation.json | 13 + plugins/MyEnergi/v1/custom_types.json | 37 + .../v1/dataStreams/boostSchedules.json | 25 + .../MyEnergi/v1/dataStreams/ctReadings.json | 28 + .../MyEnergi/v1/dataStreams/deviceStatus.json | 37 + plugins/MyEnergi/v1/dataStreams/devices.json | 30 + .../v1/dataStreams/energyHistory.json | 54 ++ .../MyEnergi/v1/dataStreams/powerFlow.json | 27 + .../v1/dataStreams/scripts/boostSchedules.js | 40 + .../v1/dataStreams/scripts/ctReadings.js | 61 ++ .../v1/dataStreams/scripts/deviceStatus.js | 162 ++++ .../v1/dataStreams/scripts/devices.js | 73 ++ .../v1/dataStreams/scripts/energyHistory.js | 40 + .../v1/dataStreams/scripts/powerFlow.js | 70 ++ .../MyEnergi/v1/defaultContent/eddi.dash.json | 698 ++++++++++++++++ .../v1/defaultContent/harvi.dash.json | 299 +++++++ .../v1/defaultContent/libbi.dash.json | 540 +++++++++++++ .../MyEnergi/v1/defaultContent/manifest.json | 24 + .../v1/defaultContent/overview.dash.json | 488 +++++++++++ .../MyEnergi/v1/defaultContent/scopes.json | 97 +++ .../v1/defaultContent/zappi.dash.json | 763 ++++++++++++++++++ plugins/MyEnergi/v1/docs/README.md | 66 ++ plugins/MyEnergi/v1/icon.svg | 6 + .../MyEnergi/v1/indexDefinitions/default.json | 27 + plugins/MyEnergi/v1/metadata.json | 52 ++ plugins/MyEnergi/v1/ui.json | 33 + 26 files changed, 3790 insertions(+) create mode 100644 plugins/MyEnergi/v1/configValidation.json create mode 100644 plugins/MyEnergi/v1/custom_types.json create mode 100644 plugins/MyEnergi/v1/dataStreams/boostSchedules.json create mode 100644 plugins/MyEnergi/v1/dataStreams/ctReadings.json create mode 100644 plugins/MyEnergi/v1/dataStreams/deviceStatus.json create mode 100644 plugins/MyEnergi/v1/dataStreams/devices.json create mode 100644 plugins/MyEnergi/v1/dataStreams/energyHistory.json create mode 100644 plugins/MyEnergi/v1/dataStreams/powerFlow.json create mode 100644 plugins/MyEnergi/v1/dataStreams/scripts/boostSchedules.js create mode 100644 plugins/MyEnergi/v1/dataStreams/scripts/ctReadings.js create mode 100644 plugins/MyEnergi/v1/dataStreams/scripts/deviceStatus.js create mode 100644 plugins/MyEnergi/v1/dataStreams/scripts/devices.js create mode 100644 plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js create mode 100644 plugins/MyEnergi/v1/dataStreams/scripts/powerFlow.js create mode 100644 plugins/MyEnergi/v1/defaultContent/eddi.dash.json create mode 100644 plugins/MyEnergi/v1/defaultContent/harvi.dash.json create mode 100644 plugins/MyEnergi/v1/defaultContent/libbi.dash.json create mode 100644 plugins/MyEnergi/v1/defaultContent/manifest.json create mode 100644 plugins/MyEnergi/v1/defaultContent/overview.dash.json create mode 100644 plugins/MyEnergi/v1/defaultContent/scopes.json create mode 100644 plugins/MyEnergi/v1/defaultContent/zappi.dash.json create mode 100644 plugins/MyEnergi/v1/docs/README.md create mode 100644 plugins/MyEnergi/v1/icon.svg create mode 100644 plugins/MyEnergi/v1/indexDefinitions/default.json create mode 100644 plugins/MyEnergi/v1/metadata.json create mode 100644 plugins/MyEnergi/v1/ui.json diff --git a/plugins/MyEnergi/v1/configValidation.json b/plugins/MyEnergi/v1/configValidation.json new file mode 100644 index 00000000..348787f4 --- /dev/null +++ b/plugins/MyEnergi/v1/configValidation.json @@ -0,0 +1,13 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { + "name": "devices" + }, + "required": true, + "error": "Could not connect. Check the hub serial number and API key match the gateway device in myenergi myaccount, and that the key has not been regenerated since.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/MyEnergi/v1/custom_types.json b/plugins/MyEnergi/v1/custom_types.json new file mode 100644 index 00000000..858c14fb --- /dev/null +++ b/plugins/MyEnergi/v1/custom_types.json @@ -0,0 +1,37 @@ +[ + { + "name": "Hub", + "sourceType": "Hub", + "icon": "wifi", + "singular": "Hub", + "plural": "Hubs" + }, + { + "name": "Zappi", + "sourceType": "Zappi", + "icon": "car", + "singular": "Zappi", + "plural": "Zappis" + }, + { + "name": "Eddi", + "sourceType": "Eddi", + "icon": "droplet", + "singular": "Eddi", + "plural": "Eddis" + }, + { + "name": "Harvi", + "sourceType": "Harvi", + "icon": "gauge", + "singular": "Harvi", + "plural": "Harvis" + }, + { + "name": "Libbi", + "sourceType": "Libbi", + "icon": "battery-full", + "singular": "Libbi", + "plural": "Libbis" + } +] diff --git a/plugins/MyEnergi/v1/dataStreams/boostSchedules.json b/plugins/MyEnergi/v1/dataStreams/boostSchedules.json new file mode 100644 index 00000000..11c03072 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/boostSchedules.json @@ -0,0 +1,25 @@ +{ + "name": "boostSchedules", + "displayName": "Boost Schedules", + "description": "Configured boost timer slots for each Zappi and Eddi", + "tags": ["Energy"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "cgi-boost-time-{{object.rawId}}", + "paging": { "mode": "none" }, + "postRequestScript": "boostSchedules.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Zappi", "Eddi"] } }, + "metadata": [ + { "name": "deviceName", "displayName": "Device", "shape": "string", "role": "label" }, + { "name": "deviceKey", "displayName": "Device key", "shape": "string", "role": "id", "visible": false }, + { "name": "deviceType", "displayName": "Type", "shape": "string" }, + { "name": "slot", "displayName": "Slot", "shape": "number" }, + { "name": "target", "displayName": "Target", "shape": "string" }, + { "name": "startTime", "displayName": "Start time", "shape": "string" }, + { "name": "duration", "displayName": "Duration", "shape": "minutes" }, + { "name": "days", "displayName": "Days", "shape": ["array", { "itemShape": "string" }] } + ], + "timeframes": false +} diff --git a/plugins/MyEnergi/v1/dataStreams/ctReadings.json b/plugins/MyEnergi/v1/dataStreams/ctReadings.json new file mode 100644 index 00000000..6583b5f9 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/ctReadings.json @@ -0,0 +1,28 @@ +{ + "name": "ctReadings", + "displayName": "CT Readings", + "description": "Live power reading from each current transformer clamp on the selected devices", + "tags": ["Energy", "Devices"], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "cgi-jstatus-*", + "paging": { "mode": "none" }, + "postRequestScript": "ctReadings.js" + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Zappi", "Eddi", "Harvi", "Libbi"] } + }, + "metadata": [ + { "name": "ctLabel", "displayName": "CT", "shape": "string", "role": "label" }, + { "name": "deviceName", "displayName": "Device", "shape": "string" }, + { "name": "deviceKey", "displayName": "Device key", "shape": "string", "visible": false }, + { "name": "deviceType", "displayName": "Device type", "shape": "string" }, + { "name": "ct", "displayName": "CT number", "shape": "number" }, + { "name": "ctType", "displayName": "CT type", "shape": "string" }, + { "name": "power", "displayName": "Power (W)", "shape": ["number", { "decimalPlaces": 0 }], "role": "value" }, + { "name": "phase", "displayName": "Phase", "shape": "number" }, + { "name": "lastUpdated", "displayName": "Last updated", "shape": "date", "role": "timestamp" } + ], + "timeframes": false +} diff --git a/plugins/MyEnergi/v1/dataStreams/deviceStatus.json b/plugins/MyEnergi/v1/dataStreams/deviceStatus.json new file mode 100644 index 00000000..52200998 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/deviceStatus.json @@ -0,0 +1,37 @@ +{ + "name": "deviceStatus", + "displayName": "Device Status", + "description": "Live status, mode, power and temperatures for each Zappi, Eddi, Harvi and Libbi", + "tags": ["Energy"], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "cgi-jstatus-*", + "paging": { "mode": "none" }, + "postRequestScript": "deviceStatus.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Zappi", "Eddi", "Harvi", "Libbi"] } }, + "metadata": [ + { "name": "deviceName", "displayName": "Device", "shape": "string", "role": "label" }, + { "name": "deviceKey", "displayName": "Device key", "shape": "string", "role": "id", "visible": false }, + { "name": "deviceType", "displayName": "Type", "shape": "string" }, + { "name": "status", "displayName": "Status", "shape": "string" }, + { "name": "health", "displayName": "Health", "shape": "state" }, + { "name": "mode", "displayName": "Mode", "shape": "string" }, + { "name": "plugStatus", "displayName": "Plug status", "shape": "string" }, + { "name": "power", "displayName": "Power (W)", "shape": "number", "role": "value" }, + { "name": "energy", "displayName": "Energy (kWh)", "shape": ["number", { "decimalPlaces": 2 }] }, + { "name": "activeHeater", "displayName": "Active heater", "shape": "string" }, + { "name": "tank1Temperature", "displayName": "Tank 1 temperature (°C)", "shape": "number" }, + { "name": "tank2Temperature", "displayName": "Tank 2 temperature (°C)", "shape": "number" }, + { "name": "batteryLevel", "displayName": "Battery level", "shape": "percent" }, + { "name": "minGreenLevel", "displayName": "Minimum green level", "shape": "percent" }, + { "name": "boosting", "displayName": "Boosting", "shape": "boolean" }, + { "name": "voltage", "displayName": "Voltage (V)", "shape": ["number", { "decimalPlaces": 1 }] }, + { "name": "frequency", "displayName": "Frequency (Hz)", "shape": ["number", { "decimalPlaces": 2 }] }, + { "name": "gridPower", "displayName": "Grid power (W)", "shape": "number" }, + { "name": "lastUpdated", "displayName": "Last updated", "shape": "date", "role": "timestamp" }, + { "name": "firmware", "displayName": "Firmware", "shape": "string" } + ], + "timeframes": false +} diff --git a/plugins/MyEnergi/v1/dataStreams/devices.json b/plugins/MyEnergi/v1/dataStreams/devices.json new file mode 100644 index 00000000..6e199d96 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/devices.json @@ -0,0 +1,30 @@ +{ + "name": "devices", + "displayName": "Devices", + "description": "The hub and every Zappi, Eddi, Harvi and Libbi registered to it", + "tags": ["Devices", "Energy"], + "baseDataSourceName": "httpRequestUnscoped", + "visibility": { "type": "hidden" }, + "config": { + "httpMethod": "get", + "endpointPath": "cgi-jstatus-*", + "paging": { "mode": "none" }, + "postRequestScript": "devices.js" + }, + "matches": "none", + "metadata": [ + { "name": "deviceKey", "displayName": "Device key", "shape": "string", "role": "id" }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "sourceType", "displayName": "Type", "shape": "string" }, + { "name": "serial", "displayName": "Serial number", "shape": "string" }, + { "name": "firmware", "displayName": "Firmware", "shape": "string" }, + { "name": "phases", "displayName": "Phases", "shape": "number" }, + { "name": "ctTypes", "displayName": "CT types", "shape": ["array", { "itemShape": "string" }] }, + { "name": "heater1Name", "displayName": "Heater 1 name", "shape": "string" }, + { "name": "heater2Name", "displayName": "Heater 2 name", "shape": "string" }, + { "name": "batteryCapacity", "displayName": "Battery capacity (Wh)", "shape": "number" }, + { "name": "inverterSize", "displayName": "Inverter size (W)", "shape": "number" }, + { "name": "server", "displayName": "Server", "shape": "string" } + ], + "timeframes": false +} diff --git a/plugins/MyEnergi/v1/dataStreams/energyHistory.json b/plugins/MyEnergi/v1/dataStreams/energyHistory.json new file mode 100644 index 00000000..edc038e7 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/energyHistory.json @@ -0,0 +1,54 @@ +{ + "name": "energyHistory", + "displayName": "Energy History", + "description": "Energy imported, exported, generated, diverted and boosted over time for the selected devices", + "tags": ["Energy"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "{{ (() => { const start = new Date(timeframe.start); const end = new Date(timeframe.end); const day = (d) => [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()].join('-'); if (end - start <= 86400000) { const from = new Date(Math.floor(start / 60000) * 60000); return 'cgi-jday-' + object.rawId + '-' + day(from) + '-' + from.getUTCHours() + '-' + from.getUTCMinutes() + '-' + Math.floor((end - from) / 60000); } const from = new Date(Math.floor(start / 3600000) * 3600000); return 'cgi-jdayhour-' + object.rawId + '-' + day(from) + '-' + from.getUTCHours() + '-' + Math.ceil((end - from) / 3600000); })() }}", + "paging": {"mode": "none"}, + "postRequestScript": "energyHistory.js" + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": ["Zappi", "Eddi", "Libbi"] + } + }, + "ui": [ + { + "type": "choiceChips", + "name": "measure", + "label": "Measure", + "defaultValue": "energy", + "options": [ + {"value": "energy", "label": "Energy (kWh)"}, + {"value": "power", "label": "Average power (kW)"} + ] + } + ], + "metadata": [ + {"name": "timestamp", "displayName": "Time", "shape": "date", "role": "timestamp"}, + {"name": "deviceName", "displayName": "Device", "shape": "string", "role": "label"}, + {"name": "deviceKey", "displayName": "Device key", "shape": "string", "visible": false}, + {"name": "deviceType", "displayName": "Device type", "shape": "string"}, + {"name": "import", "displayName": "Grid import", "shape": ["number", {"decimalPlaces": 3}]}, + {"name": "export", "displayName": "Grid export", "shape": ["number", {"decimalPlaces": 3}]}, + {"name": "generation", "displayName": "Generation", "shape": ["number", {"decimalPlaces": 3}]}, + {"name": "diverted", "displayName": "Diverted", "shape": ["number", {"decimalPlaces": 3}]}, + {"name": "boosted", "displayName": "Boosted", "shape": ["number", {"decimalPlaces": 3}]}, + {"name": "batteryCharge", "displayName": "Battery charge", "shape": ["number", {"decimalPlaces": 3}]}, + {"name": "batteryDischarge", "displayName": "Battery discharge", "shape": ["number", {"decimalPlaces": 3}]}, + {"name": "unit", "displayName": "Unit", "shape": "string", "role": "unitLabel"} + ], + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days", + "thisMonth", + "lastMonth" + ] +} diff --git a/plugins/MyEnergi/v1/dataStreams/powerFlow.json b/plugins/MyEnergi/v1/dataStreams/powerFlow.json new file mode 100644 index 00000000..3ee0e1a2 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/powerFlow.json @@ -0,0 +1,27 @@ +{ + "name": "powerFlow", + "displayName": "Power Flow", + "description": "Live site power flow including grid, generation, EV charging, hot water and battery", + "tags": ["Energy"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "cgi-jstatus-*", + "paging": { "mode": "none" }, + "postRequestScript": "powerFlow.js" + }, + "matches": "none", + "metadata": [ + { "name": "gridPower", "displayName": "Grid (W)", "shape": "number" }, + { "name": "importPower", "displayName": "Import (W)", "shape": "number" }, + { "name": "exportPower", "displayName": "Export (W)", "shape": "number" }, + { "name": "generationPower", "displayName": "Generation (W)", "shape": "number" }, + { "name": "consumptionPower", "displayName": "Consumption (W)", "shape": "number", "role": "value" }, + { "name": "houseLoadPower", "displayName": "House load (W)", "shape": "number" }, + { "name": "evChargePower", "displayName": "EV charging (W)", "shape": "number" }, + { "name": "hotWaterPower", "displayName": "Hot water (W)", "shape": "number" }, + { "name": "batteryPower", "displayName": "Battery (W)", "shape": "number" }, + { "name": "lastUpdated", "displayName": "Last updated", "shape": "date", "role": "timestamp" } + ], + "timeframes": false +} diff --git a/plugins/MyEnergi/v1/dataStreams/scripts/boostSchedules.js b/plugins/MyEnergi/v1/dataStreams/scripts/boostSchedules.js new file mode 100644 index 00000000..b7b4135e --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/scripts/boostSchedules.js @@ -0,0 +1,40 @@ +// cgi-boost-time-{key} returns every slot the device has, configured or not, as +// { boost_times: [{ slt, bsh, bsm, bdh, bdm, bdd }] }. An unset slot has an all-zero +// day mask, so a slot only counts as a schedule when at least one day is set. +// bdd is eight characters: a constant leading 0, then Monday to Sunday. +const dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; +const pad = (n) => String(n ?? 0).padStart(2, "0"); + +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const object = context.objects[0]; +const deviceKey = unwrap(object.rawId); +const deviceName = unwrap(object.name); +const deviceType = deviceKey.startsWith("Z") ? "Zappi" : "Eddi"; + +// The slot numbering for an Eddi is not documented (see the slot tens digit: 1, 2, 5, 6 +// all appear, with zero durations), so an Eddi slot gets a neutral output label +// derived from the tens digit rather than a guessed heater or relay name. +const targetFor = (slot) => + deviceType === "Zappi" ? "EV" : `Output ${Math.floor(slot / 10)}`; + +const slots = Array.isArray(data?.boost_times) ? data.boost_times : []; + +result = slots + .map((s) => { + const mask = String(s.bdd ?? "").padStart(8, "0"); + const days = dayNames.filter((_, i) => mask[i + 1] === "1"); + // A duration of 0 is reported for Eddi slots and means "no fixed length", + // not zero minutes, so it is returned as blank. + const minutes = (s.bdh ?? 0) * 60 + (s.bdm ?? 0); + return { + deviceName, + deviceKey, + deviceType, + slot: s.slt, + target: targetFor(s.slt), + startTime: `${pad(s.bsh)}:${pad(s.bsm)}`, + duration: minutes > 0 ? minutes : null, + days, + }; + }) + .filter((row) => row.days.length > 0); diff --git a/plugins/MyEnergi/v1/dataStreams/scripts/ctReadings.js b/plugins/MyEnergi/v1/dataStreams/scripts/ctReadings.js new file mode 100644 index 00000000..f4c80672 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/scripts/ctReadings.js @@ -0,0 +1,61 @@ +// cgi-jstatus-* returns every device on the hub in one response, so this scoped stream +// makes a single request and keeps only the devices that were selected. +const families = { + zappi: { type: "Zappi", prefix: "Z" }, + eddi: { type: "Eddi", prefix: "E" }, + harvi: { type: "Harvi", prefix: "H" }, + libbi: { type: "Libbi", prefix: "L" }, +}; + +const selected = new Map( + (context.objects || []).map((o) => [o.rawId, o.name]), +); + +// dat is DD-MM-YYYY and tim HH:MM:SS (UTC); Libbi pads tim with spaces ("14: 53: 03"). +const toIso = ({ dat, tim }) => { + const date = /^(\d{2})-(\d{2})-(\d{4})$/.exec(String(dat || "").trim()); + const time = /^(\d{1,2}):(\d{2}):(\d{2})$/.exec(String(tim || "").replace(/\s/g, "")); + if (!date || !time) { + return null; + } + return `${date[3]}-${date[2]}-${date[1]}T${time[1].padStart(2, "0")}:${time[2]}:${time[3]}Z`; +}; + +const rows = []; + +for (const element of Array.isArray(data) ? data : []) { + for (const [key, value] of Object.entries(element || {})) { + const family = families[key]; + if (!family || !Array.isArray(value)) { + continue; + } + for (const device of value) { + const deviceKey = `${family.prefix}${device?.sno}`; + if (device?.sno === undefined || !selected.has(deviceKey)) { + continue; + } + const deviceName = selected.get(deviceKey) || `${family.type} ${device.sno}`; + const lastUpdated = toIso(device); + for (const n of [1, 2, 3, 4, 5, 6]) { + const ctType = device[`ectt${n}`]; + // Unused clamps are reported as "None"; a used clamp reading 0 W omits ectpN. + if (!ctType || ctType === "None") { + continue; + } + rows.push({ + ctLabel: `${deviceName} · CT${n} · ${ctType}`, + deviceName, + deviceKey, + deviceType: family.type, + ct: n, + ctType, + power: Math.round(device[`ectp${n}`] || 0), + phase: device[`ect${n}p`] ?? null, + lastUpdated, + }); + } + } + } +} + +result = rows; diff --git a/plugins/MyEnergi/v1/dataStreams/scripts/deviceStatus.js b/plugins/MyEnergi/v1/dataStreams/scripts/deviceStatus.js new file mode 100644 index 00000000..ff7af150 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/scripts/deviceStatus.js @@ -0,0 +1,162 @@ +// cgi-jstatus-* returns an array of single-key elements, one per device family +// (e.g. { "zappi": [...] }). One request covers every selected device, so the +// rows are filtered to the selected objects by device key (prefix + serial). +const families = { + zappi: { type: "Zappi", prefix: "Z" }, + eddi: { type: "Eddi", prefix: "E" }, + harvi: { type: "Harvi", prefix: "H" }, + libbi: { type: "Libbi", prefix: "L" }, +}; + +const STALE_MS = 15 * 60 * 1000; + +const unknown = (code) => `Unknown (${code})`; +const lookup = (table, code) => (table[code] !== undefined ? table[code] : unknown(code)); + +const zappiStatus = { 0: "Starting", 1: "Waiting for surplus", 2: "DSR", 3: "Charging", 4: "Boosting", 5: "Charge complete", 6: "Stopped" }; +const eddiStatus = { 1: "Waiting for surplus", 3: "Diverting", 4: "Boosting", 5: "Max temperature reached", 6: "Stopped" }; +const zappiMode = { 1: "Fast", 2: "Eco", 3: "Eco+", 4: "Stop" }; +const plugStatus = { A: "EV disconnected", B1: "EV connected", B2: "Waiting for EV", C1: "EV ready to charge", C2: "Charging", F: "Fault" }; + +// Libbi codes from pymyenergi. Fault codes are split out so health can flag them. +const libbiOk = { + 0: "Off", 1: "On", 2: "Battery Full", 4: "Idle", 5: "Charging", 6: "Discharging", + 7: "Duration Charging", 8: "Duration Drain", 11: "Stopped", 12: "Target Charge", + 51: "Boosting", 53: "Boosting", 55: "Boosting", 101: "Battery Empty", 102: "Battery Full", + 104: "Full", 233: "SoC Recovery", 234: "Calibration Charge", 251: "Upgrading DSP", + 252: "Upgrading ARM", 253: "BMS Upgrading", +}; +const libbiFault = { + 58: "Generation CT Polarity", 106: "Grid CT Lost", 107: "Local Comms Lost", + 151: "Inverter Undervoltage", 152: "Inverter Overvoltage", 153: "Inverter Under Frequency", + 154: "Inverter Over Frequency", 156: "Battery Lost", 157: "Battery Undervoltage", + 158: "Battery Voltage Low", 159: "Battery Voltage High", 160: "Overload", + 161: "GFCI Over Threshold", 163: "Fan Fault", 164: "Bat Under Capacity", + 165: "BMS Discharge Over Current", 166: "BMS Charge Over Current", 167: "BMS Over Voltage", + 168: "BMS Over Temperature", 169: "BMS Discharge Temperature Low", 170: "Battery Voltage Imbalance", + 171: "BMS Communications Fault", 172: "BMS Under Voltage", 173: "BMS Charge Temperature Low", + 174: "BMS Voltage High", 175: "BMS Temperature High", 176: "Grid CT Polarity", + 183: "Inv Comms Lost", 184: "Controller Undervoltage", 185: "Controller Overvoltage", + 186: "Controller Under Frequency", 187: "Controller Over Frequency", 201: "Soft Timeout", + 202: "Internal Voltage Short", 203: "GFCI Sensor Fault", 205: "Bus Low Fault", + 206: "Bus High Fault", 207: "Bus Short Fault", 208: "PV ISO Under", 209: "PV Input Short Circuit", + 210: "Bypass Relay Fault", 211: "Inverter Overcurrent", 212: "Inverter DC Over", + 213: "Ambient Over Temperature", 214: "Sink Over Temperature", 215: "Grid Relay Fault", + 216: "Discharge Overcurrent", 217: "Charge Overcurrent", 219: "Inverter Abnormal", + 220: "EPS Relay Fault", 232: "SCI Fault", +}; + +const named = (value) => (value && value !== "None" ? value : null); +const num = (value) => (typeof value === "number" ? value : 0); + +// dat is DD-MM-YYYY and tim is HH:MM:SS, both UTC; some firmware pads tim with spaces. +const parseTimestamp = (device) => { + const date = /^(\d{2})-(\d{2})-(\d{4})$/.exec(String(device.dat || "").replace(/\s/g, "")); + const time = /^(\d{1,2}):(\d{2}):(\d{2})$/.exec(String(device.tim || "").replace(/\s/g, "")); + if (!date || !time) { + return null; + } + const iso = `${date[3]}-${date[2]}-${date[1]}T${time[1].padStart(2, "0")}:${time[2]}:${time[3]}Z`; + return Number.isNaN(Date.parse(iso)) ? null : iso; +}; + +// Very old firmware reported volts rather than decivolts. +const voltage = (vol) => { + const v = num(vol); + return v > 1000 ? v / 10 : v; +}; + +// 127 and -1 mean no probe is fitted. +const temperature = (value) => (typeof value === "number" && value !== 127 && value !== -1 ? value : null); + +const activeHeater = (device) => { + const n = device.hno; + if (!n) { + return null; + } + return named(device[`ht${n}`]) || `Heater ${n}`; +}; + +const describe = (type, device) => { + switch (type) { + case "Zappi": { + const plug = device.pst; + return { + status: plug === "A" ? plugStatus.A : lookup(zappiStatus, device.sta), + fault: plug === "F", + mode: device.zmo !== undefined ? zappiMode[device.zmo] || null : null, + plugStatus: plug ? plugStatus[plug] || unknown(plug) : null, + }; + } + case "Eddi": + return { status: lookup(eddiStatus, device.sta), fault: false, mode: null, plugStatus: null }; + case "Libbi": { + const code = device.sta; + const mode = named(device.lmo); + return { + status: libbiFault[code] || libbiOk[code] || unknown(code), + fault: libbiFault[code] !== undefined, + mode: mode ? mode.charAt(0).toUpperCase() + mode.slice(1).toLowerCase() : null, + plugStatus: null, + }; + } + default: + return { status: null, fault: false, mode: null, plugStatus: null }; + } +}; + +const selected = new Map( + (context.objects || []).map((o) => [String(o.rawId), o.name]), +); + +const now = Date.now(); +const rows = []; + +for (const element of Array.isArray(data) ? data : []) { + for (const [key, value] of Object.entries(element || {})) { + const family = families[key]; + if (!family || !Array.isArray(value)) { + continue; + } + for (const device of value) { + if (device?.sno === undefined || device?.sno === null) { + continue; + } + const deviceKey = `${family.prefix}${device.sno}`; + if (!selected.has(deviceKey)) { + continue; + } + const lastUpdated = parseTimestamp(device); + const stale = !lastUpdated || now - Date.parse(lastUpdated) > STALE_MS; + const info = describe(family.type, device); + // Harvi reports no status; fresh data is the only signal it is working. + const status = family.type === "Harvi" ? (stale ? "No recent data" : "Reporting") : info.status; + const isLibbi = family.type === "Libbi"; + + rows.push({ + deviceName: selected.get(deviceKey) || `${family.type} ${device.sno}`, + deviceKey, + deviceType: family.type, + status, + health: info.fault ? "error" : stale ? "warning" : "success", + mode: info.mode, + plugStatus: info.plugStatus, + power: family.type === "Harvi" ? null : num(device.div), + energy: family.type === "Harvi" || isLibbi ? null : num(device.che), + activeHeater: family.type === "Eddi" ? activeHeater(device) : null, + tank1Temperature: family.type === "Eddi" ? temperature(device.tp1) : null, + tank2Temperature: family.type === "Eddi" ? temperature(device.tp2) : null, + batteryLevel: isLibbi && typeof device.soc === "number" ? device.soc : null, + minGreenLevel: family.type === "Zappi" && typeof device.mgl === "number" ? device.mgl : null, + boosting: Boolean(device.bsm || device.bst || device.bss), + voltage: voltage(device.vol), + frequency: num(device.frq), + gridPower: num(device.grd), + lastUpdated, + firmware: device.fwv || null, + }); + } + } +} + +result = rows; diff --git a/plugins/MyEnergi/v1/dataStreams/scripts/devices.js b/plugins/MyEnergi/v1/dataStreams/scripts/devices.js new file mode 100644 index 00000000..7b93eeab --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/scripts/devices.js @@ -0,0 +1,73 @@ +// cgi-jstatus-* returns an array of single-key elements, one per device family +// (e.g. { "zappi": [...] }), plus a trailing { "asn", "fwv" } element describing the hub. +// The object id is the family prefix plus serial (e.g. Z16042300), which is the form +// every per-device endpoint expects. +const families = { + zappi: { type: "Zappi", prefix: "Z" }, + eddi: { type: "Eddi", prefix: "E" }, + harvi: { type: "Harvi", prefix: "H" }, + libbi: { type: "Libbi", prefix: "L" }, +}; + +// Unused CTs and heaters are reported as the string "None". +const named = (value) => (value && value !== "None" ? value : null); + +const ctTypes = (device) => + [1, 2, 3, 4, 5, 6] + .map((n) => device[`ectt${n}`]) + .filter(named); + +const rows = []; +let hub; + +for (const element of Array.isArray(data) ? data : []) { + for (const [key, value] of Object.entries(element || {})) { + if (key === "asn") { + hub = { server: value, firmware: element.fwv || null }; + continue; + } + const family = families[key]; + if (!family || !Array.isArray(value)) { + continue; + } + for (const device of value) { + if (device?.sno === undefined || device?.sno === null) { + continue; + } + rows.push({ + deviceKey: `${family.prefix}${device.sno}`, + name: `${family.type} ${device.sno}`, + sourceType: family.type, + serial: String(device.sno), + firmware: device.fwv || null, + phases: device.pha ?? null, + ctTypes: ctTypes(device), + heater1Name: named(device.ht1), + heater2Name: named(device.ht2), + batteryCapacity: device.mbc ?? null, + inverterSize: device.mic ?? null, + server: null, + }); + } + } +} + +const hubSerial = String("{{ dataSource.hubSerial }}").trim(); +if (hub && /^[0-9]+$/.test(hubSerial)) { + rows.unshift({ + deviceKey: hubSerial, + name: `Hub ${hubSerial}`, + sourceType: "Hub", + serial: hubSerial, + firmware: hub.firmware, + phases: null, + ctTypes: [], + heater1Name: null, + heater2Name: null, + batteryCapacity: null, + inverterSize: null, + server: hub.server || null, + }); +} + +result = rows; diff --git a/plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js b/plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js new file mode 100644 index 00000000..f5ec4c79 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js @@ -0,0 +1,40 @@ +// Rows are keyed by UTC calendar parts; hr and min are omitted by the API when 0, +// and so is any energy field whose value is 0. +const device = context.objects[0]; +const prop = (p) => (Array.isArray(p) ? p[0] : p); +const power = context.config?.measure === "power"; + +const start = new Date(context.timeframe.start).getTime(); +const end = new Date(context.timeframe.end).getTime(); + +// The response is { "U": [rows] }; take the first array rather than build the key. +const history = Object.values(data || {}).find(Array.isArray) || []; + +const rows = history.map((row) => ({ row, time: Date.UTC(row.yr, row.mon - 1, row.dom, row.hr || 0, row.min || 0) })); + +// Mirrors the endpoint choice in the stream (minute rows up to 24 hours, else hourly). +const seconds = end - start <= 86400000 ? 60 : 3600; + +// Joules per interval -> kWh, or average kW over the interval. +const convert = (joules) => { + const value = (joules || 0) / (power ? seconds * 1000 : 3600000); + return Math.round(value * 1e6) / 1e6; +}; +const sum = (row, keys) => keys.reduce((total, key) => total + (row[key] || 0), 0); + +result = rows + .filter(({ time }) => time + seconds * 1000 > start && time < end) + .map(({ row, time }) => ({ + timestamp: new Date(time).toISOString(), + deviceName: device.name, + deviceKey: device.rawId, + deviceType: prop(device.sourceType), + import: convert(row.imp), + export: convert(row.exp), + generation: convert(row.gep), + diverted: convert(sum(row, ["h1d", "h2d", "h3d"])), + boosted: convert(sum(row, ["h1b", "h2b", "h3b"])), + batteryCharge: convert(row.bcp1), + batteryDischarge: convert(row.bdp1), + unit: power ? "kW" : "kWh", + })); diff --git a/plugins/MyEnergi/v1/dataStreams/scripts/powerFlow.js b/plugins/MyEnergi/v1/dataStreams/scripts/powerFlow.js new file mode 100644 index 00000000..f609beb9 --- /dev/null +++ b/plugins/MyEnergi/v1/dataStreams/scripts/powerFlow.js @@ -0,0 +1,70 @@ +// cgi-jstatus-* returns one element per device family plus a trailing hub element. +// Every device repeats the same site-level grd/gen, so read them from one device, +// preferring a device that has a Grid CT as its measurement is the most direct. +const families = ["zappi", "eddi", "harvi", "libbi"]; + +const devices = []; +for (const element of Array.isArray(data) ? data : []) { + for (const family of families) { + if (Array.isArray(element?.[family])) { + for (const device of element[family]) { + if (device) { + devices.push({ family, device }); + } + } + } + } +} + +const hasGridCt = ({ device }) => + [1, 2, 3, 4, 5, 6].some((n) => device[`ectt${n}`] === "Grid"); +const hasGrid = ({ device }) => typeof device.grd === "number"; +const source = ( + devices.find((d) => hasGridCt(d) && hasGrid(d)) || + devices.find(hasGrid) || + devices[0] +)?.device; + +// The API omits zero values, so a missing div means the device is idle. +const sumDiv = (family) => + devices + .filter((d) => d.family === family) + .reduce((total, { device }) => total + (device.div || 0), 0); + +// dat is DD-MM-YYYY and tim HH:MM:SS (UTC); some firmware pads tim with spaces. +const toIso = ({ dat, tim }) => { + const date = /^(\d{2})-(\d{2})-(\d{4})$/.exec(String(dat || "").trim()); + const time = /^(\d{1,2}):(\d{2}):(\d{2})$/.exec(String(tim || "").replace(/\s/g, "")); + if (!date || !time) { + return null; + } + return `${date[3]}-${date[2]}-${date[1]}T${time[1].padStart(2, "0")}:${time[2]}:${time[3]}Z`; +}; +const lastUpdated = devices + .map(({ device }) => toIso(device)) + .filter(Boolean) + .sort() + .pop() || null; + +const grid = source?.grd || 0; +const generation = Math.max(source?.gen || 0, 0); +const evCharge = sumDiv("zappi"); +const hotWater = sumDiv("eddi"); +const battery = sumDiv("libbi"); +// Battery discharge (negative) counts as supply, so it adds to consumption. +const consumption = grid + generation - battery; + +result = [ + { + gridPower: Math.round(grid), + importPower: Math.round(Math.max(grid, 0)), + exportPower: Math.round(Math.max(-grid, 0)), + generationPower: Math.round(generation), + consumptionPower: Math.round(consumption), + houseLoadPower: Math.round(Math.max(consumption - evCharge - hotWater, 0)), + evChargePower: Math.round(evCharge), + hotWaterPower: Math.round(hotWater), + batteryPower: Math.round(battery), + lastUpdated, + }, +]; diff --git a/plugins/MyEnergi/v1/defaultContent/eddi.dash.json b/plugins/MyEnergi/v1/defaultContent/eddi.dash.json new file mode 100644 index 00000000..c5de8dee --- /dev/null +++ b/plugins/MyEnergi/v1/defaultContent/eddi.dash.json @@ -0,0 +1,698 @@ +{ + "name": "Eddi", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Eddi]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "ca8e8c12-6f2a-5ab6-86c3-15dccfc97f10", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Diverter", + "description": "Current diversion status; amber means no data in the last 15 minutes", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "deviceName", + "type": "nominal" + }, + "status": { + "field": "health", + "type": "nominal" + } + }, + "style": { + "statusGridColumnsPerRow": 1, + "statusGridSecondaryTemplate": "{{{{raw}}}}{{status}}{{{{/raw}}}}" + } + } + } + } + }, + { + "i": "d2ce8c00-d0b2-545d-af48-65726f4988e9", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Heating Now", + "description": "Power going to the immersion heater right now, against a 3.6 kW maximum", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}", + "sql": "SELECT ROUND(\"power\" / 1000, 2) AS \"heatKw\" FROM \"dataset\"" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "gauge", + "encoding": { + "value": { + "field": "heatKw", + "type": "quantitative", + "title": "Heating (kW)" + } + }, + "style": { + "gaugeMin": 0, + "gaugeMax": 3.6, + "labelFormat": "{{{{raw}}}}{{value}} kW{{{{/raw}}}}", + "gaugeBands": [ + { + "to": 1, + "color": "#F0506E" + } + ], + "gaugeLabelText": "Heating now" + } + } + } + } + }, + { + "i": "ba92341b-e875-569a-aec3-10629c1e295a", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Tank Temperatures", + "description": "Readings from the tank probes in °C, against an 80 °C scale; empty when no probe is fitted", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}", + "sql": "SELECT \"tank\", \"temperature\" FROM (SELECT 'Tank 1' AS \"tank\", \"tank1Temperature\" AS \"temperature\" FROM \"dataset\" UNION ALL SELECT 'Tank 2', \"tank2Temperature\" FROM \"dataset\") WHERE \"temperature\" IS NOT NULL ORDER BY \"tank\"" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "temperature", + "type": "quantitative" + }, + "label": { + "field": "tank", + "type": "nominal" + } + }, + "style": { + "ringGridMax": 80, + "ringGridTrackWidth": 8, + "labelFontSize": 13, + "ringGridThresholds": [ + { + "to": 0.5, + "color": "#38BDF8" + }, + { + "to": 0.7, + "color": "#F5A623" + }, + { + "to": 1, + "color": "#F0506E" + } + ] + } + } + } + } + }, + { + "i": "2ec99bce-4a96-530f-812b-f6d147eccc8b", + "x": 3, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Diverter Details", + "description": "Active heater, energy heated today and supply readings", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "activeHeater", + "title": "Active heater", + "renderer": { + "type": "text" + } + }, + { + "field": "energy", + "title": "Heated today", + "renderer": { + "type": "number", + "decimals": 2, + "suffix": " kWh" + } + }, + { + "field": "boosting", + "title": "Boosting", + "renderer": { + "type": "boolean", + "trueLabel": "Yes", + "falseLabel": "No" + } + }, + { + "field": "gridPower", + "title": "Grid now", + "renderer": { + "type": "number", + "suffix": " W" + } + }, + { + "field": "voltage", + "title": "Supply voltage", + "renderer": { + "type": "number", + "decimals": 1, + "suffix": " V" + } + }, + { + "field": "frequency", + "title": "Grid frequency", + "renderer": { + "type": "number", + "decimals": 2, + "suffix": " Hz" + } + }, + { + "field": "lastUpdated", + "title": "Last reading", + "renderer": { + "type": "date", + "relative": true + } + } + ] + }, + "style": { + "richTableTranspose": true, + "richTableCellPadding": 6 + } + } + } + } + }, + { + "i": "baf2d700-cc00-56d3-ab20-22174a65e9de", + "x": 0, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Surplus Captured (30 Days)", + "description": "kWh of solar surplus the Eddi turned into hot water, out of everything diverted plus everything exported to the grid over the last 30 days", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "SELECT 'kWh to hot water' AS \"metric\", ROUND(SUM(\"diverted\"), 1) AS \"captured\", ROUND(SUM(\"diverted\") + SUM(\"export\"), 1) AS \"available\" FROM \"dataset\"" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "captured", + "type": "quantitative" + }, + "max": { + "field": "available", + "type": "quantitative" + }, + "label": { + "field": "metric", + "type": "nominal" + } + }, + "style": { + "ringGridTrackWidth": 8, + "labelFontSize": 13, + "ringGridThresholds": [ + { + "to": 1, + "color": "#F5A623" + } + ] + } + } + } + } + }, + { + "i": "6f3a7696-3108-5cad-b260-e22c36056f7d", + "x": 1, + "y": 2, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CT Clamps", + "description": "Live power through each CT clamp on this Eddi; a grid clamp turns green when exporting", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[ctReadings]}}", + "name": "ctReadings", + "pluginConfigId": "{{configId}}", + "sql": "SELECT \"ctType\", \"power\" FROM \"dataset\" ORDER BY \"ct\" DESC" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "ctType", + "type": "nominal", + "title": "CT" + }, + "y": { + "field": "power", + "type": "quantitative", + "title": "Power (W)" + } + }, + "style": { + "cartesianHorizontal": true, + "showLabels": true, + "labelFormat": "{{{{raw}}}}{{value}} W{{{{/raw}}}}", + "cartesianBarBorderRadius": 3, + "cartesianColorRules": [ + { + "label": "Grid", + "lt": 0, + "color": "#2BB673" + }, + { + "label": "Grid", + "color": "#8E6CEF" + }, + { + "label": "Generation", + "color": "#F5A623" + }, + { + "labelContains": "Load", + "color": "#F0506E" + } + ] + } + } + } + } + }, + { + "i": "3504453d-f20c-5195-bbe1-9cce36e82a2f", + "x": 0, + "y": 4, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Heating vs Surplus", + "description": "Average power diverted to and boosted into the hot water, against power exported to the grid that could have been diverted, in kW", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "power" + }, + "sql": "SELECT \"timestamp\", \"series\", \"kw\" FROM (SELECT \"timestamp\", 'Diverted' AS \"series\", \"diverted\" AS \"kw\" FROM \"dataset\" UNION ALL SELECT \"timestamp\", 'Boosted', \"boosted\" FROM \"dataset\" UNION ALL SELECT \"timestamp\", 'Exported', \"export\" FROM \"dataset\") ORDER BY \"timestamp\"" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal" + }, + "y": { + "field": "kw", + "type": "quantitative", + "title": "kW", + "aggregate": "avg" + }, + "series": { + "field": "series", + "type": "nominal" + } + }, + "style": { + "cartesianAreaGradient": true, + "cartesianSymbol": "none", + "mappedColors": { + "Diverted": "#F5A623", + "Boosted": "#8E6CEF", + "Exported": "#2BB673" + }, + "legendPosition": "bottom", + "cartesianShowYAxisTitle": true, + "cartesianYAxisTitle": "kW" + } + } + } + } + }, + { + "i": "997f88e1-816d-5004-b6a0-123f358b6e69", + "x": 0, + "y": 7, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Hot Water and Spare Solar", + "description": "Each whole day's energy diverted and boosted into the hot water, next to the surplus exported to the grid that day", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH b AS (SELECT DATE_TRUNC('day', MIN(\"timestamp\") + INTERVAL 23 HOUR) AS lo, DATE_TRUNC('day', MAX(\"timestamp\")) AS hi FROM \"dataset\"), x AS (SELECT \"dataset\".* FROM \"dataset\", b WHERE \"timestamp\" >= b.lo AND \"timestamp\" < b.hi) SELECT STRFTIME(DATE_TRUNC('day', \"timestamp\"), '%Y-%m-%dT%H:%M:%SZ') AS \"day\", ROUND(SUM(\"diverted\"), 2) AS \"diverted\", ROUND(SUM(\"boosted\"), 2) AS \"boosted\", ROUND(SUM(\"export\"), 2) AS \"surplusExported\" FROM x GROUP BY 1 ORDER BY 1" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "day", + "type": "temporal" + } + }, + "style": { + "colors": [ + "#F5A623", + "#8E6CEF", + "#2BB673" + ], + "legendPosition": "bottom", + "cartesianBarBorderRadius": 2, + "cartesianShowYAxisTitle": true, + "cartesianYAxisTitle": "kWh" + } + } + } + } + }, + { + "i": "ad6285b9-a767-5a4d-a3af-010d9b284c8c", + "x": 0, + "y": 11, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Boost Schedules", + "description": "Boost timer slots configured on this Eddi", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[boostSchedules]}}", + "name": "boostSchedules", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "target", + "title": "Output", + "renderer": { + "type": "badge", + "defaultColor": "#64748B" + } + }, + { + "field": "startTime", + "title": "Starts", + "renderer": { + "type": "text" + } + }, + { + "field": "days", + "title": "Days", + "renderer": { + "type": "tag-list", + "color": "#F0506E" + } + } + ] + }, + "style": { + "richTableCellPadding": 6 + } + } + } + } + }, + { + "i": "4009c536-c449-5552-a3c5-a07b9d64cddb", + "x": 2, + "y": 11, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Eddi Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Eddis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Eddi]}}" + }, + "variables": [ + "{{variables.[Eddi]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "serial", + "firmware", + "heater1Name", + "heater2Name", + "ctTypes" + ], + "hiddenColumns": [ + "id", + "links", + "type", + "label", + "sourceId", + "sourceType", + "sourceName" + ], + "columnDisplayNames": { + "heater1Name": "Heater 1", + "heater2Name": "Heater 2", + "ctTypes": "CT types" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/MyEnergi/v1/defaultContent/harvi.dash.json b/plugins/MyEnergi/v1/defaultContent/harvi.dash.json new file mode 100644 index 00000000..17cf4184 --- /dev/null +++ b/plugins/MyEnergi/v1/defaultContent/harvi.dash.json @@ -0,0 +1,299 @@ +{ + "name": "Harvi", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": [ + "{{variables.[Harvi]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "4f11d4e2-c34d-5b9d-bdef-44186802709b", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Harvi Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Harvis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Harvi]}}" + }, + "variables": [ + "{{variables.[Harvi]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "serial", + "firmware", + "phases", + "ctTypes" + ], + "hiddenColumns": [ + "id", + "links", + "type", + "label", + "sourceId", + "sourceType", + "sourceName" + ], + "columnDisplayNames": { + "ctTypes": "CT types" + } + } + } + } + } + }, + { + "i": "ad164433-b069-5b84-9565-e0c72a77edfd", + "x": 1, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensor", + "description": "Whether this Harvi has reported in the last 15 minutes; a Harvi reports no other status", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Harvis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Harvi]}}" + }, + "variables": [ + "{{variables.[Harvi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "deviceName", + "type": "nominal" + }, + "status": { + "field": "health", + "type": "nominal" + } + }, + "style": { + "statusGridColumnsPerRow": 1, + "statusGridSecondaryTemplate": "{{{{raw}}}}{{status}}{{{{/raw}}}}" + } + } + } + } + }, + { + "i": "2d398838-eb93-5888-ae00-f3ff8bce470b", + "x": 2, + "y": 0, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CT Clamps", + "description": "Live power through each CT clamp on this Harvi; a grid clamp turns green when exporting", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[ctReadings]}}", + "name": "ctReadings", + "pluginConfigId": "{{configId}}", + "sql": "SELECT 'CT' || CAST(\"ct\" AS INTEGER) || ' · ' || \"ctType\" AS \"clamp\", \"power\" FROM \"dataset\" ORDER BY \"ct\" DESC" + }, + "scope": { + "scope": "{{scopes.[Harvis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Harvi]}}" + }, + "variables": [ + "{{variables.[Harvi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "clamp", + "type": "nominal", + "title": "CT" + }, + "y": { + "field": "power", + "type": "quantitative", + "title": "Power (W)" + } + }, + "style": { + "cartesianHorizontal": true, + "showLabels": true, + "labelFormat": "{{{{raw}}}}{{value}} W{{{{/raw}}}}", + "cartesianBarBorderRadius": 3, + "cartesianColorRules": [ + { + "labelContains": "Grid", + "lt": 0, + "color": "#2BB673" + }, + { + "labelContains": "Grid", + "color": "#8E6CEF" + }, + { + "labelContains": "Generation", + "color": "#F5A623" + }, + { + "labelContains": "Storage", + "color": "#14B8A6" + }, + { + "labelContains": "Load", + "color": "#38BDF8" + } + ] + } + } + } + } + }, + { + "i": "95de93a9-bee0-5fb7-afe2-2ccd198b1d37", + "x": 0, + "y": 4, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CT Readings", + "description": "Live power, phase and reading time for each CT clamp on this Harvi", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[ctReadings]}}", + "name": "ctReadings", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Harvis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Harvi]}}" + }, + "variables": [ + "{{variables.[Harvi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "ct", + "title": "CT", + "renderer": { + "type": "number" + }, + "width": 50 + }, + { + "field": "ctType", + "title": "Measures", + "renderer": { + "type": "badge", + "colors": { + "Grid": "#8E6CEF", + "Generation": "#F5A623", + "*Load*": "#4F7CFF", + "*Storage*": "#14B8A6" + }, + "defaultColor": "#64748B" + } + }, + { + "field": "power", + "title": "Power", + "renderer": { + "type": "bar", + "color": "#38BDF8" + } + }, + { + "field": "phase", + "title": "Phase", + "renderer": { + "type": "number" + } + }, + { + "field": "lastUpdated", + "title": "Last reading", + "renderer": { + "type": "date", + "relative": true + } + } + ] + }, + "style": { + "richTableSort": { + "field": "ct", + "direction": "asc" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/MyEnergi/v1/defaultContent/libbi.dash.json b/plugins/MyEnergi/v1/defaultContent/libbi.dash.json new file mode 100644 index 00000000..2fd9e94d --- /dev/null +++ b/plugins/MyEnergi/v1/defaultContent/libbi.dash.json @@ -0,0 +1,540 @@ +{ + "name": "Libbi", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Libbi]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "024f0736-1dba-5c28-84d8-3a86ee2b0eaa", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Libbi Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Libbis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Libbi]}}" + }, + "variables": [ + "{{variables.[Libbi]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "serial", + "firmware", + "batteryCapacity", + "inverterSize", + "ctTypes" + ], + "hiddenColumns": [ + "id", + "links", + "type", + "label", + "sourceId", + "sourceType", + "sourceName" + ], + "columnDisplayNames": { + "batteryCapacity": "Battery capacity", + "inverterSize": "Inverter size", + "ctTypes": "CT types" + } + } + } + } + } + }, + { + "i": "b1020006-f4a6-53f7-89ef-fe0303559fe6", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Battery", + "description": "Current battery status and mode; red means a fault, amber means no data in the last 15 minutes", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Libbis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Libbi]}}" + }, + "variables": [ + "{{variables.[Libbi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "deviceName", + "type": "nominal" + }, + "status": { + "field": "health", + "type": "nominal" + } + }, + "style": { + "statusGridColumnsPerRow": 1, + "statusGridSecondaryTemplate": "{{{{raw}}}}{{status}} · {{mode}}{{{{/raw}}}}" + } + } + } + } + }, + { + "i": "3f3800bc-90fb-5a14-ab4e-8f2253745c16", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Charge Level", + "description": "Current state of charge", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}", + "sql": "SELECT CAST(\"batteryLevel\" AS DOUBLE) AS \"level\" FROM \"dataset\"" + }, + "scope": { + "scope": "{{scopes.[Libbis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Libbi]}}" + }, + "variables": [ + "{{variables.[Libbi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "gauge", + "encoding": { + "value": { + "field": "level", + "type": "quantitative", + "title": "Charge level (%)" + } + }, + "style": { + "gaugeMin": 0, + "gaugeMax": 100, + "labelFormat": "{{{{raw}}}}{{value}}%{{{{/raw}}}}", + "gaugeBands": [ + { + "to": 0.2, + "color": "#F0506E" + }, + { + "to": 0.5, + "color": "#F5A623" + }, + { + "to": 1, + "color": "#14B8A6" + } + ], + "gaugeLabelText": "State of charge" + } + } + } + } + }, + { + "i": "6ff62c08-4966-5c9d-8d35-fc3a741f3455", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Battery Power", + "description": "Power charging (right of zero) or discharging (left of zero) the battery right now, in kW", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}", + "sql": "SELECT CASE WHEN v > 0.05 THEN 'Charging' WHEN v < -0.05 THEN 'Discharging' ELSE 'Idle' END AS \"direction\", v AS \"batteryKw\" FROM (SELECT ROUND(CAST(\"power\" AS DOUBLE) / 1000, 2) AS v FROM \"dataset\")" + }, + "scope": { + "scope": "{{scopes.[Libbis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Libbi]}}" + }, + "variables": [ + "{{variables.[Libbi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "direction", + "type": "nominal" + }, + "y": { + "field": "batteryKw", + "type": "quantitative", + "title": "kW" + } + }, + "style": { + "cartesianHorizontal": true, + "cartesianYAxisMin": -5, + "cartesianYAxisMax": 5, + "showLabels": true, + "labelFormat": "{{{{raw}}}}{{value}} kW{{{{/raw}}}}", + "labelFontSize": 16, + "cartesianBarBorderRadius": 4, + "cartesianBarWidth": 36, + "cartesianColorRules": [ + { + "lt": 0, + "color": "#38BDF8" + }, + { + "gte": 0, + "color": "#14B8A6" + } + ], + "cartesianShowVerticalGridLines": true + } + } + } + } + }, + { + "i": "e332290e-3517-5f69-87a8-2bbeb37b73dc", + "x": 1, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Energy Returned (30 Days)", + "description": "kWh discharged out of kWh charged over the last 30 days; the gap is conversion losses plus any change in stored charge", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "SELECT 'kWh out of kWh in' AS \"metric\", ROUND(SUM(\"batteryDischarge\"), 1) AS \"discharged\", ROUND(SUM(\"batteryCharge\"), 1) AS \"charged\" FROM \"dataset\"" + }, + "scope": { + "scope": "{{scopes.[Libbis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Libbi]}}" + }, + "variables": [ + "{{variables.[Libbi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "discharged", + "type": "quantitative" + }, + "max": { + "field": "charged", + "type": "quantitative" + }, + "label": { + "field": "metric", + "type": "nominal" + } + }, + "style": { + "ringGridTrackWidth": 8, + "labelFontSize": 13, + "ringGridThresholds": [ + { + "to": 1, + "color": "#14B8A6" + } + ] + } + } + } + } + }, + { + "i": "997899ba-70af-54cf-99bb-79acf915d2ff", + "x": 2, + "y": 2, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CT Clamps", + "description": "Live power through each CT clamp on this Libbi; the grid clamp turns green when exporting", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[ctReadings]}}", + "name": "ctReadings", + "pluginConfigId": "{{configId}}", + "sql": "SELECT \"ctType\", \"power\" FROM \"dataset\" ORDER BY \"ct\" DESC" + }, + "scope": { + "scope": "{{scopes.[Libbis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Libbi]}}" + }, + "variables": [ + "{{variables.[Libbi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "ctType", + "type": "nominal", + "title": "CT" + }, + "y": { + "field": "power", + "type": "quantitative", + "title": "Power (W)" + } + }, + "style": { + "cartesianHorizontal": true, + "showLabels": true, + "labelFormat": "{{{{raw}}}}{{value}} W{{{{/raw}}}}", + "cartesianBarBorderRadius": 3, + "cartesianColorRules": [ + { + "label": "Grid", + "lt": 0, + "color": "#2BB673" + }, + { + "label": "Grid", + "color": "#8E6CEF" + }, + { + "label": "Generation", + "color": "#F5A623" + }, + { + "labelContains": "Storage", + "color": "#14B8A6" + }, + { + "labelContains": "Load", + "color": "#38BDF8" + } + ] + } + } + } + } + }, + { + "i": "acfeb0f2-7838-5f12-b7a9-105c1f1a8914", + "x": 0, + "y": 4, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Charging and Discharging", + "description": "Average battery charging (above the line) and discharging (below) in kW, with solar generation for context", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "power" + }, + "sql": "SELECT \"timestamp\", \"series\", \"kw\" FROM (SELECT \"timestamp\", 'Charging' AS \"series\", \"batteryCharge\" AS \"kw\" FROM \"dataset\" UNION ALL SELECT \"timestamp\", 'Discharging', -\"batteryDischarge\" FROM \"dataset\" UNION ALL SELECT \"timestamp\", 'Solar generation', \"generation\" FROM \"dataset\") ORDER BY \"timestamp\"" + }, + "scope": { + "scope": "{{scopes.[Libbis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Libbi]}}" + }, + "variables": [ + "{{variables.[Libbi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal" + }, + "y": { + "field": "kw", + "type": "quantitative", + "title": "kW", + "aggregate": "avg" + }, + "series": { + "field": "series", + "type": "nominal" + } + }, + "style": { + "cartesianAreaGradient": true, + "cartesianSymbol": "none", + "mappedColors": { + "Charging": "#14B8A6", + "Discharging": "#38BDF8", + "Solar generation": "#F5A623" + }, + "legendPosition": "bottom", + "cartesianShowYAxisTitle": true, + "cartesianYAxisTitle": "kW" + } + } + } + } + }, + { + "i": "5bddc9ea-2119-59b2-b576-85967a865684", + "x": 0, + "y": 7, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Charge and Discharge", + "description": "Energy charged into (above the line) and discharged from (below) the battery on each of the last 29 whole days", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH b AS (SELECT DATE_TRUNC('day', MIN(\"timestamp\") + INTERVAL 23 HOUR) AS lo, DATE_TRUNC('day', MAX(\"timestamp\")) AS hi FROM \"dataset\"), x AS (SELECT \"dataset\".* FROM \"dataset\", b WHERE \"timestamp\" >= b.lo AND \"timestamp\" < b.hi) SELECT STRFTIME(DATE_TRUNC('day', \"timestamp\"), '%Y-%m-%dT%H:%M:%SZ') AS \"day\", ROUND(SUM(\"batteryCharge\"), 2) AS \"charged\", ROUND(-SUM(\"batteryDischarge\"), 2) AS \"discharged\" FROM x GROUP BY 1 ORDER BY 1" + }, + "scope": { + "scope": "{{scopes.[Libbis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Libbi]}}" + }, + "variables": [ + "{{variables.[Libbi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "day", + "type": "temporal" + } + }, + "style": { + "cartesianStack": "on", + "colors": [ + "#14B8A6", + "#38BDF8" + ], + "legendPosition": "bottom", + "cartesianBarBorderRadius": 2, + "cartesianShowYAxisTitle": true, + "cartesianYAxisTitle": "kWh" + } + } + } + } + } + ] + } +} diff --git a/plugins/MyEnergi/v1/defaultContent/manifest.json b/plugins/MyEnergi/v1/defaultContent/manifest.json new file mode 100644 index 00000000..5dda804f --- /dev/null +++ b/plugins/MyEnergi/v1/defaultContent/manifest.json @@ -0,0 +1,24 @@ +{ + "items": [ + { + "name": "overview", + "type": "dashboard" + }, + { + "name": "zappi", + "type": "dashboard" + }, + { + "name": "eddi", + "type": "dashboard" + }, + { + "name": "libbi", + "type": "dashboard" + }, + { + "name": "harvi", + "type": "dashboard" + } + ] +} diff --git a/plugins/MyEnergi/v1/defaultContent/overview.dash.json b/plugins/MyEnergi/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..1666753b --- /dev/null +++ b/plugins/MyEnergi/v1/defaultContent/overview.dash.json @@ -0,0 +1,488 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last30days", + "variables": [], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "96530d4c-95e5-56ac-b749-b9f5fc5ae232", + "x": 0, + "y": 0, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Live Energy Flow", + "description": "Where power is coming from and going to right now, in watts. Solar covers the home first; anything left over is exported.", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[powerFlow]}}", + "name": "powerFlow", + "pluginConfigId": "{{configId}}", + "sql": "WITH f AS (SELECT * FROM \"dataset\") SELECT \"source\", \"target\", \"power\" FROM (SELECT 'Solar' AS \"source\", 'Home' AS \"target\", GREATEST(\"generationPower\" - \"exportPower\", 0) AS \"power\" FROM f UNION ALL SELECT 'Solar', 'Grid export', LEAST(\"exportPower\", \"generationPower\") FROM f UNION ALL SELECT 'Grid', 'Home', \"importPower\" FROM f UNION ALL SELECT 'Battery', 'Home', GREATEST(-\"batteryPower\", 0) FROM f UNION ALL SELECT 'Home', 'EV charging', \"evChargePower\" FROM f UNION ALL SELECT 'Home', 'Hot water', \"hotWaterPower\" FROM f UNION ALL SELECT 'Home', 'Battery charging', GREATEST(\"batteryPower\", 0) FROM f UNION ALL SELECT 'Home', 'Household', \"houseLoadPower\" FROM f) WHERE \"power\" > 0" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "sankey", + "encoding": { + "source": { + "field": "source", + "type": "nominal" + }, + "target": { + "field": "target", + "type": "nominal" + }, + "value": { + "field": "power", + "type": "quantitative", + "title": "Power (W)" + } + }, + "style": { + "labelFormat": "{{{{raw}}}}{{name}} · {{value}} W{{{{/raw}}}}", + "mappedColors": { + "Solar": "#F5A623", + "Grid": "#8E6CEF", + "Battery": "#14B8A6", + "Home": "#64748B", + "Household": "#38BDF8", + "EV charging": "#4F7CFF", + "Hot water": "#F0506E", + "Battery charging": "#14B8A6", + "Grid export": "#2BB673" + } + } + } + } + } + }, + { + "i": "7b02467d-ba5b-5174-81c8-c032f99ccd96", + "x": 3, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "Live state of every Zappi, Eddi, Harvi and Libbi; amber means no data in the last 15 minutes, red means a fault", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "deviceName", + "type": "nominal" + }, + "status": { + "field": "health", + "type": "nominal" + } + }, + "style": { + "statusGridColumnsPerRow": 1, + "statusGridSecondaryTemplate": "{{{{raw}}}}{{status}}{{{{/raw}}}}", + "statusGridOrderByField": "deviceType", + "statusGridOrderDirection": "asc" + } + } + } + } + }, + { + "i": "1d1cf38d-b8b0-5e5e-a47a-a8c8d0276df1", + "x": 0, + "y": 4, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Where Your Energy Went", + "description": "Energy in kWh over the dashboard timeframe, from solar, grid and battery to the house, EV, hot water and export", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH s AS (SELECT \"timestamp\", MAX(\"import\") AS imp, MAX(\"export\") AS exp, MAX(\"generation\") AS gen, SUM(CASE WHEN \"deviceType\" = 'Zappi' THEN \"diverted\" + \"boosted\" ELSE 0 END) AS ev, SUM(CASE WHEN \"deviceType\" = 'Eddi' THEN \"diverted\" + \"boosted\" ELSE 0 END) AS hw, SUM(CASE WHEN \"deviceType\" = 'Libbi' THEN \"batteryCharge\" ELSE 0 END) AS bc, SUM(CASE WHEN \"deviceType\" = 'Libbi' THEN \"batteryDischarge\" ELSE 0 END) AS bd FROM \"dataset\" GROUP BY \"timestamp\"), t AS (SELECT SUM(imp) AS imp, SUM(exp) AS exp, SUM(gen) AS gen, SUM(ev) AS ev, SUM(hw) AS hw, SUM(bc) AS bc, SUM(bd) AS bd FROM s) SELECT \"source\", \"target\", ROUND(\"energy\", 1) AS \"energy\" FROM (SELECT 'Solar' AS \"source\", 'Home' AS \"target\", GREATEST(gen - exp, 0) AS \"energy\" FROM t UNION ALL SELECT 'Solar', 'Grid export', LEAST(exp, gen) FROM t UNION ALL SELECT 'Grid', 'Home', imp FROM t UNION ALL SELECT 'Battery', 'Home', bd FROM t UNION ALL SELECT 'Home', 'EV charging', ev FROM t UNION ALL SELECT 'Home', 'Hot water', hw FROM t UNION ALL SELECT 'Home', 'Battery charging', bc FROM t UNION ALL SELECT 'Home', 'Household', GREATEST(imp + gen - exp + bd - bc - ev - hw, 0) FROM t) WHERE \"energy\" > 0.05" + }, + "scope": { + "scope": "{{scopes.[Devices with Energy History]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "sankey", + "encoding": { + "source": { + "field": "source", + "type": "nominal" + }, + "target": { + "field": "target", + "type": "nominal" + }, + "value": { + "field": "energy", + "type": "quantitative", + "title": "Energy (kWh)" + } + }, + "style": { + "labelFormat": "{{{{raw}}}}{{name}} · {{value}} kWh{{{{/raw}}}}", + "mappedColors": { + "Solar": "#F5A623", + "Grid": "#8E6CEF", + "Battery": "#14B8A6", + "Home": "#64748B", + "Household": "#38BDF8", + "EV charging": "#4F7CFF", + "Hot water": "#F0506E", + "Battery charging": "#14B8A6", + "Grid export": "#2BB673" + } + } + } + } + } + }, + { + "i": "b6c68479-2f8b-59e1-bfb1-5fe33e59ee2d", + "x": 2, + "y": 4, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Energy Independence", + "description": "Over the dashboard timeframe. Self-powered: share of home use not drawn from the grid. Solar kept: share of generation used on site rather than exported. EV diverted: share of EV charging from diverted surplus rather than boost.", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH s AS (SELECT \"timestamp\", MAX(\"import\") AS imp, MAX(\"export\") AS exp, MAX(\"generation\") AS gen, SUM(CASE WHEN \"deviceType\" = 'Zappi' THEN \"diverted\" ELSE 0 END) AS evSolar, SUM(CASE WHEN \"deviceType\" = 'Zappi' THEN \"diverted\" + \"boosted\" ELSE 0 END) AS ev, SUM(CASE WHEN \"deviceType\" = 'Libbi' THEN \"batteryCharge\" ELSE 0 END) AS bc, SUM(CASE WHEN \"deviceType\" = 'Libbi' THEN \"batteryDischarge\" ELSE 0 END) AS bd FROM \"dataset\" GROUP BY \"timestamp\"), t AS (SELECT SUM(imp) AS imp, SUM(exp) AS exp, SUM(gen) AS gen, SUM(evSolar) AS evSolar, SUM(ev) AS ev, SUM(imp + gen - exp + bd - bc) AS use FROM s) SELECT \"metric\", ROUND(\"percent\", 0) AS \"percent\" FROM (SELECT 1 AS o, 'Self-powered' AS \"metric\", 100 * (use - imp) / NULLIF(use, 0) AS \"percent\" FROM t UNION ALL SELECT 2, 'Solar kept', 100 * (gen - exp) / NULLIF(gen, 0) FROM t UNION ALL SELECT 3, 'EV diverted', 100 * evSolar / NULLIF(ev, 0) FROM t) WHERE \"percent\" IS NOT NULL ORDER BY o" + }, + "scope": { + "scope": "{{scopes.[Devices with Energy History]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "ringGrid", + "encoding": { + "value": { + "field": "percent", + "type": "quantitative" + }, + "label": { + "field": "metric", + "type": "nominal" + } + }, + "style": { + "ringGridMax": 100, + "ringGridTrackWidth": 8, + "labelFontSize": 14, + "ringGridThresholds": [ + { + "to": 1, + "color": "#2BB673" + } + ] + } + } + } + } + }, + { + "i": "aa88e91e-7b89-5a26-9df0-0724558924b5", + "x": 0, + "y": 8, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Power", + "description": "Minute-by-minute solar generation, total home use, EV charging and hot water heating over the last 24 hours, in kW", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last24hours", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "power" + }, + "sql": "WITH s AS (SELECT \"timestamp\", MAX(\"generation\") AS solar, GREATEST(MAX(\"import\") + MAX(\"generation\") - MAX(\"export\") + SUM(CASE WHEN \"deviceType\" = 'Libbi' THEN \"batteryDischarge\" - \"batteryCharge\" ELSE 0 END), 0) AS home, SUM(CASE WHEN \"deviceType\" = 'Zappi' THEN \"diverted\" + \"boosted\" ELSE 0 END) AS ev, SUM(CASE WHEN \"deviceType\" = 'Eddi' THEN \"diverted\" + \"boosted\" ELSE 0 END) AS hw, COUNT(*) FILTER (WHERE \"deviceType\" = 'Zappi') AS zappis, COUNT(*) FILTER (WHERE \"deviceType\" = 'Eddi') AS eddis FROM \"dataset\" GROUP BY \"timestamp\") SELECT \"timestamp\", \"series\", ROUND(\"kw\", 3) AS \"kw\" FROM (SELECT \"timestamp\", 'Solar' AS \"series\", solar AS \"kw\" FROM s UNION ALL SELECT \"timestamp\", 'Home use', home FROM s UNION ALL SELECT \"timestamp\", 'EV charging', ev FROM s WHERE zappis > 0 UNION ALL SELECT \"timestamp\", 'Hot water', hw FROM s WHERE eddis > 0) ORDER BY \"timestamp\"" + }, + "scope": { + "scope": "{{scopes.[Devices with Energy History]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal" + }, + "y": { + "field": "kw", + "type": "quantitative", + "title": "kW", + "aggregate": "avg" + }, + "series": { + "field": "series", + "type": "nominal" + } + }, + "style": { + "cartesianAreaGradient": true, + "cartesianSymbol": "none", + "mappedColors": { + "Solar": "#F5A623", + "Home use": "#38BDF8", + "EV charging": "#4F7CFF", + "Hot water": "#F0506E", + "Battery": "#14B8A6" + }, + "legendPosition": "bottom", + "cartesianShowYAxisTitle": true, + "cartesianYAxisTitle": "kW" + } + } + } + } + }, + { + "i": "30cd5981-5104-5fac-bf78-7882a6043539", + "x": 0, + "y": 11, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Energy Balance", + "description": "Each whole day's home supply split into solar used on site and grid import, with export to the grid below the line", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH b AS (SELECT DATE_TRUNC('day', MIN(\"timestamp\") + INTERVAL 23 HOUR) AS lo, DATE_TRUNC('day', MAX(\"timestamp\")) AS hi FROM \"dataset\"), x AS (SELECT \"dataset\".* FROM \"dataset\", b WHERE \"timestamp\" >= b.lo AND \"timestamp\" < b.hi), s AS (SELECT \"timestamp\", MAX(\"import\") AS imp, MAX(\"export\") AS exp, MAX(\"generation\") AS gen FROM x GROUP BY \"timestamp\") SELECT STRFTIME(DATE_TRUNC('day', \"timestamp\"), '%Y-%m-%dT%H:%M:%SZ') AS \"day\", ROUND(SUM(GREATEST(gen - exp, 0)), 2) AS \"solarUsed\", ROUND(SUM(imp), 2) AS \"gridImport\", ROUND(-SUM(exp), 2) AS \"gridExport\" FROM s GROUP BY 1 ORDER BY 1" + }, + "scope": { + "scope": "{{scopes.[Devices with Energy History]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "day", + "type": "temporal" + } + }, + "style": { + "cartesianStack": "on", + "colors": [ + "#F5A623", + "#8E6CEF", + "#2BB673" + ], + "legendPosition": "bottom", + "cartesianBarBorderRadius": 2, + "cartesianShowYAxisTitle": true, + "cartesianYAxisTitle": "kWh" + } + } + } + } + }, + { + "i": "d572b640-6fe9-56b8-88d7-7203c8d60e51", + "x": 0, + "y": 15, + "w": 2, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Grid Import by Hour", + "description": "kWh drawn from the grid in each hour (UTC) of the last 29 whole days; shows when off-peak charging and evening peaks happen", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH b AS (SELECT DATE_TRUNC('day', MIN(\"timestamp\") + INTERVAL 23 HOUR) AS lo, DATE_TRUNC('day', MAX(\"timestamp\")) AS hi FROM \"dataset\"), x AS (SELECT \"dataset\".* FROM \"dataset\", b WHERE \"timestamp\" >= b.lo AND \"timestamp\" < b.hi), s AS (SELECT \"timestamp\", MAX(\"import\") AS v FROM x GROUP BY \"timestamp\"), d AS (SELECT DISTINCT CAST(\"timestamp\" AS DATE) AS d FROM s), h AS (SELECT \"range\" AS h FROM range(24)) SELECT STRFTIME(d.d, '%a %d %b') AS \"day\", LPAD(CAST(h.h AS VARCHAR), 2, '0') || ':00' AS \"hour\", ROUND(COALESCE(s.v, 0), 2) AS \"gridImport\" FROM d CROSS JOIN h LEFT JOIN s ON CAST(s.\"timestamp\" AS DATE) = d.d AND HOUR(s.\"timestamp\") = h.h ORDER BY d.d, h.h" + }, + "scope": { + "scope": "{{scopes.[Devices with Energy History]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "heatmap", + "encoding": { + "x": { + "field": "hour", + "type": "nominal" + }, + "y": { + "field": "day", + "type": "nominal" + }, + "color": { + "field": "gridImport", + "type": "quantitative", + "title": "Grid import (kWh)" + } + }, + "style": { + "colorScheme": "health", + "heatmapCellBorderWidth": 1, + "heatmapCellBorderRadius": 2 + } + } + } + } + }, + { + "i": "c8c5313a-5c5e-5865-856c-6299d8c9470a", + "x": 2, + "y": 15, + "w": 2, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Solar Generation by Hour", + "description": "kWh generated on site in each hour (UTC) of the last 29 whole days", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH b AS (SELECT DATE_TRUNC('day', MIN(\"timestamp\") + INTERVAL 23 HOUR) AS lo, DATE_TRUNC('day', MAX(\"timestamp\")) AS hi FROM \"dataset\"), x AS (SELECT \"dataset\".* FROM \"dataset\", b WHERE \"timestamp\" >= b.lo AND \"timestamp\" < b.hi), s AS (SELECT \"timestamp\", MAX(\"generation\") AS v FROM x GROUP BY \"timestamp\"), d AS (SELECT DISTINCT CAST(\"timestamp\" AS DATE) AS d FROM s), h AS (SELECT \"range\" AS h FROM range(24)) SELECT STRFTIME(d.d, '%a %d %b') AS \"day\", LPAD(CAST(h.h AS VARCHAR), 2, '0') || ':00' AS \"hour\", ROUND(COALESCE(s.v, 0), 2) AS \"solar\" FROM d CROSS JOIN h LEFT JOIN s ON CAST(s.\"timestamp\" AS DATE) = d.d AND HOUR(s.\"timestamp\") = h.h ORDER BY d.d, h.h" + }, + "scope": { + "scope": "{{scopes.[Devices with Energy History]}}", + "workspace": "{{workspaceId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "heatmap", + "encoding": { + "x": { + "field": "hour", + "type": "nominal" + }, + "y": { + "field": "day", + "type": "nominal" + }, + "color": { + "field": "solar", + "type": "quantitative", + "title": "Solar (kWh)" + } + }, + "style": { + "colorScheme": "squaredUpBlue", + "heatmapCellBorderWidth": 1, + "heatmapCellBorderRadius": 2, + "heatmapColorScaleReversed": true + } + } + } + } + } + ] + } +} diff --git a/plugins/MyEnergi/v1/defaultContent/scopes.json b/plugins/MyEnergi/v1/defaultContent/scopes.json new file mode 100644 index 00000000..4779af16 --- /dev/null +++ b/plugins/MyEnergi/v1/defaultContent/scopes.json @@ -0,0 +1,97 @@ +[ + { + "name": "Devices", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Zappi", + "Eddi", + "Harvi", + "Libbi" + ] + } + } + }, + { + "name": "Devices with Energy History", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Zappi", + "Eddi", + "Libbi" + ] + } + } + }, + { + "name": "Zappis", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Zappi" + ] + } + }, + "variable": { + "name": "Zappi", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Eddis", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Eddi" + ] + } + }, + "variable": { + "name": "Eddi", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Libbis", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Libbi" + ] + } + }, + "variable": { + "name": "Libbi", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Harvis", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Harvi" + ] + } + }, + "variable": { + "name": "Harvi", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/MyEnergi/v1/defaultContent/zappi.dash.json b/plugins/MyEnergi/v1/defaultContent/zappi.dash.json new file mode 100644 index 00000000..490c64ec --- /dev/null +++ b/plugins/MyEnergi/v1/defaultContent/zappi.dash.json @@ -0,0 +1,763 @@ +{ + "name": "Zappi", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Zappi]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "432cd23f-143c-523b-bd5d-a373b70a1f91", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Charger", + "description": "Current charging status; amber means no data in the last 15 minutes, red means a fault", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "deviceName", + "type": "nominal" + }, + "status": { + "field": "health", + "type": "nominal" + } + }, + "style": { + "statusGridColumnsPerRow": 1, + "statusGridSecondaryTemplate": "{{{{raw}}}}{{status}}{{{{/raw}}}}" + } + } + } + } + }, + { + "i": "f0f63bf0-b971-53cd-802f-355e7482e722", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Charging Now", + "description": "Power going to the EV right now, against a 7.4 kW single-phase maximum", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}", + "sql": "SELECT ROUND(\"power\" / 1000, 2) AS \"chargeKw\" FROM \"dataset\"" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "gauge", + "encoding": { + "value": { + "field": "chargeKw", + "type": "quantitative", + "title": "Charging (kW)" + } + }, + "style": { + "gaugeMin": 0, + "gaugeMax": 7.4, + "labelFormat": "{{{{raw}}}}{{value}} kW{{{{/raw}}}}", + "gaugeBands": [ + { + "to": 1, + "color": "#4F7CFF" + } + ], + "gaugeLabelText": "Charging now" + } + } + } + } + }, + { + "i": "b55c7387-7999-5c37-9749-2f22f53b10ab", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "This Session", + "description": "Energy added in the current or most recent charging session, with a rough range estimate at 3.5 miles per kWh", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}", + "sql": "SELECT ROUND(\"energy\", 1) AS \"energy\", ROUND(\"energy\" * 3.5) AS \"estMiles\" FROM \"dataset\"" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "energy", + "type": "quantitative", + "title": "kWh this session" + }, + { + "field": "estMiles", + "type": "quantitative", + "title": "≈ miles added" + } + ] + }, + "style": { + "scalarColumns": 1 + } + } + } + } + }, + { + "i": "d5f5abda-b4af-58dd-9bb5-c0f2f19b0128", + "x": 3, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Charger Details", + "description": "Charging mode, plug state, minimum green level and supply readings", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[deviceStatus]}}", + "name": "deviceStatus", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "mode", + "title": "Mode", + "renderer": { + "type": "badge", + "colors": { + "Fast": "#F0506E", + "Eco*": "#2BB673", + "Stop": "#64748B" + } + } + }, + { + "field": "plugStatus", + "title": "Plug", + "renderer": { + "type": "badge", + "colors": { + "Charging": "#4F7CFF", + "Fault": "#F0506E", + "EV disconnected": "#64748B", + "*": "#8E6CEF" + } + } + }, + { + "field": "minGreenLevel", + "title": "Min green level", + "renderer": { + "type": "progress", + "max": 100 + } + }, + { + "field": "boosting", + "title": "Boosting", + "renderer": { + "type": "boolean", + "trueLabel": "Yes", + "falseLabel": "No" + } + }, + { + "field": "voltage", + "title": "Supply voltage", + "renderer": { + "type": "number", + "decimals": 1, + "suffix": " V" + } + }, + { + "field": "frequency", + "title": "Grid frequency", + "renderer": { + "type": "number", + "decimals": 2, + "suffix": " Hz" + } + }, + { + "field": "lastUpdated", + "title": "Last reading", + "renderer": { + "type": "date", + "relative": true + } + } + ] + }, + "style": { + "richTableTranspose": true, + "richTableCellPadding": 6 + } + } + } + } + }, + { + "i": "e8f83352-27ee-50d6-baad-0a7bb0e4c283", + "x": 0, + "y": 2, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CT Clamps", + "description": "Live power through each CT clamp on this Zappi; the grid clamp turns green when exporting", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[ctReadings]}}", + "name": "ctReadings", + "pluginConfigId": "{{configId}}", + "sql": "SELECT \"ctType\", \"power\" FROM \"dataset\" ORDER BY \"ct\" DESC" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "ctType", + "type": "nominal", + "title": "CT" + }, + "y": { + "field": "power", + "type": "quantitative", + "title": "Power (W)" + } + }, + "style": { + "cartesianHorizontal": true, + "showLabels": true, + "labelFormat": "{{{{raw}}}}{{value}} W{{{{/raw}}}}", + "cartesianBarBorderRadius": 3, + "cartesianColorRules": [ + { + "label": "Grid", + "lt": 0, + "color": "#2BB673" + }, + { + "label": "Grid", + "color": "#8E6CEF" + }, + { + "label": "Generation", + "color": "#F5A623" + }, + { + "labelContains": "Load", + "color": "#4F7CFF" + } + ] + } + } + } + } + }, + { + "i": "3a90d010-2b54-5355-a516-923386204df4", + "x": 2, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Charge Sources (30 Days)", + "description": "EV energy over the last 30 days: diverted (Eco and Eco+ charging, mostly surplus solar) against boosted (timed or manual grid boost)", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "SELECT \"source\", \"energy\" FROM (SELECT 'Diverted' AS \"source\", ROUND(SUM(\"diverted\"), 1) AS \"energy\" FROM \"dataset\" UNION ALL SELECT 'Boosted', ROUND(SUM(\"boosted\"), 1) FROM \"dataset\") WHERE \"energy\" > 0" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "pie", + "encoding": { + "label": { + "field": "source", + "type": "nominal" + }, + "value": { + "field": "energy", + "type": "quantitative", + "title": "kWh" + } + }, + "style": { + "pieInnerRadius": 60, + "pieDonutCenterValue": "total", + "labelFontSize": 16, + "pieLabels": "percent", + "mappedColors": { + "Diverted": "#F5A623", + "Boosted": "#8E6CEF" + }, + "pieLegendStyle": "table" + } + } + } + } + }, + { + "i": "e721a9dd-5fcf-5e66-958d-8d1b6b505546", + "x": 0, + "y": 4, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Charging Power", + "description": "Average diverted and boosted charging power in kW, with the site's solar generation for context", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "power" + }, + "sql": "SELECT \"timestamp\", \"series\", \"kw\" FROM (SELECT \"timestamp\", 'Diverted' AS \"series\", \"diverted\" AS \"kw\" FROM \"dataset\" UNION ALL SELECT \"timestamp\", 'Boosted', \"boosted\" FROM \"dataset\" UNION ALL SELECT \"timestamp\", 'Solar generation', \"generation\" FROM \"dataset\") ORDER BY \"timestamp\"" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "line", + "encoding": { + "x": { + "field": "timestamp", + "type": "temporal" + }, + "y": { + "field": "kw", + "type": "quantitative", + "title": "kW", + "aggregate": "avg" + }, + "series": { + "field": "series", + "type": "nominal" + } + }, + "style": { + "cartesianAreaGradient": true, + "cartesianSymbol": "none", + "mappedColors": { + "Diverted": "#F5A623", + "Boosted": "#8E6CEF", + "Solar generation": "#94A3B8" + }, + "legendPosition": "bottom", + "cartesianShowYAxisTitle": true, + "cartesianYAxisTitle": "kW" + } + } + } + } + }, + { + "i": "8fbbe1c4-3496-5c41-980a-87c8837ba49f", + "x": 0, + "y": 7, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Daily Charging", + "description": "Energy charged into the EV on each of the last 29 whole days, split into diverted and boosted", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH b AS (SELECT DATE_TRUNC('day', MIN(\"timestamp\") + INTERVAL 23 HOUR) AS lo, DATE_TRUNC('day', MAX(\"timestamp\")) AS hi FROM \"dataset\"), x AS (SELECT \"dataset\".* FROM \"dataset\", b WHERE \"timestamp\" >= b.lo AND \"timestamp\" < b.hi) SELECT STRFTIME(DATE_TRUNC('day', \"timestamp\"), '%Y-%m-%dT%H:%M:%SZ') AS \"day\", ROUND(SUM(\"diverted\"), 2) AS \"diverted\", ROUND(SUM(\"boosted\"), 2) AS \"boosted\" FROM x GROUP BY 1 ORDER BY 1" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "day", + "type": "temporal" + } + }, + "style": { + "cartesianStack": "on", + "colors": [ + "#F5A623", + "#8E6CEF" + ], + "legendPosition": "bottom", + "cartesianBarBorderRadius": 2, + "cartesianShowYAxisTitle": true, + "cartesianYAxisTitle": "kWh" + } + } + } + } + }, + { + "i": "ef2875b5-f45e-5ed1-8084-3225f693eca9", + "x": 2, + "y": 7, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "When the EV Charges", + "description": "kWh charged in each hour (UTC) of the last 29 whole days; off-peak tariff windows show up as a band", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "last30days", + "dataStream": { + "id": "{{dataStreams.[energyHistory]}}", + "name": "energyHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "measure": "energy" + }, + "sql": "WITH b AS (SELECT DATE_TRUNC('day', MIN(\"timestamp\") + INTERVAL 23 HOUR) AS lo, DATE_TRUNC('day', MAX(\"timestamp\")) AS hi FROM \"dataset\"), x AS (SELECT \"dataset\".* FROM \"dataset\", b WHERE \"timestamp\" >= b.lo AND \"timestamp\" < b.hi), s AS (SELECT \"timestamp\", SUM(\"diverted\" + \"boosted\") AS v FROM x GROUP BY \"timestamp\"), d AS (SELECT DISTINCT CAST(\"timestamp\" AS DATE) AS d FROM s), h AS (SELECT \"range\" AS h FROM range(24)) SELECT STRFTIME(d.d, '%a %d %b') AS \"day\", LPAD(CAST(h.h AS VARCHAR), 2, '0') || ':00' AS \"hour\", ROUND(COALESCE(s.v, 0), 2) AS \"charged\" FROM d CROSS JOIN h LEFT JOIN s ON CAST(s.\"timestamp\" AS DATE) = d.d AND HOUR(s.\"timestamp\") = h.h ORDER BY d.d, h.h" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "heatmap", + "encoding": { + "x": { + "field": "hour", + "type": "nominal" + }, + "y": { + "field": "day", + "type": "nominal" + }, + "color": { + "field": "charged", + "type": "quantitative", + "title": "EV charge (kWh)" + } + }, + "style": { + "colorScheme": "squaredUpBlue", + "heatmapCellBorderWidth": 1, + "heatmapCellBorderRadius": 2, + "heatmapColorScaleReversed": true + } + } + } + } + }, + { + "i": "ae722478-ea0d-5d1a-8f89-15bd5d45138e", + "x": 0, + "y": 11, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Boost Schedules", + "description": "Boost timer slots configured on this Zappi", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[boostSchedules]}}", + "name": "boostSchedules", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "slot", + "title": "Slot", + "renderer": { + "type": "number" + }, + "width": 60 + }, + { + "field": "startTime", + "title": "Starts", + "renderer": { + "type": "text" + } + }, + { + "field": "duration", + "title": "Minutes", + "renderer": { + "type": "number" + } + }, + { + "field": "days", + "title": "Days", + "renderer": { + "type": "tag-list", + "color": "#4F7CFF" + } + } + ] + }, + "style": { + "richTableSort": { + "field": "slot", + "direction": "asc" + } + } + } + } + } + }, + { + "i": "ee5f0238-5097-5286-b065-3bc8f83b0e93", + "x": 2, + "y": 11, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Zappi Properties", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Zappis]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Zappi]}}" + }, + "variables": [ + "{{variables.[Zappi]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "serial", + "firmware", + "phases", + "ctTypes" + ], + "hiddenColumns": [ + "id", + "links", + "type", + "label", + "sourceId", + "sourceType", + "sourceName" + ], + "columnDisplayNames": { + "ctTypes": "CT types" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/MyEnergi/v1/docs/README.md b/plugins/MyEnergi/v1/docs/README.md new file mode 100644 index 00000000..eede240f --- /dev/null +++ b/plugins/MyEnergi/v1/docs/README.md @@ -0,0 +1,66 @@ +Monitor your [myenergi](https://www.myenergi.com) devices in SquaredUp — live power flow, EV charging on the Zappi, hot water diversion on the Eddi, CT readings from the Harvi, and battery state on the Libbi, plus per-minute and hourly energy history — via the myenergi cloud API used by the myenergi app. + +> ⚠️ myenergi does not officially support or document this API. It is the same cloud service the myenergi app uses, and myenergi may change it without notice. + +## Setup + +You will need the **serial number** of your gateway device and a myenergi **API key**. The gateway device is your myenergi hub or, if you have no hub, the Zappi or Eddi connected to your network that acts as one. + +1. Sign in to [myenergi myaccount](https://myaccount.myenergi.com) with the account that registered your devices. +2. Go to **Products**, find the gateway device (shown as **Hub**, or as the Zappi or Eddi acting as one), and note its **serial number**. +3. Open that device's **Advanced** section and click **Generate new API key**. Copy the key straight away — it is only shown once. +4. In SquaredUp, enter the serial number in **Hub serial number** and the key in **API key**. + +> ⚠️ Generating a new API key revokes the previous one. Anything else using the old key, such as Home Assistant, will need the new key too. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ---------------- | -------- | +| **Hub serial number** | The serial of the gateway device. Used as the username for every request. | [myaccount](https://myaccount.myenergi.com) → **Products**. | Yes | +| **API key** | The key generated for the gateway device. Used as the password for every request. | [myaccount](https://myaccount.myenergi.com) → **Products** → gateway device → **Advanced** → **Generate new API key**. | Yes | +| **Server** | The myenergi server to connect to. Leave blank to use `director.myenergi.net`. | Only needed if myenergi support has given you a specific server, e.g. `s18.myenergi.net`. | No | + +On save, the plugin signs in and lists the devices registered to the hub. If that fails, check the serial number belongs to the gateway device and that the API key has not been regenerated since you copied it. + +## What this plugin monitors + +- **Live power flow** — grid import and export, solar generation, total consumption, house load, and the power going to EV charging, hot water and the battery. +- **Device status** — each Zappi, Eddi, Harvi and Libbi with its status, mode, plug state, power, energy, tank temperatures, battery level, voltage and frequency, and whether its data is current. +- **CT clamps** — the live reading and configured type (Grid, Generation, Internal Load and so on) of every current transformer clamp. +- **Energy history** — grid import and export, generation, diverted (surplus) and boosted (grid) energy, and battery charge and discharge, per minute for ranges up to 24 hours and per hour beyond that. +- **Boost schedules** — the boost timers configured on each Zappi and Eddi. + +The out-of-the-box dashboards include an **Overview** of the whole site plus a perspective for each **Zappi**, **Eddi**, **Libbi** and **Harvi**. + +## Data streams + +- **Power Flow** — live power flow for the whole site, as a single row. +- **Device Status** — live status and readings, one row per selected device (any mix of Zappi, Eddi, Harvi and Libbi). +- **CT Readings** — live power on each active CT clamp, one row per clamp on the selected devices. +- **Energy History** — energy or average power over time, one row per selected Zappi, Eddi or Libbi per minute or hour. Use the **Measure** option to switch between energy (kWh) and average power (kW). +- **Boost Schedules** — configured boost timers, one row per timer on the selected Zappi and Eddi devices. + +## What gets indexed + +| Object type | API source | Represents | +| ----------- | ---------- | ---------- | +| **Hub** | `cgi-jstatus-*` | The gateway device the plugin signs in with — a myenergi hub, or the Zappi or Eddi acting as one. | +| **Zappi** | `cgi-jstatus-*` | An EV charger. | +| **Eddi** | `cgi-jstatus-*` | A hot water diverter. | +| **Harvi** | `cgi-jstatus-*` | A wireless CT sensor. | +| **Libbi** | `cgi-jstatus-*` | A home battery. | + +Devices are named by type and serial number, for example **Zappi 12345678**. Device lists refresh every 12 hours, so a newly installed device can take that long to appear. + +## Known limitations + +- **Unofficial API** — myenergi does not document or support this API, and it can change without notice. +- **Read-only** — the plugin never changes modes, boosts, schedules or any other setting on your devices. +- **History range** — energy history is available for up to the last 30 days, this month or last month. Longer ranges aren't offered, because a single request covers at most 31 days. +- **Site totals repeat per device** — every device reports the same grid import, export and generation for the site, so don't sum those columns across devices. Diverted and boosted energy are per device and can be summed. +- **Times are UTC** — the API reports history in UTC, so the hour-by-hour dashboard charts and daily totals run on UTC hours and days, an hour off local time during summer time. +- **Harvi has no history** — the API provides live readings only for Harvi, so it isn't offered in **Energy History**. +- **Harvi and Libbi are untested** — their streams and dashboards follow the community documentation of the API but have not been verified against real devices. +- **Eddi boost timers** — the Eddi reports some timers without a duration and under timer groups whose meaning isn't documented, so they are labelled by output number. +- **One hub per data source** — add a separate data source for each myenergi hub. Features that need a myenergi account sign-in, such as Libbi grid charging targets, tariffs and vehicles, aren't covered. diff --git a/plugins/MyEnergi/v1/icon.svg b/plugins/MyEnergi/v1/icon.svg new file mode 100644 index 00000000..a4504ff6 --- /dev/null +++ b/plugins/MyEnergi/v1/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/MyEnergi/v1/indexDefinitions/default.json b/plugins/MyEnergi/v1/indexDefinitions/default.json new file mode 100644 index 00000000..21bcdceb --- /dev/null +++ b/plugins/MyEnergi/v1/indexDefinitions/default.json @@ -0,0 +1,27 @@ +{ + "steps": [ + { + "name": "devices", + "dataStream": { + "name": "devices" + }, + "timeframe": "none", + "objectMapping": { + "id": "deviceKey", + "name": "name", + "type": "sourceType", + "properties": [ + "serial", + "firmware", + "phases", + "ctTypes", + "heater1Name", + "heater2Name", + "batteryCapacity", + "inverterSize", + "server" + ] + } + } + ] +} diff --git a/plugins/MyEnergi/v1/metadata.json b/plugins/MyEnergi/v1/metadata.json new file mode 100644 index 00000000..a70a144f --- /dev/null +++ b/plugins/MyEnergi/v1/metadata.json @@ -0,0 +1,52 @@ +{ + "name": "myenergi", + "displayName": "myenergi", + "version": "1.0.0", + "author": { + "name": "@adamkinniburgh", + "type": "community" + }, + "description": "Monitor solar diversion, EV charging, hot water heating and home battery storage across your myenergi Zappi, Eddi, Harvi and Libbi devices.", + "category": "Monitoring", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "myenergi", + "zappi", + "eddi", + "harvi", + "libbi", + "ev charger", + "solar", + "solar diverter", + "home battery", + "energy" + ], + "objectTypes": ["Hub", "Zappi", "Eddi", "Harvi", "Libbi"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/MyEnergi/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/MyEnergi/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://{{ (server || 'director.myenergi.net').trim().replace(/^https?:[/][/]/i, '').replace(/[/]+$/, '') }}", + "authMode": "digest", + "digestAuthUsername": "{{ hubSerial.trim() }}", + "digestAuthPassword": "{{ apiKey.trim() }}", + "headers": [], + "queryArgs": [] + } + } +} diff --git a/plugins/MyEnergi/v1/ui.json b/plugins/MyEnergi/v1/ui.json new file mode 100644 index 00000000..ae7fce19 --- /dev/null +++ b/plugins/MyEnergi/v1/ui.json @@ -0,0 +1,33 @@ +[ + { + "type": "text", + "name": "hubSerial", + "label": "Hub serial number", + "placeholder": "e.g. 12345678", + "help": "Use the serial of the gateway device (the hub, or the Zappi or Eddi acting as one) shown under **Products** in [myenergi myaccount](https://myaccount.myenergi.com).", + "validation": { + "required": true, + "pattern": { + "value": "^\\s*[0-9]+\\s*$", + "message": "Enter the serial number digits only, e.g. 12345678" + } + } + }, + { + "type": "password", + "name": "apiKey", + "label": "API key", + "placeholder": "Enter the myenergi API key", + "help": "Generate a key in [myenergi myaccount](https://myaccount.myenergi.com) under **Products → Gateway Device → Advanced**. Generating a new key revokes the previous one.", + "validation": { + "required": true + } + }, + { + "type": "text", + "name": "server", + "label": "Server", + "placeholder": "director.myenergi.net", + "help": "Leave blank unless myenergi support has given you a specific server, e.g. s18.myenergi.net." + } +] From 489105aeeff1b7c80dcc494626366e390a51df3f Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Tue, 27 Oct 2026 18:17:14 +0000 Subject: [PATCH 2/4] fix(review): Leave energy history values unrounded Display precision comes from the columns' decimalPlaces, so the script no longer rounds kWh and kW values itself. Co-Authored-By: Claude Opus 5.5 --- plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js b/plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js index f5ec4c79..4e9755d4 100644 --- a/plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js +++ b/plugins/MyEnergi/v1/dataStreams/scripts/energyHistory.js @@ -16,10 +16,7 @@ const rows = history.map((row) => ({ row, time: Date.UTC(row.yr, row.mon - 1, ro const seconds = end - start <= 86400000 ? 60 : 3600; // Joules per interval -> kWh, or average kW over the interval. -const convert = (joules) => { - const value = (joules || 0) / (power ? seconds * 1000 : 3600000); - return Math.round(value * 1e6) / 1e6; -}; +const convert = (joules) => (joules || 0) / (power ? seconds * 1000 : 3600000); const sum = (row, keys) => keys.reduce((total, key) => total + (row[key] || 0), 0); result = rows From 936a7b516f7d3284dd42864516e5eb0357889a50 Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Wed, 28 Oct 2026 13:35:19 +0000 Subject: [PATCH 3/4] Update plugins/MyEnergi/v1/metadata.json Co-authored-by: Dave Clarke --- plugins/MyEnergi/v1/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/MyEnergi/v1/metadata.json b/plugins/MyEnergi/v1/metadata.json index a70a144f..8bd4961c 100644 --- a/plugins/MyEnergi/v1/metadata.json +++ b/plugins/MyEnergi/v1/metadata.json @@ -7,7 +7,7 @@ "type": "community" }, "description": "Monitor solar diversion, EV charging, hot water heating and home battery storage across your myenergi Zappi, Eddi, Harvi and Libbi devices.", - "category": "Monitoring", + "category": "Fun", "type": "hybrid", "schemaVersion": "2.1", "importNotSupported": false, From a1bc394e8ccbfd0714c5e6cb4be6fc4e53d0c8ae Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Wed, 28 Oct 2026 13:38:52 +0000 Subject: [PATCH 4/4] Add CODEOWNER for MyEnergi plugin --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 88adf46f..f1eea6b7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -36,3 +36,4 @@ /plugins/WorldCup2026/ @TimWheeler-SQUP /plugins/Zscaler/ @shawn149 /plugins/Healthchecks.io/ @adamkinniburgh +/plugins/MyEnergi/ @adamkinniburgh