iobroker.parcelapp 0.2.11 → 0.2.13

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
@@ -11,13 +11,13 @@
11
11
 
12
12
  <img src="https://raw.githubusercontent.com/krobipd/ioBroker.parcelapp/main/admin/parcelapp.svg" width="100" />
13
13
 
14
- ioBroker adapter that connects to the [parcel.app](https://parcelapp.net) API and exposes package tracking data from 400+ carriers as ioBroker states — including delivery status, time windows, and tracking events.
14
+ ioBroker adapter that connects to the [parcel.app](https://parcelapp.net) API and exposes package tracking data as ioBroker states — including delivery status, time windows, and tracking events. Supports all carriers that parcel.app tracks.
15
15
 
16
16
  ---
17
17
 
18
18
  ## Features
19
19
 
20
- - **400+ carriers** — DHL, FedEx, UPS, Amazon, Hermes, GLS, DPD, and many more via parcel.app
20
+ - **All parcel.app carriers** — DHL, FedEx, UPS, Amazon, Hermes, GLS, DPD, and everything else parcel.app supports
21
21
  - **Per-package ioBroker states** — carrier, status, tracking number, delivery window, last event, last location
22
22
  - **Summary states** — active count, today count, combined delivery window
23
23
  - **Delivery time estimates** — today, tomorrow, in X days with combined time window
@@ -25,7 +25,7 @@ ioBroker adapter that connects to the [parcel.app](https://parcelapp.net) API an
25
25
  - **Configurable cleanup** — auto-remove delivered packages or keep them until deleted in parcel.app
26
26
  - **Add deliveries** via sendTo message from scripts or other adapters
27
27
  - **Admin UI** with connection test, polling settings, and status language selection
28
- - **Multilingual status labels** (German/English)
28
+ - **Status labels in German or English**
29
29
 
30
30
  ---
31
31
 
@@ -75,6 +75,27 @@ parcelapp.0.
75
75
 
76
76
  ---
77
77
 
78
+ ## Add Deliveries via Script
79
+
80
+ You can add new deliveries from JavaScript/Blockly scripts:
81
+
82
+ ```javascript
83
+ sendTo('parcelapp.0', 'addDelivery', {
84
+ tracking_number: '1234567890',
85
+ carrier_code: 'dhl',
86
+ description: 'My package'
87
+ });
88
+ ```
89
+
90
+ The delivery is added to your parcel.app account and immediately appears in ioBroker after an automatic poll.
91
+
92
+ **Notes:**
93
+ - **POST rate limit: 20 deliveries per day** — failed attempts (e.g. wrong `carrier_code`) also count against this limit.
94
+ - Fresh deliveries usually have no tracking events for **45–90 minutes** after they are added. That's a parcel.app-side delay, not an adapter issue.
95
+ - **Deleting packages is only possible in the parcel.app app/web UI** — the API has no delete endpoint. With `autoRemoveDelivered` enabled, the adapter still drops delivered packages from ioBroker states automatically.
96
+
97
+ ---
98
+
78
99
  ## Troubleshooting
79
100
 
80
101
  ### Connection test fails
@@ -87,13 +108,20 @@ parcelapp.0.
87
108
  - Check if you have active deliveries in the parcel.app
88
109
 
89
110
  ### Rate limit
90
- - The parcel.app API allows 20 requests per hour
91
- - The minimum poll interval is 5 minutes to stay within limits
111
+ - GET (polling): **20 requests per hour** — the minimum poll interval is 5 minutes to stay within this limit
112
+ - POST (adding deliveries): **20 requests per day**, failed attempts count too
92
113
 
93
114
  ---
94
115
 
95
116
  ## Changelog
96
117
 
118
+ ### 0.2.13 (2026-04-19)
119
+ - Latest-repo review compliance: `common.messagebox=true` added because the admin-UI `Check Connection` and `Add Delivery` buttons route through `onMessage`. Runtime behaviour unchanged.
120
+
121
+ ### 0.2.12 (2026-04-18)
122
+ - Harden API-drift guards in `ParcelClient` and `StateManager` (non-boolean `success`, non-array `deliveries`, non-string `error_code`/`error_message`, non-object carrier map, non-string delivery fields, numeric/string `status_code`, numeric-string `timestamp_expected`, malformed `events`)
123
+ - Add 38 regression tests (128 total) covering the new drift paths
124
+
97
125
  ### 0.2.11 (2026-04-12)
98
126
  - Fix: handle response stream errors (prevents unhandled exceptions on connection drop)
99
127
  - Fix: isolate per-delivery poll failures (one broken delivery no longer blocks all others)
@@ -118,12 +146,6 @@ parcelapp.0.
118
146
  ### 0.2.6 (2026-04-05)
119
147
  - Remove redundant scripts, compress documentation
120
148
 
121
- ### 0.2.5 (2026-04-04)
122
- - Fix delivery window timeout on Windows (deterministic time formatting)
123
-
124
- ### 0.2.4 (2026-04-03)
125
- - Modernize dev tooling (esbuild, TypeScript 5.9 pin, testing-action-check v2)
126
-
127
149
  Older entries have been moved to [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
128
150
 
129
151
  ---
@@ -34,6 +34,19 @@ module.exports = __toCommonJS(parcel_client_exports);
34
34
  var https = __toESM(require("node:https"));
35
35
  const API_BASE = "https://api.parcel.app/external";
36
36
  const REQUEST_TIMEOUT = 15e3;
37
+ function isTrueish(v) {
38
+ if (typeof v === "boolean") {
39
+ return v;
40
+ }
41
+ if (typeof v === "number") {
42
+ return v === 1;
43
+ }
44
+ if (typeof v === "string") {
45
+ const s = v.toLowerCase();
46
+ return s === "true" || s === "1";
47
+ }
48
+ return false;
49
+ }
37
50
  class ParcelClient {
38
51
  apiKey;
39
52
  carrierCache = null;
@@ -52,15 +65,20 @@ class ParcelClient {
52
65
  `/deliveries/?filter_mode=${filterMode}`,
53
66
  true
54
67
  );
55
- if (!response.success) {
56
- const code = response.error_code || response.error_message || "UNKNOWN";
57
- const err = new Error(
58
- `API error: ${response.error_message || code}`
59
- );
60
- err.code = code === "INVALID_API_KEY" ? "INVALID_API_KEY" : "API_ERROR";
68
+ if (!response || typeof response !== "object") {
69
+ const err = new Error("API error: malformed response");
70
+ err.code = "API_ERROR";
61
71
  throw err;
62
72
  }
63
- return response.deliveries || [];
73
+ if (!isTrueish(response.success)) {
74
+ const rawCode = typeof response.error_code === "string" ? response.error_code : "";
75
+ const rawMsg = typeof response.error_message === "string" ? response.error_message : "";
76
+ const code = rawCode || rawMsg || "UNKNOWN";
77
+ const err = new Error(`API error: ${rawMsg || code}`);
78
+ err.code = rawCode === "INVALID_API_KEY" ? "INVALID_API_KEY" : "API_ERROR";
79
+ throw err;
80
+ }
81
+ return Array.isArray(response.deliveries) ? response.deliveries : [];
64
82
  }
65
83
  /**
66
84
  * Add a new delivery to parcel.app.
@@ -81,11 +99,16 @@ class ParcelClient {
81
99
  return this.carrierCache;
82
100
  }
83
101
  try {
84
- this.carrierCache = await this.request(
102
+ const raw = await this.request(
85
103
  "GET",
86
104
  "/supported_carriers.json",
87
105
  false
88
106
  );
107
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
108
+ this.carrierCache = raw;
109
+ } else {
110
+ return {};
111
+ }
89
112
  } catch {
90
113
  return {};
91
114
  }
@@ -97,8 +120,12 @@ class ParcelClient {
97
120
  * @param carrierCode The carrier code from API
98
121
  */
99
122
  async getCarrierName(carrierCode) {
123
+ if (typeof carrierCode !== "string" || carrierCode.length === 0) {
124
+ return "UNKNOWN";
125
+ }
100
126
  const carriers = await this.getCarrierNames();
101
- return carriers[carrierCode] || carrierCode.toUpperCase();
127
+ const mapped = carriers[carrierCode];
128
+ return typeof mapped === "string" && mapped.length > 0 ? mapped : carrierCode.toUpperCase();
102
129
  }
103
130
  /** Test if the API key is valid */
104
131
  async testConnection() {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/parcel-client.ts"],
4
- "sourcesContent": ["import * as https from \"node:https\";\nimport type {\n ParcelApiResponse,\n ParcelDelivery,\n AddDeliveryRequest,\n AddDeliveryResponse,\n CarrierMap,\n} from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\nconst REQUEST_TIMEOUT = 15_000;\n\n/** HTTP client for the parcel.app API */\nexport class ParcelClient {\n private apiKey: string;\n private carrierCache: CarrierMap | null = null;\n\n /** @param apiKey The parcel.app API key */\n constructor(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * @param filterMode Filter active or recent deliveries\n */\n async getDeliveries(\n filterMode: \"active\" | \"recent\" = \"active\",\n ): Promise<ParcelDelivery[]> {\n const response = await this.request<ParcelApiResponse>(\n \"GET\",\n `/deliveries/?filter_mode=${filterMode}`,\n true,\n );\n\n if (!response.success) {\n const code = response.error_code || response.error_message || \"UNKNOWN\";\n const err = new Error(\n `API error: ${response.error_message || code}`,\n ) as Error & {\n code: string;\n };\n err.code = code === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\";\n throw err;\n }\n\n return 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(\n delivery: AddDeliveryRequest,\n ): Promise<AddDeliveryResponse> {\n return this.request<AddDeliveryResponse>(\n \"POST\",\n \"/add-delivery/\",\n true,\n delivery,\n );\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 this.carrierCache = await this.request<CarrierMap>(\n \"GET\",\n \"/supported_carriers.json\",\n false,\n );\n } catch {\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\n\n return this.carrierCache;\n }\n\n /**\n * Resolve a carrier code to a display name.\n *\n * @param carrierCode The carrier code from API\n */\n async getCarrierName(carrierCode: string): Promise<string> {\n const carriers = await this.getCarrierNames();\n return carriers[carrierCode] || 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>(\n method: string,\n path: string,\n authenticated: boolean,\n body?: unknown,\n ): Promise<T> {\n return new Promise((resolve, reject) => {\n const url = new URL(`${API_BASE}${path}`);\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 const req = https.request(options, (res) => {\n const chunks: Buffer[] = [];\n\n res.on(\"error\", (err) => reject(err));\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"end\", () => {\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n\n if (\n res.statusCode &&\n (res.statusCode < 200 || res.statusCode >= 300)\n ) {\n if (res.statusCode === 429) {\n const retryAfter = parseInt(res.headers[\"retry-after\"] || \"\", 10);\n const err = new Error(\"Rate limit exceeded\") as Error & {\n code: string;\n retryAfterSeconds: number;\n };\n err.code = \"RATE_LIMITED\";\n // Use Retry-After header or default to 5 minutes\n err.retryAfterSeconds = retryAfter > 0 ? retryAfter : 5 * 60;\n reject(err);\n return;\n }\n const err = new Error(\n `HTTP ${res.statusCode}: ${res.statusMessage}`,\n ) as Error & { code: string };\n err.code =\n res.statusCode === 401 || res.statusCode === 403\n ? \"INVALID_API_KEY\"\n : \"HTTP_ERROR\";\n reject(err);\n return;\n }\n\n try {\n resolve(JSON.parse(raw) as T);\n } catch {\n reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));\n }\n });\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", (err) => reject(err));\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 https from \"node:https\";\nimport type {\n ParcelApiResponse,\n ParcelDelivery,\n AddDeliveryRequest,\n AddDeliveryResponse,\n CarrierMap,\n} from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\nconst REQUEST_TIMEOUT = 15_000;\n\n/**\n * Coerce API-drift boolean responses. parcel.app should return a real boolean\n * for `success`, but the guard accepts common string/number encodings too.\n *\n * @param v Value to interpret as a success flag\n */\nfunction isTrueish(v: unknown): boolean {\n if (typeof v === \"boolean\") {\n return v;\n }\n if (typeof v === \"number\") {\n return v === 1;\n }\n if (typeof v === \"string\") {\n const s = v.toLowerCase();\n return s === \"true\" || s === \"1\";\n }\n return false;\n}\n\n/** HTTP client for the parcel.app API */\nexport class ParcelClient {\n private apiKey: string;\n private carrierCache: CarrierMap | null = null;\n\n /** @param apiKey The parcel.app API key */\n constructor(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * @param filterMode Filter active or recent deliveries\n */\n async getDeliveries(\n filterMode: \"active\" | \"recent\" = \"active\",\n ): Promise<ParcelDelivery[]> {\n const response = await this.request<ParcelApiResponse>(\n \"GET\",\n `/deliveries/?filter_mode=${filterMode}`,\n true,\n );\n\n // API-drift guard: response may be null or a non-object\n if (!response || typeof response !== \"object\") {\n const err = new Error(\"API error: malformed response\") as Error & {\n code: string;\n };\n err.code = \"API_ERROR\";\n throw err;\n }\n\n if (!isTrueish(response.success)) {\n const rawCode =\n typeof response.error_code === \"string\" ? response.error_code : \"\";\n const rawMsg =\n typeof response.error_message === \"string\"\n ? response.error_message\n : \"\";\n const code = rawCode || rawMsg || \"UNKNOWN\";\n const err = new Error(`API error: ${rawMsg || code}`) as Error & {\n code: string;\n };\n err.code =\n rawCode === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\";\n throw err;\n }\n\n // API-drift guard: deliveries must be an array\n return Array.isArray(response.deliveries) ? response.deliveries : [];\n }\n\n /**\n * Add a new delivery to parcel.app.\n *\n * @param delivery The delivery to add\n */\n async addDelivery(\n delivery: AddDeliveryRequest,\n ): Promise<AddDeliveryResponse> {\n return this.request<AddDeliveryResponse>(\n \"POST\",\n \"/add-delivery/\",\n true,\n delivery,\n );\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>(\n \"GET\",\n \"/supported_carriers.json\",\n false,\n );\n // API-drift guard: must be a plain object (not null, array, or primitive)\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n this.carrierCache = raw as CarrierMap;\n } else {\n return {};\n }\n } catch {\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\n\n return this.carrierCache;\n }\n\n /**\n * Resolve a carrier code to a display name.\n *\n * @param carrierCode The carrier code from API\n */\n async getCarrierName(carrierCode: unknown): Promise<string> {\n // API-drift guard: non-string codes fall back to \"UNKNOWN\"\n if (typeof carrierCode !== \"string\" || carrierCode.length === 0) {\n return \"UNKNOWN\";\n }\n const carriers = await this.getCarrierNames();\n const mapped = carriers[carrierCode];\n return typeof mapped === \"string\" && mapped.length > 0\n ? mapped\n : 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>(\n method: string,\n path: string,\n authenticated: boolean,\n body?: unknown,\n ): Promise<T> {\n return new Promise((resolve, reject) => {\n const url = new URL(`${API_BASE}${path}`);\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 const req = https.request(options, (res) => {\n const chunks: Buffer[] = [];\n\n res.on(\"error\", (err) => reject(err));\n res.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n res.on(\"end\", () => {\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n\n if (\n res.statusCode &&\n (res.statusCode < 200 || res.statusCode >= 300)\n ) {\n if (res.statusCode === 429) {\n const retryAfter = parseInt(res.headers[\"retry-after\"] || \"\", 10);\n const err = new Error(\"Rate limit exceeded\") as Error & {\n code: string;\n retryAfterSeconds: number;\n };\n err.code = \"RATE_LIMITED\";\n // Use Retry-After header or default to 5 minutes\n err.retryAfterSeconds = retryAfter > 0 ? retryAfter : 5 * 60;\n reject(err);\n return;\n }\n const err = new Error(\n `HTTP ${res.statusCode}: ${res.statusMessage}`,\n ) as Error & { code: string };\n err.code =\n res.statusCode === 401 || res.statusCode === 403\n ? \"INVALID_API_KEY\"\n : \"HTTP_ERROR\";\n reject(err);\n return;\n }\n\n try {\n resolve(JSON.parse(raw) as T);\n } catch {\n reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));\n }\n });\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", (err) => reject(err));\n\n if (body) {\n req.write(JSON.stringify(body));\n }\n req.end();\n });\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["err"]
7
7
  }
@@ -23,6 +23,16 @@ __export(state_manager_exports, {
23
23
  module.exports = __toCommonJS(state_manager_exports);
24
24
  var import_types = require("./types");
25
25
  const TRACKABLE_STATUSES = /* @__PURE__ */ new Set([2, 4, 8]);
26
+ function coerceNumber(v) {
27
+ if (typeof v === "number" && Number.isFinite(v)) {
28
+ return v;
29
+ }
30
+ if (typeof v === "string" && v.length > 0) {
31
+ const n = parseFloat(v);
32
+ return Number.isFinite(n) ? n : null;
33
+ }
34
+ return null;
35
+ }
26
36
  const ESTIMATE_LABELS = {
27
37
  de: {
28
38
  overdue: "\xFCberf\xE4llig",
@@ -45,19 +55,32 @@ class StateManager {
45
55
  }
46
56
  /**
47
57
  * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).
58
+ * API-drift guard: returns "unknown" for non-string input.
48
59
  *
49
- * @param name Raw string to sanitize
60
+ * @param name Raw value to sanitize (any type)
50
61
  */
51
62
  sanitize(name) {
63
+ if (typeof name !== "string") {
64
+ return "unknown";
65
+ }
52
66
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 50) || "unknown";
53
67
  }
54
68
  /**
55
- * Parse the status code from a delivery (API returns it as string).
69
+ * Parse the status code from a delivery. API documents `status_code` as
70
+ * a numeric string, but we accept numbers too and fall back to 0 for drift.
56
71
  *
57
72
  * @param delivery The delivery to parse
58
73
  */
59
74
  parseStatus(delivery) {
60
- return parseInt(delivery.status_code, 10) || 0;
75
+ const raw = delivery.status_code;
76
+ if (typeof raw === "number" && Number.isFinite(raw)) {
77
+ return Math.trunc(raw);
78
+ }
79
+ if (typeof raw === "string") {
80
+ const n = parseInt(raw, 10);
81
+ return Number.isFinite(n) ? n : 0;
82
+ }
83
+ return 0;
61
84
  }
62
85
  /**
63
86
  * Build a unique package ID from a delivery.
@@ -66,7 +89,7 @@ class StateManager {
66
89
  */
67
90
  packageId(delivery) {
68
91
  let id = this.sanitize(delivery.tracking_number);
69
- if (delivery.extra_information) {
92
+ if (typeof delivery.extra_information === "string" && delivery.extra_information.length > 0) {
70
93
  id += `_${this.sanitize(delivery.extra_information)}`;
71
94
  }
72
95
  return id;
@@ -80,10 +103,13 @@ class StateManager {
80
103
  async updateDelivery(delivery, carrierName) {
81
104
  const pkgId = this.packageId(delivery);
82
105
  const devicePath = `deliveries.${pkgId}`;
106
+ const description = typeof delivery.description === "string" ? delivery.description : "";
107
+ const trackingNumber = typeof delivery.tracking_number === "string" ? delivery.tracking_number : "";
108
+ const extraInfo = typeof delivery.extra_information === "string" ? delivery.extra_information : "";
83
109
  await this.adapter.extendObjectAsync(devicePath, {
84
110
  type: "device",
85
111
  common: {
86
- name: delivery.description || `Package ${delivery.tracking_number}`
112
+ name: description || `Package ${trackingNumber || pkgId}`
87
113
  },
88
114
  native: {}
89
115
  });
@@ -117,21 +143,21 @@ class StateManager {
117
143
  "Description",
118
144
  "string",
119
145
  "text",
120
- delivery.description || ""
146
+ description
121
147
  ),
122
148
  this.createAndSet(
123
149
  `${devicePath}.trackingNumber`,
124
150
  "Tracking Number",
125
151
  "string",
126
152
  "text",
127
- delivery.tracking_number
153
+ trackingNumber
128
154
  ),
129
155
  this.createAndSet(
130
156
  `${devicePath}.extraInfo`,
131
157
  "Extra Information",
132
158
  "string",
133
159
  "text",
134
- delivery.extra_information || ""
160
+ extraInfo
135
161
  ),
136
162
  this.createAndSet(
137
163
  `${devicePath}.deliveryWindow`,
@@ -240,10 +266,14 @@ class StateManager {
240
266
  return "";
241
267
  }
242
268
  const formatTime = (timestamp) => {
243
- if (!timestamp) {
269
+ const ts = coerceNumber(timestamp);
270
+ if (ts === null || ts <= 0) {
271
+ return null;
272
+ }
273
+ const d = new Date(ts * 1e3);
274
+ if (Number.isNaN(d.getTime())) {
244
275
  return null;
245
276
  }
246
- const d = new Date(timestamp * 1e3);
247
277
  return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
248
278
  };
249
279
  const start = formatTime(delivery.timestamp_expected);
@@ -264,9 +294,10 @@ class StateManager {
264
294
  return "";
265
295
  }
266
296
  let expectedDate = null;
267
- if (delivery.timestamp_expected) {
268
- expectedDate = new Date(delivery.timestamp_expected * 1e3);
269
- } else if (delivery.date_expected) {
297
+ const ts = coerceNumber(delivery.timestamp_expected);
298
+ if (ts !== null && ts > 0) {
299
+ expectedDate = new Date(ts * 1e3);
300
+ } else if (typeof delivery.date_expected === "string" && delivery.date_expected.length > 0) {
270
301
  expectedDate = new Date(delivery.date_expected);
271
302
  }
272
303
  if (!expectedDate || isNaN(expectedDate.getTime())) {
@@ -305,15 +336,18 @@ class StateManager {
305
336
  * @param delivery The delivery data
306
337
  */
307
338
  formatLastEvent(delivery) {
308
- if (!delivery.events || delivery.events.length === 0) {
339
+ if (!Array.isArray(delivery.events) || delivery.events.length === 0) {
309
340
  return "";
310
341
  }
311
342
  const latest = delivery.events[0];
343
+ if (!latest || typeof latest !== "object") {
344
+ return "";
345
+ }
312
346
  const parts = [];
313
- if (latest.event) {
347
+ if (typeof latest.event === "string" && latest.event.length > 0) {
314
348
  parts.push(latest.event);
315
349
  }
316
- if (latest.date) {
350
+ if (typeof latest.date === "string" && latest.date.length > 0) {
317
351
  parts.push(latest.date);
318
352
  }
319
353
  return parts.join(" - ");
@@ -324,10 +358,14 @@ class StateManager {
324
358
  * @param delivery The delivery data
325
359
  */
326
360
  extractLastLocation(delivery) {
327
- if (!delivery.events || delivery.events.length === 0) {
361
+ if (!Array.isArray(delivery.events) || delivery.events.length === 0) {
362
+ return "";
363
+ }
364
+ const latest = delivery.events[0];
365
+ if (!latest || typeof latest !== "object") {
328
366
  return "";
329
367
  }
330
- return delivery.events[0].location || "";
368
+ return typeof latest.location === "string" ? latest.location : "";
331
369
  }
332
370
  /**
333
371
  * Calculate combined delivery window for today's packages.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/state-manager.ts"],
4
- "sourcesContent": ["import type { AdapterInstance } from \"@iobroker/adapter-core\";\nimport type { ParcelDelivery } from \"./types\";\nimport { STATUS_LABELS_DE, STATUS_LABELS_EN } from \"./types\";\n\n/** Status codes that have expected delivery date/time */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\nconst ESTIMATE_LABELS: Record<string, Record<string, string>> = {\n de: {\n overdue: \"\u00FCberf\u00E4llig\",\n today: \"heute\",\n tomorrow: \"morgen\",\n days: \"in %d Tagen\",\n },\n en: {\n overdue: \"overdue\",\n today: \"today\",\n tomorrow: \"tomorrow\",\n days: \"in %d days\",\n },\n};\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\n\n /** @param adapter The ioBroker adapter instance */\n constructor(adapter: AdapterInstance) {\n this.adapter = adapter;\n }\n\n /**\n * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).\n *\n * @param name Raw string to sanitize\n */\n sanitize(name: string): string {\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 returns it as string).\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n return parseInt(delivery.status_code, 10) || 0;\n }\n\n /**\n * Build a unique package ID from a delivery.\n *\n * @param delivery The delivery to build an ID for\n */\n packageId(delivery: ParcelDelivery): string {\n let id = this.sanitize(delivery.tracking_number);\n if (delivery.extra_information) {\n id += `_${this.sanitize(delivery.extra_information)}`;\n }\n return id;\n }\n\n /**\n * Update or create all states for a delivery.\n *\n * @param delivery The delivery data from API\n * @param carrierName Resolved carrier display name\n */\n async updateDelivery(\n delivery: ParcelDelivery,\n carrierName: string,\n ): Promise<void> {\n const pkgId = this.packageId(delivery);\n const devicePath = `deliveries.${pkgId}`;\n\n await this.adapter.extendObjectAsync(devicePath, {\n type: \"device\",\n common: {\n name: delivery.description || `Package ${delivery.tracking_number}`,\n },\n native: {},\n });\n\n const statusCode = this.parseStatus(delivery);\n const lang = this.adapter.config.language || \"de\";\n const labels = lang === \"de\" ? STATUS_LABELS_DE : STATUS_LABELS_EN;\n\n await Promise.all([\n this.createAndSet(\n `${devicePath}.carrier`,\n \"Carrier\",\n \"string\",\n \"text\",\n carrierName,\n ),\n this.createAndSet(\n `${devicePath}.status`,\n \"Status\",\n \"string\",\n \"text\",\n labels[statusCode] || `Unknown (${statusCode})`,\n ),\n this.createAndSet(\n `${devicePath}.statusCode`,\n \"Status Code\",\n \"number\",\n \"value\",\n statusCode,\n ),\n this.createAndSet(\n `${devicePath}.description`,\n \"Description\",\n \"string\",\n \"text\",\n delivery.description || \"\",\n ),\n this.createAndSet(\n `${devicePath}.trackingNumber`,\n \"Tracking Number\",\n \"string\",\n \"text\",\n delivery.tracking_number,\n ),\n this.createAndSet(\n `${devicePath}.extraInfo`,\n \"Extra Information\",\n \"string\",\n \"text\",\n delivery.extra_information || \"\",\n ),\n this.createAndSet(\n `${devicePath}.deliveryWindow`,\n \"Delivery Window\",\n \"string\",\n \"text\",\n this.calculateDeliveryWindow(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.deliveryEstimate`,\n \"Delivery Estimate\",\n \"string\",\n \"text\",\n this.calculateDeliveryEstimate(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.lastEvent`,\n \"Last Event\",\n \"string\",\n \"text\",\n this.formatLastEvent(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastLocation`,\n \"Last Location\",\n \"string\",\n \"text\",\n this.extractLastLocation(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastUpdated`,\n \"Last Updated\",\n \"string\",\n \"date\",\n new Date().toISOString(),\n ),\n ]);\n }\n\n /**\n * Update summary states. Expects already-filtered active deliveries.\n *\n * @param activeDeliveries Only active (non-delivered) deliveries\n */\n async updateSummary(activeDeliveries: ParcelDelivery[]): Promise<void> {\n await this.adapter.extendObjectAsync(\"summary\", {\n type: \"channel\",\n common: { name: \"Summary\" },\n native: {},\n });\n\n const todayDeliveries = activeDeliveries.filter((d) => {\n const statusCode = this.parseStatus(d);\n const estimate = this.calculateDeliveryEstimate(d, statusCode);\n return estimate === \"heute\" || estimate === \"today\";\n });\n\n await Promise.all([\n this.createAndSet(\n \"summary.activeCount\",\n \"Active Deliveries\",\n \"number\",\n \"value\",\n activeDeliveries.length,\n ),\n this.createAndSet(\n \"summary.todayCount\",\n \"Deliveries Today\",\n \"number\",\n \"value\",\n todayDeliveries.length,\n ),\n this.createAndSet(\n \"summary.deliveryWindow\",\n \"Combined Delivery Window\",\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer active.\n *\n * @param activeIds List of currently active package IDs\n */\n async cleanupDeliveries(activeIds: string[]): Promise<void> {\n const activeSet = new Set(activeIds.map((id) => `deliveries.${id}`));\n\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.\\u9999`,\n });\n\n for (const row of objects.rows) {\n const relativeId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (relativeId.startsWith(\"deliveries.\") && !activeSet.has(relativeId)) {\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n }\n }\n }\n\n /**\n * Calculate delivery time window \u2014 only from Unix timestamps.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(\n delivery: ParcelDelivery,\n statusCode: number,\n ): string {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return \"\";\n }\n\n const formatTime = (timestamp?: number): string | null => {\n if (!timestamp) {\n return null;\n }\n const d = new Date(timestamp * 1000);\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n };\n\n const start = formatTime(delivery.timestamp_expected);\n const end = formatTime(delivery.timestamp_expected_end);\n\n if (!start) {\n return \"\";\n }\n return end ? `${start} - ${end}` : start;\n }\n\n /**\n * Calculate human-readable delivery estimate.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryEstimate(\n delivery: ParcelDelivery,\n statusCode: number,\n ): string {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return \"\";\n }\n\n let expectedDate: Date | null = null;\n if (delivery.timestamp_expected) {\n expectedDate = new Date(delivery.timestamp_expected * 1000);\n } else if (delivery.date_expected) {\n expectedDate = new Date(delivery.date_expected);\n }\n\n if (!expectedDate || isNaN(expectedDate.getTime())) {\n return \"\";\n }\n\n const now = new Date();\n const todayStart = new Date(\n now.getFullYear(),\n now.getMonth(),\n now.getDate(),\n );\n const expectedStart = new Date(\n expectedDate.getFullYear(),\n expectedDate.getMonth(),\n expectedDate.getDate(),\n );\n const diffDays = Math.round(\n (expectedStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24),\n );\n\n const lang = this.adapter.config.language || \"de\";\n const l = ESTIMATE_LABELS[lang] || ESTIMATE_LABELS.en;\n\n if (diffDays < 0) {\n return l.overdue;\n }\n if (diffDays === 0) {\n return l.today;\n }\n if (diffDays === 1) {\n return l.tomorrow;\n }\n return l.days.replace(\"%d\", String(diffDays));\n }\n\n /**\n * Format the latest tracking event.\n *\n * @param delivery The delivery data\n */\n private formatLastEvent(delivery: ParcelDelivery): string {\n if (!delivery.events || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n const parts: string[] = [];\n if (latest.event) {\n parts.push(latest.event);\n }\n if (latest.date) {\n parts.push(latest.date);\n }\n return parts.join(\" - \");\n }\n\n /**\n * Extract location from latest event.\n *\n * @param delivery The delivery data\n */\n private extractLastLocation(delivery: ParcelDelivery): string {\n if (!delivery.events || delivery.events.length === 0) {\n return \"\";\n }\n return delivery.events[0].location || \"\";\n }\n\n /**\n * Calculate combined delivery window for today's packages.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const windows = todayDeliveries\n .map((d) => this.calculateDeliveryWindow(d, this.parseStatus(d)))\n .filter((w) => w.length > 0);\n\n if (windows.length === 0) {\n return \"\";\n }\n if (windows.length === 1) {\n return windows[0];\n }\n\n const times: {\n /** Window start */ start: string;\n /** Window end */ end: string;\n }[] = [];\n for (const w of windows) {\n const match = w.match(/(\\d{2}:\\d{2})(?:\\s*-\\s*(\\d{2}:\\d{2}))?/);\n if (match) {\n times.push({ start: match[1], end: match[2] || match[1] });\n }\n }\n\n if (times.length === 0) {\n return \"\";\n }\n\n times.sort((a, b) => a.start.localeCompare(b.start));\n return `${times[0].start} - ${times[times.length - 1].end}`;\n }\n\n /**\n * Create/extend a read-only state and set its value.\n *\n * @param id State ID relative to adapter namespace\n * @param name Display name\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: string,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ): Promise<void> {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: { name, type, role, read: true, write: false },\n native: {},\n });\n await this.adapter.setStateAsync(id, { val, ack: true });\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AdapterInstance } from \"@iobroker/adapter-core\";\nimport type { ParcelDelivery } from \"./types\";\nimport { STATUS_LABELS_DE, STATUS_LABELS_EN } from \"./types\";\n\n/** Status codes that have expected delivery date/time */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Coerce a value to a finite number. Accepts numbers and numeric strings.\n * Returns null for anything else \u2014 used to guard against API drift.\n *\n * @param v Value to coerce\n */\nfunction coerceNumber(v: unknown): number | null {\n if (typeof v === \"number\" && Number.isFinite(v)) {\n return v;\n }\n if (typeof v === \"string\" && v.length > 0) {\n const n = parseFloat(v);\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n\nconst ESTIMATE_LABELS: Record<string, Record<string, string>> = {\n de: {\n overdue: \"\u00FCberf\u00E4llig\",\n today: \"heute\",\n tomorrow: \"morgen\",\n days: \"in %d Tagen\",\n },\n en: {\n overdue: \"overdue\",\n today: \"today\",\n tomorrow: \"tomorrow\",\n days: \"in %d days\",\n },\n};\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\n\n /** @param adapter The ioBroker adapter instance */\n constructor(adapter: AdapterInstance) {\n this.adapter = adapter;\n }\n\n /**\n * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).\n * API-drift guard: returns \"unknown\" for non-string input.\n *\n * @param name Raw value to sanitize (any type)\n */\n sanitize(name: unknown): string {\n if (typeof name !== \"string\") {\n return \"unknown\";\n }\n return (\n name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 50) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. API documents `status_code` as\n * a numeric string, but we accept numbers too and fall back to 0 for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code as unknown;\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return Math.trunc(raw);\n }\n if (typeof raw === \"string\") {\n const n = parseInt(raw, 10);\n return Number.isFinite(n) ? n : 0;\n }\n return 0;\n }\n\n /**\n * Build a unique package ID from a delivery.\n *\n * @param delivery The delivery to build an ID for\n */\n packageId(delivery: ParcelDelivery): string {\n let id = this.sanitize(delivery.tracking_number);\n // API-drift guard: only string values extend the id\n if (\n typeof delivery.extra_information === \"string\" &&\n delivery.extra_information.length > 0\n ) {\n id += `_${this.sanitize(delivery.extra_information)}`;\n }\n return id;\n }\n\n /**\n * Update or create all states for a delivery.\n *\n * @param delivery The delivery data from API\n * @param carrierName Resolved carrier display name\n */\n async updateDelivery(\n delivery: ParcelDelivery,\n carrierName: string,\n ): Promise<void> {\n const pkgId = this.packageId(delivery);\n const devicePath = `deliveries.${pkgId}`;\n\n const description =\n typeof delivery.description === \"string\" ? delivery.description : \"\";\n const trackingNumber =\n typeof delivery.tracking_number === \"string\"\n ? delivery.tracking_number\n : \"\";\n const extraInfo =\n typeof delivery.extra_information === \"string\"\n ? delivery.extra_information\n : \"\";\n\n await this.adapter.extendObjectAsync(devicePath, {\n type: \"device\",\n common: {\n name: description || `Package ${trackingNumber || pkgId}`,\n },\n native: {},\n });\n\n const statusCode = this.parseStatus(delivery);\n const lang = this.adapter.config.language || \"de\";\n const labels = lang === \"de\" ? STATUS_LABELS_DE : STATUS_LABELS_EN;\n\n await Promise.all([\n this.createAndSet(\n `${devicePath}.carrier`,\n \"Carrier\",\n \"string\",\n \"text\",\n carrierName,\n ),\n this.createAndSet(\n `${devicePath}.status`,\n \"Status\",\n \"string\",\n \"text\",\n labels[statusCode] || `Unknown (${statusCode})`,\n ),\n this.createAndSet(\n `${devicePath}.statusCode`,\n \"Status Code\",\n \"number\",\n \"value\",\n statusCode,\n ),\n this.createAndSet(\n `${devicePath}.description`,\n \"Description\",\n \"string\",\n \"text\",\n description,\n ),\n this.createAndSet(\n `${devicePath}.trackingNumber`,\n \"Tracking Number\",\n \"string\",\n \"text\",\n trackingNumber,\n ),\n this.createAndSet(\n `${devicePath}.extraInfo`,\n \"Extra Information\",\n \"string\",\n \"text\",\n extraInfo,\n ),\n this.createAndSet(\n `${devicePath}.deliveryWindow`,\n \"Delivery Window\",\n \"string\",\n \"text\",\n this.calculateDeliveryWindow(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.deliveryEstimate`,\n \"Delivery Estimate\",\n \"string\",\n \"text\",\n this.calculateDeliveryEstimate(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.lastEvent`,\n \"Last Event\",\n \"string\",\n \"text\",\n this.formatLastEvent(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastLocation`,\n \"Last Location\",\n \"string\",\n \"text\",\n this.extractLastLocation(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastUpdated`,\n \"Last Updated\",\n \"string\",\n \"date\",\n new Date().toISOString(),\n ),\n ]);\n }\n\n /**\n * Update summary states. Expects already-filtered active deliveries.\n *\n * @param activeDeliveries Only active (non-delivered) deliveries\n */\n async updateSummary(activeDeliveries: ParcelDelivery[]): Promise<void> {\n await this.adapter.extendObjectAsync(\"summary\", {\n type: \"channel\",\n common: { name: \"Summary\" },\n native: {},\n });\n\n const todayDeliveries = activeDeliveries.filter((d) => {\n const statusCode = this.parseStatus(d);\n const estimate = this.calculateDeliveryEstimate(d, statusCode);\n return estimate === \"heute\" || estimate === \"today\";\n });\n\n await Promise.all([\n this.createAndSet(\n \"summary.activeCount\",\n \"Active Deliveries\",\n \"number\",\n \"value\",\n activeDeliveries.length,\n ),\n this.createAndSet(\n \"summary.todayCount\",\n \"Deliveries Today\",\n \"number\",\n \"value\",\n todayDeliveries.length,\n ),\n this.createAndSet(\n \"summary.deliveryWindow\",\n \"Combined Delivery Window\",\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer active.\n *\n * @param activeIds List of currently active package IDs\n */\n async cleanupDeliveries(activeIds: string[]): Promise<void> {\n const activeSet = new Set(activeIds.map((id) => `deliveries.${id}`));\n\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.\\u9999`,\n });\n\n for (const row of objects.rows) {\n const relativeId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (relativeId.startsWith(\"deliveries.\") && !activeSet.has(relativeId)) {\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n }\n }\n }\n\n /**\n * Calculate delivery time window \u2014 only from Unix timestamps.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(\n delivery: ParcelDelivery,\n statusCode: number,\n ): string {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return \"\";\n }\n\n const formatTime = (timestamp: unknown): string | null => {\n const ts = coerceNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const d = new Date(ts * 1000);\n if (Number.isNaN(d.getTime())) {\n return null;\n }\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n };\n\n const start = formatTime(delivery.timestamp_expected);\n const end = formatTime(delivery.timestamp_expected_end);\n\n if (!start) {\n return \"\";\n }\n return end ? `${start} - ${end}` : start;\n }\n\n /**\n * Calculate human-readable delivery estimate.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryEstimate(\n delivery: ParcelDelivery,\n statusCode: number,\n ): string {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return \"\";\n }\n\n let expectedDate: Date | null = null;\n const ts = coerceNumber(delivery.timestamp_expected);\n if (ts !== null && ts > 0) {\n expectedDate = new Date(ts * 1000);\n } else if (\n typeof delivery.date_expected === \"string\" &&\n delivery.date_expected.length > 0\n ) {\n expectedDate = new Date(delivery.date_expected);\n }\n\n if (!expectedDate || isNaN(expectedDate.getTime())) {\n return \"\";\n }\n\n const now = new Date();\n const todayStart = new Date(\n now.getFullYear(),\n now.getMonth(),\n now.getDate(),\n );\n const expectedStart = new Date(\n expectedDate.getFullYear(),\n expectedDate.getMonth(),\n expectedDate.getDate(),\n );\n const diffDays = Math.round(\n (expectedStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24),\n );\n\n const lang = this.adapter.config.language || \"de\";\n const l = ESTIMATE_LABELS[lang] || ESTIMATE_LABELS.en;\n\n if (diffDays < 0) {\n return l.overdue;\n }\n if (diffDays === 0) {\n return l.today;\n }\n if (diffDays === 1) {\n return l.tomorrow;\n }\n return l.days.replace(\"%d\", String(diffDays));\n }\n\n /**\n * Format the latest tracking event.\n *\n * @param delivery The delivery data\n */\n private formatLastEvent(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n const parts: string[] = [];\n if (typeof latest.event === \"string\" && latest.event.length > 0) {\n parts.push(latest.event);\n }\n if (typeof latest.date === \"string\" && latest.date.length > 0) {\n parts.push(latest.date);\n }\n return parts.join(\" - \");\n }\n\n /**\n * Extract location from latest event.\n *\n * @param delivery The delivery data\n */\n private extractLastLocation(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Calculate combined delivery window for today's packages.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const windows = todayDeliveries\n .map((d) => this.calculateDeliveryWindow(d, this.parseStatus(d)))\n .filter((w) => w.length > 0);\n\n if (windows.length === 0) {\n return \"\";\n }\n if (windows.length === 1) {\n return windows[0];\n }\n\n const times: {\n /** Window start */ start: string;\n /** Window end */ end: string;\n }[] = [];\n for (const w of windows) {\n const match = w.match(/(\\d{2}:\\d{2})(?:\\s*-\\s*(\\d{2}:\\d{2}))?/);\n if (match) {\n times.push({ start: match[1], end: match[2] || match[1] });\n }\n }\n\n if (times.length === 0) {\n return \"\";\n }\n\n times.sort((a, b) => a.start.localeCompare(b.start));\n return `${times[0].start} - ${times[times.length - 1].end}`;\n }\n\n /**\n * Create/extend a read-only state and set its value.\n *\n * @param id State ID relative to adapter namespace\n * @param name Display name\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: string,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ): Promise<void> {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: { name, type, role, read: true, write: false },\n native: {},\n });\n await this.adapter.setStateAsync(id, { val, ack: true });\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "parcelapp",
4
- "version": "0.2.11",
4
+ "version": "0.2.13",
5
5
  "news": {
6
+ "0.2.13": {
7
+ "en": "Latest-repo review compliance: set common.messagebox=true because the onMessage handler is used for admin-UI Check Connection and Add Delivery.",
8
+ "de": "Latest-Repo-Review-Compliance: common.messagebox=true, weil der onMessage-Handler für Admin-UI Check Connection + Add Delivery genutzt wird.",
9
+ "ru": "Соответствие ioBroker Latest: messagebox=true для admin-UI.",
10
+ "pt": "Conformidade ioBroker Latest: messagebox=true para admin-UI.",
11
+ "nl": "Compliance ioBroker Latest: messagebox=true voor admin-UI.",
12
+ "fr": "Conformité ioBroker Latest : messagebox=true pour admin-UI.",
13
+ "it": "Conformità ioBroker Latest: messagebox=true per admin-UI.",
14
+ "es": "Cumplimiento ioBroker Latest: messagebox=true para admin-UI.",
15
+ "pl": "Zgodność ioBroker Latest: messagebox=true dla admin-UI.",
16
+ "uk": "Відповідність ioBroker Latest: messagebox=true для admin-UI.",
17
+ "zh-cn": "符合 ioBroker Latest 仓库规范: messagebox=true, 供 admin-UI 使用。"
18
+ },
19
+ "0.2.12": {
20
+ "en": "Harden API-drift guards in parcel-client and state-manager, add 38 regression tests",
21
+ "de": "API-Drift-Härtung in parcel-client und state-manager, 38 zusätzliche Regressions-Tests",
22
+ "ru": "Усиление защиты от дрейфа API в parcel-client и state-manager, добавлено 38 регрессионных тестов",
23
+ "pt": "Reforço de proteções contra API-drift em parcel-client e state-manager, 38 testes de regressão adicionais",
24
+ "nl": "API-drift guards versterkt in parcel-client en state-manager, 38 extra regressietests",
25
+ "fr": "Renforcement des protections contre la dérive d'API dans parcel-client et state-manager, 38 tests de régression supplémentaires",
26
+ "it": "Rafforzate protezioni contro API-drift in parcel-client e state-manager, 38 test di regressione aggiuntivi",
27
+ "es": "Reforzadas las protecciones contra API-drift en parcel-client y state-manager, 38 pruebas de regresión adicionales",
28
+ "pl": "Wzmocniono zabezpieczenia przed dryfem API w parcel-client i state-manager, 38 dodatkowych testów regresji",
29
+ "uk": "Посилено захист від дрейфу API у parcel-client та state-manager, 38 додаткових регресійних тестів",
30
+ "zh-cn": "加固parcel-client和state-manager中的API漂移防护,新增38个回归测试"
31
+ },
6
32
  "0.2.11": {
7
33
  "en": "Fix response stream error handling, isolate per-delivery poll failures, harden onMessage and onUnload",
8
34
  "de": "Response-Stream-Fehlerbehandlung gefixt, per-Delivery Poll-Fehler isoliert, onMessage und onUnload gehärtet",
@@ -67,32 +93,6 @@
67
93
  "pl": "Spójne etykiety interfejsu we wszystkich adapterach",
68
94
  "uk": "Узгоджені мітки інтерфейсу у всіх адаптерах",
69
95
  "zh-cn": "所有适配器中统一的界面标签"
70
- },
71
- "0.2.6": {
72
- "en": "Remove redundant scripts, compress documentation",
73
- "de": "Redundante Scripts entfernt, Dokumentation komprimiert",
74
- "ru": "Удалены избыточные скрипты, сжата документация",
75
- "pt": "Scripts redundantes removidos, documentacao comprimida",
76
- "nl": "Overbodige scripts verwijderd, documentatie gecomprimeerd",
77
- "fr": "Scripts redondants supprimes, documentation comprimee",
78
- "it": "Script ridondanti rimossi, documentazione compressa",
79
- "es": "Scripts redundantes eliminados, documentacion comprimida",
80
- "pl": "Usunieto nadmiarowe skrypty, skompresowano dokumentacje",
81
- "uk": "Видалено зайвi скрипти, стиснуто документацiю",
82
- "zh-cn": "移除冗余脚本,压缩文档"
83
- },
84
- "0.2.5": {
85
- "en": "Fix delivery window timeout on Windows (replace toLocaleTimeString with manual formatting)",
86
- "de": "Fix Zeitfenster-Timeout auf Windows (toLocaleTimeString durch manuelle Formatierung ersetzt)",
87
- "ru": "Исправлен тайм-аут окна доставки в Windows",
88
- "pt": "Correção de timeout de janela de entrega no Windows",
89
- "nl": "Fix leveringsvenster timeout op Windows",
90
- "fr": "Correction du timeout de la fenêtre de livraison sous Windows",
91
- "it": "Correzione timeout finestra di consegna su Windows",
92
- "es": "Corrección del timeout de la ventana de entrega en Windows",
93
- "pl": "Naprawiono timeout okna dostawy w systemie Windows",
94
- "uk": "Виправлено тайм-аут вікна доставки в Windows",
95
- "zh-cn": "修复Windows上的交付窗口超时问题"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -109,17 +109,17 @@
109
109
  "zh-cn": "包裹追踪"
110
110
  },
111
111
  "desc": {
112
- "en": "ioBroker adapter for parcel.app — track packages from 400+ carriers as ioBroker states",
113
- "de": "ioBroker-Adapter für parcel.app — Pakete von 400+ Versandunternehmen als ioBroker-States verfolgen",
114
- "ru": "Адаптер ioBroker для parcel.app — отслеживание посылок от 400+ перевозчиков как состояния ioBroker",
115
- "pt": "Adaptador ioBroker para parcel.app — rastreie pacotes de mais de 400 transportadoras como estados ioBroker",
116
- "nl": "ioBroker-adapter voor parcel.app — volg pakketten van 400+ vervoerders als ioBroker-states",
117
- "fr": "Adaptateur ioBroker pour parcel.app — suivez vos colis de plus de 400 transporteurs comme états ioBroker",
118
- "it": "Adattatore ioBroker per parcel.app — traccia pacchi da più di 400 corrieri come stati ioBroker",
119
- "es": "Adaptador ioBroker para parcel.app — rastrea paquetes de más de 400 transportistas como estados ioBroker",
120
- "pl": "Adapter ioBroker dla parcel.app — śledź przesyłki od 400+ przewoźników jako stany ioBroker",
121
- "uk": "Адаптер ioBroker для parcel.app — відстеження посилок від 400+ перевізників як стани ioBroker",
122
- "zh-cn": "parcel.app 的 ioBroker 适配器 — 400+ 快递公司的包裹追踪数据作为 ioBroker 状态"
112
+ "en": "ioBroker adapter for parcel.app — track packages as ioBroker states",
113
+ "de": "ioBroker-Adapter für parcel.app — Pakete als ioBroker-States verfolgen",
114
+ "ru": "Адаптер ioBroker для parcel.app — отслеживание посылок как состояния ioBroker",
115
+ "pt": "Adaptador ioBroker para parcel.app — rastreie pacotes como estados ioBroker",
116
+ "nl": "ioBroker-adapter voor parcel.app — volg pakketten als ioBroker-states",
117
+ "fr": "Adaptateur ioBroker pour parcel.app — suivez vos colis comme états ioBroker",
118
+ "it": "Adattatore ioBroker per parcel.app — traccia pacchi come stati ioBroker",
119
+ "es": "Adaptador ioBroker para parcel.app — rastrea paquetes como estados ioBroker",
120
+ "pl": "Adapter ioBroker dla parcel.app — śledź przesyłki jako stany ioBroker",
121
+ "uk": "Адаптер ioBroker для parcel.app — відстеження посилок як стани ioBroker",
122
+ "zh-cn": "parcel.app 的 ioBroker 适配器 — 将包裹追踪数据作为 ioBroker 状态"
123
123
  },
124
124
  "authors": [
125
125
  "krobi <krobi@power-dreams.com>"
@@ -144,6 +144,7 @@
144
144
  "tier": 3,
145
145
  "loglevel": "info",
146
146
  "mode": "daemon",
147
+ "messagebox": true,
147
148
  "type": "infrastructure",
148
149
  "compact": true,
149
150
  "connectionType": "cloud",
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "iobroker.parcelapp",
3
- "version": "0.2.11",
4
- "description": "ioBroker adapter for parcel.app — track packages from 400+ carriers",
3
+ "version": "0.2.13",
4
+ "description": "ioBroker adapter for parcel.app — track packages as ioBroker states",
5
5
  "author": {
6
6
  "name": "krobi",
7
7
  "email": "krobi@power-dreams.com"
8
8
  },
9
+ "contributors": [
10
+ {
11
+ "name": "Claude (Anthropic)",
12
+ "role": "Coding assistance"
13
+ }
14
+ ],
9
15
  "homepage": "https://github.com/krobipd/ioBroker.parcelapp",
10
16
  "license": "MIT",
11
17
  "keywords": [
@@ -37,7 +43,7 @@
37
43
  "@iobroker/testing": "^5.2.2",
38
44
  "@tsconfig/node20": "^20.1.9",
39
45
  "@types/iobroker": "npm:@iobroker/types@^7.1.0",
40
- "@types/node": "^25.5.0",
46
+ "@types/node": "^25.6.0",
41
47
  "rimraf": "^6.1.3",
42
48
  "typescript": "~5.9.3"
43
49
  },