@mxpicture/tesla-superchargers-mate 0.1.39 → 0.1.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxpicture/tesla-superchargers-mate",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "TeslaMate integration",
5
5
  "type": "module",
6
6
  "author": "MXPicture",
@@ -49,7 +49,7 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@mxpicture/tesla-superchargers-generated": "^0.1.39",
52
+ "@mxpicture/tesla-superchargers-generated": "^0.1.41",
53
53
  "postgres": "^3.4.9",
54
54
  "short-uuid": "^6.0.3",
55
55
  "zod": "^4.3.6"
package/sql/reset.sql CHANGED
@@ -2,6 +2,14 @@
2
2
  -- the accumulated price history this project exists to build. Run it only to
3
3
  -- start over from scratch; setup.sql alone is idempotent and safe to
4
4
  -- (re-)apply after updates.
5
+ DROP VIEW IF EXISTS pic_price_history_v;
6
+
7
+ DROP VIEW IF EXISTS pic_price_history_key_v;
8
+
9
+ DROP VIEW IF EXISTS pic_price_bound_v;
10
+
11
+ DROP VIEW IF EXISTS pic_price_fetch_id_v;
12
+
5
13
  DROP VIEW IF EXISTS pic_cp_inv_v;
6
14
 
7
15
  DROP VIEW IF EXISTS pic_cp_inv_prep_v;
package/sql/setup.sql CHANGED
@@ -492,3 +492,163 @@ SELECT
492
492
  base.cp_quan || ' kWh (' || base.cp_cost_raw || ' €)' AS cp
493
493
  FROM
494
494
  pic_cp_inv_prep_v AS base;
