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 +33 -11
- package/build/lib/parcel-client.js +36 -9
- package/build/lib/parcel-client.js.map +2 -2
- package/build/lib/state-manager.js +56 -18
- package/build/lib/state-manager.js.map +2 -2
- package/io-package.json +39 -38
- package/package.json +9 -3
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
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
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
|
-
-
|
|
91
|
-
-
|
|
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
|
|
56
|
-
const
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;AASvB,MAAM,WAAW;AACjB,MAAM,kBAAkB;
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
146
|
+
description
|
|
121
147
|
),
|
|
122
148
|
this.createAndSet(
|
|
123
149
|
`${devicePath}.trackingNumber`,
|
|
124
150
|
"Tracking Number",
|
|
125
151
|
"string",
|
|
126
152
|
"text",
|
|
127
|
-
|
|
153
|
+
trackingNumber
|
|
128
154
|
),
|
|
129
155
|
this.createAndSet(
|
|
130
156
|
`${devicePath}.extraInfo`,
|
|
131
157
|
"Extra Information",
|
|
132
158
|
"string",
|
|
133
159
|
"text",
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAAmD;AAGnD,MAAM,qBAAqB,oBAAI,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;
|
|
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.
|
|
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
|
|
113
|
-
"de": "ioBroker-Adapter für parcel.app — Pakete
|
|
114
|
-
"ru": "Адаптер ioBroker для parcel.app — отслеживание посылок
|
|
115
|
-
"pt": "Adaptador ioBroker para parcel.app — rastreie pacotes
|
|
116
|
-
"nl": "ioBroker-adapter voor parcel.app — volg pakketten
|
|
117
|
-
"fr": "Adaptateur ioBroker pour parcel.app — suivez vos colis
|
|
118
|
-
"it": "Adattatore ioBroker per parcel.app — traccia pacchi
|
|
119
|
-
"es": "Adaptador ioBroker para parcel.app — rastrea paquetes
|
|
120
|
-
"pl": "Adapter ioBroker dla parcel.app — śledź przesyłki
|
|
121
|
-
"uk": "Адаптер ioBroker для parcel.app — відстеження посилок
|
|
122
|
-
"zh-cn": "parcel.app 的 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.
|
|
4
|
-
"description": "ioBroker adapter for parcel.app — track packages
|
|
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.
|
|
46
|
+
"@types/node": "^25.6.0",
|
|
41
47
|
"rimraf": "^6.1.3",
|
|
42
48
|
"typescript": "~5.9.3"
|
|
43
49
|
},
|