iobroker.parcelapp 0.7.0 → 0.7.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 CHANGED
@@ -23,10 +23,18 @@ ioBroker adapter for the [parcel.app](https://parcelapp.net) API. Supports all c
23
23
 
24
24
  ---
25
25
 
26
+ ## Sentry / Error reporting
27
+
28
+ **This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.** Reporting only happens if you have enabled error reporting in the ioBroker diagnostics (**System settings → Diagnostics and error reporting**). Only an anonymous installation ID is transmitted — no name, e-mail address or IP address.
29
+
30
+ For details and how to disable it, see the [Sentry plugin documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry). Error reporting requires js-controller 3.0 or newer.
31
+
32
+ ---
33
+
26
34
  ## Requirements
27
35
 
28
36
  - **Node.js >= 22**
29
- - **ioBroker js-controller >= 7.0.7**
37
+ - **ioBroker js-controller >= 7.1.2**
30
38
  - **ioBroker Admin >= 7.8.23**
31
39
  - **parcel.app Premium subscription** — required for API access
32
40
 
@@ -112,18 +120,21 @@ The delivery is added to your parcel.app account and immediately appears in ioBr
112
120
 
113
121
  ---
114
122
 
115
- ## Sentry / Error reporting
116
-
117
- This adapter uses [Sentry](https://sentry.io) to automatically report exceptions and errors to the developer, so problems can be found and fixed quickly. Reporting only happens if you have enabled error reporting in the ioBroker diagnostics (**System settings → Diagnostics and error reporting**). Only an anonymous installation ID is transmitted — no name, e-mail address or IP address.
118
-
119
- For details and how to disable it, see the [Sentry plugin documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry). Error reporting requires js-controller 3.0 or newer.
120
-
121
123
  ## Changelog
122
124
 
123
125
  <!--
124
126
  Placeholder for the next version (at the beginning of the line):
125
127
  ### **WORK IN PROGRESS**
126
128
  -->
129
+ ### 0.7.2 (2026-06-12)
130
+
131
+ - Much quieter state updates: a package's last-updated timestamp now only changes when its tracking data actually changed, and device entries are no longer rewritten on every poll
132
+ - Adding a delivery with a malformed request now returns a clear error message instead of failing cryptically
133
+
134
+ ### 0.7.1 (2026-06-09)
135
+
136
+ - Fixed a timezone edge case in delivery estimates: when the API reports only a calendar date, the estimate could be off by a day in time zones west of UTC — now stable everywhere.
137
+
127
138
  ### 0.7.0 (2026-06-07)
128
139
 
129
140
  - Added optional Sentry error reporting: crashes are sent to the developer so issues get fixed faster. Active only with ioBroker diagnostics enabled; anonymous.
@@ -138,14 +149,6 @@ For details and how to disable it, see the [Sentry plugin documentation](https:/
138
149
 
139
150
  - Reduced unnecessary state-change events by skipping writes when the value has not changed.
140
151
 
141
- ### 0.5.2 (2026-05-23)
142
-
143
- - Changelog rewritten in user-centric style across all versions.
144
-
145
- ### 0.5.1 (2026-05-23)
146
-
147
- - Internal cleanup. No user-facing changes.
148
-
149
152
  [Older changelogs can be found there](CHANGELOG_OLD.md)
150
153
 
151
154
  ## Support
@@ -188,4 +191,4 @@ SOFTWARE.
188
191
 
189
192
  ---
190
193
 
191
- *Developed with assistance from Claude.ai*
194
+ _Developed with assistance from Claude.ai_
@@ -48,6 +48,14 @@ function apiError(message, code, extra) {
48
48
  class ParcelClient {
49
49
  apiKey;
50
50
  carrierCache = null;
51
+ /**
52
+ * v0.7.2: in-flight fetch for the carrier list. The per-delivery updates run
53
+ * in parallel (Promise.all) and each resolves carrier names — without this
54
+ * mutex the first poll with N packages fired N identical concurrent fetches
55
+ * of the static 447-entry file (and a persistently failing endpoint was
56
+ * retried N times per poll). Same pattern as beszel's auth mutex (B1).
57
+ */
58
+ carrierFetchInFlight = null;
51
59
  /**
52
60
  * v0.4.2 (P1): per-request AbortController. `cancelAll()` aborts every
53
61
  * pending HTTPS request — called from the adapter's `onUnload` so a slow
@@ -108,29 +116,37 @@ class ParcelClient {
108
116
  async addDelivery(delivery) {
109
117
  return this.request("POST", "/add-delivery/", true, delivery);
110
118
  }
111
- /** Get carrier names (cached after first call) */
119
+ /** Get carrier names (cached after first call; concurrent callers share one fetch) */
112
120
  async getCarrierNames() {
113
- var _a, _b, _c;
114
121
  if (this.carrierCache) {
115
122
  return this.carrierCache;
116
123
  }
124
+ if (!this.carrierFetchInFlight) {
125
+ this.carrierFetchInFlight = this.fetchCarrierNames().finally(() => {
126
+ this.carrierFetchInFlight = null;
127
+ });
128
+ }
129
+ return this.carrierFetchInFlight;
130
+ }
131
+ /** One actual carrier-list fetch. Failure → empty map, NOT cached (retry next poll). */
132
+ async fetchCarrierNames() {
133
+ var _a, _b, _c;
117
134
  try {
118
135
  const raw = await this.request("GET", "/supported_carriers.json", false);
119
136
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
120
137
  this.carrierCache = raw;
121
138
  (_a = this.log) == null ? void 0 : _a.debug(`carriers: fetched ${Object.keys(this.carrierCache).length} entries`);
122
- } else {
123
- (_b = this.log) == null ? void 0 : _b.debug(
124
- `carriers: drift (got ${Array.isArray(raw) ? "array" : typeof raw}, expected object), kept empty`
125
- );
126
- return {};
139
+ return this.carrierCache;
127
140
  }
141
+ (_b = this.log) == null ? void 0 : _b.debug(
142
+ `carriers: drift (got ${Array.isArray(raw) ? "array" : typeof raw}, expected object), kept empty`
143
+ );
144
+ return {};
128
145
  } catch (err) {
129
146
  const msg = err instanceof Error ? err.message : String(err);
130
147
  (_c = this.log) == null ? void 0 : _c.debug(`carriers: fetch failed (kept empty, will retry): ${msg}`);
131
148
  return {};
132
149
  }
133
- return this.carrierCache;
134
150
  }
135
151
  /**
136
152
  * Resolve a carrier code to a display name.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/parcel-client.ts"],
4
- "sourcesContent": ["import * as http from \"node:http\";\nimport * 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/**\n * v0.4.3: optional logger injected by the adapter so the HTTPS client can\n * trace its own request/response lifecycle. When omitted (e.g. in tests),\n * every `this.log?.debug(...)` call is a no-op \u2014 keeps the bare-`apiKey`\n * constructor signature backward-compatible.\n */\nexport interface ParcelClientLogger {\n /** Adapter debug log. Called at most once per request/response decision. */\n debug(message: string): void;\n}\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/**\n * Build an `Error` carrying a `code` (and optional extra fields such as\n * `retryAfterSeconds`). Centralizes the `new Error(...) as Error & { code }`\n * + `err.code = \u2026` pattern that was repeated at every throw/reject site.\n *\n * @param message Human-readable error message.\n * @param code Machine-readable error code used by the adapter for classification.\n * @param extra Optional additional own-properties to attach to the error.\n */\nfunction apiError(message: string, code: string, extra?: Record<string, unknown>): Error & { code: string } {\n const err = new Error(message) as Error & { code: string };\n err.code = code;\n if (extra) {\n Object.assign(err, extra);\n }\n return err;\n}\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 /** v0.4.3: optional logger for the HTTPS-layer trace. See {@link ParcelClientLogger}. */\n private readonly log?: ParcelClientLogger;\n /** API base URL. Overridable so tests can run the real `request()` against a local mock server. */\n private readonly baseUrl: string;\n\n /**\n * @param apiKey The parcel.app API key\n * @param log Optional adapter logger for HTTPS-layer trace (v0.4.3)\n * @param baseUrl API base URL \u2014 defaults to the production endpoint; overridden in tests\n */\n constructor(apiKey: string, log?: ParcelClientLogger, baseUrl: string = API_BASE) {\n this.apiKey = apiKey;\n this.log = log;\n this.baseUrl = baseUrl;\n }\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.\n */\n cancelAll(): void {\n // v0.4.3 (A12): trace the shutdown anchor so the adapter log shows\n // exactly how many HTTPS calls were aborted at unload.\n this.log?.debug(`cancelAll: aborting ${this.inflight.size} inflight requests`);\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 // v0.4.3 (A11a): trace malformed-response drift before throwing.\n this.log?.debug(`API drift: malformed response (got ${typeof response})`);\n throw apiError(\"API error: malformed response\", \"API_ERROR\");\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 // v0.4.3 (A11b): trace API-side error before throwing.\n this.log?.debug(`API drift: success=false, code='${code}', msg='${rawMsg}'`);\n throw apiError(`API error: ${rawMsg || code}`, rawCode === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\");\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 // v0.4.3 (D1): trace the one-time cache fill so a successful warm-up\n // is visible in the debug log (happens once per adapter restart).\n this.log?.debug(`carriers: fetched ${Object.keys(this.carrierCache).length} entries`);\n } else {\n // v0.4.3 (D3): non-object drift \u2014 supported_carriers.json returned\n // something that isn't an object. Empty map is returned, NOT cached.\n this.log?.debug(\n `carriers: drift (got ${Array.isArray(raw) ? \"array\" : typeof raw}, expected object), kept empty`,\n );\n return {};\n }\n } catch (err) {\n // v0.4.3 (D2): trace the fetch-fail so the empty-map fallback isn't\n // silent. NOT cached \u2014 next poll retries; the trace then shows the\n // retry, too. Without this the user sees carrier codes instead of\n // names with no log entry explaining why.\n const msg = err instanceof Error ? err.message : String(err);\n this.log?.debug(`carriers: fetch failed (kept empty, will retry): ${msg}`);\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 // v0.4.3 (D4): trace non-string code drift. Helps diagnose \"all my\n // packages show UNKNOWN carrier\" reports.\n this.log?.debug(`getCarrierName: non-string code (got ${typeof carrierCode}), returning UNKNOWN`);\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 // v0.4.3 (A0): start timestamp for elapsed-ms in the success/timeout/error\n // log lines. One LOC, no behavior change.\n const startedAt = Date.now();\n // v0.4.3 (A1): trace request entry. ~144 calls/day at the default 10-min\n // poll interval \u2014 acceptable at debug.\n this.log?.debug(`HTTP ${method} ${path}`);\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(`${this.baseUrl}${path}`);\n } catch {\n // v0.4.3 (A10): trace invalid-URL drift before throwing.\n this.log?.debug(`HTTP invalid URL: ${this.baseUrl}${path}`);\n reject(apiError(`Invalid URL: ${this.baseUrl}${path}`, \"INVALID_URL\"));\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 // Pick transport from the URL protocol so tests can run the real\n // request() against a local http mock server; production is always https.\n const transportRequest: (\n opts: https.RequestOptions,\n callback: (res: http.IncomingMessage) => void,\n ) => http.ClientRequest = url.protocol === \"http:\" ? http.request : https.request;\n\n const req = transportRequest(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 oversized responses so a compromised or\n // misconfigured endpoint can't OOM the adapter. Reject with the\n // stable BODY_TOO_LARGE code here, then destroy WITHOUT an error so\n // req.on(\"error\") doesn't fire a second, codeless rejection (the\n // earlier `req.destroy(Error)` preempted the end-handler's code).\n if (bodyBytes > MAX_BODY_BYTES) {\n oversized = true;\n // v0.4.3 (A9): trace the oversize-drop before destroying.\n this.log?.debug(`HTTP body oversized ${path}: dropping at ${bodyBytes}B`);\n cleanup();\n reject(apiError(\"Response body too large\", \"BODY_TOO_LARGE\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n res.on(\"end\", () => {\n if (oversized) {\n return; // already cleaned up + rejected in the data handler\n }\n cleanup();\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 retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;\n // v0.4.3 (A4): trace 429 with the parsed retry-after.\n this.log?.debug(`HTTP 429 ${path} \u2192 retry-after=${retryAfterSeconds}s`);\n reject(apiError(\"Rate limit exceeded\", \"RATE_LIMITED\", { retryAfterSeconds }));\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 code =\n res.statusCode === 401 ? \"INVALID_API_KEY\" : res.statusCode === 403 ? \"FORBIDDEN\" : \"HTTP_ERROR\";\n // v0.4.3 (A3): trace 4xx/5xx with body-snippet for diagnosis.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} ${code} (body=${raw.substring(0, 200)})`);\n reject(apiError(`HTTP ${res.statusCode}: ${res.statusMessage}`, code));\n return;\n }\n\n try {\n const parsed = JSON.parse(raw) as T;\n // v0.4.3 (A2): trace successful response with elapsed-ms + bytes.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} (${Date.now() - startedAt}ms, ${bodyBytes}B)`);\n resolve(parsed);\n } catch {\n // v0.4.3 (A8): trace JSON parse-fail with snippet.\n this.log?.debug(`HTTP JSON parse fail ${path}: ${raw.substring(0, 200)}`);\n reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));\n }\n });\n });\n\n ctrl.signal.addEventListener(\"abort\", () => {\n // v0.4.3: A6 deliberately omitted \u2014 `req.destroy(Error)` propagates\n // through `req.on(\"error\")` below where A7 already logs it.\n req.destroy(new Error(\"Request aborted\"));\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n cleanup();\n // v0.4.3 (A5): trace timeout with elapsed-ms.\n this.log?.debug(`HTTP timeout ${method} ${path} (${Date.now() - startedAt}ms)`);\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", err => {\n cleanup();\n // v0.4.3 (A7): trace network / abort / TLS / DNS errors with elapsed.\n // Also catches the abort case (req.destroy(Error(\"Request aborted\")))\n // \u2014 A6 deliberately not emitted to avoid double-log.\n this.log?.debug(`HTTP error ${method} ${path} (${Date.now() - startedAt}ms): ${err.message}`);\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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",
4
+ "sourcesContent": ["import * as http from \"node:http\";\nimport * 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/**\n * v0.4.3: optional logger injected by the adapter so the HTTPS client can\n * trace its own request/response lifecycle. When omitted (e.g. in tests),\n * every `this.log?.debug(...)` call is a no-op \u2014 keeps the bare-`apiKey`\n * constructor signature backward-compatible.\n */\nexport interface ParcelClientLogger {\n /** Adapter debug log. Called at most once per request/response decision. */\n debug(message: string): void;\n}\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/**\n * Build an `Error` carrying a `code` (and optional extra fields such as\n * `retryAfterSeconds`). Centralizes the `new Error(...) as Error & { code }`\n * + `err.code = \u2026` pattern that was repeated at every throw/reject site.\n *\n * @param message Human-readable error message.\n * @param code Machine-readable error code used by the adapter for classification.\n * @param extra Optional additional own-properties to attach to the error.\n */\nfunction apiError(message: string, code: string, extra?: Record<string, unknown>): Error & { code: string } {\n const err = new Error(message) as Error & { code: string };\n err.code = code;\n if (extra) {\n Object.assign(err, extra);\n }\n return err;\n}\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.7.2: in-flight fetch for the carrier list. The per-delivery updates run\n * in parallel (Promise.all) and each resolves carrier names \u2014 without this\n * mutex the first poll with N packages fired N identical concurrent fetches\n * of the static 447-entry file (and a persistently failing endpoint was\n * retried N times per poll). Same pattern as beszel's auth mutex (B1).\n */\n private carrierFetchInFlight: Promise<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 /** v0.4.3: optional logger for the HTTPS-layer trace. See {@link ParcelClientLogger}. */\n private readonly log?: ParcelClientLogger;\n /** API base URL. Overridable so tests can run the real `request()` against a local mock server. */\n private readonly baseUrl: string;\n\n /**\n * @param apiKey The parcel.app API key\n * @param log Optional adapter logger for HTTPS-layer trace (v0.4.3)\n * @param baseUrl API base URL \u2014 defaults to the production endpoint; overridden in tests\n */\n constructor(apiKey: string, log?: ParcelClientLogger, baseUrl: string = API_BASE) {\n this.apiKey = apiKey;\n this.log = log;\n this.baseUrl = baseUrl;\n }\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.\n */\n cancelAll(): void {\n // v0.4.3 (A12): trace the shutdown anchor so the adapter log shows\n // exactly how many HTTPS calls were aborted at unload.\n this.log?.debug(`cancelAll: aborting ${this.inflight.size} inflight requests`);\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 // v0.4.3 (A11a): trace malformed-response drift before throwing.\n this.log?.debug(`API drift: malformed response (got ${typeof response})`);\n throw apiError(\"API error: malformed response\", \"API_ERROR\");\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 // v0.4.3 (A11b): trace API-side error before throwing.\n this.log?.debug(`API drift: success=false, code='${code}', msg='${rawMsg}'`);\n throw apiError(`API error: ${rawMsg || code}`, rawCode === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\");\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; concurrent callers share one fetch) */\n async getCarrierNames(): Promise<CarrierMap> {\n if (this.carrierCache) {\n return this.carrierCache;\n }\n // v0.7.2: share one in-flight fetch between the parallel per-delivery\n // updates instead of firing N identical requests on the first poll.\n if (!this.carrierFetchInFlight) {\n this.carrierFetchInFlight = this.fetchCarrierNames().finally(() => {\n this.carrierFetchInFlight = null;\n });\n }\n return this.carrierFetchInFlight;\n }\n\n /** One actual carrier-list fetch. Failure \u2192 empty map, NOT cached (retry next poll). */\n private async fetchCarrierNames(): Promise<CarrierMap> {\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 // v0.4.3 (D1): trace the one-time cache fill so a successful warm-up\n // is visible in the debug log (happens once per adapter restart).\n this.log?.debug(`carriers: fetched ${Object.keys(this.carrierCache).length} entries`);\n return this.carrierCache;\n }\n // v0.4.3 (D3): non-object drift \u2014 supported_carriers.json returned\n // something that isn't an object. Empty map is returned, NOT cached.\n this.log?.debug(\n `carriers: drift (got ${Array.isArray(raw) ? \"array\" : typeof raw}, expected object), kept empty`,\n );\n return {};\n } catch (err) {\n // v0.4.3 (D2): trace the fetch-fail so the empty-map fallback isn't\n // silent. NOT cached \u2014 next poll retries; the trace then shows the\n // retry, too. Without this the user sees carrier codes instead of\n // names with no log entry explaining why.\n const msg = err instanceof Error ? err.message : String(err);\n this.log?.debug(`carriers: fetch failed (kept empty, will retry): ${msg}`);\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\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 // v0.4.3 (D4): trace non-string code drift. Helps diagnose \"all my\n // packages show UNKNOWN carrier\" reports.\n this.log?.debug(`getCarrierName: non-string code (got ${typeof carrierCode}), returning UNKNOWN`);\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 // v0.4.3 (A0): start timestamp for elapsed-ms in the success/timeout/error\n // log lines. One LOC, no behavior change.\n const startedAt = Date.now();\n // v0.4.3 (A1): trace request entry. ~144 calls/day at the default 10-min\n // poll interval \u2014 acceptable at debug.\n this.log?.debug(`HTTP ${method} ${path}`);\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(`${this.baseUrl}${path}`);\n } catch {\n // v0.4.3 (A10): trace invalid-URL drift before throwing.\n this.log?.debug(`HTTP invalid URL: ${this.baseUrl}${path}`);\n reject(apiError(`Invalid URL: ${this.baseUrl}${path}`, \"INVALID_URL\"));\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 // Pick transport from the URL protocol so tests can run the real\n // request() against a local http mock server; production is always https.\n const transportRequest: (\n opts: https.RequestOptions,\n callback: (res: http.IncomingMessage) => void,\n ) => http.ClientRequest = url.protocol === \"http:\" ? http.request : https.request;\n\n const req = transportRequest(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 oversized responses so a compromised or\n // misconfigured endpoint can't OOM the adapter. Reject with the\n // stable BODY_TOO_LARGE code here, then destroy WITHOUT an error so\n // req.on(\"error\") doesn't fire a second, codeless rejection (the\n // earlier `req.destroy(Error)` preempted the end-handler's code).\n if (bodyBytes > MAX_BODY_BYTES) {\n oversized = true;\n // v0.4.3 (A9): trace the oversize-drop before destroying.\n this.log?.debug(`HTTP body oversized ${path}: dropping at ${bodyBytes}B`);\n cleanup();\n reject(apiError(\"Response body too large\", \"BODY_TOO_LARGE\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n res.on(\"end\", () => {\n if (oversized) {\n return; // already cleaned up + rejected in the data handler\n }\n cleanup();\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 retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;\n // v0.4.3 (A4): trace 429 with the parsed retry-after.\n this.log?.debug(`HTTP 429 ${path} \u2192 retry-after=${retryAfterSeconds}s`);\n reject(apiError(\"Rate limit exceeded\", \"RATE_LIMITED\", { retryAfterSeconds }));\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 code =\n res.statusCode === 401 ? \"INVALID_API_KEY\" : res.statusCode === 403 ? \"FORBIDDEN\" : \"HTTP_ERROR\";\n // v0.4.3 (A3): trace 4xx/5xx with body-snippet for diagnosis.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} ${code} (body=${raw.substring(0, 200)})`);\n reject(apiError(`HTTP ${res.statusCode}: ${res.statusMessage}`, code));\n return;\n }\n\n try {\n const parsed = JSON.parse(raw) as T;\n // v0.4.3 (A2): trace successful response with elapsed-ms + bytes.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} (${Date.now() - startedAt}ms, ${bodyBytes}B)`);\n resolve(parsed);\n } catch {\n // v0.4.3 (A8): trace JSON parse-fail with snippet.\n this.log?.debug(`HTTP JSON parse fail ${path}: ${raw.substring(0, 200)}`);\n reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));\n }\n });\n });\n\n ctrl.signal.addEventListener(\"abort\", () => {\n // v0.4.3: A6 deliberately omitted \u2014 `req.destroy(Error)` propagates\n // through `req.on(\"error\")` below where A7 already logs it.\n req.destroy(new Error(\"Request aborted\"));\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n cleanup();\n // v0.4.3 (A5): trace timeout with elapsed-ms.\n this.log?.debug(`HTTP timeout ${method} ${path} (${Date.now() - startedAt}ms)`);\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", err => {\n cleanup();\n // v0.4.3 (A7): trace network / abort / TLS / DNS errors with elapsed.\n // Also catches the abort case (req.destroy(Error(\"Request aborted\")))\n // \u2014 A6 deliberately not emitted to avoid double-log.\n this.log?.debug(`HTTP error ${method} ${path} (${Date.now() - startedAt}ms): ${err.message}`);\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": ["_a"]
7
7
  }
@@ -45,6 +45,30 @@ class StateManager {
45
45
  * triggers a fresh creation.
46
46
  */
47
47
  createdIds = /* @__PURE__ */ new Set();
48
+ /**
49
+ * v0.7.2: last-written device-object signature per package id (the name
50
+ * source: description + tracking number). `updateDelivery` used to
51
+ * extendObject the device on EVERY poll — one object write + objectChange
52
+ * event per package per minute for data that practically never changes.
53
+ * Now the write happens only when the signature differs.
54
+ */
55
+ deviceWritten = /* @__PURE__ */ new Map();
56
+ /**
57
+ * v0.7.2: signature of the last-written state values per package id.
58
+ * `lastUpdated` is only refreshed when at least one sibling value actually
59
+ * changed — before, a fresh ISO timestamp fired one guaranteed state event
60
+ * per package per poll, defeating the v0.5.3 skip-unchanged optimization
61
+ * for that state. Semantics: `lastUpdated` = "when the tracking data last
62
+ * changed", not "when the adapter last polled".
63
+ */
64
+ valuesSig = /* @__PURE__ */ new Map();
65
+ /**
66
+ * v0.7.2: package ids known to exist as device objects. Filled from the
67
+ * object view ONCE after adapter start (reconciles leftovers from previous
68
+ * runs), afterwards maintained in memory — `cleanupDeliveries` no longer
69
+ * needs a DB round-trip per poll.
70
+ */
71
+ knownDeliveryIds = null;
48
72
  /**
49
73
  * @param adapter The ioBroker adapter instance
50
74
  * @param language Language code from system.config.language (falls back to English)
@@ -161,22 +185,28 @@ class StateManager {
161
185
  * pre-pass. Falls back to computing it here when called directly (tests).
162
186
  */
163
187
  async updateDelivery(delivery, carrierName, precomputedId) {
188
+ var _a;
164
189
  const pkgId = precomputedId != null ? precomputedId : this.packageId(delivery);
165
190
  const devicePath = `deliveries.${pkgId}`;
166
191
  const description = typeof delivery.description === "string" ? delivery.description : "";
167
192
  const trackingNumber = typeof delivery.tracking_number === "string" ? delivery.tracking_number : "";
168
193
  const extraInfo = typeof delivery.extra_information === "string" ? delivery.extra_information : "";
169
- await this.adapter.extendObjectAsync(
170
- devicePath,
171
- {
172
- type: "device",
173
- common: {
174
- name: description || `Package ${trackingNumber || pkgId}`
194
+ const deviceSig = `${description} ${trackingNumber}`;
195
+ if (this.deviceWritten.get(pkgId) !== deviceSig) {
196
+ await this.adapter.extendObjectAsync(
197
+ devicePath,
198
+ {
199
+ type: "device",
200
+ common: {
201
+ name: description || `Package ${trackingNumber || pkgId}`
202
+ },
203
+ native: {}
175
204
  },
176
- native: {}
177
- },
178
- { preserve: { common: ["name"] } }
179
- );
205
+ { preserve: { common: ["name"] } }
206
+ );
207
+ this.deviceWritten.set(pkgId, deviceSig);
208
+ }
209
+ (_a = this.knownDeliveryIds) == null ? void 0 : _a.add(pkgId);
180
210
  const statusCode = this.parseStatus(delivery);
181
211
  const labels = import_types.STATUS_LABELS[this.language];
182
212
  let statusText = labels[statusCode];
@@ -184,6 +214,10 @@ class StateManager {
184
214
  this.adapter.log.debug(`status code ${statusCode} not in STATUS_LABELS[${this.language}], using fallback`);
185
215
  statusText = `Unknown (${statusCode})`;
186
216
  }
217
+ const deliveryWindow = this.calculateDeliveryWindow(delivery, statusCode);
218
+ const deliveryEstimate = this.calculateDeliveryEstimate(delivery, statusCode);
219
+ const lastEvent = this.formatLastEvent(delivery);
220
+ const lastLocation = this.extractLastLocation(delivery);
187
221
  await Promise.all([
188
222
  this.createAndSet(`${devicePath}.carrier`, (0, import_i18n.tName)("carrier"), "string", "text", carrierName),
189
223
  this.createAndSet(`${devicePath}.status`, (0, import_i18n.tName)("status"), "string", "text", statusText),
@@ -191,36 +225,39 @@ class StateManager {
191
225
  this.createAndSet(`${devicePath}.description`, (0, import_i18n.tName)("description"), "string", "text", description),
192
226
  this.createAndSet(`${devicePath}.trackingNumber`, (0, import_i18n.tName)("trackingNumber"), "string", "text", trackingNumber),
193
227
  this.createAndSet(`${devicePath}.extraInfo`, (0, import_i18n.tName)("extraInfo"), "string", "text", extraInfo),
194
- this.createAndSet(
195
- `${devicePath}.deliveryWindow`,
196
- (0, import_i18n.tName)("deliveryWindow"),
197
- "string",
198
- "text",
199
- this.calculateDeliveryWindow(delivery, statusCode)
200
- ),
228
+ this.createAndSet(`${devicePath}.deliveryWindow`, (0, import_i18n.tName)("deliveryWindow"), "string", "text", deliveryWindow),
201
229
  this.createAndSet(
202
230
  `${devicePath}.deliveryEstimate`,
203
231
  (0, import_i18n.tName)("deliveryEstimate"),
204
232
  "string",
205
233
  "text",
206
- this.calculateDeliveryEstimate(delivery, statusCode)
234
+ deliveryEstimate
207
235
  ),
208
- this.createAndSet(
209
- `${devicePath}.lastEvent`,
210
- (0, import_i18n.tName)("lastEvent"),
211
- "string",
212
- "text",
213
- this.formatLastEvent(delivery)
214
- ),
215
- this.createAndSet(
216
- `${devicePath}.lastLocation`,
217
- (0, import_i18n.tName)("lastLocation"),
218
- "string",
219
- "text",
220
- this.extractLastLocation(delivery)
221
- ),
222
- this.createAndSet(`${devicePath}.lastUpdated`, (0, import_i18n.tName)("lastUpdated"), "string", "date", (/* @__PURE__ */ new Date()).toISOString())
236
+ this.createAndSet(`${devicePath}.lastEvent`, (0, import_i18n.tName)("lastEvent"), "string", "text", lastEvent),
237
+ this.createAndSet(`${devicePath}.lastLocation`, (0, import_i18n.tName)("lastLocation"), "string", "text", lastLocation)
223
238
  ]);
239
+ const sig = JSON.stringify([
240
+ carrierName,
241
+ statusText,
242
+ statusCode,
243
+ description,
244
+ trackingNumber,
245
+ extraInfo,
246
+ deliveryWindow,
247
+ deliveryEstimate,
248
+ lastEvent,
249
+ lastLocation
250
+ ]);
251
+ if (this.valuesSig.get(pkgId) !== sig) {
252
+ this.valuesSig.set(pkgId, sig);
253
+ await this.createAndSet(
254
+ `${devicePath}.lastUpdated`,
255
+ (0, import_i18n.tName)("lastUpdated"),
256
+ "string",
257
+ "date",
258
+ (/* @__PURE__ */ new Date()).toISOString()
259
+ );
260
+ }
224
261
  }
225
262
  /**
226
263
  * Update summary states. Expects already-filtered active deliveries.
@@ -251,26 +288,35 @@ class StateManager {
251
288
  * @param activeIds List of currently active package IDs
252
289
  */
253
290
  async cleanupDeliveries(activeIds) {
254
- const activeSet = new Set(activeIds.map((id) => `deliveries.${id}`));
255
- const objects = await this.adapter.getObjectViewAsync("system", "device", {
256
- startkey: `${this.adapter.namespace}.deliveries.`,
257
- endkey: `${this.adapter.namespace}.deliveries.${ID_RANGE_END}`
258
- });
259
- if (!(objects == null ? void 0 : objects.rows)) {
260
- this.adapter.log.debug("cleanupDeliveries: no objects view available, skipping");
261
- return;
262
- }
263
- const toDelete = [];
264
- for (const row of objects.rows) {
265
- const relativeId = row.id.replace(`${this.adapter.namespace}.`, "");
266
- if (relativeId.startsWith("deliveries.") && !activeSet.has(relativeId)) {
267
- toDelete.push(relativeId);
291
+ if (this.knownDeliveryIds === null) {
292
+ const objects = await this.adapter.getObjectViewAsync("system", "device", {
293
+ startkey: `${this.adapter.namespace}.deliveries.`,
294
+ endkey: `${this.adapter.namespace}.deliveries.${ID_RANGE_END}`
295
+ });
296
+ if (!(objects == null ? void 0 : objects.rows)) {
297
+ this.adapter.log.debug("cleanupDeliveries: no objects view available, skipping");
298
+ return;
299
+ }
300
+ this.knownDeliveryIds = /* @__PURE__ */ new Set();
301
+ for (const row of objects.rows) {
302
+ const relativeId = row.id.replace(`${this.adapter.namespace}.`, "");
303
+ if (relativeId.startsWith("deliveries.")) {
304
+ const pkgId = relativeId.slice("deliveries.".length).split(".")[0];
305
+ if (pkgId) {
306
+ this.knownDeliveryIds.add(pkgId);
307
+ }
308
+ }
268
309
  }
269
310
  }
311
+ const activeSet = new Set(activeIds);
312
+ const toDelete = [...this.knownDeliveryIds].filter((pkgId) => !activeSet.has(pkgId));
270
313
  await Promise.all(
271
- toDelete.map(async (relativeId) => {
314
+ toDelete.map(async (pkgId) => {
315
+ const relativeId = `deliveries.${pkgId}`;
272
316
  await this.adapter.delObjectAsync(relativeId, { recursive: true });
273
317
  this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);
318
+ this.deviceWritten.delete(pkgId);
319
+ this.valuesSig.delete(pkgId);
274
320
  for (const id of [...this.createdIds]) {
275
321
  if (id === relativeId || id.startsWith(`${relativeId}.`)) {
276
322
  this.createdIds.delete(id);
@@ -278,6 +324,7 @@ class StateManager {
278
324
  }
279
325
  })
280
326
  );
327
+ this.knownDeliveryIds = new Set(activeSet);
281
328
  }
282
329
  /**
283
330
  * Resolve a delivery's expected window to epoch-millis bounds. Returns null
@@ -344,7 +391,8 @@ class StateManager {
344
391
  if (ts !== null && ts > 0) {
345
392
  expectedDate = new Date(ts * 1e3);
346
393
  } else if (typeof delivery.date_expected === "string" && delivery.date_expected.length > 0) {
347
- expectedDate = new Date(delivery.date_expected);
394
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(delivery.date_expected);
395
+ expectedDate = dateOnly ? new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3])) : new Date(delivery.date_expected);
348
396
  }
349
397
  if (!expectedDate || isNaN(expectedDate.getTime())) {
350
398
  return null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/state-manager.ts"],
4
- "sourcesContent": ["import { I18n, type AdapterInstance } from \"@iobroker/adapter-core\";\nimport { coerceFiniteNumber } from \"./coerce\";\nimport { tName } from \"./i18n\";\nimport type { ParcelDelivery, ParcelEvent } from \"./types\";\nimport { STATUS_LABELS, SUPPORTED_LANGUAGES, FALLBACK_LANGUAGE, UNKNOWN_STATUS_CODE } from \"./types\";\n\n/** Status codes that have expected delivery date/time */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Upper bound for the `deliveries.*` object-view range query: the highest BMP\n * code unit, so the range covers every possible sanitized package id.\n */\nconst ID_RANGE_END = \"\uFFFF\";\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 if (Number.isFinite(n)) {\n return n;\n }\n }\n // API drift (non-numeric / non-string status_code). Return a visible\n // \"unknown\" sentinel instead of 0 (Delivered) \u2014 otherwise a garbage\n // status_code would silently filter the package out and remove it in\n // autoRemove mode. The active filter is `status !== 0`, so -1 stays visible.\n this.adapter.log.debug(\n `parseStatus drift: ${JSON.stringify(raw)} (type ${typeof raw}) \u2192 ${UNKNOWN_STATUS_CODE} (unknown, kept visible)`,\n );\n return UNKNOWN_STATUS_CODE;\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 // v0.4.3 (C3): trace the collision-suffix path. Rare event but the\n // resulting state-id divergence is hard to diagnose without a log.\n this.adapter.log.debug(\n `packageId collision: bare='${id}' owner='${owner}' new='${rawKey}' \u2192 suffixed='${suffixed}'`,\n );\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 * @param precomputedId Optional package id from the caller's deterministic\n * pre-pass. Falls back to computing it here when called directly (tests).\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string, precomputedId?: string): Promise<void> {\n const pkgId = precomputedId ?? 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(\n devicePath,\n {\n type: \"device\",\n common: {\n name: description || `Package ${trackingNumber || pkgId}`,\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n\n const statusCode = this.parseStatus(delivery);\n const labels = STATUS_LABELS[this.language];\n let statusText = labels[statusCode];\n if (!statusText) {\n // v0.4.3 (E3): trace unknown status-code (API drift). A future\n // parcel.app status (e.g. 9, 10) would render as \"Unknown (N)\"\n // without any log clue that the codes table is out of date.\n this.adapter.log.debug(`status code ${statusCode} not in STATUS_LABELS[${this.language}], using fallback`);\n statusText = `Unknown (${statusCode})`;\n }\n\n await Promise.all([\n this.createAndSet(`${devicePath}.carrier`, tName(\"carrier\"), \"string\", \"text\", carrierName),\n this.createAndSet(`${devicePath}.status`, tName(\"status\"), \"string\", \"text\", statusText),\n this.createAndSet(`${devicePath}.statusCode`, tName(\"statusCode\"), \"number\", \"value\", statusCode),\n this.createAndSet(`${devicePath}.description`, tName(\"description\"), \"string\", \"text\", description),\n this.createAndSet(`${devicePath}.trackingNumber`, tName(\"trackingNumber\"), \"string\", \"text\", trackingNumber),\n this.createAndSet(`${devicePath}.extraInfo`, tName(\"extraInfo\"), \"string\", \"text\", extraInfo),\n this.createAndSet(\n `${devicePath}.deliveryWindow`,\n tName(\"deliveryWindow\"),\n \"string\",\n \"text\",\n this.calculateDeliveryWindow(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.deliveryEstimate`,\n tName(\"deliveryEstimate\"),\n \"string\",\n \"text\",\n this.calculateDeliveryEstimate(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.lastEvent`,\n tName(\"lastEvent\"),\n \"string\",\n \"text\",\n this.formatLastEvent(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastLocation`,\n tName(\"lastLocation\"),\n \"string\",\n \"text\",\n this.extractLastLocation(delivery),\n ),\n this.createAndSet(`${devicePath}.lastUpdated`, tName(\"lastUpdated\"), \"string\", \"date\", new Date().toISOString()),\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 // v0.4.3 (E1): trace summary refresh \u2014 ~144/day at the default poll\n // interval, kept short (counts only).\n this.adapter.log.debug(\n `updateSummary: ${activeDeliveries.length} active, ${todayDeliveries.length} expected today`,\n );\n\n await Promise.all([\n this.createAndSet(\"summary.activeCount\", tName(\"activeCount\"), \"number\", \"value\", activeDeliveries.length),\n this.createAndSet(\"summary.todayCount\", tName(\"todayCount\"), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n 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.${ID_RANGE_END}`,\n });\n if (!objects?.rows) {\n // v0.4.3 (E2): trace the no-op path \u2014 happens on fresh installs or\n // when getObjectViewAsync returns falsy. Without this the early-return\n // is invisible.\n this.adapter.log.debug(\"cleanupDeliveries: no objects view available, skipping\");\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 * Resolve a delivery's expected window to epoch-millis bounds. Returns null\n * for non-trackable status or when there is no usable start timestamp.\n * `end` is null when only a single expected time is known.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private windowBoundsMs(delivery: ParcelDelivery, statusCode: number): { start: number; end: number | null } | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n const toMs = (timestamp: unknown): number | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const ms = ts * 1000;\n return Number.isNaN(new Date(ms).getTime()) ? null : ms;\n };\n const start = toMs(delivery.timestamp_expected);\n if (start === null) {\n return null;\n }\n return { start, end: toMs(delivery.timestamp_expected_end) };\n }\n\n /**\n * Format epoch-millis as local HH:MM.\n *\n * @param ms Epoch milliseconds\n */\n private static formatHHMM(ms: number): string {\n const d = new Date(ms);\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n }\n\n /**\n * Calculate a delivery time-window string \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 const bounds = this.windowBoundsMs(delivery, statusCode);\n if (!bounds) {\n return \"\";\n }\n const start = StateManager.formatHHMM(bounds.start);\n return bounds.end !== null ? `${start} - ${StateManager.formatHHMM(bounds.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 if (diffDays < 0) {\n return I18n.translate(\"estimateOverdue\");\n }\n if (diffDays === 0) {\n return I18n.translate(\"estimateToday\");\n }\n if (diffDays === 1) {\n return I18n.translate(\"estimateTomorrow\");\n }\n return I18n.translate(\"estimateDays\").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 private getLatestEvent(delivery: ParcelDelivery): ParcelEvent | null {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return null;\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return null;\n }\n return latest;\n }\n\n private formatLastEvent(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\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 private extractLastLocation(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Combined delivery window for today's packages: earliest start to latest\n * end across all windows. Computed from the raw millis (not the formatted\n * strings) so the latest end always wins \u2014 fixes the earlier bug where the\n * end of the latest-*starting* window was used instead of the maximum end.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const bounds = todayDeliveries\n .map(d => this.windowBoundsMs(d, this.parseStatus(d)))\n .filter((b): b is { start: number; end: number | null } => b !== null);\n\n if (bounds.length === 0) {\n return \"\";\n }\n\n const minStart = Math.min(...bounds.map(b => b.start));\n const maxEnd = Math.max(...bounds.map(b => b.end ?? b.start));\n const startStr = StateManager.formatHHMM(minStart);\n return maxEnd > minStart ? `${startStr} - ${StateManager.formatHHMM(maxEnd)}` : startStr;\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.setStateChangedAsync(id, { val, ack: true });\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { I18n, type AdapterInstance } from \"@iobroker/adapter-core\";\nimport { coerceFiniteNumber } from \"./coerce\";\nimport { tName } from \"./i18n\";\nimport type { ParcelDelivery, ParcelEvent } from \"./types\";\nimport { STATUS_LABELS, SUPPORTED_LANGUAGES, FALLBACK_LANGUAGE, UNKNOWN_STATUS_CODE } from \"./types\";\n\n/** Status codes that have expected delivery date/time */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Upper bound for the `deliveries.*` object-view range query: the highest BMP\n * code unit, so the range covers every possible sanitized package id.\n */\nconst ID_RANGE_END = \"\uFFFF\";\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 * v0.7.2: last-written device-object signature per package id (the name\n * source: description + tracking number). `updateDelivery` used to\n * extendObject the device on EVERY poll \u2014 one object write + objectChange\n * event per package per minute for data that practically never changes.\n * Now the write happens only when the signature differs.\n */\n private readonly deviceWritten = new Map<string, string>();\n\n /**\n * v0.7.2: signature of the last-written state values per package id.\n * `lastUpdated` is only refreshed when at least one sibling value actually\n * changed \u2014 before, a fresh ISO timestamp fired one guaranteed state event\n * per package per poll, defeating the v0.5.3 skip-unchanged optimization\n * for that state. Semantics: `lastUpdated` = \"when the tracking data last\n * changed\", not \"when the adapter last polled\".\n */\n private readonly valuesSig = new Map<string, string>();\n\n /**\n * v0.7.2: package ids known to exist as device objects. Filled from the\n * object view ONCE after adapter start (reconciles leftovers from previous\n * runs), afterwards maintained in memory \u2014 `cleanupDeliveries` no longer\n * needs a DB round-trip per poll.\n */\n private knownDeliveryIds: Set<string> | null = null;\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 if (Number.isFinite(n)) {\n return n;\n }\n }\n // API drift (non-numeric / non-string status_code). Return a visible\n // \"unknown\" sentinel instead of 0 (Delivered) \u2014 otherwise a garbage\n // status_code would silently filter the package out and remove it in\n // autoRemove mode. The active filter is `status !== 0`, so -1 stays visible.\n this.adapter.log.debug(\n `parseStatus drift: ${JSON.stringify(raw)} (type ${typeof raw}) \u2192 ${UNKNOWN_STATUS_CODE} (unknown, kept visible)`,\n );\n return UNKNOWN_STATUS_CODE;\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 // v0.4.3 (C3): trace the collision-suffix path. Rare event but the\n // resulting state-id divergence is hard to diagnose without a log.\n this.adapter.log.debug(\n `packageId collision: bare='${id}' owner='${owner}' new='${rawKey}' \u2192 suffixed='${suffixed}'`,\n );\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 * @param precomputedId Optional package id from the caller's deterministic\n * pre-pass. Falls back to computing it here when called directly (tests).\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string, precomputedId?: string): Promise<void> {\n const pkgId = precomputedId ?? 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 // v0.7.2: only write the device object when its name source changed \u2014\n // extendObject on every poll meant one object write + objectChange event\n // per package per minute for data that practically never changes.\n const deviceSig = `${description} ${trackingNumber}`;\n if (this.deviceWritten.get(pkgId) !== deviceSig) {\n await this.adapter.extendObjectAsync(\n devicePath,\n {\n type: \"device\",\n common: {\n name: description || `Package ${trackingNumber || pkgId}`,\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n this.deviceWritten.set(pkgId, deviceSig);\n }\n this.knownDeliveryIds?.add(pkgId);\n\n const statusCode = this.parseStatus(delivery);\n const labels = STATUS_LABELS[this.language];\n let statusText = labels[statusCode];\n if (!statusText) {\n // v0.4.3 (E3): trace unknown status-code (API drift). A future\n // parcel.app status (e.g. 9, 10) would render as \"Unknown (N)\"\n // without any log clue that the codes table is out of date.\n this.adapter.log.debug(`status code ${statusCode} not in STATUS_LABELS[${this.language}], using fallback`);\n statusText = `Unknown (${statusCode})`;\n }\n\n const deliveryWindow = this.calculateDeliveryWindow(delivery, statusCode);\n const deliveryEstimate = this.calculateDeliveryEstimate(delivery, statusCode);\n const lastEvent = this.formatLastEvent(delivery);\n const lastLocation = this.extractLastLocation(delivery);\n\n await Promise.all([\n this.createAndSet(`${devicePath}.carrier`, tName(\"carrier\"), \"string\", \"text\", carrierName),\n this.createAndSet(`${devicePath}.status`, tName(\"status\"), \"string\", \"text\", statusText),\n this.createAndSet(`${devicePath}.statusCode`, tName(\"statusCode\"), \"number\", \"value\", statusCode),\n this.createAndSet(`${devicePath}.description`, tName(\"description\"), \"string\", \"text\", description),\n this.createAndSet(`${devicePath}.trackingNumber`, tName(\"trackingNumber\"), \"string\", \"text\", trackingNumber),\n this.createAndSet(`${devicePath}.extraInfo`, tName(\"extraInfo\"), \"string\", \"text\", extraInfo),\n this.createAndSet(`${devicePath}.deliveryWindow`, tName(\"deliveryWindow\"), \"string\", \"text\", deliveryWindow),\n this.createAndSet(\n `${devicePath}.deliveryEstimate`,\n tName(\"deliveryEstimate\"),\n \"string\",\n \"text\",\n deliveryEstimate,\n ),\n this.createAndSet(`${devicePath}.lastEvent`, tName(\"lastEvent\"), \"string\", \"text\", lastEvent),\n this.createAndSet(`${devicePath}.lastLocation`, tName(\"lastLocation\"), \"string\", \"text\", lastLocation),\n ]);\n\n // v0.7.2: `lastUpdated` = \"when the tracking data last CHANGED\". Writing a\n // fresh timestamp every poll fired one guaranteed state event per package\n // per poll and defeated the skip-unchanged optimization for this state.\n const sig = JSON.stringify([\n carrierName,\n statusText,\n statusCode,\n description,\n trackingNumber,\n extraInfo,\n deliveryWindow,\n deliveryEstimate,\n lastEvent,\n lastLocation,\n ]);\n if (this.valuesSig.get(pkgId) !== sig) {\n this.valuesSig.set(pkgId, sig);\n await this.createAndSet(\n `${devicePath}.lastUpdated`,\n 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 // v0.4.3 (E1): trace summary refresh \u2014 ~144/day at the default poll\n // interval, kept short (counts only).\n this.adapter.log.debug(\n `updateSummary: ${activeDeliveries.length} active, ${todayDeliveries.length} expected today`,\n );\n\n await Promise.all([\n this.createAndSet(\"summary.activeCount\", tName(\"activeCount\"), \"number\", \"value\", activeDeliveries.length),\n this.createAndSet(\"summary.todayCount\", tName(\"todayCount\"), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n 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 // v0.7.2: the object view is queried only ONCE after adapter start to\n // reconcile leftovers from previous runs; afterwards the in-memory set\n // (maintained by updateDelivery + this prune) replaces the per-poll DB\n // round-trip.\n if (this.knownDeliveryIds === null) {\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.${ID_RANGE_END}`,\n });\n if (!objects?.rows) {\n // v0.4.3 (E2): trace the no-op path \u2014 happens on fresh installs or\n // when getObjectViewAsync returns falsy. Without this the early-return\n // is invisible (and the known-set stays unseeded for the next poll).\n this.adapter.log.debug(\"cleanupDeliveries: no objects view available, skipping\");\n return;\n }\n this.knownDeliveryIds = new Set<string>();\n for (const row of objects.rows) {\n const relativeId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (relativeId.startsWith(\"deliveries.\")) {\n // Direct device segment only (`deliveries.<pkgId>`).\n const pkgId = relativeId.slice(\"deliveries.\".length).split(\".\")[0];\n if (pkgId) {\n this.knownDeliveryIds.add(pkgId);\n }\n }\n }\n }\n\n const activeSet = new Set(activeIds);\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 = [...this.knownDeliveryIds].filter(pkgId => !activeSet.has(pkgId));\n\n await Promise.all(\n toDelete.map(async pkgId => {\n const relativeId = `deliveries.${pkgId}`;\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n this.deviceWritten.delete(pkgId);\n this.valuesSig.delete(pkgId);\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 this.knownDeliveryIds = new Set(activeSet);\n }\n\n /**\n * Resolve a delivery's expected window to epoch-millis bounds. Returns null\n * for non-trackable status or when there is no usable start timestamp.\n * `end` is null when only a single expected time is known.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private windowBoundsMs(delivery: ParcelDelivery, statusCode: number): { start: number; end: number | null } | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n const toMs = (timestamp: unknown): number | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const ms = ts * 1000;\n return Number.isNaN(new Date(ms).getTime()) ? null : ms;\n };\n const start = toMs(delivery.timestamp_expected);\n if (start === null) {\n return null;\n }\n return { start, end: toMs(delivery.timestamp_expected_end) };\n }\n\n /**\n * Format epoch-millis as local HH:MM.\n *\n * @param ms Epoch milliseconds\n */\n private static formatHHMM(ms: number): string {\n const d = new Date(ms);\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n }\n\n /**\n * Calculate a delivery time-window string \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 const bounds = this.windowBoundsMs(delivery, statusCode);\n if (!bounds) {\n return \"\";\n }\n const start = StateManager.formatHHMM(bounds.start);\n return bounds.end !== null ? `${start} - ${StateManager.formatHHMM(bounds.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 // A bare calendar date (\"YYYY-MM-DD\") is parsed as UTC midnight by `new Date()`,\n // but the today/tomorrow diff below reads LOCAL calendar components \u2014 in a\n // UTC-negative timezone that shifts the day one early. Parse the date-only\n // form as LOCAL midnight so the calendar-day diff is timezone-stable; fall\n // back to the native parser for any other shape (e.g. a full datetime).\n const dateOnly = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(delivery.date_expected);\n expectedDate = dateOnly\n ? new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]))\n : 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 if (diffDays < 0) {\n return I18n.translate(\"estimateOverdue\");\n }\n if (diffDays === 0) {\n return I18n.translate(\"estimateToday\");\n }\n if (diffDays === 1) {\n return I18n.translate(\"estimateTomorrow\");\n }\n return I18n.translate(\"estimateDays\").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 private getLatestEvent(delivery: ParcelDelivery): ParcelEvent | null {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return null;\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return null;\n }\n return latest;\n }\n\n private formatLastEvent(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\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 private extractLastLocation(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Combined delivery window for today's packages: earliest start to latest\n * end across all windows. Computed from the raw millis (not the formatted\n * strings) so the latest end always wins \u2014 fixes the earlier bug where the\n * end of the latest-*starting* window was used instead of the maximum end.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const bounds = todayDeliveries\n .map(d => this.windowBoundsMs(d, this.parseStatus(d)))\n .filter((b): b is { start: number; end: number | null } => b !== null);\n\n if (bounds.length === 0) {\n return \"\";\n }\n\n const minStart = Math.min(...bounds.map(b => b.start));\n const maxEnd = Math.max(...bounds.map(b => b.end ?? b.start));\n const startStr = StateManager.formatHHMM(minStart);\n return maxEnd > minStart ? `${startStr} - ${StateManager.formatHHMM(maxEnd)}` : startStr;\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.setStateChangedAsync(id, { val, ack: true });\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -5,6 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
8
12
  var __copyProps = (to, from, except, desc) => {
9
13
  if (from && typeof from === "object" || typeof from === "function") {
10
14
  for (let key of __getOwnPropNames(from))
@@ -21,6 +25,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
25
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
26
  mod
23
27
  ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var main_exports = {};
30
+ __export(main_exports, {
31
+ ParcelappAdapter: () => ParcelappAdapter
32
+ });
33
+ module.exports = __toCommonJS(main_exports);
24
34
  var utils = __toESM(require("@iobroker/adapter-core"));
25
35
  var import_adapter_core = require("@iobroker/adapter-core");
26
36
  var import_node_path = require("node:path");
@@ -35,6 +45,17 @@ const MIN_API_KEY_LENGTH = 10;
35
45
  class ParcelappAdapter extends utils.Adapter {
36
46
  client = null;
37
47
  stateManager = null;
48
+ /**
49
+ * Factories for the HTTP client + state manager — default to the real
50
+ * constructors. Test seams (fleet pattern): unit tests replace these with
51
+ * fakes to exercise the poll orchestration (throttle/force/rate-limit
52
+ * interplay, error routing, failure dedup) without real network.
53
+ *
54
+ * @param apiKey parcel.app API key
55
+ */
56
+ makeClient = (apiKey) => new import_parcel_client.ParcelClient(apiKey, { debug: (m) => this.log.debug(m) });
57
+ /** @param language Raw system language (resolution happens in StateManager) */
58
+ makeStateManager = (language) => new import_state_manager.StateManager(this, language);
38
59
  pollTimer = void 0;
39
60
  isPolling = false;
40
61
  lastPollTime = 0;
@@ -49,8 +70,6 @@ class ParcelappAdapter extends utils.Adapter {
49
70
  * the process alive past js-controller's 4-second kill deadline.
50
71
  */
51
72
  testClients = /* @__PURE__ */ new Set();
52
- /** ioBroker system language — read once in `onReady` from `system.config`. EN fallback. */
53
- systemLang = "en";
54
73
  /** @param options Adapter options */
55
74
  constructor(options = {}) {
56
75
  super({
@@ -70,18 +89,15 @@ class ParcelappAdapter extends utils.Adapter {
70
89
  );
71
90
  const sysConfig = await this.getForeignObjectAsync("system.config");
72
91
  const language = (_b = (_a = sysConfig == null ? void 0 : sysConfig.common) == null ? void 0 : _a.language) != null ? _b : "";
73
- if (typeof language === "string" && language.length > 0) {
74
- this.systemLang = language;
75
- }
76
- this.log.debug(`system language: '${language}' \u2192 using '${this.systemLang}'`);
92
+ this.log.debug(`system language: '${language}' \u2192 using '${(0, import_state_manager.resolveLanguage)(language)}'`);
77
93
  await this.setStateAsync("info.connection", { val: false, ack: true });
78
94
  const { apiKey } = this.config;
79
95
  if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {
80
96
  this.log.error("No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings");
81
97
  return;
82
98
  }
83
- this.client = new import_parcel_client.ParcelClient(apiKey.trim(), { debug: (m) => this.log.debug(m) });
84
- this.stateManager = new import_state_manager.StateManager(this, language);
99
+ this.client = this.makeClient(apiKey.trim());
100
+ this.stateManager = this.makeStateManager(language);
85
101
  await this.cleanupObsoleteStates();
86
102
  await this.poll();
87
103
  const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);
@@ -138,7 +154,7 @@ class ParcelappAdapter extends utils.Adapter {
138
154
  this.sendTo(obj.from, obj.command, { success: false, message: "API key is too short" }, obj.callback);
139
155
  return;
140
156
  }
141
- const testClient = new import_parcel_client.ParcelClient(key, { debug: (m) => this.log.debug(m) });
157
+ const testClient = this.makeClient(key);
142
158
  this.testClients.add(testClient);
143
159
  try {
144
160
  const result = await testClient.testConnection();
@@ -160,9 +176,25 @@ class ParcelappAdapter extends utils.Adapter {
160
176
  );
161
177
  return;
162
178
  }
163
- const request = obj.message;
179
+ const raw = obj.message;
180
+ const msg = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
181
+ if (typeof msg.tracking_number !== "string" || msg.tracking_number.length === 0 || typeof msg.carrier_code !== "string" || msg.carrier_code.length === 0) {
182
+ this.log.debug("addDelivery: missing tracking_number/carrier_code in message");
183
+ this.sendTo(
184
+ obj.from,
185
+ obj.command,
186
+ { success: false, error_message: "tracking_number and carrier_code are required" },
187
+ obj.callback
188
+ );
189
+ return;
190
+ }
191
+ const request = {
192
+ tracking_number: msg.tracking_number,
193
+ carrier_code: msg.carrier_code,
194
+ description: typeof msg.description === "string" ? msg.description : ""
195
+ };
164
196
  const addResult = await this.client.addDelivery(request);
165
- this.log.debug(`addDelivery: '${request == null ? void 0 : request.tracking_number}' result=${addResult.success ? "ok" : "fail"}`);
197
+ this.log.debug(`addDelivery: '${request.tracking_number}' result=${addResult.success ? "ok" : "fail"}`);
166
198
  this.sendTo(obj.from, obj.command, addResult, obj.callback);
167
199
  if (addResult.success) {
168
200
  void this.poll({ force: true }).catch(
@@ -317,4 +349,8 @@ if (require.main !== module) {
317
349
  } else {
318
350
  (() => new ParcelappAdapter())();
319
351
  }
352
+ // Annotate the CommonJS export names for ESM import in node:
353
+ 0 && (module.exports = {
354
+ ParcelappAdapter
355
+ });
320
356
  //# sourceMappingURL=main.js.map
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 { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\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 /**\n * v0.4.4: short-lived test-clients spawned from `checkConnection` admin\n * messages. The prod-`this.client` is what `onUnload` cancels, so these\n * need their own registry to be reachable at shutdown. Without this, an\n * admin clicking \"Test Connection\" right before adapter-stop could keep\n * the process alive past js-controller's 4-second kill deadline.\n */\n private testClients = new Set<ParcelClient>();\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 this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n }\n\n private async onReady(): Promise<void> {\n try {\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.log.debug(\n `onReady: starting (pollInterval=${JSON.stringify(this.config.pollInterval)}, autoRemoveDelivered=${this.config.autoRemoveDelivered})`,\n );\n\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 this.log.debug(`system language: '${language}' \u2192 using '${this.systemLang}'`);\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n\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 this.client = new ParcelClient(apiKey.trim(), { debug: (m: string) => this.log.debug(m) });\n this.stateManager = new StateManager(this, language);\n\n await this.cleanupObsoleteStates();\n\n await this.poll();\n\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n this.log.debug(`pollInterval: raw=${JSON.stringify(this.config.pollInterval)} resolved=${interval}min`);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => {\n void this.poll().catch(err => this.log.error(`Scheduled poll failed: ${errText(err)}`));\n }, intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n }\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 // v0.4.4: also abort any short-lived test-client (from checkConnection)\n // whose HTTPS-request might still be inflight at shutdown \u2014 the prod\n // `this.client.cancelAll()` only touches the production-client.\n for (const tc of this.testClients) {\n tc.cancelAll();\n }\n this.testClients.clear();\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 (err) {\n // v0.4.3 (G4): replace silent `// ignore` with a trace so shutdown\n // errors leave a debug breadcrumb. Broker-already-down errors here\n // are expected \u2014 debug-level keeps them out of the user log.\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n }\n callback();\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise.\n this.log.debug(`onMessage: command='${obj?.command}' from='${obj?.from}' has-callback=${!!obj?.callback}`);\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 // v0.4.3 (F2): trace the reject before sendTo.\n this.log.debug(\"checkConnection: apiKey too short\");\n this.sendTo(obj.from, obj.command, { success: false, message: \"API key is too short\" }, obj.callback);\n return;\n }\n // v0.4.3: same debug-logger as the prod client so checkConnection\n // failures get the same HTTPS-layer trace.\n const testClient = new ParcelClient(key, { debug: (m: string) => this.log.debug(m) });\n // v0.4.4: register test-client so onUnload can abort its inflight\n // HTTPS-request \u2014 the adapter's `this.client.cancelAll()` only\n // touches the prod-client, not these short-lived test-clients.\n this.testClients.add(testClient);\n try {\n const result = await testClient.testConnection();\n // v0.4.3 (F3): trace checkConnection result.\n this.log.debug(`checkConnection: result=${result.success ? \"ok\" : \"fail\"} (${result.message})`);\n this.sendTo(obj.from, obj.command, result, obj.callback);\n } finally {\n this.testClients.delete(testClient);\n }\n break;\n }\n case \"addDelivery\": {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\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 // v0.4.3 (F5): trace addDelivery result with the tracking number.\n this.log.debug(`addDelivery: '${request?.tracking_number}' result=${addResult.success ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (addResult.success) {\n // C5: force bypasses the 60s throttle so the freshly added package\n // shows up immediately; the rate-limit cooldown still applies.\n void this.poll({ force: true }).catch(err =>\n this.log.error(`Poll after addDelivery failed: ${errText(err)}`),\n );\n }\n break;\n }\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${obj.command}'`);\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n // v0.4.3 (F7): trace catch so the debug log shows what failed.\n // The sendTo back to the caller is preserved unchanged.\n this.log.debug(`onMessage: '${obj.command}' failed: ${errText(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(options: { force?: boolean } = {}): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n return;\n }\n\n const now = Date.now();\n // v0.4.3 (B1): poll-entry anchor \u2014 visible after the re-entry guard but\n // before the rate-limit/throttle skips. Shows mode + current error state\n // so the debug log gives context for whatever follows.\n const autoRemoveMode = this.config.autoRemoveDelivered !== false;\n this.log.debug(`poll: starting (autoRemove=${autoRemoveMode}, lastErrorCode='${this.lastErrorCode}')`);\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. A forced poll (e.g. from\n // addDelivery) bypasses this so a freshly added package shows up\n // immediately; the rate-limit cooldown above still applies to protect\n // the API.\n if (!options.force && 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 deliveries = await this.client.getDeliveries(autoRemoveMode ? \"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.setStateChangedAsync(\"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 = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset the per-poll collision tracker, then compute every\n // package id in a deterministic sequential pre-pass (stable array order)\n // BEFORE the parallel updates \u2014 collision-suffixing is then deterministic\n // and packageId runs exactly once per delivery instead of twice.\n this.stateManager.resetPollState();\n const pkgIds = visibleDeliveries.map(d => this.stateManager!.packageId(d));\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, index) => {\n const pkgId = pkgIds[index];\n try {\n // v0.4.3 (C1): per-delivery entry. ~10 packages \u00D7 144 polls/day\n // = ~1440 debug lines/day \u2014 acceptable at debug-level. Line stays\n // short (tracking + carrier + status only, no full delivery JSON).\n this.log.debug(\n `updateDelivery: '${delivery.tracking_number}' carrier=${delivery.carrier_code} status=${delivery.status_code}`,\n );\n const carrierName = await this.client!.getCarrierName(delivery.carrier_code);\n await this.stateManager!.updateDelivery(delivery, carrierName, pkgId);\n this.failedDeliveries.delete(delivery.tracking_number);\n return pkgId;\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 // Keep failedDeliveries bounded: drop entries for trackings no longer\n // present, so packages that vanish from the API don't linger forever.\n const seenTracking = new Set(visibleDeliveries.map(d => d.tracking_number));\n for (const tracking of [...this.failedDeliveries]) {\n if (!seenTracking.has(tracking)) {\n this.failedDeliveries.delete(tracking);\n }\n }\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 // C2: setStateChangedAsync avoids redundant `false` writes on sustained\n // failure. The `.catch` keeps poll() from rejecting when the broker is\n // already down, so the fire-and-forget callers never see an unhandled\n // rejection (no global process handler needed).\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker shutting down \u2014 ignore */\n });\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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",
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { coerceClampedInt, errText } from \"./lib/coerce\";\nimport { ParcelClient } from \"./lib/parcel-client\";\nimport { resolveLanguage, 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/**\n * ioBroker adapter for parcel.app package tracking. Exported so the\n * orchestration unit tests can drive its lifecycle/poll handlers directly.\n */\nexport class ParcelappAdapter extends utils.Adapter {\n private client: ParcelClient | null = null;\n private stateManager: StateManager | null = null;\n /**\n * Factories for the HTTP client + state manager \u2014 default to the real\n * constructors. Test seams (fleet pattern): unit tests replace these with\n * fakes to exercise the poll orchestration (throttle/force/rate-limit\n * interplay, error routing, failure dedup) without real network.\n *\n * @param apiKey parcel.app API key\n */\n private makeClient: (apiKey: string) => ParcelClient = apiKey =>\n new ParcelClient(apiKey, { debug: (m: string) => this.log.debug(m) });\n /** @param language Raw system language (resolution happens in StateManager) */\n private makeStateManager: (language: string) => StateManager = language => new StateManager(this, language);\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 /**\n * v0.4.4: short-lived test-clients spawned from `checkConnection` admin\n * messages. The prod-`this.client` is what `onUnload` cancels, so these\n * need their own registry to be reachable at shutdown. Without this, an\n * admin clicking \"Test Connection\" right before adapter-stop could keep\n * the process alive past js-controller's 4-second kill deadline.\n */\n private testClients = new Set<ParcelClient>();\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n }\n\n private async onReady(): Promise<void> {\n try {\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.log.debug(\n `onReady: starting (pollInterval=${JSON.stringify(this.config.pollInterval)}, autoRemoveDelivered=${this.config.autoRemoveDelivered})`,\n );\n\n const sysConfig = await this.getForeignObjectAsync(\"system.config\");\n const language = (sysConfig?.common as { language?: string } | undefined)?.language ?? \"\";\n // v0.7.2: the fallback resolution lives in StateManager (resolveLanguage)\n // \u2014 log the value that is actually used, not a dead local field.\n this.log.debug(`system language: '${language}' \u2192 using '${resolveLanguage(language)}'`);\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n\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 this.client = this.makeClient(apiKey.trim());\n this.stateManager = this.makeStateManager(language);\n\n await this.cleanupObsoleteStates();\n\n await this.poll();\n\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n this.log.debug(`pollInterval: raw=${JSON.stringify(this.config.pollInterval)} resolved=${interval}min`);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => {\n void this.poll().catch(err => this.log.error(`Scheduled poll failed: ${errText(err)}`));\n }, intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n }\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 // v0.4.4: also abort any short-lived test-client (from checkConnection)\n // whose HTTPS-request might still be inflight at shutdown \u2014 the prod\n // `this.client.cancelAll()` only touches the production-client.\n for (const tc of this.testClients) {\n tc.cancelAll();\n }\n this.testClients.clear();\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 (err) {\n // v0.4.3 (G4): replace silent `// ignore` with a trace so shutdown\n // errors leave a debug breadcrumb. Broker-already-down errors here\n // are expected \u2014 debug-level keeps them out of the user log.\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n }\n callback();\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise.\n this.log.debug(`onMessage: command='${obj?.command}' from='${obj?.from}' has-callback=${!!obj?.callback}`);\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 // v0.4.3 (F2): trace the reject before sendTo.\n this.log.debug(\"checkConnection: apiKey too short\");\n this.sendTo(obj.from, obj.command, { success: false, message: \"API key is too short\" }, obj.callback);\n return;\n }\n // v0.4.3: same debug-logger as the prod client so checkConnection\n // failures get the same HTTPS-layer trace (via the makeClient seam).\n const testClient = this.makeClient(key);\n // v0.4.4: register test-client so onUnload can abort its inflight\n // HTTPS-request \u2014 the adapter's `this.client.cancelAll()` only\n // touches the prod-client, not these short-lived test-clients.\n this.testClients.add(testClient);\n try {\n const result = await testClient.testConnection();\n // v0.4.3 (F3): trace checkConnection result.\n this.log.debug(`checkConnection: result=${result.success ? \"ok\" : \"fail\"} (${result.message})`);\n this.sendTo(obj.from, obj.command, result, obj.callback);\n } finally {\n this.testClients.delete(testClient);\n }\n break;\n }\n case \"addDelivery\": {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\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 // v0.7.2: obj.message is `unknown`-shaped \u2014 a script calling\n // sendTo(\"parcelapp\", \"addDelivery\", null) used to surface as an\n // ugly TypeError through the catch instead of a clear validation\n // message. Coerce to a plain object and validate required fields.\n const raw = obj.message;\n const msg =\n raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};\n if (\n typeof msg.tracking_number !== \"string\" ||\n msg.tracking_number.length === 0 ||\n typeof msg.carrier_code !== \"string\" ||\n msg.carrier_code.length === 0\n ) {\n this.log.debug(\"addDelivery: missing tracking_number/carrier_code in message\");\n this.sendTo(\n obj.from,\n obj.command,\n { success: false, error_message: \"tracking_number and carrier_code are required\" },\n obj.callback,\n );\n return;\n }\n const request = {\n tracking_number: msg.tracking_number,\n carrier_code: msg.carrier_code,\n description: typeof msg.description === \"string\" ? msg.description : \"\",\n };\n const addResult = await this.client.addDelivery(request);\n // v0.4.3 (F5): trace addDelivery result with the tracking number.\n this.log.debug(`addDelivery: '${request.tracking_number}' result=${addResult.success ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (addResult.success) {\n // C5: force bypasses the 60s throttle so the freshly added package\n // shows up immediately; the rate-limit cooldown still applies.\n void this.poll({ force: true }).catch(err =>\n this.log.error(`Poll after addDelivery failed: ${errText(err)}`),\n );\n }\n break;\n }\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${obj.command}'`);\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n // v0.4.3 (F7): trace catch so the debug log shows what failed.\n // The sendTo back to the caller is preserved unchanged.\n this.log.debug(`onMessage: '${obj.command}' failed: ${errText(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(options: { force?: boolean } = {}): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n return;\n }\n\n const now = Date.now();\n // v0.4.3 (B1): poll-entry anchor \u2014 visible after the re-entry guard but\n // before the rate-limit/throttle skips. Shows mode + current error state\n // so the debug log gives context for whatever follows.\n const autoRemoveMode = this.config.autoRemoveDelivered !== false;\n this.log.debug(`poll: starting (autoRemove=${autoRemoveMode}, lastErrorCode='${this.lastErrorCode}')`);\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. A forced poll (e.g. from\n // addDelivery) bypasses this so a freshly added package shows up\n // immediately; the rate-limit cooldown above still applies to protect\n // the API.\n if (!options.force && 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 deliveries = await this.client.getDeliveries(autoRemoveMode ? \"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.setStateChangedAsync(\"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 = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset the per-poll collision tracker, then compute every\n // package id in a deterministic sequential pre-pass (stable array order)\n // BEFORE the parallel updates \u2014 collision-suffixing is then deterministic\n // and packageId runs exactly once per delivery instead of twice.\n this.stateManager.resetPollState();\n const pkgIds = visibleDeliveries.map(d => this.stateManager!.packageId(d));\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, index) => {\n const pkgId = pkgIds[index];\n try {\n // v0.4.3 (C1): per-delivery entry. ~10 packages \u00D7 144 polls/day\n // = ~1440 debug lines/day \u2014 acceptable at debug-level. Line stays\n // short (tracking + carrier + status only, no full delivery JSON).\n this.log.debug(\n `updateDelivery: '${delivery.tracking_number}' carrier=${delivery.carrier_code} status=${delivery.status_code}`,\n );\n const carrierName = await this.client!.getCarrierName(delivery.carrier_code);\n await this.stateManager!.updateDelivery(delivery, carrierName, pkgId);\n this.failedDeliveries.delete(delivery.tracking_number);\n return pkgId;\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 // Keep failedDeliveries bounded: drop entries for trackings no longer\n // present, so packages that vanish from the API don't linger forever.\n const seenTracking = new Set(visibleDeliveries.map(d => d.tracking_number));\n for (const tracking of [...this.failedDeliveries]) {\n if (!seenTracking.has(tracking)) {\n this.failedDeliveries.delete(tracking);\n }\n }\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 // C2: setStateChangedAsync avoids redundant `false` writes on sustained\n // failure. The `.catch` keeps poll() from rejecting when the broker is\n // already down, so the fire-and-forget callers never see an unhandled\n // rejection (no global process handler needed).\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker shutting down \u2014 ignore */\n });\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,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "parcelapp",
4
- "version": "0.7.0",
4
+ "version": "0.7.2",
5
5
  "news": {
6
+ "0.7.2": {
7
+ "en": "Much quieter state updates: a package's last-updated timestamp only changes when its tracking data actually changed, and device entries are no longer rewritten on every poll.",
8
+ "de": "Deutlich ruhigere State-Updates: der Zuletzt-aktualisiert-Zeitstempel eines Pakets ändert sich nur noch bei echten Datenänderungen, Geräte-Einträge werden nicht mehr jeden Poll neu geschrieben.",
9
+ "ru": "Обновления стали спокойнее: метка последнего обновления посылки меняется только при реальном изменении данных, записи устройств не перезаписываются при каждом опросе.",
10
+ "pt": "Atualizações muito mais calmas: o carimbo de última atualização de um pacote só muda quando os dados realmente mudam e as entradas de dispositivos não são mais reescritas a cada sondagem.",
11
+ "nl": "Veel rustigere updates: het laatst-bijgewerkt-tijdstempel van een pakket verandert alleen bij echte datawijzigingen en apparaatvermeldingen worden niet meer bij elke poll herschreven.",
12
+ "fr": "Mises à jour plus calmes : l'horodatage de dernière mise à jour d'un colis ne change que si ses données changent réellement, les entrées d'appareils ne sont plus réécrites à chaque cycle.",
13
+ "it": "Aggiornamenti più tranquilli: il timestamp di ultimo aggiornamento di un pacco cambia solo quando i dati cambiano davvero, le voci dei dispositivi non vengono più riscritte a ogni ciclo.",
14
+ "es": "Actualizaciones más tranquilas: la marca de última actualización de un paquete solo cambia cuando sus datos realmente cambian y las entradas ya no se reescriben en cada sondeo.",
15
+ "pl": "Spokojniejsze aktualizacje: znacznik ostatniej aktualizacji paczki zmienia się tylko przy realnej zmianie danych, wpisy urządzeń nie są przepisywane przy każdym odpytaniu.",
16
+ "uk": "Спокійніші оновлення: мітка останнього оновлення посилки змінюється лише за реальної зміни даних, записи пристроїв не перезаписуються щоопитування.",
17
+ "zh-cn": "状态更新更安静:包裹的最后更新时间戳仅在跟踪数据真正变化时才更新,设备条目不再每次轮询都被重写。"
18
+ },
19
+ "0.7.1": {
20
+ "en": "Fixed a timezone edge case in delivery estimates: when the API reports only a calendar date, the estimate could be off by a day in time zones west of UTC — now stable.",
21
+ "de": "Zeitzonen-Randfall bei Lieferschätzungen behoben: Wenn die API nur ein Kalenderdatum liefert, konnte die Schätzung in Zeitzonen westlich von UTC einen Tag danebenliegen — jetzt stabil.",
22
+ "ru": "Исправлен пограничный случай с часовым поясом в оценках доставки: если API возвращает только дату, оценка могла отличаться на день в зонах западнее UTC — теперь стабильно.",
23
+ "pt": "Corrigido um caso-limite de fuso horário nas estimativas de entrega: quando a API fornece apenas uma data, a estimativa podia falhar por um dia em fusos a oeste de UTC — agora estável.",
24
+ "nl": "Tijdzone-randgeval in bezorgschattingen opgelost: als de API alleen een kalenderdatum geeft, kon de schatting in tijdzones ten westen van UTC een dag afwijken — nu stabiel.",
25
+ "fr": "Correction d'un cas limite de fuseau horaire dans les estimations de livraison : quand l'API ne fournit qu'une date, l'estimation pouvait décaler d'un jour à l'ouest d'UTC — désormais stable.",
26
+ "it": "Corretto un caso limite di fuso orario nelle stime di consegna: quando l'API fornisce solo una data, la stima poteva sbagliare di un giorno nei fusi a ovest di UTC — ora stabile.",
27
+ "es": "Corregido un caso límite de zona horaria en las estimaciones de entrega: cuando la API solo da una fecha, la estimación podía fallar por un día en zonas al oeste de UTC — ahora estable.",
28
+ "pl": "Naprawiono przypadek brzegowy strefy czasowej w szacowanym czasie dostawy: gdy API podaje tylko datę, szacunek mógł różnić się o dzień w strefach na zachód od UTC — teraz stabilny.",
29
+ "uk": "Виправлено граничний випадок часового поясу в оцінках доставки: коли API повертає лише дату, оцінка могла відрізнятися на день у зонах на захід від UTC — тепер стабільно.",
30
+ "zh-cn": "修复了配送预计时间的时区边界问题:当 API 仅提供日期时,在 UTC 以西时区的预计可能相差一天,现在已稳定。"
31
+ },
6
32
  "0.7.0": {
7
33
  "en": "Added optional Sentry error reporting: crashes are sent to the developer so issues get fixed faster. Active only with ioBroker diagnostics enabled; anonymous.",
8
34
  "de": "Optionale Fehlermeldung über Sentry hinzugefügt: Abstürze werden an den Entwickler gesendet, damit Probleme schneller behoben werden. Nur aktiv bei eingeschalteter ioBroker-Diagnose; anonym.",
@@ -67,32 +93,6 @@
67
93
  "pl": "Wewnętrzne porządki. Brak zmian dla użytkownika.",
68
94
  "uk": "Внутрішнє очищення. Без змін для користувачів.",
69
95
  "zh-cn": "内部清理。无用户可见的更改。"
70
- },
71
- "0.5.0": {
72
- "en": "User-modified state names are no longer overwritten on adapter restart",
73
- "de": "Vom Benutzer geänderte Datenpunktnamen werden beim Adapter-Neustart nicht mehr überschrieben",
74
- "ru": "Имена состояний, изменённые пользователем, больше не перезаписываются при перезапуске адаптера",
75
- "pt": "Nomes de estados modificados pelo utilizador já não são sobrescritos ao reiniciar o adaptador",
76
- "nl": "Door de gebruiker aangepaste statusnamen worden niet meer overschreven bij herstart van de adapter",
77
- "fr": "Les noms d'états modifiés par l'utilisateur ne sont plus écrasés au redémarrage de l'adaptateur",
78
- "it": "I nomi degli stati modificati dall'utente non vengono più sovrascritti al riavvio dell'adattatore",
79
- "es": "Los nombres de estados modificados por el usuario ya no se sobrescriben al reiniciar el adaptador",
80
- "pl": "Nazwy stanów zmienione przez użytkownika nie są już nadpisywane przy restarcie adaptera",
81
- "uk": "Назви станів, змінені користувачем, більше не перезаписуються при перезапуску адаптера",
82
- "zh-cn": "用户修改的状态名称在适配器重启时不再被覆盖"
83
- },
84
- "0.4.9": {
85
- "en": "Improved error handling and stability.",
86
- "de": "Verbesserte Fehlerbehandlung und Stabilität.",
87
- "ru": "Улучшена обработка ошибок и стабильность.",
88
- "pt": "Melhor tratamento de erros e estabilidade.",
89
- "nl": "Verbeterde foutafhandeling en stabiliteit.",
90
- "fr": "Amélioration de la gestion des erreurs et de la stabilité.",
91
- "it": "Migliorata la gestione degli errori e la stabilità.",
92
- "es": "Mejora del manejo de errores y la estabilidad.",
93
- "pl": "Poprawiona obsługa błędów i stabilność.",
94
- "uk": "Покращено обробку помилок та стабільність.",
95
- "zh-cn": "改进了错误处理和稳定性。"
96
96
  }
97
97
  },
98
98
  "plugins": {
@@ -162,7 +162,7 @@
162
162
  },
163
163
  "dependencies": [
164
164
  {
165
- "js-controller": ">=7.0.7"
165
+ "js-controller": ">=7.1.2"
166
166
  }
167
167
  ],
168
168
  "globalDependencies": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.parcelapp",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "ioBroker adapter for the parcel.app API",
5
5
  "author": {
6
6
  "name": "krobi",
@@ -42,10 +42,10 @@
42
42
  "@tsconfig/node22": "^22.0.5",
43
43
  "@types/iobroker": "npm:@iobroker/types@^7.1.2",
44
44
  "@types/node": "^22.19.17",
45
- "@vitest/coverage-v8": "^4.1.6",
45
+ "@vitest/coverage-v8": "^4.1.8",
46
46
  "rimraf": "^6.1.3",
47
47
  "typescript": "~6.0.3",
48
- "vitest": "^4.1.6"
48
+ "vitest": "^4.1.8"
49
49
  },
50
50
  "files": [
51
51
  "admin",