@mxpicture/tesla-superchargers-mate 0.1.24 → 0.1.26

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.
@@ -8,5 +8,6 @@ export * from "./PicInvoicesStore.js";
8
8
  export * from "./PicPricebooksStore.js";
9
9
  export * from "./PicSlugsStore.js";
10
10
  export * from "./db.js";
11
+ export * from "./schema.js";
11
12
  export * from "./statements.js";
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -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,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC"}
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,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
@@ -9,5 +9,6 @@ export * from "./PicInvoicesStore.js";
9
9
  export * from "./PicPricebooksStore.js";
10
10
  export * from "./PicSlugsStore.js";
11
11
  export * from "./db.js";
12
+ export * from "./schema.js";
12
13
  export * from "./statements.js";
13
14
  //# sourceMappingURL=index.js.map
@@ -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,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC"}
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,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Apply the idempotent `pic_*` schema (CREATE ... IF NOT EXISTS) to the
3
+ * configured database. Safe to re-run on every deploy — existing rows (including
4
+ * the accumulated price history in `pic_pricebooks`) are left untouched. The
5
+ * connection comes from the standard `PG*` environment variables.
6
+ */
7
+ export declare const applySchema: () => Promise<void>;
8
+ /**
9
+ * DESTRUCTIVE: drop every `pic_*` table, including the accumulated price history.
10
+ * Intended for a deliberate reset; pair with {@link applySchema} to recreate.
11
+ */
12
+ export declare const resetSchema: () => Promise<void>;
13
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/store/schema.ts"],"names":[],"mappings":"AAaA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,IAAI,CAEhD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,IAAI,CAEhD,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { sql } from "./db.js";
3
+ /**
4
+ * Absolute path to a bundled SQL file. The `.sql` files ship inside this package
5
+ * (see its `files` array) at `<pkg>/sql/*.sql`; this module compiles to
6
+ * `<pkg>/dist/store/schema.js`, so they sit two directories up in both `src` and
7
+ * `dist`. Resolving from `import.meta.url` (not `process.cwd()`) makes it work no
8
+ * matter where the CLI is invoked from.
9
+ */
10
+ const sqlFile = (name) => fileURLToPath(new URL(`../../sql/${name}`, import.meta.url));
11
+ /**
12
+ * Apply the idempotent `pic_*` schema (CREATE ... IF NOT EXISTS) to the
13
+ * configured database. Safe to re-run on every deploy — existing rows (including
14
+ * the accumulated price history in `pic_pricebooks`) are left untouched. The
15
+ * connection comes from the standard `PG*` environment variables.
16
+ */
17
+ export const applySchema = async () => {
18
+ await sql().file(sqlFile("setup.sql"));
19
+ };
20
+ /**
21
+ * DESTRUCTIVE: drop every `pic_*` table, including the accumulated price history.
22
+ * Intended for a deliberate reset; pair with {@link applySchema} to recreate.
23
+ */
24
+ export const resetSchema = async () => {
25
+ await sql().file(sqlFile("reset.sql"));
26
+ };
27
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/store/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AAE9B;;;;;;GAMG;AACH,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE,CACvC,aAAa,CAAC,IAAI,GAAG,CAAC,aAAa,IAAI,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,IAAmB,EAAE;IACnD,MAAM,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;AACzC,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,IAAmB,EAAE;IACnD,MAAM,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;AACzC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxpicture/tesla-superchargers-mate",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "TeslaMate integration",
5
5
  "type": "module",
6
6
  "author": "MXPicture",
@@ -33,7 +33,8 @@
33
33
  }
34
34
  },
35
35
  "files": [
36
- "dist"
36
+ "dist",
37
+ "sql"
37
38
  ],
38
39
  "engines": {
39
40
  "node": ">=24"
@@ -48,7 +49,7 @@
48
49
  "access": "public"
49
50
  },
