@mxpicture/tesla-superchargers-mate 0.1.56 → 0.1.58

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.
package/db/schema.sql CHANGED
@@ -2386,6 +2386,133 @@ CREATE VIEW public.pic_refs_base_v AS
2386
2386
  FROM row_no;
2387
2387
 
2388
2388
 
2389
+ --
2390
+ -- Name: pic_refs_hvac_v; Type: VIEW; Schema: public; Owner: -
2391
+ --
2392
+
2393
+ CREATE VIEW public.pic_refs_hvac_v AS
2394
+ WITH samples AS (
2395
+ SELECT r.car_id,
2396
+ r.ref_type,
2397
+ r.ref_id,
2398
+ r.start_date AS ref_start_date,
2399
+ r.end_date AS ref_end_date,
2400
+ p.id AS pos_id,
2401
+ p.date,
2402
+ p.is_climate_on,
2403
+ p.is_rear_defroster_on,
2404
+ p.is_front_defroster_on,
2405
+ p.battery_heater
2406
+ FROM (public.pic_refs r
2407
+ JOIN public.positions p ON (((p.car_id = r.car_id) AND (p.drive_id = r.ref_id) AND (p.date >= r.start_date) AND (p.date <= r.end_date) AND (p.is_climate_on IS NOT NULL) AND (p.is_rear_defroster_on IS NOT NULL) AND (p.is_front_defroster_on IS NOT NULL) AND (p.battery_heater IS NOT NULL))))
2408
+ WHERE ((r.ref_type = 'drive'::public.pic_ref_type) AND (r.ref_id > 0) AND (COALESCE(r.prev_id, 0) >= 0))
2409
+ UNION ALL
2410
+ SELECT r.car_id,
2411
+ r.ref_type,
2412
+ r.ref_id,
2413
+ r.start_date AS ref_start_date,
2414
+ r.end_date AS ref_end_date,
2415
+ p.id AS pos_id,
2416
+ p.date,
2417
+ p.is_climate_on,
2418
+ p.is_rear_defroster_on,
2419
+ p.is_front_defroster_on,
2420
+ p.battery_heater
2421
+ FROM (public.pic_refs r
2422
+ JOIN public.positions p ON (((p.car_id = r.car_id) AND (p.drive_id IS NULL) AND (p.date >= r.start_date) AND (p.date <= r.end_date) AND (p.is_climate_on IS NOT NULL) AND (p.is_rear_defroster_on IS NOT NULL) AND (p.is_front_defroster_on IS NOT NULL) AND (p.battery_heater IS NOT NULL))))
2423
+ WHERE ((r.ref_type = ANY (ARRAY['idle'::public.pic_ref_type, 'charge'::public.pic_ref_type])) AND (r.ref_id > 0) AND (COALESCE(r.prev_id, 0) >= 0))
2424
+ ), spans AS (
2425
+ SELECT s_1.car_id,
2426
+ s_1.ref_type,
2427
+ s_1.ref_id,
2428
+ s_1.ref_start_date,
2429
+ s_1.ref_end_date,
2430
+ s_1.pos_id,
2431
+ s_1.date,
2432
+ s_1.is_climate_on,
2433
+ s_1.is_rear_defroster_on,
2434
+ s_1.is_front_defroster_on,
2435
+ s_1.battery_heater,
2436
+ EXTRACT(epoch FROM (COALESCE(lead(s_1.date) OVER w, s_1.ref_end_date) -
2437
+ CASE
2438
+ WHEN (row_number() OVER w = 1) THEN s_1.ref_start_date
2439
+ ELSE s_1.date
2440
+ END)) AS duration_sec
2441
+ FROM samples s_1
2442
+ WINDOW w AS (PARTITION BY s_1.car_id, s_1.ref_type, s_1.ref_id ORDER BY s_1.date, s_1.pos_id)
2443
+ )
2444
+ SELECT car_id,
2445
+ ref_type,
2446
+ ref_id,
2447
+ ref_start_date,
2448
+ ref_end_date,
2449
+ EXTRACT(epoch FROM (ref_end_date - ref_start_date)) AS ref_duration_sec,
2450
+ sum(
2451
+ CASE
2452
+ WHEN is_climate_on THEN duration_sec
2453
+ ELSE (0)::numeric
2454
+ END) AS climate_on_sec,
2455
+ sum(
2456
+ CASE
2457
+ WHEN is_rear_defroster_on THEN duration_sec
2458
+ ELSE (0)::numeric
2459
+ END) AS rear_defroster_sec,
2460
+ sum(
2461
+ CASE
2462
+ WHEN is_front_defroster_on THEN duration_sec
2463
+ ELSE (0)::numeric
2464
+ END) AS front_defroster_sec,
2465
+ sum(
2466
+ CASE
2467
+ WHEN battery_heater THEN duration_sec
2468
+ ELSE (0)::numeric
2469
+ END) AS battery_heater_sec,
2470
+ sum(duration_sec) AS pos_total_sec,
2471
+ count(*) AS pos_count,
2472
+ min(date) AS pos_first_date,
2473
+ max(duration_sec) AS max_gap_sec
2474
+ FROM spans s
2475
+ GROUP BY car_id, ref_type, ref_id, ref_start_date, ref_end_date;
2476
+
2477
+
2478
+ --
2479
+ -- Name: pic_refs_hvac_ratio_v; Type: VIEW; Schema: public; Owner: -
2480
+ --
2481
+
2482
+ CREATE VIEW public.pic_refs_hvac_ratio_v AS
2483
+ SELECT car_id,
2484
+ ref_type,
2485
+ ref_id,
2486
+ ref_start_date,
2487
+ ref_end_date,
2488
+ ref_duration_sec,
2489
+ climate_on_sec,
2490
+ rear_defroster_sec,
2491
+ front_defroster_sec,
2492
+ battery_heater_sec,
2493
+ pos_total_sec,
2494
+ pos_count,
2495
+ pos_first_date,
2496
+ max_gap_sec,
2497
+ CASE
2498
+ WHEN (pos_total_sec > (0)::numeric) THEN round(LEAST((climate_on_sec / pos_total_sec), 1.0), 6)
2499
+ ELSE NULL::numeric
2500
+ END AS climate_on_ratio,
2501
+ CASE
2502
+ WHEN (pos_total_sec > (0)::numeric) THEN round(LEAST((rear_defroster_sec / pos_total_sec), 1.0), 6)
2503
+ ELSE NULL::numeric
2504
+ END AS rear_defroster_ratio,
2505
+ CASE
2506
+ WHEN (pos_total_sec > (0)::numeric) THEN round(LEAST((front_defroster_sec / pos_total_sec), 1.0), 6)
2507
+ ELSE NULL::numeric
2508
+ END AS front_defroster_ratio,
2509
+ CASE
2510
+ WHEN (pos_total_sec > (0)::numeric) THEN round(LEAST((battery_heater_sec / pos_total_sec), 1.0), 6)
2511
+ ELSE NULL::numeric
2512
+ END AS battery_heater_ratio
2513
+ FROM public.pic_refs_hvac_v b;
2514
+
2515
+
2389
2516
  --
