@mxpicture/tesla-superchargers-mate 0.1.62 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ -- pic_refs: the precision `hours` needs now that pic_refs_v divides by it, and
2
+ -- two invariants stated as CHECK constraints.
3
+ --
4
+ -- (1) hours: 2 -> 6 dp. It was a display value, which is why 20260903171045
5
+ -- moved mean_draw_kw off it and onto the raw interval. It stops being one
6
+ -- here: pic_refs_v's gross_mean_draw_kw becomes
7
+ -- gross_dropped_kwh / gross_hours, so `hours` is a divisor one level up --
8
+ -- and rounding a divisor is what caused that defect in the first place. At
9
+ -- 2 dp the resulting error on gross_mean_draw_kw reaches 5.9455 kW and two
10
+ -- sub-18-second refs round to a zero divisor and vanish; at 6 dp the error
11
+ -- is 0.0020 kW and nothing vanishes. It also keeps a row self-consistent:
12
+ -- at 2 dp, 354 of 700 rows would show a gross_mean_draw_kw that does not
13
+ -- reconcile with the gross_hours printed beside it.
14
+ --
15
+ -- Nothing else moves. mean_draw_kw at 3 dp (1 W), cost_per_kwh at 4 (the
16
+ -- 0.08 EUR/kWh band the column exists to resolve) and the rest are terminal
17
+ -- display values that feed no computation, and each scale was chosen for
18
+ -- what its column measures.
19
+ --
20
+ -- (2) efficiency > 0. Four generation expressions carried NULLIF(efficiency, 0)
21
+ -- to keep a zero out of a divisor. All that could ever do was turn a broken
22
+ -- row into a silent NULL; stating the invariant once, checked on write, is
23
+ -- the same guarantee and a better failure. A car with efficiency = 0 now
24
+ -- fails the build loudly, which is consistent with what already happens for
25
+ -- a NULL efficiency (review Md-6). The NULLIF then comes out of those four
26
+ -- expressions, each keeping its round() scale exactly as it was.
27
+ --
28
+ -- (3) end_date >= start_date. An interval that runs backwards gives a negative
29
+ -- `hours` and a mean_draw_kw whose sign means the opposite of what it says.
30
+ -- Both CHECKs are validated against every existing row on ADD, and both
31
+ -- return zero rows on the dev history; re-run the two SELECTs in section 8
32
+ -- of the plan against production before applying.
33
+ --
34
+ -- The order below is forced, not stylistic. PG18 refuses SET EXPRESSION on a
35
+ -- VIRTUAL generated column while the table carries any CHECK constraint
36
+ -- (0A000), so every expression has to be rewritten before the CHECKs go on --
37
+ -- and a later migration that wants to change one of these columns has to drop
38
+ -- the two CHECKs, rewrite, and add them back. migrate:down runs the same
39
+ -- sequence in reverse for the same reason.
40
+ --
41
+ -- Every column touched is VIRTUAL, so SET EXPRESSION is a catalog edit and none
42
+ -- of this rewrites the table. The two CHECKs do scan it once.
43
+ --
44
+ -- docs/plans/2026-09-07_pic-refs-gross-ratios.md, sections 5 and 8.
45
+ -- migrate:up
46
+ ALTER TABLE pic_refs
47
+ ALTER COLUMN hours
48
+ SET
49
+ EXPRESSION AS (
50
+ round(
51
+ EXTRACT(
52
+ epoch
53
+ FROM
54
+ (end_date - start_date)
55
+ ) / 3600.0,
56
+ 6
57
+ )
58
+ );
59
+
60
+ ALTER TABLE pic_refs
61
+ ALTER COLUMN mean_draw_kw
62
+ SET
63
+ EXPRESSION AS (
64
+ round(
65
+ (start_range_km - end_range_km) * efficiency / NULLIF(
66
+ EXTRACT(
67
+ epoch
68
+ FROM
69
+ (end_date - start_date)
70
+ ) / 3600.0,
71
+ 0::NUMERIC
72
+ ),
73
+ 3
74
+ )
75
+ );
76
+
77
+ ALTER TABLE pic_refs
78
+ ALTER COLUMN dropped_kwh
79
+ SET
80
+ EXPRESSION AS ((start_range_km - end_range_km) * efficiency);
81
+
82
+ ALTER TABLE pic_refs
83
+ ALTER COLUMN kwh_per_real_1km
84
+ SET
85
+ EXPRESSION AS (
86
+ round(
87
+ (start_range_km - end_range_km) * efficiency / NULLIF(real_km, 0::NUMERIC),
88
+ 6
89
+ )
90
+ );
91
+
92
+ ALTER TABLE pic_refs
93
+ ALTER COLUMN cost_per_kwh
94
+ SET
95
+ EXPRESSION AS (
96
+ CASE
97
+ WHEN start_range_km < end_range_km THEN NULL
98
+ ELSE round(
99
+ cost / NULLIF(
100
+ (start_range_km - end_range_km) * efficiency,
101
+ 0::NUMERIC
102
+ ),
103
+ 4
104
+ )
105
+ END
106
+ );
107
+
108
+ ALTER TABLE pic_refs
109
+ ADD CONSTRAINT pic_refs_efficiency_positive CHECK (efficiency > 0);
110
+
111
+ ALTER TABLE pic_refs
112
+ ADD CONSTRAINT pic_refs_date_order CHECK (end_date >= start_date);
113
+
114
+ -- migrate:down
115
+ ALTER TABLE pic_refs
116
+ DROP CONSTRAINT IF EXISTS pic_refs_date_order;
117
+
118
+ ALTER TABLE pic_refs
119
+ DROP CONSTRAINT IF EXISTS pic_refs_efficiency_positive;
120
+
121
+ ALTER TABLE pic_refs
122
+ ALTER COLUMN cost_per_kwh
123
+ SET
124
+ EXPRESSION AS (
125
+ CASE
126
+ WHEN start_range_km < end_range_km THEN NULL
127
+ ELSE round(
128
+ cost / NULLIF(
129
+ (start_range_km - end_range_km) * NULLIF(efficiency, 0),
130
+ 0::NUMERIC
131
+ ),
132
+ 4
133
+ )
134
+ END
135
+ );
136
+
137
+ ALTER TABLE pic_refs
138
+ ALTER COLUMN kwh_per_real_1km
139
+ SET
140
+ EXPRESSION AS (
141
+ round(
142
+ (start_range_km - end_range_km) * NULLIF(efficiency, 0) / NULLIF(real_km, 0::NUMERIC),
143
+ 6
144
+ )
145
+ );
146
+
147
+ ALTER TABLE pic_refs
148
+ ALTER COLUMN dropped_kwh
149
+ SET
150
+ EXPRESSION AS (
151
+ (start_range_km - end_range_km) * NULLIF(efficiency, 0)
152
+ );
153
+
154
+ ALTER TABLE pic_refs
155
+ ALTER COLUMN mean_draw_kw
156
+ SET
157
+ EXPRESSION AS (
158
+ round(
159
+ (start_range_km - end_range_km) * NULLIF(efficiency, 0) / NULLIF(
160
+ EXTRACT(
161
+ epoch
162
+ FROM
163
+ (end_date - start_date)
164
+ ) / 3600.0,
165
+ 0::NUMERIC
166
+ ),
167
+ 3
168
+ )
169
+ );
170
+
171
+ ALTER TABLE pic_refs
172
+ ALTER COLUMN hours
173
+ SET
174
+ EXPRESSION AS (
175
+ round(
176
+ EXTRACT(
177
+ epoch
178
+ FROM
179
+ (end_date - start_date)
180
+ ) / 3600.0,
181
+ 2
182
+ )
183
+ );
package/db/schema.sql CHANGED
@@ -415,17 +415,19 @@ CREATE TABLE public.pic_refs (
415
415
  real_km numeric(8,4),
416
416
  used_cp_ids integer[],
417
417
  cost numeric(6,2),
418
- hours numeric GENERATED ALWAYS AS (round((EXTRACT(epoch FROM (end_date - start_date)) / 3600.0), 2)),
419
- mean_draw_kw numeric GENERATED ALWAYS AS (round((((start_range_km - end_range_km) * NULLIF(efficiency, (0)::numeric)) / NULLIF((EXTRACT(epoch FROM (end_date - start_date)) / 3600.0), (0)::numeric)), 3)),
418
+ hours numeric GENERATED ALWAYS AS (round((EXTRACT(epoch FROM (end_date - start_date)) / 3600.0), 6)),
419
+ mean_draw_kw numeric GENERATED ALWAYS AS (round((((start_range_km - end_range_km) * efficiency) / NULLIF((EXTRACT(epoch FROM (end_date - start_date)) / 3600.0), (0)::numeric)), 3)),
420
420
  dropped_km numeric GENERATED ALWAYS AS ((start_range_km - end_range_km)),
421
- dropped_kwh numeric GENERATED ALWAYS AS (((start_range_km - end_range_km) * NULLIF(efficiency, (0)::numeric))),
422
- kwh_per_real_1km numeric GENERATED ALWAYS AS (round((((start_range_km - end_range_km) * NULLIF(efficiency, (0)::numeric)) / NULLIF(real_km, (0)::numeric)), 6)),
421
+ dropped_kwh numeric GENERATED ALWAYS AS (((start_range_km - end_range_km) * efficiency)),
422
+ kwh_per_real_1km numeric GENERATED ALWAYS AS (round((((start_range_km - end_range_km) * efficiency) / NULLIF(real_km, (0)::numeric)), 6)),
423
423
  cost_per_real_100km numeric GENERATED ALWAYS AS (round(((cost * (100)::numeric) / NULLIF(real_km, (0)::numeric)), 2)),
424
424
  cost_per_kwh numeric GENERATED ALWAYS AS (
425
425
  CASE
426
426
  WHEN (start_range_km < end_range_km) THEN NULL::numeric
427
- ELSE round((cost / NULLIF(((start_range_km - end_range_km) * NULLIF(efficiency, (0)::numeric)), (0)::numeric)), 4)
428
- END)
427
+ ELSE round((cost / NULLIF(((start_range_km - end_range_km) * efficiency), (0)::numeric)), 4)
428
+ END),
429
+ CONSTRAINT pic_refs_date_order CHECK ((end_date >= start_date)),
430
+ CONSTRAINT pic_refs_efficiency_positive CHECK ((efficiency > (0)::numeric))
429
431
  );
