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