@camstack/addon-provider-homeassistant 1.2.656 → 1.2.658
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/CHANGELOG.md +13 -0
- package/dist/addon.js +3 -3
- package/dist/addon.mjs +3 -3
- package/dist/ha-export/ha-export.addon.js +35 -14
- package/dist/ha-export/ha-export.addon.mjs +35 -14
- package/dist/{ha-instance-settings-Dm0tU8iv.js → ha-instance-settings-CRvQ5rH6.js} +31 -5
- package/dist/{ha-instance-settings-EQ5Ey3Q-.mjs → ha-instance-settings-qhCNJbbn.mjs} +31 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.658] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Fixed the Home Assistant export migration reading every switch as off after the instances moved to typed records; it now reads the legacy integration row and refuses when that row is unreadable.
|
|
15
|
+
- Image links sent to Home Assistant now prefer the public endpoint (falling back to the internal address when none is set), so notifications show images away from home.
|
|
16
|
+
|
|
17
|
+
## [1.2.657] - 2026-11-04
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Home Assistant broker migrations no longer read as 'skipped-newer' (and no false 'Database is newer' alert) after the addon boots
|
|
22
|
+
|
|
10
23
|
## [1.2.656] - 2026-11-04
|
|
11
24
|
|
|
12
25
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_ha_instance_settings = require("./ha-instance-settings-
|
|
2
|
+
const require_ha_instance_settings = require("./ha-instance-settings-CRvQ5rH6.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
|
|
@@ -8966,7 +8966,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8966
8966
|
onChanged: (id) => this.syncBrokerRuntime(id),
|
|
8967
8967
|
probe: (settings) => this.requireRegistry().testSettings(settings)
|
|
8968
8968
|
});
|
|
8969
|
-
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS
|
|
8969
|
+
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS, this.flattenPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
|
|
8970
8970
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
8971
8971
|
this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
|
|
8972
8972
|
this.registry.setOnBrokerConnected((brokerId) => {
|
|
@@ -8983,7 +8983,7 @@ var HaProviderAddon = class HaProviderAddon extends require_ha_instance_settings
|
|
|
8983
8983
|
this.reconcileAllConnectedBrokers();
|
|
8984
8984
|
}, HaProviderAddon.RESYNC_INTERVAL_MS);
|
|
8985
8985
|
this.ctx.logger.info("ha provider initialised", { meta: { brokerCount: this.activeBrokers.length } });
|
|
8986
|
-
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS.
|
|
8986
|
+
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS, this.flattenPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
|
|
8987
8987
|
await this.reconcileIntegrationsToBrokers();
|
|
8988
8988
|
this.subscribeIntegrationLifecycle();
|
|
8989
8989
|
return [
|
package/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as fanControlCapability, $t as createEvent, A as bestLocationMatch, At as runningFromMediaPlayer, B as climateControlCapability, Bt as tryConvertDelta, C as accessoriesCapability, Ct as pressureSensorCapability, D as ambientLightSensorCapability, Dt as resolveAttachmentUrls, E as alarmPanelCapability, Et as quantityFromUnit, F as buildAddonRouteProvider, Ft as smokeCapability, G as contactCapability, Gt as vibrationCapability, H as connectivityCapability, Ht as updateCapability, I as buildDeviceAdminUrl, J as deviceAdminLinkCapability, K as controlCapability, Kt as waterHeaterCapability, L as buttonCapability, Lt as switchCapability, M as boardKeySourceCapability, Mt as runningStateCapability, N as brightnessCapability, Nt as scriptRunnerCapability, O as automationControlCapability, P as brokerCapability, Q as eventEmitterCapability, Qt as baseUnitOf, R as calendarSourceCapability, Rt as tamperCapability, S as TargetSchema, Sn as EventCategory, St as presenceCapability, T as airQualitySensorCapability, Tt as publicEndpointReaderFrom, Ut as vacuumControlCapability, V as colorCapability, Vt as tryConvertUnit, Wt as valveCapability, Xt as RecordSecretUnavailableError, Y as deviceAdoptionCapability, Z as enumSensorCapability, _n as url, at as humiditySensorCapability, b as RUNNING_STATE_COLD, bn as DeviceType, bt as preconditionOf, cn as boolean, ct as integrationInstancesCapability, dn as number, dt as mediaPlayerCapability, en as defineAddonMigrations, et as flattenNestedRecords, fn as object, ft as motionCapability, gn as unknown, gt as numericSensorCapability, hn as union, ht as notifierCapability, it as humidifierCapability, j as binaryCapability, jt as runningFromVacuumState, k as batteryCapability, kt as runningFromLawnMower, ln as discriminatedUnion, lt as lawnMowerControlCapability, m as EnumSensorDateTimeFormatSchema, mn as string, mt as notificationOutputCapability, n as HA_BROKER_KIND, nn as defineRecordKinds, nt as floodCapability, o as BaseDevice, on as _enum, ot as imageCapability, pn as record, pt as normalizeUnit, q as coverCapability, qt as weatherCapability, rn as entryBody, rt as gasCapability, s as BaseDeviceProvider, sn as array, st as ingestReading, t as haInstanceSettingsPortsOver, tn as defineEntryKind, tt as flattenPortsOver, un as literal, ut as lockControlCapability, vn as DeviceFeature, vt as parseAdminBase, w as addonRoutesCapability, x as RecordIntegrationInstances, xn as errMsg, xt as prepareNotification, yn as DeviceRole, yt as powerMeterCapability, z as carbonMonoxideCapability, zt as temperatureSensorCapability } from "./ha-instance-settings-
|
|
1
|
+
import { $ as fanControlCapability, $t as createEvent, A as bestLocationMatch, At as runningFromMediaPlayer, B as climateControlCapability, Bt as tryConvertDelta, C as accessoriesCapability, Ct as pressureSensorCapability, D as ambientLightSensorCapability, Dt as resolveAttachmentUrls, E as alarmPanelCapability, Et as quantityFromUnit, F as buildAddonRouteProvider, Ft as smokeCapability, G as contactCapability, Gt as vibrationCapability, H as connectivityCapability, Ht as updateCapability, I as buildDeviceAdminUrl, J as deviceAdminLinkCapability, K as controlCapability, Kt as waterHeaterCapability, L as buttonCapability, Lt as switchCapability, M as boardKeySourceCapability, Mt as runningStateCapability, N as brightnessCapability, Nt as scriptRunnerCapability, O as automationControlCapability, P as brokerCapability, Q as eventEmitterCapability, Qt as baseUnitOf, R as calendarSourceCapability, Rt as tamperCapability, S as TargetSchema, Sn as EventCategory, St as presenceCapability, T as airQualitySensorCapability, Tt as publicEndpointReaderFrom, Ut as vacuumControlCapability, V as colorCapability, Vt as tryConvertUnit, Wt as valveCapability, Xt as RecordSecretUnavailableError, Y as deviceAdoptionCapability, Z as enumSensorCapability, _n as url, at as humiditySensorCapability, b as RUNNING_STATE_COLD, bn as DeviceType, bt as preconditionOf, cn as boolean, ct as integrationInstancesCapability, dn as number, dt as mediaPlayerCapability, en as defineAddonMigrations, et as flattenNestedRecords, fn as object, ft as motionCapability, gn as unknown, gt as numericSensorCapability, hn as union, ht as notifierCapability, it as humidifierCapability, j as binaryCapability, jt as runningFromVacuumState, k as batteryCapability, kt as runningFromLawnMower, ln as discriminatedUnion, lt as lawnMowerControlCapability, m as EnumSensorDateTimeFormatSchema, mn as string, mt as notificationOutputCapability, n as HA_BROKER_KIND, nn as defineRecordKinds, nt as floodCapability, o as BaseDevice, on as _enum, ot as imageCapability, pn as record, pt as normalizeUnit, q as coverCapability, qt as weatherCapability, rn as entryBody, rt as gasCapability, s as BaseDeviceProvider, sn as array, st as ingestReading, t as haInstanceSettingsPortsOver, tn as defineEntryKind, tt as flattenPortsOver, un as literal, ut as lockControlCapability, vn as DeviceFeature, vt as parseAdminBase, w as addonRoutesCapability, x as RecordIntegrationInstances, xn as errMsg, xt as prepareNotification, yn as DeviceRole, yt as powerMeterCapability, z as carbonMonoxideCapability, zt as temperatureSensorCapability } from "./ha-instance-settings-qhCNJbbn.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
|
|
@@ -8945,7 +8945,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8945
8945
|
onChanged: (id) => this.syncBrokerRuntime(id),
|
|
8946
8946
|
probe: (settings) => this.requireRegistry().testSettings(settings)
|
|
8947
8947
|
});
|
|
8948
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS
|
|
8948
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.flattenPorts(records)), { only: [FLATTEN_BROKER_RECORDS_ID] });
|
|
8949
8949
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
8950
8950
|
this.registry = new HaBrokerRegistry(this.ctx.logger, this.ctx.eventBus);
|
|
8951
8951
|
this.registry.setOnBrokerConnected((brokerId) => {
|
|
@@ -8962,7 +8962,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
8962
8962
|
this.reconcileAllConnectedBrokers();
|
|
8963
8963
|
}, HaProviderAddon.RESYNC_INTERVAL_MS);
|
|
8964
8964
|
this.ctx.logger.info("ha provider initialised", { meta: { brokerCount: this.activeBrokers.length } });
|
|
8965
|
-
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS.
|
|
8965
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.flattenPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => id !== FLATTEN_BROKER_RECORDS_ID) });
|
|
8966
8966
|
await this.reconcileIntegrationsToBrokers();
|
|
8967
8967
|
this.subscribeIntegrationLifecycle();
|
|
8968
8968
|
return [
|
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
enumerable: true
|
|
25
25
|
}) : target, mod));
|
|
26
26
|
//#endregion
|
|
27
|
-
const require_ha_instance_settings = require("../ha-instance-settings-
|
|
27
|
+
const require_ha_instance_settings = require("../ha-instance-settings-CRvQ5rH6.js");
|
|
28
28
|
let node_crypto = require("node:crypto");
|
|
29
29
|
require("node:zlib");
|
|
30
30
|
let node_fs = require("node:fs");
|
|
@@ -5090,13 +5090,21 @@ function verifyExpiringUrl(input) {
|
|
|
5090
5090
|
/** The hub address roles a Home Assistant media link may be minted on. */
|
|
5091
5091
|
var HA_MEDIA_ROLES = ["internal", "publicEndpoint"];
|
|
5092
5092
|
/**
|
|
5093
|
-
* The base a media link is minted on: the hub's role (D734)
|
|
5094
|
-
*
|
|
5095
|
-
* when
|
|
5096
|
-
*
|
|
5093
|
+
* The base a media link is minted on: the hub's role (D734) the broker chose. Links reach
|
|
5094
|
+
* Home Assistant notifications on phones away from home, so a new broker chooses `publicEndpoint`;
|
|
5095
|
+
* when that resolves to nothing the `internal` address answers instead (a LAN Home Assistant still
|
|
5096
|
+
* gets an image). A broker that chose `internal` never falls back to public. Ranked by
|
|
5097
|
+
* `local-network`, never loopback (`null` when nothing resolves): a link Home Assistant cannot
|
|
5098
|
+
* open is worse than no image.
|
|
5097
5099
|
*/
|
|
5098
|
-
function haMediaBaseUrl(book, role = "
|
|
5099
|
-
|
|
5100
|
+
function haMediaBaseUrl(book, role = "publicEndpoint") {
|
|
5101
|
+
const chosen = book.roles[role].resolved;
|
|
5102
|
+
if (chosen !== null || role !== "publicEndpoint") return chosen;
|
|
5103
|
+
return book.roles.internal.resolved;
|
|
5104
|
+
}
|
|
5105
|
+
/** True when `haMediaBaseUrl` answered with the internal address because the public endpoint was chosen but unset. */
|
|
5106
|
+
function usedInternalFallback(book, role) {
|
|
5107
|
+
return role === "publicEndpoint" && book.roles.publicEndpoint.resolved === null && book.roles.internal.resolved !== null;
|
|
5100
5108
|
}
|
|
5101
5109
|
/** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
|
|
5102
5110
|
function mediaBasesByBroker(brokerIds, book, roleOf) {
|
|
@@ -5279,6 +5287,8 @@ function integrationByBroker(rows, warn) {
|
|
|
5279
5287
|
/** Every broker the old shape has something for, with the value the old reader would have used. */
|
|
5280
5288
|
function legacyLinks(stored, rows, warn) {
|
|
5281
5289
|
const membership = membershipOf$1(stored[MEMBERSHIP_KEY]);
|
|
5290
|
+
const unreadable = rows.find((row) => Object.keys(row.settings).length === 0);
|
|
5291
|
+
if (unreadable !== void 0 && membership.size > 0) throw new Error(`the settings of integration ${unreadable.integrationId} read as empty; the export switch cannot be carried from an unreadable source, nothing was written`);
|
|
5282
5292
|
const integrations = integrationByBroker(rows, warn);
|
|
5283
5293
|
const brokers = new Set(membership.keys());
|
|
5284
5294
|
for (const key of Object.keys(stored)) for (const prefix of [EXPORT_PREFIX, ROLE_PREFIX]) if (key.startsWith(prefix) && key.length > prefix.length) brokers.add(key.slice(prefix.length));
|
|
@@ -5472,12 +5482,12 @@ var haExportLinksKind = require_ha_instance_settings.defineEntryKind(HA_EXPORT_L
|
|
|
5472
5482
|
schema: HaExportLinksBodySchema,
|
|
5473
5483
|
writeTrigger: "an operator switches the export of a Home Assistant instance, picks its image-link address, or exports a device to it"
|
|
5474
5484
|
});
|
|
5475
|
-
/** A link the operator has not touched: off, on the
|
|
5485
|
+
/** A link the operator has not touched: off, on the public endpoint, no device. */
|
|
5476
5486
|
function defaultLinks(name) {
|
|
5477
5487
|
return {
|
|
5478
5488
|
name,
|
|
5479
5489
|
exportTo: false,
|
|
5480
|
-
mediaRole: "
|
|
5490
|
+
mediaRole: "publicEndpoint",
|
|
5481
5491
|
devices: []
|
|
5482
5492
|
};
|
|
5483
5493
|
}
|
|
@@ -5521,7 +5531,7 @@ var ExportLinks = class ExportLinks {
|
|
|
5521
5531
|
return this.rows.get(brokerId)?.exportTo === true;
|
|
5522
5532
|
}
|
|
5523
5533
|
mediaRole(brokerId) {
|
|
5524
|
-
return this.rows.get(brokerId)?.mediaRole ?? "
|
|
5534
|
+
return this.rows.get(brokerId)?.mediaRole ?? "publicEndpoint";
|
|
5525
5535
|
}
|
|
5526
5536
|
/** brokerId → exported device ids, for the brokers that export at least one. */
|
|
5527
5537
|
membership() {
|
|
@@ -8247,6 +8257,7 @@ var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
|
|
|
8247
8257
|
}
|
|
8248
8258
|
/** deviceId:cap:field already reported. Bounded by devices × mapped caps. */
|
|
8249
8259
|
warnedCapFields = /* @__PURE__ */ new Set();
|
|
8260
|
+
warnedMediaFallback = /* @__PURE__ */ new Set();
|
|
8250
8261
|
warnOnce(deviceId, capName, field, line) {
|
|
8251
8262
|
const key = `${deviceId}:${capName}:${field}`;
|
|
8252
8263
|
if (this.warnedCapFields.has(key)) return;
|
|
@@ -10023,14 +10034,24 @@ var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
|
|
|
10023
10034
|
}
|
|
10024
10035
|
/**
|
|
10025
10036
|
* Each broker's media base, read per use: the palette's role the broker chose
|
|
10026
|
-
* (the `ha-export-links` record's `mediaRole`, `
|
|
10037
|
+
* (the `ha-export-links` record's `mediaRole`, `publicEndpoint` by default, internal when none is set). A role that resolves to nothing
|
|
10027
10038
|
* is `null` and says so — the image entities then carry no value, which reads as
|
|
10028
10039
|
* empty rather than as a broken link.
|
|
10029
10040
|
*/
|
|
10030
10041
|
async mediaBasesFor(brokerIds, deviceId) {
|
|
10031
10042
|
let bases;
|
|
10032
10043
|
try {
|
|
10033
|
-
|
|
10044
|
+
const answer = await this.ctx.api.localNetwork.getAddresses.query();
|
|
10045
|
+
bases = mediaBasesByBroker(brokerIds, answer, (brokerId) => this.book().mediaRole(brokerId));
|
|
10046
|
+
for (const brokerId of brokerIds) {
|
|
10047
|
+
if (this.warnedMediaFallback.has(brokerId)) continue;
|
|
10048
|
+
if (!usedInternalFallback(answer, this.book().mediaRole(brokerId))) continue;
|
|
10049
|
+
this.warnedMediaFallback.add(brokerId);
|
|
10050
|
+
this.ctx.logger.info("ha-export: no public endpoint is set, so image links use the internal address", {
|
|
10051
|
+
tags: { deviceId },
|
|
10052
|
+
meta: { brokerId }
|
|
10053
|
+
});
|
|
10054
|
+
}
|
|
10034
10055
|
} catch (err) {
|
|
10035
10056
|
this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
|
|
10036
10057
|
tags: { deviceId },
|
|
@@ -10127,7 +10148,7 @@ var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
|
|
|
10127
10148
|
id: "ha-export-transport",
|
|
10128
10149
|
title: "Connection",
|
|
10129
10150
|
titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
|
|
10130
|
-
description: "The hub address each Home Assistant instance fetches image links from.
|
|
10151
|
+
description: "The hub address each Home Assistant instance fetches image links from. Public endpoint is the default, so notifications show images away from home; when none is set the internal address is used. Choose Internal to never use the public endpoint.",
|
|
10131
10152
|
columns: 1,
|
|
10132
10153
|
fields: this.config.knownBrokers.map((broker) => ({
|
|
10133
10154
|
type: "select",
|
|
@@ -10140,7 +10161,7 @@ var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
|
|
|
10140
10161
|
value: "publicEndpoint",
|
|
10141
10162
|
label: "Public endpoint (reachable from outside)"
|
|
10142
10163
|
}],
|
|
10143
|
-
default: "
|
|
10164
|
+
default: "publicEndpoint"
|
|
10144
10165
|
}))
|
|
10145
10166
|
}] : [],
|
|
10146
10167
|
{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as buildAddonRouteProvider, It as statusInstanceOf, Jt as BaseAddon, Ot as runExportedAction, Pt as sendToExportedTarget, Qt as baseUnitOf, Sn as EventCategory, U as consumableFormOf, W as consumableKindOf, X as deviceExportCapability, Zt as RevConflictError, _ as HvacModeSchema, _t as oauthIntegrationCapability, a as AlarmArmModeSchema, an as nodePin, bt as preconditionOf, c as CAMERA_SWITCH_ORDER, cn as boolean, d as ConsumableFormSchema, en as defineAddonMigrations, f as ConsumableItemSchema, fn as object, g as FanDirectionSchema, gn as unknown, h as ExportInstanceUnknownError, i as listHomeAssistantBrokers, in as isReadingQuantity, l as COCO_TO_MACRO, mn as string, on as _enum, p as ConsumableStatusSchema, sn as array, t as haInstanceSettingsPortsOver, tn as defineEntryKind, u as CameraSwitchIdSchema, v as MediaPlayerRepeatSchema, w as addonRoutesCapability, wt as pruneDeletedEntities, y as PLUG_APPLIANCE_TYPES } from "../ha-instance-settings-
|
|
1
|
+
import { F as buildAddonRouteProvider, It as statusInstanceOf, Jt as BaseAddon, Ot as runExportedAction, Pt as sendToExportedTarget, Qt as baseUnitOf, Sn as EventCategory, U as consumableFormOf, W as consumableKindOf, X as deviceExportCapability, Zt as RevConflictError, _ as HvacModeSchema, _t as oauthIntegrationCapability, a as AlarmArmModeSchema, an as nodePin, bt as preconditionOf, c as CAMERA_SWITCH_ORDER, cn as boolean, d as ConsumableFormSchema, en as defineAddonMigrations, f as ConsumableItemSchema, fn as object, g as FanDirectionSchema, gn as unknown, h as ExportInstanceUnknownError, i as listHomeAssistantBrokers, in as isReadingQuantity, l as COCO_TO_MACRO, mn as string, on as _enum, p as ConsumableStatusSchema, sn as array, t as haInstanceSettingsPortsOver, tn as defineEntryKind, u as CameraSwitchIdSchema, v as MediaPlayerRepeatSchema, w as addonRoutesCapability, wt as pruneDeletedEntities, y as PLUG_APPLIANCE_TYPES } from "../ha-instance-settings-qhCNJbbn.mjs";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import "node:zlib";
|
|
4
4
|
import "node:fs";
|
|
@@ -5055,13 +5055,21 @@ function verifyExpiringUrl(input) {
|
|
|
5055
5055
|
/** The hub address roles a Home Assistant media link may be minted on. */
|
|
5056
5056
|
var HA_MEDIA_ROLES = ["internal", "publicEndpoint"];
|
|
5057
5057
|
/**
|
|
5058
|
-
* The base a media link is minted on: the hub's role (D734)
|
|
5059
|
-
*
|
|
5060
|
-
* when
|
|
5061
|
-
*
|
|
5058
|
+
* The base a media link is minted on: the hub's role (D734) the broker chose. Links reach
|
|
5059
|
+
* Home Assistant notifications on phones away from home, so a new broker chooses `publicEndpoint`;
|
|
5060
|
+
* when that resolves to nothing the `internal` address answers instead (a LAN Home Assistant still
|
|
5061
|
+
* gets an image). A broker that chose `internal` never falls back to public. Ranked by
|
|
5062
|
+
* `local-network`, never loopback (`null` when nothing resolves): a link Home Assistant cannot
|
|
5063
|
+
* open is worse than no image.
|
|
5062
5064
|
*/
|
|
5063
|
-
function haMediaBaseUrl(book, role = "
|
|
5064
|
-
|
|
5065
|
+
function haMediaBaseUrl(book, role = "publicEndpoint") {
|
|
5066
|
+
const chosen = book.roles[role].resolved;
|
|
5067
|
+
if (chosen !== null || role !== "publicEndpoint") return chosen;
|
|
5068
|
+
return book.roles.internal.resolved;
|
|
5069
|
+
}
|
|
5070
|
+
/** True when `haMediaBaseUrl` answered with the internal address because the public endpoint was chosen but unset. */
|
|
5071
|
+
function usedInternalFallback(book, role) {
|
|
5072
|
+
return role === "publicEndpoint" && book.roles.publicEndpoint.resolved === null && book.roles.internal.resolved !== null;
|
|
5065
5073
|
}
|
|
5066
5074
|
/** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
|
|
5067
5075
|
function mediaBasesByBroker(brokerIds, book, roleOf) {
|
|
@@ -5244,6 +5252,8 @@ function integrationByBroker(rows, warn) {
|
|
|
5244
5252
|
/** Every broker the old shape has something for, with the value the old reader would have used. */
|
|
5245
5253
|
function legacyLinks(stored, rows, warn) {
|
|
5246
5254
|
const membership = membershipOf$1(stored[MEMBERSHIP_KEY]);
|
|
5255
|
+
const unreadable = rows.find((row) => Object.keys(row.settings).length === 0);
|
|
5256
|
+
if (unreadable !== void 0 && membership.size > 0) throw new Error(`the settings of integration ${unreadable.integrationId} read as empty; the export switch cannot be carried from an unreadable source, nothing was written`);
|
|
5247
5257
|
const integrations = integrationByBroker(rows, warn);
|
|
5248
5258
|
const brokers = new Set(membership.keys());
|
|
5249
5259
|
for (const key of Object.keys(stored)) for (const prefix of [EXPORT_PREFIX, ROLE_PREFIX]) if (key.startsWith(prefix) && key.length > prefix.length) brokers.add(key.slice(prefix.length));
|
|
@@ -5423,12 +5433,12 @@ var haExportLinksKind = defineEntryKind("ha-export-links", {
|
|
|
5423
5433
|
}),
|
|
5424
5434
|
writeTrigger: "an operator switches the export of a Home Assistant instance, picks its image-link address, or exports a device to it"
|
|
5425
5435
|
});
|
|
5426
|
-
/** A link the operator has not touched: off, on the
|
|
5436
|
+
/** A link the operator has not touched: off, on the public endpoint, no device. */
|
|
5427
5437
|
function defaultLinks(name) {
|
|
5428
5438
|
return {
|
|
5429
5439
|
name,
|
|
5430
5440
|
exportTo: false,
|
|
5431
|
-
mediaRole: "
|
|
5441
|
+
mediaRole: "publicEndpoint",
|
|
5432
5442
|
devices: []
|
|
5433
5443
|
};
|
|
5434
5444
|
}
|
|
@@ -5472,7 +5482,7 @@ var ExportLinks = class ExportLinks {
|
|
|
5472
5482
|
return this.rows.get(brokerId)?.exportTo === true;
|
|
5473
5483
|
}
|
|
5474
5484
|
mediaRole(brokerId) {
|
|
5475
|
-
return this.rows.get(brokerId)?.mediaRole ?? "
|
|
5485
|
+
return this.rows.get(brokerId)?.mediaRole ?? "publicEndpoint";
|
|
5476
5486
|
}
|
|
5477
5487
|
/** brokerId → exported device ids, for the brokers that export at least one. */
|
|
5478
5488
|
membership() {
|
|
@@ -8198,6 +8208,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
8198
8208
|
}
|
|
8199
8209
|
/** deviceId:cap:field already reported. Bounded by devices × mapped caps. */
|
|
8200
8210
|
warnedCapFields = /* @__PURE__ */ new Set();
|
|
8211
|
+
warnedMediaFallback = /* @__PURE__ */ new Set();
|
|
8201
8212
|
warnOnce(deviceId, capName, field, line) {
|
|
8202
8213
|
const key = `${deviceId}:${capName}:${field}`;
|
|
8203
8214
|
if (this.warnedCapFields.has(key)) return;
|
|
@@ -9974,14 +9985,24 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
9974
9985
|
}
|
|
9975
9986
|
/**
|
|
9976
9987
|
* Each broker's media base, read per use: the palette's role the broker chose
|
|
9977
|
-
* (the `ha-export-links` record's `mediaRole`, `
|
|
9988
|
+
* (the `ha-export-links` record's `mediaRole`, `publicEndpoint` by default, internal when none is set). A role that resolves to nothing
|
|
9978
9989
|
* is `null` and says so — the image entities then carry no value, which reads as
|
|
9979
9990
|
* empty rather than as a broken link.
|
|
9980
9991
|
*/
|
|
9981
9992
|
async mediaBasesFor(brokerIds, deviceId) {
|
|
9982
9993
|
let bases;
|
|
9983
9994
|
try {
|
|
9984
|
-
|
|
9995
|
+
const answer = await this.ctx.api.localNetwork.getAddresses.query();
|
|
9996
|
+
bases = mediaBasesByBroker(brokerIds, answer, (brokerId) => this.book().mediaRole(brokerId));
|
|
9997
|
+
for (const brokerId of brokerIds) {
|
|
9998
|
+
if (this.warnedMediaFallback.has(brokerId)) continue;
|
|
9999
|
+
if (!usedInternalFallback(answer, this.book().mediaRole(brokerId))) continue;
|
|
10000
|
+
this.warnedMediaFallback.add(brokerId);
|
|
10001
|
+
this.ctx.logger.info("ha-export: no public endpoint is set, so image links use the internal address", {
|
|
10002
|
+
tags: { deviceId },
|
|
10003
|
+
meta: { brokerId }
|
|
10004
|
+
});
|
|
10005
|
+
}
|
|
9985
10006
|
} catch (err) {
|
|
9986
10007
|
this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
|
|
9987
10008
|
tags: { deviceId },
|
|
@@ -10078,7 +10099,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
10078
10099
|
id: "ha-export-transport",
|
|
10079
10100
|
title: "Connection",
|
|
10080
10101
|
titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
|
|
10081
|
-
description: "The hub address each Home Assistant instance fetches image links from.
|
|
10102
|
+
description: "The hub address each Home Assistant instance fetches image links from. Public endpoint is the default, so notifications show images away from home; when none is set the internal address is used. Choose Internal to never use the public endpoint.",
|
|
10082
10103
|
columns: 1,
|
|
10083
10104
|
fields: this.config.knownBrokers.map((broker) => ({
|
|
10084
10105
|
type: "select",
|
|
@@ -10091,7 +10112,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
10091
10112
|
value: "publicEndpoint",
|
|
10092
10113
|
label: "Public endpoint (reachable from outside)"
|
|
10093
10114
|
}],
|
|
10094
|
-
default: "
|
|
10115
|
+
default: "publicEndpoint"
|
|
10095
10116
|
}))
|
|
10096
10117
|
}] : [],
|
|
10097
10118
|
{
|
|
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
|
|
|
5981
5981
|
var ZodFirstPartyTypeKind;
|
|
5982
5982
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5983
5983
|
//#endregion
|
|
5984
|
-
//#region ../types/dist/sleep-
|
|
5984
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
5985
5985
|
/**
|
|
5986
5986
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5987
5987
|
* window to float samples (D455).
|
|
@@ -6591,7 +6591,7 @@ var NOT_RUN = {
|
|
|
6591
6591
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6592
6592
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6593
6593
|
*/
|
|
6594
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6594
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6595
6595
|
const first = migrations[0];
|
|
6596
6596
|
if (first === void 0) return NOT_RUN;
|
|
6597
6597
|
const owner = first.meta.owner;
|
|
@@ -6620,7 +6620,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6620
6620
|
else if (status === "blocked") blocked.push(id);
|
|
6621
6621
|
else skipped.push(id);
|
|
6622
6622
|
};
|
|
6623
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6623
6624
|
for (const step of declaration.steps) {
|
|
6625
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6624
6626
|
const bound = byId.get(step.id);
|
|
6625
6627
|
if (bound === void 0 || step.action === "skip") {
|
|
6626
6628
|
skipped.push(step.id);
|
|
@@ -7780,14 +7782,18 @@ var BaseAddon = class {
|
|
|
7780
7782
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7781
7783
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7782
7784
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7785
|
+
*
|
|
7786
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7787
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7788
|
+
* omitted applied ids "newer").
|
|
7783
7789
|
*/
|
|
7784
|
-
async runMigrations(migrations) {
|
|
7790
|
+
async runMigrations(migrations, options) {
|
|
7785
7791
|
const api = this.ctx.api.migrations;
|
|
7786
7792
|
const report = await runAddonMigrations({
|
|
7787
7793
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7788
7794
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7789
7795
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7790
|
-
}, migrations, this.ctx.logger);
|
|
7796
|
+
}, migrations, this.ctx.logger, options);
|
|
7791
7797
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7792
7798
|
return report;
|
|
7793
7799
|
}
|
|
@@ -9259,6 +9265,7 @@ function defineEntryKind(name, spec) {
|
|
|
9259
9265
|
class: "config",
|
|
9260
9266
|
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9261
9267
|
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9268
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9262
9269
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9263
9270
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9264
9271
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
@@ -14725,7 +14732,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
14725
14732
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
14726
14733
|
* catalogue lists the id (a newer database).
|
|
14727
14734
|
*/
|
|
14728
|
-
meta: MigrationMetaSchema.optional()
|
|
14735
|
+
meta: MigrationMetaSchema.optional(),
|
|
14736
|
+
/**
|
|
14737
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14738
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14739
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14740
|
+
*/
|
|
14741
|
+
ownerNotLoaded: boolean().optional()
|
|
14729
14742
|
});
|
|
14730
14743
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
14731
14744
|
var MigrationPendingEntrySchema = object({
|
|
@@ -56803,6 +56816,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
56803
56816
|
}), object({ success: boolean() }), {
|
|
56804
56817
|
kind: "mutation",
|
|
56805
56818
|
auth: "admin"
|
|
56819
|
+
}), method(object({ id: string() }), object({
|
|
56820
|
+
rows: number().int().nonnegative(),
|
|
56821
|
+
secrets: number().int().nonnegative()
|
|
56822
|
+
}), {
|
|
56823
|
+
kind: "mutation",
|
|
56824
|
+
auth: "admin"
|
|
56806
56825
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
56807
56826
|
addonId: string(),
|
|
56808
56827
|
settings: record(string(), unknown())
|
|
@@ -86591,6 +86610,13 @@ Object.freeze({
|
|
|
86591
86610
|
access: "create",
|
|
86592
86611
|
role: "admin"
|
|
86593
86612
|
},
|
|
86613
|
+
"integrations.clearSettings": {
|
|
86614
|
+
capName: "integrations",
|
|
86615
|
+
capScope: "system",
|
|
86616
|
+
addonId: null,
|
|
86617
|
+
access: "delete",
|
|
86618
|
+
role: "admin"
|
|
86619
|
+
},
|
|
86594
86620
|
"integrations.create": {
|
|
86595
86621
|
capName: "integrations",
|
|
86596
86622
|
capScope: "system",
|
|
@@ -5980,7 +5980,7 @@ var ZodIssueCode = {
|
|
|
5980
5980
|
var ZodFirstPartyTypeKind;
|
|
5981
5981
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5982
5982
|
//#endregion
|
|
5983
|
-
//#region ../types/dist/sleep-
|
|
5983
|
+
//#region ../types/dist/sleep-D1jjWAr8.mjs
|
|
5984
5984
|
/**
|
|
5985
5985
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5986
5986
|
* window to float samples (D455).
|
|
@@ -6590,7 +6590,7 @@ var NOT_RUN = {
|
|
|
6590
6590
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6591
6591
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6592
6592
|
*/
|
|
6593
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6593
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6594
6594
|
const first = migrations[0];
|
|
6595
6595
|
if (first === void 0) return NOT_RUN;
|
|
6596
6596
|
const owner = first.meta.owner;
|
|
@@ -6619,7 +6619,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6619
6619
|
else if (status === "blocked") blocked.push(id);
|
|
6620
6620
|
else skipped.push(id);
|
|
6621
6621
|
};
|
|
6622
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6622
6623
|
for (const step of declaration.steps) {
|
|
6624
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6623
6625
|
const bound = byId.get(step.id);
|
|
6624
6626
|
if (bound === void 0 || step.action === "skip") {
|
|
6625
6627
|
skipped.push(step.id);
|
|
@@ -7779,14 +7781,18 @@ var BaseAddon = class {
|
|
|
7779
7781
|
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7780
7782
|
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7781
7783
|
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7784
|
+
*
|
|
7785
|
+
* ALWAYS pass the whole catalogue; a phase that must run only some of it passes
|
|
7786
|
+
* `{ only: [ids] }` — never a filtered catalogue (the engine would judge the
|
|
7787
|
+
* omitted applied ids "newer").
|
|
7782
7788
|
*/
|
|
7783
|
-
async runMigrations(migrations) {
|
|
7789
|
+
async runMigrations(migrations, options) {
|
|
7784
7790
|
const api = this.ctx.api.migrations;
|
|
7785
7791
|
const report = await runAddonMigrations({
|
|
7786
7792
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7787
7793
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7788
7794
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7789
|
-
}, migrations, this.ctx.logger);
|
|
7795
|
+
}, migrations, this.ctx.logger, options);
|
|
7790
7796
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7791
7797
|
return report;
|
|
7792
7798
|
}
|
|
@@ -9258,6 +9264,7 @@ function defineEntryKind(name, spec) {
|
|
|
9258
9264
|
class: "config",
|
|
9259
9265
|
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9260
9266
|
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9267
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9261
9268
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9262
9269
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9263
9270
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
@@ -14724,7 +14731,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
14724
14731
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
14725
14732
|
* catalogue lists the id (a newer database).
|
|
14726
14733
|
*/
|
|
14727
|
-
meta: MigrationMetaSchema.optional()
|
|
14734
|
+
meta: MigrationMetaSchema.optional(),
|
|
14735
|
+
/**
|
|
14736
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14737
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14738
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14739
|
+
*/
|
|
14740
|
+
ownerNotLoaded: boolean().optional()
|
|
14728
14741
|
});
|
|
14729
14742
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
14730
14743
|
var MigrationPendingEntrySchema = object({
|
|
@@ -56802,6 +56815,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
56802
56815
|
}), object({ success: boolean() }), {
|
|
56803
56816
|
kind: "mutation",
|
|
56804
56817
|
auth: "admin"
|
|
56818
|
+
}), method(object({ id: string() }), object({
|
|
56819
|
+
rows: number().int().nonnegative(),
|
|
56820
|
+
secrets: number().int().nonnegative()
|
|
56821
|
+
}), {
|
|
56822
|
+
kind: "mutation",
|
|
56823
|
+
auth: "admin"
|
|
56805
56824
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
56806
56825
|
addonId: string(),
|
|
56807
56826
|
settings: record(string(), unknown())
|
|
@@ -86590,6 +86609,13 @@ Object.freeze({
|
|
|
86590
86609
|
access: "create",
|
|
86591
86610
|
role: "admin"
|
|
86592
86611
|
},
|
|
86612
|
+
"integrations.clearSettings": {
|
|
86613
|
+
capName: "integrations",
|
|
86614
|
+
capScope: "system",
|
|
86615
|
+
addonId: null,
|
|
86616
|
+
access: "delete",
|
|
86617
|
+
role: "admin"
|
|
86618
|
+
},
|
|
86593
86619
|
"integrations.create": {
|
|
86594
86620
|
capName: "integrations",
|
|
86595
86621
|
capScope: "system",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-homeassistant",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.658",
|
|
4
4
|
"description": "Home Assistant device provider addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
|
|
135
135
|
},
|
|
136
136
|
"requires": {
|
|
137
|
-
"server": ">=1.2.
|
|
137
|
+
"server": ">=1.2.766"
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
140
|
"files": [
|