430
432
 
431
433
 
@@ -2669,17 +2671,17 @@ CREATE VIEW public.pic_refs_v AS
2669
2671
  ELSE r.cost
2670
2672
  END AS dropped_cost,
2671
2673
  CASE
2672
- WHEN (r.ref_type = 'charge'::public.pic_ref_type) THEN ((- r.cost) - COALESCE(i.cost, 0.0))
2673
- ELSE (r.cost + COALESCE(i.cost, 0.0))
2674
+ WHEN (r.ref_type = 'charge'::public.pic_ref_type) THEN (- s.gross_cost)
2675
+ ELSE s.gross_cost
2674
2676
  END AS gross_dropped_cost,
2675
- (r.cost + COALESCE(i.cost, 0.0)) AS gross_cost,
2676
- (r.hours + COALESCE(i.hours, 0.0)) AS gross_hours,
2677
- (r.mean_draw_kw + COALESCE(i.mean_draw_kw, 0.0)) AS gross_mean_draw_kw,
2678
- (r.dropped_km + COALESCE(i.dropped_km, 0.0)) AS gross_dropped_km,
2679
- (r.dropped_kwh + COALESCE(i.dropped_kwh, 0.0)) AS gross_dropped_kwh,
2680
- (r.kwh_per_real_1km + COALESCE(i.kwh_per_real_1km, 0.0)) AS gross_kwh_per_real_1km,
2681
- (r.cost_per_real_100km + COALESCE(i.cost_per_real_100km, 0.0)) AS gross_cost_per_real_100km,
2682
- (r.cost_per_kwh + COALESCE(i.cost_per_kwh, 0.0)) AS gross_cost_per_kwh,
2677
+ s.gross_cost,
2678
+ s.gross_hours,
2679
+ g.gross_mean_draw_kw,
2680
+ s.gross_dropped_km,
2681
+ s.gross_dropped_kwh,
2682
+ g.gross_kwh_per_real_1km,
2683
+ g.gross_cost_per_real_100km,
2684
+ g.gross_cost_per_kwh,
2683
2685
  sm.seed_address_id AS start_seed_address_id,