495
+
496
+ -- price history ------------------------------------------------------------------------------
497
+ -- Dropped instead of CREATE OR REPLACE for the same reason as the views above,
498
+ -- and because a bare CREATE VIEW made setup.sql non-idempotent (re-applying it
499
+ -- failed with 42P07 "relation already exists"). Dropped in dependency order.
500
+ DROP VIEW IF EXISTS pic_price_history_v;
501
+
502
+ DROP VIEW IF EXISTS pic_price_history_key_v;
503
+
504
+ DROP VIEW IF EXISTS pic_price_bound_v;
505
+
506
+ DROP VIEW IF EXISTS pic_price_fetch_id_v;
507
+
508
+ -- One row per pricebook snapshot ("fetch group") per charger, numbered oldest
509
+ -- first so consecutive snapshots can be chained into validity intervals.
510
+ CREATE VIEW pic_price_fetch_id_v AS
511
+ SELECT
512
+ pic_chargers_id,
513
+ last_fetch_uuid,
514
+ min(fetched_at) AS fetched_at,
515
+ row_number() OVER (
516
+ PARTITION BY
517
+ pic_chargers_id
518
+ ORDER BY
519
+ (min(fetched_at))
520
+ ) AS fetch_id,
521
+ bool_or(is_tou) AS is_tou
522
+ FROM
523
+ pic_pricebooks
524
+ GROUP BY
525
+ pic_chargers_id,
526
+ last_fetch_uuid;
527
+
528
+ -- Validity interval of each snapshot: [fetched_at, end). Dates are resolved in
529
+ -- the charger's own zone, because start_time/end_time are local wall clock
530
+ -- there; deriving them with date_trunc() instead made the whole view depend on
531
+ -- the *client's* session TimeZone (psql and Grafana disagreed on 60 of 109
532
+ -- snapshots).
533
+ CREATE VIEW pic_price_bound_v AS
534
+ SELECT
535
+ base.pic_chargers_id,
536
+ base.last_fetch_uuid AS start_uuid,
537
+ base.fetch_id AS start_id,
538
+ base.fetched_at,
539
+ base.is_tou,
540
+ c.time_zone,
541
+ (base.fetched_at AT TIME ZONE c.time_zone)::date AS fetched_date,
542
+ COALESCE(next.fetched_at, now()) AS "end",
543
+ -- Exclusive day bound. A superseded snapshot ends on the day its successor
544
+ -- was fetched (that day already belongs to the successor). The still-open
545
+ -- snapshot must cover today, so its bound is tomorrow — with "today" the
546
+ -- chart always stopped one day short of now.
547
+ CASE
548
+ WHEN next.fetched_at IS NULL THEN (now() AT TIME ZONE c.time_zone)::date + 1
549
+ ELSE (next.fetched_at AT TIME ZONE c.time_zone)::date
550
+ END AS end_date
551
+ FROM
552
+ pic_price_fetch_id_v base
553
+ JOIN pic_chargers c ON c.id = base.pic_chargers_id
554
+ -- The successor is simply the charger's next snapshot. Matching on is_tou as
555
+ -- well (as this once did) left the interval open whenever a site switched
556
+ -- between flat and time-of-use pricing, so the superseded price ran to now()
557
+ -- and overlapped its replacement.
558
+ LEFT JOIN pic_price_fetch_id_v next ON next.pic_chargers_id = base.pic_chargers_id
559
+ AND next.fetch_id = (base.fetch_id + 1);
560
+
561
+ -- Explodes each validity interval into the calendar days it covers. The day
562
+ -- grid is built from plain dates (not timestamptz) so it cannot gain or lose a
563
+ -- day across a DST boundary.
564
+ CREATE VIEW pic_price_history_key_v AS
565
+ WITH
566
+ span AS (
567
+ SELECT
568
+ min(fetched_date) AS min_date,
569
+ max(end_date) AS max_date
570
+ FROM
571
+ pic_price_bound_v
572
+ ),
573
+ dates AS (
574
+ SELECT
575
+ dd::date AS date
576
+ FROM
577
+ span s,
578
+ generate_series(
579
+ s.min_date::TIMESTAMP,
580
+ s.max_date::TIMESTAMP,
581
+ '1 day'::INTERVAL
582
+ ) dd
583
+ )
584
+ SELECT
585
+ d.date,
586
+ h.pic_chargers_id,
587
+ h.start_uuid AS last_fetch_uuid,
588
+ h.is_tou,
589
+ h.time_zone,
590
+ EXTRACT(
591
+ DOW
592
+ FROM
593
+ d.date
594
+ )::SMALLINT AS day_of_week
595
+ FROM
596
+ dates d
597
+ -- inner join: a date no snapshot covers carried only NULLs and was discarded
598
+ -- downstream anyway
599
+ JOIN pic_price_bound_v h ON h.fetched_date <= d.date
600
+ AND h.end_date > d.date;
601
+
602
+ -- One row per tariff slot per covered day: the point in time a price starts
603
+ -- being charged. A flat-rate charger therefore emits a single point per day (at
604
+ -- 00:00 local) and a time-of-use charger one per slot, so panels must plot this
605
+ -- as a step series and carry the last known value into the window — see
606
+ -- test.sql. timestamp is timestamptz: emitting a naive local wall clock made
607
+ -- Grafana read it as UTC and shift every price change by the charger's offset.
608
+ CREATE VIEW pic_price_history_v AS
609
+ SELECT
610
+ ((k.date + b.start_time) AT TIME ZONE k.time_zone) AS TIMESTAMP,
611
+ k.pic_chargers_id,
612
+ c.location_url_slug,
613
+ k.last_fetch_uuid,
614
+ concat(c.name, ' - ', c.common_site_name) AS "name",
615
+ c.favourite,
616
+ b.uom,
617
+ b.days,
618
+ b.start_time,
619
+ b.end_time,
620
+ b.vehicle_make_type,
621
+ b.fee_type,
622
+ b.is_member_pricebook,
623
+ b.is_tou,
624
+ b.currency_code AS currency_code_original,
625
+ -- only claim EUR when a converted rate actually exists; rate_base_eur is
626
+ -- unpopulated today, so hardcoding 'EUR' would relabel a foreign-currency
627
+ -- rate as euros the moment a non-EUR site is tracked
628
+ CASE
629
+ WHEN b.rate_base_eur IS NOT NULL THEN 'EUR'
630
+ ELSE b.currency_code
631
+ END AS currency_code,
632
+ COALESCE(b.rate_base_eur, b.rate_base) AS rate_base,
633
+ COALESCE(b.rate_tier_1_eur, b.rate_tier_1) AS rate_tier_1,
634
+ COALESCE(b.rate_tier_2_eur, b.rate_tier_2) AS rate_tier_2
635
+ FROM
636
+ pic_price_history_key_v AS k
637
+ INNER JOIN pic_pricebooks AS b ON b.pic_chargers_id = k.pic_chargers_id
638
+ AND b.last_fetch_uuid = k.last_fetch_uuid
639
+ AND b.is_tou = k.is_tou
640
+ INNER JOIN pic_chargers AS c ON c.id = k.pic_chargers_id
641
+ LEFT JOIN unnest(b.days) AS d ON d = k.day_of_week
642
+ WHERE
643
+ b.vehicle_make_type = 'TSLA' -- todo remove and do it in grafan directly
644
+ AND b.fee_type = 'CHARGING' -- todo remove and do it in grafan directly
645
+ AND (
646
+ (
647
+ b.is_tou = TRUE
648
+ AND d IS NOT NULL
649
+ )
650
+ OR (
651
+ b.is_tou = FALSE
652
+ AND d IS NULL
653
+ )
654
+ );