@camstack/addon-provider-homeassistant 1.2.152 → 1.2.154
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 +164 -10
- package/dist/addon.mjs +164 -10
- package/dist/{dist-BITMr6s5.mjs → dist-CvzpCuzm.mjs} +787 -121
- package/dist/{dist-nbfaHTXG.js → dist-DLQgcqQT.js} +792 -120
- package/dist/ha-export/ha-export.addon.js +1 -1
- package/dist/ha-export/ha-export.addon.mjs +1 -1
- package/package.json +4 -3
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-DLQgcqQT.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
|
|
@@ -960,7 +960,9 @@ var HaBinarySensorDevice = class extends HaBaseChildDevice {
|
|
|
960
960
|
this.capName = capName;
|
|
961
961
|
this.registerCap(capName);
|
|
962
962
|
const stored = this.runtimeState.getCapState(capName);
|
|
963
|
-
|
|
963
|
+
const seededActive = storedActiveLevel(capName, stored) ?? false;
|
|
964
|
+
const seededChangedAt = storedChangedAt(capName, stored) ?? 0;
|
|
965
|
+
this.runtimeState.setCapState(this.capName, buildSlice(capName, seededActive, seededChangedAt, stored));
|
|
964
966
|
}
|
|
965
967
|
handleStatePush(state) {
|
|
966
968
|
if (state.state === "unavailable" || state.state === "unknown") return;
|
|
@@ -1052,6 +1054,32 @@ function priorLastDetectedAt(prior) {
|
|
|
1052
1054
|
const raw = prior?.["lastDetectedAt"];
|
|
1053
1055
|
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
1054
1056
|
}
|
|
1057
|
+
/** The boolean field this cap's slice uses as "active". */
|
|
1058
|
+
function activeFieldName(capName) {
|
|
1059
|
+
switch (capName) {
|
|
1060
|
+
case "contact": return "entryOpen";
|
|
1061
|
+
case "flood": return "flooded";
|
|
1062
|
+
case "tamper": return "tampered";
|
|
1063
|
+
case "connectivity": return "connected";
|
|
1064
|
+
case "binary": return "on";
|
|
1065
|
+
case "smoke":
|
|
1066
|
+
case "carbon-monoxide":
|
|
1067
|
+
case "gas":
|
|
1068
|
+
case "vibration":
|
|
1069
|
+
case "motion": return "detected";
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function storedActiveLevel(capName, stored) {
|
|
1073
|
+
if (stored === void 0) return void 0;
|
|
1074
|
+
const raw = stored[activeFieldName(capName)];
|
|
1075
|
+
return typeof raw === "boolean" ? raw : void 0;
|
|
1076
|
+
}
|
|
1077
|
+
function storedChangedAt(capName, stored) {
|
|
1078
|
+
if (stored === void 0) return void 0;
|
|
1079
|
+
if (capName === "motion") return priorLastDetectedAt(stored) ?? void 0;
|
|
1080
|
+
const raw = stored["lastChangedAt"];
|
|
1081
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : void 0;
|
|
1082
|
+
}
|
|
1055
1083
|
function buildSlice(capName, active, lastChangedAt, prior) {
|
|
1056
1084
|
switch (capName) {
|
|
1057
1085
|
case "contact": return {
|
|
@@ -5102,6 +5130,7 @@ var HaIntegrationManager = class HaIntegrationManager {
|
|
|
5102
5130
|
if (target.deviceId) targetEnvelope["device_id"] = target.deviceId;
|
|
5103
5131
|
if (Object.keys(targetEnvelope).length > 0) message["target"] = targetEnvelope;
|
|
5104
5132
|
if (payload !== void 0 && payload !== null) message["service_data"] = payload;
|
|
5133
|
+
if (target.returnResponse === true) message["return_response"] = true;
|
|
5105
5134
|
const result = await conn.sendMessagePromise(message);
|
|
5106
5135
|
if (result && typeof result === "object") return result;
|
|
5107
5136
|
return null;
|
|
@@ -7043,6 +7072,122 @@ function createHaNotificationOutputProvider(deps) {
|
|
|
7043
7072
|
};
|
|
7044
7073
|
}
|
|
7045
7074
|
//#endregion
|
|
7075
|
+
//#region src/ha-calendar-source.ts
|
|
7076
|
+
function isCalendarEntity(entityId) {
|
|
7077
|
+
return entityId.startsWith("calendar.");
|
|
7078
|
+
}
|
|
7079
|
+
function calendarName(state) {
|
|
7080
|
+
const friendly = state.attributes["friendly_name"];
|
|
7081
|
+
if (typeof friendly === "string" && friendly.trim().length > 0) return friendly.trim();
|
|
7082
|
+
return state.entity_id.replace(/^calendar\./, "").replace(/_/g, " ");
|
|
7083
|
+
}
|
|
7084
|
+
function parseHaDate(value) {
|
|
7085
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
7086
|
+
const ms = Date.parse(value.length === 10 ? `${value}T00:00:00` : value);
|
|
7087
|
+
return Number.isFinite(ms) ? ms : null;
|
|
7088
|
+
}
|
|
7089
|
+
function isRecord(x) {
|
|
7090
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
7091
|
+
}
|
|
7092
|
+
function asEntityState(raw) {
|
|
7093
|
+
const parsed = HaEntityStateSchema.safeParse(raw);
|
|
7094
|
+
return parsed.success ? parsed.data : null;
|
|
7095
|
+
}
|
|
7096
|
+
/**
|
|
7097
|
+
* HA's `calendar.list_events` response shape (return_response):
|
|
7098
|
+
* `{ response: { [entity_id]: { events: [...] } } }` or a flat
|
|
7099
|
+
* `{ events: [...] }` depending on target width.
|
|
7100
|
+
*/
|
|
7101
|
+
function extractEvents(result, entityId) {
|
|
7102
|
+
if (!isRecord(result)) return [];
|
|
7103
|
+
const response = result["response"];
|
|
7104
|
+
if (isRecord(response)) {
|
|
7105
|
+
const forEntity = response[entityId];
|
|
7106
|
+
if (isRecord(forEntity) && Array.isArray(forEntity["events"])) return forEntity["events"].filter(isRecord);
|
|
7107
|
+
for (const value of Object.values(response)) if (isRecord(value) && Array.isArray(value["events"])) return value["events"].filter(isRecord);
|
|
7108
|
+
}
|
|
7109
|
+
if (Array.isArray(result["events"])) return result["events"].filter(isRecord);
|
|
7110
|
+
return [];
|
|
7111
|
+
}
|
|
7112
|
+
function mapEvent(entityId, raw, index) {
|
|
7113
|
+
const startRaw = raw["start"];
|
|
7114
|
+
const endRaw = raw["end"];
|
|
7115
|
+
const startMs = parseHaDate(isRecord(startRaw) ? startRaw["dateTime"] ?? startRaw["date"] : raw["start"] ?? raw["start_date_time"] ?? raw["dtstart"]);
|
|
7116
|
+
const endMs = parseHaDate(isRecord(endRaw) ? endRaw["dateTime"] ?? endRaw["date"] : raw["end"] ?? raw["end_date_time"] ?? raw["dtend"]) ?? startMs;
|
|
7117
|
+
if (startMs === null || endMs === null) return null;
|
|
7118
|
+
const summary = raw["summary"] ?? raw["title"];
|
|
7119
|
+
const title = typeof summary === "string" ? summary : "(no title)";
|
|
7120
|
+
const uid = raw["uid"] ?? raw["id"];
|
|
7121
|
+
const id = typeof uid === "string" && uid.length > 0 ? `${entityId}:${uid}` : `${entityId}:${String(startMs)}:${String(index)}`;
|
|
7122
|
+
const description = raw["description"];
|
|
7123
|
+
const location = raw["location"];
|
|
7124
|
+
return {
|
|
7125
|
+
id,
|
|
7126
|
+
calendarId: entityId,
|
|
7127
|
+
title,
|
|
7128
|
+
startMs,
|
|
7129
|
+
endMs,
|
|
7130
|
+
...isRecord(startRaw) && typeof startRaw["date"] === "string" || typeof raw["start"] === "string" && raw["start"].length === 10 ? { allDay: true } : {},
|
|
7131
|
+
...typeof description === "string" && description.length > 0 ? { description } : {},
|
|
7132
|
+
...typeof location === "string" && location.length > 0 ? { location } : {}
|
|
7133
|
+
};
|
|
7134
|
+
}
|
|
7135
|
+
function createHaCalendarSource(deps) {
|
|
7136
|
+
return {
|
|
7137
|
+
listCalendars: async () => {
|
|
7138
|
+
const out = [];
|
|
7139
|
+
for (const { manager } of deps.brokers.listManagers()) {
|
|
7140
|
+
const states = manager.snapshotAllStates();
|
|
7141
|
+
for (const [entityId, raw] of Object.entries(states)) {
|
|
7142
|
+
if (!isCalendarEntity(entityId)) continue;
|
|
7143
|
+
const state = asEntityState(raw);
|
|
7144
|
+
if (state === null) continue;
|
|
7145
|
+
out.push({
|
|
7146
|
+
id: entityId,
|
|
7147
|
+
name: calendarName(state),
|
|
7148
|
+
source: "ha",
|
|
7149
|
+
enabled: state.state !== "unavailable"
|
|
7150
|
+
});
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
return out;
|
|
7154
|
+
},
|
|
7155
|
+
listEvents: async (input) => {
|
|
7156
|
+
const startIso = new Date(input.startMs).toISOString();
|
|
7157
|
+
const endIso = new Date(input.endMs).toISOString();
|
|
7158
|
+
const wanted = input.calendarIds !== void 0 && input.calendarIds.length > 0 ? new Set(input.calendarIds) : null;
|
|
7159
|
+
const out = [];
|
|
7160
|
+
for (const { brokerId, manager } of deps.brokers.listManagers()) {
|
|
7161
|
+
const states = manager.snapshotAllStates();
|
|
7162
|
+
const entityIds = Object.keys(states).filter((id) => isCalendarEntity(id) && (wanted === null || wanted.has(id)));
|
|
7163
|
+
for (const entityId of entityIds) try {
|
|
7164
|
+
const target = {
|
|
7165
|
+
domain: "calendar",
|
|
7166
|
+
service: "list_events",
|
|
7167
|
+
entityId,
|
|
7168
|
+
returnResponse: true
|
|
7169
|
+
};
|
|
7170
|
+
const rawEvents = extractEvents(await manager.publish(target, {
|
|
7171
|
+
start_date_time: startIso,
|
|
7172
|
+
end_date_time: endIso
|
|
7173
|
+
}), entityId);
|
|
7174
|
+
for (let i = 0; i < rawEvents.length; i += 1) {
|
|
7175
|
+
const mapped = mapEvent(entityId, rawEvents[i], i);
|
|
7176
|
+
if (mapped !== null) out.push(mapped);
|
|
7177
|
+
}
|
|
7178
|
+
} catch (err) {
|
|
7179
|
+
deps.logger.warn("HA calendar.list_events failed", { meta: {
|
|
7180
|
+
entityId,
|
|
7181
|
+
brokerId,
|
|
7182
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7183
|
+
} });
|
|
7184
|
+
}
|
|
7185
|
+
}
|
|
7186
|
+
return out.toSorted((a, b) => a.startMs - b.startMs);
|
|
7187
|
+
}
|
|
7188
|
+
};
|
|
7189
|
+
}
|
|
7190
|
+
//#endregion
|
|
7046
7191
|
//#region src/ha-reconcile-broker.ts
|
|
7047
7192
|
/**
|
|
7048
7193
|
* Run BOTH reconcilers for one broker off a SINGLE shared inventory fetch.
|
|
@@ -7371,11 +7516,14 @@ function buildFreshStandaloneSpec(entity) {
|
|
|
7371
7516
|
/** Stable key over the class-affecting fields — drives the drift decision.
|
|
7372
7517
|
* Excludes `type` (compared directly) but folds it in so a pure type change
|
|
7373
7518
|
* (same caps, different DeviceType — shouldn't happen but defensively safe)
|
|
7374
|
-
* still counts.
|
|
7519
|
+
* still counts. **Role is deliberately omitted**: after adopt, Role is
|
|
7520
|
+
* operator-owned (`setRole` in the UI). Including it here made every mapping
|
|
7521
|
+
* / capability drift re-push the derived role via `applyDerivedSpec` and
|
|
7522
|
+
* revert the Role select by itself.
|
|
7523
|
+
*/
|
|
7375
7524
|
function standaloneDerivedKey(s) {
|
|
7376
7525
|
return JSON.stringify({
|
|
7377
7526
|
type: s.type,
|
|
7378
|
-
role: s.role ?? null,
|
|
7379
7527
|
capabilities: [...s.capabilities],
|
|
7380
7528
|
unitOfMeasurement: s.unitOfMeasurement ?? null,
|
|
7381
7529
|
deviceClass: s.deviceClass ?? null,
|
|
@@ -7385,7 +7533,8 @@ function standaloneDerivedKey(s) {
|
|
|
7385
7533
|
});
|
|
7386
7534
|
}
|
|
7387
7535
|
/** The config patch pushed via `applyDerivedSpec` on drift — the class-affecting
|
|
7388
|
-
* fields only (mirrors the Container reconcile's `childConfigPatch`)
|
|
7536
|
+
* fields only (mirrors the Container reconcile's `childConfigPatch`), minus
|
|
7537
|
+
* `role` which is operator-owned after adopt and must not be clobbered. */
|
|
7389
7538
|
function standaloneConfigPatch(s) {
|
|
7390
7539
|
return {
|
|
7391
7540
|
capabilities: [...s.capabilities],
|
|
@@ -7393,22 +7542,21 @@ function standaloneConfigPatch(s) {
|
|
|
7393
7542
|
...s.deviceClass !== void 0 ? { deviceClass: s.deviceClass } : {},
|
|
7394
7543
|
...s.options !== void 0 ? { options: [...s.options] } : {},
|
|
7395
7544
|
...s.features !== void 0 ? { features: [...s.features] } : {},
|
|
7396
|
-
...s.role !== void 0 ? { role: s.role } : {},
|
|
7397
7545
|
...s.dateTimeFormat !== void 0 ? { dateTimeFormat: s.dateTimeFormat } : {}
|
|
7398
7546
|
};
|
|
7399
7547
|
}
|
|
7400
7548
|
/**
|
|
7401
7549
|
* Diff a persisted standalone entity device against the fresh HA mapping. Pure —
|
|
7402
|
-
* no I/O. On drift the caller pushes `{ type,
|
|
7550
|
+
* no I/O. On drift the caller pushes `{ type, config }` through
|
|
7403
7551
|
* `kernel.devices.applyDerivedSpec(deviceId, …)`; the captured cap-re-derive
|
|
7404
7552
|
* factory rebuilds the concrete per-domain class from the new `capabilities`.
|
|
7553
|
+
* Role is never part of the patch — it is operator-owned after adopt.
|
|
7405
7554
|
*/
|
|
7406
7555
|
function diffStandaloneEntity(persisted, fresh) {
|
|
7407
7556
|
if (standaloneDerivedKey(persisted) === standaloneDerivedKey(fresh)) return { changed: false };
|
|
7408
7557
|
return {
|
|
7409
7558
|
changed: true,
|
|
7410
7559
|
type: fresh.type,
|
|
7411
|
-
...fresh.role !== void 0 ? { role: fresh.role } : {},
|
|
7412
7560
|
config: standaloneConfigPatch(fresh)
|
|
7413
7561
|
};
|
|
7414
7562
|
}
|
|
@@ -7438,7 +7586,7 @@ function standaloneEntityId(device) {
|
|
|
7438
7586
|
* Pure re-sync planner. Given the broker's live registry `devices` and the
|
|
7439
7587
|
* FRESH entity inventory (by entityId), enumerate the broker's STANDALONE entity
|
|
7440
7588
|
* devices and produce one `applyDerivedSpec` action per device whose
|
|
7441
|
-
* capability/class
|
|
7589
|
+
* capability/class drifted. Role is never re-derived (operator-owned). No I/O.
|
|
7442
7590
|
*
|
|
7443
7591
|
* - Non-standalone / other-broker / Container / accessory-child devices →
|
|
7444
7592
|
* skipped (via {@link isStandaloneEntityDevice}).
|
|
@@ -7460,7 +7608,6 @@ function planStandaloneReconcile(devices, freshById, brokerId) {
|
|
|
7460
7608
|
actions.push({
|
|
7461
7609
|
deviceId: device.id,
|
|
7462
7610
|
...diff.type !== void 0 ? { type: diff.type } : {},
|
|
7463
|
-
...diff.role !== void 0 ? { role: diff.role } : {},
|
|
7464
7611
|
...diff.config !== void 0 ? { config: diff.config } : {}
|
|
7465
7612
|
});
|
|
7466
7613
|
}
|
|
@@ -7566,6 +7713,13 @@ var HaProviderAddon = class HaProviderAddon extends require_dist.BaseDeviceProvi
|
|
|
7566
7713
|
{
|
|
7567
7714
|
capability: require_dist.addonRoutesCapability,
|
|
7568
7715
|
provider: createHaIconRouteProvider()
|
|
7716
|
+
},
|
|
7717
|
+
{
|
|
7718
|
+
capability: require_dist.calendarSourceCapability,
|
|
7719
|
+
provider: createHaCalendarSource({
|
|
7720
|
+
brokers: this.requireRegistry(),
|
|
7721
|
+
logger: this.ctx.logger.child("calendar-source")
|
|
7722
|
+
})
|
|
7569
7723
|
}
|
|
7570
7724
|
];
|
|
7571
7725
|
}
|
package/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as connectivityCapability, At as string, B as floodCapability, C as buildAddonRouteProvider, Ct as _enum, D as carbonMonoxideCapability, Dt as number, E as calendarSourceCapability, Et as literal, F as deviceAdoptionCapability, G as lawnMowerControlCapability, H as humidifierCapability, J as motionCapability, K as lockControlCapability, L as enumSensorCapability, M as controlCapability, Mt as unknown, N as coverCapability, Nt as url, O as climateControlCapability, Ot as object, P as deviceAdminLinkCapability, Pt as EventCategory, Q as numericSensorCapability, R as eventEmitterCapability, S as brokerCapability, T as buttonCapability, Tt as discriminatedUnion, U as humiditySensorCapability, V as gasCapability, W as imageCapability, X as notificationOutputCapability, Y as normalizeUnit, Z as notifierCapability, _ as automationControlCapability, _t as DeviceRole, at as scriptRunnerCapability, b as binaryCapability, ct as tamperCapability, d as TargetSchema, dt as vacuumControlCapability, et as parseAdminBase, f as accessoriesCapability, ft as valveCapability, g as ambientLightSensorCapability, gt as DeviceFeature, h as alarmPanelCapability, ht as weatherCapability, it as pressureSensorCapability, j as contactCapability, jt as union, k as colorCapability, kt as record, lt as temperatureSensorCapability, m as airQualitySensorCapability, mt as waterHeaterCapability, n as BaseDevice, nt as prepareNotification, ot as smokeCapability, p as addonRoutesCapability, pt as vibrationCapability, q as mediaPlayerCapability, r as BaseDeviceProvider, rt as presenceCapability, s as EnumSensorDateTimeFormatSchema, st as switchCapability, tt as powerMeterCapability, ut as updateCapability, v as batteryCapability, vt as DeviceType, w as buildDeviceAdminUrl, wt as array, x as brightnessCapability, xt as createEvent, y as bestLocationMatch, yt as errMsg, z as fanControlCapability } from "./dist-CvzpCuzm.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
|
|
@@ -959,7 +959,9 @@ var HaBinarySensorDevice = class extends HaBaseChildDevice {
|
|
|
959
959
|
this.capName = capName;
|
|
960
960
|
this.registerCap(capName);
|
|
961
961
|
const stored = this.runtimeState.getCapState(capName);
|
|
962
|
-
|
|
962
|
+
const seededActive = storedActiveLevel(capName, stored) ?? false;
|
|
963
|
+
const seededChangedAt = storedChangedAt(capName, stored) ?? 0;
|
|
964
|
+
this.runtimeState.setCapState(this.capName, buildSlice(capName, seededActive, seededChangedAt, stored));
|
|
963
965
|
}
|
|
964
966
|
handleStatePush(state) {
|
|
965
967
|
if (state.state === "unavailable" || state.state === "unknown") return;
|
|
@@ -1051,6 +1053,32 @@ function priorLastDetectedAt(prior) {
|
|
|
1051
1053
|
const raw = prior?.["lastDetectedAt"];
|
|
1052
1054
|
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
1053
1055
|
}
|
|
1056
|
+
/** The boolean field this cap's slice uses as "active". */
|
|
1057
|
+
function activeFieldName(capName) {
|
|
1058
|
+
switch (capName) {
|
|
1059
|
+
case "contact": return "entryOpen";
|
|
1060
|
+
case "flood": return "flooded";
|
|
1061
|
+
case "tamper": return "tampered";
|
|
1062
|
+
case "connectivity": return "connected";
|
|
1063
|
+
case "binary": return "on";
|
|
1064
|
+
case "smoke":
|
|
1065
|
+
case "carbon-monoxide":
|
|
1066
|
+
case "gas":
|
|
1067
|
+
case "vibration":
|
|
1068
|
+
case "motion": return "detected";
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function storedActiveLevel(capName, stored) {
|
|
1072
|
+
if (stored === void 0) return void 0;
|
|
1073
|
+
const raw = stored[activeFieldName(capName)];
|
|
1074
|
+
return typeof raw === "boolean" ? raw : void 0;
|
|
1075
|
+
}
|
|
1076
|
+
function storedChangedAt(capName, stored) {
|
|
1077
|
+
if (stored === void 0) return void 0;
|
|
1078
|
+
if (capName === "motion") return priorLastDetectedAt(stored) ?? void 0;
|
|
1079
|
+
const raw = stored["lastChangedAt"];
|
|
1080
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : void 0;
|
|
1081
|
+
}
|
|
1054
1082
|
function buildSlice(capName, active, lastChangedAt, prior) {
|
|
1055
1083
|
switch (capName) {
|
|
1056
1084
|
case "contact": return {
|
|
@@ -5101,6 +5129,7 @@ var HaIntegrationManager = class HaIntegrationManager {
|
|
|
5101
5129
|
if (target.deviceId) targetEnvelope["device_id"] = target.deviceId;
|
|
5102
5130
|
if (Object.keys(targetEnvelope).length > 0) message["target"] = targetEnvelope;
|
|
5103
5131
|
if (payload !== void 0 && payload !== null) message["service_data"] = payload;
|
|
5132
|
+
if (target.returnResponse === true) message["return_response"] = true;
|
|
5104
5133
|
const result = await conn.sendMessagePromise(message);
|
|
5105
5134
|
if (result && typeof result === "object") return result;
|
|
5106
5135
|
return null;
|
|
@@ -7042,6 +7071,122 @@ function createHaNotificationOutputProvider(deps) {
|
|
|
7042
7071
|
};
|
|
7043
7072
|
}
|
|
7044
7073
|
//#endregion
|
|
7074
|
+
//#region src/ha-calendar-source.ts
|
|
7075
|
+
function isCalendarEntity(entityId) {
|
|
7076
|
+
return entityId.startsWith("calendar.");
|
|
7077
|
+
}
|
|
7078
|
+
function calendarName(state) {
|
|
7079
|
+
const friendly = state.attributes["friendly_name"];
|
|
7080
|
+
if (typeof friendly === "string" && friendly.trim().length > 0) return friendly.trim();
|
|
7081
|
+
return state.entity_id.replace(/^calendar\./, "").replace(/_/g, " ");
|
|
7082
|
+
}
|
|
7083
|
+
function parseHaDate(value) {
|
|
7084
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
7085
|
+
const ms = Date.parse(value.length === 10 ? `${value}T00:00:00` : value);
|
|
7086
|
+
return Number.isFinite(ms) ? ms : null;
|
|
7087
|
+
}
|
|
7088
|
+
function isRecord(x) {
|
|
7089
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
7090
|
+
}
|
|
7091
|
+
function asEntityState(raw) {
|
|
7092
|
+
const parsed = HaEntityStateSchema.safeParse(raw);
|
|
7093
|
+
return parsed.success ? parsed.data : null;
|
|
7094
|
+
}
|
|
7095
|
+
/**
|
|
7096
|
+
* HA's `calendar.list_events` response shape (return_response):
|
|
7097
|
+
* `{ response: { [entity_id]: { events: [...] } } }` or a flat
|
|
7098
|
+
* `{ events: [...] }` depending on target width.
|
|
7099
|
+
*/
|
|
7100
|
+
function extractEvents(result, entityId) {
|
|
7101
|
+
if (!isRecord(result)) return [];
|
|
7102
|
+
const response = result["response"];
|
|
7103
|
+
if (isRecord(response)) {
|
|
7104
|
+
const forEntity = response[entityId];
|
|
7105
|
+
if (isRecord(forEntity) && Array.isArray(forEntity["events"])) return forEntity["events"].filter(isRecord);
|
|
7106
|
+
for (const value of Object.values(response)) if (isRecord(value) && Array.isArray(value["events"])) return value["events"].filter(isRecord);
|
|
7107
|
+
}
|
|
7108
|
+
if (Array.isArray(result["events"])) return result["events"].filter(isRecord);
|
|
7109
|
+
return [];
|
|
7110
|
+
}
|
|
7111
|
+
function mapEvent(entityId, raw, index) {
|
|
7112
|
+
const startRaw = raw["start"];
|
|
7113
|
+
const endRaw = raw["end"];
|
|
7114
|
+
const startMs = parseHaDate(isRecord(startRaw) ? startRaw["dateTime"] ?? startRaw["date"] : raw["start"] ?? raw["start_date_time"] ?? raw["dtstart"]);
|
|
7115
|
+
const endMs = parseHaDate(isRecord(endRaw) ? endRaw["dateTime"] ?? endRaw["date"] : raw["end"] ?? raw["end_date_time"] ?? raw["dtend"]) ?? startMs;
|
|
7116
|
+
if (startMs === null || endMs === null) return null;
|
|
7117
|
+
const summary = raw["summary"] ?? raw["title"];
|
|
7118
|
+
const title = typeof summary === "string" ? summary : "(no title)";
|
|
7119
|
+
const uid = raw["uid"] ?? raw["id"];
|
|
7120
|
+
const id = typeof uid === "string" && uid.length > 0 ? `${entityId}:${uid}` : `${entityId}:${String(startMs)}:${String(index)}`;
|
|
7121
|
+
const description = raw["description"];
|
|
7122
|
+
const location = raw["location"];
|
|
7123
|
+
return {
|
|
7124
|
+
id,
|
|
7125
|
+
calendarId: entityId,
|
|
7126
|
+
title,
|
|
7127
|
+
startMs,
|
|
7128
|
+
endMs,
|
|
7129
|
+
...isRecord(startRaw) && typeof startRaw["date"] === "string" || typeof raw["start"] === "string" && raw["start"].length === 10 ? { allDay: true } : {},
|
|
7130
|
+
...typeof description === "string" && description.length > 0 ? { description } : {},
|
|
7131
|
+
...typeof location === "string" && location.length > 0 ? { location } : {}
|
|
7132
|
+
};
|
|
7133
|
+
}
|
|
7134
|
+
function createHaCalendarSource(deps) {
|
|
7135
|
+
return {
|
|
7136
|
+
listCalendars: async () => {
|
|
7137
|
+
const out = [];
|
|
7138
|
+
for (const { manager } of deps.brokers.listManagers()) {
|
|
7139
|
+
const states = manager.snapshotAllStates();
|
|
7140
|
+
for (const [entityId, raw] of Object.entries(states)) {
|
|
7141
|
+
if (!isCalendarEntity(entityId)) continue;
|
|
7142
|
+
const state = asEntityState(raw);
|
|
7143
|
+
if (state === null) continue;
|
|
7144
|
+
out.push({
|
|
7145
|
+
id: entityId,
|
|
7146
|
+
name: calendarName(state),
|
|
7147
|
+
source: "ha",
|
|
7148
|
+
enabled: state.state !== "unavailable"
|
|
7149
|
+
});
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
return out;
|
|
7153
|
+
},
|
|
7154
|
+
listEvents: async (input) => {
|
|
7155
|
+
const startIso = new Date(input.startMs).toISOString();
|
|
7156
|
+
const endIso = new Date(input.endMs).toISOString();
|
|
7157
|
+
const wanted = input.calendarIds !== void 0 && input.calendarIds.length > 0 ? new Set(input.calendarIds) : null;
|
|
7158
|
+
const out = [];
|
|
7159
|
+
for (const { brokerId, manager } of deps.brokers.listManagers()) {
|
|
7160
|
+
const states = manager.snapshotAllStates();
|
|
7161
|
+
const entityIds = Object.keys(states).filter((id) => isCalendarEntity(id) && (wanted === null || wanted.has(id)));
|
|
7162
|
+
for (const entityId of entityIds) try {
|
|
7163
|
+
const target = {
|
|
7164
|
+
domain: "calendar",
|
|
7165
|
+
service: "list_events",
|
|
7166
|
+
entityId,
|
|
7167
|
+
returnResponse: true
|
|
7168
|
+
};
|
|
7169
|
+
const rawEvents = extractEvents(await manager.publish(target, {
|
|
7170
|
+
start_date_time: startIso,
|
|
7171
|
+
end_date_time: endIso
|
|
7172
|
+
}), entityId);
|
|
7173
|
+
for (let i = 0; i < rawEvents.length; i += 1) {
|
|
7174
|
+
const mapped = mapEvent(entityId, rawEvents[i], i);
|
|
7175
|
+
if (mapped !== null) out.push(mapped);
|
|
7176
|
+
}
|
|
7177
|
+
} catch (err) {
|
|
7178
|
+
deps.logger.warn("HA calendar.list_events failed", { meta: {
|
|
7179
|
+
entityId,
|
|
7180
|
+
brokerId,
|
|
7181
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7182
|
+
} });
|
|
7183
|
+
}
|
|
7184
|
+
}
|
|
7185
|
+
return out.toSorted((a, b) => a.startMs - b.startMs);
|
|
7186
|
+
}
|
|
7187
|
+
};
|
|
7188
|
+
}
|
|
7189
|
+
//#endregion
|
|
7045
7190
|
//#region src/ha-reconcile-broker.ts
|
|
7046
7191
|
/**
|
|
7047
7192
|
* Run BOTH reconcilers for one broker off a SINGLE shared inventory fetch.
|
|
@@ -7370,11 +7515,14 @@ function buildFreshStandaloneSpec(entity) {
|
|
|
7370
7515
|
/** Stable key over the class-affecting fields — drives the drift decision.
|
|
7371
7516
|
* Excludes `type` (compared directly) but folds it in so a pure type change
|
|
7372
7517
|
* (same caps, different DeviceType — shouldn't happen but defensively safe)
|
|
7373
|
-
* still counts.
|
|
7518
|
+
* still counts. **Role is deliberately omitted**: after adopt, Role is
|
|
7519
|
+
* operator-owned (`setRole` in the UI). Including it here made every mapping
|
|
7520
|
+
* / capability drift re-push the derived role via `applyDerivedSpec` and
|
|
7521
|
+
* revert the Role select by itself.
|
|
7522
|
+
*/
|
|
7374
7523
|
function standaloneDerivedKey(s) {
|
|
7375
7524
|
return JSON.stringify({
|
|
7376
7525
|
type: s.type,
|
|
7377
|
-
role: s.role ?? null,
|
|
7378
7526
|
capabilities: [...s.capabilities],
|
|
7379
7527
|
unitOfMeasurement: s.unitOfMeasurement ?? null,
|
|
7380
7528
|
deviceClass: s.deviceClass ?? null,
|
|
@@ -7384,7 +7532,8 @@ function standaloneDerivedKey(s) {
|
|
|
7384
7532
|
});
|
|
7385
7533
|
}
|
|
7386
7534
|
/** The config patch pushed via `applyDerivedSpec` on drift — the class-affecting
|
|
7387
|
-
* fields only (mirrors the Container reconcile's `childConfigPatch`)
|
|
7535
|
+
* fields only (mirrors the Container reconcile's `childConfigPatch`), minus
|
|
7536
|
+
* `role` which is operator-owned after adopt and must not be clobbered. */
|
|
7388
7537
|
function standaloneConfigPatch(s) {
|
|
7389
7538
|
return {
|
|
7390
7539
|
capabilities: [...s.capabilities],
|
|
@@ -7392,22 +7541,21 @@ function standaloneConfigPatch(s) {
|
|
|
7392
7541
|
...s.deviceClass !== void 0 ? { deviceClass: s.deviceClass } : {},
|
|
7393
7542
|
...s.options !== void 0 ? { options: [...s.options] } : {},
|
|
7394
7543
|
...s.features !== void 0 ? { features: [...s.features] } : {},
|
|
7395
|
-
...s.role !== void 0 ? { role: s.role } : {},
|
|
7396
7544
|
...s.dateTimeFormat !== void 0 ? { dateTimeFormat: s.dateTimeFormat } : {}
|
|
7397
7545
|
};
|
|
7398
7546
|
}
|
|
7399
7547
|
/**
|
|
7400
7548
|
* Diff a persisted standalone entity device against the fresh HA mapping. Pure —
|
|
7401
|
-
* no I/O. On drift the caller pushes `{ type,
|
|
7549
|
+
* no I/O. On drift the caller pushes `{ type, config }` through
|
|
7402
7550
|
* `kernel.devices.applyDerivedSpec(deviceId, …)`; the captured cap-re-derive
|
|
7403
7551
|
* factory rebuilds the concrete per-domain class from the new `capabilities`.
|
|
7552
|
+
* Role is never part of the patch — it is operator-owned after adopt.
|
|
7404
7553
|
*/
|
|
7405
7554
|
function diffStandaloneEntity(persisted, fresh) {
|
|
7406
7555
|
if (standaloneDerivedKey(persisted) === standaloneDerivedKey(fresh)) return { changed: false };
|
|
7407
7556
|
return {
|
|
7408
7557
|
changed: true,
|
|
7409
7558
|
type: fresh.type,
|
|
7410
|
-
...fresh.role !== void 0 ? { role: fresh.role } : {},
|
|
7411
7559
|
config: standaloneConfigPatch(fresh)
|
|
7412
7560
|
};
|
|
7413
7561
|
}
|
|
@@ -7437,7 +7585,7 @@ function standaloneEntityId(device) {
|
|
|
7437
7585
|
* Pure re-sync planner. Given the broker's live registry `devices` and the
|
|
7438
7586
|
* FRESH entity inventory (by entityId), enumerate the broker's STANDALONE entity
|
|
7439
7587
|
* devices and produce one `applyDerivedSpec` action per device whose
|
|
7440
|
-
* capability/class
|
|
7588
|
+
* capability/class drifted. Role is never re-derived (operator-owned). No I/O.
|
|
7441
7589
|
*
|
|
7442
7590
|
* - Non-standalone / other-broker / Container / accessory-child devices →
|
|
7443
7591
|
* skipped (via {@link isStandaloneEntityDevice}).
|
|
@@ -7459,7 +7607,6 @@ function planStandaloneReconcile(devices, freshById, brokerId) {
|
|
|
7459
7607
|
actions.push({
|
|
7460
7608
|
deviceId: device.id,
|
|
7461
7609
|
...diff.type !== void 0 ? { type: diff.type } : {},
|
|
7462
|
-
...diff.role !== void 0 ? { role: diff.role } : {},
|
|
7463
7610
|
...diff.config !== void 0 ? { config: diff.config } : {}
|
|
7464
7611
|
});
|
|
7465
7612
|
}
|
|
@@ -7565,6 +7712,13 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
7565
7712
|
{
|
|
7566
7713
|
capability: addonRoutesCapability,
|
|
7567
7714
|
provider: createHaIconRouteProvider()
|
|
7715
|
+
},
|
|
7716
|
+
{
|
|
7717
|
+
capability: calendarSourceCapability,
|
|
7718
|
+
provider: createHaCalendarSource({
|
|
7719
|
+
brokers: this.requireRegistry(),
|
|
7720
|
+
logger: this.ctx.logger.child("calendar-source")
|
|
7721
|
+
})
|
|
7568
7722
|
}
|
|
7569
7723
|
];
|
|
7570
7724
|
}
|