@camstack/addon-provider-homeassistant 1.2.155 → 1.2.157

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-CERTsNmo.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
@@ -7047,6 +7179,11 @@ function createHaNotificationOutputProvider(deps) {
7047
7179
  }
7048
7180
  return out;
7049
7181
  },
7182
+ probeTargetConfig: async ({ kind }) => ({
7183
+ ok: false,
7184
+ resolvedUrl: "",
7185
+ error: `Kind '${kind}' does not support credential probing`
7186
+ }),
7050
7187
  send: async ({ targetId, notification }) => {
7051
7188
  const target = await store.getById(targetId);
7052
7189
  if (!target) return {
@@ -7094,9 +7231,29 @@ function calendarName(state) {
7094
7231
  if (typeof friendly === "string" && friendly.trim().length > 0) return friendly.trim();
7095
7232
  return state.entity_id.replace(/^calendar\./, "").replace(/_/g, " ");
7096
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
+ }
7097
7253
  function parseHaDate(value) {
7098
7254
  if (typeof value !== "string" || value.length === 0) return null;
7099
- 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);
7100
7257
  return Number.isFinite(ms) ? ms : null;
7101
7258
  }
7102
7259
  function isRecord(x) {
@@ -7107,17 +7264,17 @@ function asEntityState(raw) {
7107
7264
  return parsed.success ? parsed.data : null;
7108
7265
  }
7109
7266
  /**
7110
- * HA's `calendar.list_events` response shape (return_response):
7111
- * `{ response: { [entity_id]: { events: [...] } } }` or a flat
7112
- * `{ 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: [...] }`.
7113
7270
  */
7114
7271
  function extractEvents(result, entityId) {
7115
7272
  if (!isRecord(result)) return [];
7116
- const response = result["response"];
7273
+ const response = isRecord(result["response"]) ? result["response"] : result;
7117
7274
  if (isRecord(response)) {
7118
7275
  const forEntity = response[entityId];
7119
7276
  if (isRecord(forEntity) && Array.isArray(forEntity["events"])) return forEntity["events"].filter(isRecord);
7120
- 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 [];
7121
7278
  }
7122
7279
  if (Array.isArray(result["events"])) return result["events"].filter(isRecord);
7123
7280
  return [];
@@ -7145,6 +7302,29 @@ function mapEvent(entityId, raw, index) {
7145
7302
  ...typeof location === "string" && location.length > 0 ? { location } : {}
7146
7303
  };
7147
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
+ }
7148
7328
  function createHaCalendarSource(deps) {
7149
7329
  return {
7150
7330
  listCalendars: async () => {
@@ -7173,26 +7353,15 @@ function createHaCalendarSource(deps) {
7173
7353
  for (const { brokerId, manager } of deps.brokers.listManagers()) {
7174
7354
  const states = manager.snapshotAllStates();
7175
7355
  const entityIds = Object.keys(states).filter((id) => isCalendarEntity(id) && (wanted === null || wanted.has(id)));
7176
- for (const entityId of entityIds) try {
7177
- const target = {
7178
- domain: "calendar",
7179
- service: "list_events",
7180
- entityId,
7181
- returnResponse: true
7182
- };
7183
- const rawEvents = extractEvents(await manager.publish(target, {
7184
- start_date_time: startIso,
7185
- end_date_time: endIso
7186
- }), entityId);
7187
- for (let i = 0; i < rawEvents.length; i += 1) {
7188
- const mapped = mapEvent(entityId, rawEvents[i], i);
7189
- if (mapped !== null) out.push(mapped);
7190
- }
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);
7191
7360
  } catch (err) {
7192
- deps.logger.warn("HA calendar.list_events failed", { meta: {
7193
- entityId,
7361
+ deps.logger.warn("HA calendar.get_events failed", { meta: {
7362
+ entityIds,
7194
7363
  brokerId,
7195
- error: err instanceof Error ? err.message : String(err)
7364
+ error: formatHaError(err)
7196
7365
  } });
7197
7366
  }
7198
7367
  }
@@ -8075,7 +8244,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8075
8244
  let lastError = null;
8076
8245
  for (const { brokerId } of managers) try {
8077
8246
  const devices = await this.fetchHaDiscoveredDevices(brokerId);
8078
- candidateCount += devices.length;
8247
+ candidateCount += devices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID).length;
8079
8248
  const refreshedAt = Date.now();
8080
8249
  if (lastDiscoveryAt === null || refreshedAt > lastDiscoveryAt) lastDiscoveryAt = refreshedAt;
8081
8250
  } catch (err) {
@@ -8095,7 +8264,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8095
8264
  const adoptedCount = (await this.ctx.kernel.devices.getAll()).filter((d) => d.parentDeviceId === null && String(d.config.get("system") ?? "") === "homeassistant").length;
8096
8265
  return {
8097
8266
  lastDiscoveryAt: Date.now(),
8098
- candidateCount: devices.length,
8267
+ candidateCount: devices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID).length,
8099
8268
  adoptedCount,
8100
8269
  lastError: null
8101
8270
  };
@@ -8124,6 +8293,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8124
8293
  if (typeof haDeviceId === "string") adoptedHaDeviceIds.add(haDeviceId);
8125
8294
  }
8126
8295
  let haDevices = await this.fetchHaDiscoveredDevices(brokerId);
8296
+ haDevices = haDevices.filter((d) => d.haDeviceId !== HA_ORPHAN_ENTITIES_DEVICE_ID);
8127
8297
  haDevices = haDevices.map((dev) => ({
8128
8298
  ...dev,
8129
8299
  alreadyAdopted: adoptedHaDeviceIds.has(dev.haDeviceId)
@@ -8160,7 +8330,9 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8160
8330
  const haDeviceId = d.config.get("haDeviceId");
8161
8331
  if (typeof haDeviceId === "string") adoptedHaDeviceIds.add(haDeviceId);
8162
8332
  }
8163
- 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;
8164
8336
  if (!found) return null;
8165
8337
  return haDeviceToCandidate({
8166
8338
  ...found,
@@ -8188,6 +8360,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8188
8360
  let failures = 0;
8189
8361
  let reused = 0;
8190
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");
8191
8364
  const haDevice = haDeviceMap.get(haDeviceId);
8192
8365
  const deviceName = (perCandidate?.[haDeviceId])?.name ?? haDevice?.name ?? haDeviceId;
8193
8366
  const existing = existingByStableId.get(`ha:${brokerId}:dev:${haDeviceId}`);
@@ -8445,6 +8618,11 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8445
8618
  * it to the addon-internal `HaDiscoveredDevice[]` shape. Shared between
8446
8619
  * `discoverDevices` (device-provider) and the `device-adoption` handlers.
8447
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
+ *
8448
8626
  * Unlike the old `HaDiscoveryService`, this is stateless — every call
8449
8627
  * goes to the live WS; caching is intentionally omitted to keep the
8450
8628
  * adoption path simple (the modal fetches once per session).
@@ -8465,15 +8643,59 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8465
8643
  if (areaId && name) areaById.set(areaId, name);
8466
8644
  }
8467
8645
  const entitiesByDevice = /* @__PURE__ */ new Map();
8646
+ const orphanRaws = [];
8468
8647
  for (const raw of rawEntities) {
8469
8648
  const entityId = typeof raw["entity_id"] === "string" ? raw["entity_id"] : null;
8470
8649
  const deviceId = typeof raw["device_id"] === "string" ? raw["device_id"] : null;
8471
8650
  const disabledBy = raw["disabled_by"] !== null && raw["disabled_by"] !== void 0 ? String(raw["disabled_by"]) : null;
8472
- if (!entityId || !deviceId || disabledBy !== null) continue;
8651
+ if (!entityId || disabledBy !== null) continue;
8652
+ if (!deviceId) {
8653
+ orphanRaws.push(raw);
8654
+ continue;
8655
+ }
8473
8656
  const bucket = entitiesByDevice.get(deviceId);
8474
8657
  if (bucket) bucket.push(raw);
8475
8658
  else entitiesByDevice.set(deviceId, [raw]);
8476
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
+ };
8477
8699
  const out = [];
8478
8700
  for (const rawDev of rawDevices) {
8479
8701
  const devId = typeof rawDev["id"] === "string" ? rawDev["id"] : null;
@@ -8482,45 +8704,7 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8482
8704
  const areaIdRaw = typeof rawDev["area_id"] === "string" ? rawDev["area_id"] : null;
8483
8705
  const deviceAreaName = areaIdRaw ? areaById.get(areaIdRaw) ?? null : null;
8484
8706
  const entBucket = entitiesByDevice.get(devId) ?? [];
8485
- const entities = entBucket.map((raw) => {
8486
- const entityId = raw["entity_id"];
8487
- const domain = entityId.split(".")[0] ?? entityId;
8488
- const stateEntry = stateSnapshot[entityId];
8489
- const attributes = stateEntry?.attributes ?? {};
8490
- 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;
8491
- const unit = typeof attributes["unit_of_measurement"] === "string" ? attributes["unit_of_measurement"] : null;
8492
- const rawOptions = attributes["options"];
8493
- const options = Array.isArray(rawOptions) && rawOptions.length > 0 ? rawOptions.filter((x) => typeof x === "string") : null;
8494
- const entityAreaId = typeof raw["area_id"] === "string" ? raw["area_id"] : null;
8495
- const entityArea = entityAreaId ? areaById.get(entityAreaId) ?? deviceAreaName : deviceAreaName;
8496
- const rawCategory = raw["entity_category"];
8497
- const entityCategory = rawCategory === "config" || rawCategory === "diagnostic" ? rawCategory : null;
8498
- const mapping = mapHaEntityToCamStack({
8499
- domain,
8500
- deviceClass,
8501
- attributes
8502
- });
8503
- return {
8504
- entityId,
8505
- domain,
8506
- 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,
8507
- state: stateEntry?.state ?? "unknown",
8508
- deviceClass,
8509
- unitOfMeasurement: unit,
8510
- area: entityArea,
8511
- options,
8512
- entityCategory,
8513
- mappedTo: mapping ? {
8514
- deviceType: mapping.deviceType,
8515
- role: mapping.role,
8516
- capabilities: [...mapping.capabilities],
8517
- features: mapping.features ? [...mapping.features] : [],
8518
- ...mapping.dateTimeFormat !== void 0 ? { dateTimeFormat: mapping.dateTimeFormat } : {}
8519
- } : null,
8520
- alreadyAdopted: false,
8521
- adoptedCamDeviceId: null
8522
- };
8523
- });
8707
+ const entities = entBucket.map((raw) => mapRawEntity(raw, deviceAreaName));
8524
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;
8525
8709
  const integration = deriveIntegrationDomain(entBucket.map((raw) => typeof raw["platform"] === "string" ? raw["platform"] : null));
8526
8710
  out.push({
@@ -8537,6 +8721,23 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
8537
8721
  adoptedCamDeviceId: null
8538
8722
  });
8539
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
+ }
8540
8741
  return out;
8541
8742
  }
8542
8743
  /**