iobroker.parcelapp 0.10.4 → 0.11.0

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
@@ -8,6 +8,8 @@
8
8
 
9
9
  ioBroker adapter for the [parcel.app](https://parcelapp.net) API. Supports all carriers that parcel.app tracks.
10
10
 
11
+ 📖 **Full user documentation:** [English](docs/en/README.md) · [Deutsch](docs/de/README.md) — setup step by step, every datapoint explained, scripting and FAQ.
12
+
11
13
  ---
12
14
 
13
15
  ## Features
@@ -140,6 +142,14 @@ sendTo("parcelapp.0", "addDelivery", {
140
142
  Placeholder for the next version (at the beginning of the line):
141
143
  ### **WORK IN PROGRESS**
142
144
  -->
145
+ ### 0.11.0 (2026-09-04)
146
+
147
+ - Fixed: Since version 0.10.3 the Test Connection button gave no response at all, and packages added from a script never showed up — both work again.
148
+ - Fixed: On installations that already existed, the summary datapoints and the connection state kept their old English names — an update now reaches every datapoint.
149
+ - New: Datapoints whose name alone does not explain them now carry a short description in the object tree, in all eleven languages.
150
+ - New: Detailed user documentation in English and German, shown in the ioBroker documentation portal.
151
+ - Fixed: Two settings from much older versions were still listed in the instance configuration although nothing used them any more.
152
+
143
153
  ### 0.10.4 (2026-09-02)
144
154
 
145
155
  - Fixed: A malformed reply from parcel.app (empty body or a broken delivery entry) no longer aborts the poll with a cryptic internal message — it is reported as an API problem and retried next poll.
@@ -159,17 +169,6 @@ sendTo("parcelapp.0", "addDelivery", {
159
169
 
160
170
  - Internal refactoring. No user-facing changes.
161
171
 
162
- ### 0.10.0 (2026-07-08)
163
-
164
- - Fixed: the admin "Test Connection" button now reports real failures — before, it always showed "Ok" even with a wrong API key.
165
- - Fixed: a package's last-updated timestamp no longer jumps to the restart time after an adapter restart — it only changes when tracking data actually changed.
166
- - Fixed: a stalled API response can no longer freeze polling until a manual restart — every request now has a hard 60-second deadline.
167
- - Fixed: a failed adapter start now triggers an automatic restart instead of leaving the adapter idle until restarted by hand.
168
- - Changed: recurring errors such as a wrong API key are logged once instead of every poll cycle, and stopping the adapter no longer leaves a red error line in the log.
169
- - Changed: short ioBroker database hiccups no longer flip the connection indicator — it now reflects only the parcel.app connection.
170
- - Changed: the fallback package name ("Package …") is localized like all other texts, and the adapter is listed under a fitting admin category (misc-data).
171
- - Changed: the automatic poll after adding a delivery now respects the one-minute pacing, so bulk-adds can no longer exhaust the hourly API budget.
172
-
173
172
  [Older changelogs can be found there](CHANGELOG_OLD.md)
174
173
 
175
174
  ## Support
@@ -44,5 +44,15 @@
44
44
  "status_6": "Zustellversuch gescheitert",
45
45
  "status_7": "Ausnahme",
46
46
  "status_8": "Registriert",
47
- "packageName": "Paket %s"
47
+ "packageName": "Paket %s",
48
+ "descInfoConnection": "Wahr, solange der Adapter die parcel.app-API erreicht.",
49
+ "descStatusCode": "Numerischer Status von parcel.app (0-8); -1 bedeutet, dass der Wert nicht gedeutet werden konnte.",
50
+ "descExtraInfo": "Zusatzangabe, die der Zusteller benötigt, etwa Postleitzahl oder E-Mail-Adresse.",
51
+ "descDeliveryWindow": "Erwartetes Zustellfenster; ein über mehrere Tage reichendes Fenster trägt auf beiden Seiten das Datum.",
52
+ "descDeliveryEstimate": "Erwartete Zustellung in Worten - heute, morgen oder in einigen Tagen.",
53
+ "descLastEvent": "Jüngste Sendungsmeldung des Zustellers mit Datum.",
54
+ "descLastUpdated": "Zeitpunkt der letzten Änderung der Sendungsdaten - nicht der letzte Abruf.",
55
+ "descActiveCount": "Anzahl der Pakete, die noch nicht zugestellt sind.",
56
+ "descTodayCount": "Anzahl der Pakete, die heute erwartet werden.",
57
+ "descSummaryDeliveryWindow": "Gemeinsames Fenster aller heute erwarteten Pakete - frühester Beginn bis spätestes Ende."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Delivery Attempt Failed",
45
45
  "status_7": "Exception",
46
46
  "status_8": "Info Received",
47
- "packageName": "Package %s"
47
+ "packageName": "Package %s",
48
+ "descInfoConnection": "True while the adapter can reach the parcel.app API.",
49
+ "descStatusCode": "Numeric status from parcel.app (0-8); -1 means the value could not be interpreted.",
50
+ "descExtraInfo": "Additional detail the carrier needs, such as a postal code or e-mail address.",
51
+ "descDeliveryWindow": "Expected delivery time window; a window spanning several days carries the date on both sides.",
52
+ "descDeliveryEstimate": "Expected delivery in words - today, tomorrow or in a number of days.",
53
+ "descLastEvent": "Most recent tracking event reported by the carrier, with its date.",
54
+ "descLastUpdated": "When the tracking data last changed - not when the adapter last polled.",
55
+ "descActiveCount": "Number of packages that have not been delivered yet.",
56
+ "descTodayCount": "Number of packages expected to arrive today.",
57
+ "descSummaryDeliveryWindow": "Combined window of all packages expected today - earliest start to latest end."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Intento de entrega fallido",
45
45
  "status_7": "Excepción",
46
46
  "status_8": "Registrado",
47
- "packageName": "Paquete %s"
47
+ "packageName": "Paquete %s",
48
+ "descInfoConnection": "Verdadero mientras el adaptador pueda acceder a la API de parcel.app.",
49
+ "descStatusCode": "Estado numérico de parcel.app (0-8); -1 significa que el valor no se pudo interpretar.",
50
+ "descExtraInfo": "Dato adicional que necesita el transportista, por ejemplo un código postal o una dirección de correo.",
51
+ "descDeliveryWindow": "Ventana de entrega prevista; una ventana de varios días lleva la fecha a ambos lados.",
52
+ "descDeliveryEstimate": "Entrega prevista en palabras - hoy, mañana o dentro de unos días.",
53
+ "descLastEvent": "Evento de seguimiento más reciente comunicado por el transportista, con su fecha.",
54
+ "descLastUpdated": "Momento del último cambio de los datos de seguimiento - no de la última consulta.",
55
+ "descActiveCount": "Número de paquetes que aún no se han entregado.",
56
+ "descTodayCount": "Número de paquetes que se esperan hoy.",
57
+ "descSummaryDeliveryWindow": "Ventana combinada de todos los paquetes esperados hoy - del inicio más temprano al final más tardío."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Échec de la livraison",
45
45
  "status_7": "Exception",
46
46
  "status_8": "Enregistré",
47
- "packageName": "Colis %s"
47
+ "packageName": "Colis %s",
48
+ "descInfoConnection": "Vrai tant que l'adaptateur parvient à joindre l'API parcel.app.",
49
+ "descStatusCode": "Statut numérique de parcel.app (0-8) ; -1 signifie que la valeur n'a pas pu être interprétée.",
50
+ "descExtraInfo": "Information complémentaire exigée par le transporteur, par exemple un code postal ou une adresse e-mail.",
51
+ "descDeliveryWindow": "Créneau de livraison prévu ; un créneau sur plusieurs jours porte la date de chaque côté.",
52
+ "descDeliveryEstimate": "Livraison prévue en toutes lettres - aujourd'hui, demain ou dans quelques jours.",
53
+ "descLastEvent": "Dernier événement de suivi communiqué par le transporteur, avec sa date.",
54
+ "descLastUpdated": "Moment du dernier changement des données de suivi - pas de la dernière interrogation.",
55
+ "descActiveCount": "Nombre de colis qui ne sont pas encore livrés.",
56
+ "descTodayCount": "Nombre de colis attendus aujourd'hui.",
57
+ "descSummaryDeliveryWindow": "Créneau combiné de tous les colis attendus aujourd'hui - du début le plus tôt à la fin la plus tardive."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Consegna fallita",
45
45
  "status_7": "Eccezione",
46
46
  "status_8": "Registrato",
47
- "packageName": "Pacco %s"
47
+ "packageName": "Pacco %s",
48
+ "descInfoConnection": "Vero finché l'adattatore riesce a raggiungere l'API di parcel.app.",
49
+ "descStatusCode": "Stato numerico di parcel.app (0-8); -1 significa che il valore non è stato interpretabile.",
50
+ "descExtraInfo": "Dato aggiuntivo richiesto dal corriere, ad esempio un codice postale o un indirizzo e-mail.",
51
+ "descDeliveryWindow": "Finestra di consegna prevista; una finestra su più giorni riporta la data su entrambi i lati.",
52
+ "descDeliveryEstimate": "Consegna prevista a parole - oggi, domani o fra alcuni giorni.",
53
+ "descLastEvent": "Evento di tracciamento più recente comunicato dal corriere, con la data.",
54
+ "descLastUpdated": "Momento dell'ultima variazione dei dati di tracciamento - non dell'ultima interrogazione.",
55
+ "descActiveCount": "Numero di pacchi non ancora consegnati.",
56
+ "descTodayCount": "Numero di pacchi attesi oggi.",
57
+ "descSummaryDeliveryWindow": "Finestra combinata di tutti i pacchi attesi oggi - dall'inizio più precoce alla fine più tarda."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Bezorgpoging mislukt",
45
45
  "status_7": "Uitzondering",
46
46
  "status_8": "Geregistreerd",
47
- "packageName": "Pakket %s"
47
+ "packageName": "Pakket %s",
48
+ "descInfoConnection": "Waar zolang de adapter de parcel.app-API kan bereiken.",
49
+ "descStatusCode": "Numerieke status van parcel.app (0-8); -1 betekent dat de waarde niet kon worden geduid.",
50
+ "descExtraInfo": "Aanvullend gegeven dat de vervoerder nodig heeft, zoals postcode of e-mailadres.",
51
+ "descDeliveryWindow": "Verwacht bezorgvenster; een venster over meerdere dagen toont aan beide kanten de datum.",
52
+ "descDeliveryEstimate": "Verwachte bezorging in woorden - vandaag, morgen of over een aantal dagen.",
53
+ "descLastEvent": "Meest recente zendingsmelding van de vervoerder, met datum.",
54
+ "descLastUpdated": "Moment waarop de zendinggegevens voor het laatst zijn gewijzigd - niet de laatste opvraging.",
55
+ "descActiveCount": "Aantal pakketten dat nog niet is bezorgd.",
56
+ "descTodayCount": "Aantal pakketten dat vandaag wordt verwacht.",
57
+ "descSummaryDeliveryWindow": "Gecombineerd venster van alle vandaag verwachte pakketten - vroegste start tot laatste einde."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Nieudana próba doręczenia",
45
45
  "status_7": "Wyjątek",
46
46
  "status_8": "Zarejestrowane",
47
- "packageName": "Paczka %s"
47
+ "packageName": "Paczka %s",
48
+ "descInfoConnection": "Prawda, dopóki adapter może połączyć się z API parcel.app.",
49
+ "descStatusCode": "Status liczbowy z parcel.app (0-8); -1 oznacza, że wartości nie dało się zinterpretować.",
50
+ "descExtraInfo": "Dodatkowa informacja wymagana przez przewoźnika, np. kod pocztowy lub adres e-mail.",
51
+ "descDeliveryWindow": "Przewidywane okno dostawy; okno obejmujące kilka dni zawiera datę po obu stronach.",
52
+ "descDeliveryEstimate": "Przewidywana dostawa słownie - dziś, jutro lub za kilka dni.",
53
+ "descLastEvent": "Najnowsze zdarzenie śledzenia zgłoszone przez przewoźnika, wraz z datą.",
54
+ "descLastUpdated": "Moment ostatniej zmiany danych śledzenia - nie ostatniego odpytania.",
55
+ "descActiveCount": "Liczba przesyłek, które nie zostały jeszcze doręczone.",
56
+ "descTodayCount": "Liczba przesyłek oczekiwanych dzisiaj.",
57
+ "descSummaryDeliveryWindow": "Wspólne okno wszystkich przesyłek oczekiwanych dzisiaj - od najwcześniejszego początku do najpóźniejszego końca."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Tentativa de entrega falhou",
45
45
  "status_7": "Exceção",
46
46
  "status_8": "Registado",
47
- "packageName": "Encomenda %s"
47
+ "packageName": "Encomenda %s",
48
+ "descInfoConnection": "Verdadeiro enquanto o adaptador conseguir alcançar a API do parcel.app.",
49
+ "descStatusCode": "Estado numérico do parcel.app (0-8); -1 significa que o valor não pôde ser interpretado.",
50
+ "descExtraInfo": "Detalhe adicional exigido pela transportadora, como código postal ou endereço de e-mail.",
51
+ "descDeliveryWindow": "Janela de entrega prevista; uma janela que abrange vários dias mostra a data de ambos os lados.",
52
+ "descDeliveryEstimate": "Entrega prevista por extenso - hoje, amanhã ou dentro de alguns dias.",
53
+ "descLastEvent": "Evento de rastreio mais recente comunicado pela transportadora, com a data.",
54
+ "descLastUpdated": "Momento da última alteração dos dados de rastreio - não da última consulta.",
55
+ "descActiveCount": "Número de encomendas que ainda não foram entregues.",
56
+ "descTodayCount": "Número de encomendas previstas para hoje.",
57
+ "descSummaryDeliveryWindow": "Janela combinada de todas as encomendas previstas para hoje - do início mais cedo ao fim mais tarde."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Неудачная доставка",
45
45
  "status_7": "Исключение",
46
46
  "status_8": "Зарегистрировано",
47
- "packageName": "Посылка %s"
47
+ "packageName": "Посылка %s",
48
+ "descInfoConnection": "Истина, пока адаптер может связаться с API parcel.app.",
49
+ "descStatusCode": "Числовой статус от parcel.app (0-8); -1 означает, что значение не удалось распознать.",
50
+ "descExtraInfo": "Дополнительные данные, нужные перевозчику, например почтовый индекс или адрес электронной почты.",
51
+ "descDeliveryWindow": "Ожидаемое временное окно доставки; окно, охватывающее несколько дней, содержит дату с обеих сторон.",
52
+ "descDeliveryEstimate": "Ожидаемая доставка словами - сегодня, завтра или через несколько дней.",
53
+ "descLastEvent": "Последнее событие отслеживания от перевозчика с датой.",
54
+ "descLastUpdated": "Момент последнего изменения данных отслеживания - не время последнего опроса.",
55
+ "descActiveCount": "Количество посылок, которые ещё не доставлены.",
56
+ "descTodayCount": "Количество посылок, ожидаемых сегодня.",
57
+ "descSummaryDeliveryWindow": "Общее окно всех ожидаемых сегодня посылок - от самого раннего начала до самого позднего конца."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "Невдала спроба доставки",
45
45
  "status_7": "Виняток",
46
46
  "status_8": "Зареєстровано",
47
- "packageName": "Посилка %s"
47
+ "packageName": "Посилка %s",
48
+ "descInfoConnection": "Істина, доки адаптер може дістатися до API parcel.app.",
49
+ "descStatusCode": "Числовий статус від parcel.app (0-8); -1 означає, що значення не вдалося розпізнати.",
50
+ "descExtraInfo": "Додаткові дані, потрібні перевізникові, наприклад поштовий індекс або адреса електронної пошти.",
51
+ "descDeliveryWindow": "Очікуване вікно доставки; вікно, що охоплює кілька днів, містить дату з обох боків.",
52
+ "descDeliveryEstimate": "Очікувана доставка словами - сьогодні, завтра або за кілька днів.",
53
+ "descLastEvent": "Найновіша подія відстеження від перевізника, разом із датою.",
54
+ "descLastUpdated": "Момент останньої зміни даних відстеження - не останнього опитування.",
55
+ "descActiveCount": "Кількість посилок, які ще не доставлено.",
56
+ "descTodayCount": "Кількість посилок, що очікуються сьогодні.",
57
+ "descSummaryDeliveryWindow": "Спільне вікно всіх очікуваних сьогодні посилок - від найранішого початку до найпізнішого кінця."
48
58
  }
@@ -44,5 +44,15 @@
44
44
  "status_6": "派送失败",
45
45
  "status_7": "异常",
46
46
  "status_8": "已登记",
47
- "packageName": "包裹 %s"
47
+ "packageName": "包裹 %s",
48
+ "descInfoConnection": "适配器能够连接 parcel.app API 时为真。",
49
+ "descStatusCode": "来自 parcel.app 的数字状态(0-8);-1 表示该值无法识别。",
50
+ "descExtraInfo": "承运商所需的附加信息,例如邮政编码或电子邮件地址。",
51
+ "descDeliveryWindow": "预计派送时间段;跨越多天的时间段会在两侧显示日期。",
52
+ "descDeliveryEstimate": "以文字表示的预计派送时间——今天、明天或若干天后。",
53
+ "descLastEvent": "承运商报告的最新跟踪事件及其日期。",
54
+ "descLastUpdated": "跟踪数据最后一次发生变化的时间——不是最后一次轮询的时间。",
55
+ "descActiveCount": "尚未派送的包裹数量。",
56
+ "descTodayCount": "预计今天送达的包裹数量。",
57
+ "descSummaryDeliveryWindow": "今天预计送达的所有包裹的合并时间段——最早开始到最晚结束。"
48
58
  }
@@ -31,7 +31,7 @@ const DELETE_BATCH_SIZE = 25;
31
31
  class StateManager {
32
32
  adapter;
33
33
  /**
34
- * Cache of state IDs that have already passed `setObjectNotExistsAsync`.
34
+ * Cache of state IDs whose object has already been written this process.
35
35
  * Skips repeat DB lookups on the hot path — each poll touches ~11 states
36
36
  * per delivery, and most deliveries see no schema change between polls.
37
37
  * On `cleanupDeliveries`, IDs of removed packages are dropped so a re-add
@@ -236,19 +236,33 @@ class StateManager {
236
236
  const lastEvent = this.formatLastEvent(delivery);
237
237
  const lastLocation = this.extractLastLocation(delivery);
238
238
  const stateDefs = [
239
- [`${devicePath}.carrier`, (0, import_i18n.tName)("carrier"), "string", "text", carrierName],
240
- [`${devicePath}.status`, (0, import_i18n.tName)("status"), "string", "text", statusText],
241
- [`${devicePath}.statusCode`, (0, import_i18n.tName)("statusCode"), "number", "value", statusCode],
242
- [`${devicePath}.description`, (0, import_i18n.tName)("description"), "string", "text", description],
243
- [`${devicePath}.trackingNumber`, (0, import_i18n.tName)("trackingNumber"), "string", "text", trackingNumber],
244
- [`${devicePath}.extraInfo`, (0, import_i18n.tName)("extraInfo"), "string", "text", extraInfo],
245
- [`${devicePath}.deliveryWindow`, (0, import_i18n.tName)("deliveryWindow"), "string", "text", deliveryWindow],
246
- [`${devicePath}.deliveryEstimate`, (0, import_i18n.tName)("deliveryEstimate"), "string", "text", deliveryEstimate],
247
- [`${devicePath}.lastEvent`, (0, import_i18n.tName)("lastEvent"), "string", "text", lastEvent],
248
- [`${devicePath}.lastLocation`, (0, import_i18n.tName)("lastLocation"), "string", "text", lastLocation]
239
+ [`${devicePath}.carrier`, (0, import_i18n.tName)("carrier"), "string", "text", carrierName, void 0],
240
+ [`${devicePath}.status`, (0, import_i18n.tName)("status"), "string", "text", statusText, void 0],
241
+ [`${devicePath}.statusCode`, (0, import_i18n.tName)("statusCode"), "number", "value", statusCode, (0, import_i18n.tName)("descStatusCode")],
242
+ [`${devicePath}.description`, (0, import_i18n.tName)("description"), "string", "text", description, void 0],
243
+ [`${devicePath}.trackingNumber`, (0, import_i18n.tName)("trackingNumber"), "string", "text", trackingNumber, void 0],
244
+ [`${devicePath}.extraInfo`, (0, import_i18n.tName)("extraInfo"), "string", "text", extraInfo, (0, import_i18n.tName)("descExtraInfo")],
245
+ [
246
+ `${devicePath}.deliveryWindow`,
247
+ (0, import_i18n.tName)("deliveryWindow"),
248
+ "string",
249
+ "text",
250
+ deliveryWindow,
251
+ (0, import_i18n.tName)("descDeliveryWindow")
252
+ ],
253
+ [
254
+ `${devicePath}.deliveryEstimate`,
255
+ (0, import_i18n.tName)("deliveryEstimate"),
256
+ "string",
257
+ "text",
258
+ deliveryEstimate,
259
+ (0, import_i18n.tName)("descDeliveryEstimate")
260
+ ],
261
+ [`${devicePath}.lastEvent`, (0, import_i18n.tName)("lastEvent"), "string", "text", lastEvent, (0, import_i18n.tName)("descLastEvent")],
262
+ [`${devicePath}.lastLocation`, (0, import_i18n.tName)("lastLocation"), "string", "text", lastLocation, void 0]
249
263
  ];
250
264
  const changed = await Promise.all(
251
- stateDefs.map(([id, name, type, role, val]) => this.createAndSet(id, name, type, role, val))
265
+ stateDefs.map(([id, name, type, role, val, desc]) => this.createAndSet(id, name, type, role, val, desc))
252
266
  );
253
267
  if (changed.some(Boolean)) {
254
268
  await this.createAndSet(
@@ -256,7 +270,8 @@ class StateManager {
256
270
  (0, import_i18n.tName)("lastUpdated"),
257
271
  "string",
258
272
  "date",
259
- (/* @__PURE__ */ new Date()).toISOString()
273
+ (/* @__PURE__ */ new Date()).toISOString(),
274
+ (0, import_i18n.tName)("descLastUpdated")
260
275
  );
261
276
  }
262
277
  }
@@ -272,14 +287,29 @@ class StateManager {
272
287
  `updateSummary: ${activeDeliveries.length} active, ${todayDeliveries.length} expected today`
273
288
  );
