Skip to content

Commit 651a774

Browse files
committed
fix dp creation
1 parent 64270d9 commit 651a774

2 files changed

Lines changed: 153 additions & 53 deletions

File tree

lib/euDataAct.js

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ let _DICTIONARY = null;
5757
function loadDictionary() {
5858
if (_DICTIONARY) return _DICTIONARY;
5959
try {
60-
_DICTIONARY = JSON.parse(fs.readFileSync(path.join(__dirname, "euDataActDictionary.json"), "utf-8"));
60+
let raw = fs.readFileSync(path.join(__dirname, "euDataActDictionary.json"), "utf-8");
61+
// Defensive: strip a UTF-8 BOM if the dictionary was edited on Windows
62+
// (Powershell's `Out-File` adds one and JSON.parse rejects it).
63+
if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
64+
_DICTIONARY = JSON.parse(raw);
6165
} catch {
6266
_DICTIONARY = {};
6367
}
@@ -202,7 +206,9 @@ function unzipFirstJson(buf, name) {
202206
if (method === 0) raw = compressed;
203207
else if (method === 8) raw = zlib.inflateRawSync(compressed);
204208
else throw new Error(`Unsupported zip method ${method} for ${fileName} in ${name}`);
205-
return { fileName, json: JSON.parse(raw.toString("utf-8")) };
209+
let text = raw.toString("utf-8");
210+
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
211+
return { fileName, json: JSON.parse(text) };
206212
}
207213
off = dataStart + entrySize;
208214
if (flags & 0x08) {
@@ -291,13 +297,25 @@ function setNested(target, dottedName, value) {
291297
const key = parts[i];
292298
// dictionary keys sometimes use "[*]" placeholder for array indices that
293299
// appear in actual datasets as e.g. "profiles.0". keep them as-is.
294-
if (cur[key] == null || typeof cur[key] !== "object") {
300+
const existing = cur[key];
301+
if (existing == null) {
295302
cur[key] = {};
303+
} else if (typeof existing !== "object") {
304+
// A previous dataFieldName wrote a primitive at this branch (e.g.
305+
// "timestamp" then later "timestamp.foo"). Don't drop the primitive on
306+
// the floor — preserve it as a `_value` leaf so json2iob still emits it.
307+
cur[key] = { _value: existing };
296308
}
297309
cur = cur[key];
298310
}
299311
const leaf = parts[parts.length - 1];
300-
cur[leaf] = value;
312+
// Symmetric defence: if the leaf slot already holds an object, the new
313+
// primitive becomes its `_value` instead of clobbering the subtree.
314+
if (cur[leaf] != null && typeof cur[leaf] === "object" && (typeof value !== "object" || value === null)) {
315+
cur[leaf]._value = value;
316+
} else {
317+
cur[leaf] = value;
318+
}
301319
}
302320

303321
// --- client ----------------------------------------------------------------
@@ -335,32 +353,47 @@ class EuDataActClient {
335353
}
336354

337355
async _getText(url, headers) {
356+
const startedAt = Date.now();
338357
const { resp, body } = await this._req({
339358
method: "GET",
340359
url,
341360
headers: { "User-Agent": USER_AGENT, ...(headers || {}) },
342361
});
343-
return { status: resp.statusCode, url: resp.request.uri.href, body: String(body || ""), headers: resp.headers };
362+
const text = String(body || "");
363+
this.log.debug(
364+
`[euDataAct] GET ${url} -> ${resp.statusCode} (${text.length}B, ${Date.now() - startedAt}ms)`,
365+
);
366+
return { status: resp.statusCode, url: resp.request.uri.href, body: text, headers: resp.headers };
344367
}
345368

346369
async _postForm(url, form, headers) {
370+
const startedAt = Date.now();
347371
const { resp, body } = await this._req({
348372
method: "POST",
349373
url,
350374
form,
351375
followAllRedirects: true,
352376
headers: { "User-Agent": USER_AGENT, ...(headers || {}) },
353377
});
354-
return { status: resp.statusCode, url: resp.request.uri.href, body: String(body || ""), headers: resp.headers };
378+
const text = String(body || "");
379+
this.log.debug(
380+
`[euDataAct] POST ${url} -> ${resp.statusCode} (${text.length}B, ${Date.now() - startedAt}ms)`,
381+
);
382+
return { status: resp.statusCode, url: resp.request.uri.href, body: text, headers: resp.headers };
355383
}
356384

357385
async _getBuffer(url, headers) {
386+
const startedAt = Date.now();
358387
const { resp, body } = await this._req({
359388
method: "GET",
360389
url,
361390
encoding: null,
362391
headers: { "User-Agent": USER_AGENT, ...(headers || {}) },
363392
});
393+
const size = Buffer.isBuffer(body) ? body.length : 0;
394+
this.log.debug(
395+
`[euDataAct] GET ${url} (binary) -> ${resp.statusCode} (${size}B, ${Date.now() - startedAt}ms)`,
396+
);
364397
return { status: resp.statusCode, url: resp.request.uri.href, body, headers: resp.headers };
365398
}
366399

@@ -500,7 +533,8 @@ class EuDataActClient {
500533
if (r.status >= 400) {
501534
throw new Error(`Download ${name} -> HTTP ${r.status}`);
502535
}
503-
return unzipFirstJson(r.body, name);
536+
const unzipped = unzipFirstJson(r.body, name);
537+
return { ...unzipped, byteSize: r.body.length };
504538
}
505539

506540
/**
@@ -509,18 +543,25 @@ class EuDataActClient {
509543
*/
510544
async getLatestStatus(vin, identifier) {
511545
const datasets = await this.listDatasets(vin, identifier);
512-
const newest = datasets
513-
.filter((d) => d && d.name && !d.name.endsWith(NO_CONTENT_SUFFIX))
546+
const contentDatasets = datasets.filter((d) => d && d.name && !d.name.endsWith(NO_CONTENT_SUFFIX));
547+
const newest = contentDatasets
548+
.slice()
514549
.sort((a, b) => String(b.createdOn || b.name).localeCompare(String(a.createdOn || a.name)))[0];
550+
this.log.debug(
551+
`[euDataAct] ${vin} list: ${datasets.length} datasets total, ${contentDatasets.length} with content`,
552+
);
515553
if (!newest) {
516554
const error = new Error("No content datasets available yet");
517555
error.code = "NO_CONTENT";
518556
throw error;
519557
}
520-
const { fileName, json } = await this.downloadDataset(vin, identifier, newest.name);
558+
const { fileName, json, byteSize } = await this.downloadDataset(vin, identifier, newest.name);
521559
return {
522560
datasetName: newest.name,
523561
datasetCreatedOn: newest.createdOn || null,
562+
datasetCount: datasets.length,
563+
contentCount: contentDatasets.length,
564+
byteSize,
524565
fileName,
525566
raw: json,
526567
normalized: normalizeDataset(json),

main.js

Lines changed: 102 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,10 +1324,10 @@ class VwWeconnect extends utils.Adapter {
13241324
});
13251325
});
13261326
} else if (this.config.type === "id") {
1327+
// Drop-locked timers handle the regular cadence; the manual refresh
1328+
// button just kicks every VIN's timer to fire immediately.
13271329
this.vinArray.forEach((vin) => {
1328-
this.getEuDataActStatus(vin).catch((err) => {
1329-
this.log.error("EU Data Act status update failed: " + (err && err.message ? err.message : err));
1330-
});
1330+
this._scheduleEuDataActPoll(vin, 0);
13311331
});
13321332
return;
13331333
} else if (this.config.type === "audietron") {
@@ -6727,17 +6727,44 @@ class VwWeconnect extends utils.Adapter {
67276727
return;
67286728
}
67296729

6730-
// initial fetch + arm periodic refresh on the user-configured interval
6730+
// initial fetch + arm adaptive (drop-locked) refresh per VIN. The portal
6731+
// emits a new dataset every ~15 min; we use the listing's createdOn
6732+
// timestamp to schedule the next poll precisely 15 min after the newest
6733+
// drop (plus a small provisioning buffer), instead of a blind setInterval
6734+
// that would drift up to 15 min off the real cadence.
6735+
this.euDataActTimers = this.euDataActTimers || {};
67316736
for (const vin of this.vinArray) {
6732-
await this.getEuDataActStatus(vin).catch((err) => {
6733-
this.log.error(`EU Data Act status for ${vin} failed: ${err.message || err}`);
6734-
});
6737+
this._scheduleEuDataActPoll(vin, 0);
67356738
}
6739+
}
67366740