2390
2517
  -- Name: pic_refs_prep_v; Type: VIEW; Schema: public; Owner: -
2391
2518
  --
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxpicture/tesla-superchargers-mate",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
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.56",
53
+ "@mxpicture/tesla-superchargers-generated": "^0.1.58",
54
54
  "dbmate": "2.35.1",
55
55
  "postgres": "^3.4.9",
56
56
  "short-uuid": "^6.0.3",
package/sql/reset.sql CHANGED
@@ -31,6 +31,15 @@ DROP VIEW IF EXISTS pic_refs_prep_v;
31
31
 
32
32
  DROP VIEW IF EXISTS pic_refs_base_v;
33
33
 
34
+ DROP VIEW IF EXISTS pic_refs_hvac_ratio_v;
35
+
36
+ DROP VIEW IF EXISTS pic_refs_hvac_v;
37
+
38
+ -- the pre-2026-09-06 names of the two above, for a database that still has them
39
+ DROP VIEW IF EXISTS pic_refs_ratio_v;
40
+
41
+ DROP VIEW IF EXISTS pic_refs_duration_v;
42
+
34
43
  DROP VIEW IF EXISTS pic_refs_v;
35
44
 
36
45
  DROP VIEW IF EXISTS pic_cp_chain_v;
@@ -0,0 +1,26 @@
1
+ -- SELECT
2
+ -- *
3
+ -- FROM
4
+ -- pic_refs_v
5
+ -- ORDER BY
6
+ -- start_date DESC
7
+ -- LIMIT
8
+ -- 10;
9
+ SELECT
10
+ *
11
+ FROM
12
+ positions p
13
+ WHERE
14
+ p.date >= to_timestamp(
15
+ '2026-09-06 10:10:17.521',
16
+ 'YYYY-MM-DD HH24:MI:SS'
17
+ )
18
+ AND p.date <= to_timestamp(
19
+ '2026-09-06 10:33:06.469',
20
+ 'YYYY-MM-DD HH24:MI:SS'
21
+ )
22
+ ORDER BY
23
+ p.date,
24
+ p.id
25
+ LIMIT
26
+ 10;
@@ -36,6 +36,20 @@ DROP VIEW IF EXISTS pic_refs_prep_v;
36
36
 
