iobroker.parcelapp 0.2.10 → 0.2.12
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 +38 -11
- package/build/lib/parcel-client.js +37 -9
- package/build/lib/parcel-client.js.map +2 -2
- package/build/lib/state-manager.js +66 -23
- package/build/lib/state-manager.js.map +2 -2
- package/build/main.js +82 -69
- package/build/main.js.map +2 -2
- package/io-package.json +38 -38
- package/package.json +8 -2
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,25 @@ 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.12 (2026-04-18)
|
|
119
|
+
- 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`)
|
|
120
|
+
- Add 38 regression tests (128 total) covering the new drift paths
|
|
121
|
+
|
|
122
|
+
### 0.2.11 (2026-04-12)
|
|
123
|
+
- Fix: handle response stream errors (prevents unhandled exceptions on connection drop)
|
|
124
|
+
- Fix: isolate per-delivery poll failures (one broken delivery no longer blocks all others)
|
|
125
|
+
- Fix: harden onMessage with try/catch and callback guard
|
|
126
|
+
- Fix: onUnload try/catch prevents adapter hang on shutdown
|
|
127
|
+
- DRY: parseStatus helper eliminates repeated parseInt calls
|
|
128
|
+
- Simplify obsolete state cleanup, use setObjectNotExistsAsync for states
|
|
129
|
+
|
|
97
130
|
### 0.2.10 (2026-04-12)
|
|
98
131
|
- Fix test timezone bug, remove unused devDependencies, add `no-floating-promises` lint rule
|
|
99
132
|
- Remove redundant `actions/checkout` from CI workflow
|
|
@@ -110,12 +143,6 @@ parcelapp.0.
|
|
|
110
143
|
### 0.2.6 (2026-04-05)
|
|
111
144
|
- Remove redundant scripts, compress documentation
|
|
112
145
|
|
|
113
|
-
### 0.2.5 (2026-04-04)
|
|
114
|
-
- Fix delivery window timeout on Windows (deterministic time formatting)
|
|
115
|
-
|
|
116
|
-
### 0.2.4 (2026-04-03)
|
|
117
|
-
- Modernize dev tooling (esbuild, TypeScript 5.9 pin, testing-action-check v2)
|
|
118
|
-
|
|
119
146
|
Older entries have been moved to [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
120
147
|
|
|
121
148
|
---
|
|
@@ -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() {
|
|
@@ -141,6 +168,7 @@ class ParcelClient {
|
|
|
141
168
|
};
|
|
142
169
|
const req = https.request(options, (res) => {
|
|
143
170
|
const chunks = [];
|
|
171
|
+
res.on("error", (err) => reject(err));
|
|
144
172
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
145
173
|
res.on("end", () => {
|
|
146
174
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
@@ -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,12 +55,33 @@ 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
|
}
|
|
68
|
+
/**
|
|
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.
|
|
71
|
+
*
|
|
72
|
+
* @param delivery The delivery to parse
|
|
73
|
+
*/
|
|
74
|
+
parseStatus(delivery) {
|
|
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;
|
|
84
|
+
}
|
|
54
85
|
/**
|
|
55
86
|
* Build a unique package ID from a delivery.
|
|
56
87
|
*
|
|
@@ -58,7 +89,7 @@ class StateManager {
|
|
|
58
89
|
*/
|
|
59
90
|
packageId(delivery) {
|
|
60
91
|
let id = this.sanitize(delivery.tracking_number);
|
|
61
|
-
if (delivery.extra_information) {
|
|
92
|
+
if (typeof delivery.extra_information === "string" && delivery.extra_information.length > 0) {
|
|
62
93
|
id += `_${this.sanitize(delivery.extra_information)}`;
|
|
63
94
|
}
|
|
64
95
|
return id;
|
|
@@ -72,14 +103,17 @@ class StateManager {
|
|
|
72
103
|
async updateDelivery(delivery, carrierName) {
|
|
73
104
|
const pkgId = this.packageId(delivery);
|
|
74
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 : "";
|
|
75
109
|
await this.adapter.extendObjectAsync(devicePath, {
|
|
76
110
|
type: "device",
|
|
77
111
|
common: {
|
|
78
|
-
name:
|
|
112
|
+
name: description || `Package ${trackingNumber || pkgId}`
|
|
79
113
|
},
|
|
80
114
|
native: {}
|
|
81
115
|
});
|
|
82
|
-
const statusCode =
|
|
116
|
+
const statusCode = this.parseStatus(delivery);
|
|
83
117
|
const lang = this.adapter.config.language || "de";
|
|
84
118
|
const labels = lang === "de" ? import_types.STATUS_LABELS_DE : import_types.STATUS_LABELS_EN;
|
|
85
119
|
await Promise.all([
|
|
@@ -109,21 +143,21 @@ class StateManager {
|
|
|
109
143
|
"Description",
|
|
110
144
|
"string",
|
|
111
145
|
"text",
|
|
112
|
-
|
|
146
|
+
description
|
|
113
147
|
),
|
|
114
148
|
this.createAndSet(
|
|
115
149
|
`${devicePath}.trackingNumber`,
|
|
116
150
|
"Tracking Number",
|
|
117
151
|
"string",
|
|
118
152
|
"text",
|
|
119
|
-
|
|
153
|
+
trackingNumber
|
|
120
154
|
),
|
|
121
155
|
this.createAndSet(
|
|
122
156
|
`${devicePath}.extraInfo`,
|
|
123
157
|
"Extra Information",
|
|
124
158
|
"string",
|
|
125
159
|
"text",
|
|
126
|
-
|
|
160
|
+
extraInfo
|
|
127
161
|
),
|
|
128
162
|
this.createAndSet(
|
|
129
163
|
`${devicePath}.deliveryWindow`,
|
|
@@ -174,7 +208,7 @@ class StateManager {
|
|
|
174
208
|
native: {}
|
|
175
209
|
});
|
|
176
210
|
const todayDeliveries = activeDeliveries.filter((d) => {
|
|
177
|
-
const statusCode =
|
|
211
|
+
const statusCode = this.parseStatus(d);
|
|
178
212
|
const estimate = this.calculateDeliveryEstimate(d, statusCode);
|
|
179
213
|
return estimate === "heute" || estimate === "today";
|
|
180
214
|
});
|
|
@@ -232,10 +266,14 @@ class StateManager {
|
|
|
232
266
|
return "";
|
|
233
267
|
}
|
|
234
268
|
const formatTime = (timestamp) => {
|
|
235
|
-
|
|
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())) {
|
|
236
275
|
return null;
|
|
237
276
|
}
|
|
238
|
-
const d = new Date(timestamp * 1e3);
|
|
239
277
|
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
|
240
278
|
};
|
|
241
279
|
const start = formatTime(delivery.timestamp_expected);
|
|
@@ -256,9 +294,10 @@ class StateManager {
|
|
|
256
294
|
return "";
|
|
257
295
|
}
|
|
258
296
|
let expectedDate = null;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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) {
|
|
262
301
|
expectedDate = new Date(delivery.date_expected);
|
|
263
302
|
}
|
|
264
303
|
if (!expectedDate || isNaN(expectedDate.getTime())) {
|
|
@@ -297,15 +336,18 @@ class StateManager {
|
|
|
297
336
|
* @param delivery The delivery data
|
|
298
337
|
*/
|
|
299
338
|
formatLastEvent(delivery) {
|
|
300
|
-
if (!delivery.events || delivery.events.length === 0) {
|
|
339
|
+
if (!Array.isArray(delivery.events) || delivery.events.length === 0) {
|
|
301
340
|
return "";
|
|
302
341
|
}
|
|
303
342
|
const latest = delivery.events[0];
|
|
343
|
+
if (!latest || typeof latest !== "object") {
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
304
346
|
const parts = [];
|
|
305
|
-
if (latest.event) {
|
|
347
|
+
if (typeof latest.event === "string" && latest.event.length > 0) {
|
|
306
348
|
parts.push(latest.event);
|
|
307
349
|
}
|
|
308
|
-
if (latest.date) {
|
|
350
|
+
if (typeof latest.date === "string" && latest.date.length > 0) {
|
|
309
351
|
parts.push(latest.date);
|
|
310
352
|
}
|
|
311
353
|
return parts.join(" - ");
|
|
@@ -316,10 +358,14 @@ class StateManager {
|
|
|
316
358
|
* @param delivery The delivery data
|
|
317
359
|
*/
|
|
318
360
|
extractLastLocation(delivery) {
|
|
319
|
-
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") {
|
|
320
366
|
return "";
|
|
321
367
|
}
|
|
322
|
-
return
|
|
368
|
+
return typeof latest.location === "string" ? latest.location : "";
|
|
323
369
|
}
|
|
324
370
|
/**
|
|
325
371
|
* Calculate combined delivery window for today's packages.
|
|
@@ -327,10 +373,7 @@ class StateManager {
|
|
|
327
373
|
* @param todayDeliveries Deliveries expected today
|
|
328
374
|
*/
|
|
329
375
|
calculateCombinedWindow(todayDeliveries) {
|
|
330
|
-
const windows = todayDeliveries.map((d) =>
|
|
331
|
-
const sc = parseInt(d.status_code, 10) || 0;
|
|
332
|
-
return this.calculateDeliveryWindow(d, sc);
|
|
333
|
-
}).filter((w) => w.length > 0);
|
|
376
|
+
const windows = todayDeliveries.map((d) => this.calculateDeliveryWindow(d, this.parseStatus(d))).filter((w) => w.length > 0);
|
|
334
377
|
if (windows.length === 0) {
|
|
335
378
|
return "";
|
|
336
379
|
}
|
|
@@ -360,7 +403,7 @@ class StateManager {
|
|
|
360
403
|
* @param val Value to set
|
|
361
404
|
*/
|
|
362
405
|
async createAndSet(id, name, type, role, val) {
|
|
363
|
-
await this.adapter.
|
|
406
|
+
await this.adapter.setObjectNotExistsAsync(id, {
|
|
364
407
|
type: "state",
|
|
365
408
|
common: { name, type, role, read: true, write: false },
|
|
366
409
|
native: {}
|
|
@@ -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 * 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 = parseInt(delivery.status_code, 10) || 0;\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 = parseInt(d.status_code, 10) || 0;\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) => {\n const sc = parseInt(d.status_code, 10) || 0;\n return this.calculateDeliveryWindow(d, sc);\n })\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.extendObjectAsync(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/build/main.js
CHANGED
|
@@ -24,7 +24,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
var utils = __toESM(require("@iobroker/adapter-core"));
|
|
25
25
|
var import_parcel_client = require("./lib/parcel-client");
|
|
26
26
|
var import_state_manager = require("./lib/state-manager");
|
|
27
|
-
var import_types = require("./lib/types");
|
|
28
27
|
const MIN_POLL_INTERVAL = 5;
|
|
29
28
|
const MAX_POLL_INTERVAL = 60;
|
|
30
29
|
const DEFAULT_POLL_INTERVAL = 10;
|
|
@@ -37,6 +36,7 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
37
36
|
lastPollTime = 0;
|
|
38
37
|
rateLimitedUntil = 0;
|
|
39
38
|
lastErrorCode = "";
|
|
39
|
+
failedDeliveries = /* @__PURE__ */ new Set();
|
|
40
40
|
/** @param options Adapter options */
|
|
41
41
|
constructor(options = {}) {
|
|
42
42
|
super({
|
|
@@ -75,67 +75,74 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
onUnload(callback) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
try {
|
|
79
|
+
if (this.pollTimer) {
|
|
80
|
+
this.clearInterval(this.pollTimer);
|
|
81
|
+
this.pollTimer = void 0;
|
|
82
|
+
}
|
|
83
|
+
void this.setState("info.connection", { val: false, ack: true });
|
|
84
|
+
} catch {
|
|
81
85
|
}
|
|
82
|
-
void this.setState("info.connection", { val: false, ack: true });
|
|
83
86
|
callback();
|
|
84
87
|
}
|
|
85
88
|
async onMessage(obj) {
|
|
86
89
|
var _a;
|
|
87
|
-
if (!(obj == null ? void 0 : obj.command)) {
|
|
90
|
+
if (!(obj == null ? void 0 : obj.command) || !obj.callback) {
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
success: false,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
try {
|
|
94
|
+
switch (obj.command) {
|
|
95
|
+
case "checkConnection": {
|
|
96
|
+
const msg = obj.message;
|
|
97
|
+
const key = ((_a = msg == null ? void 0 : msg.apiKey) == null ? void 0 : _a.trim()) || "";
|
|
98
|
+
if (!key || key.length < 10) {
|
|
99
|
+
this.sendTo(
|
|
100
|
+
obj.from,
|
|
101
|
+
obj.command,
|
|
102
|
+
{ success: false, message: "API key is too short" },
|
|
103
|
+
obj.callback
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const testClient = new import_parcel_client.ParcelClient(key);
|
|
108
|
+
const result = await testClient.testConnection();
|
|
109
|
+
this.sendTo(obj.from, obj.command, result, obj.callback);
|
|
110
|
+
break;
|
|
105
111
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
case "addDelivery": {
|
|
113
|
+
if (!this.client) {
|
|
114
|
+
this.sendTo(
|
|
115
|
+
obj.from,
|
|
116
|
+
obj.command,
|
|
117
|
+
{ success: false, error_message: "Adapter not initialized" },
|
|
118
|
+
obj.callback
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const request = obj.message;
|
|
123
|
+
const addResult = await this.client.addDelivery(request);
|
|
124
|
+
this.sendTo(obj.from, obj.command, addResult, obj.callback);
|
|
125
|
+
if (addResult.success) {
|
|
126
|
+
void this.poll();
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
113
131
|
this.sendTo(
|
|
114
132
|
obj.from,
|
|
115
133
|
obj.command,
|
|
116
|
-
{
|
|
117
|
-
success: false,
|
|
118
|
-
error_message: "Adapter not initialized"
|
|
119
|
-
},
|
|
134
|
+
{ error: "Unknown command" },
|
|
120
135
|
obj.callback
|
|
121
136
|
);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
const request = obj.message;
|
|
125
|
-
const addResult = await this.client.addDelivery(request);
|
|
126
|
-
this.sendTo(obj.from, obj.command, addResult, obj.callback);
|
|
127
|
-
if (addResult.success) {
|
|
128
|
-
void this.poll();
|
|
129
|
-
}
|
|
130
|
-
break;
|
|
131
137
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
+
this.sendTo(
|
|
141
|
+
obj.from,
|
|
142
|
+
obj.command,
|
|
143
|
+
{ success: false, error_message: msg },
|
|
144
|
+
obj.callback
|
|
145
|
+
);
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
async cleanupObsoleteStates() {
|
|
@@ -148,17 +155,6 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
148
155
|
if (obj) {
|
|
149
156
|
await this.delObjectAsync(stateId);
|
|
150
157
|
this.log.debug(`Removed obsolete state: ${stateId}`);
|
|
151
|
-
const parentId = stateId.includes(".") ? stateId.substring(0, stateId.lastIndexOf(".")) : null;
|
|
152
|
-
if (parentId) {
|
|
153
|
-
const children = await this.getObjectListAsync({
|
|
154
|
-
startkey: `${this.namespace}.${parentId}.`,
|
|
155
|
-
endkey: `${this.namespace}.${parentId}.\u9999`
|
|
156
|
-
});
|
|
157
|
-
if ((children == null ? void 0 : children.rows.length) === 0) {
|
|
158
|
-
await this.delObjectAsync(parentId);
|
|
159
|
-
this.log.debug(`Removed empty parent: ${parentId}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
158
|
}
|
|
163
159
|
}
|
|
164
160
|
}
|
|
@@ -174,7 +170,7 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
174
170
|
if (error.code === "INVALID_API_KEY") {
|
|
175
171
|
return "INVALID_API_KEY";
|
|
176
172
|
}
|
|
177
|
-
if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED" || error.code === "ECONNRESET" || error.code === "ENETUNREACH" || error.code === "EAI_AGAIN") {
|
|
173
|
+
if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED" || error.code === "ECONNRESET" || error.code === "ENETUNREACH" || error.code === "EHOSTUNREACH" || error.code === "EAI_AGAIN") {
|
|
178
174
|
return "NETWORK";
|
|
179
175
|
}
|
|
180
176
|
if (error.message.includes("timeout") || error.code === "ETIMEDOUT") {
|
|
@@ -211,20 +207,37 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
211
207
|
this.lastErrorCode = "";
|
|
212
208
|
}
|
|
213
209
|
await this.setStateAsync("info.connection", { val: true, ack: true });
|
|
214
|
-
const
|
|
210
|
+
const activeDeliveries = deliveries.filter(
|
|
211
|
+
(d) => this.stateManager.parseStatus(d) !== 0
|
|
212
|
+
);
|
|
213
|
+
const visibleDeliveries = autoRemove ? activeDeliveries : deliveries;
|
|
215
214
|
const activeIds = [];
|
|
216
215
|
for (const delivery of visibleDeliveries) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
try {
|
|
217
|
+
const carrierName = await this.client.getCarrierName(
|
|
218
|
+
delivery.carrier_code
|
|
219
|
+
);
|
|
220
|
+
await this.stateManager.updateDelivery(delivery, carrierName);
|
|
221
|
+
activeIds.push(this.stateManager.packageId(delivery));
|
|
222
|
+
this.failedDeliveries.delete(delivery.tracking_number);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
225
|
+
if (this.failedDeliveries.has(delivery.tracking_number)) {
|
|
226
|
+
this.log.debug(
|
|
227
|
+
`Failed to update "${delivery.tracking_number}": ${msg}`
|
|
228
|
+
);
|
|
229
|
+
} else {
|
|
230
|
+
this.log.warn(
|
|
231
|
+
`Failed to update "${delivery.tracking_number}": ${msg}`
|
|
232
|
+
);
|
|
233
|
+
this.failedDeliveries.add(delivery.tracking_number);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
222
236
|
}
|
|
223
237
|
await this.stateManager.cleanupDeliveries(activeIds);
|
|
224
|
-
|
|
225
|
-
await this.stateManager.updateSummary(summaryDeliveries);
|
|
238
|
+
await this.stateManager.updateSummary(activeDeliveries);
|
|
226
239
|
this.log.debug(
|
|
227
|
-
`Polled ${visibleDeliveries.length} deliveries (${
|
|
240
|
+
`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`
|
|
228
241
|
);
|
|
229
242
|
} catch (err) {
|
|
230
243
|
const error = err;
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { ParcelClient } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport \"./lib/types\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\nconst MIN_POLL_GAP_MS = 60_000; // Minimum 60s between polls\n\n/** ioBroker adapter for parcel.app package tracking */\nclass ParcelappAdapter extends utils.Adapter {\n private client: ParcelClient | null = null;\n private stateManager: StateManager | null = null;\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n }\n\n private async onReady(): Promise<void> {\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n\n // Validate config\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < 10) {\n this.log.error(\n \"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\",\n );\n return;\n }\n\n // Initialize\n this.client = new ParcelClient(apiKey.trim());\n this.stateManager = new StateManager(this);\n\n // Cleanup obsolete states\n await this.cleanupObsoleteStates();\n\n // Initial poll\n await this.poll();\n\n // Set up recurring poll\n const interval = Math.max(\n MIN_POLL_INTERVAL,\n Math.min(\n MAX_POLL_INTERVAL,\n this.config.pollInterval ?? DEFAULT_POLL_INTERVAL,\n ),\n );\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => void this.poll(), intervalMs);\n\n this.log.info(\n `Parcel tracking started \u2014 polling every ${interval} minutes`,\n );\n }\n\n private onUnload(callback: () => void): void {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n void this.setState(\"info.connection\", { val: false, ack: true });\n callback();\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n if (!obj?.command) {\n return;\n }\n\n switch (obj.command) {\n case \"checkConnection\": {\n const msg = obj.message as { apiKey?: string };\n const key = msg?.apiKey?.trim() || \"\";\n if (!key || key.length < 10) {\n this.sendTo(\n obj.from,\n obj.command,\n {\n success: false,\n message: \"API key is too short\",\n },\n obj.callback,\n );\n return;\n }\n const testClient = new ParcelClient(key);\n const result = await testClient.testConnection();\n this.sendTo(obj.from, obj.command, result, obj.callback);\n break;\n }\n case \"addDelivery\": {\n if (!this.client) {\n this.sendTo(\n obj.from,\n obj.command,\n {\n success: false,\n error_message: \"Adapter not initialized\",\n },\n obj.callback,\n );\n return;\n }\n const request = obj.message as {\n tracking_number: string;\n carrier_code: string;\n description: string;\n };\n const addResult = await this.client.addDelivery(request);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (addResult.success) {\n // Trigger immediate poll to pick up the new delivery\n void this.poll();\n }\n break;\n }\n default:\n this.sendTo(\n obj.from,\n obj.command,\n { error: \"Unknown command\" },\n obj.callback,\n );\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n const obsoleteStates = [\n \"summary.json\", // removed in 0.2.0\n ];\n for (const stateId of obsoleteStates) {\n const obj = await this.getObjectAsync(stateId);\n if (obj) {\n await this.delObjectAsync(stateId);\n this.log.debug(`Removed obsolete state: ${stateId}`);\n\n // Clean up empty parent channel/folder\n const parentId = stateId.includes(\".\")\n ? stateId.substring(0, stateId.lastIndexOf(\".\"))\n : null;\n if (parentId) {\n const children = await this.getObjectListAsync({\n startkey: `${this.namespace}.${parentId}.`,\n endkey: `${this.namespace}.${parentId}.\\u9999`,\n });\n if (children?.rows.length === 0) {\n await this.delObjectAsync(parentId);\n this.log.debug(`Removed empty parent: ${parentId}`);\n }\n }\n }\n }\n }\n\n /**\n * Classify an error for deduplication and log-level decisions.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code === \"RATE_LIMITED\") {\n return \"RATE_LIMITED\";\n }\n if (error.code === \"INVALID_API_KEY\") {\n return \"INVALID_API_KEY\";\n }\n // Network errors: DNS, connection refused, no internet\n if (\n error.code === \"ENOTFOUND\" ||\n error.code === \"ECONNREFUSED\" ||\n error.code === \"ECONNRESET\" ||\n error.code === \"ENETUNREACH\" ||\n error.code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (error.message.includes(\"timeout\") || error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code || \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n return;\n }\n\n const now = Date.now();\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(\n `Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`,\n );\n return;\n }\n\n // Throttle: minimum gap between polls\n if (now - this.lastPollTime < MIN_POLL_GAP_MS) {\n this.log.debug(\"Skipping poll \u2014 too soon after last poll\");\n return;\n }\n\n this.isPolling = true;\n this.lastPollTime = now;\n try {\n // When keeping delivered packages, use \"recent\" to get them from API\n const autoRemove = this.config.autoRemoveDelivered !== false;\n const deliveries = await this.client.getDeliveries(\n autoRemove ? \"active\" : \"recent\",\n );\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n await this.setStateAsync(\"info.connection\", { val: true, ack: true });\n\n // Filter deliveries based on auto-remove setting\n const visibleDeliveries = autoRemove\n ? deliveries.filter((d) => parseInt(d.status_code, 10) !== 0)\n : deliveries;\n\n // Update each delivery\n const activeIds: string[] = [];\n for (const delivery of visibleDeliveries) {\n const carrierName = await this.client.getCarrierName(\n delivery.carrier_code,\n );\n await this.stateManager.updateDelivery(delivery, carrierName);\n activeIds.push(this.stateManager.packageId(delivery));\n }\n\n // Cleanup stale deliveries\n await this.stateManager.cleanupDeliveries(activeIds);\n\n // Update summary\n const summaryDeliveries = autoRemove\n ? visibleDeliveries\n : deliveries.filter((d) => parseInt(d.status_code, 10) !== 0);\n await this.stateManager.updateSummary(summaryDeliveries);\n\n this.log.debug(\n `Polled ${visibleDeliveries.length} deliveries (${summaryDeliveries.length} active)`,\n );\n } catch (err) {\n const error = err as Error & {\n code?: string;\n retryAfterSeconds?: number;\n };\n\n // Classify the error\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n if (error.code === \"RATE_LIMITED\") {\n const cooldownSec = error.retryAfterSeconds || 5 * 60;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n this.log.warn(\n `Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`,\n );\n } else if (error.code === \"INVALID_API_KEY\") {\n // Always log \u2014 user must fix config\n this.log.error(\n \"Invalid API key \u2014 please check your parcel.app API key\",\n );\n } else if (isRepeat) {\n // Same error as last time \u2014 don't spam the log\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else if (errorCode === \"NETWORK\") {\n this.log.warn(`Cannot reach parcel.app API \u2014 will keep retrying`);\n } else if (errorCode === \"TIMEOUT\") {\n this.log.warn(`API request timeout \u2014 will retry next cycle`);\n } else {\n this.log.error(`Poll failed: ${error.message}`);\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n } finally {\n this.isPolling = false;\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) =>\n new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA,YAAuB;AACvB,2BAA6B;AAC7B,2BAA6B;
|
|
4
|
+
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { ParcelClient } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\nconst MIN_POLL_GAP_MS = 60_000; // Minimum 60s between polls\n\n/** ioBroker adapter for parcel.app package tracking */\nclass ParcelappAdapter extends utils.Adapter {\n private client: ParcelClient | null = null;\n private stateManager: StateManager | null = null;\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n private failedDeliveries = new Set<string>();\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n }\n\n private async onReady(): Promise<void> {\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n\n // Validate config\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < 10) {\n this.log.error(\n \"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\",\n );\n return;\n }\n\n // Initialize\n this.client = new ParcelClient(apiKey.trim());\n this.stateManager = new StateManager(this);\n\n // Cleanup obsolete states\n await this.cleanupObsoleteStates();\n\n // Initial poll\n await this.poll();\n\n // Set up recurring poll\n const interval = Math.max(\n MIN_POLL_INTERVAL,\n Math.min(\n MAX_POLL_INTERVAL,\n this.config.pollInterval ?? DEFAULT_POLL_INTERVAL,\n ),\n );\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => void this.poll(), intervalMs);\n\n this.log.info(\n `Parcel tracking started \u2014 polling every ${interval} minutes`,\n );\n }\n\n private onUnload(callback: () => void): void {\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n void this.setState(\"info.connection\", { val: false, ack: true });\n } catch {\n // ignore\n }\n callback();\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n try {\n switch (obj.command) {\n case \"checkConnection\": {\n const msg = obj.message as { apiKey?: string };\n const key = msg?.apiKey?.trim() || \"\";\n if (!key || key.length < 10) {\n this.sendTo(\n obj.from,\n obj.command,\n { success: false, message: \"API key is too short\" },\n obj.callback,\n );\n return;\n }\n const testClient = new ParcelClient(key);\n const result = await testClient.testConnection();\n this.sendTo(obj.from, obj.command, result, obj.callback);\n break;\n }\n case \"addDelivery\": {\n if (!this.client) {\n this.sendTo(\n obj.from,\n obj.command,\n { success: false, error_message: \"Adapter not initialized\" },\n obj.callback,\n );\n return;\n }\n const request = obj.message as {\n tracking_number: string;\n carrier_code: string;\n description: string;\n };\n const addResult = await this.client.addDelivery(request);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (addResult.success) {\n void this.poll();\n }\n break;\n }\n default:\n this.sendTo(\n obj.from,\n obj.command,\n { error: \"Unknown command\" },\n obj.callback,\n );\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n this.sendTo(\n obj.from,\n obj.command,\n { success: false, error_message: msg },\n obj.callback,\n );\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n const obsoleteStates = [\n \"summary.json\", // removed in 0.2.0\n ];\n for (const stateId of obsoleteStates) {\n const obj = await this.getObjectAsync(stateId);\n if (obj) {\n await this.delObjectAsync(stateId);\n this.log.debug(`Removed obsolete state: ${stateId}`);\n }\n }\n }\n\n /**\n * Classify an error for deduplication and log-level decisions.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code === \"RATE_LIMITED\") {\n return \"RATE_LIMITED\";\n }\n if (error.code === \"INVALID_API_KEY\") {\n return \"INVALID_API_KEY\";\n }\n // Network errors: DNS, connection refused, no internet\n if (\n error.code === \"ENOTFOUND\" ||\n error.code === \"ECONNREFUSED\" ||\n error.code === \"ECONNRESET\" ||\n error.code === \"ENETUNREACH\" ||\n error.code === \"EHOSTUNREACH\" ||\n error.code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (error.message.includes(\"timeout\") || error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code || \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n return;\n }\n\n const now = Date.now();\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(\n `Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`,\n );\n return;\n }\n\n // Throttle: minimum gap between polls\n if (now - this.lastPollTime < MIN_POLL_GAP_MS) {\n this.log.debug(\"Skipping poll \u2014 too soon after last poll\");\n return;\n }\n\n this.isPolling = true;\n this.lastPollTime = now;\n try {\n // When keeping delivered packages, use \"recent\" to get them from API\n const autoRemove = this.config.autoRemoveDelivered !== false;\n const deliveries = await this.client.getDeliveries(\n autoRemove ? \"active\" : \"recent\",\n );\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n await this.setStateAsync(\"info.connection\", { val: true, ack: true });\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(\n (d) => this.stateManager!.parseStatus(d) !== 0,\n );\n const visibleDeliveries = autoRemove ? activeDeliveries : deliveries;\n\n // Update each delivery (isolated: one failure must not block others)\n const activeIds: string[] = [];\n for (const delivery of visibleDeliveries) {\n try {\n const carrierName = await this.client.getCarrierName(\n delivery.carrier_code,\n );\n await this.stateManager.updateDelivery(delivery, carrierName);\n activeIds.push(this.stateManager.packageId(delivery));\n this.failedDeliveries.delete(delivery.tracking_number);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (this.failedDeliveries.has(delivery.tracking_number)) {\n this.log.debug(\n `Failed to update \"${delivery.tracking_number}\": ${msg}`,\n );\n } else {\n this.log.warn(\n `Failed to update \"${delivery.tracking_number}\": ${msg}`,\n );\n this.failedDeliveries.add(delivery.tracking_number);\n }\n }\n }\n\n // Cleanup stale deliveries\n await this.stateManager.cleanupDeliveries(activeIds);\n\n // Update summary (always uses active/non-delivered)\n await this.stateManager.updateSummary(activeDeliveries);\n\n this.log.debug(\n `Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`,\n );\n } catch (err) {\n const error = err as Error & {\n code?: string;\n retryAfterSeconds?: number;\n };\n\n // Classify the error\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n if (error.code === \"RATE_LIMITED\") {\n const cooldownSec = error.retryAfterSeconds || 5 * 60;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n this.log.warn(\n `Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`,\n );\n } else if (error.code === \"INVALID_API_KEY\") {\n // Always log \u2014 user must fix config\n this.log.error(\n \"Invalid API key \u2014 please check your parcel.app API key\",\n );\n } else if (isRepeat) {\n // Same error as last time \u2014 don't spam the log\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else if (errorCode === \"NETWORK\") {\n this.log.warn(`Cannot reach parcel.app API \u2014 will keep retrying`);\n } else if (errorCode === \"TIMEOUT\") {\n this.log.warn(`API request timeout \u2014 will retry next cycle`);\n } else {\n this.log.error(`Poll failed: ${error.message}`);\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n } finally {\n this.isPolling = false;\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) =>\n new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "parcelapp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.12",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.2.12": {
|
|
7
|
+
"en": "Harden API-drift guards in parcel-client and state-manager, add 38 regression tests",
|
|
8
|
+
"de": "API-Drift-Härtung in parcel-client und state-manager, 38 zusätzliche Regressions-Tests",
|
|
9
|
+
"ru": "Усиление защиты от дрейфа API в parcel-client и state-manager, добавлено 38 регрессионных тестов",
|
|
10
|
+
"pt": "Reforço de proteções contra API-drift em parcel-client e state-manager, 38 testes de regressão adicionais",
|
|
11
|
+
"nl": "API-drift guards versterkt in parcel-client en state-manager, 38 extra regressietests",
|
|
12
|
+
"fr": "Renforcement des protections contre la dérive d'API dans parcel-client et state-manager, 38 tests de régression supplémentaires",
|
|
13
|
+
"it": "Rafforzate protezioni contro API-drift in parcel-client e state-manager, 38 test di regressione aggiuntivi",
|
|
14
|
+
"es": "Reforzadas las protecciones contra API-drift en parcel-client y state-manager, 38 pruebas de regresión adicionales",
|
|
15
|
+
"pl": "Wzmocniono zabezpieczenia przed dryfem API w parcel-client i state-manager, 38 dodatkowych testów regresji",
|
|
16
|
+
"uk": "Посилено захист від дрейфу API у parcel-client та state-manager, 38 додаткових регресійних тестів",
|
|
17
|
+
"zh-cn": "加固parcel-client和state-manager中的API漂移防护,新增38个回归测试"
|
|
18
|
+
},
|
|
19
|
+
"0.2.11": {
|
|
20
|
+
"en": "Fix response stream error handling, isolate per-delivery poll failures, harden onMessage and onUnload",
|
|
21
|
+
"de": "Response-Stream-Fehlerbehandlung gefixt, per-Delivery Poll-Fehler isoliert, onMessage und onUnload gehärtet",
|
|
22
|
+
"ru": "Исправлена обработка ошибок потока ответа, изолированы ошибки опроса по доставкам, усилены onMessage и onUnload",
|
|
23
|
+
"pt": "Corrigido tratamento de erros do stream de resposta, falhas de polling isoladas por entrega, onMessage e onUnload reforçados",
|
|
24
|
+
"nl": "Response stream foutafhandeling gerepareerd, per-levering poll fouten geïsoleerd, onMessage en onUnload versterkt",
|
|
25
|
+
"fr": "Correction de la gestion des erreurs du flux de réponse, isolation des erreurs de sondage par livraison, renforcement de onMessage et onUnload",
|
|
26
|
+
"it": "Corretta gestione errori stream risposta, errori di polling isolati per consegna, onMessage e onUnload rafforzati",
|
|
27
|
+
"es": "Corregido manejo de errores del flujo de respuesta, errores de sondeo aislados por envío, onMessage y onUnload reforzados",
|
|
28
|
+
"pl": "Naprawiono obsługę błędów strumienia odpowiedzi, izolacja błędów odpytywania per dostawa, wzmocnione onMessage i onUnload",
|
|
29
|
+
"uk": "Виправлено обробку помилок потоку відповіді, ізольовано помилки опитування per-delivery, посилено onMessage та onUnload",
|
|
30
|
+
"zh-cn": "修复响应流错误处理,隔离每个快递轮询失败,加固onMessage和onUnload"
|
|
31
|
+
},
|
|
6
32
|
"0.2.10": {
|
|
7
33
|
"en": "Fix test timezone bug, remove unused devDependencies, add no-floating-promises lint rule",
|
|
8
34
|
"de": "Test-Zeitzonenfehler behoben, ungenutzte devDependencies entfernt, no-floating-promises Lint-Regel hinzugefügt",
|
|
@@ -67,32 +93,6 @@
|
|
|
67
93
|
"pl": "Usunieto nadmiarowe skrypty, skompresowano dokumentacje",
|
|
68
94
|
"uk": "Видалено зайвi скрипти, стиснуто документацiю",
|
|
69
95
|
"zh-cn": "移除冗余脚本,压缩文档"
|
|
70
|
-
},
|
|
71
|
-
"0.2.5": {
|
|
72
|
-
"en": "Fix delivery window timeout on Windows (replace toLocaleTimeString with manual formatting)",
|
|
73
|
-
"de": "Fix Zeitfenster-Timeout auf Windows (toLocaleTimeString durch manuelle Formatierung ersetzt)",
|
|
74
|
-
"ru": "Исправлен тайм-аут окна доставки в Windows",
|
|
75
|
-
"pt": "Correção de timeout de janela de entrega no Windows",
|
|
76
|
-
"nl": "Fix leveringsvenster timeout op Windows",
|
|
77
|
-
"fr": "Correction du timeout de la fenêtre de livraison sous Windows",
|
|
78
|
-
"it": "Correzione timeout finestra di consegna su Windows",
|
|
79
|
-
"es": "Corrección del timeout de la ventana de entrega en Windows",
|
|
80
|
-
"pl": "Naprawiono timeout okna dostawy w systemie Windows",
|
|
81
|
-
"uk": "Виправлено тайм-аут вікна доставки в Windows",
|
|
82
|
-
"zh-cn": "修复Windows上的交付窗口超时问题"
|
|
83
|
-
},
|
|
84
|
-
"0.2.4": {
|
|
85
|
-
"en": "Modernize dev tooling (esbuild, TypeScript 5.9 pin, testing-action-check v2)",
|
|
86
|
-
"de": "Dev-Tooling modernisiert (esbuild, TypeScript 5.9 Pin, testing-action-check v2)",
|
|
87
|
-
"ru": "Модернизация инструментов разработки (esbuild, TypeScript 5.9, testing-action-check v2)",
|
|
88
|
-
"pt": "Modernização das ferramentas de desenvolvimento (esbuild, TypeScript 5.9, testing-action-check v2)",
|
|
89
|
-
"nl": "Ontwikkeltools gemoderniseerd (esbuild, TypeScript 5.9 pin, testing-action-check v2)",
|
|
90
|
-
"fr": "Modernisation des outils de développement (esbuild, TypeScript 5.9, testing-action-check v2)",
|
|
91
|
-
"it": "Modernizzazione degli strumenti di sviluppo (esbuild, TypeScript 5.9, testing-action-check v2)",
|
|
92
|
-
"es": "Modernización de herramientas de desarrollo (esbuild, TypeScript 5.9, testing-action-check v2)",
|
|
93
|
-
"pl": "Modernizacja narzędzi programistycznych (esbuild, TypeScript 5.9, testing-action-check v2)",
|
|
94
|
-
"uk": "Модернізація інструментів розробки (esbuild, TypeScript 5.9, testing-action-check v2)",
|
|
95
|
-
"zh-cn": "开发工具现代化(esbuild、TypeScript 5.9 固定、testing-action-check v2)"
|
|
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>"
|
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.12",
|
|
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": [
|