50
51
  "dependencies": {
51
- "@mxpicture/tesla-superchargers-generated": "^0.1.24",
52
+ "@mxpicture/tesla-superchargers-generated": "^0.1.26",
52
53
  "postgres": "^3.4.9",
53
54
  "short-uuid": "^6.0.3",
54
55
  "zod": "^4.3.6"
package/sql/reset.sql ADDED
@@ -0,0 +1,29 @@
1
+ -- DESTRUCTIVE reset: drops every pic_* object — INCLUDING pic_pricebooks,
2
+ -- the accumulated price history this project exists to build. Run it only to
3
+ -- start over from scratch; setup.sql alone is idempotent and safe to
4
+ -- (re-)apply after updates.
5
+ DROP VIEW IF EXISTS pic_cp_inv_v;
6
+
7
+ DROP VIEW IF EXISTS pic_cp_sc_v;
8
+
9
+ DROP VIEW IF EXISTS pic_chargers_v;
10
+
11
+ DROP VIEW IF EXISTS pic_pricebooks_tesla_ui_v;
12
+
13
+ DROP VIEW IF EXISTS pic_pricebooks_tesla_condensed_v;
14
+
15
+ DROP VIEW IF EXISTS pic_pricebooks_tesla_v;
16
+
17
+ DROP TABLE IF EXISTS pic_invoices;
18
+
19
+ DROP TABLE IF EXISTS pic_invoice_missing_states;
20
+
21
+ DROP TABLE IF EXISTS pic_invoice_files;
22
+
23
+ DROP TABLE IF EXISTS pic_pricebooks;
24
+
25
+ DROP TABLE IF EXISTS pic_chargers;
26
+
27
+ DROP TABLE IF EXISTS pic_slugs;
28
+
29
+ DROP TABLE IF EXISTS pic_charging_process_states;
package/sql/setup.sql ADDED
@@ -0,0 +1,462 @@
1
+ -- Idempotent schema setup: creates the pic_* tables next to TeslaMate's own.
2
+ -- Safe to (re-)apply — existing tables (and the price history in
3
+ -- pic_pricebooks) are left untouched. To start over, run reset.sql first
4
+ -- (DESTRUCTIVE: drops the accumulated history).
5
+ -- slugs ------------------------------------------------------------------------------
6
+ CREATE TABLE IF NOT EXISTS pic_slugs (
7
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
8
+ fetched_at TIMESTAMPTZ NOT NULL,
9
+ last_fetch_uuid TEXT NOT NULL,
10
+ failure_count SMALLINT NOT NULL,
11
+ --
12
+ latitude DOUBLE PRECISION NOT NULL, -- number
13
+ longitude DOUBLE PRECISION NOT NULL, -- number
14
+ location_types TEXT[] NOT NULL DEFAULT '{}',
15
+ location_url_slug TEXT NOT NULL, -- string
16
+ tesla_uuid TEXT UNIQUE, -- string (uuid), semantic key for syncs
17
+ title TEXT, -- string|null
18
+ access_type TEXT, -- string (supercharger_function.access_type)
19
+ actual_latitude TEXT, -- string (supercharger_function.actual_latitude)
20
+ actual_longitude TEXT, -- string (supercharger_function.actual_longitude)
21
+ charging_accessibility TEXT, -- string (supercharger_function.charging_accessibility)
22
+ coming_soon_latitude TEXT, -- string (supercharger_function.coming_soon_latitude)
23
+ coming_soon_longitude TEXT, -- string (supercharger_function.coming_soon_longitude)
24
+ coming_soon_name TEXT, -- string (supercharger_function.coming_soon_name)
25
+ open_to_non_tesla BOOLEAN, -- boolean (supercharger_function.open_to_non_tesla)
26
+ project_status TEXT, -- string (supercharger_function.project_status)
27
+ show_on_find_us TEXT, -- string (supercharger_function.show_on_find_us)
28
+ site_status TEXT, -- string (supercharger_function.site_status)
29
+ vote_winner_quarter TEXT, -- string (supercharger_function.vote_winner_quarter)
30
+ failure_notified_at TIMESTAMPTZ -- set when a failure notification covered this row; NULL = not yet notified
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS pic_slugs_location_types_gin ON pic_slugs USING gin (location_types);
34
+
35
+ CREATE INDEX IF NOT EXISTS pic_slugs_location_slug_idx ON pic_slugs (location_url_slug);
36
+
37
+ CREATE INDEX IF NOT EXISTS pic_slugs_fetched_at_idx ON pic_slugs (fetched_at);
38
+
39
+ CREATE INDEX IF NOT EXISTS pic_slugs_locslug_time_idx ON pic_slugs (location_url_slug, fetched_at DESC);
40
+
41
+ -- chargers ------------------------------------------------------------------------------
42
+ CREATE TABLE IF NOT EXISTS pic_chargers (
43
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
44
+ fetched_at TIMESTAMPTZ NOT NULL,
45
+ last_fetch_uuid TEXT NOT NULL,
46
+ deleted_at TIMESTAMPTZ,
47
+ failure_count SMALLINT NOT NULL,
48
+ pic_slugs_id BIGINT REFERENCES pic_slugs (id) ON DELETE SET NULL,
49
+ --
50
+ access_type TEXT NOT NULL, -- string (accessType)
51
+ location_guid TEXT NOT NULL UNIQUE, -- string (locationGUID), semantic key for syncs
52
+ max_power_kw SMALLINT NOT NULL, -- number (maxPowerKw)
53
+ name TEXT NOT NULL, -- string
54
+ public_stall_count SMALLINT NOT NULL, -- number
55
+ time_zone TEXT NOT NULL, -- string
56
+ entry_point_latitude DOUBLE PRECISION NOT NULL, -- number (entryPoint.latitude)
57
+ entry_point_longitude DOUBLE PRECISION NOT NULL, -- number (entryPoint.longitude)
58
+ centroid_latitude DOUBLE PRECISION NOT NULL, -- number (centroid.latitude)
59
+ centroid_longitude DOUBLE PRECISION NOT NULL, -- number (centroid.longitude)
60
+ brand_name TEXT, -- string|null (brandName)
61
+ common_site_name TEXT NOT NULL, -- string (commonSiteName)
62
+ city TEXT NOT NULL, -- string
63
+ country TEXT NOT NULL, -- string
64
+ country_code TEXT NOT NULL, -- string (countryCode)
65
+ district TEXT, -- string|null
66
+ nav_region TEXT NOT NULL, -- string (navRegion)
67
+ nearest_entrance TEXT, -- string|null (nearestEntrance)
68
+ postal_code TEXT NOT NULL, -- string (postalCode)
69
+ postal_ext TEXT, -- string|null (postalExt)
70
+ state TEXT, -- string|null
71
+ street TEXT NOT NULL, -- string
72
+ street_number TEXT NOT NULL, -- string (streetNumber)
73
+ site_id BIGINT NOT NULL, -- number (availabilityProfile.siteId.id)
74
+ site_type TEXT NOT NULL, -- string (availabilityProfile.siteId.siteType)
75
+ trt_id BIGINT NOT NULL, -- number (availabilityProfile.trtId.id)
76
+ trt_type TEXT NOT NULL, -- string (availabilityProfile.trtId.siteType)
77
+ utc_offset TEXT, -- string|null (availabilityProfile.utcOffset)
78
+ failure_notified_at TIMESTAMPTZ -- set when a failure notification covered this row; NULL = not yet notified
79
+ );
80
+
81
+ CREATE INDEX IF NOT EXISTS pic_chargers_location_slug_idx ON pic_chargers (pic_slugs_id);
82
+
83
+ CREATE INDEX IF NOT EXISTS pic_chargers_fetched_at_idx ON pic_chargers (fetched_at);
84
+
85
+ CREATE INDEX IF NOT EXISTS pic_chargers_locslug_time_idx ON pic_chargers (pic_slugs_id, fetched_at DESC);
86
+
87
+ CREATE OR REPLACE VIEW pic_chargers_v AS
88
+ SELECT
89
+ *
90
+ FROM
91
+ pic_chargers
92
+ WHERE
93
+ deleted_at IS NULL;
94
+
95
+ -- pricebooks ------------------------------------------------------------------------------
96
+ -- History table: one snapshot ("group") = all rows sharing
97
+ -- (pic_chargers_id, last_fetch_uuid). fetched_at is the group's creation time
98
+ -- and never changes; an unchanged re-fetch only bumps last_fetch_uuid.
99
+ CREATE TABLE IF NOT EXISTS pic_pricebooks (
100
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
101
+ fetched_at TIMESTAMPTZ NOT NULL,
102
+ last_fetch_uuid TEXT NOT NULL,
103
+ pic_chargers_id BIGINT NOT NULL REFERENCES pic_chargers (id) ON DELETE CASCADE,
104
+ --
105
+ currency_code TEXT NOT NULL, -- string (currencyCode)
106
+ uom TEXT NOT NULL, -- string (ucom)
107
+ days SMALLINT[] NOT NULL, -- string [0,1,...]
108
+ start_time TIME NOT NULL, -- time (startTime)
109
+ end_time TIME NOT NULL, -- time (endTime)
110
+ vehicle_make_type TEXT NOT NULL, -- string (vehicleMakeType)
111
+ fee_type TEXT NOT NULL, -- string (feeType)
112
+ is_member_pricebook BOOLEAN NOT NULL, -- boolean (isMemberPricebook)
113
+ is_tou BOOLEAN NOT NULL, -- boolean (isTou)
114
+ rate_base NUMERIC(5, 2) NOT NULL, -- number (rateBase)
115
+ rate_tier_1 NUMERIC(5, 2) NOT NULL, -- number (rateTier1)
116
+ rate_tier_2 NUMERIC(5, 2) NOT NULL, -- number (rateTier2)
117
+ -- derived: 0-based dense rank within the (pic_chargers_id, last_fetch_uuid)
118
+ -- group, ordered by start_time (rows sharing a start_time share the value);
119
+ -- NULL when is_tou = false
120
+ sort_order SMALLINT
121
+ );
122
+
123
+ CREATE INDEX IF NOT EXISTS pic_pricebooks_loc_idx ON pic_pricebooks (pic_chargers_id);
124
+
125
+ -- group lookups: current group / group at a point in time
126
+ CREATE INDEX IF NOT EXISTS pic_pricebooks_charger_time_idx ON pic_pricebooks (pic_chargers_id, fetched_at DESC);
127
+
128
+ CREATE OR REPLACE VIEW pic_pricebooks_tesla_v AS
129
+ SELECT
130
+ *
131
+ FROM
132
+ pic_pricebooks AS p
133
+ WHERE
134
+ p.vehicle_make_type = 'TSLA'
135
+ AND p.fee_type = 'CHARGING'
136
+ ORDER BY
137
+ p.pic_chargers_id,
138
+ p.is_tou,
139
+ p.start_time;
140
+
141
+ -- // todo add currency code for ui/grafana
142
+ CREATE OR REPLACE VIEW pic_pricebooks_tesla_condensed_v AS
143
+ SELECT
144
+ c.*,
145
+ base.start_time AS base_start_time,
146
+ base.end_time AS base_end_time,
147
+ base.rate_base AS base_rate_base,
148
+ base.uom AS base_uom,
149
+ s0.start_time AS s0_start_time,
150
+ s0.end_time AS s0_end_time,
151
+ s0.rate_base AS s0_rate_base,
152
+ s0.uom AS s0_uom,
153
+ s1.start_time AS s1_start_time,
154
+ s1.end_time AS s1_end_time,
155
+ s1.rate_base AS s1_rate_base,
156
+ s1.uom AS s1_uom,
157
+ s2.start_time AS s2_start_time,
158
+ s2.end_time AS s2_end_time,
159
+ s2.rate_base AS s2_rate_base,
160
+ s2.uom AS s2_uom,
161
+ s3.start_time AS s3_start_time,
162
+ s3.end_time AS s3_end_time,
163
+ s3.rate_base AS s3_rate_base,
164
+ s3.uom AS s3_uom,
165
+ s4.start_time AS s4_start_time,
166
+ s4.end_time AS s4_end_time,
167
+ s4.rate_base AS s4_rate_base,
168
+ s4.uom AS s4_uom,
169
+ s5.start_time AS s5_start_time,
170
+ s5.end_time AS s5_end_time,
171
+ s5.rate_base AS s5_rate_base,
172
+ s5.uom AS s5_uom
173
+ FROM
174
+ pic_chargers_v AS c
175
+ LEFT JOIN pic_pricebooks_tesla_v base ON base.pic_chargers_id = c.id
176
+ AND base.last_fetch_uuid = c.last_fetch_uuid
177
+ AND base.sort_order IS NULL
178
+ LEFT JOIN pic_pricebooks_tesla_v s0 ON s0.pic_chargers_id = c.id
179
+ AND s0.last_fetch_uuid = c.last_fetch_uuid
180
+ AND s0.sort_order = 0
181
+ LEFT JOIN pic_pricebooks_tesla_v s1 ON s1.pic_chargers_id = c.id
182
+ AND s1.last_fetch_uuid = c.last_fetch_uuid
183
+ AND s1.sort_order = 1
184
+ LEFT JOIN pic_pricebooks_tesla_v s2 ON s2.pic_chargers_id = c.id
185
+ AND s2.last_fetch_uuid = c.last_fetch_uuid
186
+ AND s2.sort_order = 2
187
+ LEFT JOIN pic_pricebooks_tesla_v s3 ON s3.pic_chargers_id = c.id
188
+ AND s3.last_fetch_uuid = c.last_fetch_uuid
189
+ AND s3.sort_order = 3
190
+ LEFT JOIN pic_pricebooks_tesla_v s4 ON s4.pic_chargers_id = c.id
191
+ AND s4.last_fetch_uuid = c.last_fetch_uuid
192
+ AND s4.sort_order = 4
193
+ LEFT JOIN pic_pricebooks_tesla_v s5 ON s5.pic_chargers_id = c.id
194
+ AND s5.last_fetch_uuid = c.last_fetch_uuid
195
+ AND s5.sort_order = 5;
196
+
197
+ CREATE OR REPLACE VIEW pic_pricebooks_tesla_ui_v AS
198
+ SELECT
199
+ v.*,
200
+ CASE
201
+ WHEN v.s0_start_time IS NULL THEN CONCAT(v.base_rate_base, ' €/', v.base_uom)
202
+ END AS base_timeslot,
203
+ CASE
204
+ WHEN v.s0_start_time IS NOT NULL THEN CONCAT(
205
+ to_char(v.s0_start_time, 'HH24:mm'),
206
+ '-',
207
+ to_char(v.s0_end_time, 'HH24:mm'),
208
+ ': ',
209
+ v.s0_rate_base,
210
+ ' €/',
211
+ v.s0_uom
212
+ )
213
+ END AS s0_timeslot,
214
+ CASE
215
+ WHEN v.s1_start_time IS NOT NULL THEN CONCAT(
216
+ to_char(v.s1_start_time, 'HH24:mm'),
217
+ '-',
218
+ to_char(v.s1_end_time, 'HH24:mm'),
219
+ ': ',
220
+ v.s1_rate_base,
221
+ ' €/',
222
+ v.s1_uom
223
+ )
224
+ END AS s1_timeslot,
225
+ CASE
226
+ WHEN v.s2_start_time IS NOT NULL THEN CONCAT(
227
+ to_char(v.s2_start_time, 'HH24:mm'),
228
+ '-',
229
+ to_char(v.s2_end_time, 'HH24:mm'),
230
+ ': ',
231
+ v.s2_rate_base,
232
+ ' €/',
233
+ v.s2_uom
234
+ )
235
+ END AS s2_timeslot,
236
+ CASE
237
+ WHEN v.s3_start_time IS NOT NULL THEN CONCAT(
238
+ to_char(v.s3_start_time, 'HH24:mm'),
239
+ '-',
240
+ to_char(v.s3_end_time, 'HH24:mm'),
241
+ ': ',
242
+ v.s3_rate_base,
243
+ ' €/',
244
+ v.s3_uom
245
+ )
246
+ END AS s3_timeslot,
247
+ CASE
248
+ WHEN v.s4_start_time IS NOT NULL THEN CONCAT(
249
+ to_char(v.s4_start_time, 'HH24:mm'),
250
+ '-',
251
+ to_char(v.s4_end_time, 'HH24:mm'),
252
+ ': ',
253
+ v.s4_rate_base,
254
+ ' €/',
255
+ v.s4_uom
256
+ )
257
+ END AS s4_timeslot,
258
+ CASE
259
+ WHEN v.s5_start_time IS NOT NULL THEN CONCAT(
260
+ to_char(v.s5_start_time, 'HH24:mm'),
261
+ '-',
262
+ to_char(v.s5_end_time, 'HH24:mm'),
263
+ ': ',
264
+ v.s5_rate_base,
265
+ ' €/',
266
+ v.s5_uom
267
+ )
268
+ END AS s5_timeslot
269
+ FROM
270
+ pic_pricebooks_tesla_condensed_v AS v;
271
+
272
+ -- charging process states ------------------------------------------------------------------------------
273
+ CREATE TABLE IF NOT EXISTS pic_charging_process_states (
274
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
275
+ created_at TIMESTAMPTZ NOT NULL,
276
+ updated_at TIMESTAMPTZ NOT NULL,
277
+ --
278
+ charging_processes_id INTEGER NOT NULL, -- no foreign key --> teslamate should not fail because of this extension
279
+ failure_count SMALLINT NOT NULL,
280
+ failure_notified_at TIMESTAMPTZ -- set when a failure notification covered this row; NULL = not yet notified
281
+ );
282
+
283
+ -- the failure upsert conflicts on charging_processes_id, so the index must be
284
+ -- unique; the old non-unique pic_charg_proc_idx is dropped on upgrade
285
+ DROP INDEX IF EXISTS pic_charg_proc_idx;
286
+
287
+ CREATE UNIQUE INDEX IF NOT EXISTS pic_charg_proc_uq_idx ON pic_charging_process_states (charging_processes_id);
288
+
289
+ -- invoices (from Tesla App) ------------------------------------------------------------------------------
290
+ CREATE TABLE IF NOT EXISTS pic_invoices (
291
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
292
+ fetched_at TIMESTAMPTZ NOT NULL,
293
+ last_fetch_uuid TEXT NOT NULL,
294
+ charging_processes_id INTEGER, -- no foreign key --> teslamate should not fail because of this extension
295
+ --
296
+ charge_start_date_time TIMESTAMPTZ NOT NULL,
297
+ name TEXT,
298
+ vin TEXT NOT NULL,
299
+ model TEXT,
300
+ country TEXT NOT NULL,
301
+ site_location_name TEXT NOT NULL,
302
+ description TEXT,
303
+ ucom TEXT NOT NULL,
304
+ quantity_base NUMERIC(8, 4) NOT NULL,
305
+ quantity_tier1 NUMERIC(8, 4),
306
+ quantity_tier2 NUMERIC(8, 4),
307
+ quantity_tier3 NUMERIC(8, 4),
308
+ quantity_tier4 NUMERIC(8, 4),
309
+ invoice_number TEXT NOT NULL,
310
+ unit_cost_base NUMERIC(4, 2) NOT NULL,
311
+ unit_cost_tier1 NUMERIC(4, 2),
312
+ unit_cost_tier2 NUMERIC(4, 2),
313
+ unit_cost_tier3 NUMERIC(4, 2),
314
+ unit_cost_tier4 NUMERIC(4, 2),
315
+ vat NUMERIC(5, 2) NOT NULL,
316
+ total_exc_vat NUMERIC(5, 2) NOT NULL,
317
+ total_inc_vat NUMERIC(5, 2) NOT NULL,
318
+ status TEXT,
319
+ invoice TEXT
320
+ );
321
+
322
+ CREATE UNIQUE INDEX IF NOT EXISTS pic_invoices_uq_cp_idx ON pic_invoices (charging_processes_id);
323
+
324
+ CREATE UNIQUE INDEX IF NOT EXISTS pic_invoices_uq_inv_idx ON pic_invoices (invoice_number);
325
+
326
+ -- invoice files (from Tesla App) ------------------------------------------------------------------------------
327
+ CREATE TABLE IF NOT EXISTS pic_invoice_files (
328
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
329
+ fetched_at TIMESTAMPTZ NOT NULL,
330
+ last_fetch_uuid TEXT NOT NULL,
331
+ --
332
+ name TEXT NOT NULL
333
+ );
334
+
335
+ CREATE UNIQUE INDEX IF NOT EXISTS pic_invoices_name_idx ON pic_invoice_files (name);
336
+
337
+ -- invoice missing states ------------------------------------------------------------------------------
338
+ -- One row per Supercharger charge already paged about a missing invoice
339
+ -- (notify-unmatched-invoices), so each charge pages exactly once. A row is
340
+ -- irrelevant once an invoice arrives (the charge stops matching the query).
341
+ CREATE TABLE IF NOT EXISTS pic_invoice_missing_states (
342
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
343
+ created_at TIMESTAMPTZ NOT NULL,
344
+ updated_at TIMESTAMPTZ NOT NULL,
345
+ --
346
+ charging_processes_id INTEGER NOT NULL, -- no foreign key --> teslamate should not fail because of this extension
347
+ notified_at TIMESTAMPTZ -- set when a missing-invoice notification covered this row; NULL = not yet notified
348
+ );
349
+
350
+ -- the notified upsert conflicts on charging_processes_id, so the index must be unique
351
+ CREATE UNIQUE INDEX IF NOT EXISTS pic_invoice_missing_uq_idx ON pic_invoice_missing_states (charging_processes_id);
352
+
353
+ -- charging processes <-> pic_invoices ---------------------------------------------------------
354
+ -- Dropped instead of CREATE OR REPLACE: column types/order may change between
355
+ -- versions, which OR REPLACE rejects. Views hold no data, so this stays
356
+ -- idempotent. pic_cp_inv_v depends on pic_cp_sc_v, so it is dropped first.
357
+ DROP VIEW IF EXISTS pic_cp_inv_v;
358
+
359
+ DROP VIEW IF EXISTS pic_cp_inv_prep_v;
360
+
361
+ DROP VIEW IF EXISTS pic_cp_sc_v;
362
+
363
+ -- Supercharger charging processes: a process counts as Supercharger when any
364
+ -- of its charges rows reports fast_charger_brand = 'Tesla' (AC charging
365
+ -- reports '<invalid>'). Site name comes from the reverse-geocoded address.
366
+ CREATE VIEW pic_cp_sc_v AS
367
+ SELECT
368
+ cp.id,
369
+ cp.start_date AT TIME ZONE 'UTC' AS charge_start_date_time,
370
+ COALESCE(addr.name, addr.display_name) AS site_location_name,
371
+ COALESCE(cp.charge_energy_used, cp.charge_energy_added) AS quantity,
372
+ cp.cost
373
+ FROM
374
+ charging_processes AS cp
375
+ LEFT JOIN addresses AS addr ON addr.id = cp.address_id
376
+ WHERE
377
+ EXISTS (
378
+ SELECT
379
+ 1
380
+ FROM
381
+ charges AS c
382
+ WHERE
383
+ c.charging_process_id = cp.id
384
+ AND c.fast_charger_brand = 'Tesla'
385
+ );
386
+
387
+ -- Full outer join: matched rows plus Supercharger charges without an invoice
388
+ -- (match_state = 'cp_only') and invoices without a matched charge
389
+ -- ('inv_only'). Values missing because one side is absent are 0, so sums stay
390
+ -- usable; deviations are 0 where undefined.
391
+ CREATE VIEW pic_cp_inv_prep_v AS
392
+ SELECT
393
+ COALESCE(
394
+ inv.charge_start_date_time,
395
+ cp.charge_start_date_time
396
+ ) AS charge_start_date_time,
397
+ --
398
+ COALESCE(
399
+ inv.site_location_name,
400
+ cp.site_location_name,
401
+ 'Unknown'
402
+ ) AS site_location_name,
403
+ --
404
+ CASE
405
+ WHEN inv.id IS NULL THEN 'cp_only'
406
+ WHEN cp.id IS NULL THEN 'inv_only'
407
+ ELSE 'matched'
408
+ END AS match_state,
409
+ --
410
+ inv.quantity_base AS inv_quan_raw,
411
+ --
412
+ ROUND(COALESCE(inv.quantity_base, 0), 2) AS inv_quan,
413
+ --
414
+ cp.quantity AS cp_quan_raw,
415
+ --
416
+ COALESCE(cp.quantity, 0) AS cp_quan,
417
+ --
418
+ COALESCE(
419
+ ROUND(
420
+ (cp.quantity / NULLIF(inv.quantity_base, 0)) - 1,
421
+ 4
422
+ ),
423
+ 0
424
+ ) AS deviation_quan,
425
+ --
426
+ CASE
427
+ WHEN inv.quantity_base IS NULL
428
+ OR cp.quantity IS NULL THEN 0
429
+ ELSE ROUND(inv.quantity_base - cp.quantity, 2)
430
+ END AS diff_quan,
431
+ --
432
+ inv.total_inc_vat AS inv_cost_raw,
433
+ --
434
+ ROUND(COALESCE(inv.total_inc_vat, 0), 2) AS inv_cost,
435
+ --
436
+ cp.cost AS cp_cost_raw,
437
+ --
438
+ COALESCE(cp.cost, 0) AS cp_cost,
439
+ --
440
+ COALESCE(
441
+ ROUND((cp.cost / NULLIF(inv.total_inc_vat, 0)) - 1, 4),
442
+ 0
443
+ ) AS deviation_cost,
444
+ --
445
+ CASE
446
+ WHEN inv.total_inc_vat IS NULL
447
+ OR cp.cost IS NULL THEN 0
448
+ ELSE ROUND(inv.total_inc_vat - cp.cost, 2)
449
+ END AS diff_cost
450
+ FROM
451
+ pic_cp_sc_v AS cp
452
+ FULL OUTER JOIN pic_invoices AS inv ON inv.charging_processes_id = cp.id;
453
+
454
+ --
455
+ CREATE VIEW pic_cp_inv_v AS
456
+ SELECT
457
+ base.*,
458
+ to_char(base.charge_start_date_time, 'YYYY-MM-DD hh:MM') AS formatted_date,
459
+ base.inv_quan || ' kWh (' || base.inv_cost_raw || ' €)' AS inv,
460
+ base.cp_quan || ' kWh (' || base.cp_cost_raw || ' €)' AS cp
461
+ FROM
462
+ pic_cp_inv_prep_v AS base;