@camstack/addon-provider-homeassistant 1.2.156 → 1.2.158

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/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("./dist-CUS3XDdU.js");
2
+ const require_dist = require("./dist-M6hP_tw3.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_zlib = require("node:zlib");
5
5
  //#region src/ha-admin-link.ts
@@ -730,12 +730,16 @@ var HaSwitchDevice = class extends HaBaseChildDevice {
730
730
  constructor(ctx) {
731
731
  super(ctx);
732
732
  this.registerSwitchCap();
733
+ const stored = this.runtimeState.getCapState("switch");
734
+ const seededOn = typeof stored?.on === "boolean" ? stored.on : false;
735
+ const seededChangedAt = typeof stored?.lastChangedAt === "number" && Number.isFinite(stored.lastChangedAt) && stored.lastChangedAt > 0 ? stored.lastChangedAt : 0;
733
736
  this.runtimeState.setCapState("switch", {
734
- on: false,
735
- lastChangedAt: 0
737
+ on: seededOn,
738
+ lastChangedAt: seededChangedAt
736
739
  });
737
740
  }
738
741
  handleStatePush(state) {
742
+ if (state.state === "unavailable" || state.state === "unknown") return;
739
743
  const on = state.state === "on";
740
744
  const lastChangedAt = Date.parse(state.last_changed) || Date.now();
741
745
  const previous = this.runtimeState.getCapState("switch");
@@ -5138,7 +5142,7 @@ var HaIntegrationManager = class HaIntegrationManager {
5138
5142
  service: target.service
5139
5143
  };
5140
5144
  const targetEnvelope = {};
5141
- if (target.entityId) targetEnvelope["entity_id"] = target.entityId;
5145
+ if (target.entityId !== void 0) targetEnvelope["entity_id"] = Array.isArray(target.entityId) ? [...target.entityId] : target.entityId;
5142
5146
  if (target.areaId) targetEnvelope["area_id"] = target.areaId;
5143
5147
  if (target.deviceId) targetEnvelope["device_id"] = target.deviceId;
5144
5148
  if (Object.keys(targetEnvelope).length > 0) message["target"] = targetEnvelope;
@@ -6041,6 +6045,12 @@ function mapImage(_attributes) {
6041
6045
  capabilities: ["image"]
6042
6046
  };
6043
6047
  }
6048
+ /** True when HA `unit_of_measurement` clearly means temperature (°C / °F). */
6049
+ function isTemperatureUnit(unit) {
6050
+ if (typeof unit !== "string") return false;
6051
+ const n = unit.trim().toLowerCase().replace(/\s+/g, "");
6052
+ return n === "°c" || n === "°f" || n === "℃" || n === "℉" || n === "c" || n === "f" || n === "celsius" || n === "fahrenheit";
6053
+ }
6044
6054
  function mapNumericSensor(deviceClass, _attributes) {
6045
6055
  switch (deviceClass) {
6046
6056
  case "temperature": return {
@@ -6136,11 +6146,17 @@ function mapNumericSensor(deviceClass, _attributes) {
6136
6146
  capabilities: ["enum-sensor"],
6137
6147
  dateTimeFormat: "date"
6138
6148
  };
6139
- case null: return {
6140
- deviceType: require_dist.DeviceType.Sensor,
6141
- role: require_dist.DeviceRole.EnumSensor,
6142
- capabilities: ["enum-sensor"]
6143
- };
6149
+ case null:
6150
+ if (isTemperatureUnit(_attributes["unit_of_measurement"])) return {
6151
+ deviceType: require_dist.DeviceType.Sensor,
6152
+ role: require_dist.DeviceRole.TemperatureSensor,
6153
+ capabilities: ["temperature-sensor"]
6154
+ };
6155
+ return {
6156
+ deviceType: require_dist.DeviceType.Sensor,
6157
+ role: require_dist.DeviceRole.EnumSensor,
6158
+ capabilities: ["enum-sensor"]
6159
+ };
6144
6160
  default: return {
6145
6161
  deviceType: require_dist.DeviceType.Sensor,
6146
6162
  role: require_dist.DeviceRole.NumericSensor,
@@ -6445,6 +6461,122 @@ function buildDiscoveryCandidates(input) {
6445
6461
  return candidates;
6446
6462
  }
6447
6463
  //#endregion
6464
+ //#region src/ha-discovery-types.ts
6465
+ /**
6466
+ * HA-specific discovery data types — addon-internal.
6467
+ *
6468
+ * These shapes were formerly the `ha-discovery` cap's data schemas.
6469
+ * That cap was removed in favour of the generic `device-adoption`
6470
+ * cap, so these types are no longer a public cap surface — they live
6471
+ * here as the HA addon's own discovery model. The `HaDiscoveryService`
6472
+ * produces them and `haDeviceToCandidate` adapts an `HaDiscoveredDevice`
6473
+ * onto the shared `DiscoveredChildDevice` the `device-adoption` cap
6474
+ * surfaces.
6475
+ */
6476
+ /**
6477
+ * Synthetic `haDeviceId` bucket for `entity_registry` rows with
6478
+ * `device_id === null` (helpers, template sensors, `daily` statistics, …).
6479
+ * Flattened into the Entities adoption tab / standalone reconcile; never
6480
+ * shown as a Devices-tab candidate.
6481
+ */
6482
+ var HA_ORPHAN_ENTITIES_DEVICE_ID = "__ha_orphan_entities__";
6483
+ var HaEntityMappingSchema = require_dist.object({
6484
+ /** CamStack DeviceType for the accessory child. */
6485
+ deviceType: require_dist._enum(require_dist.DeviceType),
6486
+ /** Optional CamStack DeviceRole sub-role (e.g. `ContactSensor` for
6487
+ * a binary_sensor with `device_class: door`). */
6488
+ role: require_dist._enum(require_dist.DeviceRole).nullable(),
6489
+ /** Capability names the accessory child will register
6490
+ * (`switch` / `temperature-sensor` / …). UI may render the cap
6491
+ * badges to help the operator decide what to hide. */
6492
+ capabilities: require_dist.array(require_dist.string()).readonly(),
6493
+ /** Mapping-derived DeviceFeature flags (e.g. `lock-open`). Persisted onto
6494
+ * the child's `device.features` so the UI gates feature affordances. */
6495
+ features: require_dist.array(require_dist.string()).readonly().optional(),
6496
+ /** Locale-render shape for a `DateTimeSensor`-role enum sensor
6497
+ * (`device_class=timestamp` → `'datetime'`, `date` → `'date'`). Threaded
6498
+ * onto the child config so the enum-sensor device seeds it into the cap
6499
+ * slice. Absent for every non-DateTimeSensor mapping. */
6500
+ dateTimeFormat: require_dist.EnumSensorDateTimeFormatSchema.optional()
6501
+ }).nullable();
6502
+ var HaDiscoveredEntitySchema = require_dist.object({
6503
+ /** Fully-qualified HA entity id (`switch.kitchen`). */
6504
+ entityId: require_dist.string(),
6505
+ /** HA domain — first segment of entity_id (`switch` / `light` /
6506
+ * `sensor` / `binary_sensor` / `cover` / …). */
6507
+ domain: require_dist.string(),
6508
+ /** Operator-visible friendly name (HA `friendly_name` attribute or
6509
+ * the entity_id when missing). */
6510
+ name: require_dist.string(),
6511
+ /** Current state value (e.g. `'on'`, `'21.5'`, `'open'`). */
6512
+ state: require_dist.string(),
6513
+ /** HA `device_class` attribute when present — drives the role +
6514
+ * cap mapping for sensors / binary_sensors / covers. */
6515
+ deviceClass: require_dist.string().nullable(),
6516
+ /** HA `unit_of_measurement` attribute when present. */
6517
+ unitOfMeasurement: require_dist.string().nullable(),
6518
+ /** Area name for this entity — the entity's own HA area if set, else its
6519
+ * parent device's area. Null when neither is assigned. */
6520
+ area: require_dist.string().nullable().default(null),
6521
+ /** HA `options` attribute — present on `select` / `input_select` entities.
6522
+ * Captured at discovery so the adopt handler can persist them alongside
6523
+ * other entity config fields. Absent on non-select entities. */
6524
+ options: require_dist.array(require_dist.string()).nullable().default(null),
6525
+ /** HA `entity_category` — `config` / `diagnostic` / null (a normal control).
6526
+ * Drives the accordion section the adopted child renders in. Null/absent for
6527
+ * the common control case. */
6528
+ entityCategory: require_dist._enum(["config", "diagnostic"]).nullable().default(null),
6529
+ /** Pre-computed CamStack mapping. `null` when the entity's domain
6530
+ * has no supported mapping (the operator may still adopt the
6531
+ * parent — the entity will be skipped or surfaced as a fallback). */
6532
+ mappedTo: HaEntityMappingSchema,
6533
+ /** When `true`, the entity was already materialised as a CamStack
6534
+ * accessory child under a prior adoption — surfaced so the UI can
6535
+ * highlight existing wiring. */
6536
+ alreadyAdopted: require_dist.boolean(),
6537
+ /** When `alreadyAdopted === true`, the kernel-assigned accessory
6538
+ * child id. */
6539
+ adoptedCamDeviceId: require_dist.number().int().nonnegative().nullable()
6540
+ });
6541
+ require_dist.object({
6542
+ /** Stable HA `device_registry` id — survives renames + restarts. */
6543
+ haDeviceId: require_dist.string(),
6544
+ /** Operator-visible name (HA device `name` or first entity's
6545
+ * friendly_name when device has none). */
6546
+ name: require_dist.string(),
6547
+ /** Area name from HA `area_registry` join. Null when unassigned. */
6548
+ area: require_dist.string().nullable(),
6549
+ manufacturer: require_dist.string().nullable(),
6550
+ model: require_dist.string().nullable(),
6551
+ swVersion: require_dist.string().nullable(),
6552
+ /** Dominant integration / platform domain for this device, derived from its
6553
+ * entities' HA `entity_registry.platform` (e.g. `dreame_vacuum`). Generic /
6554
+ * helper platforms (`unifi` / `hacs` / `hassio`) are excluded when a real
6555
+ * platform is available. Null when the device has no entities. */
6556
+ integration: require_dist.string().nullable().default(null),
6557
+ /** Convenience field — count of `entities[]`. */
6558
+ entityCount: require_dist.number().int().nonnegative(),
6559
+ /** Every entity HA's `entity_registry` links to this device. The
6560
+ * list survives row collapse — full devices are returned so
6561
+ * accordion expansion is a no-roundtrip render. */
6562
+ entities: require_dist.array(HaDiscoveredEntitySchema).readonly(),
6563
+ /** Whether this HA device has an active CamStack adoption. */
6564
+ alreadyAdopted: require_dist.boolean(),
6565
+ /** When `alreadyAdopted === true`, the kernel-assigned parent id. */
6566
+ adoptedCamDeviceId: require_dist.number().int().nonnegative().nullable()
6567
+ });
6568
+ require_dist.object({
6569
+ /** Last refresh timestamp (ms epoch) — null when never refreshed. */
6570
+ lastDiscoveryAt: require_dist.number().int().nonnegative().nullable(),
6571
+ /** Count of HA devices in the discovery cache (across all brokers). */
6572
+ deviceCount: require_dist.number().int().nonnegative(),
6573
+ /** Count of HA devices currently materialised as CamStack parent
6574
+ * devices. */
6575
+ adoptedCount: require_dist.number().int().nonnegative(),
6576
+ /** Last error message from a refresh attempt. */
6577
+ lastError: require_dist.string().nullable()
6578
+ });
6579
+ //#endregion
6448
6580
  //#region src/ha-integration-domain.ts
6449
6581
  /**
6450
6582
  * Derive the dominant integration / platform domain for an HA device from
@@ -7099,9 +7231,29 @@ function calendarName(state) {
7099
7231
  if (typeof friendly === "string" && friendly.trim().length > 0) return friendly.trim();
7100
7232
  return state.entity_id.replace(/^calendar\./, "").replace(/_/g, " ");
7101
7233
  }
7234
+ /** HA errors from `sendMessagePromise` are plain objects, not `Error`. */
7235
+ function formatHaError(err) {
7236
+ if (err instanceof Error) return err.message;
7237
+ if (typeof err === "string") return err;
7238
+ if (typeof err === "object" && err !== null) {
7239
+ const rec = err;
7240
+ const message = rec["message"];
7241
+ if (typeof message === "string" && message.length > 0) {
7242
+ const code = rec["code"];
7243
+ return typeof code === "string" || typeof code === "number" ? `${String(code)}: ${message}` : message;
7244
+ }
7245
+ try {
7246
+ return JSON.stringify(err);
7247
+ } catch {
7248
+ return Object.prototype.toString.call(err);
7249
+ }
7250
+ }
7251
+ return String(err);
7252
+ }
7102
7253
  function parseHaDate(value) {
7103
7254
  if (typeof value !== "string" || value.length === 0) return null;
7104
- const ms = Date.parse(value.length === 10 ? `${value}T00:00:00` : value);
7255
+ const normalised = value.length === 10 ? `${value}T00:00:00` : value.includes("T") ? value : value.replace(" ", "T");
7256
+ const ms = Date.parse(normalised);
7105
7257
  return Number.isFinite(ms) ? ms : null;
7106
7258
  }
7107
7259
  function isRecord(x) {
@@ -7112,17 +7264,17 @@ function asEntityState(raw) {
7112
7264
  return parsed.success ? parsed.data : null;
7113
7265
  }
7114
7266
  /**
7115
- * HA's `calendar.list_events` response shape (return_response):
7116
- * `{ response: { [entity_id]: { events: [...] } } }` or a flat
7117
- * `{ events: [...] }` depending on target width.
7267
+ * HA's `calendar.get_events` response shape (return_response):
7268
+ * `{ response: { [entity_id]: { events: [...] } } }` — also tolerates a
7269
+ * bare `{ [entity_id]: { events } }` or a legacy flat `{ events: [...] }`.
7118
7270
  */
7119
7271
  function extractEvents(result, entityId) {
7120
7272
  if (!isRecord(result)) return [];
7121
- const response = result["response"];
7273
+ const response = isRecord(result["response"]) ? result["response"] : result;
7122
7274
  if (isRecord(response)) {
7123
7275
  const forEntity = response[entityId];
7124
7276
  if (isRecord(forEntity) && Array.isArray(forEntity["events"])) return forEntity["events"].filter(isRecord);
7125
- for (const value of Object.values(response)) if (isRecord(value) && Array.isArray(value["events"])) return value["events"].filter(isRecord);
7277
+ if (entityId in response) return [];
7126
7278
  }
7127
7279
  if (Array.isArray(result["events"])) return result["events"].filter(isRecord);
7128
7280
  return [];
@@ -7150,6 +7302,29 @@ function mapEvent(entityId, raw, index) {
7150
7302
  ...typeof location === "string" && location.length > 0 ? { location } : {}
7151
7303
  };
7152
7304
  }
7305
+ /** One `get_events` call can target many calendars (HA keys the response). */
7306
+ async function fetchEventsForEntities(manager, entityIds, startIso, endIso) {
7307
+ if (entityIds.length === 0) return [];
7308
+ const target = {
7309
+ domain: "calendar",
7310
+ service: "get_events",
7311
+ entityId: entityIds.length === 1 ? entityIds[0] : entityIds,
7312
+ returnResponse: true
7313
+ };
7314
+ const result = await manager.publish(target, {
7315
+ start_date_time: startIso,
7316
+ end_date_time: endIso
7317
+ });
7318
+ const out = [];
7319
+ for (const entityId of entityIds) {
7320
+ const rawEvents = extractEvents(result, entityId);
7321
+ for (let i = 0; i < rawEvents.length; i += 1) {
7322
+ const mapped = mapEvent(entityId, rawEvents[i], i);
7323
+ if (mapped !== null) out.push(mapped);
7324
+ }
7325
+ }
7326
+ return out;
7327
+ }
7153
7328
  function createHaCalendarSource(deps) {
7154
7329
  return {
7155
7330
  listCalendars: async () => {
@@ -7178,26 +7353,15 @@ function createHaCalendarSource(deps) {
7178
7353
  for (const { brokerId, manager } of deps.brokers.listManagers()) {
7179
7354
  const states = manager.snapshotAllStates();
7180
7355
  const entityIds = Object.keys(states).filter((id) => isCalendarEntity(id) && (wanted === null || wanted.has(id)));
7181
- for (const entityId of entityIds) try {
7182
- const target = {
7183
- domain: "calendar",
7184
- service: "list_events",
7185
- entityId,
7186
- returnResponse: true
7187
- };
7188
- const rawEvents = extractEvents(await manager.publish(target, {
7189
- start_date_time: startIso,
7190
- end_date_time: endIso
7191
- }), entityId);
7192
- for (let i = 0; i < rawEvents.length; i += 1) {
7193
- const mapped = mapEvent(entityId, rawEvents[i], i);
7194
- if (mapped !== null) out.push(mapped);
7195
- }
7356
+ if (entityIds.length === 0) continue;
7357
+ try {
7358
+ const events = await fetchEventsForEntities(manager, entityIds, startIso, endIso);
7359
+ for (const ev of events) out.push(ev);
7196
7360
  } catch (err) {
7197
- deps.logger.warn("HA calendar.list_events failed", { meta: {
7198
- entityId,
7361
+ deps.logger.warn("HA calendar.get_events failed", { meta: {
7362
+ entityIds,
7199
7363
  brokerId,
7200
- error: err instanceof Error ? err.message : String(err)
7364
+ error: formatHaError(err)
7201
7365
  } });
7202
7366
  }
7203
7367
  }
@@ -8080,7 +8244,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8080
8244
  let lastError = null;
8081
8245
  for (const { brokerId } of managers) try {
8082
8246
  const devices = await this.fetchHaDiscoveredDevices(brokerId);
8083
- candidateCount += devices.length;
8247
+ candidateCount += devices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID).length;
8084
8248
  const refreshedAt = Date.now();
8085
8249
  if (lastDiscoveryAt === null || refreshedAt > lastDiscoveryAt) lastDiscoveryAt = refreshedAt;
8086
8250
  } catch (err) {
@@ -8100,7 +8264,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8100
8264
  const adoptedCount = (await this.ctx.kernel.devices.getAll()).filter((d) => d.parentDeviceId === null && String(d.config.get("system") ?? "") === "homeassistant").length;
8101
8265
  return {
8102
8266
  lastDiscoveryAt: Date.now(),
8103
- candidateCount: devices.length,
8267
+ candidateCount: devices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID).length,
8104
8268
  adoptedCount,
8105
8269
  lastError: null
8106
8270
  };
@@ -8129,6 +8293,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8129
8293
  if (typeof haDeviceId === "string") adoptedHaDeviceIds.add(haDeviceId);
8130
8294
  }
8131
8295
  let haDevices = await this.fetchHaDiscoveredDevices(brokerId);
8296
+ haDevices = haDevices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID);
8132
8297
  haDevices = haDevices.map((dev) => ({
8133
8298
  ...dev,
8134
8299
  alreadyAdopted: adoptedHaDeviceIds.has(dev.haDeviceId)
@@ -8165,7 +8330,9 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8165
8330
  const haDeviceId = d.config.get("haDeviceId");
8166
8331
  if (typeof haDeviceId === "string") adoptedHaDeviceIds.add(haDeviceId);
8167
8332
  }
8168
- const found = (await this.fetchHaDiscoveredDevices(brokerId)).find((d) => d.haDeviceId === childNativeId) ?? null;
8333
+ const haDevices = await this.fetchHaDiscoveredDevices(brokerId);
8334
+ if (childNativeId === "__ha_orphan_entities__") return null;
8335
+ const found = haDevices.find((d) => d.haDeviceId === childNativeId) ?? null;
8169
8336
  if (!found) return null;
8170
8337
  return haDeviceToCandidate({
8171
8338
  ...found,
@@ -8193,6 +8360,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8193
8360
  let failures = 0;
8194
8361
  let reused = 0;
8195
8362
  for (const haDeviceId of childNativeIds) try {
8363
+ if (haDeviceId === "__ha_orphan_entities__") throw new Error("orphan entity bucket is not adoptable as a device — use the Entities tab");
8196
8364
  const haDevice = haDeviceMap.get(haDeviceId);
8197
8365
  const deviceName = (perCandidate?.[haDeviceId])?.name ?? haDevice?.name ?? haDeviceId;
8198
8366
  const existing = existingByStableId.get(`ha:${brokerId}:dev:${haDeviceId}`);
@@ -8450,6 +8618,11 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8450
8618
  * it to the addon-internal `HaDiscoveredDevice[]` shape. Shared between
8451
8619
  * `discoverDevices` (device-provider) and the `device-adoption` handlers.
8452
8620
  *
8621
+ * Entities with `device_id === null` (helpers / template / daily stats) are
8622
+ * collected under a synthetic {@link HA_ORPHAN_ENTITIES_DEVICE_ID} bucket so
8623
+ * the Entities adoption tab + standalone reconcile can see them. Callers on
8624
+ * the Devices path must filter that bucket out.
8625
+ *
8453
8626
  * Unlike the old `HaDiscoveryService`, this is stateless — every call
8454
8627
  * goes to the live WS; caching is intentionally omitted to keep the
8455
8628
  * adoption path simple (the modal fetches once per session).
@@ -8470,15 +8643,59 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8470
8643
  if (areaId && name) areaById.set(areaId, name);
8471
8644
  }
8472
8645
  const entitiesByDevice = /* @__PURE__ */ new Map();
8646
+ const orphanRaws = [];
8473
8647
  for (const raw of rawEntities) {
8474
8648
  const entityId = typeof raw["entity_id"] === "string" ? raw["entity_id"] : null;
8475
8649
  const deviceId = typeof raw["device_id"] === "string" ? raw["device_id"] : null;
8476
8650
  const disabledBy = raw["disabled_by"] !== null && raw["disabled_by"] !== void 0 ? String(raw["disabled_by"]) : null;
8477
- if (!entityId || !deviceId || disabledBy !== null) continue;
8651
+ if (!entityId || disabledBy !== null) continue;
8652
+ if (!deviceId) {
8653
+ orphanRaws.push(raw);
8654
+ continue;
8655
+ }
8478
8656
  const bucket = entitiesByDevice.get(deviceId);
8479
8657
  if (bucket) bucket.push(raw);
8480
8658
  else entitiesByDevice.set(deviceId, [raw]);
8481
8659
  }
8660
+ const mapRawEntity = (raw, deviceAreaName) => {
8661
+ const entityId = raw["entity_id"];
8662
+ const domain = entityId.split(".")[0] ?? entityId;
8663
+ const stateEntry = stateSnapshot[entityId];
8664
+ const attributes = stateEntry?.attributes ?? {};
8665
+ const deviceClass = typeof raw["device_class"] === "string" ? raw["device_class"] : typeof raw["original_device_class"] === "string" ? raw["original_device_class"] : typeof attributes["device_class"] === "string" ? attributes["device_class"] : null;
8666
+ const unit = typeof attributes["unit_of_measurement"] === "string" ? attributes["unit_of_measurement"] : null;
8667
+ const rawOptions = attributes["options"];
8668
+ const options = Array.isArray(rawOptions) && rawOptions.length > 0 ? rawOptions.filter((x) => typeof x === "string") : null;
8669
+ const entityAreaId = typeof raw["area_id"] === "string" ? raw["area_id"] : null;
8670
+ const entityArea = entityAreaId ? areaById.get(entityAreaId) ?? deviceAreaName : deviceAreaName;
8671
+ const rawCategory = raw["entity_category"];
8672
+ const entityCategory = rawCategory === "config" || rawCategory === "diagnostic" ? rawCategory : null;
8673
+ const mapping = mapHaEntityToCamStack({
8674
+ domain,
8675
+ deviceClass,
8676
+ attributes
8677
+ });
8678
+ return {
8679
+ entityId,
8680
+ domain,
8681
+ name: typeof raw["name"] === "string" && raw["name"] ? raw["name"] : typeof raw["original_name"] === "string" && raw["original_name"] ? raw["original_name"] : (typeof attributes["friendly_name"] === "string" ? attributes["friendly_name"] : null) ?? entityId,
8682
+ state: stateEntry?.state ?? "unknown",
8683
+ deviceClass,
8684
+ unitOfMeasurement: unit,
8685
+ area: entityArea,
8686
+ options,
8687
+ entityCategory,
8688
+ mappedTo: mapping ? {
8689
+ deviceType: mapping.deviceType,
8690
+ role: mapping.role,
8691
+ capabilities: [...mapping.capabilities],
8692
+ features: mapping.features ? [...mapping.features] : [],
8693
+ ...mapping.dateTimeFormat !== void 0 ? { dateTimeFormat: mapping.dateTimeFormat } : {}
8694
+ } : null,
8695
+ alreadyAdopted: false,
8696
+ adoptedCamDeviceId: null
8697
+ };
8698
+ };
8482
8699
  const out = [];
8483
8700
  for (const rawDev of rawDevices) {
8484
8701
  const devId = typeof rawDev["id"] === "string" ? rawDev["id"] : null;
@@ -8487,45 +8704,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8487
8704
  const areaIdRaw = typeof rawDev["area_id"] === "string" ? rawDev["area_id"] : null;
8488
8705
  const deviceAreaName = areaIdRaw ? areaById.get(areaIdRaw) ?? null : null;
8489
8706
  const entBucket = entitiesByDevice.get(devId) ?? [];
8490
- const entities = entBucket.map((raw) => {
8491
- const entityId = raw["entity_id"];
8492
- const domain = entityId.split(".")[0] ?? entityId;
8493
- const stateEntry = stateSnapshot[entityId];
8494
- const attributes = stateEntry?.attributes ?? {};
8495
- const deviceClass = typeof raw["device_class"] === "string" ? raw["device_class"] : typeof raw["original_device_class"] === "string" ? raw["original_device_class"] : typeof attributes["device_class"] === "string" ? attributes["device_class"] : null;
8496
- const unit = typeof attributes["unit_of_measurement"] === "string" ? attributes["unit_of_measurement"] : null;
8497
- const rawOptions = attributes["options"];
8498
- const options = Array.isArray(rawOptions) && rawOptions.length > 0 ? rawOptions.filter((x) => typeof x === "string") : null;
8499
- const entityAreaId = typeof raw["area_id"] === "string" ? raw["area_id"] : null;
8500
- const entityArea = entityAreaId ? areaById.get(entityAreaId) ?? deviceAreaName : deviceAreaName;
8501
- const rawCategory = raw["entity_category"];
8502
- const entityCategory = rawCategory === "config" || rawCategory === "diagnostic" ? rawCategory : null;
8503
- const mapping = mapHaEntityToCamStack({
8504
- domain,
8505
- deviceClass,
8506
- attributes
8507
- });
8508
- return {
8509
- entityId,
8510
- domain,
8511
- name: typeof raw["name"] === "string" && raw["name"] ? raw["name"] : typeof raw["original_name"] === "string" && raw["original_name"] ? raw["original_name"] : (typeof attributes["friendly_name"] === "string" ? attributes["friendly_name"] : null) ?? entityId,
8512
- state: stateEntry?.state ?? "unknown",
8513
- deviceClass,
8514
- unitOfMeasurement: unit,
8515
- area: entityArea,
8516
- options,
8517
- entityCategory,
8518
- mappedTo: mapping ? {
8519
- deviceType: mapping.deviceType,
8520
- role: mapping.role,
8521
- capabilities: [...mapping.capabilities],
8522
- features: mapping.features ? [...mapping.features] : [],
8523
- ...mapping.dateTimeFormat !== void 0 ? { dateTimeFormat: mapping.dateTimeFormat } : {}
8524
- } : null,
8525
- alreadyAdopted: false,
8526
- adoptedCamDeviceId: null
8527
- };
8528
- });
8707
+ const entities = entBucket.map((raw) => mapRawEntity(raw, deviceAreaName));
8529
8708
  const devName = typeof rawDev["name_by_user"] === "string" && rawDev["name_by_user"] ? rawDev["name_by_user"] : typeof rawDev["name"] === "string" && rawDev["name"] ? rawDev["name"] : entBucket[0] ? String(entBucket[0]["name"] ?? entBucket[0]["original_name"] ?? devId) : devId;
8530
8709
  const integration = deriveIntegrationDomain(entBucket.map((raw) => typeof raw["platform"] === "string" ? raw["platform"] : null));
8531
8710
  out.push({
@@ -8542,6 +8721,23 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8542
8721
  adoptedCamDeviceId: null
8543
8722
  });
8544
8723
  }
8724
+ if (orphanRaws.length > 0) {
8725
+ const entities = orphanRaws.map((raw) => mapRawEntity(raw, null));
8726
+ const entityPlatforms = orphanRaws.map((raw) => typeof raw["platform"] === "string" ? raw["platform"] : null);
8727
+ out.push({
8728
+ haDeviceId: HA_ORPHAN_ENTITIES_DEVICE_ID,
8729
+ name: "Standalone entities",
8730
+ area: null,
8731
+ manufacturer: null,
8732
+ model: null,
8733
+ swVersion: null,
8734
+ integration: deriveIntegrationDomain(entityPlatforms),
8735
+ entityCount: entities.length,
8736
+ entities,
8737
+ alreadyAdopted: false,
8738
+ adoptedCamDeviceId: null
8739
+ });
8740
+ }
8545
8741
  return out;
8546
8742
  }
8547
8743
  /**
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as connectivityCapability, At as string, B as floodCapability, C as buildAddonRouteProvider, Ct as _enum, D as carbonMonoxideCapability, Dt as number, E as calendarSourceCapability, Et as literal, F as deviceAdoptionCapability, G as lawnMowerControlCapability, H as humidifierCapability, J as motionCapability, K as lockControlCapability, L as enumSensorCapability, M as controlCapability, Mt as unknown, N as coverCapability, Nt as url, O as climateControlCapability, Ot as object, P as deviceAdminLinkCapability, Pt as EventCategory, Q as numericSensorCapability, R as eventEmitterCapability, S as brokerCapability, T as buttonCapability, Tt as discriminatedUnion, U as humiditySensorCapability, V as gasCapability, W as imageCapability, X as notificationOutputCapability, Y as normalizeUnit, Z as notifierCapability, _ as automationControlCapability, _t as DeviceRole, at as scriptRunnerCapability, b as binaryCapability, ct as tamperCapability, d as TargetSchema, dt as vacuumControlCapability, et as parseAdminBase, f as accessoriesCapability, ft as valveCapability, g as ambientLightSensorCapability, gt as DeviceFeature, h as alarmPanelCapability, ht as weatherCapability, it as pressureSensorCapability, j as contactCapability, jt as union, k as colorCapability, kt as record, lt as temperatureSensorCapability, m as airQualitySensorCapability, mt as waterHeaterCapability, n as BaseDevice, nt as prepareNotification, ot as smokeCapability, p as addonRoutesCapability, pt as vibrationCapability, q as mediaPlayerCapability, r as BaseDeviceProvider, rt as presenceCapability, s as EnumSensorDateTimeFormatSchema, st as switchCapability, tt as powerMeterCapability, ut as updateCapability, v as batteryCapability, vt as DeviceType, w as buildDeviceAdminUrl, wt as array, x as brightnessCapability, xt as createEvent, y as bestLocationMatch, yt as errMsg, z as fanControlCapability } from "./dist-CbGUsakM.mjs";
1
+ import { A as connectivityCapability, At as record, B as floodCapability, C as buildAddonRouteProvider, Ct as _enum, D as carbonMonoxideCapability, Dt as literal, E as calendarSourceCapability, Et as discriminatedUnion, F as deviceAdoptionCapability, Ft as EventCategory, G as lawnMowerControlCapability, H as humidifierCapability, J as motionCapability, K as lockControlCapability, L as enumSensorCapability, M as controlCapability, Mt as union, N as coverCapability, Nt as unknown, O as climateControlCapability, Ot as number, P as deviceAdminLinkCapability, Pt as url, Q as numericSensorCapability, R as eventEmitterCapability, S as brokerCapability, T as buttonCapability, Tt as boolean, U as humiditySensorCapability, V as gasCapability, W as imageCapability, X as notificationOutputCapability, Y as normalizeUnit, Z as notifierCapability, _ as automationControlCapability, _t as DeviceRole, at as scriptRunnerCapability, b as binaryCapability, ct as tamperCapability, d as TargetSchema, dt as vacuumControlCapability, et as parseAdminBase, f as accessoriesCapability, ft as valveCapability, g as ambientLightSensorCapability, gt as DeviceFeature, h as alarmPanelCapability, ht as weatherCapability, it as pressureSensorCapability, j as contactCapability, jt as string, k as colorCapability, kt as object, lt as temperatureSensorCapability, m as airQualitySensorCapability, mt as waterHeaterCapability, n as BaseDevice, nt as prepareNotification, ot as smokeCapability, p as addonRoutesCapability, pt as vibrationCapability, q as mediaPlayerCapability, r as BaseDeviceProvider, rt as presenceCapability, s as EnumSensorDateTimeFormatSchema, st as switchCapability, tt as powerMeterCapability, ut as updateCapability, v as batteryCapability, vt as DeviceType, w as buildDeviceAdminUrl, wt as array, x as brightnessCapability, xt as createEvent, y as bestLocationMatch, yt as errMsg, z as fanControlCapability } from "./dist-BgTykMb6.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-admin-link.ts
@@ -729,12 +729,16 @@ var HaSwitchDevice = class extends HaBaseChildDevice {
729
729
  constructor(ctx) {
730
730
  super(ctx);
731
731
  this.registerSwitchCap();
732
+ const stored = this.runtimeState.getCapState("switch");
733
+ const seededOn = typeof stored?.on === "boolean" ? stored.on : false;
734
+ const seededChangedAt = typeof stored?.lastChangedAt === "number" && Number.isFinite(stored.lastChangedAt) && stored.lastChangedAt > 0 ? stored.lastChangedAt : 0;
732
735
  this.runtimeState.setCapState("switch", {
733
- on: false,
734
- lastChangedAt: 0
736
+ on: seededOn,
737
+ lastChangedAt: seededChangedAt
735
738
  });
736
739
  }
737
740
  handleStatePush(state) {
741
+ if (state.state === "unavailable" || state.state === "unknown") return;
738
742
  const on = state.state === "on";
739
743
  const lastChangedAt = Date.parse(state.last_changed) || Date.now();
740
744
  const previous = this.runtimeState.getCapState("switch");
@@ -5137,7 +5141,7 @@ var HaIntegrationManager = class HaIntegrationManager {
5137
5141
  service: target.service
5138
5142
  };
5139
5143
  const targetEnvelope = {};
5140
- if (target.entityId) targetEnvelope["entity_id"] = target.entityId;
5144
+ if (target.entityId !== void 0) targetEnvelope["entity_id"] = Array.isArray(target.entityId) ? [...target.entityId] : target.entityId;
5141
5145
  if (target.areaId) targetEnvelope["area_id"] = target.areaId;
5142
5146
  if (target.deviceId) targetEnvelope["device_id"] = target.deviceId;
5143
5147
  if (Object.keys(targetEnvelope).length > 0) message["target"] = targetEnvelope;
@@ -6040,6 +6044,12 @@ function mapImage(_attributes) {
6040
6044
  capabilities: ["image"]
6041
6045
  };
6042
6046
  }
6047
+ /** True when HA `unit_of_measurement` clearly means temperature (°C / °F). */
6048
+ function isTemperatureUnit(unit) {
6049
+ if (typeof unit !== "string") return false;
6050
+ const n = unit.trim().toLowerCase().replace(/\s+/g, "");
6051
+ return n === "°c" || n === "°f" || n === "℃" || n === "℉" || n === "c" || n === "f" || n === "celsius" || n === "fahrenheit";
6052
+ }
6043
6053
  function mapNumericSensor(deviceClass, _attributes) {
6044
6054
  switch (deviceClass) {
6045
6055
  case "temperature": return {
@@ -6135,11 +6145,17 @@ function mapNumericSensor(deviceClass, _attributes) {
6135
6145
  capabilities: ["enum-sensor"],
6136
6146
  dateTimeFormat: "date"
6137
6147
  };
6138
- case null: return {
6139
- deviceType: DeviceType.Sensor,
6140
- role: DeviceRole.EnumSensor,
6141
- capabilities: ["enum-sensor"]
6142
- };
6148
+ case null:
6149
+ if (isTemperatureUnit(_attributes["unit_of_measurement"])) return {
6150
+ deviceType: DeviceType.Sensor,
6151
+ role: DeviceRole.TemperatureSensor,
6152
+ capabilities: ["temperature-sensor"]
6153
+ };
6154
+ return {
6155
+ deviceType: DeviceType.Sensor,
6156
+ role: DeviceRole.EnumSensor,
6157
+ capabilities: ["enum-sensor"]
6158
+ };
6143
6159
  default: return {
6144
6160
  deviceType: DeviceType.Sensor,
6145
6161
  role: DeviceRole.NumericSensor,
@@ -6444,6 +6460,122 @@ function buildDiscoveryCandidates(input) {
6444
6460
  return candidates;
6445
6461
  }
6446
6462
  //#endregion
6463
+ //#region src/ha-discovery-types.ts
6464
+ /**
6465
+ * HA-specific discovery data types — addon-internal.
6466
+ *
6467
+ * These shapes were formerly the `ha-discovery` cap's data schemas.
6468
+ * That cap was removed in favour of the generic `device-adoption`
6469
+ * cap, so these types are no longer a public cap surface — they live
6470
+ * here as the HA addon's own discovery model. The `HaDiscoveryService`
6471
+ * produces them and `haDeviceToCandidate` adapts an `HaDiscoveredDevice`
6472
+ * onto the shared `DiscoveredChildDevice` the `device-adoption` cap
6473
+ * surfaces.
6474
+ */
6475
+ /**
6476
+ * Synthetic `haDeviceId` bucket for `entity_registry` rows with
6477
+ * `device_id === null` (helpers, template sensors, `daily` statistics, …).
6478
+ * Flattened into the Entities adoption tab / standalone reconcile; never
6479
+ * shown as a Devices-tab candidate.
6480
+ */
6481
+ var HA_ORPHAN_ENTITIES_DEVICE_ID = "__ha_orphan_entities__";
6482
+ var HaEntityMappingSchema = object({
6483
+ /** CamStack DeviceType for the accessory child. */
6484
+ deviceType: _enum(DeviceType),
6485
+ /** Optional CamStack DeviceRole sub-role (e.g. `ContactSensor` for
6486
+ * a binary_sensor with `device_class: door`). */
6487
+ role: _enum(DeviceRole).nullable(),
6488
+ /** Capability names the accessory child will register
6489
+ * (`switch` / `temperature-sensor` / …). UI may render the cap
6490
+ * badges to help the operator decide what to hide. */
6491
+ capabilities: array(string()).readonly(),
6492
+ /** Mapping-derived DeviceFeature flags (e.g. `lock-open`). Persisted onto
6493
+ * the child's `device.features` so the UI gates feature affordances. */
6494
+ features: array(string()).readonly().optional(),
6495
+ /** Locale-render shape for a `DateTimeSensor`-role enum sensor
6496
+ * (`device_class=timestamp` → `'datetime'`, `date` → `'date'`). Threaded
6497
+ * onto the child config so the enum-sensor device seeds it into the cap
6498
+ * slice. Absent for every non-DateTimeSensor mapping. */
6499
+ dateTimeFormat: EnumSensorDateTimeFormatSchema.optional()
6500
+ }).nullable();
6501
+ var HaDiscoveredEntitySchema = object({
6502
+ /** Fully-qualified HA entity id (`switch.kitchen`). */
6503
+ entityId: string(),
6504
+ /** HA domain — first segment of entity_id (`switch` / `light` /
6505
+ * `sensor` / `binary_sensor` / `cover` / …). */
6506
+ domain: string(),
6507
+ /** Operator-visible friendly name (HA `friendly_name` attribute or
6508
+ * the entity_id when missing). */
6509
+ name: string(),
6510
+ /** Current state value (e.g. `'on'`, `'21.5'`, `'open'`). */
6511
+ state: string(),
6512
+ /** HA `device_class` attribute when present — drives the role +
6513
+ * cap mapping for sensors / binary_sensors / covers. */
6514
+ deviceClass: string().nullable(),
6515
+ /** HA `unit_of_measurement` attribute when present. */
6516
+ unitOfMeasurement: string().nullable(),
6517
+ /** Area name for this entity — the entity's own HA area if set, else its
6518
+ * parent device's area. Null when neither is assigned. */
6519
+ area: string().nullable().default(null),
6520
+ /** HA `options` attribute — present on `select` / `input_select` entities.
6521
+ * Captured at discovery so the adopt handler can persist them alongside
6522
+ * other entity config fields. Absent on non-select entities. */
6523
+ options: array(string()).nullable().default(null),
6524
+ /** HA `entity_category` — `config` / `diagnostic` / null (a normal control).
6525
+ * Drives the accordion section the adopted child renders in. Null/absent for
6526
+ * the common control case. */
6527
+ entityCategory: _enum(["config", "diagnostic"]).nullable().default(null),
6528
+ /** Pre-computed CamStack mapping. `null` when the entity's domain
6529
+ * has no supported mapping (the operator may still adopt the
6530
+ * parent — the entity will be skipped or surfaced as a fallback). */
6531
+ mappedTo: HaEntityMappingSchema,
6532
+ /** When `true`, the entity was already materialised as a CamStack
6533
+ * accessory child under a prior adoption — surfaced so the UI can
6534
+ * highlight existing wiring. */
6535
+ alreadyAdopted: boolean(),
6536
+ /** When `alreadyAdopted === true`, the kernel-assigned accessory
6537
+ * child id. */
6538
+ adoptedCamDeviceId: number().int().nonnegative().nullable()
6539
+ });
6540
+ object({
6541
+ /** Stable HA `device_registry` id — survives renames + restarts. */
6542
+ haDeviceId: string(),
6543
+ /** Operator-visible name (HA device `name` or first entity's
6544
+ * friendly_name when device has none). */
6545
+ name: string(),
6546
+ /** Area name from HA `area_registry` join. Null when unassigned. */
6547
+ area: string().nullable(),
6548
+ manufacturer: string().nullable(),
6549
+ model: string().nullable(),
6550
+ swVersion: string().nullable(),
6551
+ /** Dominant integration / platform domain for this device, derived from its
6552
+ * entities' HA `entity_registry.platform` (e.g. `dreame_vacuum`). Generic /
6553
+ * helper platforms (`unifi` / `hacs` / `hassio`) are excluded when a real
6554
+ * platform is available. Null when the device has no entities. */
6555
+ integration: string().nullable().default(null),
6556
+ /** Convenience field — count of `entities[]`. */
6557
+ entityCount: number().int().nonnegative(),
6558
+ /** Every entity HA's `entity_registry` links to this device. The
6559
+ * list survives row collapse — full devices are returned so
6560
+ * accordion expansion is a no-roundtrip render. */
6561
+ entities: array(HaDiscoveredEntitySchema).readonly(),
6562
+ /** Whether this HA device has an active CamStack adoption. */
6563
+ alreadyAdopted: boolean(),
6564
+ /** When `alreadyAdopted === true`, the kernel-assigned parent id. */
6565
+ adoptedCamDeviceId: number().int().nonnegative().nullable()
6566
+ });
6567
+ object({
6568
+ /** Last refresh timestamp (ms epoch) — null when never refreshed. */
6569
+ lastDiscoveryAt: number().int().nonnegative().nullable(),
6570
+ /** Count of HA devices in the discovery cache (across all brokers). */
6571
+ deviceCount: number().int().nonnegative(),
6572
+ /** Count of HA devices currently materialised as CamStack parent
6573
+ * devices. */
6574
+ adoptedCount: number().int().nonnegative(),
6575
+ /** Last error message from a refresh attempt. */
6576
+ lastError: string().nullable()
6577
+ });
6578
+ //#endregion
6447
6579
  //#region src/ha-integration-domain.ts
6448
6580
  /**
6449
6581
  * Derive the dominant integration / platform domain for an HA device from
@@ -7098,9 +7230,29 @@ function calendarName(state) {
7098
7230
  if (typeof friendly === "string" && friendly.trim().length > 0) return friendly.trim();
7099
7231
  return state.entity_id.replace(/^calendar\./, "").replace(/_/g, " ");
7100
7232
  }
7233
+ /** HA errors from `sendMessagePromise` are plain objects, not `Error`. */
7234
+ function formatHaError(err) {
7235
+ if (err instanceof Error) return err.message;
7236
+ if (typeof err === "string") return err;
7237
+ if (typeof err === "object" && err !== null) {
7238
+ const rec = err;
7239
+ const message = rec["message"];
7240
+ if (typeof message === "string" && message.length > 0) {
7241
+ const code = rec["code"];
7242
+ return typeof code === "string" || typeof code === "number" ? `${String(code)}: ${message}` : message;
7243
+ }
7244
+ try {
7245
+ return JSON.stringify(err);
7246
+ } catch {
7247
+ return Object.prototype.toString.call(err);
7248
+ }
7249
+ }
7250
+ return String(err);
7251
+ }
7101
7252
  function parseHaDate(value) {
7102
7253
  if (typeof value !== "string" || value.length === 0) return null;
7103
- const ms = Date.parse(value.length === 10 ? `${value}T00:00:00` : value);
7254
+ const normalised = value.length === 10 ? `${value}T00:00:00` : value.includes("T") ? value : value.replace(" ", "T");
7255
+ const ms = Date.parse(normalised);
7104
7256
  return Number.isFinite(ms) ? ms : null;
7105
7257
  }
7106
7258
  function isRecord(x) {
@@ -7111,17 +7263,17 @@ function asEntityState(raw) {
7111
7263
  return parsed.success ? parsed.data : null;
7112
7264
  }
7113
7265
  /**
7114
- * HA's `calendar.list_events` response shape (return_response):
7115
- * `{ response: { [entity_id]: { events: [...] } } }` or a flat
7116
- * `{ events: [...] }` depending on target width.
7266
+ * HA's `calendar.get_events` response shape (return_response):
7267
+ * `{ response: { [entity_id]: { events: [...] } } }` — also tolerates a
7268
+ * bare `{ [entity_id]: { events } }` or a legacy flat `{ events: [...] }`.
7117
7269
  */
7118
7270
  function extractEvents(result, entityId) {
7119
7271
  if (!isRecord(result)) return [];
7120
- const response = result["response"];
7272
+ const response = isRecord(result["response"]) ? result["response"] : result;
7121
7273
  if (isRecord(response)) {
7122
7274
  const forEntity = response[entityId];
7123
7275
  if (isRecord(forEntity) && Array.isArray(forEntity["events"])) return forEntity["events"].filter(isRecord);
7124
- for (const value of Object.values(response)) if (isRecord(value) && Array.isArray(value["events"])) return value["events"].filter(isRecord);
7276
+ if (entityId in response) return [];
7125
7277
  }
7126
7278
  if (Array.isArray(result["events"])) return result["events"].filter(isRecord);
7127
7279
  return [];
@@ -7149,6 +7301,29 @@ function mapEvent(entityId, raw, index) {
7149
7301
  ...typeof location === "string" && location.length > 0 ? { location } : {}
7150
7302
  };
7151
7303
  }
7304
+ /** One `get_events` call can target many calendars (HA keys the response). */
7305
+ async function fetchEventsForEntities(manager, entityIds, startIso, endIso) {
7306
+ if (entityIds.length === 0) return [];
7307
+ const target = {
7308
+ domain: "calendar",
7309
+ service: "get_events",
7310
+ entityId: entityIds.length === 1 ? entityIds[0] : entityIds,
7311
+ returnResponse: true
7312
+ };
7313
+ const result = await manager.publish(target, {
7314
+ start_date_time: startIso,
7315
+ end_date_time: endIso
7316
+ });
7317
+ const out = [];
7318
+ for (const entityId of entityIds) {
7319
+ const rawEvents = extractEvents(result, entityId);
7320
+ for (let i = 0; i < rawEvents.length; i += 1) {
7321
+ const mapped = mapEvent(entityId, rawEvents[i], i);
7322
+ if (mapped !== null) out.push(mapped);
7323
+ }
7324
+ }
7325
+ return out;
7326
+ }
7152
7327
  function createHaCalendarSource(deps) {
7153
7328
  return {
7154
7329
  listCalendars: async () => {
@@ -7177,26 +7352,15 @@ function createHaCalendarSource(deps) {
7177
7352
  for (const { brokerId, manager } of deps.brokers.listManagers()) {
7178
7353
  const states = manager.snapshotAllStates();
7179
7354
  const entityIds = Object.keys(states).filter((id) => isCalendarEntity(id) && (wanted === null || wanted.has(id)));
7180
- for (const entityId of entityIds) try {
7181
- const target = {
7182
- domain: "calendar",
7183
- service: "list_events",
7184
- entityId,
7185
- returnResponse: true
7186
- };
7187
- const rawEvents = extractEvents(await manager.publish(target, {
7188
- start_date_time: startIso,
7189
- end_date_time: endIso
7190
- }), entityId);
7191
- for (let i = 0; i < rawEvents.length; i += 1) {
7192
- const mapped = mapEvent(entityId, rawEvents[i], i);
7193
- if (mapped !== null) out.push(mapped);
7194
- }
7355
+ if (entityIds.length === 0) continue;
7356
+ try {
7357
+ const events = await fetchEventsForEntities(manager, entityIds, startIso, endIso);
7358
+ for (const ev of events) out.push(ev);
7195
7359
  } catch (err) {
7196
- deps.logger.warn("HA calendar.list_events failed", { meta: {
7197
- entityId,
7360
+ deps.logger.warn("HA calendar.get_events failed", { meta: {
7361
+ entityIds,
7198
7362
  brokerId,
7199
- error: err instanceof Error ? err.message : String(err)
7363
+ error: formatHaError(err)
7200
7364
  } });
7201
7365
  }
7202
7366
  }
@@ -8079,7 +8243,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8079
8243
  let lastError = null;
8080
8244
  for (const { brokerId } of managers) try {
8081
8245
  const devices = await this.fetchHaDiscoveredDevices(brokerId);
8082
- candidateCount += devices.length;
8246
+ candidateCount += devices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID).length;
8083
8247
  const refreshedAt = Date.now();
8084
8248
  if (lastDiscoveryAt === null || refreshedAt > lastDiscoveryAt) lastDiscoveryAt = refreshedAt;
8085
8249
  } catch (err) {
@@ -8099,7 +8263,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8099
8263
  const adoptedCount = (await this.ctx.kernel.devices.getAll()).filter((d) => d.parentDeviceId === null && String(d.config.get("system") ?? "") === "homeassistant").length;
8100
8264
  return {
8101
8265
  lastDiscoveryAt: Date.now(),
8102
- candidateCount: devices.length,
8266
+ candidateCount: devices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID).length,
8103
8267
  adoptedCount,
8104
8268
  lastError: null
8105
8269
  };
@@ -8128,6 +8292,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8128
8292
  if (typeof haDeviceId === "string") adoptedHaDeviceIds.add(haDeviceId);
8129
8293
  }
8130
8294
  let haDevices = await this.fetchHaDiscoveredDevices(brokerId);
8295
+ haDevices = haDevices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID);
8131
8296
  haDevices = haDevices.map((dev) => ({
8132
8297
  ...dev,
8133
8298
  alreadyAdopted: adoptedHaDeviceIds.has(dev.haDeviceId)
@@ -8164,7 +8329,9 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8164
8329
  const haDeviceId = d.config.get("haDeviceId");
8165
8330
  if (typeof haDeviceId === "string") adoptedHaDeviceIds.add(haDeviceId);
8166
8331
  }
8167
- const found = (await this.fetchHaDiscoveredDevices(brokerId)).find((d) => d.haDeviceId === childNativeId) ?? null;
8332
+ const haDevices = await this.fetchHaDiscoveredDevices(brokerId);
8333
+ if (childNativeId === "__ha_orphan_entities__") return null;
8334
+ const found = haDevices.find((d) => d.haDeviceId === childNativeId) ?? null;
8168
8335
  if (!found) return null;
8169
8336
  return haDeviceToCandidate({
8170
8337
  ...found,
@@ -8192,6 +8359,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8192
8359
  let failures = 0;
8193
8360
  let reused = 0;
8194
8361
  for (const haDeviceId of childNativeIds) try {
8362
+ if (haDeviceId === "__ha_orphan_entities__") throw new Error("orphan entity bucket is not adoptable as a device — use the Entities tab");
8195
8363
  const haDevice = haDeviceMap.get(haDeviceId);
8196
8364
  const deviceName = (perCandidate?.[haDeviceId])?.name ?? haDevice?.name ?? haDeviceId;
8197
8365
  const existing = existingByStableId.get(`ha:${brokerId}:dev:${haDeviceId}`);
@@ -8449,6 +8617,11 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8449
8617
  * it to the addon-internal `HaDiscoveredDevice[]` shape. Shared between
8450
8618
  * `discoverDevices` (device-provider) and the `device-adoption` handlers.
8451
8619
  *
8620
+ * Entities with `device_id === null` (helpers / template / daily stats) are
8621
+ * collected under a synthetic {@link HA_ORPHAN_ENTITIES_DEVICE_ID} bucket so
8622
+ * the Entities adoption tab + standalone reconcile can see them. Callers on
8623
+ * the Devices path must filter that bucket out.
8624
+ *
8452
8625
  * Unlike the old `HaDiscoveryService`, this is stateless — every call
8453
8626
  * goes to the live WS; caching is intentionally omitted to keep the
8454
8627
  * adoption path simple (the modal fetches once per session).
@@ -8469,15 +8642,59 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8469
8642
  if (areaId && name) areaById.set(areaId, name);
8470
8643
  }
8471
8644
  const entitiesByDevice = /* @__PURE__ */ new Map();
8645
+ const orphanRaws = [];
8472
8646
  for (const raw of rawEntities) {
8473
8647
  const entityId = typeof raw["entity_id"] === "string" ? raw["entity_id"] : null;
8474
8648
  const deviceId = typeof raw["device_id"] === "string" ? raw["device_id"] : null;
8475
8649
  const disabledBy = raw["disabled_by"] !== null && raw["disabled_by"] !== void 0 ? String(raw["disabled_by"]) : null;
8476
- if (!entityId || !deviceId || disabledBy !== null) continue;
8650
+ if (!entityId || disabledBy !== null) continue;
8651
+ if (!deviceId) {
8652
+ orphanRaws.push(raw);
8653
+ continue;
8654
+ }
8477
8655
  const bucket = entitiesByDevice.get(deviceId);
8478
8656
  if (bucket) bucket.push(raw);
8479
8657
  else entitiesByDevice.set(deviceId, [raw]);
8480
8658
  }
8659
+ const mapRawEntity = (raw, deviceAreaName) => {
8660
+ const entityId = raw["entity_id"];
8661
+ const domain = entityId.split(".")[0] ?? entityId;
8662
+ const stateEntry = stateSnapshot[entityId];
8663
+ const attributes = stateEntry?.attributes ?? {};
8664
+ const deviceClass = typeof raw["device_class"] === "string" ? raw["device_class"] : typeof raw["original_device_class"] === "string" ? raw["original_device_class"] : typeof attributes["device_class"] === "string" ? attributes["device_class"] : null;
8665
+ const unit = typeof attributes["unit_of_measurement"] === "string" ? attributes["unit_of_measurement"] : null;
8666
+ const rawOptions = attributes["options"];
8667
+ const options = Array.isArray(rawOptions) && rawOptions.length > 0 ? rawOptions.filter((x) => typeof x === "string") : null;
8668
+ const entityAreaId = typeof raw["area_id"] === "string" ? raw["area_id"] : null;
8669
+ const entityArea = entityAreaId ? areaById.get(entityAreaId) ?? deviceAreaName : deviceAreaName;
8670
+ const rawCategory = raw["entity_category"];
8671
+ const entityCategory = rawCategory === "config" || rawCategory === "diagnostic" ? rawCategory : null;
8672
+ const mapping = mapHaEntityToCamStack({
8673
+ domain,
8674
+ deviceClass,
8675
+ attributes
8676
+ });
8677
+ return {
8678
+ entityId,
8679
+ domain,
8680
+ name: typeof raw["name"] === "string" && raw["name"] ? raw["name"] : typeof raw["original_name"] === "string" && raw["original_name"] ? raw["original_name"] : (typeof attributes["friendly_name"] === "string" ? attributes["friendly_name"] : null) ?? entityId,
8681
+ state: stateEntry?.state ?? "unknown",
8682
+ deviceClass,
8683
+ unitOfMeasurement: unit,
8684
+ area: entityArea,
8685
+ options,
8686
+ entityCategory,
8687
+ mappedTo: mapping ? {
8688
+ deviceType: mapping.deviceType,
8689
+ role: mapping.role,
8690
+ capabilities: [...mapping.capabilities],
8691
+ features: mapping.features ? [...mapping.features] : [],
8692
+ ...mapping.dateTimeFormat !== void 0 ? { dateTimeFormat: mapping.dateTimeFormat } : {}
8693
+ } : null,
8694
+ alreadyAdopted: false,
8695
+ adoptedCamDeviceId: null
8696
+ };
8697
+ };
8481
8698
  const out = [];
8482
8699
  for (const rawDev of rawDevices) {
8483
8700
  const devId = typeof rawDev["id"] === "string" ? rawDev["id"] : null;
@@ -8486,45 +8703,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8486
8703
  const areaIdRaw = typeof rawDev["area_id"] === "string" ? rawDev["area_id"] : null;
8487
8704
  const deviceAreaName = areaIdRaw ? areaById.get(areaIdRaw) ?? null : null;
8488
8705
  const entBucket = entitiesByDevice.get(devId) ?? [];
8489
- const entities = entBucket.map((raw) => {
8490
- const entityId = raw["entity_id"];
8491
- const domain = entityId.split(".")[0] ?? entityId;
8492
- const stateEntry = stateSnapshot[entityId];
8493
- const attributes = stateEntry?.attributes ?? {};
8494
- const deviceClass = typeof raw["device_class"] === "string" ? raw["device_class"] : typeof raw["original_device_class"] === "string" ? raw["original_device_class"] : typeof attributes["device_class"] === "string" ? attributes["device_class"] : null;
8495
- const unit = typeof attributes["unit_of_measurement"] === "string" ? attributes["unit_of_measurement"] : null;
8496
- const rawOptions = attributes["options"];
8497
- const options = Array.isArray(rawOptions) && rawOptions.length > 0 ? rawOptions.filter((x) => typeof x === "string") : null;
8498
- const entityAreaId = typeof raw["area_id"] === "string" ? raw["area_id"] : null;
8499
- const entityArea = entityAreaId ? areaById.get(entityAreaId) ?? deviceAreaName : deviceAreaName;
8500
- const rawCategory = raw["entity_category"];
8501
- const entityCategory = rawCategory === "config" || rawCategory === "diagnostic" ? rawCategory : null;
8502
- const mapping = mapHaEntityToCamStack({
8503
- domain,
8504
- deviceClass,
8505
- attributes
8506
- });
8507
- return {
8508
- entityId,
8509
- domain,
8510
- name: typeof raw["name"] === "string" && raw["name"] ? raw["name"] : typeof raw["original_name"] === "string" && raw["original_name"] ? raw["original_name"] : (typeof attributes["friendly_name"] === "string" ? attributes["friendly_name"] : null) ?? entityId,
8511
- state: stateEntry?.state ?? "unknown",
8512
- deviceClass,
8513
- unitOfMeasurement: unit,
8514
- area: entityArea,
8515
- options,
8516
- entityCategory,
8517
- mappedTo: mapping ? {
8518
- deviceType: mapping.deviceType,
8519
- role: mapping.role,
8520
- capabilities: [...mapping.capabilities],
8521
- features: mapping.features ? [...mapping.features] : [],
8522
- ...mapping.dateTimeFormat !== void 0 ? { dateTimeFormat: mapping.dateTimeFormat } : {}
8523
- } : null,
8524
- alreadyAdopted: false,
8525
- adoptedCamDeviceId: null
8526
- };
8527
- });
8706
+ const entities = entBucket.map((raw) => mapRawEntity(raw, deviceAreaName));
8528
8707
  const devName = typeof rawDev["name_by_user"] === "string" && rawDev["name_by_user"] ? rawDev["name_by_user"] : typeof rawDev["name"] === "string" && rawDev["name"] ? rawDev["name"] : entBucket[0] ? String(entBucket[0]["name"] ?? entBucket[0]["original_name"] ?? devId) : devId;
8529
8708
  const integration = deriveIntegrationDomain(entBucket.map((raw) => typeof raw["platform"] === "string" ? raw["platform"] : null));
8530
8709
  out.push({
@@ -8541,6 +8720,23 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8541
8720
  adoptedCamDeviceId: null
8542
8721
  });
8543
8722
  }
8723
+ if (orphanRaws.length > 0) {
8724
+ const entities = orphanRaws.map((raw) => mapRawEntity(raw, null));
8725
+ const entityPlatforms = orphanRaws.map((raw) => typeof raw["platform"] === "string" ? raw["platform"] : null);
8726
+ out.push({
8727
+ haDeviceId: HA_ORPHAN_ENTITIES_DEVICE_ID,
8728
+ name: "Standalone entities",
8729
+ area: null,
8730
+ manufacturer: null,
8731
+ model: null,
8732
+ swVersion: null,
8733
+ integration: deriveIntegrationDomain(entityPlatforms),
8734
+ entityCount: entities.length,
8735
+ entities,
8736
+ alreadyAdopted: false,
8737
+ adoptedCamDeviceId: null
8738
+ });
8739
+ }
8544
8740
  return out;
8545
8741
  }
8546
8742
  /**
@@ -11132,7 +11132,16 @@ var AutomationActionSchema = discriminatedUnion("kind", [
11132
11132
  }),
11133
11133
  object({
11134
11134
  kind: literal("cap"),
11135
- deviceId: union([number().int(), object({ $var: string().min(1) })]),
11135
+ /**
11136
+ * One device, several same-family devices (locks / climate family / …),
11137
+ * or `{ $var }` for the device that triggered the run. An array is
11138
+ * fan-out at codegen — each id gets the same cap.method call.
11139
+ */
11140
+ deviceId: union([
11141
+ number().int(),
11142
+ array(number().int()).min(1),
11143
+ object({ $var: string().min(1) })
11144
+ ]),
11136
11145
  cap: string().min(1),
11137
11146
  method: string().min(1),
11138
11147
  /** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
@@ -14455,7 +14464,7 @@ DeviceType.Camera;
14455
14464
  * This matters here more than anywhere else in the repo, because every provider
14456
14465
  * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14457
14466
  * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14458
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14467
+ * and `http://admin:hunter2@192.168.x.x/` is a URL a browser accepts. The
14459
14468
  * camera's own page will ask for its own login. That is correct, and pre-
14460
14469
  * filling it is the operator's business, not ours.
14461
14470
  *
@@ -20855,17 +20864,41 @@ var NC_ALARM_TRIGGERED_MAX_SEC = 3600;
20855
20864
  var NcAlarmDelaySecSchema = number().int().min(0).max(600);
20856
20865
  var NcAlarmTriggeredSecSchema = number().int().min(0).max(NC_ALARM_TRIGGERED_MAX_SEC);
20857
20866
  var uniqueModes = (entries) => new Set(entries.map((e) => e.mode)).size === entries.length;
20867
+ /**
20868
+ * What the panel does when a bounded triggered window ends.
20869
+ * - `rearmAfter` — return to the armed mode
20870
+ * - `disableAfter` — auto-disarm
20871
+ * - `doNothing` — stay triggered until a human disarms
20872
+ */
20873
+ var NcAlarmPostTriggeredActionSchema = _enum([
20874
+ "rearmAfter",
20875
+ "disableAfter",
20876
+ "doNothing"
20877
+ ]);
20858
20878
  var NcAlarmModeDelaysListSchema = array(object({
20859
20879
  mode: AlarmArmModeSchema,
20860
20880
  exitDelaySec: NcAlarmDelaySecSchema.optional(),
20861
20881
  entryDelaySec: NcAlarmDelaySecSchema.optional(),
20862
- triggeredDurationSec: NcAlarmTriggeredSecSchema.optional()
20882
+ /**
20883
+ * Duration of `triggered` before {@link postTriggeredAction}. Kept as the
20884
+ * historical field name so older clients still read/write a number.
20885
+ */
20886
+ triggeredDurationSec: NcAlarmTriggeredSecSchema.optional(),
20887
+ postTriggeredAction: NcAlarmPostTriggeredActionSchema.optional(),
20888
+ /**
20889
+ * When this mode may be armed. Absent / null = always allowed (same as a
20890
+ * rule with no `schedule`). Evaluated with {@link isScheduleActive}.
20891
+ */
20892
+ schedule: NcScheduleSchema.optional()
20863
20893
  })).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
20864
20894
  var NcAlarmModeDelaysEditListSchema = array(object({
20865
20895
  mode: AlarmArmModeSchema,
20866
20896
  exitDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
20867
20897
  entryDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
20868
- triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional()
20898
+ triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional(),
20899
+ postTriggeredAction: NcAlarmPostTriggeredActionSchema.nullable().optional(),
20900
+ /** Set a schedule, or `null` to clear (mode always allowed again). */
20901
+ schedule: NcScheduleSchema.nullable().optional()
20869
20902
  })).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
20870
20903
  /** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
20871
20904
  var NcAlarmNonBlockingEditSchema = object({
@@ -20937,12 +20970,18 @@ var NcAlarmSettingsSchema = object({
20937
20970
  /** Grace period between a trigger and the alarm firing. 0 = immediate. */
20938
20971
  entryDelaySec: NcAlarmDelaySecSchema,
20939
20972
  /**
20940
- * How long `triggered` lasts before the panel re-arms itself.
20941
- * **0 = until an operator disarms it** — the behaviour before this field
20942
- * existed, and therefore what an untouched install keeps doing.
20973
+ * How long `triggered` lasts before {@link postTriggeredAction} runs.
20974
+ * Ignored when the action is `doNothing`. Historical name kept so older
20975
+ * clients still round-trip a number.
20943
20976
  */
20944
20977
  triggeredDurationSec: NcAlarmTriggeredSecSchema,
20945
20978
  /**
20979
+ * What happens when the triggered window ends. Absent on a blob older than
20980
+ * this field → derived from {@link triggeredDurationSec} (`>0` ⇒ rearmAfter,
20981
+ * else doNothing).
20982
+ */
20983
+ postTriggeredAction: NcAlarmPostTriggeredActionSchema.optional(),
20984
+ /**
20946
20985
  * Deprecated by D630 — every transition is announced. Parsed, never read;
20947
20986
  * remove after one release. Optional so an older admin's patch (which still
20948
20987
  * sends it) parses rather than failing the whole save.
@@ -32493,7 +32532,7 @@ var LocalInterfaceSchema = object({
32493
32532
  name: string(),
32494
32533
  family: _enum(["IPv4", "IPv6"]),
32495
32534
  address: string(),
32496
- /** CIDR shorthand (e.g. `192.168.1.10/24`). Empty when OS doesn't expose. */
32535
+ /** CIDR shorthand (e.g. `192.168.x.x/24`). Empty when OS doesn't expose. */
32497
32536
  cidr: string(),
32498
32537
  /** OS netmask. Stable across calls so the SDK can hash on it. */
32499
32538
  netmask: string(),
@@ -33007,7 +33046,7 @@ var MeshPeerSchema = object({
33007
33046
  tags: array(string()).readonly(),
33008
33047
  /** True when the peer advertises itself as an exit-node candidate. */
33009
33048
  exitNodeOption: boolean(),
33010
- /** Subnet routes the peer advertises (e.g. `["192.168.1.0/24"]`). */
33049
+ /** Subnet routes the peer advertises (e.g. `["192.168.x.x/24"]`). */
33011
33050
  advertisedRoutes: array(string()).readonly(),
33012
33051
  /** Owner account login (e.g. `foo@example.com`). Null for tagged peers. */
33013
33052
  userLogin: string().nullable(),
@@ -48578,6 +48617,7 @@ Object.freeze({
48578
48617
  access: "create"
48579
48618
  }
48580
48619
  });
48620
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
48581
48621
  DeviceFeature.BatteryOperated;
48582
48622
  /**
48583
48623
  * The vocabulary, in the order a picker offers it.
@@ -53764,4 +53804,4 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
53764
53804
  var MB = 1024 * 1024;
53765
53805
  1024 * MB, 3072 * MB;
53766
53806
  //#endregion
53767
- export { oauthIntegrationCapability as $, connectivityCapability as A, string as At, floodCapability as B, buildAddonRouteProvider as C, _enum as Ct, carbonMonoxideCapability as D, number as Dt, calendarSourceCapability as E, literal as Et, deviceAdoptionCapability as F, lawnMowerControlCapability as G, humidifierCapability as H, deviceExportCapability as I, motionCapability as J, lockControlCapability as K, enumSensorCapability as L, controlCapability as M, unknown as Mt, coverCapability as N, url as Nt, climateControlCapability as O, object as Ot, deviceAdminLinkCapability as P, EventCategory as Pt, numericSensorCapability as Q, eventEmitterCapability as R, brokerCapability as S, nodePin as St, buttonCapability as T, discriminatedUnion as Tt, humiditySensorCapability as U, gasCapability as V, imageCapability as W, notificationOutputCapability as X, normalizeUnit as Y, notifierCapability as Z, automationControlCapability as _, DeviceRole as _t, COCO_TO_MACRO as a, scriptRunnerCapability as at, binaryCapability as b, BaseAddon as bt, FanDirectionSchema as c, tamperCapability as ct, TargetSchema as d, vacuumControlCapability as dt, parseAdminBase as et, accessoriesCapability as f, valveCapability as ft, ambientLightSensorCapability as g, DeviceFeature as gt, alarmPanelCapability as h, weatherCapability as ht, CAMERA_SWITCH_ORDER as i, pressureSensorCapability as it, contactCapability as j, union as jt, colorCapability as k, record as kt, HvacModeSchema as l, temperatureSensorCapability as lt, airQualitySensorCapability as m, waterHeaterCapability as mt, BaseDevice as n, prepareNotification as nt, CameraSwitchIdSchema as o, smokeCapability as ot, addonRoutesCapability as p, vibrationCapability as pt, mediaPlayerCapability as q, BaseDeviceProvider as r, presenceCapability as rt, EnumSensorDateTimeFormatSchema as s, switchCapability as st, AlarmArmModeSchema as t, powerMeterCapability as tt, MediaPlayerRepeatSchema as u, updateCapability as ut, batteryCapability as v, DeviceType as vt, buildDeviceAdminUrl as w, array as wt, brightnessCapability as x, createEvent as xt, bestLocationMatch as y, errMsg as yt, fanControlCapability as z };
53807
+ export { oauthIntegrationCapability as $, connectivityCapability as A, record as At, floodCapability as B, buildAddonRouteProvider as C, _enum as Ct, carbonMonoxideCapability as D, literal as Dt, calendarSourceCapability as E, discriminatedUnion as Et, deviceAdoptionCapability as F, EventCategory as Ft, lawnMowerControlCapability as G, humidifierCapability as H, deviceExportCapability as I, motionCapability as J, lockControlCapability as K, enumSensorCapability as L, controlCapability as M, union as Mt, coverCapability as N, unknown as Nt, climateControlCapability as O, number as Ot, deviceAdminLinkCapability as P, url as Pt, numericSensorCapability as Q, eventEmitterCapability as R, brokerCapability as S, nodePin as St, buttonCapability as T, boolean as Tt, humiditySensorCapability as U, gasCapability as V, imageCapability as W, notificationOutputCapability as X, normalizeUnit as Y, notifierCapability as Z, automationControlCapability as _, DeviceRole as _t, COCO_TO_MACRO as a, scriptRunnerCapability as at, binaryCapability as b, BaseAddon as bt, FanDirectionSchema as c, tamperCapability as ct, TargetSchema as d, vacuumControlCapability as dt, parseAdminBase as et, accessoriesCapability as f, valveCapability as ft, ambientLightSensorCapability as g, DeviceFeature as gt, alarmPanelCapability as h, weatherCapability as ht, CAMERA_SWITCH_ORDER as i, pressureSensorCapability as it, contactCapability as j, string as jt, colorCapability as k, object as kt, HvacModeSchema as l, temperatureSensorCapability as lt, airQualitySensorCapability as m, waterHeaterCapability as mt, BaseDevice as n, prepareNotification as nt, CameraSwitchIdSchema as o, smokeCapability as ot, addonRoutesCapability as p, vibrationCapability as pt, mediaPlayerCapability as q, BaseDeviceProvider as r, presenceCapability as rt, EnumSensorDateTimeFormatSchema as s, switchCapability as st, AlarmArmModeSchema as t, powerMeterCapability as tt, MediaPlayerRepeatSchema as u, updateCapability as ut, batteryCapability as v, DeviceType as vt, buildDeviceAdminUrl as w, array as wt, brightnessCapability as x, createEvent as xt, bestLocationMatch as y, errMsg as yt, fanControlCapability as z };
@@ -11133,7 +11133,16 @@ var AutomationActionSchema = discriminatedUnion("kind", [
11133
11133
  }),
11134
11134
  object({
11135
11135
  kind: literal("cap"),
11136
- deviceId: union([number().int(), object({ $var: string().min(1) })]),
11136
+ /**
11137
+ * One device, several same-family devices (locks / climate family / …),
11138
+ * or `{ $var }` for the device that triggered the run. An array is
11139
+ * fan-out at codegen — each id gets the same cap.method call.
11140
+ */
11141
+ deviceId: union([
11142
+ number().int(),
11143
+ array(number().int()).min(1),
11144
+ object({ $var: string().min(1) })
11145
+ ]),
11137
11146
  cap: string().min(1),
11138
11147
  method: string().min(1),
11139
11148
  /** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
@@ -14456,7 +14465,7 @@ DeviceType.Camera;
14456
14465
  * This matters here more than anywhere else in the repo, because every provider
14457
14466
  * that knows a device's host knows its PASSWORD too: `{ host, port, username,
14458
14467
  * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
14459
- * and `http://admin:hunter2@192.168.50.139/` is a URL a browser accepts. The
14468
+ * and `http://admin:hunter2@192.168.x.x/` is a URL a browser accepts. The
14460
14469
  * camera's own page will ask for its own login. That is correct, and pre-
14461
14470
  * filling it is the operator's business, not ours.
14462
14471
  *
@@ -20856,17 +20865,41 @@ var NC_ALARM_TRIGGERED_MAX_SEC = 3600;
20856
20865
  var NcAlarmDelaySecSchema = number().int().min(0).max(600);
20857
20866
  var NcAlarmTriggeredSecSchema = number().int().min(0).max(NC_ALARM_TRIGGERED_MAX_SEC);
20858
20867
  var uniqueModes = (entries) => new Set(entries.map((e) => e.mode)).size === entries.length;
20868
+ /**
20869
+ * What the panel does when a bounded triggered window ends.
20870
+ * - `rearmAfter` — return to the armed mode
20871
+ * - `disableAfter` — auto-disarm
20872
+ * - `doNothing` — stay triggered until a human disarms
20873
+ */
20874
+ var NcAlarmPostTriggeredActionSchema = _enum([
20875
+ "rearmAfter",
20876
+ "disableAfter",
20877
+ "doNothing"
20878
+ ]);
20859
20879
  var NcAlarmModeDelaysListSchema = array(object({
20860
20880
  mode: AlarmArmModeSchema,
20861
20881
  exitDelaySec: NcAlarmDelaySecSchema.optional(),
20862
20882
  entryDelaySec: NcAlarmDelaySecSchema.optional(),
20863
- triggeredDurationSec: NcAlarmTriggeredSecSchema.optional()
20883
+ /**
20884
+ * Duration of `triggered` before {@link postTriggeredAction}. Kept as the
20885
+ * historical field name so older clients still read/write a number.
20886
+ */
20887
+ triggeredDurationSec: NcAlarmTriggeredSecSchema.optional(),
20888
+ postTriggeredAction: NcAlarmPostTriggeredActionSchema.optional(),
20889
+ /**
20890
+ * When this mode may be armed. Absent / null = always allowed (same as a
20891
+ * rule with no `schedule`). Evaluated with {@link isScheduleActive}.
20892
+ */
20893
+ schedule: NcScheduleSchema.optional()
20864
20894
  })).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
20865
20895
  var NcAlarmModeDelaysEditListSchema = array(object({
20866
20896
  mode: AlarmArmModeSchema,
20867
20897
  exitDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
20868
20898
  entryDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
20869
- triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional()
20899
+ triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional(),
20900
+ postTriggeredAction: NcAlarmPostTriggeredActionSchema.nullable().optional(),
20901
+ /** Set a schedule, or `null` to clear (mode always allowed again). */
20902
+ schedule: NcScheduleSchema.nullable().optional()
20870
20903
  })).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
20871
20904
  /** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
20872
20905
  var NcAlarmNonBlockingEditSchema = object({
@@ -20938,12 +20971,18 @@ var NcAlarmSettingsSchema = object({
20938
20971
  /** Grace period between a trigger and the alarm firing. 0 = immediate. */
20939
20972
  entryDelaySec: NcAlarmDelaySecSchema,
20940
20973
  /**
20941
- * How long `triggered` lasts before the panel re-arms itself.
20942
- * **0 = until an operator disarms it** — the behaviour before this field
20943
- * existed, and therefore what an untouched install keeps doing.
20974
+ * How long `triggered` lasts before {@link postTriggeredAction} runs.
20975
+ * Ignored when the action is `doNothing`. Historical name kept so older
20976
+ * clients still round-trip a number.
20944
20977
  */
20945
20978
  triggeredDurationSec: NcAlarmTriggeredSecSchema,
20946
20979
  /**
20980
+ * What happens when the triggered window ends. Absent on a blob older than
20981
+ * this field → derived from {@link triggeredDurationSec} (`>0` ⇒ rearmAfter,
20982
+ * else doNothing).
20983
+ */
20984
+ postTriggeredAction: NcAlarmPostTriggeredActionSchema.optional(),
20985
+ /**
20947
20986
  * Deprecated by D630 — every transition is announced. Parsed, never read;
20948
20987
  * remove after one release. Optional so an older admin's patch (which still
20949
20988
  * sends it) parses rather than failing the whole save.
@@ -32494,7 +32533,7 @@ var LocalInterfaceSchema = object({
32494
32533
  name: string(),
32495
32534
  family: _enum(["IPv4", "IPv6"]),
32496
32535
  address: string(),
32497
- /** CIDR shorthand (e.g. `192.168.1.10/24`). Empty when OS doesn't expose. */
32536
+ /** CIDR shorthand (e.g. `192.168.x.x/24`). Empty when OS doesn't expose. */
32498
32537
  cidr: string(),
32499
32538
  /** OS netmask. Stable across calls so the SDK can hash on it. */
32500
32539
  netmask: string(),
@@ -33008,7 +33047,7 @@ var MeshPeerSchema = object({
33008
33047
  tags: array(string()).readonly(),
33009
33048
  /** True when the peer advertises itself as an exit-node candidate. */
33010
33049
  exitNodeOption: boolean(),
33011
- /** Subnet routes the peer advertises (e.g. `["192.168.1.0/24"]`). */
33050
+ /** Subnet routes the peer advertises (e.g. `["192.168.x.x/24"]`). */
33012
33051
  advertisedRoutes: array(string()).readonly(),
33013
33052
  /** Owner account login (e.g. `foo@example.com`). Null for tagged peers. */
33014
33053
  userLogin: string().nullable(),
@@ -48579,6 +48618,7 @@ Object.freeze({
48579
48618
  access: "create"
48580
48619
  }
48581
48620
  });
48621
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
48582
48622
  DeviceFeature.BatteryOperated;
48583
48623
  /**
48584
48624
  * The vocabulary, in the order a picker offers it.
@@ -53927,6 +53967,12 @@ Object.defineProperty(exports, "binaryCapability", {
53927
53967
  return binaryCapability;
53928
53968
  }
53929
53969
  });
53970
+ Object.defineProperty(exports, "boolean", {
53971
+ enumerable: true,
53972
+ get: function() {
53973
+ return boolean;
53974
+ }
53975
+ });
53930
53976
  Object.defineProperty(exports, "brightnessCapability", {
53931
53977
  enumerable: true,
53932
53978
  get: function() {
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  //#endregion
6
- const require_dist = require("../dist-CUS3XDdU.js");
6
+ const require_dist = require("../dist-M6hP_tw3.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/ha-export/topics.ts
9
9
  /**
@@ -1,4 +1,4 @@
1
- import { $ as oauthIntegrationCapability, At as string, C as buildAddonRouteProvider, I as deviceExportCapability, Pt as EventCategory, St as nodePin, a as COCO_TO_MACRO, bt as BaseAddon, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema } from "../dist-CbGUsakM.mjs";
1
+ import { $ as oauthIntegrationCapability, C as buildAddonRouteProvider, Ft as EventCategory, I as deviceExportCapability, St as nodePin, a as COCO_TO_MACRO, bt as BaseAddon, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, jt as string, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema } from "../dist-BgTykMb6.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  //#region src/ha-export/topics.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.156",
3
+ "version": "1.2.158",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",