iobroker.parcelapp 0.4.1 → 0.4.2
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/README.md +7 -3
- package/build/lib/coerce.js +9 -0
- package/build/lib/coerce.js.map +2 -2
- package/build/lib/parcel-client.js +71 -6
- package/build/lib/parcel-client.js.map +2 -2
- package/build/lib/state-manager.js +62 -3
- package/build/lib/state-manager.js.map +2 -2
- package/build/main.js +51 -25
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,6 +122,13 @@ The delivery is added to your parcel.app account and immediately appears in ioBr
|
|
|
122
122
|
Placeholder for the next version (at the beginning of the line):
|
|
123
123
|
### **WORK IN PROGRESS**
|
|
124
124
|
-->
|
|
125
|
+
### 0.4.2 (2026-05-10)
|
|
126
|
+
|
|
127
|
+
- Adapter shuts down cleanly even if parcel.app is slow — pending requests are aborted instead of hanging until kill.
|
|
128
|
+
- "Forbidden" responses (e.g. when the Premium subscription is no longer active) now log a clear hint pointing to your parcel.app account, instead of looping reauth as if the API key were just wrong.
|
|
129
|
+
- Two parcels whose tracking numbers differ only in special characters no longer overwrite each other in the state tree — the second one gets a hash suffix.
|
|
130
|
+
- Defensive: bogus poll-interval values can no longer turn into a tight loop hammering the API; rate-limit cooldowns can no longer get stuck near zero.
|
|
131
|
+
|
|
125
132
|
### 0.4.1 (2026-05-09)
|
|
126
133
|
|
|
127
134
|
- Adapter log messages are now English only, in line with the ioBroker community standard. Localized state names (11 languages) are unchanged.
|
|
@@ -137,9 +144,6 @@ The delivery is added to your parcel.app account and immediately appears in ioBr
|
|
|
137
144
|
### 0.3.1 (2026-05-01)
|
|
138
145
|
- Documentation cleanup. No code changes.
|
|
139
146
|
|
|
140
|
-
### 0.3.0 (2026-04-30)
|
|
141
|
-
- Internal cleanup. No user-facing changes.
|
|
142
|
-
|
|
143
147
|
Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
144
148
|
|
|
145
149
|
## Support
|
package/build/lib/coerce.js
CHANGED
|
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var coerce_exports = {};
|
|
20
20
|
__export(coerce_exports, {
|
|
21
21
|
coerceBoolean: () => coerceBoolean,
|
|
22
|
+
coerceClampedInt: () => coerceClampedInt,
|
|
22
23
|
coerceFiniteNumber: () => coerceFiniteNumber,
|
|
23
24
|
coerceString: () => coerceString,
|
|
24
25
|
errText: () => errText,
|
|
@@ -87,9 +88,17 @@ function errText(err) {
|
|
|
87
88
|
return Object.prototype.toString.call(err);
|
|
88
89
|
}
|
|
89
90
|
}
|
|
91
|
+
function coerceClampedInt(raw, min, max, defaultValue) {
|
|
92
|
+
const n = typeof raw === "number" ? raw : typeof raw === "string" ? parseFloat(raw) : NaN;
|
|
93
|
+
if (!Number.isFinite(n)) {
|
|
94
|
+
return defaultValue;
|
|
95
|
+
}
|
|
96
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
97
|
+
}
|
|
90
98
|
// Annotate the CommonJS export names for ESM import in node:
|
|
91
99
|
0 && (module.exports = {
|
|
92
100
|
coerceBoolean,
|
|
101
|
+
coerceClampedInt,
|
|
93
102
|
coerceFiniteNumber,
|
|
94
103
|
coerceString,
|
|
95
104
|
errText,
|
package/build/lib/coerce.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/coerce.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Boundary coercion helpers for external API data.\n *\n * The parcel.app API is documented but field types still drift in practice\n * (rare success-flag returned as `\"true\"` string, occasional null where a\n * number is expected). These helpers guard against NaN/Infinity/non-string\n * values reaching ioBroker states.\n */\n\n// Strict decimal regex \u2014 only optional minus sign + digits + optional fractional part.\n// Rejects HEX (`0x...`), exponential (`1e3`), Infinity, NaN, leading/trailing whitespace.\n// Hassemu (E8 in v1.9.0) hardened the same coerce-helper this way; homewizard\n// adopted it in v0.7.2 (D8). Consistent with both adapters.\nconst DECIMAL_NUMBER_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Coerce to a finite number or null.\n * Accepts numbers directly; parses strict decimal strings; rejects NaN, Infinity,\n * HEX (`0x...`) and exponential notation (`1e3`).\n *\n * @param value Unknown external value\n */\nexport function coerceFiniteNumber(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n if (typeof value === \"string\" && DECIMAL_NUMBER_RE.test(value)) {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/**\n * Coerce to a non-empty string, or null.\n *\n * @param value Unknown external value\n */\nexport function coerceString(value: unknown): string | null {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n return null;\n}\n\n/**\n * Coerce to a boolean (only `true`/`false` accepted \u2014 no truthy/falsy JS rules).\n *\n * @param value Unknown external value\n */\nexport function coerceBoolean(value: unknown): boolean | null {\n if (typeof value === \"boolean\") {\n return value;\n }\n return null;\n}\n\n/**\n * Guard for plain objects (not arrays, not null).\n *\n * @param value Unknown external value\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Coerce a parcel.app `success` flag. The API returns a real boolean in normal\n * operation, but the guard accepts common string/number encodings (`1`, `\"true\"`,\n * `\"1\"`) so a one-off drift doesn't break the entire poll cycle.\n *\n * @param v Value to interpret as a success flag\n */\nexport function isTrueish(v: unknown): boolean {\n if (typeof v === \"boolean\") {\n return v;\n }\n if (typeof v === \"number\") {\n return v === 1;\n }\n if (typeof v === \"string\") {\n const s = v.toLowerCase();\n return s === \"true\" || s === \"1\";\n }\n return false;\n}\n\n/**\n * Extract a log-friendly message from a thrown / rejected value. Centralizes the\n * `err instanceof Error ? err.message : String(err)` pattern that otherwise\n * gets repeated at every catch-site. Plain objects are JSON-stringified so a\n * `[object Object]` log is avoided when callers throw bag-of-fields.\n *\n * @param err Caught value of unknown shape (Error, string, undefined, ...).\n */\nexport function errText(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n if (err === null) {\n return \"null\";\n }\n if (err === undefined) {\n return \"undefined\";\n }\n if (typeof err === \"string\") {\n return err;\n }\n if (typeof err === \"number\" || typeof err === \"boolean\" || typeof err === \"bigint\") {\n return String(err);\n }\n // Plain objects + symbols would otherwise stringify to \"[object Object]\" / fail.\n // Prefer JSON for the common case so the log is at least diagnosable.\n try {\n return JSON.stringify(err);\n } catch {\n return Object.prototype.toString.call(err);\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,MAAM,oBAAoB;AASnB,SAAS,mBAAmB,OAA+B;AAChE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,EAC1C;AACA,MAAI,OAAO,UAAU,YAAY,kBAAkB,KAAK,KAAK,GAAG;AAC9D,UAAM,IAAI,OAAO,KAAK;AACtB,WAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAOO,SAAS,aAAa,OAA+B;AAC1D,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,cAAc,OAAgC;AAC5D,MAAI,OAAO,UAAU,WAAW;AAC9B,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,cAAc,OAAkD;AAC9E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AASO,SAAS,UAAU,GAAqB;AAC7C,MAAI,OAAO,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,WAAO,MAAM;AAAA,EACf;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,UAAM,IAAI,EAAE,YAAY;AACxB,WAAO,MAAM,UAAU,MAAM;AAAA,EAC/B;AACA,SAAO;AACT;AAUO,SAAS,QAAQ,KAAsB;AAC5C,MAAI,eAAe,OAAO;AACxB,WAAO,IAAI;AAAA,EACb;AACA,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,aAAa,OAAO,QAAQ,UAAU;AAClF,WAAO,OAAO,GAAG;AAAA,EACnB;AAGA,MAAI;AACF,WAAO,KAAK,UAAU,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO,OAAO,UAAU,SAAS,KAAK,GAAG;AAAA,EAC3C;AACF;",
|
|
4
|
+
"sourcesContent": ["/**\n * Boundary coercion helpers for external API data.\n *\n * The parcel.app API is documented but field types still drift in practice\n * (rare success-flag returned as `\"true\"` string, occasional null where a\n * number is expected). These helpers guard against NaN/Infinity/non-string\n * values reaching ioBroker states.\n */\n\n// Strict decimal regex \u2014 only optional minus sign + digits + optional fractional part.\n// Rejects HEX (`0x...`), exponential (`1e3`), Infinity, NaN, leading/trailing whitespace.\n// Hassemu (E8 in v1.9.0) hardened the same coerce-helper this way; homewizard\n// adopted it in v0.7.2 (D8). Consistent with both adapters.\nconst DECIMAL_NUMBER_RE = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Coerce to a finite number or null.\n * Accepts numbers directly; parses strict decimal strings; rejects NaN, Infinity,\n * HEX (`0x...`) and exponential notation (`1e3`).\n *\n * @param value Unknown external value\n */\nexport function coerceFiniteNumber(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n if (typeof value === \"string\" && DECIMAL_NUMBER_RE.test(value)) {\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\n/**\n * Coerce to a non-empty string, or null.\n *\n * @param value Unknown external value\n */\nexport function coerceString(value: unknown): string | null {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n return null;\n}\n\n/**\n * Coerce to a boolean (only `true`/`false` accepted \u2014 no truthy/falsy JS rules).\n *\n * @param value Unknown external value\n */\nexport function coerceBoolean(value: unknown): boolean | null {\n if (typeof value === \"boolean\") {\n return value;\n }\n return null;\n}\n\n/**\n * Guard for plain objects (not arrays, not null).\n *\n * @param value Unknown external value\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Coerce a parcel.app `success` flag. The API returns a real boolean in normal\n * operation, but the guard accepts common string/number encodings (`1`, `\"true\"`,\n * `\"1\"`) so a one-off drift doesn't break the entire poll cycle.\n *\n * @param v Value to interpret as a success flag\n */\nexport function isTrueish(v: unknown): boolean {\n if (typeof v === \"boolean\") {\n return v;\n }\n if (typeof v === \"number\") {\n return v === 1;\n }\n if (typeof v === \"string\") {\n const s = v.toLowerCase();\n return s === \"true\" || s === \"1\";\n }\n return false;\n}\n\n/**\n * Extract a log-friendly message from a thrown / rejected value. Centralizes the\n * `err instanceof Error ? err.message : String(err)` pattern that otherwise\n * gets repeated at every catch-site. Plain objects are JSON-stringified so a\n * `[object Object]` log is avoided when callers throw bag-of-fields.\n *\n * @param err Caught value of unknown shape (Error, string, undefined, ...).\n */\nexport function errText(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n if (err === null) {\n return \"null\";\n }\n if (err === undefined) {\n return \"undefined\";\n }\n if (typeof err === \"string\") {\n return err;\n }\n if (typeof err === \"number\" || typeof err === \"boolean\" || typeof err === \"bigint\") {\n return String(err);\n }\n // Plain objects + symbols would otherwise stringify to \"[object Object]\" / fail.\n // Prefer JSON for the common case so the log is at least diagnosable.\n try {\n return JSON.stringify(err);\n } catch {\n return Object.prototype.toString.call(err);\n }\n}\n\n/**\n * v0.4.2 (X5): coerce an admin-config integer setting (number-or-string)\n * to a finite, clamped integer. Returns `defaultValue` for non-finite\n * input \u2014 guards against `setInterval(fn, NaN)` tight-loops when the\n * config field happens to come back as a string from the admin UI.\n *\n * @param raw Raw value from `this.config.<field>`.\n * @param min Inclusive lower bound.\n * @param max Inclusive upper bound.\n * @param defaultValue Fallback when raw is missing or unparseable.\n */\nexport function coerceClampedInt(raw: unknown, min: number, max: number, defaultValue: number): number {\n const n = typeof raw === \"number\" ? raw : typeof raw === \"string\" ? parseFloat(raw) : NaN;\n if (!Number.isFinite(n)) {\n return defaultValue;\n }\n return Math.max(min, Math.min(max, Math.floor(n)));\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,MAAM,oBAAoB;AASnB,SAAS,mBAAmB,OAA+B;AAChE,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,EAC1C;AACA,MAAI,OAAO,UAAU,YAAY,kBAAkB,KAAK,KAAK,GAAG;AAC9D,UAAM,IAAI,OAAO,KAAK;AACtB,WAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAOO,SAAS,aAAa,OAA+B;AAC1D,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,cAAc,OAAgC;AAC5D,MAAI,OAAO,UAAU,WAAW;AAC9B,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAOO,SAAS,cAAc,OAAkD;AAC9E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AASO,SAAS,UAAU,GAAqB;AAC7C,MAAI,OAAO,MAAM,WAAW;AAC1B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,WAAO,MAAM;AAAA,EACf;AACA,MAAI,OAAO,MAAM,UAAU;AACzB,UAAM,IAAI,EAAE,YAAY;AACxB,WAAO,MAAM,UAAU,MAAM;AAAA,EAC/B;AACA,SAAO;AACT;AAUO,SAAS,QAAQ,KAAsB;AAC5C,MAAI,eAAe,OAAO;AACxB,WAAO,IAAI;AAAA,EACb;AACA,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,QAAW;AACrB,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,EACT;AACA,MAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,aAAa,OAAO,QAAQ,UAAU;AAClF,WAAO,OAAO,GAAG;AAAA,EACnB;AAGA,MAAI;AACF,WAAO,KAAK,UAAU,GAAG;AAAA,EAC3B,QAAQ;AACN,WAAO,OAAO,UAAU,SAAS,KAAK,GAAG;AAAA,EAC3C;AACF;AAaO,SAAS,iBAAiB,KAAc,KAAa,KAAa,cAA8B;AACrG,QAAM,IAAI,OAAO,QAAQ,WAAW,MAAM,OAAO,QAAQ,WAAW,WAAW,GAAG,IAAI;AACtF,MAAI,CAAC,OAAO,SAAS,CAAC,GAAG;AACvB,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;AACnD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -35,13 +35,29 @@ var https = __toESM(require("node:https"));
|
|
|
35
35
|
var import_coerce = require("./coerce");
|
|
36
36
|
const API_BASE = "https://api.parcel.app/external";
|
|
37
37
|
const REQUEST_TIMEOUT = 15e3;
|
|
38
|
+
const MAX_BODY_BYTES = 1 << 20;
|
|
38
39
|
class ParcelClient {
|
|
39
40
|
apiKey;
|
|
40
41
|
carrierCache = null;
|
|
42
|
+
/**
|
|
43
|
+
* v0.4.2 (P1): per-request AbortController. `cancelAll()` aborts every
|
|
44
|
+
* pending HTTPS request — called from the adapter's `onUnload` so a slow
|
|
45
|
+
* parcel.app endpoint can't keep the adapter alive past js-controller's
|
|
46
|
+
* 4-second kill deadline.
|
|
47
|
+
*/
|
|
48
|
+
inflight = /* @__PURE__ */ new Set();
|
|
41
49
|
/** @param apiKey The parcel.app API key */
|
|
42
50
|
constructor(apiKey) {
|
|
43
51
|
this.apiKey = apiKey;
|
|
44
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.
|
|
55
|
+
*/
|
|
56
|
+
cancelAll() {
|
|
57
|
+
for (const ctrl of this.inflight) {
|
|
58
|
+
ctrl.abort();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
45
61
|
/**
|
|
46
62
|
* Fetch deliveries from parcel.app.
|
|
47
63
|
*
|
|
@@ -125,7 +141,15 @@ class ParcelClient {
|
|
|
125
141
|
*/
|
|
126
142
|
request(method, path, authenticated, body) {
|
|
127
143
|
return new Promise((resolve, reject) => {
|
|
128
|
-
|
|
144
|
+
let url;
|
|
145
|
+
try {
|
|
146
|
+
url = new URL(`${API_BASE}${path}`);
|
|
147
|
+
} catch {
|
|
148
|
+
const err = new Error(`Invalid URL: ${API_BASE}${path}`);
|
|
149
|
+
err.code = "INVALID_URL";
|
|
150
|
+
reject(err);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
129
153
|
const headers = {};
|
|
130
154
|
if (authenticated) {
|
|
131
155
|
headers["api-key"] = this.apiKey;
|
|
@@ -141,23 +165,57 @@ class ParcelClient {
|
|
|
141
165
|
headers,
|
|
142
166
|
timeout: REQUEST_TIMEOUT
|
|
143
167
|
};
|
|
168
|
+
const ctrl = new AbortController();
|
|
169
|
+
this.inflight.add(ctrl);
|
|
170
|
+
const cleanup = () => {
|
|
171
|
+
this.inflight.delete(ctrl);
|
|
172
|
+
};
|
|
144
173
|
const req = https.request(options, (res) => {
|
|
145
174
|
const chunks = [];
|
|
146
|
-
|
|
147
|
-
|
|
175
|
+
let bodyBytes = 0;
|
|
176
|
+
let oversized = false;
|
|
177
|
+
res.on("error", (err) => {
|
|
178
|
+
cleanup();
|
|
179
|
+
reject(err);
|
|
180
|
+
});
|
|
181
|
+
res.on("data", (chunk) => {
|
|
182
|
+
if (oversized) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
bodyBytes += chunk.length;
|
|
186
|
+
if (bodyBytes > MAX_BODY_BYTES) {
|
|
187
|
+
oversized = true;
|
|
188
|
+
req.destroy(new Error(`Response body exceeds ${MAX_BODY_BYTES} bytes`));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
chunks.push(chunk);
|
|
192
|
+
});
|
|
148
193
|
res.on("end", () => {
|
|
194
|
+
cleanup();
|
|
195
|
+
if (oversized) {
|
|
196
|
+
const err = new Error("Response body too large");
|
|
197
|
+
err.code = "BODY_TOO_LARGE";
|
|
198
|
+
reject(err);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
149
201
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
150
202
|
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
151
203
|
if (res.statusCode === 429) {
|
|
152
204
|
const retryAfter = parseInt(res.headers["retry-after"] || "", 10);
|
|
153
205
|
const err2 = new Error("Rate limit exceeded");
|
|
154
206
|
err2.code = "RATE_LIMITED";
|
|
155
|
-
err2.retryAfterSeconds = retryAfter > 0 ? retryAfter : 5 * 60;
|
|
207
|
+
err2.retryAfterSeconds = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;
|
|
156
208
|
reject(err2);
|
|
157
209
|
return;
|
|
158
210
|
}
|
|
159
211
|
const err = new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`);
|
|
160
|
-
|
|
212
|
+
if (res.statusCode === 401) {
|
|
213
|
+
err.code = "INVALID_API_KEY";
|
|
214
|
+
} else if (res.statusCode === 403) {
|
|
215
|
+
err.code = "FORBIDDEN";
|
|
216
|
+
} else {
|
|
217
|
+
err.code = "HTTP_ERROR";
|
|
218
|
+
}
|
|
161
219
|
reject(err);
|
|
162
220
|
return;
|
|
163
221
|
}
|
|
@@ -168,11 +226,18 @@ class ParcelClient {
|
|
|
168
226
|
}
|
|
169
227
|
});
|
|
170
228
|
});
|
|
229
|
+
ctrl.signal.addEventListener("abort", () => {
|
|
230
|
+
req.destroy(new Error("Request aborted"));
|
|
231
|
+
});
|
|
171
232
|
req.on("timeout", () => {
|
|
172
233
|
req.destroy();
|
|
234
|
+
cleanup();
|
|
173
235
|
reject(new Error("Request timeout"));
|
|
174
236
|
});
|
|
175
|
-
req.on("error", (err) =>
|
|
237
|
+
req.on("error", (err) => {
|
|
238
|
+
cleanup();
|
|
239
|
+
reject(err);
|
|
240
|
+
});
|
|
176
241
|
if (body) {
|
|
177
242
|
req.write(JSON.stringify(body));
|
|
178
243
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/parcel-client.ts"],
|
|
4
|
-
"sourcesContent": ["import * as https from \"node:https\";\nimport { isTrueish } from \"./coerce\";\nimport type { ParcelApiResponse, ParcelDelivery, AddDeliveryRequest, AddDeliveryResponse, CarrierMap } from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\nconst REQUEST_TIMEOUT = 15_000;\n\n/** HTTP client for the parcel.app API */\nexport class ParcelClient {\n private apiKey: string;\n private carrierCache: CarrierMap | null = null;\n\n /** @param apiKey The parcel.app API key */\n constructor(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * @param filterMode Filter active or recent deliveries\n */\n async getDeliveries(filterMode: \"active\" | \"recent\" = \"active\"): Promise<ParcelDelivery[]> {\n const response = await this.request<ParcelApiResponse>(\"GET\", `/deliveries/?filter_mode=${filterMode}`, true);\n\n // API-drift guard: response may be null or a non-object\n if (!response || typeof response !== \"object\") {\n const err = new Error(\"API error: malformed response\") as Error & {\n code: string;\n };\n err.code = \"API_ERROR\";\n throw err;\n }\n\n if (!isTrueish(response.success)) {\n const rawCode = typeof response.error_code === \"string\" ? response.error_code : \"\";\n const rawMsg = typeof response.error_message === \"string\" ? response.error_message : \"\";\n const code = rawCode || rawMsg || \"UNKNOWN\";\n const err = new Error(`API error: ${rawMsg || code}`) as Error & {\n code: string;\n };\n err.code = rawCode === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\";\n throw err;\n }\n\n // API-drift guard: deliveries must be an array\n return Array.isArray(response.deliveries) ? response.deliveries : [];\n }\n\n /**\n * Add a new delivery to parcel.app.\n *\n * @param delivery The delivery to add\n */\n async addDelivery(delivery: AddDeliveryRequest): Promise<AddDeliveryResponse> {\n return this.request<AddDeliveryResponse>(\"POST\", \"/add-delivery/\", true, delivery);\n }\n\n /** Get carrier names (cached after first call) */\n async getCarrierNames(): Promise<CarrierMap> {\n if (this.carrierCache) {\n return this.carrierCache;\n }\n\n try {\n const raw = await this.request<unknown>(\"GET\", \"/supported_carriers.json\", false);\n // API-drift guard: must be a plain object (not null, array, or primitive)\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n this.carrierCache = raw as CarrierMap;\n } else {\n return {};\n }\n } catch {\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\n\n return this.carrierCache;\n }\n\n /**\n * Resolve a carrier code to a display name.\n *\n * @param carrierCode The carrier code from API\n */\n async getCarrierName(carrierCode: unknown): Promise<string> {\n // API-drift guard: non-string codes fall back to \"UNKNOWN\"\n if (typeof carrierCode !== \"string\" || carrierCode.length === 0) {\n return \"UNKNOWN\";\n }\n const carriers = await this.getCarrierNames();\n const mapped = carriers[carrierCode];\n return typeof mapped === \"string\" && mapped.length > 0 ? mapped : carrierCode.toUpperCase();\n }\n\n /** Test if the API key is valid */\n async testConnection(): Promise<{ success: boolean; message: string }> {\n try {\n await this.getDeliveries(\"active\");\n return { success: true, message: \"Connection successful\" };\n } catch (err) {\n const error = err as Error & { code?: string };\n if (error.code === \"INVALID_API_KEY\") {\n return { success: false, message: \"Invalid API key\" };\n }\n return { success: false, message: error.message };\n }\n }\n\n /**\n * Execute an HTTP request against the parcel.app API.\n *\n * @param method HTTP method\n * @param path API path\n * @param authenticated Whether to send the API key\n * @param body Optional request body\n */\n private request<T>(method: string, path: string, authenticated: boolean, body?: unknown): Promise<T> {\n return new Promise((resolve, reject) => {\n
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;AACvB,oBAA0B;AAG1B,MAAM,WAAW;AACjB,MAAM,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import * as https from \"node:https\";\nimport { isTrueish } from \"./coerce\";\nimport type { ParcelApiResponse, ParcelDelivery, AddDeliveryRequest, AddDeliveryResponse, CarrierMap } from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\nconst REQUEST_TIMEOUT = 15_000;\n/**\n * v0.4.2 (P9): hard cap on response body size. parcel.app deliveries lists\n * are tiny (~1 kB per package, max ~50 packages = 50 kB), so a 1 MiB cap is\n * 20\u00D7 the realistic max while still defending against a runaway response.\n */\nconst MAX_BODY_BYTES = 1 << 20; // 1 MiB\n\n/** HTTP client for the parcel.app API */\nexport class ParcelClient {\n private apiKey: string;\n private carrierCache: CarrierMap | null = null;\n /**\n * v0.4.2 (P1): per-request AbortController. `cancelAll()` aborts every\n * pending HTTPS request \u2014 called from the adapter's `onUnload` so a slow\n * parcel.app endpoint can't keep the adapter alive past js-controller's\n * 4-second kill deadline.\n */\n private readonly inflight = new Set<AbortController>();\n\n /** @param apiKey The parcel.app API key */\n constructor(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.\n */\n cancelAll(): void {\n for (const ctrl of this.inflight) {\n ctrl.abort();\n }\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * @param filterMode Filter active or recent deliveries\n */\n async getDeliveries(filterMode: \"active\" | \"recent\" = \"active\"): Promise<ParcelDelivery[]> {\n const response = await this.request<ParcelApiResponse>(\"GET\", `/deliveries/?filter_mode=${filterMode}`, true);\n\n // API-drift guard: response may be null or a non-object\n if (!response || typeof response !== \"object\") {\n const err = new Error(\"API error: malformed response\") as Error & {\n code: string;\n };\n err.code = \"API_ERROR\";\n throw err;\n }\n\n if (!isTrueish(response.success)) {\n const rawCode = typeof response.error_code === \"string\" ? response.error_code : \"\";\n const rawMsg = typeof response.error_message === \"string\" ? response.error_message : \"\";\n const code = rawCode || rawMsg || \"UNKNOWN\";\n const err = new Error(`API error: ${rawMsg || code}`) as Error & {\n code: string;\n };\n err.code = rawCode === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\";\n throw err;\n }\n\n // API-drift guard: deliveries must be an array\n return Array.isArray(response.deliveries) ? response.deliveries : [];\n }\n\n /**\n * Add a new delivery to parcel.app.\n *\n * @param delivery The delivery to add\n */\n async addDelivery(delivery: AddDeliveryRequest): Promise<AddDeliveryResponse> {\n return this.request<AddDeliveryResponse>(\"POST\", \"/add-delivery/\", true, delivery);\n }\n\n /** Get carrier names (cached after first call) */\n async getCarrierNames(): Promise<CarrierMap> {\n if (this.carrierCache) {\n return this.carrierCache;\n }\n\n try {\n const raw = await this.request<unknown>(\"GET\", \"/supported_carriers.json\", false);\n // API-drift guard: must be a plain object (not null, array, or primitive)\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n this.carrierCache = raw as CarrierMap;\n } else {\n return {};\n }\n } catch {\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\n\n return this.carrierCache;\n }\n\n /**\n * Resolve a carrier code to a display name.\n *\n * @param carrierCode The carrier code from API\n */\n async getCarrierName(carrierCode: unknown): Promise<string> {\n // API-drift guard: non-string codes fall back to \"UNKNOWN\"\n if (typeof carrierCode !== \"string\" || carrierCode.length === 0) {\n return \"UNKNOWN\";\n }\n const carriers = await this.getCarrierNames();\n const mapped = carriers[carrierCode];\n return typeof mapped === \"string\" && mapped.length > 0 ? mapped : carrierCode.toUpperCase();\n }\n\n /** Test if the API key is valid */\n async testConnection(): Promise<{ success: boolean; message: string }> {\n try {\n await this.getDeliveries(\"active\");\n return { success: true, message: \"Connection successful\" };\n } catch (err) {\n const error = err as Error & { code?: string };\n if (error.code === \"INVALID_API_KEY\") {\n return { success: false, message: \"Invalid API key\" };\n }\n return { success: false, message: error.message };\n }\n }\n\n /**\n * Execute an HTTP request against the parcel.app API.\n *\n * @param method HTTP method\n * @param path API path\n * @param authenticated Whether to send the API key\n * @param body Optional request body\n */\n private request<T>(method: string, path: string, authenticated: boolean, body?: unknown): Promise<T> {\n return new Promise((resolve, reject) => {\n // v0.4.2 (E3): URL-shape validation defensive \u2014 paths are hardcoded\n // upstream but a future caller could pass garbage; surface a clear\n // error class instead of a TypeError thrown sync from the executor.\n let url: URL;\n try {\n url = new URL(`${API_BASE}${path}`);\n } catch {\n const err = new Error(`Invalid URL: ${API_BASE}${path}`) as Error & { code: string };\n err.code = \"INVALID_URL\";\n reject(err);\n return;\n }\n\n const headers: Record<string, string> = {};\n if (authenticated) {\n headers[\"api-key\"] = this.apiKey;\n }\n if (body) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n const options: https.RequestOptions = {\n hostname: url.hostname,\n port: url.port || 443,\n path: url.pathname + url.search,\n method,\n headers,\n timeout: REQUEST_TIMEOUT,\n };\n\n // v0.4.2 (P1): per-request AbortController. `cancelAll()` (called\n // from `onUnload`) aborts everything pending without waiting for\n // the configured timeout.\n const ctrl = new AbortController();\n this.inflight.add(ctrl);\n const cleanup = (): void => {\n this.inflight.delete(ctrl);\n };\n\n const req = https.request(options, res => {\n const chunks: Buffer[] = [];\n let bodyBytes = 0;\n let oversized = false;\n\n res.on(\"error\", err => {\n cleanup();\n reject(err);\n });\n res.on(\"data\", (chunk: Buffer) => {\n if (oversized) {\n return;\n }\n bodyBytes += chunk.length;\n // v0.4.2 (P9): drop the connection on oversized responses so a\n // compromised or misconfigured endpoint can't OOM the adapter.\n if (bodyBytes > MAX_BODY_BYTES) {\n oversized = true;\n req.destroy(new Error(`Response body exceeds ${MAX_BODY_BYTES} bytes`));\n return;\n }\n chunks.push(chunk);\n });\n res.on(\"end\", () => {\n cleanup();\n if (oversized) {\n const err = new Error(\"Response body too large\") as Error & { code: string };\n err.code = \"BODY_TOO_LARGE\";\n reject(err);\n return;\n }\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n\n if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {\n if (res.statusCode === 429) {\n // v0.4.2 (P6): clamp Retry-After parser. Bogus values (0,\n // negative, NaN) used to fall through `>0` and default to\n // 5 min \u2014 keep that, but also reject infinity/extreme.\n const retryAfter = parseInt(res.headers[\"retry-after\"] || \"\", 10);\n const err = new Error(\"Rate limit exceeded\") as Error & {\n code: string;\n retryAfterSeconds: number;\n };\n err.code = \"RATE_LIMITED\";\n err.retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;\n reject(err);\n return;\n }\n // v0.4.2 (P3): split 401 (invalid key) from 403 (permission /\n // no premium). Adapter treats them differently \u2014 INVALID_API_KEY\n // says \"fix the key\", FORBIDDEN says \"fix the account\".\n const err = new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`) as Error & { code: string };\n if (res.statusCode === 401) {\n err.code = \"INVALID_API_KEY\";\n } else if (res.statusCode === 403) {\n err.code = \"FORBIDDEN\";\n } else {\n err.code = \"HTTP_ERROR\";\n }\n reject(err);\n return;\n }\n\n try {\n resolve(JSON.parse(raw) as T);\n } catch {\n reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));\n }\n });\n });\n\n ctrl.signal.addEventListener(\"abort\", () => {\n req.destroy(new Error(\"Request aborted\"));\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n cleanup();\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", err => {\n cleanup();\n reject(err);\n });\n\n if (body) {\n req.write(JSON.stringify(body));\n }\n req.end();\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["err"]
|
|
7
7
|
}
|
|
@@ -154,6 +154,11 @@ class StateManager {
|
|
|
154
154
|
/**
|
|
155
155
|
* Build a unique package ID from a delivery.
|
|
156
156
|
*
|
|
157
|
+
* v0.4.2 (S3): when the bare `sanitize(tracking_number)` collides with
|
|
158
|
+
* another active package (e.g. two trackings differ only in special
|
|
159
|
+
* chars that strip down to the same id), append a stable hash of the
|
|
160
|
+
* full tracking number so both end up at distinct state IDs.
|
|
161
|
+
*
|
|
157
162
|
* @param delivery The delivery to build an ID for
|
|
158
163
|
*/
|
|
159
164
|
packageId(delivery) {
|
|
@@ -161,8 +166,53 @@ class StateManager {
|
|
|
161
166
|
if (typeof delivery.extra_information === "string" && delivery.extra_information.length > 0) {
|
|
162
167
|
id += `_${this.sanitize(delivery.extra_information)}`;
|
|
163
168
|
}
|
|
169
|
+
const owner = this.idOwner.get(id);
|
|
170
|
+
const rawKey = StateManager.rawIdKey(delivery);
|
|
171
|
+
if (owner !== void 0 && owner !== rawKey) {
|
|
172
|
+
const suffixed = `${id}__${StateManager.shortHash(rawKey)}`;
|
|
173
|
+
this.idOwner.set(suffixed, rawKey);
|
|
174
|
+
return suffixed;
|
|
175
|
+
}
|
|
176
|
+
this.idOwner.set(id, rawKey);
|
|
164
177
|
return id;
|
|
165
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* v0.4.2 (S3): build a stable raw-key for collision tracking.
|
|
181
|
+
*
|
|
182
|
+
* @param delivery The delivery whose raw tracking identifies it.
|
|
183
|
+
*/
|
|
184
|
+
static rawIdKey(delivery) {
|
|
185
|
+
const t = typeof delivery.tracking_number === "string" ? delivery.tracking_number : "";
|
|
186
|
+
const e = typeof delivery.extra_information === "string" ? delivery.extra_information : "";
|
|
187
|
+
return `${t}\0${e}`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* v0.4.2 (S3): FNV-1a 32-bit short hash → 6 hex chars.
|
|
191
|
+
*
|
|
192
|
+
* @param s Input string to hash.
|
|
193
|
+
*/
|
|
194
|
+
static shortHash(s) {
|
|
195
|
+
let h = 2166136261;
|
|
196
|
+
for (let i = 0; i < s.length; i++) {
|
|
197
|
+
h ^= s.charCodeAt(i);
|
|
198
|
+
h = Math.imul(h, 16777619);
|
|
199
|
+
}
|
|
200
|
+
return (h >>> 0).toString(16).padStart(8, "0").slice(0, 6);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* v0.4.2 (S3): which raw-tracking-key currently "owns" each sanitized id
|
|
204
|
+
* within the running poll. Cleared via `resetIdOwners()` between polls so
|
|
205
|
+
* the same delivery keeps its bare id as long as it's unique.
|
|
206
|
+
*/
|
|
207
|
+
idOwner = /* @__PURE__ */ new Map();
|
|
208
|
+
/**
|
|
209
|
+
* v0.4.2 (S3): reset the per-poll collision tracker. Call from main.ts
|
|
210
|
+
* before iterating deliveries so the bare id always wins for the first
|
|
211
|
+
* occurrence in each poll.
|
|
212
|
+
*/
|
|
213
|
+
resetPollState() {
|
|
214
|
+
this.idOwner.clear();
|
|
215
|
+
}
|
|
166
216
|
/**
|
|
167
217
|
* Update or create all states for a delivery.
|
|
168
218
|
*
|
|
@@ -272,18 +322,27 @@ class StateManager {
|
|
|
272
322
|
startkey: `${this.adapter.namespace}.deliveries.`,
|
|
273
323
|
endkey: `${this.adapter.namespace}.deliveries.\u9999`
|
|
274
324
|
});
|
|
325
|
+
if (!(objects == null ? void 0 : objects.rows)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const toDelete = [];
|
|
275
329
|
for (const row of objects.rows) {
|
|
276
330
|
const relativeId = row.id.replace(`${this.adapter.namespace}.`, "");
|
|
277
331
|
if (relativeId.startsWith("deliveries.") && !activeSet.has(relativeId)) {
|
|
332
|
+
toDelete.push(relativeId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
await Promise.all(
|
|
336
|
+
toDelete.map(async (relativeId) => {
|
|
278
337
|
await this.adapter.delObjectAsync(relativeId, { recursive: true });
|
|
279
338
|
this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);
|
|
280
|
-
for (const id of this.createdIds) {
|
|
339
|
+
for (const id of [...this.createdIds]) {
|
|
281
340
|
if (id === relativeId || id.startsWith(`${relativeId}.`)) {
|
|
282
341
|
this.createdIds.delete(id);
|
|
283
342
|
}
|
|
284
343
|
}
|
|
285
|
-
}
|
|
286
|
-
|
|
344
|
+
})
|
|
345
|
+
);
|
|
287
346
|
}
|
|
288
347
|
/**
|
|
289
348
|
* Calculate delivery time window — only from Unix timestamps.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/state-manager.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AdapterInstance } from \"@iobroker/adapter-core\";\nimport { coerceFiniteNumber } from \"./coerce\";\nimport type { StateName } from \"./i18n-states\";\nimport { tName } from \"./i18n-states\";\nimport type { ParcelDelivery } from \"./types\";\nimport { STATUS_LABELS, SUPPORTED_LANGUAGES, FALLBACK_LANGUAGE } from \"./types\";\n\n/** Status codes that have expected delivery date/time */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Translation-object cast helper \u2014 ioBroker's `common.name` accepts `StringOrTranslated`.\n *\n * @param name Translation object from {@link STATE_NAMES}.\n */\nfunction asName(name: StateName): ioBroker.StringOrTranslated {\n return name;\n}\n\n/** Delivery-estimate labels keyed by language code. Keys must match STATUS_LABELS. */\nconst ESTIMATE_LABELS: Record<string, Record<string, string>> = {\n de: {\n overdue: \"\u00FCberf\u00E4llig\",\n today: \"heute\",\n tomorrow: \"morgen\",\n days: \"in %d Tagen\",\n },\n en: {\n overdue: \"overdue\",\n today: \"today\",\n tomorrow: \"tomorrow\",\n days: \"in %d days\",\n },\n ru: {\n overdue: \"\u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043E\",\n today: \"\u0441\u0435\u0433\u043E\u0434\u043D\u044F\",\n tomorrow: \"\u0437\u0430\u0432\u0442\u0440\u0430\",\n days: \"\u0447\u0435\u0440\u0435\u0437 %d \u0434\u043D.\",\n },\n pt: {\n overdue: \"atrasado\",\n today: \"hoje\",\n tomorrow: \"amanh\u00E3\",\n days: \"em %d dias\",\n },\n nl: {\n overdue: \"te laat\",\n today: \"vandaag\",\n tomorrow: \"morgen\",\n days: \"over %d dagen\",\n },\n fr: {\n overdue: \"en retard\",\n today: \"aujourd'hui\",\n tomorrow: \"demain\",\n days: \"dans %d jours\",\n },\n it: {\n overdue: \"in ritardo\",\n today: \"oggi\",\n tomorrow: \"domani\",\n days: \"tra %d giorni\",\n },\n es: {\n overdue: \"atrasado\",\n today: \"hoy\",\n tomorrow: \"ma\u00F1ana\",\n days: \"en %d d\u00EDas\",\n },\n pl: {\n overdue: \"zaleg\u0142e\",\n today: \"dzisiaj\",\n tomorrow: \"jutro\",\n days: \"za %d dni\",\n },\n uk: {\n overdue: \"\u043F\u0440\u043E\u0441\u0442\u0440\u043E\u0447\u0435\u043D\u043E\",\n today: \"\u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456\",\n tomorrow: \"\u0437\u0430\u0432\u0442\u0440\u0430\",\n days: \"\u0447\u0435\u0440\u0435\u0437 %d \u0434\u043D.\",\n },\n \"zh-cn\": {\n overdue: \"\u5DF2\u903E\u671F\",\n today: \"\u4ECA\u5929\",\n tomorrow: \"\u660E\u5929\",\n days: \"%d \u5929\u540E\",\n },\n};\n\n/**\n * Resolve a language code to one that has labels. Falls back to English\n * when the system language is not one of the supported ioBroker languages.\n *\n * @param language Raw language code (e.g. from system.config.language)\n */\nexport function resolveLanguage(language: unknown): string {\n if (typeof language === \"string\" && SUPPORTED_LANGUAGES.includes(language)) {\n return language;\n }\n return FALLBACK_LANGUAGE;\n}\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\n private language: string;\n /**\n * Cache of state IDs that have already passed `setObjectNotExistsAsync`.\n * Skips repeat DB lookups on the hot path \u2014 each poll touches ~11 states\n * per delivery, and most deliveries see no schema change between polls.\n * On `cleanupDeliveries`, IDs of removed packages are dropped so a re-add\n * triggers a fresh creation.\n */\n private readonly createdIds = new Set<string>();\n\n /**\n * @param adapter The ioBroker adapter instance\n * @param language Language code from system.config.language (falls back to English)\n */\n constructor(adapter: AdapterInstance, language: string) {\n this.adapter = adapter;\n this.language = resolveLanguage(language);\n }\n\n /**\n * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).\n * API-drift guard: returns \"unknown\" for non-string input.\n *\n * @param name Raw value to sanitize (any type)\n */\n sanitize(name: unknown): string {\n if (typeof name !== \"string\") {\n return \"unknown\";\n }\n return (\n name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 50) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. API documents `status_code` as\n * a numeric string, but we accept numbers too and fall back to 0 for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code as unknown;\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return Math.trunc(raw);\n }\n if (typeof raw === \"string\") {\n const n = parseInt(raw, 10);\n return Number.isFinite(n) ? n : 0;\n }\n return 0;\n }\n\n /**\n * Build a unique package ID from a delivery.\n *\n * @param delivery The delivery to build an ID for\n */\n packageId(delivery: ParcelDelivery): string {\n let id = this.sanitize(delivery.tracking_number);\n // API-drift guard: only string values extend the id\n if (typeof delivery.extra_information === \"string\" && delivery.extra_information.length > 0) {\n id += `_${this.sanitize(delivery.extra_information)}`;\n }\n return id;\n }\n\n /**\n * Update or create all states for a delivery.\n *\n * @param delivery The delivery data from API\n * @param carrierName Resolved carrier display name\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string): Promise<void> {\n const pkgId = this.packageId(delivery);\n const devicePath = `deliveries.${pkgId}`;\n\n const description = typeof delivery.description === \"string\" ? delivery.description : \"\";\n const trackingNumber = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const extraInfo = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n\n await this.adapter.extendObjectAsync(devicePath, {\n type: \"device\",\n common: {\n name: description || `Package ${trackingNumber || pkgId}`,\n },\n native: {},\n });\n\n const statusCode = this.parseStatus(delivery);\n const labels = STATUS_LABELS[this.language];\n const statusText = labels[statusCode] || `Unknown (${statusCode})`;\n\n await Promise.all([\n this.createAndSet(`${devicePath}.carrier`, asName(tName(\"carrier\")), \"string\", \"text\", carrierName),\n this.createAndSet(`${devicePath}.status`, asName(tName(\"status\")), \"string\", \"text\", statusText),\n this.createAndSet(`${devicePath}.statusCode`, asName(tName(\"statusCode\")), \"number\", \"value\", statusCode),\n this.createAndSet(`${devicePath}.description`, asName(tName(\"description\")), \"string\", \"text\", description),\n this.createAndSet(\n `${devicePath}.trackingNumber`,\n asName(tName(\"trackingNumber\")),\n \"string\",\n \"text\",\n trackingNumber,\n ),\n this.createAndSet(`${devicePath}.extraInfo`, asName(tName(\"extraInfo\")), \"string\", \"text\", extraInfo),\n this.createAndSet(\n `${devicePath}.deliveryWindow`,\n asName(tName(\"deliveryWindow\")),\n \"string\",\n \"text\",\n this.calculateDeliveryWindow(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.deliveryEstimate`,\n asName(tName(\"deliveryEstimate\")),\n \"string\",\n \"text\",\n this.calculateDeliveryEstimate(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.lastEvent`,\n asName(tName(\"lastEvent\")),\n \"string\",\n \"text\",\n this.formatLastEvent(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastLocation`,\n asName(tName(\"lastLocation\")),\n \"string\",\n \"text\",\n this.extractLastLocation(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastUpdated`,\n asName(tName(\"lastUpdated\")),\n \"string\",\n \"date\",\n new Date().toISOString(),\n ),\n ]);\n }\n\n /**\n * Update summary states. Expects already-filtered active deliveries.\n * The `summary` channel itself is declared via io-package.json instanceObjects.\n *\n * @param activeDeliveries Only active (non-delivered) deliveries\n */\n async updateSummary(activeDeliveries: ParcelDelivery[]): Promise<void> {\n const todayDeliveries = activeDeliveries.filter(d => this.isToday(d, this.parseStatus(d)));\n\n await Promise.all([\n this.createAndSet(\n \"summary.activeCount\",\n asName(tName(\"activeCount\")),\n \"number\",\n \"value\",\n activeDeliveries.length,\n ),\n this.createAndSet(\"summary.todayCount\", asName(tName(\"todayCount\")), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n asName(tName(\"summaryDeliveryWindow\")),\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer active.\n *\n * @param activeIds List of currently active package IDs\n */\n async cleanupDeliveries(activeIds: string[]): Promise<void> {\n const activeSet = new Set(activeIds.map(id => `deliveries.${id}`));\n\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.\u9999`,\n });\n\n for (const row of objects.rows) {\n const relativeId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (relativeId.startsWith(\"deliveries.\") && !activeSet.has(relativeId)) {\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n // Drop cached child IDs \u2014 re-pairing the same delivery must re-create\n // its states from scratch.\n for (const id of this.createdIds) {\n if (id === relativeId || id.startsWith(`${relativeId}.`)) {\n this.createdIds.delete(id);\n }\n }\n }\n }\n }\n\n /**\n * Calculate delivery time window \u2014 only from Unix timestamps.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(delivery: ParcelDelivery, statusCode: number): string {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return \"\";\n }\n\n const formatTime = (timestamp: unknown): string | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const d = new Date(ts * 1000);\n if (Number.isNaN(d.getTime())) {\n return null;\n }\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n };\n\n const start = formatTime(delivery.timestamp_expected);\n const end = formatTime(delivery.timestamp_expected_end);\n\n if (!start) {\n return \"\";\n }\n return end ? `${start} - ${end}` : start;\n }\n\n /**\n * Days from today to the expected delivery date. Returns null when the\n * delivery has no usable expected date or is in a non-trackable status.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private computeDiffDays(delivery: ParcelDelivery, statusCode: number): number | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n\n let expectedDate: Date | null = null;\n const ts = coerceFiniteNumber(delivery.timestamp_expected);\n if (ts !== null && ts > 0) {\n expectedDate = new Date(ts * 1000);\n } else if (typeof delivery.date_expected === \"string\" && delivery.date_expected.length > 0) {\n expectedDate = new Date(delivery.date_expected);\n }\n\n if (!expectedDate || isNaN(expectedDate.getTime())) {\n return null;\n }\n\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const expectedStart = new Date(expectedDate.getFullYear(), expectedDate.getMonth(), expectedDate.getDate());\n return Math.round((expectedStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24));\n }\n\n /**\n * Calculate human-readable delivery estimate.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryEstimate(delivery: ParcelDelivery, statusCode: number): string {\n const diffDays = this.computeDiffDays(delivery, statusCode);\n if (diffDays === null) {\n return \"\";\n }\n const l = ESTIMATE_LABELS[this.language];\n if (diffDays < 0) {\n return l.overdue;\n }\n if (diffDays === 0) {\n return l.today;\n }\n if (diffDays === 1) {\n return l.tomorrow;\n }\n return l.days.replace(\"%d\", String(diffDays));\n }\n\n /**\n * Whether the delivery is expected today. Language-agnostic, used by the\n * summary filter so `todayCount` works across all languages.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private isToday(delivery: ParcelDelivery, statusCode: number): boolean {\n return this.computeDiffDays(delivery, statusCode) === 0;\n }\n\n /**\n * Format the latest tracking event.\n *\n * @param delivery The delivery data\n */\n private formatLastEvent(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n const parts: string[] = [];\n if (typeof latest.event === \"string\" && latest.event.length > 0) {\n parts.push(latest.event);\n }\n if (typeof latest.date === \"string\" && latest.date.length > 0) {\n parts.push(latest.date);\n }\n return parts.join(\" - \");\n }\n\n /**\n * Extract location from latest event.\n *\n * @param delivery The delivery data\n */\n private extractLastLocation(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Calculate combined delivery window for today's packages.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const windows = todayDeliveries\n .map(d => this.calculateDeliveryWindow(d, this.parseStatus(d)))\n .filter(w => w.length > 0);\n\n if (windows.length === 0) {\n return \"\";\n }\n if (windows.length === 1) {\n return windows[0];\n }\n\n const times: {\n /** Window start */ start: string;\n /** Window end */ end: string;\n }[] = [];\n for (const w of windows) {\n const match = w.match(/(\\d{2}:\\d{2})(?:\\s*-\\s*(\\d{2}:\\d{2}))?/);\n if (match) {\n times.push({ start: match[1], end: match[2] || match[1] });\n }\n }\n\n if (times.length === 0) {\n return \"\";\n }\n\n times.sort((a, b) => a.start.localeCompare(b.start));\n return `${times[0].start} - ${times[times.length - 1].end}`;\n }\n\n /**\n * Create/extend a read-only state and set its value. Skips the\n * `setObjectNotExistsAsync` round-trip once the ID is in the cache \u2014\n * states are static after first creation; only the value changes per poll.\n *\n * @param id State ID relative to adapter namespace\n * @param name Display name (translation object or plain string)\n * @param type Value type\n * @param role ioBroker role\n * @param val Value to set\n */\n private async createAndSet(\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ): Promise<void> {\n if (!this.createdIds.has(id)) {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: { name, type, role, read: true, write: false },\n native: {},\n });\n this.createdIds.add(id);\n }\n await this.adapter.setStateAsync(id, { val, ack: true });\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAAmC;AAEnC,yBAAsB;AAEtB,mBAAsE;AAGtE,MAAM,qBAAqB,oBAAI,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;AAO5C,SAAS,OAAO,MAA8C;AAC5D,SAAO;AACT;AAGA,MAAM,kBAA0D;AAAA,EAC9D,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AACF;AAQO,SAAS,gBAAgB,UAA2B;AACzD,MAAI,OAAO,aAAa,YAAY,iCAAoB,SAAS,QAAQ,GAAG;AAC1E,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,MAAM,aAAa;AAAA,EAChB;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQS,aAAa,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAM9C,YAAY,SAA0B,UAAkB;AACtD,SAAK,UAAU;AACf,SAAK,WAAW,gBAAgB,QAAQ;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,SAAS,MAAuB;AAC9B,QAAI,OAAO,SAAS,UAAU;AAC5B,aAAO;AAAA,IACT;AACA,WACE,KACG,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE,KAAK;AAAA,EAEvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,YAAY,UAAkC;AAC5C,UAAM,MAAM,SAAS;AACrB,QAAI,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,GAAG;AACnD,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB;AACA,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAM,IAAI,SAAS,KAAK,EAAE;AAC1B,aAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,IAClC;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { AdapterInstance } from \"@iobroker/adapter-core\";\nimport { coerceFiniteNumber } from \"./coerce\";\nimport type { StateName } from \"./i18n-states\";\nimport { tName } from \"./i18n-states\";\nimport type { ParcelDelivery } from \"./types\";\nimport { STATUS_LABELS, SUPPORTED_LANGUAGES, FALLBACK_LANGUAGE } from \"./types\";\n\n/** Status codes that have expected delivery date/time */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Translation-object cast helper \u2014 ioBroker's `common.name` accepts `StringOrTranslated`.\n *\n * @param name Translation object from {@link STATE_NAMES}.\n */\nfunction asName(name: StateName): ioBroker.StringOrTranslated {\n return name;\n}\n\n/** Delivery-estimate labels keyed by language code. Keys must match STATUS_LABELS. */\nconst ESTIMATE_LABELS: Record<string, Record<string, string>> = {\n de: {\n overdue: \"\u00FCberf\u00E4llig\",\n today: \"heute\",\n tomorrow: \"morgen\",\n days: \"in %d Tagen\",\n },\n en: {\n overdue: \"overdue\",\n today: \"today\",\n tomorrow: \"tomorrow\",\n days: \"in %d days\",\n },\n ru: {\n overdue: \"\u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043E\",\n today: \"\u0441\u0435\u0433\u043E\u0434\u043D\u044F\",\n tomorrow: \"\u0437\u0430\u0432\u0442\u0440\u0430\",\n days: \"\u0447\u0435\u0440\u0435\u0437 %d \u0434\u043D.\",\n },\n pt: {\n overdue: \"atrasado\",\n today: \"hoje\",\n tomorrow: \"amanh\u00E3\",\n days: \"em %d dias\",\n },\n nl: {\n overdue: \"te laat\",\n today: \"vandaag\",\n tomorrow: \"morgen\",\n days: \"over %d dagen\",\n },\n fr: {\n overdue: \"en retard\",\n today: \"aujourd'hui\",\n tomorrow: \"demain\",\n days: \"dans %d jours\",\n },\n it: {\n overdue: \"in ritardo\",\n today: \"oggi\",\n tomorrow: \"domani\",\n days: \"tra %d giorni\",\n },\n es: {\n overdue: \"atrasado\",\n today: \"hoy\",\n tomorrow: \"ma\u00F1ana\",\n days: \"en %d d\u00EDas\",\n },\n pl: {\n overdue: \"zaleg\u0142e\",\n today: \"dzisiaj\",\n tomorrow: \"jutro\",\n days: \"za %d dni\",\n },\n uk: {\n overdue: \"\u043F\u0440\u043E\u0441\u0442\u0440\u043E\u0447\u0435\u043D\u043E\",\n today: \"\u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456\",\n tomorrow: \"\u0437\u0430\u0432\u0442\u0440\u0430\",\n days: \"\u0447\u0435\u0440\u0435\u0437 %d \u0434\u043D.\",\n },\n \"zh-cn\": {\n overdue: \"\u5DF2\u903E\u671F\",\n today: \"\u4ECA\u5929\",\n tomorrow: \"\u660E\u5929\",\n days: \"%d \u5929\u540E\",\n },\n};\n\n/**\n * Resolve a language code to one that has labels. Falls back to English\n * when the system language is not one of the supported ioBroker languages.\n *\n * @param language Raw language code (e.g. from system.config.language)\n */\nexport function resolveLanguage(language: unknown): string {\n if (typeof language === \"string\" && SUPPORTED_LANGUAGES.includes(language)) {\n return language;\n }\n return FALLBACK_LANGUAGE;\n}\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\n private language: string;\n /**\n * Cache of state IDs that have already passed `setObjectNotExistsAsync`.\n * Skips repeat DB lookups on the hot path \u2014 each poll touches ~11 states\n * per delivery, and most deliveries see no schema change between polls.\n * On `cleanupDeliveries`, IDs of removed packages are dropped so a re-add\n * triggers a fresh creation.\n */\n private readonly createdIds = new Set<string>();\n\n /**\n * @param adapter The ioBroker adapter instance\n * @param language Language code from system.config.language (falls back to English)\n */\n constructor(adapter: AdapterInstance, language: string) {\n this.adapter = adapter;\n this.language = resolveLanguage(language);\n }\n\n /**\n * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).\n * API-drift guard: returns \"unknown\" for non-string input.\n *\n * @param name Raw value to sanitize (any type)\n */\n sanitize(name: unknown): string {\n if (typeof name !== \"string\") {\n return \"unknown\";\n }\n return (\n name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 50) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. API documents `status_code` as\n * a numeric string, but we accept numbers too and fall back to 0 for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code as unknown;\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return Math.trunc(raw);\n }\n if (typeof raw === \"string\") {\n const n = parseInt(raw, 10);\n return Number.isFinite(n) ? n : 0;\n }\n return 0;\n }\n\n /**\n * Build a unique package ID from a delivery.\n *\n * v0.4.2 (S3): when the bare `sanitize(tracking_number)` collides with\n * another active package (e.g. two trackings differ only in special\n * chars that strip down to the same id), append a stable hash of the\n * full tracking number so both end up at distinct state IDs.\n *\n * @param delivery The delivery to build an ID for\n */\n packageId(delivery: ParcelDelivery): string {\n let id = this.sanitize(delivery.tracking_number);\n // API-drift guard: only string values extend the id\n if (typeof delivery.extra_information === \"string\" && delivery.extra_information.length > 0) {\n id += `_${this.sanitize(delivery.extra_information)}`;\n }\n // v0.4.2 (S3): collision suffix when two distinct (raw) trackings would\n // collapse to the same id. Bare id is kept as long as it's unique\n // within this poll (back-compat with existing installs).\n const owner = this.idOwner.get(id);\n const rawKey = StateManager.rawIdKey(delivery);\n if (owner !== undefined && owner !== rawKey) {\n const suffixed = `${id}__${StateManager.shortHash(rawKey)}`;\n this.idOwner.set(suffixed, rawKey);\n return suffixed;\n }\n this.idOwner.set(id, rawKey);\n return id;\n }\n\n /**\n * v0.4.2 (S3): build a stable raw-key for collision tracking.\n *\n * @param delivery The delivery whose raw tracking identifies it.\n */\n private static rawIdKey(delivery: ParcelDelivery): string {\n const t = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const e = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n return `${t}\u0000${e}`;\n }\n\n /**\n * v0.4.2 (S3): FNV-1a 32-bit short hash \u2192 6 hex chars.\n *\n * @param s Input string to hash.\n */\n private static shortHash(s: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, \"0\").slice(0, 6);\n }\n\n /**\n * v0.4.2 (S3): which raw-tracking-key currently \"owns\" each sanitized id\n * within the running poll. Cleared via `resetIdOwners()` between polls so\n * the same delivery keeps its bare id as long as it's unique.\n */\n private readonly idOwner = new Map<string, string>();\n\n /**\n * v0.4.2 (S3): reset the per-poll collision tracker. Call from main.ts\n * before iterating deliveries so the bare id always wins for the first\n * occurrence in each poll.\n */\n resetPollState(): void {\n this.idOwner.clear();\n }\n\n /**\n * Update or create all states for a delivery.\n *\n * @param delivery The delivery data from API\n * @param carrierName Resolved carrier display name\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string): Promise<void> {\n const pkgId = this.packageId(delivery);\n const devicePath = `deliveries.${pkgId}`;\n\n const description = typeof delivery.description === \"string\" ? delivery.description : \"\";\n const trackingNumber = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const extraInfo = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n\n await this.adapter.extendObjectAsync(devicePath, {\n type: \"device\",\n common: {\n name: description || `Package ${trackingNumber || pkgId}`,\n },\n native: {},\n });\n\n const statusCode = this.parseStatus(delivery);\n const labels = STATUS_LABELS[this.language];\n const statusText = labels[statusCode] || `Unknown (${statusCode})`;\n\n await Promise.all([\n this.createAndSet(`${devicePath}.carrier`, asName(tName(\"carrier\")), \"string\", \"text\", carrierName),\n this.createAndSet(`${devicePath}.status`, asName(tName(\"status\")), \"string\", \"text\", statusText),\n this.createAndSet(`${devicePath}.statusCode`, asName(tName(\"statusCode\")), \"number\", \"value\", statusCode),\n this.createAndSet(`${devicePath}.description`, asName(tName(\"description\")), \"string\", \"text\", description),\n this.createAndSet(\n `${devicePath}.trackingNumber`,\n asName(tName(\"trackingNumber\")),\n \"string\",\n \"text\",\n trackingNumber,\n ),\n this.createAndSet(`${devicePath}.extraInfo`, asName(tName(\"extraInfo\")), \"string\", \"text\", extraInfo),\n this.createAndSet(\n `${devicePath}.deliveryWindow`,\n asName(tName(\"deliveryWindow\")),\n \"string\",\n \"text\",\n this.calculateDeliveryWindow(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.deliveryEstimate`,\n asName(tName(\"deliveryEstimate\")),\n \"string\",\n \"text\",\n this.calculateDeliveryEstimate(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.lastEvent`,\n asName(tName(\"lastEvent\")),\n \"string\",\n \"text\",\n this.formatLastEvent(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastLocation`,\n asName(tName(\"lastLocation\")),\n \"string\",\n \"text\",\n this.extractLastLocation(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastUpdated`,\n asName(tName(\"lastUpdated\")),\n \"string\",\n \"date\",\n new Date().toISOString(),\n ),\n ]);\n }\n\n /**\n * Update summary states. Expects already-filtered active deliveries.\n * The `summary` channel itself is declared via io-package.json instanceObjects.\n *\n * @param activeDeliveries Only active (non-delivered) deliveries\n */\n async updateSummary(activeDeliveries: ParcelDelivery[]): Promise<void> {\n const todayDeliveries = activeDeliveries.filter(d => this.isToday(d, this.parseStatus(d)));\n\n await Promise.all([\n this.createAndSet(\n \"summary.activeCount\",\n asName(tName(\"activeCount\")),\n \"number\",\n \"value\",\n activeDeliveries.length,\n ),\n this.createAndSet(\"summary.todayCount\", asName(tName(\"todayCount\")), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n asName(tName(\"summaryDeliveryWindow\")),\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer active.\n *\n * @param activeIds List of currently active package IDs\n */\n async cleanupDeliveries(activeIds: string[]): Promise<void> {\n const activeSet = new Set(activeIds.map(id => `deliveries.${id}`));\n\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.\u9999`,\n });\n if (!objects?.rows) {\n return;\n }\n\n // v0.4.2 (S1): collect first, then delete in parallel. Earlier each\n // stale package took a sequential broker round-trip.\n const toDelete: string[] = [];\n for (const row of objects.rows) {\n const relativeId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (relativeId.startsWith(\"deliveries.\") && !activeSet.has(relativeId)) {\n toDelete.push(relativeId);\n }\n }\n\n await Promise.all(\n toDelete.map(async relativeId => {\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n // v0.4.2 (S2): snapshot to array first \u2014 defensive against any future\n // engine that diverges from spec on Set.delete during for-of iteration.\n for (const id of [...this.createdIds]) {\n if (id === relativeId || id.startsWith(`${relativeId}.`)) {\n this.createdIds.delete(id);\n }\n }\n }),\n );\n }\n\n /**\n * Calculate delivery time window \u2014 only from Unix timestamps.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(delivery: ParcelDelivery, statusCode: number): string {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return \"\";\n }\n\n const formatTime = (timestamp: unknown): string | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const d = new Date(ts * 1000);\n if (Number.isNaN(d.getTime())) {\n return null;\n }\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n };\n\n const start = formatTime(delivery.timestamp_expected);\n const end = formatTime(delivery.timestamp_expected_end);\n\n if (!start) {\n return \"\";\n }\n return end ? `${start} - ${end}` : start;\n }\n\n /**\n * Days from today to the expected delivery date. Returns null when the\n * delivery has no usable expected date or is in a non-trackable status.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private computeDiffDays(delivery: ParcelDelivery, statusCode: number): number | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n\n let expectedDate: Date | null = null;\n const ts = coerceFiniteNumber(delivery.timestamp_expected);\n if (ts !== null && ts > 0) {\n expectedDate = new Date(ts * 1000);\n } else if (typeof delivery.date_expected === \"string\" && delivery.date_expected.length > 0) {\n expectedDate = new Date(delivery.date_expected);\n }\n\n if (!expectedDate || isNaN(expectedDate.getTime())) {\n return null;\n }\n\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const expectedStart = new Date(expectedDate.getFullYear(), expectedDate.getMonth(), expectedDate.getDate());\n return Math.round((expectedStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24));\n }\n\n /**\n * Calculate human-readable delivery estimate.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryEstimate(delivery: ParcelDelivery, statusCode: number): string {\n const diffDays = this.computeDiffDays(delivery, statusCode);\n if (diffDays === null) {\n return \"\";\n }\n const l = ESTIMATE_LABELS[this.language];\n if (diffDays < 0) {\n return l.overdue;\n }\n if (diffDays === 0) {\n return l.today;\n }\n if (diffDays === 1) {\n return l.tomorrow;\n }\n return l.days.replace(\"%d\", String(diffDays));\n }\n\n /**\n * Whether the delivery is expected today. Language-agnostic, used by the\n * summary filter so `todayCount` works across all languages.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private isToday(delivery: ParcelDelivery, statusCode: number): boolean {\n return this.computeDiffDays(delivery, statusCode) === 0;\n }\n\n /**\n * Format the latest tracking event.\n *\n * @param delivery The delivery data\n */\n private formatLastEvent(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n const parts: string[] = [];\n if (typeof latest.event === \"string\" && latest.event.length > 0) {\n parts.push(latest.event);\n }\n if (typeof latest.date === \"string\" && latest.date.length > 0) {\n parts.push(latest.date);\n }\n return parts.join(\" - \");\n }\n\n /**\n * Extract location from latest event.\n *\n * @param delivery The delivery data\n */\n private extractLastLocation(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Calculate combined delivery window for today's packages.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const windows = todayDeliveries\n .map(d => this.calculateDeliveryWindow(d, this.parseStatus(d)))\n .filter(w => w.length > 0);\n\n if (windows.length === 0) {\n return \"\";\n }\n if (windows.length === 1) {\n return windows[0];\n }\n\n const times: {\n /** Window start */ start: string;\n /** Window end */ end: string;\n }[] = [];\n for (const w of windows) {\n const match = w.match(/(\\d{2}:\\d{2})(?:\\s*-\\s*(\\d{2}:\\d{2}))?/);\n if (match) {\n times.push({ start: match[1], end: match[2] || match[1] });\n }\n }\n\n if (times.length === 0) {\n return \"\";\n }\n\n times.sort((a, b) => a.start.localeCompare(b.start));\n return `${times[0].start} - ${times[times.length - 1].end}`;\n }\n\n /**\n * Create/extend a read-only state and set its value. Skips the\n * `setObjectNotExistsAsync` round-trip once the ID is in the cache \u2014\n * states are static after first creation; only the value changes per poll.\n *\n * @param id State ID relative to adapter namespace\n * @param name Display name (translation object or plain string)\n * @param type Value type\n * @param role ioBroker role\n * @param val Value to set\n */\n private async createAndSet(\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ): Promise<void> {\n if (!this.createdIds.has(id)) {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: { name, type, role, read: true, write: false },\n native: {},\n });\n this.createdIds.add(id);\n }\n await this.adapter.setStateAsync(id, { val, ack: true });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -29,6 +29,7 @@ const MIN_POLL_INTERVAL = 5;
|
|
|
29
29
|
const MAX_POLL_INTERVAL = 60;
|
|
30
30
|
const DEFAULT_POLL_INTERVAL = 10;
|
|
31
31
|
const MIN_POLL_GAP_MS = 6e4;
|
|
32
|
+
const MIN_API_KEY_LENGTH = 10;
|
|
32
33
|
class ParcelappAdapter extends utils.Adapter {
|
|
33
34
|
client = null;
|
|
34
35
|
stateManager = null;
|
|
@@ -56,16 +57,20 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
56
57
|
this.onMessage(obj).catch((err) => this.log.error(`onMessage failed: ${(0, import_coerce.errText)(err)}`));
|
|
57
58
|
});
|
|
58
59
|
this.unhandledRejectionHandler = (reason) => {
|
|
60
|
+
var _a;
|
|
59
61
|
this.log.error(`Unhandled rejection: ${(0, import_coerce.errText)(reason)}`);
|
|
62
|
+
(_a = this.terminate) == null ? void 0 : _a.call(this, 11);
|
|
60
63
|
};
|
|
61
64
|
this.uncaughtExceptionHandler = (err) => {
|
|
65
|
+
var _a;
|
|
62
66
|
this.log.error(`Uncaught exception: ${(0, import_coerce.errText)(err)}`);
|
|
67
|
+
(_a = this.terminate) == null ? void 0 : _a.call(this, 11);
|
|
63
68
|
};
|
|
64
69
|
process.on("unhandledRejection", this.unhandledRejectionHandler);
|
|
65
70
|
process.on("uncaughtException", this.uncaughtExceptionHandler);
|
|
66
71
|
}
|
|
67
72
|
async onReady() {
|
|
68
|
-
var _a, _b
|
|
73
|
+
var _a, _b;
|
|
69
74
|
const sysConfig = await this.getForeignObjectAsync("system.config");
|
|
70
75
|
const language = (_b = (_a = sysConfig == null ? void 0 : sysConfig.common) == null ? void 0 : _a.language) != null ? _b : "";
|
|
71
76
|
if (typeof language === "string" && language.length > 0) {
|
|
@@ -73,7 +78,7 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
73
78
|
}
|
|
74
79
|
await this.setStateAsync("info.connection", { val: false, ack: true });
|
|
75
80
|
const { apiKey } = this.config;
|
|
76
|
-
if (!apiKey || apiKey.trim().length <
|
|
81
|
+
if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {
|
|
77
82
|
this.log.error("No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings");
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
@@ -81,20 +86,27 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
81
86
|
this.stateManager = new import_state_manager.StateManager(this, language);
|
|
82
87
|
await this.cleanupObsoleteStates();
|
|
83
88
|
await this.poll();
|
|
84
|
-
const interval =
|
|
85
|
-
MIN_POLL_INTERVAL,
|
|
86
|
-
Math.min(MAX_POLL_INTERVAL, (_c = this.config.pollInterval) != null ? _c : DEFAULT_POLL_INTERVAL)
|
|
87
|
-
);
|
|
89
|
+
const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);
|
|
88
90
|
const intervalMs = interval * 60 * 1e3;
|
|
89
91
|
this.pollTimer = this.setInterval(() => void this.poll(), intervalMs);
|
|
90
92
|
this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* v0.4.2 (M5+X5): delegate to the shared `coerceClampedInt` helper.
|
|
96
|
+
*
|
|
97
|
+
* @param raw Raw `pollInterval` from admin config (number or numeric string).
|
|
98
|
+
*/
|
|
99
|
+
static coercePollInterval(raw) {
|
|
100
|
+
return (0, import_coerce.coerceClampedInt)(raw, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL, DEFAULT_POLL_INTERVAL);
|
|
101
|
+
}
|
|
92
102
|
onUnload(callback) {
|
|
103
|
+
var _a;
|
|
93
104
|
try {
|
|
94
105
|
if (this.pollTimer) {
|
|
95
106
|
this.clearInterval(this.pollTimer);
|
|
96
107
|
this.pollTimer = void 0;
|
|
97
108
|
}
|
|
109
|
+
(_a = this.client) == null ? void 0 : _a.cancelAll();
|
|
98
110
|
if (this.unhandledRejectionHandler) {
|
|
99
111
|
process.off("unhandledRejection", this.unhandledRejectionHandler);
|
|
100
112
|
this.unhandledRejectionHandler = null;
|
|
@@ -103,7 +115,8 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
103
115
|
process.off("uncaughtException", this.uncaughtExceptionHandler);
|
|
104
116
|
this.uncaughtExceptionHandler = null;
|
|
105
117
|
}
|
|
106
|
-
void this.setState("info.connection", { val: false, ack: true })
|
|
118
|
+
void this.setState("info.connection", { val: false, ack: true }).catch(() => {
|
|
119
|
+
});
|
|
107
120
|
} catch {
|
|
108
121
|
}
|
|
109
122
|
callback();
|
|
@@ -118,7 +131,7 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
118
131
|
case "checkConnection": {
|
|
119
132
|
const msg = obj.message;
|
|
120
133
|
const key = ((_a = msg == null ? void 0 : msg.apiKey) == null ? void 0 : _a.trim()) || "";
|
|
121
|
-
if (!key || key.length <
|
|
134
|
+
if (!key || key.length < MIN_API_KEY_LENGTH) {
|
|
122
135
|
this.sendTo(obj.from, obj.command, { success: false, message: "API key is too short" }, obj.callback);
|
|
123
136
|
return;
|
|
124
137
|
}
|
|
@@ -177,6 +190,9 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
177
190
|
if (error.code === "INVALID_API_KEY") {
|
|
178
191
|
return "INVALID_API_KEY";
|
|
179
192
|
}
|
|
193
|
+
if (error.code === "FORBIDDEN") {
|
|
194
|
+
return "FORBIDDEN";
|
|
195
|
+
}
|
|
180
196
|
if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED" || error.code === "ECONNRESET" || error.code === "ENETUNREACH" || error.code === "EHOSTUNREACH" || error.code === "EAI_AGAIN") {
|
|
181
197
|
return "NETWORK";
|
|
182
198
|
}
|
|
@@ -186,6 +202,7 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
186
202
|
return error.code || "UNKNOWN";
|
|
187
203
|
}
|
|
188
204
|
async poll() {
|
|
205
|
+
var _a;
|
|
189
206
|
if (this.isPolling || !this.client || !this.stateManager) {
|
|
190
207
|
return;
|
|
191
208
|
}
|
|
@@ -212,23 +229,27 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
212
229
|
await this.setStateAsync("info.connection", { val: true, ack: true });
|
|
213
230
|
const activeDeliveries = deliveries.filter((d) => this.stateManager.parseStatus(d) !== 0);
|
|
214
231
|
const visibleDeliveries = autoRemove ? activeDeliveries : deliveries;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
this.stateManager.resetPollState();
|
|
233
|
+
const idResults = await Promise.all(
|
|
234
|
+
visibleDeliveries.map(async (delivery) => {
|
|
235
|
+
try {
|
|
236
|
+
const carrierName = await this.client.getCarrierName(delivery.carrier_code);
|
|
237
|
+
await this.stateManager.updateDelivery(delivery, carrierName);
|
|
238
|
+
this.failedDeliveries.delete(delivery.tracking_number);
|
|
239
|
+
return this.stateManager.packageId(delivery);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const msg = (0, import_coerce.errText)(err);
|
|
242
|
+
if (this.failedDeliveries.has(delivery.tracking_number)) {
|
|
243
|
+
this.log.debug(`Failed to update "${delivery.tracking_number}": ${msg}`);
|
|
244
|
+
} else {
|
|
245
|
+
this.log.warn(`Failed to update '${delivery.tracking_number}': ${msg}`);
|
|
246
|
+
this.failedDeliveries.add(delivery.tracking_number);
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
229
249
|
}
|
|
230
|
-
}
|
|
231
|
-
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
const activeIds = idResults.filter((id) => id !== null);
|
|
232
253
|
await this.stateManager.cleanupDeliveries(activeIds);
|
|
233
254
|
await this.stateManager.updateSummary(activeDeliveries);
|
|
234
255
|
this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);
|
|
@@ -238,9 +259,14 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
238
259
|
const isRepeat = errorCode === this.lastErrorCode;
|
|
239
260
|
this.lastErrorCode = errorCode;
|
|
240
261
|
if (error.code === "RATE_LIMITED") {
|
|
241
|
-
const
|
|
262
|
+
const rawCooldown = (_a = error.retryAfterSeconds) != null ? _a : 0;
|
|
263
|
+
const cooldownSec = Number.isFinite(rawCooldown) && rawCooldown > 0 ? Math.min(24 * 3600, Math.max(60, Math.floor(rawCooldown))) : 5 * 60;
|
|
242
264
|
this.rateLimitedUntil = Date.now() + cooldownSec * 1e3;
|
|
243
265
|
this.log.warn(`Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`);
|
|
266
|
+
} else if (error.code === "FORBIDDEN") {
|
|
267
|
+
this.log.error(
|
|
268
|
+
"parcel.app returned 403 Forbidden \u2014 your account may not have an active Premium subscription, or the API key was revoked. Check your account on parcelapp.net."
|
|
269
|
+
);
|
|
244
270
|
} else if (error.code === "INVALID_API_KEY") {
|
|
245
271
|
this.log.error("Invalid API key \u2014 please check your parcel.app API key");
|
|
246
272
|
} else if (isRepeat) {
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { errText } from \"./lib/coerce\";\nimport { ParcelClient } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\nconst MIN_POLL_GAP_MS = 60_000; // Minimum 60s between polls\n\n/** ioBroker adapter for parcel.app package tracking */\nclass ParcelappAdapter extends utils.Adapter {\n private client: ParcelClient | null = null;\n private stateManager: StateManager | null = null;\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n private failedDeliveries = new Set<string>();\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n /** ioBroker system language \u2014 read once in `onReady` from `system.config`. EN fallback. */\n private systemLang: string = \"en\";\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n // Wrap async handlers with .catch() so a rejection can never become an\n // unhandled promise rejection (which would SIGKILL the adapter and trap\n // js-controller in a restart loop without any stack trace).\n this.on(\"ready\", () => {\n this.onReady().catch(err => this.log.error(`onReady failed: ${errText(err)}`));\n });\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", obj => {\n this.onMessage(obj).catch(err => this.log.error(`onMessage failed: ${errText(err)}`));\n });\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths (e.g. `void this.poll()`). The per-handler\n // .catch() wrappers cover the documented async paths; this catches\n // anything that slips past during refactors.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(`Unhandled rejection: ${errText(reason)}`);\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${errText(err)}`);\n };\n process.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n process.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n }\n\n private async onReady(): Promise<void> {\n // Pick the system language up-front so all user-facing logs go out in the\n // user's language. StateManager also gets it for state-name localization.\n const sysConfig = await this.getForeignObjectAsync(\"system.config\");\n const language = (sysConfig?.common as { language?: string } | undefined)?.language ?? \"\";\n if (typeof language === \"string\" && language.length > 0) {\n this.systemLang = language;\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n\n // Validate config\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < 10) {\n this.log.error(\"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\");\n return;\n }\n\n // Initialize\n this.client = new ParcelClient(apiKey.trim());\n this.stateManager = new StateManager(this, language);\n\n // Cleanup obsolete states\n await this.cleanupObsoleteStates();\n\n // Initial poll\n await this.poll();\n\n // Set up recurring poll\n const interval = Math.max(\n MIN_POLL_INTERVAL,\n Math.min(MAX_POLL_INTERVAL, this.config.pollInterval ?? DEFAULT_POLL_INTERVAL),\n );\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => void this.poll(), intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n }\n\n private onUnload(callback: () => void): void {\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n if (this.unhandledRejectionHandler) {\n process.off(\"unhandledRejection\", this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off(\"uncaughtException\", this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n void this.setState(\"info.connection\", { val: false, ack: true });\n } catch {\n // ignore\n }\n callback();\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n try {\n switch (obj.command) {\n case \"checkConnection\": {\n const msg = obj.message as { apiKey?: string };\n const key = msg?.apiKey?.trim() || \"\";\n if (!key || key.length < 10) {\n this.sendTo(obj.from, obj.command, { success: false, message: \"API key is too short\" }, obj.callback);\n return;\n }\n const testClient = new ParcelClient(key);\n const result = await testClient.testConnection();\n this.sendTo(obj.from, obj.command, result, obj.callback);\n break;\n }\n case \"addDelivery\": {\n if (!this.client) {\n this.sendTo(\n obj.from,\n obj.command,\n { success: false, error_message: \"Adapter not initialized\" },\n obj.callback,\n );\n return;\n }\n const request = obj.message as {\n tracking_number: string;\n carrier_code: string;\n description: string;\n };\n const addResult = await this.client.addDelivery(request);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (addResult.success) {\n void this.poll();\n }\n break;\n }\n default:\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n this.sendTo(obj.from, obj.command, { success: false, error_message: errText(err) }, obj.callback);\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n const obsoleteStates = [\n \"summary.json\", // removed in 0.2.0\n ];\n for (const stateId of obsoleteStates) {\n const obj = await this.getObjectAsync(stateId);\n if (obj) {\n await this.delObjectAsync(stateId);\n this.log.debug(`Removed obsolete state: ${stateId}`);\n }\n }\n }\n\n /**\n * Classify an error for deduplication and log-level decisions.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code === \"RATE_LIMITED\") {\n return \"RATE_LIMITED\";\n }\n if (error.code === \"INVALID_API_KEY\") {\n return \"INVALID_API_KEY\";\n }\n // Network errors: DNS, connection refused, no internet\n if (\n error.code === \"ENOTFOUND\" ||\n error.code === \"ECONNREFUSED\" ||\n error.code === \"ECONNRESET\" ||\n error.code === \"ENETUNREACH\" ||\n error.code === \"EHOSTUNREACH\" ||\n error.code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (error.message.includes(\"timeout\") || error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code || \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n return;\n }\n\n const now = Date.now();\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);\n return;\n }\n\n // Throttle: minimum gap between polls\n if (now - this.lastPollTime < MIN_POLL_GAP_MS) {\n this.log.debug(\"Skipping poll \u2014 too soon after last poll\");\n return;\n }\n\n this.isPolling = true;\n this.lastPollTime = now;\n try {\n // When keeping delivered packages, use \"recent\" to get them from API\n const autoRemove = this.config.autoRemoveDelivered !== false;\n const deliveries = await this.client.getDeliveries(autoRemove ? \"active\" : \"recent\");\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n await this.setStateAsync(\"info.connection\", { val: true, ack: true });\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => this.stateManager!.parseStatus(d) !== 0);\n const visibleDeliveries = autoRemove ? activeDeliveries : deliveries;\n\n // Update each delivery (isolated: one failure must not block others)\n const activeIds: string[] = [];\n for (const delivery of visibleDeliveries) {\n try {\n const carrierName = await this.client.getCarrierName(delivery.carrier_code);\n await this.stateManager.updateDelivery(delivery, carrierName);\n activeIds.push(this.stateManager.packageId(delivery));\n this.failedDeliveries.delete(delivery.tracking_number);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(delivery.tracking_number)) {\n this.log.debug(`Failed to update \"${delivery.tracking_number}\": ${msg}`);\n } else {\n this.log.warn(`Failed to update '${delivery.tracking_number}': ${msg}`);\n this.failedDeliveries.add(delivery.tracking_number);\n }\n }\n }\n\n // Cleanup stale deliveries\n await this.stateManager.cleanupDeliveries(activeIds);\n\n // Update summary (always uses active/non-delivered)\n await this.stateManager.updateSummary(activeDeliveries);\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n const error = err as Error & {\n code?: string;\n retryAfterSeconds?: number;\n };\n\n // Classify the error\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n if (error.code === \"RATE_LIMITED\") {\n const cooldownSec = error.retryAfterSeconds || 5 * 60;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n this.log.warn(`Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`);\n } else if (error.code === \"INVALID_API_KEY\") {\n // Always log \u2014 user must fix config\n this.log.error(\"Invalid API key \u2014 please check your parcel.app API key\");\n } else if (isRepeat) {\n // Same error as last time \u2014 don't spam the log\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else if (errorCode === \"NETWORK\") {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n } else if (errorCode === \"TIMEOUT\") {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n } else {\n this.log.error(`Poll failed: ${error.message}`);\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n } finally {\n this.isPolling = false;\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA,YAAuB;AACvB,
|
|
4
|
+
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { coerceClampedInt, errText } from \"./lib/coerce\";\nimport { ParcelClient } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\nconst MIN_POLL_GAP_MS = 60_000; // Minimum 60s between polls\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n\n/** ioBroker adapter for parcel.app package tracking */\nclass ParcelappAdapter extends utils.Adapter {\n private client: ParcelClient | null = null;\n private stateManager: StateManager | null = null;\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n private failedDeliveries = new Set<string>();\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n /** ioBroker system language \u2014 read once in `onReady` from `system.config`. EN fallback. */\n private systemLang: string = \"en\";\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n // Wrap async handlers with .catch() so a rejection can never become an\n // unhandled promise rejection (which would SIGKILL the adapter and trap\n // js-controller in a restart loop without any stack trace).\n this.on(\"ready\", () => {\n this.onReady().catch(err => this.log.error(`onReady failed: ${errText(err)}`));\n });\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", obj => {\n this.onMessage(obj).catch(err => this.log.error(`onMessage failed: ${errText(err)}`));\n });\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths (e.g. `void this.poll()`). The per-handler\n // .catch() wrappers cover the documented async paths; this catches\n // anything that slips past during refactors.\n // v0.4.2 (M1): log + terminate(11) instead of leaving the process alive\n // in an undefined state. The per-handler wrappers cover expected paths;\n // anything reaching here is by definition unexpected.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(`Unhandled rejection: ${errText(reason)}`);\n this.terminate?.(11);\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${errText(err)}`);\n this.terminate?.(11);\n };\n process.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n process.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n }\n\n private async onReady(): Promise<void> {\n // Pick the system language up-front so all user-facing logs go out in the\n // user's language. StateManager also gets it for state-name localization.\n const sysConfig = await this.getForeignObjectAsync(\"system.config\");\n const language = (sysConfig?.common as { language?: string } | undefined)?.language ?? \"\";\n if (typeof language === \"string\" && language.length > 0) {\n this.systemLang = language;\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n\n // Validate config\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {\n this.log.error(\"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\");\n return;\n }\n\n // Initialize\n this.client = new ParcelClient(apiKey.trim());\n this.stateManager = new StateManager(this, language);\n\n // Cleanup obsolete states\n await this.cleanupObsoleteStates();\n\n // Initial poll\n await this.poll();\n\n // v0.4.2 (M5): coerce explicitly. Admin can store `pollInterval` as a\n // string; `Math.min(60, \"10\")` happens to coerce, but `Math.max(5,\n // undefined)` returns NaN, and `setInterval(fn, NaN)` becomes\n // `setInterval(fn, 0)` \u2014 a tight loop that hammers the API.\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => void this.poll(), intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n }\n\n /**\n * v0.4.2 (M5+X5): delegate to the shared `coerceClampedInt` helper.\n *\n * @param raw Raw `pollInterval` from admin config (number or numeric string).\n */\n private static coercePollInterval(raw: unknown): number {\n return coerceClampedInt(raw, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL, DEFAULT_POLL_INTERVAL);\n }\n\n private onUnload(callback: () => void): void {\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n // v0.4.2 (M11+P1): cancel every in-flight HTTPS request so a slow\n // parcel.app endpoint doesn't keep the adapter alive past\n // js-controller's 4-second kill deadline.\n this.client?.cancelAll();\n if (this.unhandledRejectionHandler) {\n process.off(\"unhandledRejection\", this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off(\"uncaughtException\", this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n // v0.4.2 (M10): explicit `.catch(() => {})` on the fire-and-forget so\n // a broker-already-down doesn't leak as an unhandled rejection.\n void this.setState(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker is shutting down \u2014 ignore */\n });\n } catch {\n // ignore\n }\n callback();\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n try {\n switch (obj.command) {\n case \"checkConnection\": {\n const msg = obj.message as { apiKey?: string };\n const key = msg?.apiKey?.trim() || \"\";\n if (!key || key.length < MIN_API_KEY_LENGTH) {\n this.sendTo(obj.from, obj.command, { success: false, message: \"API key is too short\" }, obj.callback);\n return;\n }\n const testClient = new ParcelClient(key);\n const result = await testClient.testConnection();\n this.sendTo(obj.from, obj.command, result, obj.callback);\n break;\n }\n case \"addDelivery\": {\n if (!this.client) {\n this.sendTo(\n obj.from,\n obj.command,\n { success: false, error_message: \"Adapter not initialized\" },\n obj.callback,\n );\n return;\n }\n const request = obj.message as {\n tracking_number: string;\n carrier_code: string;\n description: string;\n };\n const addResult = await this.client.addDelivery(request);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (addResult.success) {\n void this.poll();\n }\n break;\n }\n default:\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n this.sendTo(obj.from, obj.command, { success: false, error_message: errText(err) }, obj.callback);\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n const obsoleteStates = [\n \"summary.json\", // removed in 0.2.0\n ];\n for (const stateId of obsoleteStates) {\n const obj = await this.getObjectAsync(stateId);\n if (obj) {\n await this.delObjectAsync(stateId);\n this.log.debug(`Removed obsolete state: ${stateId}`);\n }\n }\n }\n\n /**\n * Classify an error for deduplication and log-level decisions.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code === \"RATE_LIMITED\") {\n return \"RATE_LIMITED\";\n }\n if (error.code === \"INVALID_API_KEY\") {\n return \"INVALID_API_KEY\";\n }\n // v0.4.2 (P3): 403 is a permission issue, distinct from invalid api-key.\n if (error.code === \"FORBIDDEN\") {\n return \"FORBIDDEN\";\n }\n // Network errors: DNS, connection refused, no internet\n if (\n error.code === \"ENOTFOUND\" ||\n error.code === \"ECONNREFUSED\" ||\n error.code === \"ECONNRESET\" ||\n error.code === \"ENETUNREACH\" ||\n error.code === \"EHOSTUNREACH\" ||\n error.code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (error.message.includes(\"timeout\") || error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code || \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n return;\n }\n\n const now = Date.now();\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);\n return;\n }\n\n // Throttle: minimum gap between polls\n if (now - this.lastPollTime < MIN_POLL_GAP_MS) {\n this.log.debug(\"Skipping poll \u2014 too soon after last poll\");\n return;\n }\n\n this.isPolling = true;\n this.lastPollTime = now;\n try {\n // When keeping delivered packages, use \"recent\" to get them from API\n const autoRemove = this.config.autoRemoveDelivered !== false;\n const deliveries = await this.client.getDeliveries(autoRemove ? \"active\" : \"recent\");\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n await this.setStateAsync(\"info.connection\", { val: true, ack: true });\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => this.stateManager!.parseStatus(d) !== 0);\n const visibleDeliveries = autoRemove ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset per-poll collision tracker so the bare-id wins\n // for the first occurrence in this poll (deterministic, back-compat).\n this.stateManager.resetPollState();\n\n // v0.4.2 (M4): per-delivery updates run in parallel, each wrapped in\n // try/catch so one bad delivery doesn't poison the others.\n const idResults = await Promise.all(\n visibleDeliveries.map(async delivery => {\n try {\n const carrierName = await this.client!.getCarrierName(delivery.carrier_code);\n await this.stateManager!.updateDelivery(delivery, carrierName);\n this.failedDeliveries.delete(delivery.tracking_number);\n return this.stateManager!.packageId(delivery);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(delivery.tracking_number)) {\n this.log.debug(`Failed to update \"${delivery.tracking_number}\": ${msg}`);\n } else {\n this.log.warn(`Failed to update '${delivery.tracking_number}': ${msg}`);\n this.failedDeliveries.add(delivery.tracking_number);\n }\n return null;\n }\n }),\n );\n const activeIds = idResults.filter((id): id is string => id !== null);\n\n // Cleanup stale deliveries\n await this.stateManager.cleanupDeliveries(activeIds);\n\n // Update summary (always uses active/non-delivered)\n await this.stateManager.updateSummary(activeDeliveries);\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n const error = err as Error & {\n code?: string;\n retryAfterSeconds?: number;\n };\n\n // Classify the error\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n if (error.code === \"RATE_LIMITED\") {\n // v0.4.2 (M9): clamp Retry-After value into [60s, 24h]. A bogus 0,\n // negative, or fractional value used to either wipe the cooldown\n // (set rateLimitedUntil to past) or set it for fractions of a\n // second \u2014 neither is the intended behavior.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(24 * 3600, Math.max(60, Math.floor(rawCooldown)))\n : 5 * 60;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n this.log.warn(`Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`);\n } else if (error.code === \"FORBIDDEN\") {\n // v0.4.2 (P3): 403 is a permission issue (e.g. Premium subscription\n // expired). Reauth wouldn't help \u2014 surface a clear hint.\n this.log.error(\n \"parcel.app returned 403 Forbidden \u2014 your account may not have an active Premium subscription, or the API key was revoked. Check your account on parcelapp.net.\",\n );\n } else if (error.code === \"INVALID_API_KEY\") {\n // Always log \u2014 user must fix config\n this.log.error(\"Invalid API key \u2014 please check your parcel.app API key\");\n } else if (isRepeat) {\n // Same error as last time \u2014 don't spam the log\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else if (errorCode === \"NETWORK\") {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n } else if (errorCode === \"TIMEOUT\") {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n } else {\n this.log.error(`Poll failed: ${error.message}`);\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n } finally {\n this.isPolling = false;\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "parcelapp",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.4.2": {
|
|
7
|
+
"en": "Adapter shuts down cleanly when parcel.app is slow, 403 Forbidden gives a 'check Premium' hint instead of reauth-loop, two trackings with the same sanitized id no longer overwrite.",
|
|
8
|
+
"de": "Adapter beendet sauber wenn parcel.app langsam ist, 403-Fehler liefert 'Premium prüfen'-Hinweis statt Reauth-Loop, und zwei Trackings mit identischer Sanitize-ID überschreiben sich nicht mehr.",
|
|
9
|
+
"ru": "Адаптер корректно завершается при медленной parcel.app, 403 даёт подсказку 'проверьте Premium' вместо цикла переавторизации, два трекинга с одинаковым ID больше не перезаписываются.",
|
|
10
|
+
"pt": "O adaptador encerra de forma limpa quando o parcel.app está lento, 403 mostra dica 'verificar Premium' em vez de re-autenticar em loop, dois rastreios com o mesmo ID já não se sobrescrevem.",
|
|
11
|
+
"nl": "Adapter sluit netjes af als parcel.app traag is, 403 geeft 'Premium controleren'-hint in plaats van reauth-loop, en twee trackings met dezelfde id overschrijven elkaar niet meer.",
|
|
12
|
+
"fr": "L'adaptateur s'arrête proprement quand parcel.app est lent, un 403 donne 'vérifier Premium' au lieu de boucler la réauth, et deux suivis avec le même id ne s'écrasent plus.",
|
|
13
|
+
"it": "L'adapter si arresta correttamente quando parcel.app è lento, un 403 mostra 'controlla Premium' invece di un loop di reauth, due tracciamenti con lo stesso id non si sovrascrivono più.",
|
|
14
|
+
"es": "El adaptador se cierra limpiamente cuando parcel.app va lento, 403 muestra 'verificar Premium' en vez de bucle de reauth, y dos seguimientos con el mismo id ya no se sobrescriben.",
|
|
15
|
+
"pl": "Adapter zamyka się czysto, gdy parcel.app jest powolny, 403 daje wskazówkę 'sprawdź Premium' zamiast pętli reauth, dwa śledzenia z tym samym id już się nie nadpisują.",
|
|
16
|
+
"uk": "Адаптер коректно вимикається при повільному parcel.app, 403 дає підказку 'перевірте Premium' замість циклу входу, два трекінги з однаковим id більше не перезаписуються.",
|
|
17
|
+
"zh-cn": "当 parcel.app 响应缓慢时,适配器现在能干净退出(终止进行中的请求)。403 错误会提示检查 Premium 订阅,而不是反复重新认证。两个清洗后 ID 相同的快递不再互相覆盖。"
|
|
18
|
+
},
|
|
6
19
|
"0.4.1": {
|
|
7
20
|
"en": "Adapter log messages are now English only, in line with the ioBroker community standard. Localized state names (11 languages) remain unchanged.",
|
|
8
21
|
"de": "Adapter-Logs sind jetzt nur noch auf Englisch, gemäß ioBroker-Community-Standard. Lokalisierte Datenpunkt-Namen (11 Sprachen) bleiben erhalten.",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Wewnętrzne porządki. Brak zmian widocznych dla użytkownika.",
|
|
81
94
|
"uk": "Внутрішнє прибирання. Без помітних для користувача змін.",
|
|
82
95
|
"zh-cn": "内部清理。对用户无可见变化。"
|
|
83
|
-
},
|
|
84
|
-
"0.2.17": {
|
|
85
|
-
"en": "Internal cleanup. No user-facing changes.",
|
|
86
|
-
"de": "Interne Bereinigung. Keine User-sichtbaren Änderungen.",
|
|
87
|
-
"ru": "Внутренняя очистка. Без видимых изменений для пользователя.",
|
|
88
|
-
"pt": "Limpeza interna. Sem alterações visíveis para o usuário.",
|
|
89
|
-
"nl": "Interne opschoning. Geen wijzigingen voor de gebruiker.",
|
|
90
|
-
"fr": "Nettoyage interne. Aucune modification visible pour l'utilisateur.",
|
|
91
|
-
"it": "Pulizia interna. Nessuna modifica visibile all'utente.",
|
|
92
|
-
"es": "Limpieza interna. Sin cambios visibles para el usuario.",
|
|
93
|
-
"pl": "Wewnętrzne porządki. Brak zmian widocznych dla użytkownika.",
|
|
94
|
-
"uk": "Внутрішнє прибирання. Без помітних для користувача змін.",
|
|
95
|
-
"zh-cn": "内部清理。对用户无可见变化。"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|