Add go-e wallbox charger to live and statistics views
- PowerPoint/EnergyBar: add charger/charge fields - getPower: query charger_10m/charger_1h alongside PV and meters - getBars: compute daily charge kWh via LAG on charger_daily.eto_wh - EnergyView: green Charger series in live power chart - StatisticsView: purple Charge bars in statistics chart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ export interface EnergyBar {
|
|||||||
sell: number
|
sell: number
|
||||||
produce: number
|
produce: number
|
||||||
consume: number
|
consume: number
|
||||||
|
charge: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PowerPoint {
|
export interface PowerPoint {
|
||||||
@@ -35,6 +36,7 @@ export interface PowerPoint {
|
|||||||
pv: number
|
pv: number
|
||||||
house: number
|
house: number
|
||||||
barn: number
|
barn: number
|
||||||
|
charger: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatteryPoint {
|
export interface BatteryPoint {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const COLORS = {
|
|||||||
pv: { bg: 'rgba(255,213,79,0.15)', border: '#ffd54f' },
|
pv: { bg: 'rgba(255,213,79,0.15)', border: '#ffd54f' },
|
||||||
house: { bg: 'rgba(66,165,245,0.15)', border: '#42a5f5' },
|
house: { bg: 'rgba(66,165,245,0.15)', border: '#42a5f5' },
|
||||||
barn: { bg: 'rgba(239,83,80,0.15)', border: '#ef5350' },
|
barn: { bg: 'rgba(239,83,80,0.15)', border: '#ef5350' },
|
||||||
|
charger: { bg: 'rgba(102,187,106,0.15)', border: '#66bb6a' },
|
||||||
battery: { bg: 'rgba(66,165,245,0.2)', border: '#42a5f5' },
|
battery: { bg: 'rgba(66,165,245,0.2)', border: '#42a5f5' },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ function buildPowerChart(canvas: HTMLCanvasElement, pts: PowerPoint[], range: st
|
|||||||
{ label: 'PV Production', data: pts.map(p => p.pv), borderColor: COLORS.pv.border, backgroundColor: COLORS.pv.bg, borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3 },
|
{ label: 'PV Production', data: pts.map(p => p.pv), borderColor: COLORS.pv.border, backgroundColor: COLORS.pv.bg, borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3 },
|
||||||
{ label: 'House', data: pts.map(p => p.house), borderColor: COLORS.house.border, backgroundColor: COLORS.house.bg, borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3 },
|
{ label: 'House', data: pts.map(p => p.house), borderColor: COLORS.house.border, backgroundColor: COLORS.house.bg, borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3 },
|
||||||
{ label: 'Barn', data: pts.map(p => p.barn), borderColor: COLORS.barn.border, backgroundColor: COLORS.barn.bg, borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3 },
|
{ label: 'Barn', data: pts.map(p => p.barn), borderColor: COLORS.barn.border, backgroundColor: COLORS.barn.bg, borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3 },
|
||||||
|
{ label: 'Charger', data: pts.map(p => p.charger), borderColor: COLORS.charger.border, backgroundColor: COLORS.charger.bg, borderWidth: 1.5, pointRadius: 0, fill: true, tension: 0.3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const COLORS = {
|
|||||||
sell: { bg: 'rgba(66,165,245,0.8)', border: '#42a5f5' },
|
sell: { bg: 'rgba(66,165,245,0.8)', border: '#42a5f5' },
|
||||||
produce: { bg: 'rgba(102,187,106,0.8)', border: '#66bb6a' },
|
produce: { bg: 'rgba(102,187,106,0.8)', border: '#66bb6a' },
|
||||||
consume: { bg: 'rgba(255,167,38,0.8)', border: '#ffa726' },
|
consume: { bg: 'rgba(255,167,38,0.8)', border: '#ffa726' },
|
||||||
|
charge: { bg: 'rgba(171,71,188,0.8)', border: '#ab47bc' },
|
||||||
}
|
}
|
||||||
|
|
||||||
function labelFor(t: string): string {
|
function labelFor(t: string): string {
|
||||||
@@ -78,6 +79,7 @@ function datasets(data: EnergyBar[]): ChartDataset<'bar'>[] {
|
|||||||
{ label: 'Sell', data: data.map(b => b.sell), backgroundColor: COLORS.sell.bg, borderColor: COLORS.sell.border, borderWidth: 1 },
|
{ label: 'Sell', data: data.map(b => b.sell), backgroundColor: COLORS.sell.bg, borderColor: COLORS.sell.border, borderWidth: 1 },
|
||||||
{ label: 'Produce', data: data.map(b => b.produce), backgroundColor: COLORS.produce.bg, borderColor: COLORS.produce.border, borderWidth: 1 },
|
{ label: 'Produce', data: data.map(b => b.produce), backgroundColor: COLORS.produce.bg, borderColor: COLORS.produce.border, borderWidth: 1 },
|
||||||
{ label: 'Consume', data: data.map(b => b.consume), backgroundColor: COLORS.consume.bg, borderColor: COLORS.consume.border, borderWidth: 1 },
|
{ label: 'Consume', data: data.map(b => b.consume), backgroundColor: COLORS.consume.bg, borderColor: COLORS.consume.border, borderWidth: 1 },
|
||||||
|
{ label: 'Charge', data: data.map(b => b.charge), backgroundColor: COLORS.charge.bg, borderColor: COLORS.charge.border, borderWidth: 1 },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type EnergyBar struct {
|
|||||||
Sell float64 `json:"sell"`
|
Sell float64 `json:"sell"`
|
||||||
Produce float64 `json:"produce"`
|
Produce float64 `json:"produce"`
|
||||||
Consume float64 `json:"consume"`
|
Consume float64 `json:"consume"`
|
||||||
|
Charge float64 `json:"charge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PowerPoint struct {
|
type PowerPoint struct {
|
||||||
@@ -22,6 +23,7 @@ type PowerPoint struct {
|
|||||||
PV float64 `json:"pv"`
|
PV float64 `json:"pv"`
|
||||||
Barn float64 `json:"barn"`
|
Barn float64 `json:"barn"`
|
||||||
House float64 `json:"house"`
|
House float64 `json:"house"`
|
||||||
|
Charger float64 `json:"charger"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BatteryPoint struct {
|
type BatteryPoint struct {
|
||||||
@@ -36,16 +38,16 @@ var periodDefaults = map[string]int{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// viewForRange picks the right continuous aggregate views for the given time range.
|
// viewForRange picks the right continuous aggregate views for the given time range.
|
||||||
func viewForRange(timeRange string) (invView, meterView, interval string) {
|
func viewForRange(timeRange string) (invView, meterView, chargerView, interval string) {
|
||||||
switch timeRange {
|
switch timeRange {
|
||||||
case "1h":
|
case "1h":
|
||||||
return "inverter_10m", "power_meter_10m", "1 hour"
|
return "inverter_10m", "power_meter_10m", "charger_10m", "1 hour"
|
||||||
case "6h":
|
case "6h":
|
||||||
return "inverter_10m", "power_meter_10m", "6 hours"
|
return "inverter_10m", "power_meter_10m", "charger_10m", "6 hours"
|
||||||
case "7d":
|
case "7d":
|
||||||
return "inverter_1h", "power_meter_1h", "7 days"
|
return "inverter_1h", "power_meter_1h", "charger_1h", "7 days"
|
||||||
default: // "24h" and anything else
|
default: // "24h" and anything else
|
||||||
return "inverter_10m", "power_meter_10m", "24 hours"
|
return "inverter_10m", "power_meter_10m", "charger_10m", "24 hours"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ func sortedUnion(maps ...map[time.Time]float64) []time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getPower(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]PowerPoint, error) {
|
func getPower(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]PowerPoint, error) {
|
||||||
invView, meterView, interval := viewForRange(timeRange)
|
invView, meterView, chargerView, interval := viewForRange(timeRange)
|
||||||
|
|
||||||
type colRow struct {
|
type colRow struct {
|
||||||
bucket time.Time
|
bucket time.Time
|
||||||
@@ -97,6 +99,9 @@ func getPower(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]Powe
|
|||||||
barnSQL := fmt.Sprintf(
|
barnSQL := fmt.Sprintf(
|
||||||
`SELECT bucket, COALESCE(total_power, 0) FROM %s WHERE device = 'barn' AND bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
`SELECT bucket, COALESCE(total_power, 0) FROM %s WHERE device = 'barn' AND bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||||
meterView, interval)
|
meterView, interval)
|
||||||
|
chargerSQL := fmt.Sprintf(
|
||||||
|
`SELECT bucket, COALESCE(power, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||||
|
chargerView, interval)
|
||||||
|
|
||||||
type result struct {
|
type result struct {
|
||||||
m map[time.Time]float64
|
m map[time.Time]float64
|
||||||
@@ -105,14 +110,17 @@ func getPower(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]Powe
|
|||||||
pvCh := make(chan result, 1)
|
pvCh := make(chan result, 1)
|
||||||
houseCh := make(chan result, 1)
|
houseCh := make(chan result, 1)
|
||||||
barnCh := make(chan result, 1)
|
barnCh := make(chan result, 1)
|
||||||
|
chargerCh := make(chan result, 1)
|
||||||
|
|
||||||
go func() { m, e := queryCol(pvSQL); pvCh <- result{m, e} }()
|
go func() { m, e := queryCol(pvSQL); pvCh <- result{m, e} }()
|
||||||
go func() { m, e := queryCol(houseSQL); houseCh <- result{m, e} }()
|
go func() { m, e := queryCol(houseSQL); houseCh <- result{m, e} }()
|
||||||
go func() { m, e := queryCol(barnSQL); barnCh <- result{m, e} }()
|
go func() { m, e := queryCol(barnSQL); barnCh <- result{m, e} }()
|
||||||
|
go func() { m, e := queryCol(chargerSQL); chargerCh <- result{m, e} }()
|
||||||
|
|
||||||
pvRes := <-pvCh
|
pvRes := <-pvCh
|
||||||
houseRes := <-houseCh
|
houseRes := <-houseCh
|
||||||
barnRes := <-barnCh
|
barnRes := <-barnCh
|
||||||
|
chargerRes := <-chargerCh
|
||||||
|
|
||||||
if pvRes.err != nil {
|
if pvRes.err != nil {
|
||||||
return nil, pvRes.err
|
return nil, pvRes.err
|
||||||
@@ -123,8 +131,11 @@ func getPower(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]Powe
|
|||||||
if barnRes.err != nil {
|
if barnRes.err != nil {
|
||||||
return nil, barnRes.err
|
return nil, barnRes.err
|
||||||
}
|
}
|
||||||
|
if chargerRes.err != nil {
|
||||||
|
return nil, chargerRes.err
|
||||||
|
}
|
||||||
|
|
||||||
times := sortedUnion(pvRes.m, houseRes.m, barnRes.m)
|
times := sortedUnion(pvRes.m, houseRes.m, barnRes.m, chargerRes.m)
|
||||||
pts := make([]PowerPoint, 0, len(times))
|
pts := make([]PowerPoint, 0, len(times))
|
||||||
for _, t := range times {
|
for _, t := range times {
|
||||||
pts = append(pts, PowerPoint{
|
pts = append(pts, PowerPoint{
|
||||||
@@ -132,13 +143,14 @@ func getPower(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]Powe
|
|||||||
PV: pvRes.m[t],
|
PV: pvRes.m[t],
|
||||||
House: houseRes.m[t],
|
House: houseRes.m[t],
|
||||||
Barn: barnRes.m[t],
|
Barn: barnRes.m[t],
|
||||||
|
Charger: chargerRes.m[t],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return pts, nil
|
return pts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBattery(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]BatteryPoint, error) {
|
func getBattery(ctx context.Context, pool *pgxpool.Pool, timeRange string) ([]BatteryPoint, error) {
|
||||||
invView, _, interval := viewForRange(timeRange)
|
invView, _, _, interval := viewForRange(timeRange)
|
||||||
|
|
||||||
sql := fmt.Sprintf(
|
sql := fmt.Sprintf(
|
||||||
`SELECT bucket, COALESCE(battery_soc, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
`SELECT bucket, COALESCE(battery_soc, 0) FROM %s WHERE bucket >= NOW() - '%s'::interval ORDER BY bucket`,
|
||||||
@@ -206,6 +218,14 @@ barn_last AS (
|
|||||||
WHERE device = 'barn' AND bucket >= NOW() - '%[2]s'::interval
|
WHERE device = 'barn' AND bucket >= NOW() - '%[2]s'::interval
|
||||||
ORDER BY date_trunc('%[1]s', bucket), bucket DESC
|
ORDER BY date_trunc('%[1]s', bucket), bucket DESC
|
||||||
),
|
),
|
||||||
|
charger_last AS (
|
||||||
|
SELECT DISTINCT ON (date_trunc('%[1]s', bucket))
|
||||||
|
date_trunc('%[1]s', bucket) AS period,
|
||||||
|
eto_wh
|
||||||
|
FROM charger_daily
|
||||||
|
WHERE bucket >= NOW() - '%[2]s'::interval
|
||||||
|
ORDER BY date_trunc('%[1]s', bucket), bucket DESC
|
||||||
|
),
|
||||||
joined AS (
|
joined AS (
|
||||||
SELECT
|
SELECT
|
||||||
i.period,
|
i.period,
|
||||||
@@ -218,17 +238,21 @@ joined AS (
|
|||||||
h.import_kwh AS house_kwh,
|
h.import_kwh AS house_kwh,
|
||||||
LAG(h.import_kwh) OVER (ORDER BY i.period) AS prev_house,
|
LAG(h.import_kwh) OVER (ORDER BY i.period) AS prev_house,
|
||||||
b.import_kwh AS barn_kwh,
|
b.import_kwh AS barn_kwh,
|
||||||
LAG(b.import_kwh) OVER (ORDER BY i.period) AS prev_barn
|
LAG(b.import_kwh) OVER (ORDER BY i.period) AS prev_barn,
|
||||||
|
c.eto_wh AS charger_wh,
|
||||||
|
LAG(c.eto_wh) OVER (ORDER BY i.period) AS prev_charger_wh
|
||||||
FROM inv_last i
|
FROM inv_last i
|
||||||
LEFT JOIN house_last h ON h.period = i.period
|
LEFT JOIN house_last h ON h.period = i.period
|
||||||
LEFT JOIN barn_last b ON b.period = i.period
|
LEFT JOIN barn_last b ON b.period = i.period
|
||||||
|
LEFT JOIN charger_last c ON c.period = i.period
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
period,
|
period,
|
||||||
GREATEST(0, grid_import_kwh - prev_import) AS buy,
|
GREATEST(0, grid_import_kwh - prev_import) AS buy,
|
||||||
GREATEST(0, grid_export_kwh - prev_export) AS sell,
|
GREATEST(0, grid_export_kwh - prev_export) AS sell,
|
||||||
GREATEST(0, pv_energy_kwh - prev_pv) AS produce,
|
GREATEST(0, pv_energy_kwh - prev_pv) AS produce,
|
||||||
GREATEST(0, COALESCE(house_kwh - prev_house, 0) + COALESCE(barn_kwh - prev_barn, 0)) AS consume
|
GREATEST(0, COALESCE(house_kwh - prev_house, 0) + COALESCE(barn_kwh - prev_barn, 0)) AS consume,
|
||||||
|
GREATEST(0, COALESCE((charger_wh - prev_charger_wh) / 1000.0, 0)) AS charge
|
||||||
FROM joined
|
FROM joined
|
||||||
WHERE prev_import IS NOT NULL
|
WHERE prev_import IS NOT NULL
|
||||||
ORDER BY period
|
ORDER BY period
|
||||||
@@ -243,7 +267,7 @@ ORDER BY period
|
|||||||
var bars []EnergyBar
|
var bars []EnergyBar
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var b EnergyBar
|
var b EnergyBar
|
||||||
if err := rows.Scan(&b.Time, &b.Buy, &b.Sell, &b.Produce, &b.Consume); err != nil {
|
if err := rows.Scan(&b.Time, &b.Buy, &b.Sell, &b.Produce, &b.Consume, &b.Charge); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
bars = append(bars, b)
|
bars = append(bars, b)
|
||||||
|
|||||||
Reference in New Issue
Block a user