@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 +277 -76
- package/dist/addon.mjs +277 -76
- package/dist/{dist-DqKiElqb.mjs → dist-BgTykMb6.mjs} +144 -13
- package/dist/{dist-CERTsNmo.js → dist-M6hP_tw3.js} +149 -12
- package/dist/ha-export/ha-export.addon.js +110 -4
- package/dist/ha-export/ha-export.addon.mjs +110 -4
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_dist = require("./dist-
|
|
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:
|
|
735
|
-
lastChangedAt:
|
|
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:
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
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
|
|
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.
|
|
7111
|
-
* `{ response: { [entity_id]: { events: [...] } } }`
|
|
7112
|
-
* `{
|
|
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
|
-
|
|
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
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
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.
|
|
7193
|
-
|
|
7361
|
+
deps.logger.warn("HA calendar.get_events failed", { meta: {
|
|
7362
|
+
entityIds,
|
|
7194
7363
|
brokerId,
|
|
7195
|
-
error:
|
|
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
|
|
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 ||
|
|
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
|
/**
|