iobroker.parcelapp 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,6 +94,12 @@ parcelapp.0.
94
94
 
95
95
  ## Changelog
96
96
 
97
+ ### 0.2.9 (2026-04-08)
98
+ - Add standard ioBroker test suite, optimize test build config
99
+
100
+ ### 0.2.8 (2026-04-05)
101
+ - Clean up empty parent folders after removing obsolete states
102
+
97
103
  ### 0.2.7 (2026-04-05)
98
104
  - Consistent UI labels across all adapters
99
105
 
@@ -103,19 +109,7 @@ parcelapp.0.
103
109
  ### 0.2.5 (2026-04-04)
104
110
  - Fix delivery window timeout on Windows (deterministic time formatting)
105
111
 
106
- ### 0.2.4 (2026-04-03)
107
- - Modernize dev tooling (esbuild, TypeScript 5.9 pin, testing-action-check v2)
108
-
109
- ### 0.2.3 (2026-03-28)
110
- - Fix carrier name cache not retrying after initial failure
111
-
112
- ### 0.2.2 (2026-03-28)
113
- - Adapter-managed timers, CI on Windows/macOS, consistent i18n keys, full MIT license in README
114
-
115
- ### 0.2.1 (2026-03-25)
116
- - Robust error handling: rate limit detection, connection error deduplication, poll throttling
117
-
118
- Older changelog: [CHANGELOG.md](CHANGELOG.md)
112
+ Older changelog: [CHANGELOG_OLD.md](CHANGELOG_OLD.md)
119
113
 
120
114
  ---
121
115
 
@@ -44,7 +44,7 @@ class StateManager {
44
44
  this.adapter = adapter;
45
45
  }
46
46
  /**
47
- * Sanitize a string for use as ioBroker object ID.
47
+ * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).
48
48
  *
49
49
  * @param name Raw string to sanitize
50
50
  */
@@ -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.\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"],
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
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -148,6 +148,17 @@ class ParcelappAdapter extends utils.Adapter {
148
148
  if (obj) {
149
149
  await this.delObjectAsync(stateId);
150
150
  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
+ }
151
162
  }
152
163
  }