6737-
this.updateInterval && clearInterval(this.updateInterval);
6738-
this.updateInterval = setInterval(() => {
6739-
this.updateStatus();
6740-
}, this.config.interval * 60 * 1000);
6741+
/**
6742+
* Adaptive poll for one VIN: fetch, then schedule the next call based on
6743+
* when the portal is expected to publish the next 15-min dataset.
6744+
*/
6745+
_scheduleEuDataActPoll(vin, delayMs) {
6746+
if (!this.euDataActTimers) this.euDataActTimers = {};
6747+
if (this.euDataActTimers[vin]) {
6748+
clearTimeout(this.euDataActTimers[vin]);
6749+
}
6750+
this.euDataActTimers[vin] = setTimeout(async () => {
6751+
const next = await this.getEuDataActStatus(vin)
6752+
.catch((err) => {
6753+
this.log.error(`EU Data Act status for ${vin} failed: ${err.message || err}`);
6754+
return null;
6755+
});
6756+
// If the fetch didn't return a target time (NO_CONTENT, error,
6757+
// metadata still missing), fall back to a 1-min retry so we pick up
6758+
// the very next drop without waiting a full interval.
6759+
const fallbackMs = 60 * 1000;
6760+
const userMaxMs = Math.max(60, (this.config.interval || 15) * 60) * 1000;
6761+
let nextMs = typeof next === "number" && next > 0 ? next : fallbackMs;
6762+
// Never poll faster than 30s and never slower than the user-configured
6763+
// ceiling — the latter caps schedule drift if createdOn is way off.
6764+
nextMs = Math.max(30 * 1000, Math.min(nextMs, userMaxMs));
6765+
this.log.debug(`EU Data Act: ${vin} next poll in ${Math.round(nextMs / 1000)}s`);
6766+
this._scheduleEuDataActPoll(vin, nextMs);
6767+
}, delayMs);
67416768
}
67426769

