@mxpicture/tesla-superchargers-mate 0.1.62 → 0.1.64
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/migrations/20260907181456_refs_precision_checks.sql +183 -0
- package/db/migrations/20260913164819_additional_costs.sql +61 -0
- package/db/schema.sql +83 -18
- package/dist/store/CarsStore.d.ts +20 -0
- package/dist/store/CarsStore.d.ts.map +1 -0
- package/dist/store/CarsStore.js +20 -0
- package/dist/store/CarsStore.js.map +1 -0
- package/dist/store/PicAdditionalCostsStore.d.ts +60 -0
- package/dist/store/PicAdditionalCostsStore.d.ts.map +1 -0
- package/dist/store/PicAdditionalCostsStore.js +71 -0
- package/dist/store/PicAdditionalCostsStore.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -1
- package/package.json +2 -2
- package/sql/reset.sql +7 -0
- package/sql/test/history1.sql +55 -0
- package/sql/test/history2.sql +181 -0
- package/sql/views.sql +61 -10
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- pic_additional_costs: costs TeslaMate never sees -- service, repairs, tyres,
|
|
2
|
+
-- insurance, tax, subsidies -- entered by hand and attributed to a car and a day.
|
|
3
|
+
--
|
|
4
|
+
-- The first pic_* table whose rows are not derivable: everything else here is a
|
|
5
|
+
-- cache of a feed or of an algorithm and can be rebuilt, these can only be
|
|
6
|
+
-- retyped. No upsert path, no sync, no soft delete.
|
|
7
|
+
--
|
|
8
|
+
-- A recurring cost is ONE row: recurrence = 'monthly' with date as the first
|
|
9
|
+
-- occurrence, end_date as the last (NULL = still running) and cost as the amount
|
|
10
|
+
-- PER MONTH. The future Grafana view expands it with generate_series; nothing
|
|
11
|
+
-- materializes the occurrences.
|
|
12
|
+
-- migrate:up
|
|
13
|
+
CREATE TYPE pic_cost_category AS ENUM(
|
|
14
|
+
'service',
|
|
15
|
+
'repair',
|
|
16
|
+
'tyres',
|
|
17
|
+
'insurance',
|
|
18
|
+
'tax',
|
|
19
|
+
'funding', -- subsidies and quota credits; normally a NEGATIVE cost
|
|
20
|
+
'other' -- escape hatch, so an unforeseen cost never needs a migration
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TYPE pic_cost_recurrence AS ENUM('once', 'monthly');
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS pic_additional_costs (
|
|
26
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
27
|
+
-- SMALLINT and no FK, like pic_refs.car_id: a REFERENCES cars (id) would put
|
|
28
|
+
-- constraint triggers on a TeslaMate table, which this project never writes to.
|
|
29
|
+
car_id SMALLINT NOT NULL,
|
|
30
|
+
category pic_cost_category NOT NULL,
|
|
31
|
+
-- A calendar day, not an instant; for a recurring row, the first occurrence.
|
|
32
|
+
-- Read and written as YYYY-MM-DD text by the store -- see the plan for why.
|
|
33
|
+
date DATE NOT NULL,
|
|
34
|
+
description TEXT NOT NULL,
|
|
35
|
+
-- EUR, like every stored cost here. Negative is allowed: a refund or a
|
|
36
|
+
-- 'funding' credit. On a recurring row this is the amount per occurrence.
|
|
37
|
+
cost NUMERIC(10, 2) NOT NULL,
|
|
38
|
+
recurrence pic_cost_recurrence NOT NULL DEFAULT 'once',
|
|
39
|
+
-- last occurrence of a monthly row; NULL = still running
|
|
40
|
+
end_date DATE,
|
|
41
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
42
|
+
CONSTRAINT pic_additional_costs_description_ck CHECK (btrim(description) <> ''),
|
|
43
|
+
CONSTRAINT pic_additional_costs_end_date_ck CHECK (
|
|
44
|
+
end_date IS NULL
|
|
45
|
+
OR (
|
|
46
|
+
recurrence = 'monthly'
|
|
47
|
+
AND end_date >= date
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- The predicate a per-car cost-over-time panel will filter and order on.
|
|
53
|
+
-- NOT unique: several entries per car and day are explicitly allowed.
|
|
54
|
+
CREATE INDEX IF NOT EXISTS pic_additional_costs_car_date_idx ON pic_additional_costs (car_id, date);
|
|
55
|
+
|
|
56
|
+
-- migrate:down
|
|
57
|
+
DROP TABLE IF EXISTS pic_additional_costs;
|
|
58
|
+
|
|
59
|
+
DROP TYPE IF EXISTS pic_cost_recurrence;
|
|
60
|
+
|
|
61
|
+
DROP TYPE IF EXISTS pic_cost_category;
|
package/db/schema.sql
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
CREATE TYPE public.pic_address_type AS ENUM ('geofence', 'cluster', 'address', 'position');
|
|
13
13
|
|
|
14
|
+
CREATE TYPE public.pic_cost_category AS ENUM ('service', 'repair', 'tyres', 'insurance', 'tax', 'funding', 'other');
|
|
15
|
+
|
|
16
|
+
CREATE TYPE public.pic_cost_recurrence AS ENUM ('once', 'monthly');
|
|
17
|
+
|
|
14
18
|
CREATE TYPE public.pic_ref_type AS ENUM ('drive', 'charge', 'idle');
|
|
15
19
|
|
|
16
20
|
--
|
|
@@ -35,6 +39,39 @@ SET default_tablespace = '';
|
|
|
35
39
|
|
|
36
40
|
SET default_table_access_method = heap;
|
|
37
41
|
|
|
42
|
+
--
|
|
43
|
+
-- Name: pic_additional_costs; Type: TABLE; Schema: public; Owner: -
|
|
44
|
+
--
|
|
45
|
+
|
|
46
|
+
CREATE TABLE public.pic_additional_costs (
|
|
47
|
+
id bigint NOT NULL,
|
|
48
|
+
car_id smallint NOT NULL,
|
|
49
|
+
category public.pic_cost_category NOT NULL,
|
|
50
|
+
date date NOT NULL,
|
|
51
|
+
description text NOT NULL,
|
|
52
|
+
cost numeric(10,2) NOT NULL,
|
|
53
|
+
recurrence public.pic_cost_recurrence DEFAULT 'once'::public.pic_cost_recurrence NOT NULL,
|
|
54
|
+
end_date date,
|
|
55
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
56
|
+
CONSTRAINT pic_additional_costs_description_ck CHECK ((btrim(description) <> ''::text)),
|
|
57
|
+
CONSTRAINT pic_additional_costs_end_date_ck CHECK (((end_date IS NULL) OR ((recurrence = 'monthly'::public.pic_cost_recurrence) AND (end_date >= date))))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
--
|
|
62
|
+
-- Name: pic_additional_costs_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
|
63
|
+
--
|
|
64
|
+
|
|
65
|
+
ALTER TABLE public.pic_additional_costs ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
|
|
66
|
+
SEQUENCE NAME public.pic_additional_costs_id_seq
|
|
67
|
+
START WITH 1
|
|
68
|
+
INCREMENT BY 1
|
|
69
|
+
NO MINVALUE
|
|
70
|
+
NO MAXVALUE
|
|
71
|
+
CACHE 1
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
|
|
38
75
|
--
|
|
39
76
|
-- Name: pic_zip_country; Type: TABLE; Schema: public; Owner: -
|
|
40
77
|
--
|
|
@@ -415,17 +452,19 @@ CREATE TABLE public.pic_refs (
|
|
|
415
452
|
real_km numeric(8,4),
|
|
416
453
|
used_cp_ids integer[],
|
|
417
454
|
cost numeric(6,2),
|
|
418
|
-
hours numeric GENERATED ALWAYS AS (round((EXTRACT(epoch FROM (end_date - start_date)) / 3600.0),
|
|
419
|
-
mean_draw_kw numeric GENERATED ALWAYS AS (round((((start_range_km - end_range_km) *
|
|
455
|
+
hours numeric GENERATED ALWAYS AS (round((EXTRACT(epoch FROM (end_date - start_date)) / 3600.0), 6)),
|
|
456
|
+
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
457
|
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) *
|
|
422
|
-
kwh_per_real_1km numeric GENERATED ALWAYS AS (round((((start_range_km - end_range_km) *
|
|
458
|
+
dropped_kwh numeric GENERATED ALWAYS AS (((start_range_km - end_range_km) * efficiency)),
|
|
459
|
+
kwh_per_real_1km numeric GENERATED ALWAYS AS (round((((start_range_km - end_range_km) * efficiency) / NULLIF(real_km, (0)::numeric)), 6)),
|
|
423
460
|
cost_per_real_100km numeric GENERATED ALWAYS AS (round(((cost * (100)::numeric) / NULLIF(real_km, (0)::numeric)), 2)),
|
|
424
461
|
cost_per_kwh numeric GENERATED ALWAYS AS (
|
|
425
462
|
CASE
|
|
426
463
|
WHEN (start_range_km < end_range_km) THEN NULL::numeric
|
|
427
|
-
ELSE round((cost / NULLIF(((start_range_km - end_range_km) *
|
|
428
|
-
END)
|
|
464
|
+
ELSE round((cost / NULLIF(((start_range_km - end_range_km) * efficiency), (0)::numeric)), 4)
|
|
465
|
+
END),
|
|
466
|
+
CONSTRAINT pic_refs_date_order CHECK ((end_date >= start_date)),
|
|
467
|
+
CONSTRAINT pic_refs_efficiency_positive CHECK ((efficiency > (0)::numeric))
|
|
429
468
|
);
|
|
430
469
|
|
|
431
470
|
|
|
@@ -534,6 +573,14 @@ CREATE UNLOGGED TABLE public.pic_zip_import (
|
|
|
534
573
|
);
|
|
535
574
|
|
|
536
575
|
|
|
576
|
+
--
|
|
577
|
+
-- Name: pic_additional_costs pic_additional_costs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
|
578
|
+
--
|
|
579
|
+
|
|
580
|
+
ALTER TABLE ONLY public.pic_additional_costs
|
|
581
|
+
ADD CONSTRAINT pic_additional_costs_pkey PRIMARY KEY (id);
|
|
582
|
+
|
|
583
|
+
|
|
537
584
|
--
|
|
538
585
|
-- Name: pic_chargers pic_chargers_location_guid_key; Type: CONSTRAINT; Schema: public; Owner: -
|
|
539
586
|
--
|
|
@@ -678,6 +725,13 @@ ALTER TABLE ONLY public.pic_zip_country
|
|
|
678
725
|
ADD CONSTRAINT pic_zip_country_pkey PRIMARY KEY (country_code);
|
|
679
726
|
|
|
680
727
|
|
|
728
|
+
--
|
|
729
|
+
-- Name: pic_additional_costs_car_date_idx; Type: INDEX; Schema: public; Owner: -
|
|
730
|
+
--
|
|
731
|
+
|
|
732
|
+
CREATE INDEX pic_additional_costs_car_date_idx ON public.pic_additional_costs USING btree (car_id, date);
|
|
733
|
+
|
|
734
|
+
|
|
681
735
|
--
|
|
682
736
|
-- Name: pic_charg_proc_link_uq_idx; Type: INDEX; Schema: public; Owner: -
|
|
683
737
|
--
|
|
@@ -2669,17 +2723,17 @@ CREATE VIEW public.pic_refs_v AS
|
|
|
2669
2723
|
ELSE r.cost
|
|
2670
2724
|
END AS dropped_cost,
|
|
2671
2725
|
CASE
|
|
2672
|
-
WHEN (r.ref_type = 'charge'::public.pic_ref_type) THEN (
|
|
2673
|
-
ELSE
|
|
2726
|
+
WHEN (r.ref_type = 'charge'::public.pic_ref_type) THEN (- s.gross_cost)
|
|
2727
|
+
ELSE s.gross_cost
|
|
2674
2728
|
END AS gross_dropped_cost,
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2729
|
+
s.gross_cost,
|
|
2730
|
+
s.gross_hours,
|
|
2731
|
+
g.gross_mean_draw_kw,
|
|
2732
|
+
s.gross_dropped_km,
|
|
2733
|
+
s.gross_dropped_kwh,
|
|
2734
|
+
g.gross_kwh_per_real_1km,
|
|
2735
|
+
g.gross_cost_per_real_100km,
|
|
2736
|
+
g.gross_cost_per_kwh,
|
|
2683
2737
|
sm.seed_address_id AS start_seed_address_id,
|
|
2684
2738
|
em.seed_address_id AS end_seed_address_id,
|
|
2685
2739
|
sm.cluster_id AS start_cluster_id,
|
|
@@ -2700,8 +2754,8 @@ CREATE VIEW public.pic_refs_v AS
|
|
|
2700
2754
|
END AS end_address_type,
|
|
2701
2755
|
COALESCE(sg.name, (sa.address)::character varying, (sa_fallback.address)::character varying) AS start_address,
|
|
2702
2756
|
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))))
|
|
2757
|
+
FROM (((((((((((public.pic_refs r
|
|
2758
|
+
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
2759
|
LEFT JOIN public.pic_cluster_map_v sm ON ((sm.address_id = r.start_address_id)))
|
|
2706
2760
|
LEFT JOIN public.pic_cluster_map_v em ON ((em.address_id = r.end_address_id)))
|
|
2707
2761
|
LEFT JOIN public.pic_addresses_v sa ON ((sa.id = sm.seed_address_id)))
|
|
@@ -2710,6 +2764,17 @@ CREATE VIEW public.pic_refs_v AS
|
|
|
2710
2764
|
LEFT JOIN public.pic_addresses_v ea_fallback ON ((ea_fallback.id = r.end_address_id)))
|
|
2711
2765
|
LEFT JOIN public.geofences sg ON ((sg.id = r.start_geofence_id)))
|
|
2712
2766
|
LEFT JOIN public.geofences eg ON ((eg.id = r.end_geofence_id)))
|
|
2767
|
+
CROSS JOIN LATERAL ( SELECT (r.cost + COALESCE(i.cost, 0.0)) AS gross_cost,
|
|
2768
|
+
(r.hours + COALESCE(i.hours, 0.0)) AS gross_hours,
|
|
2769
|
+
(r.dropped_km + COALESCE(i.dropped_km, 0.0)) AS gross_dropped_km,
|
|
2770
|
+
(r.dropped_kwh + COALESCE(i.dropped_kwh, 0.0)) AS gross_dropped_kwh) s)
|
|
2771
|
+
CROSS JOIN LATERAL ( SELECT round((s.gross_dropped_kwh / NULLIF(s.gross_hours, (0)::numeric)), 3) AS gross_mean_draw_kw,
|
|
2772
|
+
round((s.gross_dropped_kwh / NULLIF(r.real_km, (0)::numeric)), 6) AS gross_kwh_per_real_1km,
|
|
2773
|
+
round(((s.gross_cost * (100)::numeric) / NULLIF(r.real_km, (0)::numeric)), 2) AS gross_cost_per_real_100km,
|
|
2774
|
+
CASE
|
|
2775
|
+
WHEN (s.gross_dropped_kwh <= (0)::numeric) THEN NULL::numeric
|
|
2776
|
+
ELSE round((s.gross_cost / s.gross_dropped_kwh), 4)
|
|
2777
|
+
END AS gross_cost_per_kwh) g)
|
|
2713
2778
|
WHERE ((r.ref_id > 0) AND (COALESCE(r.prev_id, 0) >= 0));
|
|
2714
2779
|
|
|
2715
2780
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Cars, type CarsId, type CarsInitializer, type CarsMutator } from "@mxpicture/tesla-superchargers-generated/public";
|
|
2
|
+
import { IMateStore } from "./IMateStore.js";
|
|
3
|
+
import { type SqlTx } from "./db.js";
|
|
4
|
+
/** A car as a picker renders it; `name` is nullable in TeslaMate's own table. */
|
|
5
|
+
export interface CarOption {
|
|
6
|
+
id: CarsId;
|
|
7
|
+
name: string | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read access to TeslaMate's `cars` table. Only {@link CarsStore.queryNames}
|
|
11
|
+
* today: the additional-costs form needs a car dropdown, and with no FK on
|
|
12
|
+
* `pic_additional_costs.car_id` a free-text number would book a cost against a
|
|
13
|
+
* car that may not exist. Read-only, like every TeslaMate table here.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CarsStore extends IMateStore<CarsId, Cars, CarsInitializer, CarsMutator> {
|
|
16
|
+
constructor();
|
|
17
|
+
queryNames(tx?: SqlTx): Promise<CarOption[]>;
|
|
18
|
+
}
|
|
19
|
+
export declare const carsStore: () => CarsStore;
|
|
20
|
+
//# sourceMappingURL=CarsStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CarsStore.d.ts","sourceRoot":"","sources":["../../src/store/CarsStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,IAAI,EACT,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,KAAK,WAAW,EACjB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAO,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAE1C,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,UAAU,CACvC,MAAM,EACN,IAAI,EACJ,eAAe,EACf,WAAW,CACZ;;IAKc,UAAU,CAAC,EAAE,GAAE,KAAa,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;CAGjE;AAGD,eAAO,MAAM,SAAS,iBAAqC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { carsInitializer, carsMutator, } from "@mxpicture/tesla-superchargers-generated/public";
|
|
2
|
+
import { IMateStore } from "./IMateStore.js";
|
|
3
|
+
import { sql } from "./db.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read access to TeslaMate's `cars` table. Only {@link CarsStore.queryNames}
|
|
6
|
+
* today: the additional-costs form needs a car dropdown, and with no FK on
|
|
7
|
+
* `pic_additional_costs.car_id` a free-text number would book a cost against a
|
|
8
|
+
* car that may not exist. Read-only, like every TeslaMate table here.
|
|
9
|
+
*/
|
|
10
|
+
export class CarsStore extends IMateStore {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("cars", carsInitializer, carsMutator);
|
|
13
|
+
}
|
|
14
|
+
async queryNames(tx = sql()) {
|
|
15
|
+
return await tx `SELECT id, name FROM cars ORDER BY id`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
let _store = null;
|
|
19
|
+
export const carsStore = () => (_store ??= new CarsStore());
|
|
20
|
+
//# sourceMappingURL=CarsStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CarsStore.js","sourceRoot":"","sources":["../../src/store/CarsStore.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,WAAW,GAKZ,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,GAAG,EAAc,MAAM,SAAS,CAAC;AAQ1C;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,UAK9B;IACC;QACE,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC;IAC9C,CAAC;IAEM,KAAK,CAAC,UAAU,CAAC,KAAY,GAAG,EAAE;QACvC,OAAO,MAAM,EAAE,CAAa,uCAAuC,CAAC;IACtE,CAAC;CACF;AAED,IAAI,MAAM,GAAqB,IAAI,CAAC;AACpC,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PicAdditionalCostsId, PicCostCategory, PicCostRecurrence } from "@mxpicture/tesla-superchargers-generated/public";
|
|
2
|
+
import { type SqlTx } from "./db.js";
|
|
3
|
+
/**
|
|
4
|
+
* One stored entry. Both dates are `YYYY-MM-DD` and `cost` a NUMERIC string,
|
|
5
|
+
* exactly as they leave Postgres.
|
|
6
|
+
*/
|
|
7
|
+
export interface AdditionalCost {
|
|
8
|
+
id: PicAdditionalCostsId;
|
|
9
|
+
car_id: number;
|
|
10
|
+
category: PicCostCategory;
|
|
11
|
+
date: string;
|
|
12
|
+
description: string;
|
|
13
|
+
cost: string;
|
|
14
|
+
recurrence: PicCostRecurrence;
|
|
15
|
+
end_date: string | null;
|
|
16
|
+
created_at: Date;
|
|
17
|
+
}
|
|
18
|
+
/** A full row as the UI edits it; `id` and `created_at` are the store's. */
|
|
19
|
+
export interface AdditionalCostInput {
|
|
20
|
+
car_id: number;
|
|
21
|
+
category: PicCostCategory;
|
|
22
|
+
/** YYYY-MM-DD */
|
|
23
|
+
date: string;
|
|
24
|
+
description: string;
|
|
25
|
+
cost: string;
|
|
26
|
+
recurrence: PicCostRecurrence;
|
|
27
|
+
/** YYYY-MM-DD, or null on a one-off row and an open-ended monthly one */
|
|
28
|
+
end_date: string | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `pic_additional_costs` — the costs TeslaMate never sees, typed in through
|
|
32
|
+
* the web UI. Full CRUD, because nothing else creates these rows: they are the
|
|
33
|
+
* only `pic_*` rows that cannot be rebuilt from a feed or an algorithm.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately NOT an {@link IMateStore}, for the same reason
|
|
36
|
+
* {@link PicZipCodesStore} is not: the base class reads with `SELECT *` and
|
|
37
|
+
* writes through the generated zod schemas, and both put a JS `Date` on the
|
|
38
|
+
* path of a `DATE` column. postgres.js parses `DATE` (OID 1082) into a `Date`
|
|
39
|
+
* at *local* midnight, which `JSON.stringify` then emits as the previous day
|
|
40
|
+
* anywhere east of UTC; writing one back casts a timestamptz to `date` in the
|
|
41
|
+
* *session* zone, which is not the process's. So this store is the boundary
|
|
42
|
+
* and it speaks `YYYY-MM-DD` strings: every read projects `date::TEXT`, every
|
|
43
|
+
* write binds `${…}::DATE`, and no JS `Date` ever exists. Overriding all four
|
|
44
|
+
* inherited methods to do that would leave nothing inherited.
|
|
45
|
+
*/
|
|
46
|
+
export declare class PicAdditionalCostsStore {
|
|
47
|
+
readonly table = "pic_additional_costs";
|
|
48
|
+
/** Newest day first; ties broken by insertion order, newest first. */
|
|
49
|
+
query(tx?: SqlTx): Promise<AdditionalCost[]>;
|
|
50
|
+
insert(input: AdditionalCostInput, tx?: SqlTx): Promise<AdditionalCost>;
|
|
51
|
+
/**
|
|
52
|
+
* Replace every editable column of one row — the UI edits a row as a whole,
|
|
53
|
+
* so there is no patch path. Null when no row has that id.
|
|
54
|
+
*/
|
|
55
|
+
update(id: PicAdditionalCostsId, input: AdditionalCostInput, tx?: SqlTx): Promise<AdditionalCost | null>;
|
|
56
|
+
/** False when the row was already gone — deleting twice is not an error. */
|
|
57
|
+
remove(id: PicAdditionalCostsId, tx?: SqlTx): Promise<boolean>;
|
|
58
|
+
}
|
|
59
|
+
export declare const picAdditionalCostsStore: () => PicAdditionalCostsStore;
|
|
60
|
+
//# sourceMappingURL=PicAdditionalCostsStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PicAdditionalCostsStore.d.ts","sourceRoot":"","sources":["../../src/store/PicAdditionalCostsStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EAClB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAO,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAE1C;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,oBAAoB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,iBAAiB,CAAC;IAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,eAAe,CAAC;IAC1B,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,iBAAiB,CAAC;IAC9B,yEAAyE;IACzE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,uBAAuB;IAClC,SAAgB,KAAK,0BAA0B;IAE/C,sEAAsE;IACzD,KAAK,CAAC,EAAE,GAAE,KAAa,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IASnD,MAAM,CACjB,KAAK,EAAE,mBAAmB,EAC1B,EAAE,GAAE,KAAa,GAChB,OAAO,CAAC,cAAc,CAAC;IAe1B;;;OAGG;IACU,MAAM,CACjB,EAAE,EAAE,oBAAoB,EACxB,KAAK,EAAE,mBAAmB,EAC1B,EAAE,GAAE,KAAa,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAiBjC,4EAA4E;IAC/D,MAAM,CACjB,EAAE,EAAE,oBAAoB,EACxB,EAAE,GAAE,KAAa,GAChB,OAAO,CAAC,OAAO,CAAC;CAIpB;AAGD,eAAO,MAAM,uBAAuB,+BACQ,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { sql } from "./db.js";
|
|
2
|
+
/**
|
|
3
|
+
* `pic_additional_costs` — the costs TeslaMate never sees, typed in through
|
|
4
|
+
* the web UI. Full CRUD, because nothing else creates these rows: they are the
|
|
5
|
+
* only `pic_*` rows that cannot be rebuilt from a feed or an algorithm.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately NOT an {@link IMateStore}, for the same reason
|
|
8
|
+
* {@link PicZipCodesStore} is not: the base class reads with `SELECT *` and
|
|
9
|
+
* writes through the generated zod schemas, and both put a JS `Date` on the
|
|
10
|
+
* path of a `DATE` column. postgres.js parses `DATE` (OID 1082) into a `Date`
|
|
11
|
+
* at *local* midnight, which `JSON.stringify` then emits as the previous day
|
|
12
|
+
* anywhere east of UTC; writing one back casts a timestamptz to `date` in the
|
|
13
|
+
* *session* zone, which is not the process's. So this store is the boundary
|
|
14
|
+
* and it speaks `YYYY-MM-DD` strings: every read projects `date::TEXT`, every
|
|
15
|
+
* write binds `${…}::DATE`, and no JS `Date` ever exists. Overriding all four
|
|
16
|
+
* inherited methods to do that would leave nothing inherited.
|
|
17
|
+
*/
|
|
18
|
+
export class PicAdditionalCostsStore {
|
|
19
|
+
table = "pic_additional_costs";
|
|
20
|
+
/** Newest day first; ties broken by insertion order, newest first. */
|
|
21
|
+
async query(tx = sql()) {
|
|
22
|
+
return await tx `
|
|
23
|
+
SELECT
|
|
24
|
+
id, car_id, category, date::TEXT AS date, description, cost,
|
|
25
|
+
recurrence, end_date::TEXT AS end_date, created_at
|
|
26
|
+
FROM pic_additional_costs
|
|
27
|
+
ORDER BY date DESC, id DESC`;
|
|
28
|
+
}
|
|
29
|
+
async insert(input, tx = sql()) {
|
|
30
|
+
const [row] = await tx `
|
|
31
|
+
INSERT INTO pic_additional_costs (
|
|
32
|
+
car_id, category, date, description, cost, recurrence, end_date
|
|
33
|
+
) VALUES (
|
|
34
|
+
${input.car_id}, ${input.category}, ${input.date}::DATE,
|
|
35
|
+
${input.description}, ${input.cost}, ${input.recurrence},
|
|
36
|
+
${input.end_date}::DATE
|
|
37
|
+
)
|
|
38
|
+
RETURNING
|
|
39
|
+
id, car_id, category, date::TEXT AS date, description, cost,
|
|
40
|
+
recurrence, end_date::TEXT AS end_date, created_at`;
|
|
41
|
+
return row;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Replace every editable column of one row — the UI edits a row as a whole,
|
|
45
|
+
* so there is no patch path. Null when no row has that id.
|
|
46
|
+
*/
|
|
47
|
+
async update(id, input, tx = sql()) {
|
|
48
|
+
const rows = await tx `
|
|
49
|
+
UPDATE pic_additional_costs SET
|
|
50
|
+
car_id = ${input.car_id},
|
|
51
|
+
category = ${input.category},
|
|
52
|
+
date = ${input.date}::DATE,
|
|
53
|
+
description = ${input.description},
|
|
54
|
+
cost = ${input.cost},
|
|
55
|
+
recurrence = ${input.recurrence},
|
|
56
|
+
end_date = ${input.end_date}::DATE
|
|
57
|
+
WHERE id = ${id}
|
|
58
|
+
RETURNING
|
|
59
|
+
id, car_id, category, date::TEXT AS date, description, cost,
|
|
60
|
+
recurrence, end_date::TEXT AS end_date, created_at`;
|
|
61
|
+
return rows[0] ?? null;
|
|
62
|
+
}
|
|
63
|
+
/** False when the row was already gone — deleting twice is not an error. */
|
|
64
|
+
async remove(id, tx = sql()) {
|
|
65
|
+
const result = await tx `DELETE FROM pic_additional_costs WHERE id = ${id}`;
|
|
66
|
+
return result.count > 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let _store = null;
|
|
70
|
+
export const picAdditionalCostsStore = () => (_store ??= new PicAdditionalCostsStore());
|
|
71
|
+
//# sourceMappingURL=PicAdditionalCostsStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PicAdditionalCostsStore.js","sourceRoot":"","sources":["../../src/store/PicAdditionalCostsStore.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAc,MAAM,SAAS,CAAC;AA+B1C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,uBAAuB;IAClB,KAAK,GAAG,sBAAsB,CAAC;IAE/C,sEAAsE;IAC/D,KAAK,CAAC,KAAK,CAAC,KAAY,GAAG,EAAE;QAClC,OAAO,MAAM,EAAE,CAAkB;;;;;kCAKH,CAAC;IACjC,CAAC;IAEM,KAAK,CAAC,MAAM,CACjB,KAA0B,EAC1B,KAAY,GAAG,EAAE;QAEjB,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,CAAkB;;;;UAIlC,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,QAAQ,KAAK,KAAK,CAAC,IAAI;UAC9C,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,UAAU;UACrD,KAAK,CAAC,QAAQ;;;;2DAImC,CAAC;QACxD,OAAO,GAAG,CAAC;IACb,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,MAAM,CACjB,EAAwB,EACxB,KAA0B,EAC1B,KAAY,GAAG,EAAE;QAEjB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAkB;;mBAExB,KAAK,CAAC,MAAM;qBACV,KAAK,CAAC,QAAQ;iBAClB,KAAK,CAAC,IAAI;wBACH,KAAK,CAAC,WAAW;iBACxB,KAAK,CAAC,IAAI;uBACJ,KAAK,CAAC,UAAU;qBAClB,KAAK,CAAC,QAAQ;mBAChB,EAAE;;;2DAGsC,CAAC;QACxD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACzB,CAAC;IAED,4EAA4E;IACrE,KAAK,CAAC,MAAM,CACjB,EAAwB,EACxB,KAAY,GAAG,EAAE;QAEjB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAA,+CAA+C,EAAE,EAAE,CAAC;QAC3E,OAAO,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC;IAC1B,CAAC;CACF;AAED,IAAI,MAAM,GAAmC,IAAI,CAAC;AAClD,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,EAAE,CAC1C,CAAC,MAAM,KAAK,IAAI,uBAAuB,EAAE,CAAC,CAAC"}
|
package/dist/store/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
export * from "./CarsStore.js";
|
|
1
2
|
export * from "./ChargingProcessesStore.js";
|
|
2
3
|
export * from "./GeofencesStore.js";
|
|
3
4
|
export * from "./IFailableStore.js";
|
|
4
5
|
export * from "./IMateStore.js";
|
|
6
|
+
export * from "./PicAdditionalCostsStore.js";
|
|
5
7
|
export * from "./PicCPLinksStore.js";
|
|
6
8
|
export * from "./PicChargersStore.js";
|
|
7
9
|
export * from "./PicInvoiceFilesStore.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AACA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AACA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
|
package/dist/store/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// This file is auto-generated by build-cli. Do not edit manually.
|
|
2
|
+
export * from "./CarsStore.js";
|
|
2
3
|
export * from "./ChargingProcessesStore.js";
|
|
3
4
|
export * from "./GeofencesStore.js";
|
|
4
5
|
export * from "./IFailableStore.js";
|
|
5
6
|
export * from "./IMateStore.js";
|
|
7
|
+
export * from "./PicAdditionalCostsStore.js";
|
|
6
8
|
export * from "./PicCPLinksStore.js";
|
|
7
9
|
export * from "./PicChargersStore.js";
|
|
8
10
|
export * from "./PicInvoiceFilesStore.js";
|
package/dist/store/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mxpicture/tesla-superchargers-mate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.64",
|
|
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.
|
|
53
|
+
"@mxpicture/tesla-superchargers-generated": "^0.1.64",
|
|
54
54
|
"dbmate": "2.35.1",
|
|
55
55
|
"postgres": "^3.4.9",
|
|
56
56
|
"short-uuid": "^6.0.3",
|
package/sql/reset.sql
CHANGED
|
@@ -115,6 +115,13 @@ DROP PROCEDURE IF EXISTS pic_build_refs_car (SMALLINT);
|
|
|
115
115
|
|
|
116
116
|
DROP PROCEDURE IF EXISTS pic_build_costs_car (SMALLINT);
|
|
117
117
|
|
|
118
|
+
DROP TABLE IF EXISTS pic_additional_costs;
|
|
119
|
+
|
|
120
|
+
-- After the table: both enums are only ever used by its columns.
|
|
121
|
+
DROP TYPE IF EXISTS pic_cost_recurrence;
|
|
122
|
+
|
|
123
|
+
DROP TYPE IF EXISTS pic_cost_category;
|
|
124
|
+
|
|
118
125
|
DROP TABLE IF EXISTS pic_refs;
|
|
119
126
|
|
|
120
127
|
DROP SEQUENCE IF EXISTS pic_refs_idle_id;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
WITH
|
|
2
|
+
base AS (
|
|
3
|
+
SELECT
|
|
4
|
+
r.car_id,
|
|
5
|
+
r.ref_type,
|
|
6
|
+
sum(
|
|
7
|
+
EXTRACT(
|
|
8
|
+
EPOCH
|
|
9
|
+
FROM
|
|
10
|
+
r.end_date - r.start_date
|
|
11
|
+
)
|
|
12
|
+
) AS duration_sec,
|
|
13
|
+
sum(r.real_km) AS real_km,
|
|
14
|
+
sum(r.start_range_km - r.end_range_km) AS dropped_km,
|
|
15
|
+
sum(r.cost) AS cost
|
|
16
|
+
FROM
|
|
17
|
+
pic_refs r
|
|
18
|
+
WHERE
|
|
19
|
+
r.car_id = 1
|
|
20
|
+
-- AND $__timeFilter(r.start_date)
|
|
21
|
+
GROUP BY
|
|
22
|
+
r.car_id,
|
|
23
|
+
r.ref_type
|
|
24
|
+
)
|
|
25
|
+
SELECT
|
|
26
|
+
r.*,
|
|
27
|
+
(
|
|
28
|
+
round(
|
|
29
|
+
(r.dropped_km * c.efficiency)::NUMERIC / NULLIF(r.duration_sec / 3600.0, (0)::NUMERIC),
|
|
30
|
+
3
|
|
31
|
+
)
|
|
32
|
+
) AS mean_draw_kw
|
|
33
|
+
-- kwh_per_real_1km
|
|
34
|
+
-- cost_per_real_100km
|
|
35
|
+
-- cost_per_kwh
|
|
36
|
+
-- floor(
|
|
37
|
+
-- extract(
|
|
38
|
+
-- epoch
|
|
39
|
+
-- FROM
|
|
40
|
+
-- r.start_date
|
|
41
|
+
-- )
|
|
42
|
+
-- ) * 1000 AS start_date_ts,
|
|
43
|
+
-- ceil(
|
|
44
|
+
-- extract(
|
|
45
|
+
-- epoch
|
|
46
|
+
-- FROM
|
|
47
|
+
-- r.end_date
|
|
48
|
+
-- )
|
|
49
|
+
-- ) * 1000 AS end_date_ts
|
|
50
|
+
FROM
|
|
51
|
+
base r
|
|
52
|
+
INNER JOIN cars c ON c.id = r.car_id;
|
|
53
|
+
|
|
54
|
+
-- ORDER BY
|
|
55
|
+
-- r.start_date DESC;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
WITH
|
|
2
|
+
base AS (
|
|
3
|
+
SELECT
|
|
4
|
+
*
|
|
5
|
+
FROM
|
|
6
|
+
pic_refs_v
|
|
7
|
+
WHERE
|
|
8
|
+
car_id = 1
|
|
9
|
+
-- car_id = $car_id
|
|
10
|
+
-- AND $__timeFilter(start_date)
|
|
11
|
+
),
|
|
12
|
+
latest_base AS (
|
|
13
|
+
SELECT
|
|
14
|
+
*
|
|
15
|
+
FROM
|
|
16
|
+
pic_refs_v b
|
|
17
|
+
WHERE
|
|
18
|
+
car_id = 1
|
|
19
|
+
-- car_id = $car_id
|
|
20
|
+
ORDER BY
|
|
21
|
+
start_date DESC,
|
|
22
|
+
end_date DESC
|
|
23
|
+
LIMIT
|
|
24
|
+
1
|
|
25
|
+
),
|
|
26
|
+
latest_selected AS (
|
|
27
|
+
SELECT
|
|
28
|
+
car_id,
|
|
29
|
+
ref_id,
|
|
30
|
+
ref_type
|
|
31
|
+
FROM
|
|
32
|
+
pic_refs_v b
|
|
33
|
+
WHERE
|
|
34
|
+
car_id = 1
|
|
35
|
+
-- car_id = $car_id
|
|
36
|
+
-- AND $__timeFilter(start_date)
|
|
37
|
+
AND ref_type = 'drive'
|
|
38
|
+
ORDER BY
|
|
39
|
+
start_date DESC,
|
|
40
|
+
end_date DESC
|
|
41
|
+
LIMIT
|
|
42
|
+
1
|
|
43
|
+
),
|
|
44
|
+
latest AS (
|
|
45
|
+
SELECT
|
|
46
|
+
b.car_id,
|
|
47
|
+
'idle'::pic_ref_type AS ref_type,
|
|
48
|
+
(-100)::INTEGER AS ref_id,
|
|
49
|
+
end_date AS start_date,
|
|
50
|
+
(
|
|
51
|
+
SELECT
|
|
52
|
+
now()
|
|
53
|
+
) AS end_date,
|
|
54
|
+
b.end_address AS start_address,
|
|
55
|
+
b.end_address,
|
|
56
|
+
NULL:NUMERIC AS real_km,
|
|
57
|
+
b.dropped_kwh,
|
|
58
|
+
b.gross_dropped_kwh,
|
|
59
|
+
b.dropped_cost,
|
|
60
|
+
b.gross_dropped_cost,
|
|
61
|
+
b.cost_per_real_100km,
|
|
62
|
+
b.gross_cost_per_real_100km,
|
|
63
|
+
b.kwh_per_real_1km,
|
|
64
|
+
b.gross_kwh_per_real_1km,
|
|
65
|
+
b.cost_per_kwh,
|
|
66
|
+
b.gross_cost_per_kwh
|
|
67
|
+
FROM
|
|
68
|
+
latest_base b
|
|
69
|
+
INNER JOIN latest_selected s ON s.car_id = b.car_id
|
|
70
|
+
AND s.ref_type = b.ref_type
|
|
71
|
+
AND s.ref_id = b.ref_id
|
|
72
|
+
LIMIT
|
|
73
|
+
1
|
|
74
|
+
),
|
|
75
|
+
res AS (
|
|
76
|
+
SELECT
|
|
77
|
+
rb.start_date,
|
|
78
|
+
rb.ref_type,
|
|
79
|
+
rb.end_date,
|
|
80
|
+
rb.start_address,
|
|
81
|
+
rb.end_address,
|
|
82
|
+
rb.real_km,
|
|
83
|
+
rb.dropped_kwh,
|
|
84
|
+
rb.gross_dropped_kwh,
|
|
85
|
+
rb.dropped_cost,
|
|
86
|
+
rb.gross_dropped_cost,
|
|
87
|
+
rb.cost_per_real_100km,
|
|
88
|
+
rb.gross_cost_per_real_100km,
|
|
89
|
+
rb.kwh_per_real_1km,
|
|
90
|
+
rb.gross_kwh_per_real_1km,
|
|
91
|
+
rb.cost_per_kwh,
|
|
92
|
+
rb.gross_cost_per_kwh,
|
|
93
|
+
rb.car_id,
|
|
94
|
+
rb.ref_id
|
|
95
|
+
FROM
|
|
96
|
+
base rb
|
|
97
|
+
UNION ALL
|
|
98
|
+
SELECT
|
|
99
|
+
rl.start_date,
|
|
100
|
+
rl.ref_type,
|
|
101
|
+
rl.end_date,
|
|
102
|
+
rl.start_address,
|
|
103
|
+
rl.end_address,
|
|
104
|
+
rl.real_km,
|
|
105
|
+
rl.dropped_kwh,
|
|
106
|
+
rl.gross_dropped_kwh,
|
|
107
|
+
rl.dropped_cost,
|
|
108
|
+
rl.gross_dropped_cost,
|
|
109
|
+
rl.cost_per_real_100km,
|
|
110
|
+
rl.gross_cost_per_real_100km,
|
|
111
|
+
rl.kwh_per_real_1km,
|
|
112
|
+
rl.gross_kwh_per_real_1km,
|
|
113
|
+
rl.cost_per_kwh,
|
|
114
|
+
rl.gross_cost_per_kwh,
|
|
115
|
+
rl.car_id,
|
|
116
|
+
rl.ref_id
|
|
117
|
+
FROM
|
|
118
|
+
latest rl
|
|
119
|
+
)
|
|
120
|
+
SELECT
|
|
121
|
+
r.start_date AS "Start",
|
|
122
|
+
CASE
|
|
123
|
+
WHEN r.ref_type = 'drive' THEN '🚗 Driving'
|
|
124
|
+
WHEN r.ref_type = 'idle' THEN '🅿️ Parking'
|
|
125
|
+
WHEN r.ref_type = 'charge' THEN '🔋 Charging'
|
|
126
|
+
ELSE NULL
|
|
127
|
+
END AS "Action",
|
|
128
|
+
EXTRACT(
|
|
129
|
+
EPOCH
|
|
130
|
+
FROM
|
|
131
|
+
r.end_date - r.start_date
|
|
132
|
+
) AS "Duration",
|
|
133
|
+
r.start_address AS "From",
|
|
134
|
+
r.end_address AS "To",
|
|
135
|
+
r.real_km AS "Distance",
|
|
136
|
+
r.dropped_kwh AS "Energy (net)",
|
|
137
|
+
CASE
|
|
138
|
+
WHEN r.ref_type = 'drive' THEN r.gross_dropped_kwh
|
|
139
|
+
END AS "Energy (gross)",
|
|
140
|
+
r.dropped_cost AS "Cost (net)",
|
|
141
|
+
CASE
|
|
142
|
+
WHEN r.ref_type = 'drive' THEN r.gross_dropped_cost
|
|
143
|
+
END AS "Cost (gross)",
|
|
144
|
+
r.cost_per_real_100km AS "Cost / 100 km (net)",
|
|
145
|
+
CASE
|
|
146
|
+
WHEN r.ref_type = 'drive' THEN r.gross_cost_per_real_100km
|
|
147
|
+
END AS "Cost / 100 km (gross)",
|
|
148
|
+
r.kwh_per_real_1km AS "Energy / km (net)",
|
|
149
|
+
CASE
|
|
150
|
+
WHEN r.ref_type = 'drive' THEN r.gross_kwh_per_real_1km
|
|
151
|
+
END AS "Energy / km (gross)",
|
|
152
|
+
r.cost_per_kwh AS "Cost / kWh (net)",
|
|
153
|
+
CASE
|
|
154
|
+
WHEN r.ref_type = 'drive' THEN r.gross_cost_per_kwh
|
|
155
|
+
END AS "Cost / kWh (gross)",
|
|
156
|
+
floor(
|
|
157
|
+
extract(
|
|
158
|
+
epoch
|
|
159
|
+
FROM
|
|
160
|
+
r.start_date
|
|
161
|
+
)
|
|
162
|
+
) * 1000 AS start_date_ts,
|
|
163
|
+
ceil(
|
|
164
|
+
extract(
|
|
165
|
+
epoch
|
|
166
|
+
FROM
|
|
167
|
+
r.end_date
|
|
168
|
+
)
|
|
169
|
+
) * 1000 AS end_date_ts,
|
|
170
|
+
r.car_id,
|
|
171
|
+
r.ref_id AS drive_id,
|
|
172
|
+
-- h.climate_on_ratio,
|
|
173
|
+
-- CONCAT('F: ', round(h.front_defroster_ratio * 100.0, 0), ' %, R: ', round(h.rear_defroster_ratio * 100.0, 0), ' %') AS defroster_ratio,
|
|
174
|
+
h.battery_heater_ratio AS "Battery Heater"
|
|
175
|
+
FROM
|
|
176
|
+
res r
|
|
177
|
+
LEFT JOIN pic_refs_hvac_ratio_v h ON h.car_id = r.car_id
|
|
178
|
+
AND h.ref_type = r.ref_type
|
|
179
|
+
AND h.ref_id = r.ref_id
|
|
180
|
+
ORDER BY
|
|
181
|
+
r.start_date DESC;
|
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 -
|
|
1054
|
-
ELSE
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|