37
37
  DROP VIEW IF EXISTS pic_refs_base_v;
38
38
 
39
+ DROP VIEW IF EXISTS pic_refs_hvac_ratio_v;
40
+
41
+ DROP VIEW IF EXISTS pic_refs_hvac_v;
42
+
43
+ -- RETIRED by rename, same rule as the group above: these are the names the two
44
+ -- HVAC views carried before 2026-09-06 (pic_refs_duration_v read as "the
45
+ -- duration of refs", which is pic_refs.hours -- a different thing). An existing
46
+ -- database sheds them here; a database that never had them shrugs at the IF
47
+ -- EXISTS. Delete these two lines once no database this code can reach still has
48
+ -- them -- production carries them until the deploy that lands this rename.
49
+ DROP VIEW IF EXISTS pic_refs_ratio_v;
50
+
51
+ DROP VIEW IF EXISTS pic_refs_duration_v;
52
+
39
53
  DROP VIEW IF EXISTS pic_refs_v;
40
54
 
41
55
  DROP VIEW IF EXISTS pic_addresses_v;
package/sql/views.sql CHANGED
@@ -1093,6 +1093,239 @@ WHERE
1093
1093
  -- opening balance anchored.
1094
1094
  AND COALESCE(r.prev_id, 0) >= 0;
1095
1095
 
1096
+ -- Per-ref HVAC duty, measured from the position samples inside the interval.
1097
+ --
1098
+ -- TeslaMate records is_climate_on, the two defrosters and battery_heater on
1099
+ -- every `positions` row as a state, not a duration: the sample says what was
1100
+ -- on at that instant and nothing about how long. So each sample is charged
1101
+ -- with the time until the next sample inside the same ref, and the last one
1102
+ -- runs to the ref's end_date -- a lead() chain of the same shape as
1103
+ -- pic_positions_chain_v, partitioned per ref instead of running over the whole
1104
+ -- table.
1105
+ --
1106
+ -- The same hold is applied at the head: the FIRST sample's span starts at the
1107
+ -- ref's start_date, not at the sample, so its state carries backwards over the
1108
+ -- lead-in exactly as the last sample's carries forwards over the tail. That
1109
+ -- makes pos_total_sec equal to ref_duration_sec by construction, so the
1110
+ -- seconds of one ref sum to its whole duration -- which is what lets them be
1111
+ -- summed across refs at all. Both columns stay, and
1112
+ -- `pos_total_sec <> ref_duration_sec` is then an assertion that this view is
1113
+ -- broken rather than a fact about the data.
1114
+ --
1115
+ -- lead() returns the next sample's date directly rather than its id, and the
1116
+ -- window orders by (date, id) rather than by id alone. Both matter. Ordering
1117
+ -- by id assumes positions.id order matches date order; the table already holds
1118
+ -- rows where it does not, and the failure would be silent -- negative spans
1119
+ -- inside otherwise plausible sums. Taking the date from lead() is what keeps
1120
+ -- the CTE referenced once: a second reference makes Postgres materialise it,
1121
+ -- and a materialised CTE cannot take a predicate, so every lookup of a single
1122
+ -- ref would first join the whole of positions against the whole of pic_refs.
1123
+ --
1124
+ -- The two ref types find their samples differently, which is why this is a
1125
+ -- UNION of two nearly identical halves rather than one join: a drive's
1126
+ -- positions carry its drive_id, a parked one carries none, and folding that
1127
+ -- into a CASE would cost each half its index. 'charge' rides along with 'idle'
1128
+ -- in the second half -- a charging process owns no position rows, but the car
1129
+ -- is parked and still polled inside the window, and those samples are where
1130
+ -- preconditioning and cabin pre-heat show up.
1131
+ --
1132
+ -- A sample missing any of the four flags is skipped rather than read as off:
1133
+ -- TeslaMate leaves them NULL when the car reported nothing, and counting NULL
1134
+ -- as off would silently deflate every share. The skip is a hole in the
1135
+ -- sampling, not in the timeline -- the previous usable sample's state carries
1136
+ -- straight across it. A ref with no usable sample at all does not appear here.
1137
+ --
1138
+ -- Because the hold is applied at both ends, pos_total_sec carries no
1139
+ -- information any more -- it is ref_duration_sec. What the row does not say on
1140
+ -- its own is how much of that duration was actually observed, and a share of a
1141
+ -- ref sampled twice reads exactly like a share of a ref sampled forty times.
1142
+ -- pos_count, pos_first_date and max_gap_sec are that missing signal, and cost
1143
+ -- nothing: they come out of the aggregate that is already running. max_gap_sec
1144
+ -- is the one to filter on -- the longest stretch a single sample was made to
1145
+ -- speak for. On this history it is minutes on a drive and up to two days on an
1146
+ -- idle gap the car slept through.
1147
+ --
1148
+ -- ref_id > 0 AND COALESCE(prev_id, 0) >= 0 is the pair of guards pic_refs_v
1149
+ -- applies, and it takes both: they catch different rows. The per-car seed is
1150
+ -- ref_id = -1 with a NULL prev_id, so only the first one sees it; the idle gap
1151
+ -- trailing the seed carries a positive sequence ref_id and prev_id = -1, so
1152
+ -- only the second one does. Both intervals are constructed -- endpoints
1153
+ -- derived rather than observed -- so a sample inside either would be charged
1154
+ -- with a span that never happened. The seed used to escape this view by being
1155
+ -- ref_type = 'charge'; matching charges (above) is what makes the explicit
1156
+ -- guard necessary, and production had exactly one usable sample sitting in
1157
+ -- that constructed hour.
1158
+ CREATE OR REPLACE VIEW pic_refs_hvac_v AS
1159
+ WITH
1160
+ samples AS (
1161
+ SELECT
1162
+ r.car_id,
1163
+ r.ref_type,
1164
+ r.ref_id,
1165
+ r.start_date AS ref_start_date,
1166
+ r.end_date AS ref_end_date,
1167
+ p.id AS pos_id,
1168
+ p.date,
1169
+ p.is_climate_on,
1170
+ p.is_rear_defroster_on,
1171
+ p.is_front_defroster_on,
1172
+ p.battery_heater
1173
+ FROM
1174
+ pic_refs r
1175
+ INNER JOIN positions p ON p.car_id = r.car_id
1176
+ AND p.drive_id = r.ref_id
1177
+ AND p.date >= r.start_date
1178
+ AND p.date <= r.end_date
1179
+ AND p.is_climate_on IS NOT NULL
1180
+ AND p.is_rear_defroster_on IS NOT NULL
1181
+ AND p.is_front_defroster_on IS NOT NULL
1182
+ AND p.battery_heater IS NOT NULL
1183
+ WHERE
1184
+ r.ref_type = 'drive'
1185
+ AND r.ref_id > 0
1186
+ AND COALESCE(r.prev_id, 0) >= 0
1187
+ UNION ALL
1188
+ SELECT
1189
+ r.car_id,
1190
+ r.ref_type,
1191
+ r.ref_id,
1192
+ r.start_date AS ref_start_date,
1193
+ r.end_date AS ref_end_date,
1194
+ p.id AS pos_id,
1195
+ p.date,
1196
+ p.is_climate_on,
1197
+ p.is_rear_defroster_on,
1198
+ p.is_front_defroster_on,
1199
+ p.battery_heater
1200
+ FROM
1201
+ pic_refs r
1202
+ INNER JOIN positions p ON p.car_id = r.car_id
1203
+ AND p.drive_id IS NULL
1204
+ AND p.date >= r.start_date
1205
+ AND p.date <= r.end_date
1206
+ AND p.is_climate_on IS NOT NULL
1207
+ AND p.is_rear_defroster_on IS NOT NULL
1208
+ AND p.is_front_defroster_on IS NOT NULL
1209
+ AND p.battery_heater IS NOT NULL
1210
+ WHERE
1211
+ r.ref_type IN ('idle', 'charge')
1212
+ AND r.ref_id > 0
1213
+ AND COALESCE(r.prev_id, 0) >= 0
1214
+ ),
1215
+ spans AS (
1216
+ SELECT
1217
+ s.*,
1218
+ EXTRACT(
1219
+ EPOCH
1220
+ FROM
1221
+ COALESCE(lead(s.date) OVER w, s.ref_end_date) - CASE
1222
+ WHEN row_number() OVER w = 1 THEN s.ref_start_date
1223
+ ELSE s.date
1224
+ END
1225
+ ) AS duration_sec
1226
+ FROM
1227
+ samples s
1228
+ WINDOW
1229
+ w AS (
1230
+ PARTITION BY
1231
+ s.car_id,
1232
+ s.ref_type,
1233
+ s.ref_id
1234
+ ORDER BY
1235
+ s.date,
1236
+ s.pos_id
1237
+ )
1238
+ )
1239
+ SELECT
1240
+ s.car_id,
1241
+ s.ref_type,
1242
+ s.ref_id,
1243
+ s.ref_start_date,
1244
+ s.ref_end_date,
1245
+ EXTRACT(
1246
+ EPOCH
1247
+ FROM
1248
+ s.ref_end_date - s.ref_start_date
1249
+ ) AS ref_duration_sec,
1250
+ sum(
1251
+ CASE
1252
+ WHEN s.is_climate_on THEN s.duration_sec
1253
+ ELSE 0
1254
+ END
1255
+ ) AS climate_on_sec,
1256
+ sum(
1257
+ CASE
1258
+ WHEN s.is_rear_defroster_on THEN s.duration_sec
1259
+ ELSE 0
1260
+ END
1261
+ ) AS rear_defroster_sec,
1262
+ sum(
1263
+ CASE
1264
+ WHEN s.is_front_defroster_on THEN s.duration_sec
1265
+ ELSE 0
1266
+ END
1267
+ ) AS front_defroster_sec,
1268
+ sum(
1269
+ CASE
1270
+ WHEN s.battery_heater THEN s.duration_sec
1271
+ ELSE 0
1272
+ END
1273
+ ) AS battery_heater_sec,
1274
+ sum(s.duration_sec) AS pos_total_sec,
1275
+ count(*) AS pos_count,
1276
+ min(s.date) AS pos_first_date,
1277
+ max(s.duration_sec) AS max_gap_sec
1278
+ FROM
1279
+ spans s
1280
+ GROUP BY
1281
+ s.car_id,
1282
+ s.ref_type,
1283
+ s.ref_id,
1284
+ s.ref_start_date,
1285
+ s.ref_end_date;
1286
+
1287
+ -- The same seconds as shares of the ref, one row for one row.
1288
+ --
1289
+ -- The guard is on the denominator, not inside LEAST. Postgres LEAST and
1290
+ -- GREATEST IGNORE their NULL arguments -- unlike Oracle and MySQL, the result
1291
+ -- is NULL only when every argument is -- so the obvious-looking
1292
+ -- LEAST(x / NULLIF(d, 0), 1.0) returns 1.0 for a zero denominator rather than
1293
+ -- NULL, and the row reports climate, both defrosters and the battery heater at
1294
+ -- 100 %. A ratio above 1 in a dashboard reads as data rather than as the
1295
+ -- arithmetic slip it is, and that one reads as a fully-loaded interval.
1296
+ --
1297
+ -- Since pos_total_sec is the ref's whole duration, a zero denominator now
1298
+ -- means a zero-length ref and nothing else. LEAST stays as the cap it was
1299
+ -- meant to be: with the spans tiling the ref exactly, no share can exceed 1
1300
+ -- by construction, so it is there to make a broken sum visible as a clamp
1301
+ -- rather than as a plausible number.
1302
+ CREATE OR REPLACE VIEW pic_refs_hvac_ratio_v AS
1303
+ SELECT
1304
+ b.*,
1305
+ CASE
1306
+ WHEN b.pos_total_sec > 0 THEN round(LEAST(b.climate_on_sec / b.pos_total_sec, 1.0), 6)
1307
+ END AS climate_on_ratio,
1308
+ CASE
1309
+ WHEN b.pos_total_sec > 0 THEN round(
1310
+ LEAST(b.rear_defroster_sec / b.pos_total_sec, 1.0),
1311
+ 6
1312
+ )
1313
+ END AS rear_defroster_ratio,
1314
+ CASE
1315
+ WHEN b.pos_total_sec > 0 THEN round(
1316
+ LEAST(b.front_defroster_sec / b.pos_total_sec, 1.0),
1317
+ 6
1318
+ )
1319
+ END AS front_defroster_ratio,
1320
+ CASE
1321
+ WHEN b.pos_total_sec > 0 THEN round(
1322
+ LEAST(b.battery_heater_sec / b.pos_total_sec, 1.0),
1323
+ 6
1324
+ )
1325
+ END AS battery_heater_ratio
1326
+ FROM
1327
+ pic_refs_hvac_v b;
1328
+
1096
1329
  CREATE OR REPLACE VIEW pic_refs_base_v AS
1097
1330
  WITH
1098
1331
  cp AS (
@@ -1325,7 +1558,38 @@ WITH
1325
1558
  ),
1326
1559
  idle AS (
1327
1560
  SELECT
1328
- *
1561
+ *,
1562
+ -- Rated range that ROSE across a SHORT gap ending in a charge is the
1563
+ -- charging process opening late, not the car standing still, and the
1564
+ -- range belongs to the charge. Measured read-only against production on
1565
+ -- 2026-09-06: range rises after 7 of the 9 drives a charge follows and
1566
+ -- after 1 of the 315 a drive follows, so it is not a post-drive rebound;
1567
+ -- and the real Tesla invoice (pic_invoices.quantity_base) bills energy
1568
+ -- TeslaMate never logged, in the amount of the gap -- adopting it pulls
1569
+ -- billed/logged from as high as 1.264 back into the 1.055-1.089 band
1570
+ -- every gapless charge sits in. See
1571
+ -- docs/plans/2026-09-06_pre-charge-idle-adoption.md.
1572
+ --
1573
+ -- Such a gap is absorbed into the charge by `collection` below rather
1574
+ -- than kept as an idle ref. The bound is what separates it from a
1575
+ -- charging session TeslaMate missed ENTIRELY -- 218.76 km over 47.9 h at
1576
+ -- home -- which pic_build_costs_car prices at the fallback rate off its
1577
+ -- _rebound_km branch and which must never be handed to the next charge's
1578
+ -- invoice: that charge billed 1.70 EUR, and adopting 218.76 km into it
1579
+ -- would price the energy at roughly nothing and under-price every drive
1580
+ -- that drains it. The two populations are three orders of magnitude
1581
+ -- apart -- the longest gap adopted here ran 2.8 min, the nearest one
1582
+ -- excluded ran 47.9 h, and production holds nothing in between -- so the
1583
+ -- bound is a duration, which separates them cleanly, rather than a
1584
+ -- distance, which would have to guess at how fast a charger ramps.
1585
+ --
1586
+ -- The seed charge is out of reach by construction: it is every car's
1587
+ -- first row, so no gap precedes it.
1588
+ (
1589
+ next_type = 'charge'::pic_ref_type
1590
+ AND end_range_km > start_range_km
1591
+ AND sec_to_next <= 600
1592
+ ) AS adopted
1329
1593
  FROM
1330
1594
  chain
1331
1595
  WHERE
@@ -1346,28 +1610,54 @@ WITH
1346
1610
  -- row drops out here: an interval that has not ended yet is not priced.
1347
1611
  sec_to_next > 60
1348
1612
  ),
1349
- collection AS (
1613
+ -- The gaps a charge takes over, keyed by the charge that takes them.
1614
+ adoption AS (
1350
1615
  SELECT
1351
- t AS ref_type,
1352
- id::INTEGER AS ref_id,
1353
1616
  car_id,
1617
+ next_id,
1354
1618
  start_date,
1355
- end_date,
1356
- start_range_km,
1357
- end_range_km,
1619
+ start_range_km
1620
+ FROM
1621
+ idle
1622
+ WHERE
1623
+ adopted
1624
+ ),
1625
+ collection AS (
1626
+ SELECT
1627
+ b.t AS ref_type,
1628
+ b.id::INTEGER AS ref_id,
1629
+ b.car_id,
1630
+ -- An adopted charge begins where the car parked rather than where
1631
+ -- TeslaMate noticed it. A DELIBERATE divergence: for these rows
1632
+ -- start_date and start_range_km no longer echo charging_processes'
1633
+ -- start_date and start_rated_range_km. Both move together on purpose --
1634
+ -- the range the charge gained has to be spread over the time it really
1635
+ -- took, or mean_draw_kw reads high -- and because the absorbed idle no
1636
+ -- longer sits between this row and the drive before it, the two now meet
1637
+ -- exactly: the charge starts at the instant the drive ended, leaving
1638
+ -- neither a gap for the queue to anchor across nor an overlap for
1639
+ -- pic_refs_hvac_v to count a position twice in.
1640
+ COALESCE(a.start_date, b.start_date) AS start_date,
1641
+ b.end_date,
1642
+ COALESCE(a.start_range_km, b.start_range_km) AS start_range_km,
1643
+ b.end_range_km,
1358
1644
  NULL::pic_ref_type AS prev_type,
1359
1645
  NULL::INTEGER AS prev_id,
1360
1646
  NULL::pic_ref_type AS next_type,
1361
1647
  NULL::INTEGER AS next_id,
1362
- start_position_id,
1363
- end_position_id,
1364
- start_address_id,
1365
- end_address_id,
1366
- start_geofence_id,
1367
- end_geofence_id,
1368
- real_km
1648
+ b.start_position_id,
1649
+ b.end_position_id,
1650
+ b.start_address_id,
1651
+ b.end_address_id,
1652
+ b.start_geofence_id,
1653
+ b.end_geofence_id,
1654
+ b.real_km
1369
1655
  FROM
1370
- pic_refs_base_v
1656
+ pic_refs_base_v b
1657
+ -- The type is part of the join: a drive and a charge can share an id.
1658
+ LEFT JOIN adoption a ON a.car_id = b.car_id
1659
+ AND a.next_id = b.id
1660
+ AND b.t = 'charge'::pic_ref_type
1371
1661
  UNION ALL
1372
1662
  SELECT
1373
1663
  'idle'::pic_ref_type AS ref_type,
@@ -1390,6 +1680,8 @@ WITH
1390
1680
  NULL::NUMERIC AS real_km
1391
1681
  FROM
1392
1682
  idle
1683
+ WHERE
1684
+ NOT adopted
1393
1685
  )
1394
1686
  SELECT
1395
1687
  c.*,
@@ -1715,10 +2007,19 @@ DECLARE
1715
2007
  -- pic_energy_ledger_v's cfg used the same constant for a car that had never
1716
2008
  -- charged.
1717
2009
  _base_price CONSTANT NUMERIC := 0.40;
1718
- -- Rated range REBOUNDS after a drive (observed: +16.72 km within 1.3 min).
1719
- -- An idle gain below this is that artefact; a gain above it is energy that
1720
- -- really arrived and TeslaMate never logged. Same threshold the retired
1721
- -- pic_energy_ledger_v used for its rebound_km.
2010
+ -- Rated range rebounds a little after a drive. An idle gain below this is
2011
+ -- that artefact; a gain above it is energy that really arrived and TeslaMate
2012
+ -- never logged. Same threshold the retired pic_energy_ledger_v used for its
2013
+ -- rebound_km.
2014
+ --
2015
+ -- The +16.72 km within 1.3 min this constant was once justified by was NOT a
2016
+ -- rebound: it was cp 18's pre-charge gap, which pic_refs_prep_v now adopts
2017
+ -- into the charge, and measuring the rest of production put the true
2018
+ -- post-drive rebound at 1.29 km or less. So the threshold is uncalibrated
2019
+ -- between roughly 1.3 km and 20 km -- production holds no observation in
2020
+ -- that band. It still earns its keep at the top of the range, where it
2021
+ -- catches the 218.76 km home session TeslaMate slept through, and lowering
2022
+ -- it would only start guessing in the empty middle.
1722
2023
  _rebound_km CONSTANT NUMERIC := 20.0;
1723
2024
  BEGIN
1724
2025
  -- Same lock as pic_build_refs_car: this procedure rewrites cost and
@@ -1,243 +0,0 @@
1
- WITH
2
- base AS (
3
- SELECT
4
- p.car_id,
5
- r.ref_type,
6
- r.ref_id,
7
- r.start_date AS ref_start_date,
8
- r.end_date AS ref_end_date,
9
- p.id AS pos_id,
10
- p.drive_id,
11
- p.is_climate_on,
12
- p.is_rear_defroster_on,
13
- p.is_front_defroster_on,
14
- p.battery_heater
15
- FROM
16
- pic_refs r
17
- LEFT JOIN positions p ON p.car_id = r.car_id
18
- AND p.drive_id = r.ref_id
19
- AND p.date >= r.start_date
20
- AND p.date <= r.end_date
21
- AND p.is_climate_on IS NOT NULL
22
- AND p.is_rear_defroster_on IS NOT NULL
23
- AND p.is_front_defroster_on IS NOT NULL
24
- AND p.battery_heater IS NOT NULL
25
- WHERE
26
- r.ref_type = 'drive'
27
- UNION ALL
28
- SELECT
29
- p.car_id,
30
- r.ref_type,
31
- r.ref_id,
32
- r.start_date AS ref_start_date,
33
- r.end_date AS ref_end_date,
34
- p.id AS pos_id,
35
- p.drive_id,
36
- p.is_climate_on,
37
- p.is_rear_defroster_on,
38
- p.is_front_defroster_on,
39
- p.battery_heater
40
- FROM
41
- pic_refs r
42
- LEFT JOIN positions p ON p.car_id = r.car_id
43
- AND p.drive_id IS NULL
44
- AND p.date >= r.start_date
45
- AND p.date <= r.end_date
46
- AND p.is_climate_on IS NOT NULL
47
- AND p.is_rear_defroster_on IS NOT NULL
48
- AND p.is_front_defroster_on IS NOT NULL
49
- AND p.battery_heater IS NOT NULL
50
- WHERE
51
- r.ref_type = 'idle'
52
- ORDER BY
53
- car_id,
54
- ref_type,
55
- ref_id,
56
- ref_start_date,
57
- ref_end_date
58
- ),
59
- base_chain_ids AS (
60
- SELECT
61
- car_id,
62
- ref_type,
63
- ref_id,
64
- ref_start_date,
65
- ref_end_date,
66
- pos_id,
67
- lead(pos_id) OVER (
68
- PARTITION BY
69
- car_id,
70
- ref_type,
71
- ref_id
72
- ORDER BY
73
- car_id,
74
- ref_type,
75
- ref_id,
76
- ref_start_date,
77
- ref_end_date,
78
- pos_id
79
- ) AS next_pos_id
80
- FROM
81
- base
82
- ORDER BY
83
- car_id,
84
- ref_type,
85
- ref_id,
86
- ref_start_date,
87
- ref_end_date
88
- ),
89
- base_pos AS (
90
- SELECT
91
- b.car_id,
92
- b.ref_type,
93
- b.ref_id,
94
- b.ref_start_date,
95
- b.ref_end_date,
96
- b.pos_id,
97
- b.next_pos_id,
98
- p.date,
99
- p.is_climate_on,
100
- p.is_rear_defroster_on,
101
- p.is_front_defroster_on,
102
- p.drive_id,
103
- p.battery_heater
104
- FROM
105
- base_chain_ids b
106
- INNER JOIN positions p ON p.id = b.pos_id
107
- ),
108
- base_duration AS (
109
- SELECT
110
- b.*,
111
- n.date AS next_date,
112
- EXTRACT(
113
- EPOCH
114
- FROM
115
- n.date
116
- ) - EXTRACT(
117
- EPOCH
118
- FROM
119
- b.date
120
- ) AS duration_sec
121
- FROM
122
- base_pos b
123
- LEFT JOIN base_pos n ON n.car_id = b.car_id
124
- AND n.ref_type = b.ref_type
125
- AND n.ref_id = b.ref_id
126
- AND n.pos_id = b.next_pos_id
127
- ORDER BY
128
- b.car_id,
129
- b.ref_type,
130
- b.ref_id,
131
- b.ref_start_date,
132
- b.ref_end_date,
133
- b.date
134
- ),
135
- condensed AS (
136
- SELECT
137
- b.car_id,
138
- b.ref_type,
139
- b.ref_id,
140
- min(b.ref_start_date) AS ref_start_date,
141
- max(b.ref_end_date) AS ref_end_date,
142
- EXTRACT(
143
- EPOCH
144
- FROM
145
- max(b.ref_end_date)
146
- ) - EXTRACT(
147
- EPOCH
148
- FROM
149
- min(b.ref_start_date)
150
- ) AS ref_duration_sec,
151
- sum(
152
- CASE
153
- WHEN b.is_climate_on THEN b.duration_sec
154
- ELSE 0
155
- END
156
- ) AS climate_on_sec,
157
- sum(
158
- CASE
159
- WHEN b.is_rear_defroster_on THEN b.duration_sec
160
- ELSE 0
161
- END
162
- ) AS rear_defroster_sec,
163
- sum(
164
- CASE
165
- WHEN b.is_front_defroster_on THEN b.duration_sec
166
- ELSE 0
167
- END
168
- ) AS front_defroster_sec,
169
- sum(
170
- CASE
171
- WHEN b.battery_heater THEN b.duration_sec
172
- ELSE 0
173
- END
174
- ) AS battery_heater_sec,
175
- sum(b.duration_sec) AS pos_total_sec
176
- FROM
177
- base_duration b
178
- GROUP BY
179
- b.car_id,
180
- b.ref_type,
181
- b.ref_id
182
- ),
183
- factored AS (
184
- SELECT
185
- b.*,
186
- round(
187
- LEAST(climate_on_sec / NULLIF(pos_total_sec, 0), 1.0),
188
- 6
189
- ) AS climate_on_factor,
190
- round(
191
- LEAST(
192
- rear_defroster_sec / NULLIF(pos_total_sec, 0),
193
- 1.0
194
- ),
195
- 6
196
- ) AS rear_defroster_factor,
197
- round(
198
- LEAST(
199
- front_defroster_sec / NULLIF(pos_total_sec, 0),
200
- 1.0
201
- ),
202
- 6
203
- ) AS front_defroster_factor,
204
- round(
205
- LEAST(
206
- battery_heater_sec / NULLIF(pos_total_sec, 0),
207
- 1.0
208
- ),
209
- 6
210
- ) AS battery_heater_factor
211
- FROM
212
- condensed b
213
- )
214
- SELECT
215
- b.*
216
- -- min(b.pos_total_sec - b.ref_duration_sec) AS min_error_sec
217
- -- max(b.pos_total_sec - b.ref_duration_sec) AS max_error_sec
218
- FROM
219
- factored b
220
- -- SELECT
221
- -- b.car_id,
222
- -- b.ref_type,
223
- -- b.ref_id,
224
- -- round(
225
- -- b.climate_on_count::NUMERIC / b.total_count::NUMERIC,
226
- -- 4
227
- -- ) AS climate_on_ratio,
228
- -- round(
229
- -- b.rear_defroster_count::NUMERIC / b.total_count::NUMERIC,
230
- -- 4
231
- -- ) AS rear_defroster_ratio,
232
- -- round(
233
- -- b.front_defroster_count::NUMERIC / b.total_count::NUMERIC,
234
- -- 4
235
- -- ) AS front_defroster_ratio,
236
- -- round(
237
- -- b.battery_heater_count::NUMERIC / b.total_count::NUMERIC,
238
- -- 4
239
- -- ) AS battery_heater_ratio
240
- -- FROM
241
- -- condensed b
242
- -- LIMIT
243
- -- 2000;