67436770
/**
@@ -6757,29 +6784,31 @@ class VwWeconnect extends utils.Adapter {
67576784
common: { name: v.nickname || vin },
67586785
native: {},
67596786
});
6760-
await this.extendObjectAsync(vin + ".general", {
6761-
type: "channel",
6762-
common: { name: "General Information" },
6763-
native: {},
6787+
await this.json2iob.parse(vin + ".general", { vin, nickname: v.nickname || "" }, {
6788+
forceIndex: true,
6789+
channelName: "General Information",
67646790
});
6765-
await this.setStateAsync(vin + ".general.vin", vin, true);
6766-
if (v.nickname) {
6767-
await this.setStateAsync(vin + ".general.nickname", v.nickname, true);
6768-
}
67696791
try {
67706792
const meta = await this.euDataAct.getMetadata(vin);
67716793
if (meta && meta.Identifier) {
67726794
this.euDataActIdentifiers[vin] = meta.Identifier;
67736795
this.log.debug(
6774-
`EU Data Act: ${vin} Identifier=${meta.Identifier} Frequency=${meta.Frequency || "?"}`,
6796+
`EU Data Act: ${vin} data request - ` +
6797+
`Identifier=${meta.Identifier} ` +
6798+
`Name=${meta.Name || "?"} ` +
6799+
`Frequency=${meta.Frequency || "?"} ` +
6800+
`StartDate=${meta.StartDate || "?"} ` +
6801+
`EndDate=${meta.EndDate || "?"} ` +
6802+
`EmailFrequency=${meta.EmailFrequency || "?"} ` +
6803+
`LastNotificationDate=${meta.LastNotificationDate || "?"} ` +
6804+
`DataClusters=[${(meta.DataClusters || []).join(", ")}]`,
67756805
);
67766806
} else {
67776807
this.log.warn(
67786808
`EU Data Act: ${vin} has no Identifier - enable a continuous data request on the portal first`,
67796809
);
6810+
this.log.debug(`EU Data Act: ${vin} metadata raw: ${JSON.stringify(meta || {})}`);
67806811
}
6781-
// Persist metadata for transparency.
6782-
await this.json2iob.parse(vin + ".datarequest", meta || {}, { forceIndex: true });
67836812
} catch (err) {
67846813
this.log.warn(`EU Data Act: metadata fetch failed for ${vin}: ${err.message || err}`);
67856814
}
@@ -6792,7 +6821,7 @@ class VwWeconnect extends utils.Adapter {
67926821
* stored in `<vin>.statuseudata.rawJson` when `config.rawJson` is enabled.
67936822
*/
67946823
async getEuDataActStatus(vin) {
6795-
if (!this.euDataAct) return;
6824+
if (!this.euDataAct) return null;
67966825
const identifier = this.euDataActIdentifiers && this.euDataActIdentifiers[vin];
67976826
if (!identifier) {
67986827
this.log.debug(`EU Data Act: no Identifier for ${vin}, retrying metadata`);
@@ -6802,42 +6831,67 @@ class VwWeconnect extends utils.Adapter {
68026831
this.euDataActIdentifiers = this.euDataActIdentifiers || {};
68036832
this.euDataActIdentifiers[vin] = meta.Identifier;
68046833
} else {
6805-
return;
6834+
return 60 * 1000;
68066835
}
68076836
} catch (err) {
68086837
this.log.debug(`EU Data Act: metadata retry for ${vin} failed: ${err.message || err}`);
6809-
return;
6838+
return 60 * 1000;
68106839
}
68116840
}
68126841
try {
6842+
const startedAt = Date.now();
68136843
const result = await this.euDataAct.getLatestStatus(vin, this.euDataActIdentifiers[vin]);
6844+
const elapsed = Date.now() - startedAt;
6845+
const dataPoints = (result.raw.Data || []).length;
6846+
const normalizedKeys = Object.keys(result.normalized || {}).length;
68146847
this.log.debug(
6815-
`EU Data Act: ${vin} dataset=${result.datasetName} (${result.datasetCreatedOn}) ` +
6816-
`points=${(result.raw.Data || []).length}`,
6848+
`EU Data Act: ${vin} fetched dataset in ${elapsed}ms - ` +
6849+
`name=${result.datasetName} ` +
6850+
`createdOn=${result.datasetCreatedOn || "?"} ` +
6851+
`rawPoints=${dataPoints} ` +
6852+
`normalizedKeys=${normalizedKeys} ` +
6853+
`listSeen=${result.datasetCount || "?"}/${result.contentCount || "?"} ` +
6854+
`bytes=${result.byteSize || "?"} ` +
6855+
`inner=${result.fileName}`,
68176856
);
6818-
await this.json2iob.parse(vin + ".statuseudata", result.normalized, { forceIndex: true });
6819-
await this.setStateAsync(vin + ".statuseudata._dataset_name", result.datasetName, true);
6857+
// json2iob creates the root channel + every leaf state itself; we just
6858+
// tag the dataset metadata into the same object so it gets the same
6859+
// treatment (no manual extendObject/setState dance needed).
6860+
const payload = {
6861+
...result.normalized,
6862+
_dataset_name: result.datasetName,
6863+
};
68206864
if (result.datasetCreatedOn) {
6821-
await this.setStateAsync(vin + ".statuseudata._dataset_created_on", result.datasetCreatedOn, true);
6865+
payload._dataset_created_on = result.datasetCreatedOn;
68226866
}
68236867
if (this.config.rawJson) {
6824-
await this.extendObjectAsync(vin + ".statuseudata.rawJson", {
6825-
type: "state",
6826-
common: {
6827-
name: "Raw EU Data Act dataset JSON",
6828-
role: "json",
6829-
type: "string",
6830-
read: true,
6831-
write: false,
6832-
},
6833-
native: {},
6834-
});
6835-
await this.setStateAsync(vin + ".statuseudata.rawJson", JSON.stringify(result.raw), true);
6868+
payload._raw_json = JSON.stringify(result.raw);
68366869
}
6870+
await this.json2iob.parse(vin + ".statuseudata", payload, {
6871+
forceIndex: true,
6872+
channelName: "EU Data Act 15-min dataset",
6873+
});
6874+
// Drop-locked rescheduling: the portal publishes one dataset per
6875+
// 15-min slot, timestamped in createdOn. Aim for createdOn + 15min +
6876+
// 45s buffer so we hit the new file as soon as it's provisioned. If
6877+
// that target is already in the past (the slot is overdue), retry in
6878+
// 60s until the new file shows up.
6879+
if (result.datasetCreatedOn) {
6880+
const created = Date.parse(result.datasetCreatedOn);
6881+
if (!isNaN(created)) {
6882+
const target = created + 15 * 60 * 1000 + 45 * 1000;
6883+
const delta = target - Date.now();
6884+
return delta > 30 * 1000 ? delta : 60 * 1000;
6885+
}
6886+
}
6887+
return 60 * 1000;
68376888
} catch (err) {
68386889
if (err && err.code === "NO_CONTENT") {
68396890
this.log.debug(`EU Data Act: ${vin} no content datasets yet`);
6840-
return;
6891+
// The portal has nothing yet (fresh setup or car asleep). 1-min
6892+
// retries until the first dataset shows up; once it does we lock
6893+
// onto the 15-min cadence.
6894+
return 60 * 1000;
68416895
}
68426896
// The lib already retries once on 401/403 internally; if it still fails
68436897
// here the session is genuinely dead and a manual re-login won't help.
@@ -6851,6 +6905,7 @@ class VwWeconnect extends utils.Adapter {
68516905
if (this.euDataActIdentifiers) delete this.euDataActIdentifiers[vin];
68526906
}
68536907
this.log.error(`EU Data Act: status fetch failed for ${vin}: ${err.message || err}`);
6908+
return null;
68546909
}
68556910
}
68566911

@@ -6865,6 +6920,10 @@ class VwWeconnect extends utils.Adapter {
68656920
clearTimeout(this.refreshTokenTimeout);
68666921
clearTimeout(this.refreshTimeout);
68676922
clearTimeout(this.restartTimeout);
6923+
if (this.euDataActTimers) {
6924+
for (const t of Object.values(this.euDataActTimers)) clearTimeout(t);
6925+
this.euDataActTimers = {};
6926+
}
68686927
this.mqttClient && this.mqttClient.end();
68696928
callback();
68706929
} catch (e) {

0 commit comments

Comments
 (0)