153
164
  }
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 }\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": "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",
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": "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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.7",
4
+ "version": "0.2.9",
5
5
  "news": {
6
+ "0.2.9": {
7
+ "en": "Add standard ioBroker test suite, optimize test build config",
8
+ "de": "Standard-ioBroker-Testsuite hinzugefügt, Test-Build-Konfiguration optimiert",
9
+ "ru": "Добавлен стандартный набор тестов ioBroker, оптимизирована конфигурация сборки тестов",
10
+ "pt": "Adicionado conjunto de testes padrão do ioBroker, configuração de build de teste otimizada",
11
+ "nl": "Standaard ioBroker-testsuite toegevoegd, testbuild-configuratie geoptimaliseerd",
12
+ "fr": "Ajout de la suite de tests standard ioBroker, configuration de build de test optimisée",
13
+ "it": "Aggiunta suite di test standard ioBroker, configurazione build test ottimizzata",
14
+ "es": "Añadida suite de pruebas estándar de ioBroker, configuración de compilación de pruebas optimizada",
15
+ "pl": "Dodano standardowy zestaw testów ioBroker, zoptymalizowano konfigurację budowania testów",
16
+ "uk": "Додано стандартний набір тестів ioBroker, оптимізовано конфігурацію збірки тестів",
17
+ "zh-cn": "添加标准 ioBroker 测试套件,优化测试构建配置"
18
+ },
19
+ "0.2.8": {
20
+ "en": "Clean up empty parent folders after removing obsolete states",
21
+ "de": "Leere Eltern-Ordner nach Entfernen obsoleter States löschen",
22
+ "ru": "Удаление пустых родительских папок после удаления устаревших состояний",
23
+ "pt": "Limpar pastas pai vazias após remover estados obsoletos",
24
+ "nl": "Lege bovenliggende mappen opruimen na verwijderen van verouderde statussen",
25
+ "fr": "Nettoyage des dossiers parents vides après suppression des états obsolètes",
26
+ "it": "Pulizia delle cartelle padre vuote dopo la rimozione degli stati obsoleti",
27
+ "es": "Limpiar carpetas padre vacías después de eliminar estados obsoletos",
28
+ "pl": "Usuwanie pustych folderów nadrzędnych po usunięciu przestarzałych stanów",
29
+ "uk": "Видалення порожніх батьківських папок після видалення застарілих станів",
30
+ "zh-cn": "移除过时状态后清理空父文件夹"
31
+ },
6
32
  "0.2.7": {
7
33
  "en": "Consistent UI labels across all adapters",
8
34
  "de": "Einheitliche UI-Labels über alle Adapter",
@@ -67,32 +93,6 @@
67
93
  "pl": "Poprawka: pamięć podręczna nazw przewoźników ponawia próbę po początkowym błędzie",
68
94
  "uk": "Виправлення: кеш назв перевізників повторює спробу після початкової помилки",
69
95
  "zh-cn": "修复:运营商名称缓存在初次失败后重新尝试加载"
70
- },
71
- "0.2.2": {
72
- "en": "Adapter-managed timers, CI on Windows/macOS, consistent admin UI i18n keys, full MIT license in README",
73
- "de": "Adapter-verwaltete Timer, CI auf Windows/macOS, konsistente Admin-UI i18n-Keys, vollständige MIT-Lizenz in README",
74
- "ru": "Таймеры, управляемые адаптером, CI на Windows/macOS, согласованные ключи i18n в Admin UI, полная лицензия MIT в README",
75
- "pt": "Timers gerenciados pelo adaptador, CI no Windows/macOS, chaves i18n consistentes na UI admin, licença MIT completa no README",
76
- "nl": "Adapter-beheerde timers, CI op Windows/macOS, consistente admin-UI i18n-sleutels, volledige MIT-licentie in README",
77
- "fr": "Minuteries gérées par l adaptateur, CI sur Windows/macOS, clés i18n cohérentes dans l interface admin, licence MIT complète dans README",
78
- "it": "Timer gestiti dall adattatore, CI su Windows/macOS, chiavi i18n coerenti nell interfaccia admin, licenza MIT completa nel README",
79
- "es": "Temporizadores gestionados por el adaptador, CI en Windows/macOS, claves i18n consistentes en la UI de admin, licencia MIT completa en README",
80
- "pl": "Timery zarządzane przez adapter, CI na Windows/macOS, spójne klucze i18n w admin UI, pełna licencja MIT w README",
81
- "uk": "Таймери, керовані адаптером, CI на Windows/macOS, узгоджені ключі i18n в Admin UI, повна ліцензія MIT в README",
82
- "zh-cn": "适配器管理的定时器,Windows/macOS CI,一致的管理界面 i18n 键,README 中的完整 MIT 许可证"
83
- },
84
- "0.2.1": {
85
- "en": "Robust error handling: API rate limit detection, connection error deduplication, poll throttling",
86
- "de": "Robuste Fehlerbehandlung: Erkennung von API-Ratenlimits, Deduplizierung von Verbindungsfehlern, Polling-Drosselung",
87
- "ru": "Надежная обработка ошибок: обнаружение ограничения скорости API, дедупликация ошибок соединения, регулирование опроса.",
88
- "pt": "Tratamento robusto de erros: detecção de limite de taxa de API, desduplicação de erros de conexão, otimização de pesquisa",
89
- "nl": "Robuuste foutafhandeling: detectie van API-snelheidslimieten, deduplicatie van verbindingsfouten, poll-throttling",
90
- "fr": "Gestion robuste des erreurs : détection de limite de débit API, déduplication des erreurs de connexion, limitation des interrogations",
91
- "it": "Gestione efficace degli errori: rilevamento del limite di velocità dell'API, deduplicazione degli errori di connessione, limitazione del poll",
92
- "es": "Manejo sólido de errores: detección de límite de tasa de API, deduplicación de errores de conexión, limitación de encuestas",
93
- "pl": "Solidna obsługa błędów: wykrywanie limitów szybkości interfejsu API, deduplikacja błędów połączenia, ograniczanie odpytywania",
94
- "uk": "Надійна обробка помилок: виявлення обмеження швидкості API, дедуплікація помилок підключення, регулювання опитування",
95
- "zh-cn": "强大的错误处理:API 速率限制检测、连接错误重复数据删除、轮询限制"
96
96
  }
97
97
  },
98
98
  "titleLang": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.parcelapp",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "ioBroker adapter for parcel.app — track packages from 300+ carriers",
5
5
  "author": {
6
6
  "name": "krobi",
@@ -56,10 +56,10 @@
56
56
  "build:test": "tsc -p tsconfig.test.json",
57
57
  "watch": "build-adapter ts --watch",
58
58
  "check": "tsc --noEmit",
59
- "test:package": "npm run build:test && mocha --exit \"build/test/testPackageFiles.js\"",
60
- "test:integration": "npm run build:test && mocha --exit \"build/test/**/*.js\"",
61
- "test": "npm run build:test && mocha --exit \"build/test/**/*.js\"",
62
- "test:ci": "npm run build:test && mocha --exit --reporter spec \"build/test/**/*.js\"",
59
+ "test:ts": "npm run build:test && mocha --exit \"build/test/testParcelClient.js\" \"build/test/testStateManager.js\"",
60
+ "test:package": "mocha test/package --exit",
61
+ "test:integration": "mocha test/integration --exit",
62
+ "test": "npm run test:ts && npm run test:package",
63
63
  "translate": "translate-adapter",
64
64
  "lint": "eslint",
65
65
  "lint:fix": "eslint --fix",