@camstack/addon-provider-homeassistant 1.2.156 → 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 +272 -76
- package/dist/addon.mjs +272 -76
- package/dist/{dist-CbGUsakM.mjs → dist-BgTykMb6.mjs} +50 -10
- package/dist/{dist-CUS3XDdU.js → dist-M6hP_tw3.js} +55 -9
- package/dist/ha-export/ha-export.addon.js +1 -1
- package/dist/ha-export/ha-export.addon.mjs +1 -1
- 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
|
|
@@ -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
|
|
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.
|
|
7116
|
-
* `{ response: { [entity_id]: { events: [...] } } }`
|
|
7117
|
-
* `{
|
|
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
|
-
|
|
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
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
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.
|
|
7198
|
-
|
|
7361
|
+
deps.logger.warn("HA calendar.get_events failed", { meta: {
|
|
7362
|
+
entityIds,
|
|
7199
7363
|
brokerId,
|
|
7200
|
-
error:
|
|
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
|
|
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 ||
|
|
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
|
|
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
|
|
@@ -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
|
|
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.
|
|
7115
|
-
* `{ response: { [entity_id]: { events: [...] } } }`
|
|
7116
|
-
* `{
|
|
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
|
-
|
|
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
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
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.
|
|
7197
|
-
|
|
7360
|
+
deps.logger.warn("HA calendar.get_events failed", { meta: {
|
|
7361
|
+
entityIds,
|
|
7198
7362
|
brokerId,
|
|
7199
|
-
error:
|
|
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
|
|
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 ||
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
20941
|
-
*
|
|
20942
|
-
*
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
20942
|
-
*
|
|
20943
|
-
*
|
|
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.
|
|
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.
|
|
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-
|
|
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,
|
|
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
|
/**
|