274
289
  await Promise.all([
275
- this.createAndSet("summary.activeCount", (0, import_i18n.tName)("activeCount"), "number", "value", activeDeliveries.length),
276
- this.createAndSet("summary.todayCount", (0, import_i18n.tName)("todayCount"), "number", "value", todayDeliveries.length),
290
+ this.createAndSet(
291
+ "summary.activeCount",
292
+ (0, import_i18n.tName)("activeCount"),
293
+ "number",
294
+ "value",
295
+ activeDeliveries.length,
296
+ (0, import_i18n.tName)("descActiveCount")
297
+ ),
298
+ this.createAndSet(
299
+ "summary.todayCount",
300
+ (0, import_i18n.tName)("todayCount"),
301
+ "number",
302
+ "value",
303
+ todayDeliveries.length,
304
+ (0, import_i18n.tName)("descTodayCount")
305
+ ),
277
306
  this.createAndSet(
278
307
  "summary.deliveryWindow",
279
308
  (0, import_i18n.tName)("summaryDeliveryWindow"),
280
309
  "string",
281
310
  "text",
282
- this.calculateCombinedWindow(todayDeliveries)
311
+ this.calculateCombinedWindow(todayDeliveries),
312
+ (0, import_i18n.tName)("descSummaryDeliveryWindow")
283
313
  )
284
314
  ]);
285
315
  }
@@ -579,26 +609,38 @@ class StateManager {
579
609
  return StateManager.formatWindow(minStart, maxEnd);
580
610
  }
581
611
  /**
582
- * Create/extend a read-only state and set its value. Skips the
583
- * `setObjectNotExistsAsync` round-trip once the ID is in the cache
584
- * states are static after first creation; only the value changes per poll.
612
+ * Create/refresh a read-only state and set its value. Runs the object write
613
+ * once per ID per process (the cache skips the repeat round-trip on the hot
614
+ * path); only the value changes per poll.
615
+ *
616
+ * v0.11.0: `extendObject` instead of `setObjectNotExistsAsync`. The old call
617
+ * only ever touched an object that did not exist yet, so a changed name,
618
+ * description, role or type reached FRESH installs only — an existing tree
619
+ * kept the text it was created with, while manifest, linter, type check and
620
+ * the name gate all stayed green. Measured on a live install: the three
621
+ * permanent `summary.*` states still carried their plain-English pre-i18n
622
+ * names, while the per-package states looked correct only because packages
623
+ * are deleted and recreated. `extendObject` merges, so a user-set `custom`
624
+ * (history/logging) survives — the name deliberately does NOT: the adapter
625
+ * owns the names of its own states.
585
626
  *
586
627
  * @param id State ID relative to adapter namespace
587
628
  * @param name Display name (translation object or plain string)
588
629
  * @param type Value type
589
630
  * @param role ioBroker role
590
631
  * @param val Value to set
632
+ * @param desc Short explanation, or undefined where there is nothing to explain
591
633
  * @returns true when the broker actually wrote the value (it differed or the
592
634
  * state was new) — the DB-backed "did anything change" signal driving
593
635
  * `lastUpdated` (v0.10.0, M5)
594
636
  */