2684
2686
  em.seed_address_id AS end_seed_address_id,
2685
2687
  sm.cluster_id AS start_cluster_id,
@@ -2700,8 +2702,8 @@ CREATE VIEW public.pic_refs_v AS
2700
2702
  END AS end_address_type,
2701
2703
  COALESCE(sg.name, (sa.address)::character varying, (sa_fallback.address)::character varying) AS start_address,
2702
2704
  COALESCE(eg.name, (ea.address)::character varying, (ea_fallback.address)::character varying) AS end_address
2703
- FROM (((((((((public.pic_refs r
2704
- LEFT JOIN public.pic_refs i ON (((i.ref_type = 'idle'::public.pic_ref_type) AND (r.ref_type = 'drive'::public.pic_ref_type) AND (i.next_type = r.ref_type) AND (i.next_id = r.ref_id))))
2705
+ FROM (((((((((((public.pic_refs r
2706
+ LEFT JOIN public.pic_refs i ON (((i.ref_type = 'idle'::public.pic_ref_type) AND (r.ref_type = 'drive'::public.pic_ref_type) AND (i.car_id = r.car_id) AND (i.next_type = r.ref_type) AND (i.next_id = r.ref_id))))
2705
2707
  LEFT JOIN public.pic_cluster_map_v sm ON ((sm.address_id = r.start_address_id)))
2706
2708
  LEFT JOIN public.pic_cluster_map_v em ON ((em.address_id = r.end_address_id)))
2707
2709
  LEFT JOIN public.pic_addresses_v sa ON ((sa.id = sm.seed_address_id)))
@@ -2710,6 +2712,17 @@ CREATE VIEW public.pic_refs_v AS
2710
2712
  LEFT JOIN public.pic_addresses_v ea_fallback ON ((ea_fallback.id = r.end_address_id)))
2711
2713
  LEFT JOIN public.geofences sg ON ((sg.id = r.start_geofence_id)))
2712
2714
  LEFT JOIN public.geofences eg ON ((eg.id = r.end_geofence_id)))
2715
+ CROSS JOIN LATERAL ( SELECT (r.cost + COALESCE(i.cost, 0.0)) AS gross_cost,
2716
+ (r.hours + COALESCE(i.hours, 0.0)) AS gross_hours,
2717
+ (r.dropped_km + COALESCE(i.dropped_km, 0.0)) AS gross_dropped_km,
2718
+ (r.dropped_kwh + COALESCE(i.dropped_kwh, 0.0)) AS gross_dropped_kwh) s)
2719
+ CROSS JOIN LATERAL ( SELECT round((s.gross_dropped_kwh / NULLIF(s.gross_hours, (0)::numeric)), 3) AS gross_mean_draw_kw,
2720
+ round((s.gross_dropped_kwh / NULLIF(r.real_km, (0)::numeric)), 6) AS gross_kwh_per_real_1km,
2721
+ round(((s.gross_cost * (100)::numeric) / NULLIF(r.real_km, (0)::numeric)), 2) AS gross_cost_per_real_100km,
2722
+ CASE
2723
+ WHEN (s.gross_dropped_kwh <= (0)::numeric) THEN NULL::numeric
2724
+ ELSE round((s.gross_cost / s.gross_dropped_kwh), 4)
2725
+ END AS gross_cost_per_kwh) g)
2713
2726
  WHERE ((r.ref_id > 0) AND (COALESCE(r.prev_id, 0) >= 0));
2714
2727
 
2715
2728
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxpicture/tesla-superchargers-mate",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "TeslaMate integration",
5
5
  "type": "module",
6
6
  "author": "MXPicture",
@@ -50,7 +50,7 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@mxpicture/tesla-superchargers-generated": "^0.1.62",
53
+ "@mxpicture/tesla-superchargers-generated": "^0.1.63",
54
54
  "dbmate": "2.35.1",
55
55
  "postgres": "^3.4.9",
56
56
  "short-uuid": "^6.0.3",
package/sql/views.sql CHANGED
@@ -1049,18 +1049,25 @@ SELECT
1049
1049
  WHEN r.ref_type = 'charge'::pic_ref_type THEN - r.cost
1050
1050
  ELSE r.cost
1051
1051
  END AS dropped_cost,
1052
+ -- The gross_* block: this ref plus the idle that fed it. The extensive
1053
+ -- quantities -- cost, hours, dropped_km, dropped_kwh -- are summed in `s`
1054
+ -- and genuinely do add. The four ratios are recomputed over those sums in
1055
+ -- `g` and are never added: the sum of two rates is not the rate over the
1056
+ -- combined window. A 0.3 kW idle before a 25 kW drive reported 25.3 kW, and
1057
+ -- adding the two EUR/kWh figures was out by up to 0.5853 -- seven times the
1058
+ -- 0.08 EUR/kWh band that cost_per_kwh carries four decimals to resolve.
1052
1059
  CASE
1053
- WHEN r.ref_type = 'charge'::pic_ref_type THEN - r.cost - COALESCE(i.cost, 0.0)
1054
- ELSE r.cost + COALESCE(i.cost, 0.0)
1060
+ WHEN r.ref_type = 'charge'::pic_ref_type THEN - s.gross_cost
1061
+ ELSE s.gross_cost
1055
1062
  END AS gross_dropped_cost,
1056
- r.cost + COALESCE(i.cost, 0.0) AS gross_cost,
1057
- r.hours + COALESCE(i.hours, 0.0) AS gross_hours,
1058
- r.mean_draw_kw + COALESCE(i.mean_draw_kw, 0.0) AS gross_mean_draw_kw,
1059
- r.dropped_km + COALESCE(i.dropped_km, 0.0) AS gross_dropped_km,
1060
- r.dropped_kwh + COALESCE(i.dropped_kwh, 0.0) AS gross_dropped_kwh,
1061
- r.kwh_per_real_1km + COALESCE(i.kwh_per_real_1km, 0.0) AS gross_kwh_per_real_1km,
1062
- r.cost_per_real_100km + COALESCE(i.cost_per_real_100km, 0.0) AS gross_cost_per_real_100km,
1063
- r.cost_per_kwh + COALESCE(i.cost_per_kwh, 0.0) AS gross_cost_per_kwh,
1063
+ s.gross_cost,
1064
+ s.gross_hours,
1065
+ g.gross_mean_draw_kw,
1066
+ s.gross_dropped_km,
1067
+ s.gross_dropped_kwh,
1068
+ g.gross_kwh_per_real_1km,
1069
+ g.gross_cost_per_real_100km,
1070
+ g.gross_cost_per_kwh,
1064
1071
  sm.seed_address_id AS start_seed_address_id,
1065
1072
  em.seed_address_id AS end_seed_address_id,
1066
1073
  sm.cluster_id AS start_cluster_id,
@@ -1083,8 +1090,11 @@ SELECT
1083
1090
  COALESCE(eg.name, ea.address, ea_fallback.address) AS end_address
1084
1091
  FROM
1085
1092
  pic_refs r
1093
+ -- next_type/next_id identify the idle on their own, but the pack replay is
1094
+ -- per-car and the join says so.
1086
1095
  LEFT JOIN pic_refs i ON i.ref_type = 'idle'::pic_ref_type
1087
1096
  AND r.ref_type = 'drive'::pic_ref_type
1097
+ AND i.car_id = r.car_id
1088
1098
  AND i.next_type = r.ref_type
1089
1099
  AND i.next_id = r.ref_id
1090
1100
  LEFT JOIN pic_cluster_map_v sm ON sm.address_id = r.start_address_id
@@ -1095,6 +1105,47 @@ FROM
1095
1105
  LEFT JOIN pic_addresses_v ea_fallback ON ea_fallback.id = r.end_address_id
1096
1106
  LEFT JOIN geofences sg ON sg.id = r.start_geofence_id
1097
1107
  LEFT JOIN geofences eg ON eg.id = r.end_geofence_id
1108
+ -- Two chained LATERALs rather than generated columns on pic_refs: a
1109
+ -- generation expression cannot reference another generated column, and
1110
+ -- gross_mean_draw_kw needs both of `s`'s sums. Each returns exactly one row,
1111
+ -- so neither changes the cardinality above.
1112
+ CROSS JOIN LATERAL (
1113
+ SELECT
1114
+ r.cost + COALESCE(i.cost, 0.0) AS gross_cost,
1115
+ r.hours + COALESCE(i.hours, 0.0) AS gross_hours,
1116
+ r.dropped_km + COALESCE(i.dropped_km, 0.0) AS gross_dropped_km,
1117
+ r.dropped_kwh + COALESCE(i.dropped_kwh, 0.0) AS gross_dropped_kwh
1118
+ ) s
1119
+ -- The ratios over those sums, each rounded to the scale of its per-row twin
1120
+ -- so that gross_X and X read at the same precision. gross_hours is what
1121
+ -- makes `hours` load-bearing rather than a display value, and why
1122
+ -- 20260907181456 took it to 6 dp.
1123
+ --
1124
+ -- The per-km pair divides by r.real_km alone: an idle covers no distance, so
1125
+ -- the drive's kilometres are the whole of the combined window's. That makes
1126
+ -- both a per-trip fact with a legitimately heavy tail -- 24 of 320 drives
1127
+ -- exceed 1 kWh/km, because a one-kilometre drive after a three-day park
1128
+ -- genuinely did cost that much per km once standby is charged to it. The
1129
+ -- car's own consumption rate is the adjacent kwh_per_real_1km on the same
1130
+ -- row; the tail is not a defect and nothing here thresholds it away.
1131
+ --
1132
+ -- What the tail does break is averaging. Aggregate these as
1133
+ -- sum(numerator) / sum(denominator), never avg(ratio): over the same drives,
1134
+ -- sum(gross_dropped_kwh) / sum(real_km) is 0.2212 kWh/km where
1135
+ -- avg(gross_kwh_per_real_1km) reads 0.4740. Both sums are columns of this
1136
+ -- view.
1137
+ CROSS JOIN LATERAL (
1138
+ SELECT
1139
+ round(s.gross_dropped_kwh / NULLIF(s.gross_hours, 0), 3) AS gross_mean_draw_kw,
1140
+ round(s.gross_dropped_kwh / NULLIF(r.real_km, 0), 6) AS gross_kwh_per_real_1km,
1141
+ round(s.gross_cost * 100 / NULLIF(r.real_km, 0), 2) AS gross_cost_per_real_100km,
1142
+ -- The same guard as pic_refs.cost_per_kwh, for the same reason: a
1143
+ -- non-positive kWh denominator yields a EUR/kWh that does not exist.
1144
+ CASE
1145
+ WHEN s.gross_dropped_kwh <= 0 THEN NULL
1146
+ ELSE round(s.gross_cost / s.gross_dropped_kwh, 4)
1147
+ END AS gross_cost_per_kwh
1148
+ ) g
1098
1149
  WHERE
1099
1150
  -- The per-car seed (ref_id = -car_id) is a modelling artefact that gives the
1100
1151
  -- pack its opening balance; it is not a charge that happened. Anything