@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("./dist-nbfaHTXG.js");
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
- this.runtimeState.setCapState(this.capName, buildSlice(capName, false, 0, stored));
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, role?, config }` through
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/role drifted. No I/O.
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 { $ as parseAdminBase, A as contactCapability, At as union, B as gasCapability, C as buildAddonRouteProvider, Ct as array, D as climateControlCapability, Dt as object, E as carbonMonoxideCapability, Et as number, G as lockControlCapability, H as humiditySensorCapability, I as enumSensorCapability, J as normalizeUnit, K as mediaPlayerCapability, L as eventEmitterCapability, M as coverCapability, Mt as url, N as deviceAdminLinkCapability, Nt as EventCategory, O as colorCapability, Ot as record, P as deviceAdoptionCapability, R as fanControlCapability, S as brokerCapability, St as _enum, T as buttonCapability, Tt as literal, U as imageCapability, V as humidifierCapability, W as lawnMowerControlCapability, X as notifierCapability, Y as notificationOutputCapability, Z as numericSensorCapability, _ as automationControlCapability, _t as DeviceType, at as smokeCapability, b as binaryCapability, bt as createEvent, ct as temperatureSensorCapability, d as TargetSchema, dt as valveCapability, et as powerMeterCapability, f as accessoriesCapability, ft as vibrationCapability, g as ambientLightSensorCapability, gt as DeviceRole, h as alarmPanelCapability, ht as DeviceFeature, it as scriptRunnerCapability, j as controlCapability, jt as unknown, k as connectivityCapability, kt as string, lt as updateCapability, m as airQualitySensorCapability, mt as weatherCapability, n as BaseDevice, nt as presenceCapability, ot as switchCapability, p as addonRoutesCapability, pt as waterHeaterCapability, q as motionCapability, r as BaseDeviceProvider, rt as pressureSensorCapability, s as EnumSensorDateTimeFormatSchema, st as tamperCapability, tt as prepareNotification, ut as vacuumControlCapability, v as batteryCapability, vt as errMsg, w as buildDeviceAdminUrl, wt as discriminatedUnion, x as brightnessCapability, y as bestLocationMatch, z as floodCapability } from "./dist-BITMr6s5.mjs";
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
- this.runtimeState.setCapState(this.capName, buildSlice(capName, false, 0, stored));
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, role?, config }` through
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/role drifted. No I/O.
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
  }