Calculate battery earnings using SQL

I’ve created an SQL to calculate the earnings of a battery. It suits my needs and I think it’s quite flexible to fit other needs. I’ve used this SQL in the SQL integration so I have a sensor displaying the current battery earnings. Now the SQL:

/**
This query calculates a couple of things:
- `cost_with_batt` Current energy cost: only flows through the meter
- `cost_wo_batt` Energy cost without battery: compensate meter usage with battery usage
- `earnings_batt` Difference between the two above: how many your batteries have earned you
- `cost_fixed_pricing` Calculate like if you had a fixed contract.

See the comments in the query to configure it to your wishes
*/
WITH RECURSIVE timeseries(gmt) AS (
-- Configure the from-date below. You could also configure something like `start of month`
  SELECT datetime('2025-04-01')
  UNION ALL
-- It's not recommend or tested to change the interval, as the statistics table seems to store it on an hourly interval
  SELECT datetime(gmt, '+1 hour') FROM timeseries
   WHERE datetime(gmt, '+1 hour')<=datetime()
), sensors(statid, type) AS (
-- The first column is the entity id, the second column should not change
-- The pvproduction row is not used
	SELECT 'sensor.energy_consumption_tarif_1', 'meter_in'
	UNION SELECT 'sensor.energy_consumption_tarif_2', 'meter_in'
	UNION SELECT 'sensor.energy_production_tarif_1', 'meter_out'
	UNION SELECT 'sensor.energy_production_tarif_2', 'meter_out'
	UNION SELECT 'sensor.growatt_dischargeenergytotal', 'batt_out'
	UNION SELECT 'sensor.growatt_chargeenergytotal', 'batt_in'
	UNION SELECT 'sensor.growatt_pv1energytotal', 'pvproduction'
	UNION SELECT 'sensor.tibber_price_details', 'price'
), meta(metadata_id, statid, type) AS (
	SELECT statistics_meta.id, statistics_meta.statistic_id, sensors.type
	FROM statistics_meta
	INNER JOIN sensors ON sensors.statid = statistics_meta.statistic_id 
), hourlystates(type, statid, state, ts) AS (
  SELECT	
		type,
		meta.statid,
        CASE WHEN type = 'price' THEN statistics.mean ELSE statistics.state END, 
		datetime(strftime('%F %H:00:00', datetime(statistics.start_ts, 'unixepoch'))) AS ts
  FROM
	statistics
	INNER JOIN meta ON meta.metadata_id = statistics.metadata_id
	WHERE statistics.start_ts > (SELECT unixepoch(MIN(gmt)) FROM timeseries)
), hourlyvalues(type, statid, ts, val) AS (
	SELECT 
		type, 
		statid,
		ts, 
		CASE WHEN type = 'price' THEN (last_value(state) OVER w) ELSE last_value(state) OVER w - first_value(state) OVER w END AS val
	FROM hourlystates
	WINDOW w AS (PARTITION BY type, statid ORDER BY ts ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)
), hourlysums(type, ts, val) AS (
	SELECT 
		type, 
		ts, 
		SUM(val) AS val 
	FROM hourlyvalues
	GROUP BY type, ts 
	ORDER BY ts ASC, type
)
 SELECT DISTINCT
 -- below are the result columns
	ROUND(SUM(price.val * (m_in.val - m_out.val)), 2) AS cost_with_batt,
	ROUND(SUM(price.val * (m_in.val + batt_out.val - m_out.val - batt_in.val)), 2) AS cost_wo_batt,
	ROUND(SUM(price.val * (m_in.val + batt_out.val - m_out.val - batt_in.val)) - SUM(price.val * (m_in.val - m_out.val)), 2) AS earnings_batt,
 	ROUND(SUM(m_in.val * 0.25 - m_out.val * 0.15), 2) AS cost_fixed_pricing
FROM timeseries
LEFT JOIN hourlysums AS price ON price.type = 'price' AND price.ts = timeseries.gmt
LEFT JOIN hourlysums AS m_in ON m_in.type = 'meter_in' AND m_in.ts = timeseries.gmt
LEFT JOIN hourlysums AS m_out ON m_out.type = 'meter_out' AND m_out.ts = timeseries.gmt
LEFT JOIN hourlysums AS batt_in ON batt_in.type = 'batt_in' AND batt_in.ts = timeseries.gmt
LEFT JOIN hourlysums AS batt_out ON batt_out.type = 'batt_out' AND batt_out.ts = timeseries.gmt
LEFT JOIN hourlysums AS pvprod ON pvprod.type = 'pvproduction' AND pvprod.ts = timeseries.gmt
GROUP BY true
ORDER BY 1 ASC

This is configured as follows:

2 Likes