595
- async createAndSet(id, name, type, role, val) {
637
+ async createAndSet(id, name, type, role, val, desc) {
596
638
  if (!this.createdIds.has(id)) {
597
- await this.adapter.setObjectNotExistsAsync(id, {
598
- type: "state",
599
- common: { name, type, role, read: true, write: false },
600
- native: {}
601
- });
639
+ const common = { name, type, role, read: true, write: false };
640
+ if (desc !== void 0) {
641
+ common.desc = desc;
642
+ }
643
+ await this.adapter.extendObject(id, { type: "state", common, native: {} });
602
644
  this.createdIds.add(id);
603
645
  }
604
646
  const result = await this.adapter.setStateChangedAsync(id, { val, ack: true });
@@ -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 { coerceFiniteNumber, oneLine } from \"./coerce\";\nimport { packageName, statusLabel, tName, tText } from \"./i18n\";\nimport type { ParcelDelivery, ParcelEvent } from \"./types\";\nimport { UNKNOWN_STATUS_CODE } from \"./types\";\n\n/** Status codes that have expected delivery date/time: 2=In Transit, 4=Out for Delivery, 8=Info Received */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Upper bound for the `deliveries.*` object-view range query: the highest BMP\n * code unit, so the range covers every possible sanitized package id.\n */\nconst ID_RANGE_END = \"\uFFFF\";\n\n/** Max length of a sanitized package-id segment (collision suffix handles truncation clashes). */\nconst MAX_ID_LENGTH = 50;\n\n/**\n * v0.10.0 (I2): cap the parallel recursive deletes in cleanupDeliveries the\n * same way main.ts caps the update fan-out \u2014 a poll that suddenly loses many\n * packages must not flood the broker in one burst.\n */\nconst DELETE_BATCH_SIZE = 25;\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\n /**\n * Cache of state IDs that have already passed `setObjectNotExistsAsync`.\n * Skips repeat DB lookups on the hot path \u2014 each poll touches ~11 states\n * per delivery, and most deliveries see no schema change between polls.\n * On `cleanupDeliveries`, IDs of removed packages are dropped so a re-add\n * triggers a fresh creation.\n */\n private readonly createdIds = new Set<string>();\n\n /**\n * v0.10.0 (DP-5): package ids whose device object was ensured this process.\n * Replaces the former description+tracking signature map: with\n * `preserve: { common: [\"name\"] }` a rewrite never changed an existing\n * object's name anyway, so ensuring existence ONCE per process is the\n * honest version of what the signature cache actually did.\n */\n private readonly deviceEnsured = new Set<string>();\n\n /**\n * v0.7.2: package ids known to exist as device objects. Filled from the\n * object view ONCE after adapter start (reconciles leftovers from previous\n * runs), afterwards maintained in memory \u2014 `cleanupDeliveries` no longer\n * needs a DB round-trip per poll.\n */\n private knownDeliveryIds: Set<string> | null = null;\n\n /**\n * v0.4.2 (S3): which raw-tracking-key currently \"owns\" each sanitized id\n * within the running poll. Cleared via `resetPollState()` between polls so\n * the same delivery keeps its bare id as long as it's unique.\n */\n private readonly idOwner = new Map<string, string>();\n\n /**\n * L1: per-delivery-object memo for `parseStatus`. A poll parses the SAME\n * delivery object at up to four sites (active filter, updateDelivery,\n * updateSummary's isToday filter, combined window); memoizing keyed by the\n * object means the parse \u2014 and its drift debug line \u2014 runs ONCE per delivery\n * instead of per site. No reset needed: each poll's deliveries are fresh\n * objects (JSON.parse) and GC'd afterwards, and nothing holds a long-lived\n * delivery reference (idOwner/failedDeliveries/knownDeliveryIds store strings).\n */\n private readonly statusMemo = new WeakMap<ParcelDelivery, number>();\n\n /**\n * @param adapter The ioBroker adapter instance\n */\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, MAX_ID_LENGTH) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. The API sends an int; we also accept\n * a numeric string and fall back to the \"unknown\" sentinel (-1) for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n // L1: memoize per delivery object so the parse (and its drift log) runs\n // once per delivery per poll, not once per call site.\n const memoized = this.statusMemo.get(delivery);\n if (memoized !== undefined) {\n return memoized;\n }\n const code = this.computeStatus(delivery);\n this.statusMemo.set(delivery, code);\n return code;\n }\n\n /**\n * Parse the status code without memoization. Split from {@link parseStatus}\n * (L1) so the memo wraps a single pure computation \u2014 including the one-per-\n * delivery drift log.\n *\n * @param delivery The delivery to parse\n */\n private computeStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code;\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return Math.trunc(raw);\n }\n if (typeof raw === \"string\") {\n const n = parseInt(raw, 10);\n if (Number.isFinite(n)) {\n return n;\n }\n }\n // API drift (non-numeric / non-string status_code). Return a visible\n // \"unknown\" sentinel instead of 0 (Delivered) \u2014 otherwise a garbage\n // status_code would silently filter the package out and remove it in\n // autoRemove mode. The active filter is `status !== 0`, so -1 stays visible.\n this.adapter.log.debug(\n `parseStatus drift: ${JSON.stringify(raw)} (type ${typeof raw}) \u2192 ${UNKNOWN_STATUS_CODE} (unknown, kept visible)`,\n );\n return UNKNOWN_STATUS_CODE;\n }\n\n /**\n * Build a unique package ID from a delivery.\n *\n * v0.4.2 (S3): when the bare `sanitize(tracking_number)` collides with\n * another active package (e.g. two trackings differ only in special\n * chars that strip down to the same id), append a stable hash of the\n * full tracking number so both end up at distinct state IDs.\n *\n * @param delivery The delivery to build an ID for\n */\n packageId(delivery: ParcelDelivery): string {\n let id = this.sanitize(delivery.tracking_number);\n // API-drift guard: only string values extend the id\n if (typeof delivery.extra_information === \"string\" && delivery.extra_information.length > 0) {\n id += `_${this.sanitize(delivery.extra_information)}`;\n }\n // v0.4.2 (S3): collision suffix when two distinct (raw) trackings would\n // collapse to the same id. Bare id is kept as long as it's unique\n // within this poll (back-compat with existing installs).\n const owner = this.idOwner.get(id);\n const rawKey = StateManager.rawIdKey(delivery);\n if (owner !== undefined && owner !== rawKey) {\n const suffixed = `${id}__${StateManager.shortHash(rawKey)}`;\n // v0.4.3 (C3): trace the collision-suffix path. Rare event but the\n // resulting state-id divergence is hard to diagnose without a log.\n this.adapter.log.debug(\n `packageId collision: bare='${id}' owner='${oneLine(owner)}' new='${oneLine(rawKey)}' \u2192 suffixed='${suffixed}'`,\n );\n this.idOwner.set(suffixed, rawKey);\n return suffixed;\n }\n this.idOwner.set(id, rawKey);\n return id;\n }\n\n /**\n * v0.4.2 (S3): build a stable raw-key for collision tracking.\n *\n * @param delivery The delivery whose raw tracking identifies it.\n */\n private static rawIdKey(delivery: ParcelDelivery): string {\n const t = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const e = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n return `${t}\\u0000${e}`;\n }\n\n /**\n * v0.4.2 (S3): FNV-1a 32-bit short hash \u2192 6 hex chars.\n *\n * @param s Input string to hash.\n */\n private static shortHash(s: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, \"0\").slice(0, 6);\n }\n\n /**\n * v0.4.2 (S3): reset the per-poll collision tracker. Call from main.ts\n * before iterating deliveries so the bare id always wins for the first\n * occurrence in each poll.\n */\n resetPollState(): void {\n this.idOwner.clear();\n }\n\n /**\n * Extract the package-id segment from a relative object id\n * (`deliveries.<pkgId>` or `deliveries.<pkgId>.<state>`); \"\" when the id is\n * outside the deliveries tree. Single source for the id-schema knowledge\n * (v0.10.0, L15).\n *\n * @param relativeId Object id relative to the adapter namespace\n */\n private static pkgIdOf(relativeId: string): string {\n return relativeId.startsWith(\"deliveries.\") ? relativeId.slice(\"deliveries.\".length).split(\".\")[0] : \"\";\n }\n\n /**\n * Update or create all states for a delivery.\n *\n * @param delivery The delivery data from API\n * @param carrierName Resolved carrier display name\n * @param pkgId Package id from the caller's deterministic pre-pass \u2014 always\n * computed via `packageId()` so the collision suffixing stays deterministic\n * (v0.10.0, L11: no test-only fallback path anymore).\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string, pkgId: string): Promise<void> {\n const devicePath = `deliveries.${pkgId}`;\n\n const description = typeof delivery.description === \"string\" ? delivery.description : \"\";\n const trackingNumber = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const extraInfo = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n\n // v0.10.0 (DP-5): ensure the device object once per process. `preserve:\n // name` keeps an existing name (user renames win), so the name \u2014 localized\n // fallback when the API sends no description (L18) \u2014 only matters at\n // first creation; the former per-change rewrite never had a visible effect.\n if (!this.deviceEnsured.has(pkgId)) {\n await this.adapter.extendObject(\n devicePath,\n {\n type: \"device\",\n common: {\n name: description || packageName(trackingNumber || pkgId),\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n this.deviceEnsured.add(pkgId);\n }\n this.knownDeliveryIds?.add(pkgId);\n\n const statusCode = this.parseStatus(delivery);\n let statusText = statusLabel(statusCode);\n if (statusText === undefined) {\n // v0.4.3 (E3): trace unknown status-code (API drift). A future\n // parcel.app status (e.g. 9, 10) would render as \"Unknown (N)\"\n // without any log clue that the label table is out of date.\n this.adapter.log.debug(`status code ${statusCode} has no status_* label, using fallback`);\n statusText = `Unknown (${statusCode})`;\n }\n\n const deliveryWindow = this.calculateDeliveryWindow(delivery, statusCode);\n const deliveryEstimate = this.calculateDeliveryEstimate(delivery, statusCode);\n const lastEvent = this.formatLastEvent(delivery);\n const lastLocation = this.extractLastLocation(delivery);\n\n // v0.10.0 (M5): ONE definition list drives the writes AND the lastUpdated\n // decision \u2014 the former parallel JSON.stringify signature array was a\n // silent drift trap (a new field added to one list but not the other).\n const stateDefs: [\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ][] = [\n [`${devicePath}.carrier`, tName(\"carrier\"), \"string\", \"text\", carrierName],\n [`${devicePath}.status`, tName(\"status\"), \"string\", \"text\", statusText],\n [`${devicePath}.statusCode`, tName(\"statusCode\"), \"number\", \"value\", statusCode],\n [`${devicePath}.description`, tName(\"description\"), \"string\", \"text\", description],\n [`${devicePath}.trackingNumber`, tName(\"trackingNumber\"), \"string\", \"text\", trackingNumber],\n [`${devicePath}.extraInfo`, tName(\"extraInfo\"), \"string\", \"text\", extraInfo],\n [`${devicePath}.deliveryWindow`, tName(\"deliveryWindow\"), \"string\", \"text\", deliveryWindow],\n [`${devicePath}.deliveryEstimate`, tName(\"deliveryEstimate\"), \"string\", \"text\", deliveryEstimate],\n [`${devicePath}.lastEvent`, tName(\"lastEvent\"), \"string\", \"text\", lastEvent],\n [`${devicePath}.lastLocation`, tName(\"lastLocation\"), \"string\", \"text\", lastLocation],\n ];\n const changed = await Promise.all(\n stateDefs.map(([id, name, type, role, val]) => this.createAndSet(id, name, type, role, val)),\n );\n\n // v0.10.0 (M5): `lastUpdated` = \"when the tracking data last CHANGED\".\n // The decision now rides on the broker's own setStateChanged answer\n // (notChanged=false \u21D2 a sibling value really differed in the DB), so an\n // adapter restart no longer stamps every package with a fresh timestamp \u2014\n // the old in-memory signature map always missed on the first poll after a\n // restart, and it was updated BEFORE its write survived (ASYNC-6).\n if (changed.some(Boolean)) {\n await this.createAndSet(\n `${devicePath}.lastUpdated`,\n tName(\"lastUpdated\"),\n \"string\",\n \"date\",\n new Date().toISOString(),\n );\n }\n }\n\n /**\n * Update summary states. Expects already-filtered active deliveries.\n * The `summary` channel itself is declared via io-package.json instanceObjects.\n *\n * @param activeDeliveries Only active (non-delivered) deliveries\n */\n async updateSummary(activeDeliveries: ParcelDelivery[]): Promise<void> {\n const todayDeliveries = activeDeliveries.filter(d => this.isToday(d, this.parseStatus(d)));\n // v0.4.3 (E1): trace summary refresh \u2014 ~144/day at the default poll\n // interval, kept short (counts only).\n this.adapter.log.debug(\n `updateSummary: ${activeDeliveries.length} active, ${todayDeliveries.length} expected today`,\n );\n\n await Promise.all([\n this.createAndSet(\"summary.activeCount\", tName(\"activeCount\"), \"number\", \"value\", activeDeliveries.length),\n this.createAndSet(\"summary.todayCount\", tName(\"todayCount\"), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n tName(\"summaryDeliveryWindow\"),\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer present in the API response.\n *\n * @param keepIds Package IDs the API still returns this poll (kept). Every\n * currently-known delivery NOT in this set is removed. The caller passes\n * ALL visible package ids, not only the ones whose state-write succeeded \u2014\n * a transient write failure must not delete a still-present package.\n */\n async cleanupDeliveries(keepIds: string[]): Promise<void> {\n // v0.7.2: the object view is queried only ONCE after adapter start to\n // reconcile leftovers from previous runs; afterwards the in-memory set\n // (maintained by updateDelivery + this prune) replaces the per-poll DB\n // round-trip.\n if (this.knownDeliveryIds === null) {\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.${ID_RANGE_END}`,\n });\n if (!objects?.rows) {\n // v0.4.3 (E2): trace the no-op path \u2014 happens on fresh installs or\n // when getObjectViewAsync returns falsy. Without this the early-return\n // is invisible (and the known-set stays unseeded for the next poll).\n this.adapter.log.debug(\"cleanupDeliveries: no objects view available, skipping\");\n return;\n }\n this.knownDeliveryIds = new Set<string>();\n for (const row of objects.rows) {\n // The range query guarantees the namespace prefix \u2014 cut it instead of\n // pattern-replacing (v0.10.0, KISS-13).\n const pkgId = StateManager.pkgIdOf(row.id.slice(this.adapter.namespace.length + 1));\n if (pkgId) {\n this.knownDeliveryIds.add(pkgId);\n }\n }\n }\n\n const keepSet = new Set(keepIds);\n // v0.4.2 (S1): collect first, then delete in parallel \u2014 capped in batches\n // (v0.10.0, I2) like the update fan-out in main.ts.\n const toDelete = [...this.knownDeliveryIds].filter(pkgId => !keepSet.has(pkgId));\n const toDeleteSet = new Set(toDelete);\n\n for (let start = 0; start < toDelete.length; start += DELETE_BATCH_SIZE) {\n const batch = toDelete.slice(start, start + DELETE_BATCH_SIZE);\n await Promise.all(\n batch.map(async pkgId => {\n const relativeId = `deliveries.${pkgId}`;\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n this.deviceEnsured.delete(pkgId);\n }),\n );\n }\n\n // v0.9.0 (S2): prune createdIds for every removed package in ONE pass over\n // the set \u2014 O(created). A createdId is `deliveries.<pkgId>` or\n // `deliveries.<pkgId>.<state>` \u2014 extract the pkgId and drop it if removed.\n if (toDeleteSet.size > 0) {\n for (const id of [...this.createdIds]) {\n if (toDeleteSet.has(StateManager.pkgIdOf(id))) {\n this.createdIds.delete(id);\n }\n }\n }\n this.knownDeliveryIds = new Set(keepSet);\n }\n\n /**\n * Parse a parcel.app expected-date string to LOCAL epoch-millis.\n *\n * The API delivers `date_expected`/`date_expected_end` \"without specific\n * timezone information\"; parse with explicit local calendar components so the\n * value lands on the intended local day/time (`new Date(\"YYYY-MM-DD\")` would\n * be UTC midnight). `hasTime` is false for a bare date or a midnight time\n * (a day, not an hour-window). Ambiguous carrier formats (dotted, weekday\n * names) are deliberately NOT guessed \u2014 they return null rather than risk a\n * wrong date.\n *\n * @param value Raw date/time string from the API\n */\n private static parseExpectedToMs(value: unknown): { ms: number; hasTime: boolean } | null {\n if (typeof value !== \"string\") {\n return null;\n }\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/.exec(value.trim());\n if (!m) {\n return null;\n }\n const hasClock = m[4] !== undefined;\n const year = Number(m[1]);\n const month = Number(m[2]); // 1-12\n const day = Number(m[3]);\n const hour = hasClock ? Number(m[4]) : 0;\n const min = hasClock ? Number(m[5]) : 0;\n const sec = m[6] !== undefined ? Number(m[6]) : 0;\n // Range-validate the components. The regex only checks digit COUNT, not\n // value range, and `new Date(2026, 12, 40, 25, \u2026)` silently ROLLS OVER to a\n // wrong date (getTime() is NOT NaN). Reject out-of-range rather than guess.\n if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 || sec > 59) {\n return null;\n }\n const date = new Date(year, month - 1, day, hour, min, sec);\n // Catch day-of-month overflow the range check misses (Feb 30, Apr 31, \u2026):\n // a real date round-trips the month and day it was built from.\n if (Number.isNaN(date.getTime()) || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n const hasTime = hasClock && !(hour === 0 && min === 0 && sec === 0);\n return { ms: date.getTime(), hasTime };\n }\n\n /**\n * Resolve a delivery's expected window to epoch-millis bounds. Returns null\n * for non-trackable status or when there is no usable start time.\n *\n * Prefers the Unix timestamp fields; for carriers that report the window only\n * as a date/time string (`date_expected`/`date_expected_end`) it falls back to\n * those \u2014 but only when the string carries a real time-of-day (a bare date or\n * midnight is a day, not an hour-window). Carrier-agnostic.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private windowBoundsMs(delivery: ParcelDelivery, statusCode: number): { start: number; end: number | null } | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n const toMs = (timestamp: unknown): number | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const ms = ts * 1000;\n return Number.isNaN(new Date(ms).getTime()) ? null : ms;\n };\n const dateMs = (value: unknown): number | null => {\n const parsed = StateManager.parseExpectedToMs(value);\n return parsed && parsed.hasTime ? parsed.ms : null;\n };\n const start = toMs(delivery.timestamp_expected) ?? dateMs(delivery.date_expected);\n if (start === null) {\n return null;\n }\n const end = toMs(delivery.timestamp_expected_end) ?? dateMs(delivery.date_expected_end);\n return { start, end };\n }\n\n /**\n * Format epoch-millis as local HH:MM.\n *\n * @param ms Epoch milliseconds\n */\n private static formatHHMM(ms: number): string {\n const d = new Date(ms);\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n }\n\n /**\n * Local \"MM-DD HH:MM\" \u2014 used when a window spans more than one calendar day.\n *\n * @param ms Epoch milliseconds\n */\n private static formatDateHHMM(ms: number): string {\n const d = new Date(ms);\n const mm = (d.getMonth() + 1).toString().padStart(2, \"0\");\n const dd = d.getDate().toString().padStart(2, \"0\");\n return `${mm}-${dd} ${StateManager.formatHHMM(ms)}`;\n }\n\n /**\n * Whether two epoch-millis fall on the same LOCAL calendar day.\n *\n * @param aMs First epoch milliseconds\n * @param bMs Second epoch milliseconds\n */\n private static sameLocalDay(aMs: number, bMs: number): boolean {\n const a = new Date(aMs);\n const b = new Date(bMs);\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n }\n\n /**\n * Format a start\u2192end window as a local string. A real end (> start) on the\n * SAME day renders \"HH:MM - HH:MM\"; an end on a LATER day carries the date on\n * both sides (\"12-06 14:30 - 12-08 18:30\") so a multi-day window is not shown\n * as if it were same-day. No end, or an end <= start (reversed/equal), renders\n * just the start.\n *\n * @param startMs Window start (epoch ms)\n * @param endMs Window end (epoch ms) or null\n */\n private static formatWindow(startMs: number, endMs: number | null): string {\n if (endMs === null || endMs <= startMs) {\n return StateManager.formatHHMM(startMs);\n }\n return StateManager.sameLocalDay(startMs, endMs)\n ? `${StateManager.formatHHMM(startMs)} - ${StateManager.formatHHMM(endMs)}`\n : `${StateManager.formatDateHHMM(startMs)} - ${StateManager.formatDateHHMM(endMs)}`;\n }\n\n /**\n * Calculate a delivery time-window string from the resolved expected bounds.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(delivery: ParcelDelivery, statusCode: number): string {\n const bounds = this.windowBoundsMs(delivery, statusCode);\n if (!bounds) {\n return \"\";\n }\n return StateManager.formatWindow(bounds.start, bounds.end);\n }\n\n /**\n * Days from today to the expected delivery date. Returns null when the\n * delivery has no usable expected date or is in a non-trackable status.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private computeDiffDays(delivery: ParcelDelivery, statusCode: number): number | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n\n let expectedDate: Date | null = null;\n const ts = coerceFiniteNumber(delivery.timestamp_expected);\n if (ts !== null && ts > 0) {\n expectedDate = new Date(ts * 1000);\n } else {\n // Shares the window's date parser (one source of format-truth). Only the\n // calendar day matters here, so the time-of-day flag is ignored; the\n // local-component parse keeps the day timezone-stable.\n const parsed = StateManager.parseExpectedToMs(delivery.date_expected);\n expectedDate = parsed ? new Date(parsed.ms) : null;\n }\n\n if (!expectedDate || Number.isNaN(expectedDate.getTime())) {\n return null;\n }\n\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const expectedStart = new Date(expectedDate.getFullYear(), expectedDate.getMonth(), expectedDate.getDate());\n return Math.round((expectedStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24));\n }\n\n /**\n * Calculate human-readable delivery estimate.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryEstimate(delivery: ParcelDelivery, statusCode: number): string {\n const diffDays = this.computeDiffDays(delivery, statusCode);\n if (diffDays === null) {\n return \"\";\n }\n if (diffDays < 0) {\n return tText(\"estimateOverdue\");\n }\n if (diffDays === 0) {\n return tText(\"estimateToday\");\n }\n if (diffDays === 1) {\n return tText(\"estimateTomorrow\");\n }\n return tText(\"estimateDays\", diffDays);\n }\n\n /**\n * Whether the delivery is expected today. Language-agnostic, used by the\n * summary filter so `todayCount` works across all languages.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private isToday(delivery: ParcelDelivery, statusCode: number): boolean {\n return this.computeDiffDays(delivery, statusCode) === 0;\n }\n\n private getLatestEvent(delivery: ParcelDelivery): ParcelEvent | null {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return null;\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return null;\n }\n return latest;\n }\n\n private formatLastEvent(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\n return \"\";\n }\n const parts: string[] = [];\n if (typeof latest.event === \"string\" && latest.event.length > 0) {\n parts.push(latest.event);\n }\n if (typeof latest.date === \"string\" && latest.date.length > 0) {\n parts.push(latest.date);\n }\n return parts.join(\" - \");\n }\n\n private extractLastLocation(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Combined delivery window for today's packages: earliest start to latest\n * end across all windows. Computed from the raw millis (not the formatted\n * strings) so the latest end always wins \u2014 fixes the earlier bug where the\n * end of the latest-*starting* window was used instead of the maximum end.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const bounds = todayDeliveries\n .map(d => this.windowBoundsMs(d, this.parseStatus(d)))\n .filter((b): b is { start: number; end: number | null } => b !== null);\n\n if (bounds.length === 0) {\n return \"\";\n }\n\n // L3: fold instead of Math.min/max(...spread). The bounds array is capped\n // by the 1 MiB response limit, but a spread over a large array can still hit\n // V8's argument-count limit (RangeError); reduce is O(n) and unbounded-safe\n // \u2014 consistent with beszel's computeMaxTemp hardening.\n const minStart = bounds.reduce((m, b) => (b.start < m ? b.start : m), bounds[0].start);\n const maxEnd = bounds.reduce((m, b) => {\n const e = b.end ?? b.start;\n return e > m ? e : m;\n }, bounds[0].end ?? bounds[0].start);\n return StateManager.formatWindow(minStart, maxEnd);\n }\n\n /**\n * Create/extend a read-only state and set its value. Skips the\n * `setObjectNotExistsAsync` round-trip once the ID is in the cache \u2014\n * states are static after first creation; only the value changes per poll.\n *\n * @param id State ID relative to adapter namespace\n * @param name Display name (translation object or plain string)\n * @param type Value type\n * @param role ioBroker role\n * @param val Value to set\n * @returns true when the broker actually wrote the value (it differed or the\n * state was new) \u2014 the DB-backed \"did anything change\" signal driving\n * `lastUpdated` (v0.10.0, M5)\n */\n private async createAndSet(\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ): Promise<boolean> {\n if (!this.createdIds.has(id)) {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: { name, type, role, read: true, write: false },\n native: {},\n });\n this.createdIds.add(id);\n }\n // The bundled @iobroker/types 7.1.2 types this promise as `string`, but\n // js-controller \u22657.2.2 (our dependency floor) resolves { id, notChanged }\n // \u2014 verified at v7.2.2: adapter.ts invokes the callback with\n // (null, res.id, res.notChanged) and tools.promisify(['id','notChanged'])\n // builds the object from exactly these named args. Narrow locally instead\n // of trusting the stale published type.\n const result: unknown = await this.adapter.setStateChangedAsync(id, { val, ack: true });\n // Only an explicit notChanged=false counts as a write \u2014 anything else\n // (missing field, drifted runtime) must not fake \"changed\" on every poll.\n return typeof result === \"object\" && result !== null && (result as { notChanged?: unknown }).notChanged === false;\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAA4C;AAC5C,kBAAuD;AAEvD,mBAAoC;AAGpC,MAAM,qBAAqB,oBAAI,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;AAM5C,MAAM,eAAe;AAGrB,MAAM,gBAAgB;AAOtB,MAAM,oBAAoB;AAGnB,MAAM,aAAa;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQS,aAAa,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS7B,gBAAgB,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQzC,mBAAuC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO9B,UAAU,oBAAI,IAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWlC,aAAa,oBAAI,QAAgC;AAAA;AAAA;AAAA;AAAA,EAKlE,YAAY,SAA0B;AACpC,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,SAAS,MAAuB;AAC9B,QAAI,OAAO,SAAS,UAAU;AAC5B,aAAO;AAAA,IACT;AACA,WACE,KACG,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,aAAa,KAAK;AAAA,EAElC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,YAAY,UAAkC;AAG5C,UAAM,WAAW,KAAK,WAAW,IAAI,QAAQ;AAC7C,QAAI,aAAa,QAAW;AAC1B,aAAO;AAAA,IACT;AACA,UAAM,OAAO,KAAK,cAAc,QAAQ;AACxC,SAAK,WAAW,IAAI,UAAU,IAAI;AAClC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,cAAc,UAAkC;AACtD,UAAM,MAAM,SAAS;AACrB,QAAI,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,GAAG;AACnD,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB;AACA,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAM,IAAI,SAAS,KAAK,EAAE;AAC1B,UAAI,OAAO,SAAS,CAAC,GAAG;AACtB,eAAO;AAAA,MACT;AAAA,IACF;AAKA,SAAK,QAAQ,IAAI;AAAA,MACf,sBAAsB,KAAK,UAAU,GAAG,CAAC,UAAU,OAAO,GAAG,YAAO,gCAAmB;AAAA,IACzF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,UAAU,UAAkC;AAC1C,QAAI,KAAK,KAAK,SAAS,SAAS,eAAe;AAE/C,QAAI,OAAO,SAAS,sBAAsB,YAAY,SAAS,kBAAkB,SAAS,GAAG;AAC3F,YAAM,IAAI,KAAK,SAAS,SAAS,iBAAiB,CAAC;AAAA,IACrD;AAIA,UAAM,QAAQ,KAAK,QAAQ,IAAI,EAAE;AACjC,UAAM,SAAS,aAAa,SAAS,QAAQ;AAC7C,QAAI,UAAU,UAAa,UAAU,QAAQ;AAC3C,YAAM,WAAW,GAAG,EAAE,KAAK,aAAa,UAAU,MAAM,CAAC;AAGzD,WAAK,QAAQ,IAAI;AAAA,QACf,8BAA8B,EAAE,gBAAY,uBAAQ,KAAK,CAAC,cAAU,uBAAQ,MAAM,CAAC,sBAAiB,QAAQ;AAAA,MAC9G;AACA,WAAK,QAAQ,IAAI,UAAU,MAAM;AACjC,aAAO;AAAA,IACT;AACA,SAAK,QAAQ,IAAI,IAAI,MAAM;AAC3B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAe,SAAS,UAAkC;AACxD,UAAM,IAAI,OAAO,SAAS,oBAAoB,WAAW,SAAS,kBAAkB;AACpF,UAAM,IAAI,OAAO,SAAS,sBAAsB,WAAW,SAAS,oBAAoB;AACxF,WAAO,GAAG,CAAC,KAAS,CAAC;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAe,UAAU,GAAmB;AAC1C,QAAI,IAAI;AACR,aAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,WAAK,EAAE,WAAW,CAAC;AACnB,UAAI,KAAK,KAAK,GAAG,QAAU;AAAA,IAC7B;AACA,YAAQ,MAAM,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,EAAE,MAAM,GAAG,CAAC;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAuB;AACrB,SAAK,QAAQ,MAAM;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,OAAe,QAAQ,YAA4B;AACjD,WAAO,WAAW,WAAW,aAAa,IAAI,WAAW,MAAM,cAAc,MAAM,EAAE,MAAM,GAAG,EAAE,CAAC,IAAI;AAAA,EACvG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,eAAe,UAA0B,aAAqB,OAA8B;AA1OpG;AA2OI,UAAM,aAAa,cAAc,KAAK;AAEtC,UAAM,cAAc,OAAO,SAAS,gBAAgB,WAAW,SAAS,cAAc;AACtF,UAAM,iBAAiB,OAAO,SAAS,oBAAoB,WAAW,SAAS,kBAAkB;AACjG,UAAM,YAAY,OAAO,SAAS,sBAAsB,WAAW,SAAS,oBAAoB;AAMhG,QAAI,CAAC,KAAK,cAAc,IAAI,KAAK,GAAG;AAClC,YAAM,KAAK,QAAQ;AAAA,QACjB;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,QAAQ;AAAA,YACN,MAAM,mBAAe,yBAAY,kBAAkB,KAAK;AAAA,UAC1D;AAAA,UACA,QAAQ,CAAC;AAAA,QACX;AAAA,QACA,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE;AAAA,MACnC;AACA,WAAK,cAAc,IAAI,KAAK;AAAA,IAC9B;AACA,eAAK,qBAAL,mBAAuB,IAAI;AAE3B,UAAM,aAAa,KAAK,YAAY,QAAQ;AAC5C,QAAI,iBAAa,yBAAY,UAAU;AACvC,QAAI,eAAe,QAAW;AAI5B,WAAK,QAAQ,IAAI,MAAM,eAAe,UAAU,wCAAwC;AACxF,mBAAa,YAAY,UAAU;AAAA,IACrC;AAEA,UAAM,iBAAiB,KAAK,wBAAwB,UAAU,UAAU;AACxE,UAAM,mBAAmB,KAAK,0BAA0B,UAAU,UAAU;AAC5E,UAAM,YAAY,KAAK,gBAAgB,QAAQ;AAC/C,UAAM,eAAe,KAAK,oBAAoB,QAAQ;AAKtD,UAAM,YAMA;AAAA,MACJ,CAAC,GAAG,UAAU,gBAAY,mBAAM,SAAS,GAAG,UAAU,QAAQ,WAAW;AAAA,MACzE,CAAC,GAAG,UAAU,eAAW,mBAAM,QAAQ,GAAG,UAAU,QAAQ,UAAU;AAAA,MACtE,CAAC,GAAG,UAAU,mBAAe,mBAAM,YAAY,GAAG,UAAU,SAAS,UAAU;AAAA,MAC/E,CAAC,GAAG,UAAU,oBAAgB,mBAAM,aAAa,GAAG,UAAU,QAAQ,WAAW;AAAA,MACjF,CAAC,GAAG,UAAU,uBAAmB,mBAAM,gBAAgB,GAAG,UAAU,QAAQ,cAAc;AAAA,MAC1F,CAAC,GAAG,UAAU,kBAAc,mBAAM,WAAW,GAAG,UAAU,QAAQ,SAAS;AAAA,MAC3E,CAAC,GAAG,UAAU,uBAAmB,mBAAM,gBAAgB,GAAG,UAAU,QAAQ,cAAc;AAAA,MAC1F,CAAC,GAAG,UAAU,yBAAqB,mBAAM,kBAAkB,GAAG,UAAU,QAAQ,gBAAgB;AAAA,MAChG,CAAC,GAAG,UAAU,kBAAc,mBAAM,WAAW,GAAG,UAAU,QAAQ,SAAS;AAAA,MAC3E,CAAC,GAAG,UAAU,qBAAiB,mBAAM,cAAc,GAAG,UAAU,QAAQ,YAAY;AAAA,IACtF;AACA,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,UAAU,IAAI,CAAC,CAAC,IAAI,MAAM,MAAM,MAAM,GAAG,MAAM,KAAK,aAAa,IAAI,MAAM,MAAM,MAAM,GAAG,CAAC;AAAA,IAC7F;AAQA,QAAI,QAAQ,KAAK,OAAO,GAAG;AACzB,YAAM,KAAK;AAAA,QACT,GAAG,UAAU;AAAA,YACb,mBAAM,aAAa;AAAA,QACnB;AAAA,QACA;AAAA,SACA,oBAAI,KAAK,GAAE,YAAY;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,cAAc,kBAAmD;AACrE,UAAM,kBAAkB,iBAAiB,OAAO,OAAK,KAAK,QAAQ,GAAG,KAAK,YAAY,CAAC,CAAC,CAAC;AAGzF,SAAK,QAAQ,IAAI;AAAA,MACf,kBAAkB,iBAAiB,MAAM,YAAY,gBAAgB,MAAM;AAAA,IAC7E;AAEA,UAAM,QAAQ,IAAI;AAAA,MAChB,KAAK,aAAa,2BAAuB,mBAAM,aAAa,GAAG,UAAU,SAAS,iBAAiB,MAAM;AAAA,MACzG,KAAK,aAAa,0BAAsB,mBAAM,YAAY,GAAG,UAAU,SAAS,gBAAgB,MAAM;AAAA,MACtG,KAAK;AAAA,QACH;AAAA,YACA,mBAAM,uBAAuB;AAAA,QAC7B;AAAA,QACA;AAAA,QACA,KAAK,wBAAwB,eAAe;AAAA,MAC9C;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,kBAAkB,SAAkC;AAKxD,QAAI,KAAK,qBAAqB,MAAM;AAClC,YAAM,UAAU,MAAM,KAAK,QAAQ,mBAAmB,UAAU,UAAU;AAAA,QACxE,UAAU,GAAG,KAAK,QAAQ,SAAS;AAAA,QACnC,QAAQ,GAAG,KAAK,QAAQ,SAAS,eAAe,YAAY;AAAA,MAC9D,CAAC;AACD,UAAI,EAAC,mCAAS,OAAM;AAIlB,aAAK,QAAQ,IAAI,MAAM,wDAAwD;AAC/E;AAAA,MACF;AACA,WAAK,mBAAmB,oBAAI,IAAY;AACxC,iBAAW,OAAO,QAAQ,MAAM;AAG9B,cAAM,QAAQ,aAAa,QAAQ,IAAI,GAAG,MAAM,KAAK,QAAQ,UAAU,SAAS,CAAC,CAAC;AAClF,YAAI,OAAO;AACT,eAAK,iBAAiB,IAAI,KAAK;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAU,IAAI,IAAI,OAAO;AAG/B,UAAM,WAAW,CAAC,GAAG,KAAK,gBAAgB,EAAE,OAAO,WAAS,CAAC,QAAQ,IAAI,KAAK,CAAC;AAC/E,UAAM,cAAc,IAAI,IAAI,QAAQ;AAEpC,aAAS,QAAQ,GAAG,QAAQ,SAAS,QAAQ,SAAS,mBAAmB;AACvE,YAAM,QAAQ,SAAS,MAAM,OAAO,QAAQ,iBAAiB;AAC7D,YAAM,QAAQ;AAAA,QACZ,MAAM,IAAI,OAAM,UAAS;AACvB,gBAAM,aAAa,cAAc,KAAK;AACtC,gBAAM,KAAK,QAAQ,eAAe,YAAY,EAAE,WAAW,KAAK,CAAC;AACjE,eAAK,QAAQ,IAAI,MAAM,2BAA2B,UAAU,EAAE;AAC9D,eAAK,cAAc,OAAO,KAAK;AAAA,QACjC,CAAC;AAAA,MACH;AAAA,IACF;AAKA,QAAI,YAAY,OAAO,GAAG;AACxB,iBAAW,MAAM,CAAC,GAAG,KAAK,UAAU,GAAG;AACrC,YAAI,YAAY,IAAI,aAAa,QAAQ,EAAE,CAAC,GAAG;AAC7C,eAAK,WAAW,OAAO,EAAE;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AACA,SAAK,mBAAmB,IAAI,IAAI,OAAO;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,OAAe,kBAAkB,OAAyD;AACxF,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO;AAAA,IACT;AACA,UAAM,IAAI,iEAAiE,KAAK,MAAM,KAAK,CAAC;AAC5F,QAAI,CAAC,GAAG;AACN,aAAO;AAAA,IACT;AACA,UAAM,WAAW,EAAE,CAAC,MAAM;AAC1B,UAAM,OAAO,OAAO,EAAE,CAAC,CAAC;AACxB,UAAM,QAAQ,OAAO,EAAE,CAAC,CAAC;AACzB,UAAM,MAAM,OAAO,EAAE,CAAC,CAAC;AACvB,UAAM,OAAO,WAAW,OAAO,EAAE,CAAC,CAAC,IAAI;AACvC,UAAM,MAAM,WAAW,OAAO,EAAE,CAAC,CAAC,IAAI;AACtC,UAAM,MAAM,EAAE,CAAC,MAAM,SAAY,OAAO,EAAE,CAAC,CAAC,IAAI;AAIhD,QAAI,QAAQ,KAAK,QAAQ,MAAM,MAAM,KAAK,MAAM,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,IAAI;AACvF,aAAO;AAAA,IACT;AACA,UAAM,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG,KAAK,MAAM,KAAK,GAAG;AAG1D,QAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,KAAK,KAAK,SAAS,MAAM,QAAQ,KAAK,KAAK,QAAQ,MAAM,KAAK;AAC3F,aAAO;AAAA,IACT;AACA,UAAM,UAAU,YAAY,EAAE,SAAS,KAAK,QAAQ,KAAK,QAAQ;AACjE,WAAO,EAAE,IAAI,KAAK,QAAQ,GAAG,QAAQ;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcQ,eAAe,UAA0B,YAAkE;AApdrH;AAqdI,QAAI,CAAC,mBAAmB,IAAI,UAAU,GAAG;AACvC,aAAO;AAAA,IACT;AACA,UAAM,OAAO,CAAC,cAAsC;AAClD,YAAM,SAAK,kCAAmB,SAAS;AACvC,UAAI,OAAO,QAAQ,MAAM,GAAG;AAC1B,eAAO;AAAA,MACT;AACA,YAAM,KAAK,KAAK;AAChB,aAAO,OAAO,MAAM,IAAI,KAAK,EAAE,EAAE,QAAQ,CAAC,IAAI,OAAO;AAAA,IACvD;AACA,UAAM,SAAS,CAAC,UAAkC;AAChD,YAAM,SAAS,aAAa,kBAAkB,KAAK;AACnD,aAAO,UAAU,OAAO,UAAU,OAAO,KAAK;AAAA,IAChD;AACA,UAAM,SAAQ,UAAK,SAAS,kBAAkB,MAAhC,YAAqC,OAAO,SAAS,aAAa;AAChF,QAAI,UAAU,MAAM;AAClB,aAAO;AAAA,IACT;AACA,UAAM,OAAM,UAAK,SAAS,sBAAsB,MAApC,YAAyC,OAAO,SAAS,iBAAiB;AACtF,WAAO,EAAE,OAAO,IAAI;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAe,WAAW,IAAoB;AAC5C,UAAM,IAAI,IAAI,KAAK,EAAE;AACrB,WAAO,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,EAClG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAe,eAAe,IAAoB;AAChD,UAAM,IAAI,IAAI,KAAK,EAAE;AACrB,UAAM,MAAM,EAAE,SAAS,IAAI,GAAG,SAAS,EAAE,SAAS,GAAG,GAAG;AACxD,UAAM,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG;AACjD,WAAO,GAAG,EAAE,IAAI,EAAE,IAAI,aAAa,WAAW,EAAE,CAAC;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAe,aAAa,KAAa,KAAsB;AAC7D,UAAM,IAAI,IAAI,KAAK,GAAG;AACtB,UAAM,IAAI,IAAI,KAAK,GAAG;AACtB,WAAO,EAAE,YAAY,MAAM,EAAE,YAAY,KAAK,EAAE,SAAS,MAAM,EAAE,SAAS,KAAK,EAAE,QAAQ,MAAM,EAAE,QAAQ;AAAA,EAC3G;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,OAAe,aAAa,SAAiB,OAA8B;AACzE,QAAI,UAAU,QAAQ,SAAS,SAAS;AACtC,aAAO,aAAa,WAAW,OAAO;AAAA,IACxC;AACA,WAAO,aAAa,aAAa,SAAS,KAAK,IAC3C,GAAG,aAAa,WAAW,OAAO,CAAC,MAAM,aAAa,WAAW,KAAK,CAAC,KACvE,GAAG,aAAa,eAAe,OAAO,CAAC,MAAM,aAAa,eAAe,KAAK,CAAC;AAAA,EACrF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,wBAAwB,UAA0B,YAA4B;AACpF,UAAM,SAAS,KAAK,eAAe,UAAU,UAAU;AACvD,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AACA,WAAO,aAAa,aAAa,OAAO,OAAO,OAAO,GAAG;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,gBAAgB,UAA0B,YAAmC;AACnF,QAAI,CAAC,mBAAmB,IAAI,UAAU,GAAG;AACvC,aAAO;AAAA,IACT;AAEA,QAAI,eAA4B;AAChC,UAAM,SAAK,kCAAmB,SAAS,kBAAkB;AACzD,QAAI,OAAO,QAAQ,KAAK,GAAG;AACzB,qBAAe,IAAI,KAAK,KAAK,GAAI;AAAA,IACnC,OAAO;AAIL,YAAM,SAAS,aAAa,kBAAkB,SAAS,aAAa;AACpE,qBAAe,SAAS,IAAI,KAAK,OAAO,EAAE,IAAI;AAAA,IAChD;AAEA,QAAI,CAAC,gBAAgB,OAAO,MAAM,aAAa,QAAQ,CAAC,GAAG;AACzD,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,aAAa,IAAI,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,GAAG,IAAI,QAAQ,CAAC;AAC5E,UAAM,gBAAgB,IAAI,KAAK,aAAa,YAAY,GAAG,aAAa,SAAS,GAAG,aAAa,QAAQ,CAAC;AAC1G,WAAO,KAAK,OAAO,cAAc,QAAQ,IAAI,WAAW,QAAQ,MAAM,MAAO,KAAK,KAAK,GAAG;AAAA,EAC5F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,0BAA0B,UAA0B,YAA4B;AACtF,UAAM,WAAW,KAAK,gBAAgB,UAAU,UAAU;AAC1D,QAAI,aAAa,MAAM;AACrB,aAAO;AAAA,IACT;AACA,QAAI,WAAW,GAAG;AAChB,iBAAO,mBAAM,iBAAiB;AAAA,IAChC;AACA,QAAI,aAAa,GAAG;AAClB,iBAAO,mBAAM,eAAe;AAAA,IAC9B;AACA,QAAI,aAAa,GAAG;AAClB,iBAAO,mBAAM,kBAAkB;AAAA,IACjC;AACA,eAAO,mBAAM,gBAAgB,QAAQ;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,QAAQ,UAA0B,YAA6B;AACrE,WAAO,KAAK,gBAAgB,UAAU,UAAU,MAAM;AAAA,EACxD;AAAA,EAEQ,eAAe,UAA8C;AACnE,QAAI,CAAC,MAAM,QAAQ,SAAS,MAAM,KAAK,SAAS,OAAO,WAAW,GAAG;AACnE,aAAO;AAAA,IACT;AACA,UAAM,SAAS,SAAS,OAAO,CAAC;AAChC,QAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,gBAAgB,UAAkC;AACxD,UAAM,SAAS,KAAK,eAAe,QAAQ;AAC3C,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AACA,UAAM,QAAkB,CAAC;AACzB,QAAI,OAAO,OAAO,UAAU,YAAY,OAAO,MAAM,SAAS,GAAG;AAC/D,YAAM,KAAK,OAAO,KAAK;AAAA,IACzB;AACA,QAAI,OAAO,OAAO,SAAS,YAAY,OAAO,KAAK,SAAS,GAAG;AAC7D,YAAM,KAAK,OAAO,IAAI;AAAA,IACxB;AACA,WAAO,MAAM,KAAK,KAAK;AAAA,EACzB;AAAA,EAEQ,oBAAoB,UAAkC;AAC5D,UAAM,SAAS,KAAK,eAAe,QAAQ;AAC3C,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AACA,WAAO,OAAO,OAAO,aAAa,WAAW,OAAO,WAAW;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,wBAAwB,iBAA2C;AA7pB7E;AA8pBI,UAAM,SAAS,gBACZ,IAAI,OAAK,KAAK,eAAe,GAAG,KAAK,YAAY,CAAC,CAAC,CAAC,EACpD,OAAO,CAAC,MAAkD,MAAM,IAAI;AAEvE,QAAI,OAAO,WAAW,GAAG;AACvB,aAAO;AAAA,IACT;AAMA,UAAM,WAAW,OAAO,OAAO,CAAC,GAAG,MAAO,EAAE,QAAQ,IAAI,EAAE,QAAQ,GAAI,OAAO,CAAC,EAAE,KAAK;AACrF,UAAM,SAAS,OAAO,OAAO,CAAC,GAAG,MAAM;AA3qB3C,UAAAA;AA4qBM,YAAM,KAAIA,MAAA,EAAE,QAAF,OAAAA,MAAS,EAAE;AACrB,aAAO,IAAI,IAAI,IAAI;AAAA,IACrB,IAAG,YAAO,CAAC,EAAE,QAAV,YAAiB,OAAO,CAAC,EAAE,KAAK;AACnC,WAAO,aAAa,aAAa,UAAU,MAAM;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAc,aACZ,IACA,MACA,MACA,MACA,KACkB;AAClB,QAAI,CAAC,KAAK,WAAW,IAAI,EAAE,GAAG;AAC5B,YAAM,KAAK,QAAQ,wBAAwB,IAAI;AAAA,QAC7C,MAAM;AAAA,QACN,QAAQ,EAAE,MAAM,MAAM,MAAM,MAAM,MAAM,OAAO,MAAM;AAAA,QACrD,QAAQ,CAAC;AAAA,MACX,CAAC;AACD,WAAK,WAAW,IAAI,EAAE;AAAA,IACxB;AAOA,UAAM,SAAkB,MAAM,KAAK,QAAQ,qBAAqB,IAAI,EAAE,KAAK,KAAK,KAAK,CAAC;AAGtF,WAAO,OAAO,WAAW,YAAY,WAAW,QAAS,OAAoC,eAAe;AAAA,EAC9G;AACF;",
4
+ "sourcesContent": ["import { type AdapterInstance } from \"@iobroker/adapter-core\";\nimport { coerceFiniteNumber, oneLine } from \"./coerce\";\nimport { packageName, statusLabel, tName, tText } from \"./i18n\";\nimport type { ParcelDelivery, ParcelEvent } from \"./types\";\nimport { UNKNOWN_STATUS_CODE } from \"./types\";\n\n/** Status codes that have expected delivery date/time: 2=In Transit, 4=Out for Delivery, 8=Info Received */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Upper bound for the `deliveries.*` object-view range query: the highest BMP\n * code unit, so the range covers every possible sanitized package id.\n */\nconst ID_RANGE_END = \"\uFFFF\";\n\n/** Max length of a sanitized package-id segment (collision suffix handles truncation clashes). */\nconst MAX_ID_LENGTH = 50;\n\n/**\n * v0.10.0 (I2): cap the parallel recursive deletes in cleanupDeliveries the\n * same way main.ts caps the update fan-out \u2014 a poll that suddenly loses many\n * packages must not flood the broker in one burst.\n */\nconst DELETE_BATCH_SIZE = 25;\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\n /**\n * Cache of state IDs whose object has already been written this process.\n * Skips repeat DB lookups on the hot path \u2014 each poll touches ~11 states\n * per delivery, and most deliveries see no schema change between polls.\n * On `cleanupDeliveries`, IDs of removed packages are dropped so a re-add\n * triggers a fresh creation.\n */\n private readonly createdIds = new Set<string>();\n\n /**\n * v0.10.0 (DP-5): package ids whose device object was ensured this process.\n * Replaces the former description+tracking signature map: with\n * `preserve: { common: [\"name\"] }` a rewrite never changed an existing\n * object's name anyway, so ensuring existence ONCE per process is the\n * honest version of what the signature cache actually did.\n */\n private readonly deviceEnsured = new Set<string>();\n\n /**\n * v0.7.2: package ids known to exist as device objects. Filled from the\n * object view ONCE after adapter start (reconciles leftovers from previous\n * runs), afterwards maintained in memory \u2014 `cleanupDeliveries` no longer\n * needs a DB round-trip per poll.\n */\n private knownDeliveryIds: Set<string> | null = null;\n\n /**\n * v0.4.2 (S3): which raw-tracking-key currently \"owns\" each sanitized id\n * within the running poll. Cleared via `resetPollState()` between polls so\n * the same delivery keeps its bare id as long as it's unique.\n */\n private readonly idOwner = new Map<string, string>();\n\n /**\n * L1: per-delivery-object memo for `parseStatus`. A poll parses the SAME\n * delivery object at up to four sites (active filter, updateDelivery,\n * updateSummary's isToday filter, combined window); memoizing keyed by the\n * object means the parse \u2014 and its drift debug line \u2014 runs ONCE per delivery\n * instead of per site. No reset needed: each poll's deliveries are fresh\n * objects (JSON.parse) and GC'd afterwards, and nothing holds a long-lived\n * delivery reference (idOwner/failedDeliveries/knownDeliveryIds store strings).\n */\n private readonly statusMemo = new WeakMap<ParcelDelivery, number>();\n\n /**\n * @param adapter The ioBroker adapter instance\n */\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, MAX_ID_LENGTH) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. The API sends an int; we also accept\n * a numeric string and fall back to the \"unknown\" sentinel (-1) for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n // L1: memoize per delivery object so the parse (and its drift log) runs\n // once per delivery per poll, not once per call site.\n const memoized = this.statusMemo.get(delivery);\n if (memoized !== undefined) {\n return memoized;\n }\n const code = this.computeStatus(delivery);\n this.statusMemo.set(delivery, code);\n return code;\n }\n\n /**\n * Parse the status code without memoization. Split from {@link parseStatus}\n * (L1) so the memo wraps a single pure computation \u2014 including the one-per-\n * delivery drift log.\n *\n * @param delivery The delivery to parse\n */\n private computeStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code;\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return Math.trunc(raw);\n }\n if (typeof raw === \"string\") {\n const n = parseInt(raw, 10);\n if (Number.isFinite(n)) {\n return n;\n }\n }\n // API drift (non-numeric / non-string status_code). Return a visible\n // \"unknown\" sentinel instead of 0 (Delivered) \u2014 otherwise a garbage\n // status_code would silently filter the package out and remove it in\n // autoRemove mode. The active filter is `status !== 0`, so -1 stays visible.\n this.adapter.log.debug(\n `parseStatus drift: ${JSON.stringify(raw)} (type ${typeof raw}) \u2192 ${UNKNOWN_STATUS_CODE} (unknown, kept visible)`,\n );\n return UNKNOWN_STATUS_CODE;\n }\n\n /**\n * Build a unique package ID from a delivery.\n *\n * v0.4.2 (S3): when the bare `sanitize(tracking_number)` collides with\n * another active package (e.g. two trackings differ only in special\n * chars that strip down to the same id), append a stable hash of the\n * full tracking number so both end up at distinct state IDs.\n *\n * @param delivery The delivery to build an ID for\n */\n packageId(delivery: ParcelDelivery): string {\n let id = this.sanitize(delivery.tracking_number);\n // API-drift guard: only string values extend the id\n if (typeof delivery.extra_information === \"string\" && delivery.extra_information.length > 0) {\n id += `_${this.sanitize(delivery.extra_information)}`;\n }\n // v0.4.2 (S3): collision suffix when two distinct (raw) trackings would\n // collapse to the same id. Bare id is kept as long as it's unique\n // within this poll (back-compat with existing installs).\n const owner = this.idOwner.get(id);\n const rawKey = StateManager.rawIdKey(delivery);\n if (owner !== undefined && owner !== rawKey) {\n const suffixed = `${id}__${StateManager.shortHash(rawKey)}`;\n // v0.4.3 (C3): trace the collision-suffix path. Rare event but the\n // resulting state-id divergence is hard to diagnose without a log.\n this.adapter.log.debug(\n `packageId collision: bare='${id}' owner='${oneLine(owner)}' new='${oneLine(rawKey)}' \u2192 suffixed='${suffixed}'`,\n );\n this.idOwner.set(suffixed, rawKey);\n return suffixed;\n }\n this.idOwner.set(id, rawKey);\n return id;\n }\n\n /**\n * v0.4.2 (S3): build a stable raw-key for collision tracking.\n *\n * @param delivery The delivery whose raw tracking identifies it.\n */\n private static rawIdKey(delivery: ParcelDelivery): string {\n const t = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const e = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n return `${t}\\u0000${e}`;\n }\n\n /**\n * v0.4.2 (S3): FNV-1a 32-bit short hash \u2192 6 hex chars.\n *\n * @param s Input string to hash.\n */\n private static shortHash(s: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, \"0\").slice(0, 6);\n }\n\n /**\n * v0.4.2 (S3): reset the per-poll collision tracker. Call from main.ts\n * before iterating deliveries so the bare id always wins for the first\n * occurrence in each poll.\n */\n resetPollState(): void {\n this.idOwner.clear();\n }\n\n /**\n * Extract the package-id segment from a relative object id\n * (`deliveries.<pkgId>` or `deliveries.<pkgId>.<state>`); \"\" when the id is\n * outside the deliveries tree. Single source for the id-schema knowledge\n * (v0.10.0, L15).\n *\n * @param relativeId Object id relative to the adapter namespace\n */\n private static pkgIdOf(relativeId: string): string {\n return relativeId.startsWith(\"deliveries.\") ? relativeId.slice(\"deliveries.\".length).split(\".\")[0] : \"\";\n }\n\n /**\n * Update or create all states for a delivery.\n *\n * @param delivery The delivery data from API\n * @param carrierName Resolved carrier display name\n * @param pkgId Package id from the caller's deterministic pre-pass \u2014 always\n * computed via `packageId()` so the collision suffixing stays deterministic\n * (v0.10.0, L11: no test-only fallback path anymore).\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string, pkgId: string): Promise<void> {\n const devicePath = `deliveries.${pkgId}`;\n\n const description = typeof delivery.description === \"string\" ? delivery.description : \"\";\n const trackingNumber = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const extraInfo = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n\n // v0.10.0 (DP-5): ensure the device object once per process. `preserve:\n // name` keeps an existing name (user renames win), so the name \u2014 localized\n // fallback when the API sends no description (L18) \u2014 only matters at\n // first creation; the former per-change rewrite never had a visible effect.\n if (!this.deviceEnsured.has(pkgId)) {\n await this.adapter.extendObject(\n devicePath,\n {\n type: \"device\",\n common: {\n name: description || packageName(trackingNumber || pkgId),\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n this.deviceEnsured.add(pkgId);\n }\n this.knownDeliveryIds?.add(pkgId);\n\n const statusCode = this.parseStatus(delivery);\n let statusText = statusLabel(statusCode);\n if (statusText === undefined) {\n // v0.4.3 (E3): trace unknown status-code (API drift). A future\n // parcel.app status (e.g. 9, 10) would render as \"Unknown (N)\"\n // without any log clue that the label table is out of date.\n this.adapter.log.debug(`status code ${statusCode} has no status_* label, using fallback`);\n statusText = `Unknown (${statusCode})`;\n }\n\n const deliveryWindow = this.calculateDeliveryWindow(delivery, statusCode);\n const deliveryEstimate = this.calculateDeliveryEstimate(delivery, statusCode);\n const lastEvent = this.formatLastEvent(delivery);\n const lastLocation = this.extractLastLocation(delivery);\n\n // v0.10.0 (M5): ONE definition list drives the writes AND the lastUpdated\n // decision \u2014 the former parallel JSON.stringify signature array was a\n // silent drift trap (a new field added to one list but not the other).\n // `desc` is an EXPLANATION, only where the name alone does not give it \u2014\n // `undefined` means \"nothing to explain here\", and an invented sentence\n // would be worse than none (fleet standard, 2026-09-02).\n const stateDefs: [\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n desc: ioBroker.StringOrTranslated | undefined,\n ][] = [\n [`${devicePath}.carrier`, tName(\"carrier\"), \"string\", \"text\", carrierName, undefined],\n [`${devicePath}.status`, tName(\"status\"), \"string\", \"text\", statusText, undefined],\n [`${devicePath}.statusCode`, tName(\"statusCode\"), \"number\", \"value\", statusCode, tName(\"descStatusCode\")],\n [`${devicePath}.description`, tName(\"description\"), \"string\", \"text\", description, undefined],\n [`${devicePath}.trackingNumber`, tName(\"trackingNumber\"), \"string\", \"text\", trackingNumber, undefined],\n [`${devicePath}.extraInfo`, tName(\"extraInfo\"), \"string\", \"text\", extraInfo, tName(\"descExtraInfo\")],\n [\n `${devicePath}.deliveryWindow`,\n tName(\"deliveryWindow\"),\n \"string\",\n \"text\",\n deliveryWindow,\n tName(\"descDeliveryWindow\"),\n ],\n [\n `${devicePath}.deliveryEstimate`,\n tName(\"deliveryEstimate\"),\n \"string\",\n \"text\",\n deliveryEstimate,\n tName(\"descDeliveryEstimate\"),\n ],\n [`${devicePath}.lastEvent`, tName(\"lastEvent\"), \"string\", \"text\", lastEvent, tName(\"descLastEvent\")],\n [`${devicePath}.lastLocation`, tName(\"lastLocation\"), \"string\", \"text\", lastLocation, undefined],\n ];\n const changed = await Promise.all(\n stateDefs.map(([id, name, type, role, val, desc]) => this.createAndSet(id, name, type, role, val, desc)),\n );\n\n // v0.10.0 (M5): `lastUpdated` = \"when the tracking data last CHANGED\".\n // The decision now rides on the broker's own setStateChanged answer\n // (notChanged=false \u21D2 a sibling value really differed in the DB), so an\n // adapter restart no longer stamps every package with a fresh timestamp \u2014\n // the old in-memory signature map always missed on the first poll after a\n // restart, and it was updated BEFORE its write survived (ASYNC-6).\n if (changed.some(Boolean)) {\n await this.createAndSet(\n `${devicePath}.lastUpdated`,\n tName(\"lastUpdated\"),\n \"string\",\n \"date\",\n new Date().toISOString(),\n tName(\"descLastUpdated\"),\n );\n }\n }\n\n /**\n * Update summary states. Expects already-filtered active deliveries.\n * The `summary` channel itself is declared via io-package.json instanceObjects.\n *\n * @param activeDeliveries Only active (non-delivered) deliveries\n */\n async updateSummary(activeDeliveries: ParcelDelivery[]): Promise<void> {\n const todayDeliveries = activeDeliveries.filter(d => this.isToday(d, this.parseStatus(d)));\n // v0.4.3 (E1): trace summary refresh \u2014 ~144/day at the default poll\n // interval, kept short (counts only).\n this.adapter.log.debug(\n `updateSummary: ${activeDeliveries.length} active, ${todayDeliveries.length} expected today`,\n );\n\n await Promise.all([\n this.createAndSet(\n \"summary.activeCount\",\n tName(\"activeCount\"),\n \"number\",\n \"value\",\n activeDeliveries.length,\n tName(\"descActiveCount\"),\n ),\n this.createAndSet(\n \"summary.todayCount\",\n tName(\"todayCount\"),\n \"number\",\n \"value\",\n todayDeliveries.length,\n tName(\"descTodayCount\"),\n ),\n this.createAndSet(\n \"summary.deliveryWindow\",\n tName(\"summaryDeliveryWindow\"),\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n tName(\"descSummaryDeliveryWindow\"),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer present in the API response.\n *\n * @param keepIds Package IDs the API still returns this poll (kept). Every\n * currently-known delivery NOT in this set is removed. The caller passes\n * ALL visible package ids, not only the ones whose state-write succeeded \u2014\n * a transient write failure must not delete a still-present package.\n */\n async cleanupDeliveries(keepIds: string[]): Promise<void> {\n // v0.7.2: the object view is queried only ONCE after adapter start to\n // reconcile leftovers from previous runs; afterwards the in-memory set\n // (maintained by updateDelivery + this prune) replaces the per-poll DB\n // round-trip.\n if (this.knownDeliveryIds === null) {\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.${ID_RANGE_END}`,\n });\n if (!objects?.rows) {\n // v0.4.3 (E2): trace the no-op path \u2014 happens on fresh installs or\n // when getObjectViewAsync returns falsy. Without this the early-return\n // is invisible (and the known-set stays unseeded for the next poll).\n this.adapter.log.debug(\"cleanupDeliveries: no objects view available, skipping\");\n return;\n }\n this.knownDeliveryIds = new Set<string>();\n for (const row of objects.rows) {\n // The range query guarantees the namespace prefix \u2014 cut it instead of\n // pattern-replacing (v0.10.0, KISS-13).\n const pkgId = StateManager.pkgIdOf(row.id.slice(this.adapter.namespace.length + 1));\n if (pkgId) {\n this.knownDeliveryIds.add(pkgId);\n }\n }\n }\n\n const keepSet = new Set(keepIds);\n // v0.4.2 (S1): collect first, then delete in parallel \u2014 capped in batches\n // (v0.10.0, I2) like the update fan-out in main.ts.\n const toDelete = [...this.knownDeliveryIds].filter(pkgId => !keepSet.has(pkgId));\n const toDeleteSet = new Set(toDelete);\n\n for (let start = 0; start < toDelete.length; start += DELETE_BATCH_SIZE) {\n const batch = toDelete.slice(start, start + DELETE_BATCH_SIZE);\n await Promise.all(\n batch.map(async pkgId => {\n const relativeId = `deliveries.${pkgId}`;\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n this.deviceEnsured.delete(pkgId);\n }),\n );\n }\n\n // v0.9.0 (S2): prune createdIds for every removed package in ONE pass over\n // the set \u2014 O(created). A createdId is `deliveries.<pkgId>` or\n // `deliveries.<pkgId>.<state>` \u2014 extract the pkgId and drop it if removed.\n if (toDeleteSet.size > 0) {\n for (const id of [...this.createdIds]) {\n if (toDeleteSet.has(StateManager.pkgIdOf(id))) {\n this.createdIds.delete(id);\n }\n }\n }\n this.knownDeliveryIds = new Set(keepSet);\n }\n\n /**\n * Parse a parcel.app expected-date string to LOCAL epoch-millis.\n *\n * The API delivers `date_expected`/`date_expected_end` \"without specific\n * timezone information\"; parse with explicit local calendar components so the\n * value lands on the intended local day/time (`new Date(\"YYYY-MM-DD\")` would\n * be UTC midnight). `hasTime` is false for a bare date or a midnight time\n * (a day, not an hour-window). Ambiguous carrier formats (dotted, weekday\n * names) are deliberately NOT guessed \u2014 they return null rather than risk a\n * wrong date.\n *\n * @param value Raw date/time string from the API\n */\n private static parseExpectedToMs(value: unknown): { ms: number; hasTime: boolean } | null {\n if (typeof value !== \"string\") {\n return null;\n }\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/.exec(value.trim());\n if (!m) {\n return null;\n }\n const hasClock = m[4] !== undefined;\n const year = Number(m[1]);\n const month = Number(m[2]); // 1-12\n const day = Number(m[3]);\n const hour = hasClock ? Number(m[4]) : 0;\n const min = hasClock ? Number(m[5]) : 0;\n const sec = m[6] !== undefined ? Number(m[6]) : 0;\n // Range-validate the components. The regex only checks digit COUNT, not\n // value range, and `new Date(2026, 12, 40, 25, \u2026)` silently ROLLS OVER to a\n // wrong date (getTime() is NOT NaN). Reject out-of-range rather than guess.\n if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 || sec > 59) {\n return null;\n }\n const date = new Date(year, month - 1, day, hour, min, sec);\n // Catch day-of-month overflow the range check misses (Feb 30, Apr 31, \u2026):\n // a real date round-trips the month and day it was built from.\n if (Number.isNaN(date.getTime()) || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n const hasTime = hasClock && !(hour === 0 && min === 0 && sec === 0);\n return { ms: date.getTime(), hasTime };\n }\n\n /**\n * Resolve a delivery's expected window to epoch-millis bounds. Returns null\n * for non-trackable status or when there is no usable start time.\n *\n * Prefers the Unix timestamp fields; for carriers that report the window only\n * as a date/time string (`date_expected`/`date_expected_end`) it falls back to\n * those \u2014 but only when the string carries a real time-of-day (a bare date or\n * midnight is a day, not an hour-window). Carrier-agnostic.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private windowBoundsMs(delivery: ParcelDelivery, statusCode: number): { start: number; end: number | null } | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n const toMs = (timestamp: unknown): number | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const ms = ts * 1000;\n return Number.isNaN(new Date(ms).getTime()) ? null : ms;\n };\n const dateMs = (value: unknown): number | null => {\n const parsed = StateManager.parseExpectedToMs(value);\n return parsed && parsed.hasTime ? parsed.ms : null;\n };\n const start = toMs(delivery.timestamp_expected) ?? dateMs(delivery.date_expected);\n if (start === null) {\n return null;\n }\n const end = toMs(delivery.timestamp_expected_end) ?? dateMs(delivery.date_expected_end);\n return { start, end };\n }\n\n /**\n * Format epoch-millis as local HH:MM.\n *\n * @param ms Epoch milliseconds\n */\n private static formatHHMM(ms: number): string {\n const d = new Date(ms);\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n }\n\n /**\n * Local \"MM-DD HH:MM\" \u2014 used when a window spans more than one calendar day.\n *\n * @param ms Epoch milliseconds\n */\n private static formatDateHHMM(ms: number): string {\n const d = new Date(ms);\n const mm = (d.getMonth() + 1).toString().padStart(2, \"0\");\n const dd = d.getDate().toString().padStart(2, \"0\");\n return `${mm}-${dd} ${StateManager.formatHHMM(ms)}`;\n }\n\n /**\n * Whether two epoch-millis fall on the same LOCAL calendar day.\n *\n * @param aMs First epoch milliseconds\n * @param bMs Second epoch milliseconds\n */\n private static sameLocalDay(aMs: number, bMs: number): boolean {\n const a = new Date(aMs);\n const b = new Date(bMs);\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n }\n\n /**\n * Format a start\u2192end window as a local string. A real end (> start) on the\n * SAME day renders \"HH:MM - HH:MM\"; an end on a LATER day carries the date on\n * both sides (\"12-06 14:30 - 12-08 18:30\") so a multi-day window is not shown\n * as if it were same-day. No end, or an end <= start (reversed/equal), renders\n * just the start.\n *\n * @param startMs Window start (epoch ms)\n * @param endMs Window end (epoch ms) or null\n */\n private static formatWindow(startMs: number, endMs: number | null): string {\n if (endMs === null || endMs <= startMs) {\n return StateManager.formatHHMM(startMs);\n }\n return StateManager.sameLocalDay(startMs, endMs)\n ? `${StateManager.formatHHMM(startMs)} - ${StateManager.formatHHMM(endMs)}`\n : `${StateManager.formatDateHHMM(startMs)} - ${StateManager.formatDateHHMM(endMs)}`;\n }\n\n /**\n * Calculate a delivery time-window string from the resolved expected bounds.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(delivery: ParcelDelivery, statusCode: number): string {\n const bounds = this.windowBoundsMs(delivery, statusCode);\n if (!bounds) {\n return \"\";\n }\n return StateManager.formatWindow(bounds.start, bounds.end);\n }\n\n /**\n * Days from today to the expected delivery date. Returns null when the\n * delivery has no usable expected date or is in a non-trackable status.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private computeDiffDays(delivery: ParcelDelivery, statusCode: number): number | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n\n let expectedDate: Date | null = null;\n const ts = coerceFiniteNumber(delivery.timestamp_expected);\n if (ts !== null && ts > 0) {\n expectedDate = new Date(ts * 1000);\n } else {\n // Shares the window's date parser (one source of format-truth). Only the\n // calendar day matters here, so the time-of-day flag is ignored; the\n // local-component parse keeps the day timezone-stable.\n const parsed = StateManager.parseExpectedToMs(delivery.date_expected);\n expectedDate = parsed ? new Date(parsed.ms) : null;\n }\n\n if (!expectedDate || Number.isNaN(expectedDate.getTime())) {\n return null;\n }\n\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const expectedStart = new Date(expectedDate.getFullYear(), expectedDate.getMonth(), expectedDate.getDate());\n return Math.round((expectedStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24));\n }\n\n /**\n * Calculate human-readable delivery estimate.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryEstimate(delivery: ParcelDelivery, statusCode: number): string {\n const diffDays = this.computeDiffDays(delivery, statusCode);\n if (diffDays === null) {\n return \"\";\n }\n if (diffDays < 0) {\n return tText(\"estimateOverdue\");\n }\n if (diffDays === 0) {\n return tText(\"estimateToday\");\n }\n if (diffDays === 1) {\n return tText(\"estimateTomorrow\");\n }\n return tText(\"estimateDays\", diffDays);\n }\n\n /**\n * Whether the delivery is expected today. Language-agnostic, used by the\n * summary filter so `todayCount` works across all languages.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private isToday(delivery: ParcelDelivery, statusCode: number): boolean {\n return this.computeDiffDays(delivery, statusCode) === 0;\n }\n\n private getLatestEvent(delivery: ParcelDelivery): ParcelEvent | null {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return null;\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return null;\n }\n return latest;\n }\n\n private formatLastEvent(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\n return \"\";\n }\n const parts: string[] = [];\n if (typeof latest.event === \"string\" && latest.event.length > 0) {\n parts.push(latest.event);\n }\n if (typeof latest.date === \"string\" && latest.date.length > 0) {\n parts.push(latest.date);\n }\n return parts.join(\" - \");\n }\n\n private extractLastLocation(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Combined delivery window for today's packages: earliest start to latest\n * end across all windows. Computed from the raw millis (not the formatted\n * strings) so the latest end always wins \u2014 fixes the earlier bug where the\n * end of the latest-*starting* window was used instead of the maximum end.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const bounds = todayDeliveries\n .map(d => this.windowBoundsMs(d, this.parseStatus(d)))\n .filter((b): b is { start: number; end: number | null } => b !== null);\n\n if (bounds.length === 0) {\n return \"\";\n }\n\n // L3: fold instead of Math.min/max(...spread). The bounds array is capped\n // by the 1 MiB response limit, but a spread over a large array can still hit\n // V8's argument-count limit (RangeError); reduce is O(n) and unbounded-safe\n // \u2014 consistent with beszel's computeMaxTemp hardening.\n const minStart = bounds.reduce((m, b) => (b.start < m ? b.start : m), bounds[0].start);\n const maxEnd = bounds.reduce((m, b) => {\n const e = b.end ?? b.start;\n return e > m ? e : m;\n }, bounds[0].end ?? bounds[0].start);\n return StateManager.formatWindow(minStart, maxEnd);\n }\n\n /**\n * Create/refresh a read-only state and set its value. Runs the object write\n * once per ID per process (the cache skips the repeat round-trip on the hot\n * path); only the value changes per poll.\n *\n * v0.11.0: `extendObject` instead of `setObjectNotExistsAsync`. The old call\n * only ever touched an object that did not exist yet, so a changed name,\n * description, role or type reached FRESH installs only \u2014 an existing tree\n * kept the text it was created with, while manifest, linter, type check and\n * the name gate all stayed green. Measured on a live install: the three\n * permanent `summary.*` states still carried their plain-English pre-i18n\n * names, while the per-package states looked correct only because packages\n * are deleted and recreated. `extendObject` merges, so a user-set `custom`\n * (history/logging) survives \u2014 the name deliberately does NOT: the adapter\n * owns the names of its own states.\n *\n * @param id State ID relative to adapter namespace\n * @param name Display name (translation object or plain string)\n * @param type Value type\n * @param role ioBroker role\n * @param val Value to set\n * @param desc Short explanation, or undefined where there is nothing to explain\n * @returns true when the broker actually wrote the value (it differed or the\n * state was new) \u2014 the DB-backed \"did anything change\" signal driving\n * `lastUpdated` (v0.10.0, M5)\n */\n private async createAndSet(\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n desc?: ioBroker.StringOrTranslated,\n ): Promise<boolean> {\n if (!this.createdIds.has(id)) {\n const common: ioBroker.StateCommon = { name, type, role, read: true, write: false };\n if (desc !== undefined) {\n common.desc = desc;\n }\n await this.adapter.extendObject(id, { type: \"state\", common, native: {} });\n this.createdIds.add(id);\n }\n // The bundled @iobroker/types 7.1.2 types this promise as `string`, but\n // js-controller \u22657.2.2 (our dependency floor) resolves { id, notChanged }\n // \u2014 verified at v7.2.2: adapter.ts invokes the callback with\n // (null, res.id, res.notChanged) and tools.promisify(['id','notChanged'])\n // builds the object from exactly these named args. Narrow locally instead\n // of trusting the stale published type.\n const result: unknown = await this.adapter.setStateChangedAsync(id, { val, ack: true });\n // Only an explicit notChanged=false counts as a write \u2014 anything else\n // (missing field, drifted runtime) must not fake \"changed\" on every poll.\n return typeof result === \"object\" && result !== null && (result as { notChanged?: unknown }).notChanged === false;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
package/build/main.js CHANGED
@@ -35,6 +35,7 @@ var utils = __toESM(require("@iobroker/adapter-core"));
35
35
  var import_adapter_core = require("@iobroker/adapter-core");
36
36
  var import_node_path = require("node:path");
37
37
  var import_coerce = require("./lib/coerce");
38
+ var import_i18n = require("./lib/i18n");
38
39
  var import_parcel_client = require("./lib/parcel-client");
39
40
  var import_state_manager = require("./lib/state-manager");
40
41
  var import_types = require("./lib/types");
@@ -47,6 +48,7 @@ const MAX_ADD_FIELD_LEN = 512;
47
48
  const UPDATE_BATCH_SIZE = 25;
48
49
  const MAX_ADDS_PER_WINDOW = 20;
49
50
  const ADD_WINDOW_MS = 6e4;
51
+ const OBSOLETE_NATIVE_KEYS = ["filterMode", "language"];
50
52
  const NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
51
53
  "ENOTFOUND",
52
54
  "ECONNREFUSED",
@@ -117,44 +119,129 @@ class ParcelappAdapter extends utils.Adapter {
117
119
  this.on("message", this.onMessage.bind(this));
118
120
  }
119
121
  /**
120
- * Switch off `supportedMessages.stopInstance` on this instance's own object.
122
+ * Repair leftovers an upgrade cannot remove from this instance's own object.
121
123
  *
122
- * The entry was dropped from the manifest, which only helps a FRESH install: an upgrade
123
- * merges the manifest into the existing instance object and never removes a key, so the old
124
- * `true` survives in the database — and that is what the host reads. With it the host kills
125
- * the process one second after asking it to stop, `onUnload` never runs, and the
126
- * `info.connection=false` write on the way out never reaches the database.
124
+ * js-controller MERGES the manifest into an existing instance object and never drops a key,
125
+ * so anything a past version wrote survives forever. Two such leftovers exist:
127
126
  *
128
- * Only written when it is actually still on: every instance-object change restarts the
129
- * instance, so doing it unconditionally would be a restart loop.
127
+ * 1. `common.supportedMessages` (dropped from the manifest in v0.10.3). The whole key is
128
+ * removed, and setting it to `{stopInstance: false}` what v0.10.3 through v0.10.4 did —
129
+ * is NOT a fix but a second, worse defect. `isMessageboxSupported()` in the adapter package
130
+ * stops looking at `common.messagebox` the moment `supportedMessages` is an object, and then
131
+ * only reports true when SOME value is not false:
130
132
  *
131
- * @returns true when the correction was written and the restart is coming — the caller has
133
+ * if (!isObject(common.supportedMessages)) return !!common.messagebox;
134
+ * return Object.values(common.supportedMessages).find(v => v !== false) !== undefined;
135
+ *
136
+ * With `{stopInstance: false}` that is `false`, so the adapter never runs
137
+ * `subscribeMessage` and NOTHING reaches `onMessage` — the admin's "Test Connection" button
138
+ * and every `sendTo(..., "addDelivery", ...)` from a script die silently. Silently, because
139
+ * the error branch next to that call only fires for `options.message`, and we register via
140
+ * `this.on("message", …)`. The old `{stopInstance: true}` kept the box open by accident
141
+ * (`true !== false`), which is why this only broke with the v0.10.3 correction.
142
+ * 2. `native.filterMode` (dropped in v0.2.0) and `native.language` (dropped before v0.5.0).
143
+ * Dead config keys nothing reads — the adapter owns its own configuration inventory the
144
+ * same way it owns its states (`cleanupObsoleteStates`).
145
+ *
146
+ * Both are corrected in ONE write, because every instance-object change restarts the instance:
147
+ * two writes would mean two restarts. And it is written only when something is actually stale,
148
+ * otherwise the correction would be a restart loop.
149
+ *
150
+ * `extendObject` cannot do this: its deep merge (`node.extend`) SETS a key given as `null`
151
+ * instead of dropping it — measured against the bundled js-controller. Removing a key needs a
152
+ * read/delete/`setForeignObject` round-trip, which is the same pattern js-controller's own
153
+ * `updateConfig()`/`disable()` use. `native.apiKey` rides along untouched and stays encrypted:
154
+ * `getForeignObject` does not decrypt (that happens only into `this.config`).
155
+ *
156
+ * @returns true when a correction was written and the restart is coming — the caller has
132
157
  * to stop right there instead of polling in a process that is going down.
133
158
  */
134
- async clearStopInstanceFlag() {
135
- var _a;
159
+ async correctInstanceObject() {
160
+ var _a, _b;
136
161
  const id = `system.adapter.${this.namespace}`;
137
162
  try {
138
163
  const obj = await this.getForeignObjectAsync(id);
139
- const supported = (_a = obj == null ? void 0 : obj.common) == null ? void 0 : _a.supportedMessages;
140
- if (!(supported == null ? void 0 : supported.stopInstance)) {
164
+ if (!obj) {
141
165
  return false;
142
166
  }
143
- this.log.info("Correcting a leftover setting from an earlier version \u2014 this instance restarts once");
144
- await this.extendForeignObjectAsync(id, { common: { supportedMessages: { stopInstance: false } } });
167
+ const staleSupportedMessages = "supportedMessages" in ((_a = obj.common) != null ? _a : {});
168
+ const native = (_b = obj.native) != null ? _b : {};
169
+ const staleNativeKeys = OBSOLETE_NATIVE_KEYS.filter((key) => key in native);
170
+ if (!staleSupportedMessages && staleNativeKeys.length === 0) {
171
+ return false;
172
+ }
173
+ this.log.info("Correcting leftover settings from an earlier version \u2014 this instance restarts once");
174
+ if (staleSupportedMessages) {
175
+ this.log.debug("Removing the obsolete supportedMessages entry so the message box stays open");
176
+ delete obj.common.supportedMessages;
177
+ }
178
+ for (const key of staleNativeKeys) {
179
+ this.log.debug(`Removing obsolete configuration key: native.${key}`);
180
+ delete native[key];
181
+ }
182
+ obj.native = native;
183
+ await this.setForeignObject(id, obj);
145
184
  return true;
146
185
  } catch (err) {
147
186
  this.log.debug(`Could not check the instance object ${id}: ${(0, import_coerce.errText)(err)}`);
148
187
  return false;
149
188
  }
150
189
  }
190
+ /**
191
+ * Re-apply the manifest objects' `common` to an EXISTING installation.
192
+ *
193
+ * js-controller creates `io-package.json:instanceObjects` only where they are MISSING, so a
194
+ * changed name or description otherwise reaches fresh installs only — measured on a live
195
+ * install, `info` and `info.connection` still carried their plain-English pre-i18n names while
196
+ * manifest, linter, type check and the name gate were all green. Each id is spelled out rather
197
+ * than looped over the manifest: the consistency gate looks for the literal id at the
198
+ * `extendObject` call, and a loop would be DRYer but unverifiable.
199
+ *
200
+ * `desc` is set only where there is something to explain — a folder called "Deliveries"
201
+ * explains itself, and an invented sentence is worse than none.
202
+ */
203
+ async refreshManifestObjects() {
204
+ await this.extendObject("info", {
205
+ type: "channel",
206
+ common: { name: (0, import_i18n.tName)("info") },
207
+ native: {}
208
+ });
209
+ await this.extendObject("info.connection", {
210
+ type: "state",
211
+ common: {
212
+ name: (0, import_i18n.tName)("infoConnection"),
213
+ desc: (0, import_i18n.tName)("descInfoConnection"),
214
+ type: "boolean",
215
+ role: "indicator.connected",
216
+ read: true,
217
+ write: false,
218
+ def: false
219
+ },
220
+ native: {}
221
+ });
222
+ await this.extendObject("deliveries", {
223
+ type: "folder",
224
+ common: { name: (0, import_i18n.tName)("deliveries") },
225
+ native: {}
226
+ });
227
+ await this.extendObject("summary", {
228
+ type: "channel",
229
+ common: { name: (0, import_i18n.tName)("summary") },
230
+ native: {}
231
+ });
232
+ }
151
233
  async onReady() {
152
234
  try {
153
- if (await this.clearStopInstanceFlag()) {
235
+ if (await this.correctInstanceObject()) {
154
236
  return;
155
237
  }
156
238
  await import_adapter_core.I18n.init((0, import_node_path.join)(this.adapterDir, "admin"), this);
157
239
  this.log.debug(`onReady: starting (autoRemoveDelivered=${this.config.autoRemoveDelivered})`);
240
+ try {
241
+ await this.refreshManifestObjects();
242
+ } catch (err) {
243
+ this.log.warn(`Refreshing the manifest objects failed (continuing): ${(0, import_coerce.errText)(err)}`);
244
+ }
158
245
  await this.setState("info.connection", { val: false, ack: true });
159
246
  const { apiKey } = this.config;
160
247
  if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {
package/build/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { coerceClampedInt, errText, isTrueish, oneLine } from \"./lib/coerce\";\nimport { ParcelClient, RETRY_AFTER_DEFAULT_SEC, RETRY_AFTER_MAX_SEC } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport { DELIVERED_STATUS_CODE } from \"./lib/types\";\nimport type { AddDeliveryRequest } from \"./lib/types\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\n// Minimum 60s between polls. Also the natural pacing after addDelivery: a\n// typical single add still polls immediately (the last poll is >60s ago),\n// while a burst of adds collapses to at most one extra GET per minute.\nconst MIN_POLL_GAP_MS = 60_000;\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n// v0.9.0 (S5): cap addDelivery field lengths. A sendTo caller is local, but a\n// runaway script must not push a multi-MB POST body to parcel.app. Identifiers\n// are short; this is generous for a description.\nconst MAX_ADD_FIELD_LEN = 512;\n// v0.9.0 (S3): cap the per-poll broker fan-out. Updates run in batches of this\n// size instead of all deliveries at once, so an abnormally large API response\n// can't flood the broker with thousands of concurrent writes.\nconst UPDATE_BATCH_SIZE = 25;\n// v0.9.0 (S4): client-side throttle on addDelivery POSTs. parcel.app enforces\n// ~20 POST/day server-side; this caps a runaway/buggy script's burst so it can't\n// hammer the API. The window is generous enough never to block a real batch-add\n// (the daily server limit is the real cap).\nconst MAX_ADDS_PER_WINDOW = 20;\nconst ADD_WINDOW_MS = 60_000;\n\n/**\n * Node transport-level error codes treated as (transient) NETWORK problems:\n * DNS, connection refused/reset, unreachable \u2014 plus EPIPE/ECONNABORTED/EPROTO\n * (v0.10.0, I9), which belong to the same warn+keep-retrying class.\n */\nconst NETWORK_ERROR_CODES = new Set([\n \"ENOTFOUND\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENETUNREACH\",\n \"EHOSTUNREACH\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"ECONNABORTED\",\n \"EPROTO\",\n]);\n\n/**\n * v0.10.0 (L12): the seam contracts name exactly the members main uses \u2014\n * structural Picks let the orchestration tests inject plain object fakes\n * without unsafe double-casts, and the compiler documents what a fake must\n * provide.\n */\ntype ClientLike = Pick<\n ParcelClient,\n \"getDeliveries\" | \"getCarrierName\" | \"addDelivery\" | \"testConnection\" | \"cancelAll\"\n>;\ntype StateManagerLike = Pick<\n StateManager,\n \"resetPollState\" | \"packageId\" | \"parseStatus\" | \"updateDelivery\" | \"updateSummary\" | \"cleanupDeliveries\"\n>;\n\n/**\n * ioBroker adapter for parcel.app package tracking. Exported so the\n * orchestration unit tests can drive its lifecycle/poll handlers directly.\n */\nexport class ParcelappAdapter extends utils.Adapter {\n private client: ClientLike | null = null;\n private stateManager: StateManagerLike | null = null;\n /**\n * Factories for the HTTP client + state manager \u2014 default to the real\n * constructors. Test seams (fleet pattern): unit tests replace these with\n * fakes to exercise the poll orchestration (throttle/rate-limit interplay,\n * error routing, failure dedup) without real network.\n *\n * @param apiKey parcel.app API key\n */\n private makeClient: (apiKey: string) => ClientLike = apiKey =>\n new ParcelClient(apiKey, { debug: (m: string) => this.log.debug(m) });\n private makeStateManager: () => StateManagerLike = () => new StateManager(this);\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n /**\n * v0.10.0 (L2): set in onUnload. onReady checks it after its awaits so a\n * stop during the first poll can no longer arm the interval or log\n * \"started\" after the unload already ran; batch failures during shutdown\n * degrade to debug.\n */\n private unloaded = false;\n /**\n * Package ids (not raw tracking numbers) whose last updateDelivery failed.\n * Keyed like the states so the dedup survives a sanitize collision or a\n * missing tracking number; pruned each poll against the visible pkgIds.\n */\n private failedDeliveries = new Set<string>();\n /** Timestamps of recent addDelivery POSTs \u2014 the S4 throttle window. */\n private addTimestamps: number[] = [];\n /**\n * L2: true while a checkConnection test GET is in flight. A test hits the same\n * 20/hour GET budget as polling; this guards against a concurrent second test\n * (double-click / admin re-render) stacking a redundant GET. A sequential\n * re-test after the current one settles runs normally.\n */\n private testConnectionInFlight = false;\n /**\n * v0.4.4: short-lived test-clients spawned from `checkConnection` admin\n * messages. The prod-`this.client` is what `onUnload` cancels, so these\n * need their own registry to be reachable at shutdown. Without this, an\n * admin clicking \"Test Connection\" right before adapter-stop could keep\n * the process alive past js-controller's 4-second kill deadline.\n */\n private testClients = new Set<ClientLike>();\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 /**\n * Switch off `supportedMessages.stopInstance` on this instance's own object.\n *\n * The entry was dropped from the manifest, which only helps a FRESH install: an upgrade\n * merges the manifest into the existing instance object and never removes a key, so the old\n * `true` survives in the database \u2014 and that is what the host reads. With it the host kills\n * the process one second after asking it to stop, `onUnload` never runs, and the\n * `info.connection=false` write on the way out never reaches the database.\n *\n * Only written when it is actually still on: every instance-object change restarts the\n * instance, so doing it unconditionally would be a restart loop.\n *\n * @returns true when the correction was written and the restart is coming \u2014 the caller has\n * to stop right there instead of polling in a process that is going down.\n */\n private async clearStopInstanceFlag(): Promise<boolean> {\n const id = `system.adapter.${this.namespace}`;\n try {\n const obj = await this.getForeignObjectAsync(id);\n const supported = obj?.common?.supportedMessages as { stopInstance?: unknown } | undefined;\n if (!supported?.stopInstance) {\n return false;\n }\n this.log.info(\"Correcting a leftover setting from an earlier version \u2014 this instance restarts once\");\n await this.extendForeignObjectAsync(id, { common: { supportedMessages: { stopInstance: false } } });\n return true;\n } catch (err) {\n // Objects DB unreachable \u2014 not worth failing the start over; the next start retries.\n this.log.debug(`Could not check the instance object ${id}: ${errText(err)}`);\n return false;\n }\n }\n\n private async onReady(): Promise<void> {\n try {\n // First: without this the whole shutdown path stays dead on an updated install.\n // A correction means the host is restarting us \u2014 no point starting a poll cycle.\n if (await this.clearStopInstanceFlag()) {\n return;\n }\n // I18n.init resolves system.config.language itself (adapter-core reads\n // the foreign object internally; unknown languages fall back to English)\n // \u2014 the former separate getForeignObject round-trip and the local\n // resolveLanguage step are gone with the STATUS_LABELS table (L20).\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.log.debug(`onReady: starting (autoRemoveDelivered=${this.config.autoRemoveDelivered})`);\n\n await this.setState(\"info.connection\", { val: false, ack: true });\n\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {\n this.log.error(\"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\");\n return;\n }\n\n this.client = this.makeClient(apiKey.trim());\n this.stateManager = this.makeStateManager();\n\n try {\n await this.cleanupObsoleteStates();\n } catch (err) {\n // C8: a cleanup failure must not abort startup. Without this guard the\n // outer catch would skip arming the poll interval below, and the adapter\n // would never poll until a manual restart. Degrade to a warning.\n this.log.warn(`cleanupObsoleteStates failed (continuing): ${errText(err)}`);\n }\n\n await this.poll();\n\n // v0.10.0 (L2): a stop during the awaits above must not arm a timer or\n // log a start line after onUnload already ran.\n if (this.unloaded) {\n return;\n }\n\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n this.log.debug(`pollInterval: raw=${JSON.stringify(this.config.pollInterval)} resolved=${interval}min`);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => {\n void this.poll().catch(err => this.log.error(`Scheduled poll failed: ${errText(err)}`));\n }, intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n // v0.10.0 (L4): a transient startup failure (i18n files, DB hiccup) used\n // to leave a green-looking zombie \u2014 no client, no timer, no retry until\n // a manual restart. Terminate instead: js-controller restarts the\n // instance, which self-heals the transient case; its restart-loop guard\n // backstops a persistent one.\n if (!this.unloaded) {\n this.terminate(\"startup failed \u2014 requesting restart\", utils.EXIT_CODES.START_IMMEDIATELY_AFTER_STOP);\n }\n }\n }\n\n /**\n * v0.4.2 (M5+X5): delegate to the shared `coerceClampedInt` helper.\n *\n * @param raw Raw `pollInterval` from admin config (number or numeric string).\n */\n private static coercePollInterval(raw: unknown): number {\n return coerceClampedInt(raw, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL, DEFAULT_POLL_INTERVAL);\n }\n\n private onUnload(callback: () => void): void {\n this.unloaded = true;\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n // v0.4.2 (M11+P1): cancel every in-flight HTTPS request so a slow\n // parcel.app endpoint doesn't keep the adapter alive past\n // js-controller's 4-second kill deadline. cancelAll also marks the\n // client terminal (v0.10.0, L3) \u2014 requests started after this point\n // are refused instead of opening fresh connections.\n this.client?.cancelAll();\n // v0.4.4: also abort any short-lived test-client (from checkConnection)\n // whose HTTPS-request might still be inflight at shutdown \u2014 the prod\n // `this.client.cancelAll()` only touches the production-client.\n for (const tc of this.testClients) {\n tc.cancelAll();\n }\n this.testClients.clear();\n // v0.10.3: report done only once the disconnect write landed. Calling back\n // first means the host tears the process down while the write is still in\n // flight and the instance keeps claiming a live connection. No own deadline\n // needed \u2014 the host already has one (`common.stopTimeout`), and\n // `this.setTimeout` refuses during shutdown anyway.\n void this.setState(\"info.connection\", { val: false, ack: true })\n .catch(() => {\n /* broker is shutting down \u2014 ignore */\n })\n .finally(callback);\n return;\n } catch (err) {\n // v0.4.3 (G4): replace silent `// ignore` with a trace so shutdown\n // errors leave a debug breadcrumb. Broker-already-down errors here\n // are expected \u2014 debug-level keeps them out of the user log.\n try {\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n } catch {\n /* logger already gone \u2014 nothing left to report to */\n }\n // v0.10.0 (I7): the callback is the contract with js-controller \u2014 exactly\n // once. v0.10.3: the happy path returns above with the callback chained to\n // the disconnect write, so this catch is the only other exit.\n callback();\n }\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise. Inside the try since\n // v0.10.0 (I6) so the handler body is gap-free top-level guarded.\n this.log.debug(\n `onMessage: command='${oneLine(String(obj?.command ?? \"\"))}' from='${obj?.from}' has-callback=${!!obj?.callback}`,\n );\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n switch (obj.command) {\n case \"checkConnection\":\n await this.handleCheckConnection(obj);\n break;\n case \"addDelivery\":\n await this.handleAddDelivery(obj);\n break;\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${oneLine(String(obj.command))}'`);\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n // v0.4.3 (F7): trace catch so the debug log shows what failed. The\n // reply itself is guarded too (v0.10.0, I6) \u2014 a synchronous sendTo\n // throw must not escape the message handler. checkConnection replies\n // use the admin {error} envelope (H1), scripts keep the documented\n // {success, error_message} shape.\n try {\n this.log.debug(`onMessage: '${oneLine(String(obj?.command ?? \"\"))}' failed: ${errText(err)}`);\n if (obj?.callback) {\n const reply =\n obj.command === \"checkConnection\"\n ? { error: errText(err) }\n : { success: false, error_message: errText(err) };\n this.sendTo(obj.from, obj.command, reply, obj.callback);\n }\n } catch {\n /* reply channel gone \u2014 nothing left to do */\n }\n }\n }\n\n /**\n * Admin \"Test Connection\" button (H1). The jsonConfig sendTo component reads\n * ONLY `response.error` / `response.result` \u2014 never success/message \u2014 so the\n * internal `{success, message}` result is mapped to that contract here.\n * Before this, a FAILED test showed a false-positive \"Ok\" in the admin\n * (fleet fix; beszel's message-router is the model).\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleCheckConnection(obj: ioBroker.Message): Promise<void> {\n const msg = obj.message as { apiKey?: unknown } | null | undefined;\n // API-boundary guard: a script may put anything here \u2014 a number used to\n // surface as \"trim is not a function\" instead of the plain rejection.\n const key = typeof msg?.apiKey === \"string\" ? msg.apiKey.trim() : \"\";\n if (!key || key.length < MIN_API_KEY_LENGTH) {\n // v0.4.3 (F2): trace the reject before sendTo.\n this.log.debug(\"checkConnection: apiKey too short\");\n this.sendTo(obj.from, obj.command, { error: \"API key is too short\" }, obj.callback);\n return;\n }\n // L2: a Test-Connection GET counts against the same 20/hour budget as\n // polling. Guard against a concurrent second test (double-click / admin\n // re-render) so stacked clicks can't each burn a GET (which could later trip\n // the poll's rate-limit cooldown). Set BEFORE the await so the check is\n // synchronous against a still-in-flight first test.\n if (this.testConnectionInFlight) {\n this.log.debug(\"checkConnection: a test is already running\");\n this.sendTo(obj.from, obj.command, { error: \"A connection test is already running \u2014 please wait\" }, obj.callback);\n return;\n }\n this.testConnectionInFlight = true;\n // v0.4.3: same debug-logger as the prod client so checkConnection\n // failures get the same HTTPS-layer trace (via the makeClient seam).\n const testClient = this.makeClient(key);\n // v0.4.4: register test-client so onUnload can abort its inflight\n // HTTPS-request \u2014 the adapter's `this.client.cancelAll()` only\n // touches the prod-client, not these short-lived test-clients.\n this.testClients.add(testClient);\n try {\n const result = await testClient.testConnection();\n // v0.4.3 (F3): trace checkConnection result.\n this.log.debug(`checkConnection: result=${result.success ? \"ok\" : \"fail\"} (${result.message})`);\n this.sendTo(\n obj.from,\n obj.command,\n result.success ? { result: result.message } : { error: result.message },\n obj.callback,\n );\n } finally {\n this.testClients.delete(testClient);\n this.testConnectionInFlight = false;\n }\n }\n\n /**\n * Reply an addDelivery failure to the sendTo caller. This is the documented\n * script API envelope (`{success: false, error_message}`) \u2014 unchanged for\n * backward compatibility; only the admin checkConnection uses {error}.\n *\n * @param obj The sendTo message being answered\n * @param message Human-readable failure reason\n */\n private replyAddError(obj: ioBroker.Message, message: string): void {\n this.sendTo(obj.from, obj.command, { success: false, error_message: message }, obj.callback);\n }\n\n /**\n * Script-facing addDelivery command: validate the message shape, cap field\n * lengths, throttle bursts, forward to the API and trigger a poll on\n * success. Extracted from the onMessage switch (M9) \u2014 one command, one\n * method, one change reason.\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleAddDelivery(obj: ioBroker.Message): Promise<void> {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\n this.replyAddError(obj, \"Adapter not initialized\");\n return;\n }\n // v0.7.2: obj.message is `unknown`-shaped \u2014 a script calling\n // sendTo(\"parcelapp\", \"addDelivery\", null) used to surface as an\n // ugly TypeError through the catch instead of a clear validation\n // message. Coerce to a plain object and validate required fields.\n const raw = obj.message;\n const msg = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};\n if (\n typeof msg.tracking_number !== \"string\" ||\n msg.tracking_number.length === 0 ||\n typeof msg.carrier_code !== \"string\" ||\n msg.carrier_code.length === 0 ||\n typeof msg.description !== \"string\" ||\n msg.description.length === 0\n ) {\n this.log.debug(\"addDelivery: missing tracking_number/carrier_code/description in message\");\n this.replyAddError(obj, \"tracking_number, carrier_code and description are required\");\n return;\n }\n // v0.9.0 (S5): cap field lengths so a runaway local script can't push\n // a multi-MB POST body to parcel.app. Checked after the required-field\n // guard above so the two validation messages stay distinct.\n if (\n msg.tracking_number.length > MAX_ADD_FIELD_LEN ||\n msg.carrier_code.length > MAX_ADD_FIELD_LEN ||\n msg.description.length > MAX_ADD_FIELD_LEN ||\n (typeof msg.language === \"string\" && msg.language.length > MAX_ADD_FIELD_LEN)\n ) {\n this.log.debug(\"addDelivery: a field exceeds the maximum length\");\n this.replyAddError(obj, `each field must be at most ${MAX_ADD_FIELD_LEN} characters`);\n return;\n }\n // Pass the optional API fields through when the caller supplies them\n // (language: ISO 639-1 two-letter code; send_push_confirmation: push\n // notification once the delivery is added).\n const request: AddDeliveryRequest = {\n tracking_number: msg.tracking_number,\n carrier_code: msg.carrier_code,\n description: msg.description,\n };\n if (typeof msg.language === \"string\" && msg.language.length > 0) {\n request.language = msg.language;\n }\n if (typeof msg.send_push_confirmation === \"boolean\") {\n request.send_push_confirmation = msg.send_push_confirmation;\n }\n // v0.9.0 (S4): throttle addDelivery POSTs. parcel.app caps ~20/day\n // server-side; this stops a runaway/buggy script from hammering the API\n // with a burst. Record the attempt before the await so concurrent\n // callers count too.\n const nowMs = Date.now();\n this.addTimestamps = this.addTimestamps.filter(t => nowMs - t < ADD_WINDOW_MS);\n if (this.addTimestamps.length >= MAX_ADDS_PER_WINDOW) {\n this.log.warn(`addDelivery throttled: more than ${MAX_ADDS_PER_WINDOW} requests within ${ADD_WINDOW_MS / 1000}s`);\n this.replyAddError(obj, `too many addDelivery requests; max ${MAX_ADDS_PER_WINDOW} per ${ADD_WINDOW_MS / 1000}s`);\n return;\n }\n this.addTimestamps.push(nowMs);\n const addResult = await this.client.addDelivery(request);\n // v0.4.3 (F5): trace addDelivery result with the (flattened) tracking number.\n // v0.10.0 (L9): the drift-guarded isTrueish \u2014 getDeliveries hardens the same\n // API flag; a drifted `success: \"false\"` string must not read as truthy.\n const added = isTrueish(addResult.success);\n this.log.debug(`addDelivery: '${oneLine(request.tracking_number)}' result=${added ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (added) {\n // v0.10.0 (L5): plain poll, no force \u2014 a single add still polls right\n // away (the last poll is usually >60s back), but an add-burst can no\n // longer stack force-GETs past the 20/h API budget. Nothing is lost:\n // the server caches the list ~45-90 min, a fresh package rarely shows\n // tracking data immediately anyway.\n void this.poll().catch(err => this.log.error(`Poll after addDelivery failed: ${errText(err)}`));\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n // One getObject per adapter start, forever \u2014 deliberately kept (I3/ARCH-24):\n // a one-shot migration marker would cost more mechanics than this read.\n // Drop the list entirely at the next major once 0.1.x installs are gone.\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 * v0.10.0 (M1): the client codes every failure it raises (TIMEOUT,\n * PARSE_ERROR, ABORTED, RATE_LIMITED, \u2026) \u2014 a present machine code always\n * wins. The message-substring sniff only remains for code-less foreign\n * errors, so an API error_message merely CONTAINING \"timeout\" can no longer\n * be misclassified.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code) {\n if (NETWORK_ERROR_CODES.has(error.code)) {\n return \"NETWORK\";\n }\n if (error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code;\n }\n if (error.message.includes(\"timeout\")) {\n return \"TIMEOUT\";\n }\n return \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n // v0.10.0 (M4): make the re-entry/uninitialized skip visible like the\n // rate-limit/throttle skips below \u2014 this used to be the one silent spot\n // where \"the adapter does nothing\" left no trace.\n this.log.debug(\"Skipping poll \u2014 already running or not initialized\");\n return;\n }\n // v0.10.0 (L14): local bindings instead of non-null assertions in the\n // closures below \u2014 provable narrowing the compiler checks, robust against\n // a future refactor nulling the fields mid-poll.\n const client = this.client;\n const stateManager = this.stateManager;\n\n const now = Date.now();\n // v0.4.3 (B1): poll-entry anchor \u2014 visible after the re-entry guard but\n // before the rate-limit/throttle skips. Shows mode + current error state\n // so the debug log gives context for whatever follows.\n const autoRemoveMode = this.config.autoRemoveDelivered !== false;\n this.log.debug(`poll: starting (autoRemove=${autoRemoveMode}, lastErrorCode='${this.lastErrorCode}')`);\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);\n return;\n }\n\n // Throttle: minimum gap between polls (also paces the poll after a\n // successful addDelivery \u2014 see handleAddDelivery).\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 deliveries = await client.getDeliveries(autoRemoveMode ? \"active\" : \"recent\");\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n // The GET succeeded \u2014 from here on a failure is the broker's, not the\n // API's. v0.10.0 (M2) fenced cleanup/summary that way but left this write\n // outside: a broker hiccup here painted a \"Poll failed\" error line, flipped\n // the indicator to false and poisoned the error dedup.\n try {\n await this.setStateChangedAsync(\"info.connection\", { val: true, ack: true });\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => stateManager.parseStatus(d) !== DELIVERED_STATUS_CODE);\n const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset the per-poll collision tracker, then compute every\n // package id in a deterministic sequential pre-pass (stable array order)\n // BEFORE the parallel updates \u2014 collision-suffixing is then deterministic\n // and packageId runs exactly once per delivery instead of twice.\n stateManager.resetPollState();\n const pkgIds = visibleDeliveries.map(d => stateManager.packageId(d));\n\n // v0.4.2 (M4): per-delivery updates run in parallel, each wrapped in\n // try/catch so one bad delivery doesn't poison the others.\n // v0.9.0 (S3): process the updates in bounded batches instead of one\n // broker fan-out for ALL deliveries at once. The keep-set is still every\n // visible pkgId (computed above), so this only caps concurrency \u2014 it never\n // drops a package. A normal poll (a handful of packages) is a single batch.\n if (visibleDeliveries.length > UPDATE_BATCH_SIZE) {\n this.log.debug(`Updating ${visibleDeliveries.length} deliveries in batches of ${UPDATE_BATCH_SIZE}`);\n }\n for (let start = 0; start < visibleDeliveries.length; start += UPDATE_BATCH_SIZE) {\n const batch = visibleDeliveries.slice(start, start + UPDATE_BATCH_SIZE);\n await Promise.all(\n batch.map(async (delivery, offset) => {\n const pkgId = pkgIds[start + offset];\n // Pre-sanitize the externally-sourced strings once for logging. The\n // fields are optional (the API can drop them), so default to \"\" before\n // oneLine flattens them onto a single log line.\n const tracking = oneLine(delivery.tracking_number ?? \"\");\n const carrier = oneLine(delivery.carrier_code ?? \"\");\n try {\n // v0.4.3 (C1): per-delivery entry. ~10 packages \u00D7 144 polls/day\n // = ~1440 debug lines/day \u2014 acceptable at debug-level. Line stays\n // short (tracking + carrier + status only, no full delivery JSON).\n // v0.10.0 (M6): status_code is typed number|string (drift), so it\n // is flattened like the other externally-sourced fields.\n this.log.debug(\n `updateDelivery: '${tracking}' carrier=${carrier} status=${oneLine(String(delivery.status_code))}`,\n );\n const carrierName = await client.getCarrierName(delivery.carrier_code);\n await stateManager.updateDelivery(delivery, carrierName, pkgId);\n this.failedDeliveries.delete(pkgId);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(pkgId)) {\n this.log.debug(`Failed to update '${tracking}': ${msg}`);\n } else if (this.unloaded) {\n // v0.10.0 (L2): broker teardown mid-batch is expected noise\n // during shutdown, not a per-package warning.\n this.log.debug(`Failed to update '${tracking}' during shutdown: ${msg}`);\n } else {\n this.log.warn(`Failed to update '${tracking}': ${msg}`);\n this.failedDeliveries.add(pkgId);\n }\n }\n }),\n );\n }\n\n // v0.9.0 (C1): keep-set = EVERY package the API still returns this poll\n // (pkgIds), NOT only the writes that just succeeded. A transient\n // updateDelivery failure leaves a package's states stale, but it must not\n // drop the package from cleanup \u2014 that would delete a still-present\n // package's states (and any user-set device name) at green info.connection.\n // v0.10.0 (M2): broker-side failures in cleanup/summary are NOT API\n // failures \u2014 they must neither flip info.connection to false (the GET\n // above just succeeded) nor run through the API error classification.\n try {\n await stateManager.cleanupDeliveries(pkgIds);\n // Update summary (always uses active/non-delivered)\n await stateManager.updateSummary(activeDeliveries);\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Keep failedDeliveries bounded: drop entries for package ids no longer\n // present, so packages that vanish from the API don't linger forever.\n const seenPkgIds = new Set(pkgIds);\n for (const id of [...this.failedDeliveries]) {\n if (!seenPkgIds.has(id)) {\n this.failedDeliveries.delete(id);\n }\n }\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n await this.handlePollError(err as Error & { code?: string; retryAfterSeconds?: number });\n } finally {\n this.isPolling = false;\n }\n }\n\n /**\n * Classify + route a poll failure: log level, dedup, cooldown and the\n * info.connection=false write. Extracted from poll()'s catch (M9) so the\n * happy path reads as a plain sequence and the error policy sits next to\n * classifyError. Dispatches on the CLASSIFIED code only (L6) \u2014 one source\n * of truth for the error class.\n *\n * @param error The poll failure (usually an ApiError from the client)\n */\n private async handlePollError(error: Error & { code?: string; retryAfterSeconds?: number }): Promise<void> {\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n switch (errorCode) {\n case \"ABORTED\":\n // v0.10.0 (M1): expected during shutdown \u2014 cancelAll aborts the\n // in-flight GET. A deliberate stop must not paint a red error line.\n this.log.debug(`Poll aborted: ${error.message}`);\n break;\n case \"RATE_LIMITED\": {\n // v0.4.2 (M9): clamp Retry-After into [60s, 24h] (shared constants\n // with the client parser, L7). A bogus 0/negative/fractional value\n // must neither wipe the cooldown nor set it for milliseconds.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(RETRY_AFTER_MAX_SEC, Math.max(60, Math.floor(rawCooldown)))\n : RETRY_AFTER_DEFAULT_SEC;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n // v0.10.0 (M3): warn once \u2014 a persistent 429 repeats at debug.\n const line = `Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`;\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.warn(line);\n }\n break;\n }\n case \"FORBIDDEN\": {\n // v0.4.2 (P3): 403 is a permission issue (e.g. Premium subscription\n // expired). Reauth wouldn't help \u2014 surface a clear hint.\n // v0.10.0 (M3): once at error level, repeats at debug \u2014 not 144\n // identical error lines per day for one unchanged account problem.\n const line =\n \"parcel.app returned 403 Forbidden \u2014 your account may not have an active Premium subscription, or the API key was revoked. Check your account on parcelapp.net.\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"INVALID_API_KEY\": {\n // v0.10.0 (M3): first occurrence at error (the user must fix the\n // config; info.connection goes red too) \u2014 repeats at debug.\n const line = \"Invalid API key \u2014 please check your parcel.app API key\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"NETWORK\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n }\n break;\n case \"TIMEOUT\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n }\n break;\n default:\n 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 {\n this.log.error(`Poll failed: ${error.message}`);\n }\n }\n\n // C2: setStateChangedAsync avoids redundant `false` writes on sustained\n // failure. The `.catch` keeps poll() from rejecting when the broker is\n // already down, so the fire-and-forget callers never see an unhandled\n // rejection (no global process handler needed).\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker shutting down \u2014 ignore */\n });\n }\n}\n\n// Process entry point. Not reachable from the unit tests (they import the class\n// instead of running the file as a process), but NOT untested: `npm run\n// test:integration` boots the adapter through exactly this branch inside a real\n// js-controller, which is what catches module-scope startup crashes.\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { coerceClampedInt, errText, isTrueish, oneLine } from \"./lib/coerce\";\nimport { tName } from \"./lib/i18n\";\nimport { ParcelClient, RETRY_AFTER_DEFAULT_SEC, RETRY_AFTER_MAX_SEC } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport { DELIVERED_STATUS_CODE } from \"./lib/types\";\nimport type { AddDeliveryRequest } from \"./lib/types\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\n// Minimum 60s between polls. Also the natural pacing after addDelivery: a\n// typical single add still polls immediately (the last poll is >60s ago),\n// while a burst of adds collapses to at most one extra GET per minute.\nconst MIN_POLL_GAP_MS = 60_000;\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n// v0.9.0 (S5): cap addDelivery field lengths. A sendTo caller is local, but a\n// runaway script must not push a multi-MB POST body to parcel.app. Identifiers\n// are short; this is generous for a description.\nconst MAX_ADD_FIELD_LEN = 512;\n// v0.9.0 (S3): cap the per-poll broker fan-out. Updates run in batches of this\n// size instead of all deliveries at once, so an abnormally large API response\n// can't flood the broker with thousands of concurrent writes.\nconst UPDATE_BATCH_SIZE = 25;\n// v0.9.0 (S4): client-side throttle on addDelivery POSTs. parcel.app enforces\n// ~20 POST/day server-side; this caps a runaway/buggy script's burst so it can't\n// hammer the API. The window is generous enough never to block a real batch-add\n// (the daily server limit is the real cap).\nconst MAX_ADDS_PER_WINDOW = 20;\nconst ADD_WINDOW_MS = 60_000;\n/**\n * v0.11.0: config keys past versions wrote into the instance object and that nothing reads any\n * more \u2014 `filterMode` was replaced by `autoRemoveDelivered` in v0.2.0, `language` by the system\n * language before v0.5.0. An upgrade merges the manifest in and never removes a key, so they\n * survive in every installation old enough to have them. See `correctInstanceObject`.\n */\nconst OBSOLETE_NATIVE_KEYS = [\"filterMode\", \"language\"] as const;\n\n/**\n * Node transport-level error codes treated as (transient) NETWORK problems:\n * DNS, connection refused/reset, unreachable \u2014 plus EPIPE/ECONNABORTED/EPROTO\n * (v0.10.0, I9), which belong to the same warn+keep-retrying class.\n */\nconst NETWORK_ERROR_CODES = new Set([\n \"ENOTFOUND\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENETUNREACH\",\n \"EHOSTUNREACH\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"ECONNABORTED\",\n \"EPROTO\",\n]);\n\n/**\n * v0.10.0 (L12): the seam contracts name exactly the members main uses \u2014\n * structural Picks let the orchestration tests inject plain object fakes\n * without unsafe double-casts, and the compiler documents what a fake must\n * provide.\n */\ntype ClientLike = Pick<\n ParcelClient,\n \"getDeliveries\" | \"getCarrierName\" | \"addDelivery\" | \"testConnection\" | \"cancelAll\"\n>;\ntype StateManagerLike = Pick<\n StateManager,\n \"resetPollState\" | \"packageId\" | \"parseStatus\" | \"updateDelivery\" | \"updateSummary\" | \"cleanupDeliveries\"\n>;\n\n/**\n * ioBroker adapter for parcel.app package tracking. Exported so the\n * orchestration unit tests can drive its lifecycle/poll handlers directly.\n */\nexport class ParcelappAdapter extends utils.Adapter {\n private client: ClientLike | null = null;\n private stateManager: StateManagerLike | null = null;\n /**\n * Factories for the HTTP client + state manager \u2014 default to the real\n * constructors. Test seams (fleet pattern): unit tests replace these with\n * fakes to exercise the poll orchestration (throttle/rate-limit interplay,\n * error routing, failure dedup) without real network.\n *\n * @param apiKey parcel.app API key\n */\n private makeClient: (apiKey: string) => ClientLike = apiKey =>\n new ParcelClient(apiKey, { debug: (m: string) => this.log.debug(m) });\n private makeStateManager: () => StateManagerLike = () => new StateManager(this);\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n /**\n * v0.10.0 (L2): set in onUnload. onReady checks it after its awaits so a\n * stop during the first poll can no longer arm the interval or log\n * \"started\" after the unload already ran; batch failures during shutdown\n * degrade to debug.\n */\n private unloaded = false;\n /**\n * Package ids (not raw tracking numbers) whose last updateDelivery failed.\n * Keyed like the states so the dedup survives a sanitize collision or a\n * missing tracking number; pruned each poll against the visible pkgIds.\n */\n private failedDeliveries = new Set<string>();\n /** Timestamps of recent addDelivery POSTs \u2014 the S4 throttle window. */\n private addTimestamps: number[] = [];\n /**\n * L2: true while a checkConnection test GET is in flight. A test hits the same\n * 20/hour GET budget as polling; this guards against a concurrent second test\n * (double-click / admin re-render) stacking a redundant GET. A sequential\n * re-test after the current one settles runs normally.\n */\n private testConnectionInFlight = false;\n /**\n * v0.4.4: short-lived test-clients spawned from `checkConnection` admin\n * messages. The prod-`this.client` is what `onUnload` cancels, so these\n * need their own registry to be reachable at shutdown. Without this, an\n * admin clicking \"Test Connection\" right before adapter-stop could keep\n * the process alive past js-controller's 4-second kill deadline.\n */\n private testClients = new Set<ClientLike>();\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 /**\n * Repair leftovers an upgrade cannot remove from this instance's own object.\n *\n * js-controller MERGES the manifest into an existing instance object and never drops a key,\n * so anything a past version wrote survives forever. Two such leftovers exist:\n *\n * 1. `common.supportedMessages` (dropped from the manifest in v0.10.3). The whole key is\n * removed, and setting it to `{stopInstance: false}` \u2014 what v0.10.3 through v0.10.4 did \u2014\n * is NOT a fix but a second, worse defect. `isMessageboxSupported()` in the adapter package\n * stops looking at `common.messagebox` the moment `supportedMessages` is an object, and then\n * only reports true when SOME value is not false:\n *\n * if (!isObject(common.supportedMessages)) return !!common.messagebox;\n * return Object.values(common.supportedMessages).find(v => v !== false) !== undefined;\n *\n * With `{stopInstance: false}` that is `false`, so the adapter never runs\n * `subscribeMessage` and NOTHING reaches `onMessage` \u2014 the admin's \"Test Connection\" button\n * and every `sendTo(..., \"addDelivery\", ...)` from a script die silently. Silently, because\n * the error branch next to that call only fires for `options.message`, and we register via\n * `this.on(\"message\", \u2026)`. The old `{stopInstance: true}` kept the box open by accident\n * (`true !== false`), which is why this only broke with the v0.10.3 correction.\n * 2. `native.filterMode` (dropped in v0.2.0) and `native.language` (dropped before v0.5.0).\n * Dead config keys nothing reads \u2014 the adapter owns its own configuration inventory the\n * same way it owns its states (`cleanupObsoleteStates`).\n *\n * Both are corrected in ONE write, because every instance-object change restarts the instance:\n * two writes would mean two restarts. And it is written only when something is actually stale,\n * otherwise the correction would be a restart loop.\n *\n * `extendObject` cannot do this: its deep merge (`node.extend`) SETS a key given as `null`\n * instead of dropping it \u2014 measured against the bundled js-controller. Removing a key needs a\n * read/delete/`setForeignObject` round-trip, which is the same pattern js-controller's own\n * `updateConfig()`/`disable()` use. `native.apiKey` rides along untouched and stays encrypted:\n * `getForeignObject` does not decrypt (that happens only into `this.config`).\n *\n * @returns true when a correction was written and the restart is coming \u2014 the caller has\n * to stop right there instead of polling in a process that is going down.\n */\n private async correctInstanceObject(): Promise<boolean> {\n const id = `system.adapter.${this.namespace}`;\n try {\n const obj = await this.getForeignObjectAsync(id);\n if (!obj) {\n return false;\n }\n // Any leftover supportedMessages has to GO, not be set to false. isMessageboxSupported()\n // stops looking at common.messagebox the moment supportedMessages is an object, and then\n // only accepts it when some value is not false \u2014 `{stopInstance: false}` closes the box.\n const staleSupportedMessages = \"supportedMessages\" in (obj.common ?? {});\n const native = (obj.native ?? {}) as Record<string, unknown>;\n const staleNativeKeys = OBSOLETE_NATIVE_KEYS.filter(key => key in native);\n if (!staleSupportedMessages && staleNativeKeys.length === 0) {\n return false;\n }\n this.log.info(\"Correcting leftover settings from an earlier version \u2014 this instance restarts once\");\n if (staleSupportedMessages) {\n this.log.debug(\"Removing the obsolete supportedMessages entry so the message box stays open\");\n delete obj.common.supportedMessages;\n }\n for (const key of staleNativeKeys) {\n this.log.debug(`Removing obsolete configuration key: native.${key}`);\n delete native[key];\n }\n obj.native = native;\n await this.setForeignObject(id, obj);\n return true;\n } catch (err) {\n // Objects DB unreachable \u2014 not worth failing the start over; the next start retries.\n this.log.debug(`Could not check the instance object ${id}: ${errText(err)}`);\n return false;\n }\n }\n\n /**\n * Re-apply the manifest objects' `common` to an EXISTING installation.\n *\n * js-controller creates `io-package.json:instanceObjects` only where they are MISSING, so a\n * changed name or description otherwise reaches fresh installs only \u2014 measured on a live\n * install, `info` and `info.connection` still carried their plain-English pre-i18n names while\n * manifest, linter, type check and the name gate were all green. Each id is spelled out rather\n * than looped over the manifest: the consistency gate looks for the literal id at the\n * `extendObject` call, and a loop would be DRYer but unverifiable.\n *\n * `desc` is set only where there is something to explain \u2014 a folder called \"Deliveries\"\n * explains itself, and an invented sentence is worse than none.\n */\n private async refreshManifestObjects(): Promise<void> {\n await this.extendObject(\"info\", {\n type: \"channel\",\n common: { name: tName(\"info\") },\n native: {},\n });\n await this.extendObject(\"info.connection\", {\n type: \"state\",\n common: {\n name: tName(\"infoConnection\"),\n desc: tName(\"descInfoConnection\"),\n type: \"boolean\",\n role: \"indicator.connected\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n });\n await this.extendObject(\"deliveries\", {\n type: \"folder\",\n common: { name: tName(\"deliveries\") },\n native: {},\n });\n await this.extendObject(\"summary\", {\n type: \"channel\",\n common: { name: tName(\"summary\") },\n native: {},\n });\n }\n\n private async onReady(): Promise<void> {\n try {\n // First: without this the whole shutdown path stays dead on an updated install.\n // A correction means the host is restarting us \u2014 no point starting a poll cycle.\n if (await this.correctInstanceObject()) {\n return;\n }\n // I18n.init resolves system.config.language itself (adapter-core reads\n // the foreign object internally; unknown languages fall back to English)\n // \u2014 the former separate getForeignObject round-trip and the local\n // resolveLanguage step are gone with the STATUS_LABELS table (L20).\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.log.debug(`onReady: starting (autoRemoveDelivered=${this.config.autoRemoveDelivered})`);\n\n try {\n await this.refreshManifestObjects();\n } catch (err) {\n // Same class as cleanupObsoleteStates (C8): a broker hiccup while refreshing object\n // texts must not abort the start \u2014 the next start reapplies them. Degrade to a warning.\n this.log.warn(`Refreshing the manifest objects failed (continuing): ${errText(err)}`);\n }\n\n await this.setState(\"info.connection\", { val: false, ack: true });\n\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {\n this.log.error(\"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\");\n return;\n }\n\n this.client = this.makeClient(apiKey.trim());\n this.stateManager = this.makeStateManager();\n\n try {\n await this.cleanupObsoleteStates();\n } catch (err) {\n // C8: a cleanup failure must not abort startup. Without this guard the\n // outer catch would skip arming the poll interval below, and the adapter\n // would never poll until a manual restart. Degrade to a warning.\n this.log.warn(`cleanupObsoleteStates failed (continuing): ${errText(err)}`);\n }\n\n await this.poll();\n\n // v0.10.0 (L2): a stop during the awaits above must not arm a timer or\n // log a start line after onUnload already ran.\n if (this.unloaded) {\n return;\n }\n\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n this.log.debug(`pollInterval: raw=${JSON.stringify(this.config.pollInterval)} resolved=${interval}min`);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => {\n void this.poll().catch(err => this.log.error(`Scheduled poll failed: ${errText(err)}`));\n }, intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n // v0.10.0 (L4): a transient startup failure (i18n files, DB hiccup) used\n // to leave a green-looking zombie \u2014 no client, no timer, no retry until\n // a manual restart. Terminate instead: js-controller restarts the\n // instance, which self-heals the transient case; its restart-loop guard\n // backstops a persistent one.\n if (!this.unloaded) {\n this.terminate(\"startup failed \u2014 requesting restart\", utils.EXIT_CODES.START_IMMEDIATELY_AFTER_STOP);\n }\n }\n }\n\n /**\n * v0.4.2 (M5+X5): delegate to the shared `coerceClampedInt` helper.\n *\n * @param raw Raw `pollInterval` from admin config (number or numeric string).\n */\n private static coercePollInterval(raw: unknown): number {\n return coerceClampedInt(raw, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL, DEFAULT_POLL_INTERVAL);\n }\n\n private onUnload(callback: () => void): void {\n this.unloaded = true;\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n // v0.4.2 (M11+P1): cancel every in-flight HTTPS request so a slow\n // parcel.app endpoint doesn't keep the adapter alive past\n // js-controller's 4-second kill deadline. cancelAll also marks the\n // client terminal (v0.10.0, L3) \u2014 requests started after this point\n // are refused instead of opening fresh connections.\n this.client?.cancelAll();\n // v0.4.4: also abort any short-lived test-client (from checkConnection)\n // whose HTTPS-request might still be inflight at shutdown \u2014 the prod\n // `this.client.cancelAll()` only touches the production-client.\n for (const tc of this.testClients) {\n tc.cancelAll();\n }\n this.testClients.clear();\n // v0.10.3: report done only once the disconnect write landed. Calling back\n // first means the host tears the process down while the write is still in\n // flight and the instance keeps claiming a live connection. No own deadline\n // needed \u2014 the host already has one (`common.stopTimeout`), and\n // `this.setTimeout` refuses during shutdown anyway.\n void this.setState(\"info.connection\", { val: false, ack: true })\n .catch(() => {\n /* broker is shutting down \u2014 ignore */\n })\n .finally(callback);\n return;\n } catch (err) {\n // v0.4.3 (G4): replace silent `// ignore` with a trace so shutdown\n // errors leave a debug breadcrumb. Broker-already-down errors here\n // are expected \u2014 debug-level keeps them out of the user log.\n try {\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n } catch {\n /* logger already gone \u2014 nothing left to report to */\n }\n // v0.10.0 (I7): the callback is the contract with js-controller \u2014 exactly\n // once. v0.10.3: the happy path returns above with the callback chained to\n // the disconnect write, so this catch is the only other exit.\n callback();\n }\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise. Inside the try since\n // v0.10.0 (I6) so the handler body is gap-free top-level guarded.\n this.log.debug(\n `onMessage: command='${oneLine(String(obj?.command ?? \"\"))}' from='${obj?.from}' has-callback=${!!obj?.callback}`,\n );\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n switch (obj.command) {\n case \"checkConnection\":\n await this.handleCheckConnection(obj);\n break;\n case \"addDelivery\":\n await this.handleAddDelivery(obj);\n break;\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${oneLine(String(obj.command))}'`);\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n // v0.4.3 (F7): trace catch so the debug log shows what failed. The\n // reply itself is guarded too (v0.10.0, I6) \u2014 a synchronous sendTo\n // throw must not escape the message handler. checkConnection replies\n // use the admin {error} envelope (H1), scripts keep the documented\n // {success, error_message} shape.\n try {\n this.log.debug(`onMessage: '${oneLine(String(obj?.command ?? \"\"))}' failed: ${errText(err)}`);\n if (obj?.callback) {\n const reply =\n obj.command === \"checkConnection\"\n ? { error: errText(err) }\n : { success: false, error_message: errText(err) };\n this.sendTo(obj.from, obj.command, reply, obj.callback);\n }\n } catch {\n /* reply channel gone \u2014 nothing left to do */\n }\n }\n }\n\n /**\n * Admin \"Test Connection\" button (H1). The jsonConfig sendTo component reads\n * ONLY `response.error` / `response.result` \u2014 never success/message \u2014 so the\n * internal `{success, message}` result is mapped to that contract here.\n * Before this, a FAILED test showed a false-positive \"Ok\" in the admin\n * (fleet fix; beszel's message-router is the model).\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleCheckConnection(obj: ioBroker.Message): Promise<void> {\n const msg = obj.message as { apiKey?: unknown } | null | undefined;\n // API-boundary guard: a script may put anything here \u2014 a number used to\n // surface as \"trim is not a function\" instead of the plain rejection.\n const key = typeof msg?.apiKey === \"string\" ? msg.apiKey.trim() : \"\";\n if (!key || key.length < MIN_API_KEY_LENGTH) {\n // v0.4.3 (F2): trace the reject before sendTo.\n this.log.debug(\"checkConnection: apiKey too short\");\n this.sendTo(obj.from, obj.command, { error: \"API key is too short\" }, obj.callback);\n return;\n }\n // L2: a Test-Connection GET counts against the same 20/hour budget as\n // polling. Guard against a concurrent second test (double-click / admin\n // re-render) so stacked clicks can't each burn a GET (which could later trip\n // the poll's rate-limit cooldown). Set BEFORE the await so the check is\n // synchronous against a still-in-flight first test.\n if (this.testConnectionInFlight) {\n this.log.debug(\"checkConnection: a test is already running\");\n this.sendTo(obj.from, obj.command, { error: \"A connection test is already running \u2014 please wait\" }, obj.callback);\n return;\n }\n this.testConnectionInFlight = true;\n // v0.4.3: same debug-logger as the prod client so checkConnection\n // failures get the same HTTPS-layer trace (via the makeClient seam).\n const testClient = this.makeClient(key);\n // v0.4.4: register test-client so onUnload can abort its inflight\n // HTTPS-request \u2014 the adapter's `this.client.cancelAll()` only\n // touches the prod-client, not these short-lived test-clients.\n this.testClients.add(testClient);\n try {\n const result = await testClient.testConnection();\n // v0.4.3 (F3): trace checkConnection result.\n this.log.debug(`checkConnection: result=${result.success ? \"ok\" : \"fail\"} (${result.message})`);\n this.sendTo(\n obj.from,\n obj.command,\n result.success ? { result: result.message } : { error: result.message },\n obj.callback,\n );\n } finally {\n this.testClients.delete(testClient);\n this.testConnectionInFlight = false;\n }\n }\n\n /**\n * Reply an addDelivery failure to the sendTo caller. This is the documented\n * script API envelope (`{success: false, error_message}`) \u2014 unchanged for\n * backward compatibility; only the admin checkConnection uses {error}.\n *\n * @param obj The sendTo message being answered\n * @param message Human-readable failure reason\n */\n private replyAddError(obj: ioBroker.Message, message: string): void {\n this.sendTo(obj.from, obj.command, { success: false, error_message: message }, obj.callback);\n }\n\n /**\n * Script-facing addDelivery command: validate the message shape, cap field\n * lengths, throttle bursts, forward to the API and trigger a poll on\n * success. Extracted from the onMessage switch (M9) \u2014 one command, one\n * method, one change reason.\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleAddDelivery(obj: ioBroker.Message): Promise<void> {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\n this.replyAddError(obj, \"Adapter not initialized\");\n return;\n }\n // v0.7.2: obj.message is `unknown`-shaped \u2014 a script calling\n // sendTo(\"parcelapp\", \"addDelivery\", null) used to surface as an\n // ugly TypeError through the catch instead of a clear validation\n // message. Coerce to a plain object and validate required fields.\n const raw = obj.message;\n const msg = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};\n if (\n typeof msg.tracking_number !== \"string\" ||\n msg.tracking_number.length === 0 ||\n typeof msg.carrier_code !== \"string\" ||\n msg.carrier_code.length === 0 ||\n typeof msg.description !== \"string\" ||\n msg.description.length === 0\n ) {\n this.log.debug(\"addDelivery: missing tracking_number/carrier_code/description in message\");\n this.replyAddError(obj, \"tracking_number, carrier_code and description are required\");\n return;\n }\n // v0.9.0 (S5): cap field lengths so a runaway local script can't push\n // a multi-MB POST body to parcel.app. Checked after the required-field\n // guard above so the two validation messages stay distinct.\n if (\n msg.tracking_number.length > MAX_ADD_FIELD_LEN ||\n msg.carrier_code.length > MAX_ADD_FIELD_LEN ||\n msg.description.length > MAX_ADD_FIELD_LEN ||\n (typeof msg.language === \"string\" && msg.language.length > MAX_ADD_FIELD_LEN)\n ) {\n this.log.debug(\"addDelivery: a field exceeds the maximum length\");\n this.replyAddError(obj, `each field must be at most ${MAX_ADD_FIELD_LEN} characters`);\n return;\n }\n // Pass the optional API fields through when the caller supplies them\n // (language: ISO 639-1 two-letter code; send_push_confirmation: push\n // notification once the delivery is added).\n const request: AddDeliveryRequest = {\n tracking_number: msg.tracking_number,\n carrier_code: msg.carrier_code,\n description: msg.description,\n };\n if (typeof msg.language === \"string\" && msg.language.length > 0) {\n request.language = msg.language;\n }\n if (typeof msg.send_push_confirmation === \"boolean\") {\n request.send_push_confirmation = msg.send_push_confirmation;\n }\n // v0.9.0 (S4): throttle addDelivery POSTs. parcel.app caps ~20/day\n // server-side; this stops a runaway/buggy script from hammering the API\n // with a burst. Record the attempt before the await so concurrent\n // callers count too.\n const nowMs = Date.now();\n this.addTimestamps = this.addTimestamps.filter(t => nowMs - t < ADD_WINDOW_MS);\n if (this.addTimestamps.length >= MAX_ADDS_PER_WINDOW) {\n this.log.warn(`addDelivery throttled: more than ${MAX_ADDS_PER_WINDOW} requests within ${ADD_WINDOW_MS / 1000}s`);\n this.replyAddError(obj, `too many addDelivery requests; max ${MAX_ADDS_PER_WINDOW} per ${ADD_WINDOW_MS / 1000}s`);\n return;\n }\n this.addTimestamps.push(nowMs);\n const addResult = await this.client.addDelivery(request);\n // v0.4.3 (F5): trace addDelivery result with the (flattened) tracking number.\n // v0.10.0 (L9): the drift-guarded isTrueish \u2014 getDeliveries hardens the same\n // API flag; a drifted `success: \"false\"` string must not read as truthy.\n const added = isTrueish(addResult.success);\n this.log.debug(`addDelivery: '${oneLine(request.tracking_number)}' result=${added ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (added) {\n // v0.10.0 (L5): plain poll, no force \u2014 a single add still polls right\n // away (the last poll is usually >60s back), but an add-burst can no\n // longer stack force-GETs past the 20/h API budget. Nothing is lost:\n // the server caches the list ~45-90 min, a fresh package rarely shows\n // tracking data immediately anyway.\n void this.poll().catch(err => this.log.error(`Poll after addDelivery failed: ${errText(err)}`));\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n // One getObject per adapter start, forever \u2014 deliberately kept (I3/ARCH-24):\n // a one-shot migration marker would cost more mechanics than this read.\n // Drop the list entirely at the next major once 0.1.x installs are gone.\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 * v0.10.0 (M1): the client codes every failure it raises (TIMEOUT,\n * PARSE_ERROR, ABORTED, RATE_LIMITED, \u2026) \u2014 a present machine code always\n * wins. The message-substring sniff only remains for code-less foreign\n * errors, so an API error_message merely CONTAINING \"timeout\" can no longer\n * be misclassified.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code) {\n if (NETWORK_ERROR_CODES.has(error.code)) {\n return \"NETWORK\";\n }\n if (error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code;\n }\n if (error.message.includes(\"timeout\")) {\n return \"TIMEOUT\";\n }\n return \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n // v0.10.0 (M4): make the re-entry/uninitialized skip visible like the\n // rate-limit/throttle skips below \u2014 this used to be the one silent spot\n // where \"the adapter does nothing\" left no trace.\n this.log.debug(\"Skipping poll \u2014 already running or not initialized\");\n return;\n }\n // v0.10.0 (L14): local bindings instead of non-null assertions in the\n // closures below \u2014 provable narrowing the compiler checks, robust against\n // a future refactor nulling the fields mid-poll.\n const client = this.client;\n const stateManager = this.stateManager;\n\n const now = Date.now();\n // v0.4.3 (B1): poll-entry anchor \u2014 visible after the re-entry guard but\n // before the rate-limit/throttle skips. Shows mode + current error state\n // so the debug log gives context for whatever follows.\n const autoRemoveMode = this.config.autoRemoveDelivered !== false;\n this.log.debug(`poll: starting (autoRemove=${autoRemoveMode}, lastErrorCode='${this.lastErrorCode}')`);\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);\n return;\n }\n\n // Throttle: minimum gap between polls (also paces the poll after a\n // successful addDelivery \u2014 see handleAddDelivery).\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 deliveries = await client.getDeliveries(autoRemoveMode ? \"active\" : \"recent\");\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n // The GET succeeded \u2014 from here on a failure is the broker's, not the\n // API's. v0.10.0 (M2) fenced cleanup/summary that way but left this write\n // outside: a broker hiccup here painted a \"Poll failed\" error line, flipped\n // the indicator to false and poisoned the error dedup.\n try {\n await this.setStateChangedAsync(\"info.connection\", { val: true, ack: true });\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => stateManager.parseStatus(d) !== DELIVERED_STATUS_CODE);\n const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset the per-poll collision tracker, then compute every\n // package id in a deterministic sequential pre-pass (stable array order)\n // BEFORE the parallel updates \u2014 collision-suffixing is then deterministic\n // and packageId runs exactly once per delivery instead of twice.\n stateManager.resetPollState();\n const pkgIds = visibleDeliveries.map(d => stateManager.packageId(d));\n\n // v0.4.2 (M4): per-delivery updates run in parallel, each wrapped in\n // try/catch so one bad delivery doesn't poison the others.\n // v0.9.0 (S3): process the updates in bounded batches instead of one\n // broker fan-out for ALL deliveries at once. The keep-set is still every\n // visible pkgId (computed above), so this only caps concurrency \u2014 it never\n // drops a package. A normal poll (a handful of packages) is a single batch.\n if (visibleDeliveries.length > UPDATE_BATCH_SIZE) {\n this.log.debug(`Updating ${visibleDeliveries.length} deliveries in batches of ${UPDATE_BATCH_SIZE}`);\n }\n for (let start = 0; start < visibleDeliveries.length; start += UPDATE_BATCH_SIZE) {\n const batch = visibleDeliveries.slice(start, start + UPDATE_BATCH_SIZE);\n await Promise.all(\n batch.map(async (delivery, offset) => {\n const pkgId = pkgIds[start + offset];\n // Pre-sanitize the externally-sourced strings once for logging. The\n // fields are optional (the API can drop them), so default to \"\" before\n // oneLine flattens them onto a single log line.\n const tracking = oneLine(delivery.tracking_number ?? \"\");\n const carrier = oneLine(delivery.carrier_code ?? \"\");\n try {\n // v0.4.3 (C1): per-delivery entry. ~10 packages \u00D7 144 polls/day\n // = ~1440 debug lines/day \u2014 acceptable at debug-level. Line stays\n // short (tracking + carrier + status only, no full delivery JSON).\n // v0.10.0 (M6): status_code is typed number|string (drift), so it\n // is flattened like the other externally-sourced fields.\n this.log.debug(\n `updateDelivery: '${tracking}' carrier=${carrier} status=${oneLine(String(delivery.status_code))}`,\n );\n const carrierName = await client.getCarrierName(delivery.carrier_code);\n await stateManager.updateDelivery(delivery, carrierName, pkgId);\n this.failedDeliveries.delete(pkgId);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(pkgId)) {\n this.log.debug(`Failed to update '${tracking}': ${msg}`);\n } else if (this.unloaded) {\n // v0.10.0 (L2): broker teardown mid-batch is expected noise\n // during shutdown, not a per-package warning.\n this.log.debug(`Failed to update '${tracking}' during shutdown: ${msg}`);\n } else {\n this.log.warn(`Failed to update '${tracking}': ${msg}`);\n this.failedDeliveries.add(pkgId);\n }\n }\n }),\n );\n }\n\n // v0.9.0 (C1): keep-set = EVERY package the API still returns this poll\n // (pkgIds), NOT only the writes that just succeeded. A transient\n // updateDelivery failure leaves a package's states stale, but it must not\n // drop the package from cleanup \u2014 that would delete a still-present\n // package's states (and any user-set device name) at green info.connection.\n // v0.10.0 (M2): broker-side failures in cleanup/summary are NOT API\n // failures \u2014 they must neither flip info.connection to false (the GET\n // above just succeeded) nor run through the API error classification.\n try {\n await stateManager.cleanupDeliveries(pkgIds);\n // Update summary (always uses active/non-delivered)\n await stateManager.updateSummary(activeDeliveries);\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Keep failedDeliveries bounded: drop entries for package ids no longer\n // present, so packages that vanish from the API don't linger forever.\n const seenPkgIds = new Set(pkgIds);\n for (const id of [...this.failedDeliveries]) {\n if (!seenPkgIds.has(id)) {\n this.failedDeliveries.delete(id);\n }\n }\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n await this.handlePollError(err as Error & { code?: string; retryAfterSeconds?: number });\n } finally {\n this.isPolling = false;\n }\n }\n\n /**\n * Classify + route a poll failure: log level, dedup, cooldown and the\n * info.connection=false write. Extracted from poll()'s catch (M9) so the\n * happy path reads as a plain sequence and the error policy sits next to\n * classifyError. Dispatches on the CLASSIFIED code only (L6) \u2014 one source\n * of truth for the error class.\n *\n * @param error The poll failure (usually an ApiError from the client)\n */\n private async handlePollError(error: Error & { code?: string; retryAfterSeconds?: number }): Promise<void> {\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n switch (errorCode) {\n case \"ABORTED\":\n // v0.10.0 (M1): expected during shutdown \u2014 cancelAll aborts the\n // in-flight GET. A deliberate stop must not paint a red error line.\n this.log.debug(`Poll aborted: ${error.message}`);\n break;\n case \"RATE_LIMITED\": {\n // v0.4.2 (M9): clamp Retry-After into [60s, 24h] (shared constants\n // with the client parser, L7). A bogus 0/negative/fractional value\n // must neither wipe the cooldown nor set it for milliseconds.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(RETRY_AFTER_MAX_SEC, Math.max(60, Math.floor(rawCooldown)))\n : RETRY_AFTER_DEFAULT_SEC;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n // v0.10.0 (M3): warn once \u2014 a persistent 429 repeats at debug.\n const line = `Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`;\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.warn(line);\n }\n break;\n }\n case \"FORBIDDEN\": {\n // v0.4.2 (P3): 403 is a permission issue (e.g. Premium subscription\n // expired). Reauth wouldn't help \u2014 surface a clear hint.\n // v0.10.0 (M3): once at error level, repeats at debug \u2014 not 144\n // identical error lines per day for one unchanged account problem.\n const line =\n \"parcel.app returned 403 Forbidden \u2014 your account may not have an active Premium subscription, or the API key was revoked. Check your account on parcelapp.net.\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"INVALID_API_KEY\": {\n // v0.10.0 (M3): first occurrence at error (the user must fix the\n // config; info.connection goes red too) \u2014 repeats at debug.\n const line = \"Invalid API key \u2014 please check your parcel.app API key\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"NETWORK\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n }\n break;\n case \"TIMEOUT\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n }\n break;\n default:\n 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 {\n this.log.error(`Poll failed: ${error.message}`);\n }\n }\n\n // C2: setStateChangedAsync avoids redundant `false` writes on sustained\n // failure. The `.catch` keeps poll() from rejecting when the broker is\n // already down, so the fire-and-forget callers never see an unhandled\n // rejection (no global process handler needed).\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker shutting down \u2014 ignore */\n });\n }\n}\n\n// Process entry point. Not reachable from the unit tests (they import the class\n// instead of running the file as a process), but NOT untested: `npm run\n// test:integration` boots the adapter through exactly this branch inside a real\n// js-controller, which is what catches module-scope startup crashes.\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "parcelapp",
4
- "version": "0.10.4",
4
+ "version": "0.11.0",
5
5
  "news": {
6
+ "0.11.0": {
7
+ "en": "Since 0.10.3 the Test Connection button gave no response, and packages added from a script never showed up — both work again.\nAn update now reaches every datapoint of an existing installation: names and new short descriptions arrive in your system language.\nDetailed user documentation in English and German, plus two leftover settings from much older versions are cleaned up.",
8
+ "de": "Seit 0.10.3 reagierte der Verbindungstest-Knopf gar nicht mehr, und per Skript hinzugefügte Pakete tauchten nie auf — beides geht wieder.\nEin Update erreicht jetzt jeden Datenpunkt einer bestehenden Anlage: Namen und neue kurze Beschreibungen kommen in deiner Systemsprache an.\nAusführliche Nutzerdoku auf Englisch und Deutsch, und zwei Altlasten aus viel älteren Versionen werden aufgeräumt.",
9
+ "ru": "С 0.10.3 кнопка проверки соединения не реагировала, а посылки, добавленные скриптом, не появлялись — оба случая исправлены.\nОбновление теперь доходит до каждой точки данных существующей установки: названия и новые краткие описания приходят на языке системы.\nПодробная документация на английском и немецком, а две настройки из очень старых версий удаляются.",
10
+ "pt": "Desde a 0.10.3 o botão de teste de ligação não reagia e as encomendas adicionadas por script nunca apareciam — ambos voltam a funcionar.\nUma atualização chega agora a cada ponto de dados de uma instalação existente: nomes e novas descrições curtas surgem no idioma do sistema.\nDocumentação detalhada em inglês e alemão, e duas definições de versões muito antigas são removidas.",
11
+ "nl": "Sinds 0.10.3 reageerde de verbindingstestknop niet en verschenen via een script toegevoegde pakketten nooit — beide werken weer.\nEen update bereikt nu elk datapunt van een bestaande installatie: namen en nieuwe korte beschrijvingen komen in je systeemtaal binnen.\nUitgebreide gebruikersdocumentatie in het Engels en Duits, en twee instellingen uit veel oudere versies worden opgeruimd.",
12
+ "fr": "Depuis la 0.10.3, le bouton de test de connexion ne réagissait plus et les colis ajoutés par script n'apparaissaient jamais — les deux refonctionnent.\nUne mise à jour atteint désormais chaque point de données d'une installation existante : noms et nouvelles descriptions courtes dans la langue du système.\nDocumentation détaillée en anglais et en allemand, et deux réglages de versions bien plus anciennes sont supprimés.",
13
+ "it": "Dalla 0.10.3 il pulsante di test della connessione non reagiva e i pacchi aggiunti da script non comparivano mai — entrambi funzionano di nuovo.\nUn aggiornamento ora raggiunge ogni punto dati di un'installazione esistente: nomi e nuove brevi descrizioni arrivano nella lingua di sistema.\nDocumentazione dettagliata in inglese e tedesco, e due impostazioni di versioni molto più vecchie vengono rimosse.",
14
+ "es": "Desde la 0.10.3 el botón de prueba de conexión no reaccionaba y los paquetes añadidos por script nunca aparecían — ambos vuelven a funcionar.\nUna actualización llega ahora a cada punto de datos de una instalación existente: nombres y nuevas descripciones breves en el idioma del sistema.\nDocumentación detallada en inglés y alemán, y se eliminan dos ajustes de versiones mucho más antiguas.",
15
+ "pl": "Od 0.10.3 przycisk testu połączenia nie reagował, a przesyłki dodane skryptem nigdy się nie pojawiały — oba znów działają.\nAktualizacja dociera teraz do każdego punktu danych istniejącej instalacji: nazwy i nowe krótkie opisy pojawiają się w języku systemu.\nSzczegółowa dokumentacja po angielsku i niemiecku, a dwa ustawienia z dużo starszych wersji zostają usunięte.",
16
+ "uk": "Від 0.10.3 кнопка перевірки з'єднання не реагувала, а додані скриптом посилки не з'являлися — обидва випадки виправлено.\nОновлення тепер доходить до кожної точки даних наявної встановленої копії: назви та нові короткі описи з'являються мовою системи.\nДокладна документація англійською та німецькою, а два налаштування з набагато старіших версій прибираються.",
17
+ "zh-cn": "自 0.10.3 起,连接测试按钮毫无反应,脚本添加的包裹也从不出现——两者均已恢复。\n更新现在会到达现有安装的每一个数据点:名称和新的简短说明会以系统语言显示。\n提供英语和德语的详细用户文档,并清理了两项来自更早版本的遗留设置。"
18
+ },
6
19
  "0.10.4": {
7
20
  "en": "Malformed parcel.app replies and odd checkConnection input get a clear answer; a brief database hiccup no longer turns the connection indicator red.",
8
21
  "de": "Fehlerhafte parcel.app-Antworten und unpassende checkConnection-Eingaben bekommen eine klare Antwort; ein kurzer Datenbank-Aussetzer färbt die Verbindungsanzeige nicht mehr rot.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Śledzone paczki nie znikają już po tymczasowym błędzie lub nieoczekiwanej odpowiedzi API, a wielodniowe okna dostawy pokazują teraz datę po obu stronach.",
81
94
  "uk": "Відстежувані посилки більше не зникають після тимчасової помилки або неочікуваної відповіді API, а багатоденні вікна доставки тепер показують дату з обох боків.",
82
95
  "zh-cn": "跟踪的包裹在临时错误或意外的 API 响应后不再消失,跨多天的派送时段现在会在两侧显示日期。"
83
- },
84
- "0.8.0": {
85
- "en": "The delivery window is now also shown for carriers that report it only as a date/time range, not just as a Unix timestamp.",
86
- "de": "Das Zustellfenster wird jetzt auch für Anbieter angezeigt, die es nur als Datums-/Zeitbereich melden, nicht nur als Unix-Zeitstempel.",
87
- "ru": "Окно доставки теперь показывается и для перевозчиков, которые сообщают его только как диапазон даты/времени, а не как метку времени Unix.",
88
- "pt": "A janela de entrega agora também aparece para transportadoras que a informam apenas como intervalo de data/hora, não como carimbo Unix.",
89
- "nl": "Het bezorgvenster wordt nu ook getoond voor vervoerders die het alleen als datum-/tijdbereik doorgeven, niet alleen als Unix-tijdstempel.",
90
- "fr": "Le créneau de livraison apparaît désormais aussi pour les transporteurs qui l'indiquent seulement comme plage date/heure, pas comme horodatage Unix.",
91
- "it": "La finestra di consegna ora viene mostrata anche per i corrieri che la indicano solo come intervallo data/ora, non come timestamp Unix.",
92
- "es": "La ventana de entrega ahora también se muestra para transportistas que la indican solo como rango de fecha/hora, no como marca de tiempo Unix.",
93
- "pl": "Okno dostawy jest teraz pokazywane także dla przewoźników, którzy podają je tylko jako zakres daty/godziny, a nie jako znacznik czasu Unix.",
94
- "uk": "Вікно доставки тепер показується й для перевізників, які повідомляють його лише як діапазон дати/часу, а не як позначку часу Unix.",
95
- "zh-cn": "现在,对于仅以日期/时间范围(而非 Unix 时间戳)报告的承运商,也会显示派送时段。"
96
96
  }
97
97
  },
98
98
  "plugins": {
@@ -145,6 +145,18 @@
145
145
  "icon": "parcelapp.svg",
146
146
  "extIcon": "https://cdn.jsdelivr.net/gh/krobipd/ioBroker.parcelapp@main/admin/parcelapp.svg",
147
147
  "readme": "https://github.com/krobipd/ioBroker.parcelapp/blob/main/README.md",
148
+ "docs": {
149
+ "en": [
150
+ "docs/en/README.md",
151
+ "docs/en/scripting.md",
152
+ "docs/en/faq.md"
153
+ ],
154
+ "de": [
155
+ "docs/de/README.md",
156
+ "docs/de/scripting.md",
157
+ "docs/de/faq.md"
158
+ ]
159
+ },
148
160
  "enabled": true,
149
161
  "tier": 3,
150
162
  "loglevel": "info",
@@ -221,7 +233,20 @@
221
233
  "role": "indicator.connected",
222
234
  "read": true,
223
235
  "write": false,
224
- "def": false
236
+ "def": false,
237
+ "desc": {
238
+ "en": "True while the adapter can reach the parcel.app API.",
239
+ "de": "Wahr, solange der Adapter die parcel.app-API erreicht.",
240
+ "ru": "Истина, пока адаптер может связаться с API parcel.app.",
241
+ "pt": "Verdadeiro enquanto o adaptador conseguir alcançar a API do parcel.app.",
242
+ "nl": "Waar zolang de adapter de parcel.app-API kan bereiken.",
243
+ "fr": "Vrai tant que l'adaptateur parvient à joindre l'API parcel.app.",
244
+ "it": "Vero finché l'adattatore riesce a raggiungere l'API di parcel.app.",
245
+ "es": "Verdadero mientras el adaptador pueda acceder a la API de parcel.app.",
246
+ "pl": "Prawda, dopóki adapter może połączyć się z API parcel.app.",
247
+ "uk": "Істина, доки адаптер може дістатися до API parcel.app.",
248
+ "zh-cn": "适配器能够连接 parcel.app API 时为真。"
249
+ }
225
250
  },
226
251
  "native": {}
227
252
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.parcelapp",
3
- "version": "0.10.4",
3
+ "version": "0.11.0",
4
4
  "description": "ioBroker adapter for the parcel.app API",
5
5
  "author": {
6
6
  "name": "krobi",
@@ -42,11 +42,11 @@
42
42
  "@tsconfig/node22": "^22.0.5",
43
43
  "@types/iobroker": "npm:@iobroker/types@^7.2.2",
44
44
  "@types/node": "^22.19.17",
45
- "@vitest/coverage-v8": "^4.1.11",
45
+ "@vitest/coverage-v8": "^5.0.0",
46
46
  "iobroker-adapter-checks": "^0.3.0",
47
47
  "rimraf": "^6.1.3",
48
48
  "typescript": "~6.0.3",
49
- "vitest": "^4.1.11"
49
+ "vitest": "^5.0.0"
50
50
  },
51
51
  "files": [
52
52
  "admin",
@@ -71,7 +71,10 @@
71
71
  "lint:fix": "eslint --fix",
72
72
  "format": "prettier --write . \"!build/**\" \"!io-package.json\" \"!.github/dependabot.yml\"",
73
73
  "format:check": "prettier --check . \"!build/**\" \"!io-package.json\" \"!.github/dependabot.yml\"",
74
- "release": "release-script"
74
+ "release": "release-script",
75
+ "release-patch": "release-script patch --yes --all",
76
+ "release-minor": "release-script minor --yes --all",
77
+ "release-major": "release-script major --yes --all"
75
78
  },
76
79
  "bugs": {
77
80
  "url": "https://github.com/krobipd/ioBroker.parcelapp/issues"