@camstack/addon-provider-homeassistant 1.2.653 → 1.2.654

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as humidifierCapability, $t as boolean, A as brightnessCapability, At as switchCapability, Bt as weatherCapability, C as alarmPanelCapability, Ct as runningFromLawnMower, D as bestLocationMatch, Dt as scriptRunnerCapability, E as batteryCapability, Et as runningStateCapability, F as calendarSourceCapability, Ft as updateCapability, G as deviceAdminLinkCapability, H as contactCapability, Ht as RecordSecretUnavailableError, I as carbonMonoxideCapability, It as vacuumControlCapability, J as enumSensorCapability, Jt as entryBody, K as deviceAdoptionCapability, Kt as defineEntryKind, L as climateControlCapability, Lt as valveCapability, M as buildAddonRouteProvider, Mt as temperatureSensorCapability, N as buildDeviceAdminUrl, Nt as tryConvertDelta, O as binaryCapability, P as buttonCapability, Pt as tryConvertUnit, Q as gasCapability, Qt as array, R as colorCapability, Rt as vibrationCapability, S as airQualitySensorCapability, T as automationControlCapability, Tt as runningFromVacuumState, U as controlCapability, Ut as baseUnitOf, W as coverCapability, Wt as createEvent, X as fanControlCapability, Y as eventEmitterCapability, Z as floodCapability, Zt as _enum, _t as pressureSensorCapability, a as BaseDevice, an as string, at as mediaPlayerCapability, b as accessoriesCapability, bt as quantityFromUnit, cn as url, ct as notificationOutputCapability, dn as DeviceType, en as discriminatedUnion, et as humiditySensorCapability, fn as errMsg, ft as parseAdminBase, gt as presenceCapability, ht as prepareNotification, in as record, it as lockControlCapability, j as brokerCapability, jt as tamperCapability, k as boardKeySourceCapability, kt as smokeCapability, ln as DeviceFeature, lt as notifierCapability, nn as number, nt as ingestReading, o as BaseDeviceProvider, on as union, ot as motionCapability, p as EnumSensorDateTimeFormatSchema, pn as EventCategory, pt as powerMeterCapability, qt as defineRecordKinds, rn as object, rt as lawnMowerControlCapability, sn as unknown, st as normalizeUnit, t as HA_BROKER_KIND, tn as literal, tt as imageCapability, un as DeviceRole, ut as numericSensorCapability, v as RUNNING_STATE_COLD, w as ambientLightSensorCapability, wt as runningFromMediaPlayer, x as addonRoutesCapability, xt as resolveAttachmentUrls, y as TargetSchema, yt as publicEndpointReaderFrom, z as connectivityCapability, zt as waterHeaterCapability } from "./ha-broker-source-CKdH9XHn.mjs";
1
+ import { $ as fanControlCapability, A as bestLocationMatch, At as scriptRunnerCapability, B as climateControlCapability, Bt as valveCapability, C as accessoriesCapability, Ct as quantityFromUnit, D as ambientLightSensorCapability, Dt as runningFromMediaPlayer, E as alarmPanelCapability, Et as runningFromLawnMower, F as buildAddonRouteProvider, Ft as temperatureSensorCapability, G as contactCapability, Gt as RecordSecretUnavailableError, H as connectivityCapability, Ht as waterHeaterCapability, I as buildDeviceAdminUrl, It as tryConvertDelta, J as deviceAdminLinkCapability, Jt as defineAddonMigrations, K as controlCapability, Kt as baseUnitOf, L as buttonCapability, Lt as tryConvertUnit, M as boardKeySourceCapability, Mt as smokeCapability, N as brightnessCapability, Nt as switchCapability, O as automationControlCapability, Ot as runningFromVacuumState, P as brokerCapability, Pt as tamperCapability, Q as eventEmitterCapability, R as calendarSourceCapability, Rt as updateCapability, S as TargetSchema, St as publicEndpointReaderFrom, T as airQualitySensorCapability, Ut as weatherCapability, V as colorCapability, Vt as vibrationCapability, Xt as defineRecordKinds, Y as deviceAdoptionCapability, Yt as defineEntryKind, Z as enumSensorCapability, Zt as entryBody, _t as preconditionOf, an as number, at as ingestReading, bt as pressureSensorCapability, c as BaseDevice, cn as string, ct as mediaPlayerCapability, dn as url, dt as notificationOutputCapability, en as _enum, et as floodCapability, fn as DeviceFeature, ft as notifierCapability, g as EnumSensorDateTimeFormatSchema, gn as EventCategory, gt as powerMeterCapability, hn as errMsg, ht as parseAdminBase, i as HA_BROKER_KIND, in as literal, it as imageCapability, j as binaryCapability, k as batteryCapability, kt as runningStateCapability, l as BaseDeviceProvider, ln as union, lt as motionCapability, mn as DeviceType, nn as boolean, nt as humidifierCapability, on as object, ot as lawnMowerControlCapability, pn as DeviceRole, pt as numericSensorCapability, q as coverCapability, qt as createEvent, rn as discriminatedUnion, rt as humiditySensorCapability, sn as record, st as lockControlCapability, t as haInstanceSettingsPortsOver, tn as array, tt as gasCapability, un as unknown, ut as normalizeUnit, vt as prepareNotification, w as addonRoutesCapability, wt as resolveAttachmentUrls, x as RUNNING_STATE_COLD, yt as presenceCapability, z as carbonMonoxideCapability, zt as vacuumControlCapability } from "./ha-instance-settings-g5pwb3Fi.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
@@ -2496,11 +2496,33 @@ var HaMediaPlayerDevice = class extends HaBaseChildDevice {
2496
2496
  registerRunningCap() {
2497
2497
  this.ctx.registerNativeCap(runningStateCapability, { getStatus: async () => this.runtimeState.getCapState("running-state") ?? RUNNING_STATE_COLD });
2498
2498
  }
2499
+ /** One service call per step, in order: HA has no "N steps" form. */
2500
+ async repeatHaService(service, steps) {
2501
+ for (let step = 0; step < steps; step += 1) await this.callHaService("media_player", service);
2502
+ }
2499
2503
  registerMediaPlayerCap() {
2500
2504
  this.ctx.registerNativeCap(mediaPlayerCapability, {
2501
2505
  getStatus: async () => {
2502
2506
  return this.runtimeState.getCapState("media-player") ?? COLD_START$10;
2503
2507
  },
2508
+ turnOn: async () => {
2509
+ await this.callHaService("media_player", "turn_on");
2510
+ },
2511
+ turnOff: async () => {
2512
+ await this.callHaService("media_player", "turn_off");
2513
+ },
2514
+ volumeUp: async ({ steps }) => {
2515
+ await this.repeatHaService("volume_up", steps);
2516
+ },
2517
+ volumeDown: async ({ steps }) => {
2518
+ await this.repeatHaService("volume_down", steps);
2519
+ },
2520
+ channelUp: async ({ steps }) => {
2521
+ await this.repeatHaService("media_next_track", steps);
2522
+ },
2523
+ channelDown: async ({ steps }) => {
2524
+ await this.repeatHaService("media_previous_track", steps);
2525
+ },
2504
2526
  play: async () => {
2505
2527
  await this.callHaService("media_player", "media_play");
2506
2528
  },
@@ -6439,6 +6461,8 @@ function mapMediaPlayer(attributes) {
6439
6461
  if ((sf & 8) !== 0) features.push(DeviceFeature.MediaPlayerMute);
6440
6462
  if ((sf & 16) !== 0) features.push(DeviceFeature.MediaPlayerPrevious);
6441
6463
  if ((sf & 32) !== 0) features.push(DeviceFeature.MediaPlayerNext);
6464
+ if ((sf & 384) === 384) features.push(DeviceFeature.MediaPlayerPower);
6465
+ if ((sf & 1024) !== 0) features.push(DeviceFeature.MediaPlayerVolumeStep);
6442
6466
  if ((sf & 512) !== 0) features.push(DeviceFeature.MediaPlayerPlayMedia);
6443
6467
  if ((sf & 2048) !== 0) features.push(DeviceFeature.MediaPlayerSelectSource);
6444
6468
  if ((sf & 4096) !== 0) features.push(DeviceFeature.MediaPlayerStop);
@@ -7932,6 +7956,127 @@ function createHaBoardKeyProvider(source) {
7932
7956
  };
7933
7957
  }
7934
7958
  //#endregion
7959
+ //#region src/migrations/calendar-ids-to-integration.ts
7960
+ /**
7961
+ * The HA calendars an instance imports are a fact of THAT instance (D985): each Home Assistant
7962
+ * integration carries its own `calendarIds` in its settings, and the addon-level `calendarIds`
7963
+ * setting (one set of `calendar.*` entity ids for every broker) goes away.
7964
+ *
7965
+ * Two migrations, in this order:
7966
+ *
7967
+ * 1. `calendar-ids-to-integration` (procedure) copies the shared set to EVERY integration that
7968
+ * has no `calendarIds` yet. An integration that already has one is never overwritten; one
7969
+ * deleted meanwhile is skipped, never recreated; a stored value that is not a list of strings
7970
+ * is an ambiguity and throws (nothing is guessed).
7971
+ * 2. `drop-shared-calendar-ids` (plan) deletes the shared setting after a read-back: every
7972
+ * integration must show its own set. When one holds a set that differs from the shared one,
7973
+ * the shared value is the loser and stays PARKED in place, never deleted.
7974
+ *
7975
+ * Both are idempotent.
7976
+ */
7977
+ var HA_PROVIDER_ID = "provider-homeassistant";
7978
+ /** The canonical settings table: one row per key, id `<addonId>.<key>`. */
7979
+ var ADDON_SETTINGS_TABLE = "addon-settings";
7980
+ var SHARED_CALENDAR_IDS_KEY = "calendarIds";
7981
+ async function readSharedSet(ports) {
7982
+ const answer = await ports.readStored(SHARED_CALENDAR_IDS_KEY);
7983
+ if (answer.kind === "unreadable") throw new Error(`provider-homeassistant: the shared calendarIds cannot be read (${answer.reason})`);
7984
+ if (answer.kind === "absent") return null;
7985
+ const value = answer.value;
7986
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new Error("provider-homeassistant: the shared calendarIds is not a list of entity ids — nothing copied");
7987
+ return value;
7988
+ }
7989
+ async function copyCalendarIdsToIntegrations(ports) {
7990
+ const shared = await readSharedSet(ports);
7991
+ if (shared === null) return {
7992
+ outcome: "already-done",
7993
+ summary: { reason: "no-shared-set" }
7994
+ };
7995
+ let copied = 0;
7996
+ let kept = 0;
7997
+ let gone = 0;
7998
+ for (const instance of await ports.listInstances()) {
7999
+ if ("calendarIds" in instance.settings) {
8000
+ kept += 1;
8001
+ continue;
8002
+ }
8003
+ if (await ports.patchInstanceSettings(instance.integrationId, { calendarIds: [...shared] })) copied += 1;
8004
+ else gone += 1;
8005
+ }
8006
+ return {
8007
+ outcome: copied === 0 ? "already-done" : "done",
8008
+ summary: {
8009
+ copied,
8010
+ kept,
8011
+ gone
8012
+ }
8013
+ };
8014
+ }
8015
+ var asIds = (value) => Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
8016
+ var sameSet = (a, b) => {
8017
+ const left = new Set(a);
8018
+ const right = new Set(b);
8019
+ return left.size === right.size && [...left].every((id) => right.has(id));
8020
+ };
8021
+ async function planDropSharedCalendarIds(ports) {
8022
+ const shared = await readSharedSet(ports);
8023
+ if (shared === null) return { edits: [] };
8024
+ const instances = await ports.listInstances();
8025
+ if (instances.length === 0) return { edits: [] };
8026
+ const own = instances.map((instance) => ({
8027
+ integrationId: instance.integrationId,
8028
+ ids: asIds(instance.settings[SHARED_CALENDAR_IDS_KEY])
8029
+ }));
8030
+ const missing = own.filter((entry) => entry.ids === null);
8031
+ if (missing.length > 0) throw new Error(`provider-homeassistant: the shared calendarIds is not dropped, ${missing.map((m) => m.integrationId).join(", ")} has no set of its own`);
8032
+ if (!own.every((entry) => sameSet(entry.ids ?? [], shared))) return { edits: [] };
8033
+ const rowId = `${HA_PROVIDER_ID}.${SHARED_CALENDAR_IDS_KEY}`;
8034
+ return { edits: [{
8035
+ op: "deleteKeys",
8036
+ scope: ADDON_SETTINGS_TABLE,
8037
+ keys: [rowId],
8038
+ precondition: preconditionOf({ [rowId]: {
8039
+ addonId: HA_PROVIDER_ID,
8040
+ key: SHARED_CALENDAR_IDS_KEY,
8041
+ value: shared
8042
+ } })
8043
+ }] };
8044
+ }
8045
+ //#endregion
8046
+ //#region src/migrations/index.ts
8047
+ /** The `provider-homeassistant` catalogue (D987): offered to the engine when the addon initialises. */
8048
+ var ADDON_MIGRATIONS = [{
8049
+ meta: {
8050
+ id: "provider-homeassistant/2026-11-03-calendar-ids-to-integration",
8051
+ owner: "provider-homeassistant",
8052
+ level: "addon-state",
8053
+ seq: 1,
8054
+ dependsOn: [],
8055
+ store: "settings",
8056
+ scope: "cluster",
8057
+ onFailure: "tolerate",
8058
+ bodyKind: "procedure",
8059
+ description: "Copy the shared calendar set to the settings of every Home Assistant integration that has none",
8060
+ fixtureMark: "the broker link stored beside the calendars of an integration"
8061
+ },
8062
+ procedure: copyCalendarIdsToIntegrations
8063
+ }, {
8064
+ meta: {
8065
+ id: "provider-homeassistant/2026-11-03-drop-shared-calendar-ids",
8066
+ owner: "provider-homeassistant",
8067
+ level: "addon-state",
8068
+ seq: 2,
8069
+ dependsOn: ["provider-homeassistant/2026-11-03-calendar-ids-to-integration"],
8070
+ store: "settings",
8071
+ scope: "cluster",
8072
+ onFailure: "tolerate",
8073
+ bodyKind: "plan",
8074
+ description: "Delete the shared calendar set once every integration shows its own",
8075
+ fixtureMark: "the shared set parked when an integration holds a different one"
8076
+ },
8077
+ plan: planDropSharedCalendarIds
8078
+ }];
8079
+ //#endregion
7935
8080
  //#region src/ha-calendar-source.ts
7936
8081
  function isCalendarEntity(entityId) {
7937
8082
  return entityId.startsWith("calendar.");
@@ -8042,9 +8187,9 @@ function isSelected(entityId, selected) {
8042
8187
  function createHaCalendarSource(deps) {
8043
8188
  return {
8044
8189
  listCalendars: async () => {
8045
- const selected = deps.selectedCalendarIds();
8046
8190
  const out = [];
8047
- for (const { manager } of deps.brokers.listManagers()) {
8191
+ for (const { brokerId, manager } of deps.brokers.listManagers()) {
8192
+ const selected = deps.selectedCalendarIds(brokerId);
8048
8193
  const states = manager.snapshotAllStates();
8049
8194
  for (const [entityId, raw] of Object.entries(states)) {
8050
8195
  if (!isCalendarEntity(entityId)) continue;
@@ -8064,10 +8209,10 @@ function createHaCalendarSource(deps) {
8064
8209
  listEvents: async (input) => {
8065
8210
  const startIso = new Date(input.startMs).toISOString();
8066
8211
  const endIso = new Date(input.endMs).toISOString();
8067
- const selected = deps.selectedCalendarIds();
8068
8212
  const wanted = input.calendarIds !== void 0 && input.calendarIds.length > 0 ? new Set(input.calendarIds) : null;
8069
8213
  const out = [];
8070
8214
  for (const { brokerId, manager } of deps.brokers.listManagers()) {
8215
+ const selected = deps.selectedCalendarIds(brokerId);
8071
8216
  const states = manager.snapshotAllStates();
8072
8217
  const entityIds = Object.keys(states).filter((id) => {
8073
8218
  if (!isCalendarEntity(id)) return false;
@@ -8091,10 +8236,11 @@ function createHaCalendarSource(deps) {
8091
8236
  };
8092
8237
  }
8093
8238
  /** Discover every HA calendar entity (ignores the import filter) — for settings catalog. */
8094
- function listHaCalendarCatalog(brokers) {
8239
+ function listHaCalendarCatalog(brokers, onlyBrokerId) {
8095
8240
  const out = [];
8096
8241
  const seen = /* @__PURE__ */ new Set();
8097
- for (const { manager } of brokers.listManagers()) {
8242
+ for (const { brokerId, manager } of brokers.listManagers()) {
8243
+ if (onlyBrokerId !== void 0 && brokerId !== onlyBrokerId) continue;
8098
8244
  const states = manager.snapshotAllStates();
8099
8245
  for (const [entityId, raw] of Object.entries(states)) {
8100
8246
  if (!isCalendarEntity(entityId) || seen.has(entityId)) continue;
@@ -8628,6 +8774,10 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8628
8774
  this.reconcileAllConnectedBrokers();
8629
8775
  }, HaProviderAddon.RESYNC_INTERVAL_MS);
8630
8776
  this.ctx.logger.info("ha provider initialised", { meta: { brokerCount: this.activeBrokers.length } });
8777
+ await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, {
8778
+ readStored: (key) => this.readStoredGlobalKey(key),
8779
+ ...haInstanceSettingsPortsOver(this.ctx.api.integrations)
8780
+ }));
8631
8781
  await this.reconcileIntegrationsToBrokers();
8632
8782
  this.subscribeIntegrationLifecycle();
8633
8783
  return [
@@ -8656,7 +8806,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8656
8806
  provider: createHaCalendarSource({
8657
8807
  brokers: this.requireRegistry(),
8658
8808
  logger: this.ctx.logger.child("calendar-source"),
8659
- selectedCalendarIds: () => new Set(this.config.calendarIds ?? [])
8809
+ selectedCalendarIds: (brokerId) => this.calendarIdsOf(brokerId)
8660
8810
  })
8661
8811
  }
8662
8812
  ];
@@ -8754,6 +8904,20 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8754
8904
  *
8755
8905
  * Idempotent — runs on boot + on every integration.* event.
8756
8906
  */
8907
+ /**
8908
+ * Calendars to import, per broker (D985): the integration's own selection, filled by the
8909
+ * reconcile that already reads each integration's settings. A broker whose integration has no
8910
+ * selection yet (the move has not run) uses the legacy shared set, so a boot mid-move never
8911
+ * changes what is imported.
8912
+ */
8913
+ calendarIdsByBroker = /* @__PURE__ */ new Map();
8914
+ noteInstanceCalendars(brokerId, raw) {
8915
+ if (Array.isArray(raw) && raw.every((id) => typeof id === "string")) this.calendarIdsByBroker.set(brokerId, new Set(raw));
8916
+ else this.calendarIdsByBroker.delete(brokerId);
8917
+ }
8918
+ calendarIdsOf(brokerId) {
8919
+ return this.calendarIdsByBroker.get(brokerId) ?? new Set(this.config.calendarIds ?? []);
8920
+ }
8757
8921
  async reconcileIntegrationsToBrokers() {
8758
8922
  const api = this.ctx.api;
8759
8923
  if (!api.integrations?.list?.query || !api.integrations.getSettings?.query) return;
@@ -8765,6 +8929,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8765
8929
  const settings = await api.integrations.getSettings.query({ id: integration.id });
8766
8930
  const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : null;
8767
8931
  if (!brokerId) continue;
8932
+ this.noteInstanceCalendars(brokerId, settings["calendarIds"]);
8768
8933
  if (!this.activeBrokers.find((b) => b.id === brokerId)) {
8769
8934
  this.ctx.logger.warn("integration→broker: linked broker not found", { meta: {
8770
8935
  integrationId: integration.id,
@@ -8804,23 +8969,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8804
8969
  eb.subscribe({ category: "integration.disabled" }, handler);
8805
8970
  eb.subscribe({ category: "integration.deleted" }, handler);
8806
8971
  }
8807
- globalSettingsSchema(cap) {
8808
- if (cap === "calendar-source") return this.schema({ sections: [{
8809
- id: "ha-calendars-import",
8810
- title: "Calendars to import",
8811
- titleKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.title",
8812
- columns: 1,
8813
- fields: [{
8814
- type: "multiselect",
8815
- key: "calendarIds",
8816
- label: "Calendars",
8817
- labelKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.calendarIds.label",
8818
- options: [],
8819
- optionsFrom: "_calendarCatalog",
8820
- description: "Discovered from connected HA brokers. Select at least one to import; leave empty to import none.",
8821
- descriptionKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.calendarIds.description"
8822
- }]
8823
- }] });
8972
+ globalSettingsSchema(_cap) {
8824
8973
  return this.schema({ sections: [{
8825
8974
  id: "ha-broker",
8826
8975
  title: "Home Assistant broker",
@@ -8835,14 +8984,39 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
8835
8984
  }]
8836
8985
  }] });
8837
8986
  }
8838
- async getGlobalSettings(overlay, cap, nodeId) {
8839
- if (cap !== "calendar-source") return super.getGlobalSettings(overlay, cap, nodeId);
8840
- const catalog = this.registry ? listHaCalendarCatalog(this.registry) : [];
8841
- return super.getGlobalSettings({
8842
- ...overlay,
8843
- calendarIds: this.config.calendarIds ?? [],
8844
- ...catalog.length > 0 ? { _calendarCatalog: catalog } : {}
8845
- }, cap, nodeId);
8987
+ /**
8988
+ * The calendars of ONE Home Assistant instance (D985): picked on its integration, offered from
8989
+ * that broker's own `calendar.*` entities.
8990
+ */
8991
+ async integrationSettingsSchema(instance) {
8992
+ if (instance.integrationId === null || !this.registry) return null;
8993
+ let brokerId;
8994
+ try {
8995
+ brokerId = (await this.ctx.api.integrations.getSettings.query({ id: instance.integrationId }))["brokerId"];
8996
+ } catch (err) {
8997
+ this.ctx.logger.warn("HA integration form: instance lookup failed — broker form shown", { meta: {
8998
+ integrationId: instance.integrationId,
8999
+ error: errMsg(err)
9000
+ } });
9001
+ return null;
9002
+ }
9003
+ if (typeof brokerId !== "string") return null;
9004
+ const options = listHaCalendarCatalog(this.registry, brokerId);
9005
+ return this.schema({ sections: [{
9006
+ id: "ha-calendars-import",
9007
+ title: "Calendars to import",
9008
+ titleKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.title",
9009
+ columns: 1,
9010
+ fields: [{
9011
+ type: "multiselect",
9012
+ key: "calendarIds",
9013
+ label: "Calendars",
9014
+ labelKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.calendarIds.label",
9015
+ options,
9016
+ description: "Discovered from this Home Assistant instance. Select at least one to import; leave empty to import none.",
9017
+ descriptionKey: "provider-homeassistant:globalSettings.s_ha-calendars-import.calendarIds.description"
9018
+ }]
9019
+ }] });
8846
9020
  }
8847
9021
  async onShutdown() {
8848
9022
  if (this.resyncTimer) {