@camstack/addon-provider-homeassistant 1.2.652 → 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/CHANGELOG.md +13 -0
- package/dist/addon.js +669 -495
- package/dist/addon.mjs +206 -32
- package/dist/ha-export/ha-export.addon.js +382 -130
- package/dist/ha-export/ha-export.addon.mjs +336 -84
- package/dist/{ha-broker-source-D3ScsO0T.js → ha-instance-settings-Bn_Trz6p.js} +502 -220
- package/dist/{ha-broker-source-CBWl6Wat.mjs → ha-instance-settings-g5pwb3Fi.mjs} +484 -220
- package/package.json +2 -2
|
@@ -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
|
|
27
|
+
const require_ha_instance_settings = require("../ha-instance-settings-Bn_Trz6p.js");
|
|
28
28
|
let node_crypto = require("node:crypto");
|
|
29
29
|
require("node:zlib");
|
|
30
30
|
let node_fs = require("node:fs");
|
|
@@ -91,7 +91,7 @@ async function applyCameraFunctionWrite(api, deviceId, write, sourceNodeId) {
|
|
|
91
91
|
await api.streamBroker.setDeviceAudioMute.mutate({
|
|
92
92
|
deviceId,
|
|
93
93
|
muted: write.muted
|
|
94
|
-
},
|
|
94
|
+
}, require_ha_instance_settings.nodePin(sourceNodeId));
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -725,8 +725,8 @@ function consumableEntity(key, facet) {
|
|
|
725
725
|
* rather than to an icon for nothing.
|
|
726
726
|
*/
|
|
727
727
|
function consumableForm(item) {
|
|
728
|
-
const declared =
|
|
729
|
-
return
|
|
728
|
+
const declared = require_ha_instance_settings.ConsumableFormSchema.safeParse(item.form);
|
|
729
|
+
return require_ha_instance_settings.consumableFormOf({
|
|
730
730
|
...item.kind !== void 0 ? { kind: item.kind } : {},
|
|
731
731
|
...declared.success ? { form: declared.data } : {}
|
|
732
732
|
});
|
|
@@ -739,7 +739,7 @@ function partIcon(key, label) {
|
|
|
739
739
|
function formIcon(item, form) {
|
|
740
740
|
switch (form) {
|
|
741
741
|
case "part": return partIcon(item.key, item.label);
|
|
742
|
-
case "tank": return
|
|
742
|
+
case "tank": return require_ha_instance_settings.consumableKindOf(item) === "waste" ? "mdi:water-off" : "mdi:water";
|
|
743
743
|
case "product": return "mdi:shaker-outline";
|
|
744
744
|
case "bin": return "mdi:delete";
|
|
745
745
|
}
|
|
@@ -750,8 +750,8 @@ function formIcon(item, form) {
|
|
|
750
750
|
* form this export does not know degrades one icon instead of refusing the
|
|
751
751
|
* whole slice.
|
|
752
752
|
*/
|
|
753
|
-
var ExportConsumableItemSchema =
|
|
754
|
-
var ExportConsumablesSchema =
|
|
753
|
+
var ExportConsumableItemSchema = require_ha_instance_settings.ConsumableItemSchema.extend({ form: require_ha_instance_settings.unknown().optional() });
|
|
754
|
+
var ExportConsumablesSchema = require_ha_instance_settings.object({ items: require_ha_instance_settings.array(ExportConsumableItemSchema) });
|
|
755
755
|
/**
|
|
756
756
|
* A worn part's life is a diagnostic; a tank's fill is what the device page
|
|
757
757
|
* is for (a vacuum's clean water, a dishwasher's salt), so it is not hidden.
|
|
@@ -764,7 +764,7 @@ function consumableEntities(item) {
|
|
|
764
764
|
capName: "consumables",
|
|
765
765
|
itemKey: item.key
|
|
766
766
|
};
|
|
767
|
-
const kind =
|
|
767
|
+
const kind = require_ha_instance_settings.consumableKindOf(item);
|
|
768
768
|
const form = consumableForm(item);
|
|
769
769
|
const icon = formIcon(item, form);
|
|
770
770
|
/** What the item IS, on the sensors that describe it (not on a counter or a button). */
|
|
@@ -804,7 +804,7 @@ function consumableEntities(item) {
|
|
|
804
804
|
platform: "sensor",
|
|
805
805
|
label: `${item.label} status`,
|
|
806
806
|
deviceClass: "enum",
|
|
807
|
-
options:
|
|
807
|
+
options: require_ha_instance_settings.ConsumableStatusSchema.options,
|
|
808
808
|
icon,
|
|
809
809
|
attributes,
|
|
810
810
|
entityCategory: "diagnostic"
|
|
@@ -2405,7 +2405,7 @@ var CAP_ENTITY_MAP = {
|
|
|
2405
2405
|
* failure. `fanMode` and `preset` get no such list at all: they are
|
|
2406
2406
|
* free-form strings, so they stay sensors.
|
|
2407
2407
|
*/
|
|
2408
|
-
options:
|
|
2408
|
+
options: require_ha_instance_settings.HvacModeSchema.options,
|
|
2409
2409
|
extras: {
|
|
2410
2410
|
"current-temp": {
|
|
2411
2411
|
platform: "sensor",
|
|
@@ -2555,7 +2555,7 @@ var CAP_ENTITY_MAP = {
|
|
|
2555
2555
|
field: "direction",
|
|
2556
2556
|
label: "Direction",
|
|
2557
2557
|
writable: true,
|
|
2558
|
-
options:
|
|
2558
|
+
options: require_ha_instance_settings.FanDirectionSchema.options,
|
|
2559
2559
|
enabledByDefault: false
|
|
2560
2560
|
}
|
|
2561
2561
|
}
|
|
@@ -2985,7 +2985,7 @@ var CAP_ENTITY_MAP = {
|
|
|
2985
2985
|
field: "repeat",
|
|
2986
2986
|
label: "Repeat",
|
|
2987
2987
|
writable: true,
|
|
2988
|
-
options:
|
|
2988
|
+
options: require_ha_instance_settings.MediaPlayerRepeatSchema.options,
|
|
2989
2989
|
enabledByDefault: false
|
|
2990
2990
|
},
|
|
2991
2991
|
shuffle: {
|
|
@@ -3481,7 +3481,7 @@ function controlEntitySpecs(slice, quantity) {
|
|
|
3481
3481
|
const min = readFiniteNumber(slice, "min") ?? PERCENT_RANGE.min;
|
|
3482
3482
|
const max = readFiniteNumber(slice, "max") ?? PERCENT_RANGE.max;
|
|
3483
3483
|
const step = readFiniteNumber(slice, "step") ?? PERCENT_RANGE.step;
|
|
3484
|
-
const unit = quantity === void 0 ? void 0 :
|
|
3484
|
+
const unit = quantity === void 0 ? void 0 : require_ha_instance_settings.baseUnitOf(quantity);
|
|
3485
3485
|
if (min < max && step > 0) return [{
|
|
3486
3486
|
entity: primary,
|
|
3487
3487
|
platform: "number",
|
|
@@ -3803,7 +3803,7 @@ function buildDerivedPlan(device, options = UNNEGOTIATED_PLAN_OPTIONS) {
|
|
|
3803
3803
|
continue;
|
|
3804
3804
|
}
|
|
3805
3805
|
if (cap === "numeric-sensor") {
|
|
3806
|
-
const unit = device.readingQuantity === void 0 ? void 0 :
|
|
3806
|
+
const unit = device.readingQuantity === void 0 ? void 0 : require_ha_instance_settings.baseUnitOf(device.readingQuantity);
|
|
3807
3807
|
specs.push(...capEntitySpecs(cap).map((spec) => degradeUnsupported(unit === void 0 ? spec : {
|
|
3808
3808
|
...spec,
|
|
3809
3809
|
unit
|
|
@@ -3878,7 +3878,7 @@ function buildDevicePlan(device, options = UNNEGOTIATED_PLAN_OPTIONS) {
|
|
|
3878
3878
|
* was.
|
|
3879
3879
|
*/
|
|
3880
3880
|
/** The camera function ids, keyed by the entity slug the catalog emits. */
|
|
3881
|
-
var CAMERA_SWITCH_BY_SLUG = Object.fromEntries(
|
|
3881
|
+
var CAMERA_SWITCH_BY_SLUG = Object.fromEntries(require_ha_instance_settings.CAMERA_SWITCH_ORDER.map((id) => [toSlug(id), id]));
|
|
3882
3882
|
var PTZ_DIRECTION_BY_ENTITY = Object.fromEntries(PTZ_BUTTONS.map((entity) => [entity, entity.slice(4)]));
|
|
3883
3883
|
function parseBool(value) {
|
|
3884
3884
|
const lower = value.trim().toLowerCase();
|
|
@@ -3941,7 +3941,7 @@ function parseAlarmAction(value) {
|
|
|
3941
3941
|
if (lower === "disarm" || lower === "disarmed") return { kind: "disarm" };
|
|
3942
3942
|
if (lower === "trigger" || lower === "triggered") return { kind: "trigger" };
|
|
3943
3943
|
const withoutPrefix = lower.startsWith("armed_") ? lower.slice(6) : lower.startsWith("arm_") ? lower.slice(4) : lower;
|
|
3944
|
-
const mode =
|
|
3944
|
+
const mode = require_ha_instance_settings.AlarmArmModeSchema.safeParse(withoutPrefix);
|
|
3945
3945
|
return mode.success ? {
|
|
3946
3946
|
kind: "arm",
|
|
3947
3947
|
mode: mode.data
|
|
@@ -4090,7 +4090,7 @@ var CAP_COMMAND_ROUTES = {
|
|
|
4090
4090
|
},
|
|
4091
4091
|
"climate-control": {
|
|
4092
4092
|
primary: (deviceId, value) => {
|
|
4093
|
-
const mode = parseEnum(
|
|
4093
|
+
const mode = parseEnum(require_ha_instance_settings.HvacModeSchema, value);
|
|
4094
4094
|
return mode === null ? null : {
|
|
4095
4095
|
kind: "climate-mode",
|
|
4096
4096
|
deviceId,
|
|
@@ -4205,7 +4205,7 @@ var CAP_COMMAND_ROUTES = {
|
|
|
4205
4205
|
};
|
|
4206
4206
|
},
|
|
4207
4207
|
direction: (deviceId, value) => {
|
|
4208
|
-
const direction = parseEnum(
|
|
4208
|
+
const direction = parseEnum(require_ha_instance_settings.FanDirectionSchema, value);
|
|
4209
4209
|
return direction === null ? null : {
|
|
4210
4210
|
kind: "fan-direction",
|
|
4211
4211
|
deviceId,
|
|
@@ -4409,7 +4409,7 @@ var CAP_COMMAND_ROUTES = {
|
|
|
4409
4409
|
};
|
|
4410
4410
|
},
|
|
4411
4411
|
repeat: (deviceId, value) => {
|
|
4412
|
-
const repeat = parseEnum(
|
|
4412
|
+
const repeat = parseEnum(require_ha_instance_settings.MediaPlayerRepeatSchema, value);
|
|
4413
4413
|
return repeat === null ? null : {
|
|
4414
4414
|
kind: "media-repeat",
|
|
4415
4415
|
deviceId,
|
|
@@ -5001,32 +5001,13 @@ async function planDropPublicBaseUrl(ports) {
|
|
|
5001
5001
|
op: "deleteKeys",
|
|
5002
5002
|
scope: ADDON_SETTINGS_TABLE,
|
|
5003
5003
|
keys: present.map(rowId),
|
|
5004
|
-
precondition:
|
|
5004
|
+
precondition: require_ha_instance_settings.preconditionOf(Object.fromEntries(present.map((key) => [rowId(key), {
|
|
5005
5005
|
addonId: HA_EXPORT_ADDON_ID,
|
|
5006
5006
|
key,
|
|
5007
5007
|
value: stored[key]
|
|
5008
5008
|
}])))
|
|
5009
5009
|
}] };
|
|
5010
5010
|
}
|
|
5011
|
-
//#endregion
|
|
5012
|
-
//#region src/ha-export/migrations/index.ts
|
|
5013
|
-
/** The `homeassistant-export` catalogue (D987): offered to the engine when the addon initialises. */
|
|
5014
|
-
var ADDON_MIGRATIONS = [{
|
|
5015
|
-
meta: {
|
|
5016
|
-
id: "homeassistant-export/2026-11-02-drop-public-base-url",
|
|
5017
|
-
owner: "homeassistant-export",
|
|
5018
|
-
level: "addon-state",
|
|
5019
|
-
seq: 1,
|
|
5020
|
-
dependsOn: [],
|
|
5021
|
-
store: "settings",
|
|
5022
|
-
scope: "cluster",
|
|
5023
|
-
onFailure: "tolerate",
|
|
5024
|
-
bodyKind: "plan",
|
|
5025
|
-
description: "Delete the retired publicBaseUrl setting; each instance now chooses a hub address role",
|
|
5026
|
-
fixtureMark: "the reconcile interval the migration does not know"
|
|
5027
|
-
},
|
|
5028
|
-
plan: planDropPublicBaseUrl
|
|
5029
|
-
}];
|
|
5030
5011
|
process.platform, process.env["SystemRoot"];
|
|
5031
5012
|
/**
|
|
5032
5013
|
* The ONE derivation of "a signed, expiring URL".
|
|
@@ -5082,65 +5063,6 @@ function verifyExpiringUrl(input) {
|
|
|
5082
5063
|
return (0, node_crypto.timingSafeEqual)(a, b);
|
|
5083
5064
|
}
|
|
5084
5065
|
//#endregion
|
|
5085
|
-
//#region src/ha-export/media-url.ts
|
|
5086
|
-
/**
|
|
5087
|
-
* Signed, expiring media links.
|
|
5088
|
-
*
|
|
5089
|
-
* Images never cross the push transport. Home Assistant is handed a URL
|
|
5090
|
-
* and fetches the bytes itself, on demand, which is what keeps a camera
|
|
5091
|
-
* with a `last_image` entity from turning every detection into a
|
|
5092
|
-
* multi-megabyte POST.
|
|
5093
|
-
*
|
|
5094
|
-
* The link is signed rather than protected by a session, because the
|
|
5095
|
-
* fetcher is HA's own frontend or a phone — neither of which holds a
|
|
5096
|
-
* camstack token.
|
|
5097
|
-
*
|
|
5098
|
-
* The HMAC itself is NOT restated here. It used to be — this module and the
|
|
5099
|
-
* notification artifact plane each carried their own copy, because addons never
|
|
5100
|
-
* import each other and neither could reach the other's. That reasoning was
|
|
5101
|
-
* right and the conclusion was wrong: both now delegate to the single
|
|
5102
|
-
* derivation in `@camstack/types/node`, which every addon may depend on. What
|
|
5103
|
-
* stays local is policy — the TTL below, and the id this plane signs.
|
|
5104
|
-
*
|
|
5105
|
-
* Expiry is checked BEFORE the constant-time compare (in the shared verifier):
|
|
5106
|
-
* an expired link is refused whether or not its signature is valid, so a leaked
|
|
5107
|
-
* URL stops working on its own.
|
|
5108
|
-
*/
|
|
5109
|
-
/** The settings key that carries one broker's media role (per-instance, like `exportTo:`). */
|
|
5110
|
-
function mediaRoleKey(brokerId) {
|
|
5111
|
-
return `mediaRole:${brokerId}`;
|
|
5112
|
-
}
|
|
5113
|
-
/** A broker's chosen role; anything stored that is not a known role is `internal`. */
|
|
5114
|
-
function haMediaRoleOf(store, brokerId) {
|
|
5115
|
-
return store[mediaRoleKey(brokerId)] === "publicEndpoint" ? "publicEndpoint" : "internal";
|
|
5116
|
-
}
|
|
5117
|
-
/**
|
|
5118
|
-
* The base a media link is minted on: the hub's role (D734) — `internal` unless the
|
|
5119
|
-
* broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
|
|
5120
|
-
* when the role resolves to nothing): a link Home Assistant cannot open is worse
|
|
5121
|
-
* than no image.
|
|
5122
|
-
*/
|
|
5123
|
-
function haMediaBaseUrl(book, role = "internal") {
|
|
5124
|
-
return book.roles[role].resolved;
|
|
5125
|
-
}
|
|
5126
|
-
/** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
|
|
5127
|
-
function mediaBasesByBroker(store, brokerIds, book) {
|
|
5128
|
-
return new Map(brokerIds.map((brokerId) => [brokerId, haMediaBaseUrl(book, haMediaRoleOf(store, brokerId))]));
|
|
5129
|
-
}
|
|
5130
|
-
/** How long a minted link stays valid. Long enough for HA to render it. */
|
|
5131
|
-
var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
|
|
5132
|
-
function signMedia(secret, id, expMs) {
|
|
5133
|
-
return signExpiringUrl(secret, id, expMs);
|
|
5134
|
-
}
|
|
5135
|
-
function buildMediaUrl(input) {
|
|
5136
|
-
const base = input.baseUrl.replace(/\/+$/, "");
|
|
5137
|
-
const sig = signMedia(input.secret, input.id, input.expMs);
|
|
5138
|
-
return `${base}${input.routePrefix}/${encodeURIComponent(input.id)}?exp=${input.expMs}&sig=${sig}`;
|
|
5139
|
-
}
|
|
5140
|
-
function verifyMediaSignature(input) {
|
|
5141
|
-
return verifyExpiringUrl(input);
|
|
5142
|
-
}
|
|
5143
|
-
//#endregion
|
|
5144
5066
|
//#region src/ha-export/membership.ts
|
|
5145
5067
|
function devicesForBroker(membership, brokerId) {
|
|
5146
5068
|
return membership[brokerId] ?? [];
|
|
@@ -5202,10 +5124,19 @@ function pruneBrokers(membership, knownBrokerIds) {
|
|
|
5202
5124
|
*
|
|
5203
5125
|
* A broker that is no longer known is not enabled, whatever the store says:
|
|
5204
5126
|
* a stale `exportTo:` key for a deleted instance must not resurrect it.
|
|
5127
|
+
*
|
|
5128
|
+
* The switch is a setting of the INSTANCE (D985): `perBroker` carries each broker's integration
|
|
5129
|
+
* settings, and a broker whose integration has an `exportTo` answers from it. A broker whose
|
|
5130
|
+
* integration has none yet — the migration has not copied it — falls back to the legacy
|
|
5131
|
+
* `exportTo:<brokerId>` key, so the move never flips a switch mid-way.
|
|
5205
5132
|
*/
|
|
5206
|
-
function enabledBrokerIds(store, knownBrokerIds) {
|
|
5207
|
-
return knownBrokerIds.filter((brokerId) =>
|
|
5133
|
+
function enabledBrokerIds(store, knownBrokerIds, perBroker = NO_INSTANCE_SETTINGS) {
|
|
5134
|
+
return knownBrokerIds.filter((brokerId) => {
|
|
5135
|
+
const own = perBroker.get(brokerId)?.["exportTo"];
|
|
5136
|
+
return typeof own === "boolean" ? own : store[`exportTo:${brokerId}`] === true;
|
|
5137
|
+
});
|
|
5208
5138
|
}
|
|
5139
|
+
var NO_INSTANCE_SETTINGS = /* @__PURE__ */ new Map();
|
|
5209
5140
|
/** Same brokers, same order, same names — nothing for the form to re-render. */
|
|
5210
5141
|
function sameBrokers(a, b) {
|
|
5211
5142
|
if (a.length !== b.length) return false;
|
|
@@ -5259,6 +5190,282 @@ function brokerRefreshPatch(current, next) {
|
|
|
5259
5190
|
};
|
|
5260
5191
|
}
|
|
5261
5192
|
//#endregion
|
|
5193
|
+
//#region src/ha-export/media-url.ts
|
|
5194
|
+
/**
|
|
5195
|
+
* Signed, expiring media links.
|
|
5196
|
+
*
|
|
5197
|
+
* Images never cross the push transport. Home Assistant is handed a URL
|
|
5198
|
+
* and fetches the bytes itself, on demand, which is what keeps a camera
|
|
5199
|
+
* with a `last_image` entity from turning every detection into a
|
|
5200
|
+
* multi-megabyte POST.
|
|
5201
|
+
*
|
|
5202
|
+
* The link is signed rather than protected by a session, because the
|
|
5203
|
+
* fetcher is HA's own frontend or a phone — neither of which holds a
|
|
5204
|
+
* camstack token.
|
|
5205
|
+
*
|
|
5206
|
+
* The HMAC itself is NOT restated here. It used to be — this module and the
|
|
5207
|
+
* notification artifact plane each carried their own copy, because addons never
|
|
5208
|
+
* import each other and neither could reach the other's. That reasoning was
|
|
5209
|
+
* right and the conclusion was wrong: both now delegate to the single
|
|
5210
|
+
* derivation in `@camstack/types/node`, which every addon may depend on. What
|
|
5211
|
+
* stays local is policy — the TTL below, and the id this plane signs.
|
|
5212
|
+
*
|
|
5213
|
+
* Expiry is checked BEFORE the constant-time compare (in the shared verifier):
|
|
5214
|
+
* an expired link is refused whether or not its signature is valid, so a leaked
|
|
5215
|
+
* URL stops working on its own.
|
|
5216
|
+
*/
|
|
5217
|
+
/** The hub address roles a Home Assistant media link may be minted on. */
|
|
5218
|
+
var HA_MEDIA_ROLES = ["internal", "publicEndpoint"];
|
|
5219
|
+
/** The settings key that carries one broker's media role (per-instance, like `exportTo:`). */
|
|
5220
|
+
function mediaRoleKey(brokerId) {
|
|
5221
|
+
return `mediaRole:${brokerId}`;
|
|
5222
|
+
}
|
|
5223
|
+
/**
|
|
5224
|
+
* A broker's chosen role; anything stored that is not a known role is `internal`. The role is a
|
|
5225
|
+
* setting of the INSTANCE (D985): the integration's `mediaRole` answers when it has one, and a
|
|
5226
|
+
* broker the migration has not reached falls back to the legacy `mediaRole:<brokerId>` key.
|
|
5227
|
+
*/
|
|
5228
|
+
function haMediaRoleOf(store, brokerId, perBroker = NO_INSTANCE_SETTINGS) {
|
|
5229
|
+
const own = perBroker.get(brokerId)?.["mediaRole"];
|
|
5230
|
+
return (own !== void 0 ? own : store[mediaRoleKey(brokerId)]) === "publicEndpoint" ? "publicEndpoint" : "internal";
|
|
5231
|
+
}
|
|
5232
|
+
/**
|
|
5233
|
+
* The base a media link is minted on: the hub's role (D734) — `internal` unless the
|
|
5234
|
+
* broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
|
|
5235
|
+
* when the role resolves to nothing): a link Home Assistant cannot open is worse
|
|
5236
|
+
* than no image.
|
|
5237
|
+
*/
|
|
5238
|
+
function haMediaBaseUrl(book, role = "internal") {
|
|
5239
|
+
return book.roles[role].resolved;
|
|
5240
|
+
}
|
|
5241
|
+
/** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
|
|
5242
|
+
function mediaBasesByBroker(store, brokerIds, book, perBroker = NO_INSTANCE_SETTINGS) {
|
|
5243
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, haMediaBaseUrl(book, haMediaRoleOf(store, brokerId, perBroker))]));
|
|
5244
|
+
}
|
|
5245
|
+
/** How long a minted link stays valid. Long enough for HA to render it. */
|
|
5246
|
+
var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
|
|
5247
|
+
function signMedia(secret, id, expMs) {
|
|
5248
|
+
return signExpiringUrl(secret, id, expMs);
|
|
5249
|
+
}
|
|
5250
|
+
function buildMediaUrl(input) {
|
|
5251
|
+
const base = input.baseUrl.replace(/\/+$/, "");
|
|
5252
|
+
const sig = signMedia(input.secret, input.id, input.expMs);
|
|
5253
|
+
return `${base}${input.routePrefix}/${encodeURIComponent(input.id)}?exp=${input.expMs}&sig=${sig}`;
|
|
5254
|
+
}
|
|
5255
|
+
function verifyMediaSignature(input) {
|
|
5256
|
+
return verifyExpiringUrl(input);
|
|
5257
|
+
}
|
|
5258
|
+
//#endregion
|
|
5259
|
+
//#region src/ha-export/migrations/per-broker-flags.ts
|
|
5260
|
+
/**
|
|
5261
|
+
* Whether the export reaches a Home Assistant instance, and which hub address role its image
|
|
5262
|
+
* links use, are facts of THAT instance (D985): they live in the instance's integration settings
|
|
5263
|
+
* as `exportTo` and `mediaRole`, not in the addon store as one dynamic key per broker
|
|
5264
|
+
* (`exportTo:<brokerId>`, `mediaRole:<brokerId>`).
|
|
5265
|
+
*
|
|
5266
|
+
* Two migrations:
|
|
5267
|
+
*
|
|
5268
|
+
* 1. `per-broker-flags-to-integration` (procedure) copies each legacy key to the integration that
|
|
5269
|
+
* links its broker. A flag the integration already has is never overwritten; a key whose
|
|
5270
|
+
* broker has no integration any more (a deleted instance) is skipped, never recreated; two
|
|
5271
|
+
* integrations linking one broker are an ambiguity and throw; a value that is not a boolean /
|
|
5272
|
+
* a known role is skipped (the reader already treats it as off / `internal`).
|
|
5273
|
+
* 2. `drop-per-broker-flags` (plan) deletes a legacy key only after a read-back: the integration
|
|
5274
|
+
* must show the same flag. A key the integration disagrees with (its own value was there
|
|
5275
|
+
* first) or whose broker has no integration stays PARKED in place, never deleted.
|
|
5276
|
+
*
|
|
5277
|
+
* Both are idempotent.
|
|
5278
|
+
*/
|
|
5279
|
+
var PREFIX = {
|
|
5280
|
+
exportTo: "exportTo:",
|
|
5281
|
+
mediaRole: "mediaRole:"
|
|
5282
|
+
};
|
|
5283
|
+
var validValue = (flag, value) => flag === "exportTo" ? typeof value === "boolean" : typeof value === "string" && HA_MEDIA_ROLES.some((role) => role === value);
|
|
5284
|
+
/** Every legacy key with a value the reader understands, per broker. */
|
|
5285
|
+
function legacyFlags(stored) {
|
|
5286
|
+
const out = [];
|
|
5287
|
+
for (const [storeKey, value] of Object.entries(stored)) for (const flag of ["exportTo", "mediaRole"]) {
|
|
5288
|
+
const prefix = PREFIX[flag];
|
|
5289
|
+
if (!storeKey.startsWith(prefix) || storeKey.length === prefix.length) continue;
|
|
5290
|
+
if (validValue(flag, value)) out.push({
|
|
5291
|
+
storeKey,
|
|
5292
|
+
brokerId: storeKey.slice(prefix.length),
|
|
5293
|
+
flag,
|
|
5294
|
+
value
|
|
5295
|
+
});
|
|
5296
|
+
}
|
|
5297
|
+
return out;
|
|
5298
|
+
}
|
|
5299
|
+
/** The one integration per broker; two integrations linking one broker are an ambiguity. */
|
|
5300
|
+
function instanceByBroker(rows) {
|
|
5301
|
+
const out = /* @__PURE__ */ new Map();
|
|
5302
|
+
for (const row of rows) {
|
|
5303
|
+
if (row.brokerId === null) continue;
|
|
5304
|
+
const other = out.get(row.brokerId);
|
|
5305
|
+
if (other !== void 0) throw new Error(`homeassistant-export: integrations ${other.integrationId} and ${row.integrationId} both link broker ${row.brokerId} — nothing copied`);
|
|
5306
|
+
out.set(row.brokerId, row);
|
|
5307
|
+
}
|
|
5308
|
+
return out;
|
|
5309
|
+
}
|
|
5310
|
+
async function copyFlagsToIntegrations(ports) {
|
|
5311
|
+
const legacy = legacyFlags(await ports.readStored());
|
|
5312
|
+
if (legacy.length === 0) return {
|
|
5313
|
+
outcome: "already-done",
|
|
5314
|
+
summary: { reason: "no-legacy-flags" }
|
|
5315
|
+
};
|
|
5316
|
+
const byBroker = instanceByBroker(await ports.listInstances());
|
|
5317
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5318
|
+
let kept = 0;
|
|
5319
|
+
let skipped = 0;
|
|
5320
|
+
for (const entry of legacy) {
|
|
5321
|
+
const instance = byBroker.get(entry.brokerId);
|
|
5322
|
+
if (instance === void 0) {
|
|
5323
|
+
skipped += 1;
|
|
5324
|
+
continue;
|
|
5325
|
+
}
|
|
5326
|
+
if (entry.flag in instance.settings) {
|
|
5327
|
+
kept += 1;
|
|
5328
|
+
continue;
|
|
5329
|
+
}
|
|
5330
|
+
pending.set(instance.integrationId, {
|
|
5331
|
+
...pending.get(instance.integrationId),
|
|
5332
|
+
[entry.flag]: entry.value
|
|
5333
|
+
});
|
|
5334
|
+
}
|
|
5335
|
+
let copied = 0;
|
|
5336
|
+
let gone = 0;
|
|
5337
|
+
for (const [integrationId, patch] of pending) if (await ports.patchInstanceSettings(integrationId, patch)) copied += Object.keys(patch).length;
|
|
5338
|
+
else gone += 1;
|
|
5339
|
+
return {
|
|
5340
|
+
outcome: copied === 0 ? "already-done" : "done",
|
|
5341
|
+
summary: {
|
|
5342
|
+
copied,
|
|
5343
|
+
kept,
|
|
5344
|
+
skipped,
|
|
5345
|
+
gone
|
|
5346
|
+
}
|
|
5347
|
+
};
|
|
5348
|
+
}
|
|
5349
|
+
async function planDropPerBrokerFlags(ports) {
|
|
5350
|
+
const legacy = legacyFlags(await ports.readStored());
|
|
5351
|
+
if (legacy.length === 0) return { edits: [] };
|
|
5352
|
+
const byBroker = instanceByBroker(await ports.listInstances());
|
|
5353
|
+
const rowId = (key) => `${HA_EXPORT_ADDON_ID}.${key}`;
|
|
5354
|
+
const drop = [];
|
|
5355
|
+
for (const entry of legacy) {
|
|
5356
|
+
const instance = byBroker.get(entry.brokerId);
|
|
5357
|
+
if (instance === void 0) continue;
|
|
5358
|
+
if (!(entry.flag in instance.settings)) throw new Error(`homeassistant-export: ${entry.storeKey} is not dropped, integration ${instance.integrationId} does not show ${entry.flag}`);
|
|
5359
|
+
if (instance.settings[entry.flag] === entry.value) drop.push(entry);
|
|
5360
|
+
}
|
|
5361
|
+
if (drop.length === 0) return { edits: [] };
|
|
5362
|
+
return { edits: [{
|
|
5363
|
+
op: "deleteKeys",
|
|
5364
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
5365
|
+
keys: drop.map((entry) => rowId(entry.storeKey)),
|
|
5366
|
+
precondition: require_ha_instance_settings.preconditionOf(Object.fromEntries(drop.map((entry) => [rowId(entry.storeKey), {
|
|
5367
|
+
addonId: HA_EXPORT_ADDON_ID,
|
|
5368
|
+
key: entry.storeKey,
|
|
5369
|
+
value: entry.value
|
|
5370
|
+
}])))
|
|
5371
|
+
}] };
|
|
5372
|
+
}
|
|
5373
|
+
//#endregion
|
|
5374
|
+
//#region src/ha-export/migrations/index.ts
|
|
5375
|
+
/** The `homeassistant-export` catalogue (D987): offered to the engine when the addon initialises. */
|
|
5376
|
+
var ADDON_MIGRATIONS = [
|
|
5377
|
+
{
|
|
5378
|
+
meta: {
|
|
5379
|
+
id: "homeassistant-export/2026-11-02-drop-public-base-url",
|
|
5380
|
+
owner: "homeassistant-export",
|
|
5381
|
+
level: "addon-state",
|
|
5382
|
+
seq: 1,
|
|
5383
|
+
dependsOn: [],
|
|
5384
|
+
store: "settings",
|
|
5385
|
+
scope: "cluster",
|
|
5386
|
+
onFailure: "tolerate",
|
|
5387
|
+
bodyKind: "plan",
|
|
5388
|
+
description: "Delete the retired publicBaseUrl setting; each instance now chooses a hub address role",
|
|
5389
|
+
fixtureMark: "the reconcile interval the migration does not know"
|
|
5390
|
+
},
|
|
5391
|
+
plan: planDropPublicBaseUrl
|
|
5392
|
+
},
|
|
5393
|
+
{
|
|
5394
|
+
meta: {
|
|
5395
|
+
id: "homeassistant-export/2026-11-03-per-broker-flags-to-integration",
|
|
5396
|
+
owner: "homeassistant-export",
|
|
5397
|
+
level: "addon-state",
|
|
5398
|
+
seq: 2,
|
|
5399
|
+
dependsOn: [],
|
|
5400
|
+
store: "settings",
|
|
5401
|
+
scope: "cluster",
|
|
5402
|
+
onFailure: "tolerate",
|
|
5403
|
+
bodyKind: "procedure",
|
|
5404
|
+
description: "Copy each exportTo:<broker> and mediaRole:<broker> key to the settings of the integration that links the broker",
|
|
5405
|
+
fixtureMark: "the broker link stored beside the flags of an integration"
|
|
5406
|
+
},
|
|
5407
|
+
procedure: copyFlagsToIntegrations
|
|
5408
|
+
},
|
|
5409
|
+
{
|
|
5410
|
+
meta: {
|
|
5411
|
+
id: "homeassistant-export/2026-11-03-drop-per-broker-flags",
|
|
5412
|
+
owner: "homeassistant-export",
|
|
5413
|
+
level: "addon-state",
|
|
5414
|
+
seq: 3,
|
|
5415
|
+
dependsOn: ["homeassistant-export/2026-11-03-per-broker-flags-to-integration"],
|
|
5416
|
+
store: "settings",
|
|
5417
|
+
scope: "cluster",
|
|
5418
|
+
onFailure: "tolerate",
|
|
5419
|
+
bodyKind: "plan",
|
|
5420
|
+
description: "Delete the per-broker keys once the integration shows the same flag",
|
|
5421
|
+
fixtureMark: "a legacy flag parked when the integration holds a different one"
|
|
5422
|
+
},
|
|
5423
|
+
plan: planDropPerBrokerFlags
|
|
5424
|
+
}
|
|
5425
|
+
];
|
|
5426
|
+
//#endregion
|
|
5427
|
+
//#region src/ha-export/instance-flags.ts
|
|
5428
|
+
var exportKey = (brokerId) => `exportTo:${brokerId}`;
|
|
5429
|
+
var KEYS = [{
|
|
5430
|
+
flag: "exportTo",
|
|
5431
|
+
keyOf: exportKey
|
|
5432
|
+
}, {
|
|
5433
|
+
flag: "mediaRole",
|
|
5434
|
+
keyOf: mediaRoleKey
|
|
5435
|
+
}];
|
|
5436
|
+
function splitInstanceKeys(patch, instances) {
|
|
5437
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
5438
|
+
const toInstances = /* @__PURE__ */ new Map();
|
|
5439
|
+
for (const row of instances) {
|
|
5440
|
+
if (row.brokerId === null) continue;
|
|
5441
|
+
for (const { flag, keyOf } of KEYS) {
|
|
5442
|
+
const key = keyOf(row.brokerId);
|
|
5443
|
+
if (!(key in patch)) continue;
|
|
5444
|
+
claimed.add(key);
|
|
5445
|
+
toInstances.set(row.integrationId, {
|
|
5446
|
+
...toInstances.get(row.integrationId),
|
|
5447
|
+
[flag]: patch[key]
|
|
5448
|
+
});
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
return {
|
|
5452
|
+
toInstances,
|
|
5453
|
+
rest: Object.fromEntries(Object.entries(patch).filter(([key]) => !claimed.has(key)))
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5456
|
+
/** The form's dynamic keys carrying each instance's own value. */
|
|
5457
|
+
function instanceOverlay(instances) {
|
|
5458
|
+
const overlay = {};
|
|
5459
|
+
for (const row of instances) {
|
|
5460
|
+
if (row.brokerId === null) continue;
|
|
5461
|
+
for (const { flag, keyOf } of KEYS) {
|
|
5462
|
+
const value = row.settings[flag];
|
|
5463
|
+
if (value !== void 0) overlay[keyOf(row.brokerId)] = value;
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
return overlay;
|
|
5467
|
+
}
|
|
5468
|
+
//#endregion
|
|
5262
5469
|
//#region src/ha-export/node-reads.ts
|
|
5263
5470
|
var UNREAD = {
|
|
5264
5471
|
footageUsedBytes: null,
|
|
@@ -5970,7 +6177,7 @@ function reduceLastTriggers(rows) {
|
|
|
5970
6177
|
function macroOf(className) {
|
|
5971
6178
|
const lower = className.toLowerCase();
|
|
5972
6179
|
if (EXPORTED_MACROS.includes(lower)) return lower;
|
|
5973
|
-
const mapped =
|
|
6180
|
+
const mapped = require_ha_instance_settings.COCO_TO_MACRO.mapping[lower];
|
|
5974
6181
|
return typeof mapped === "string" ? mapped : null;
|
|
5975
6182
|
}
|
|
5976
6183
|
function iso(timestamp) {
|
|
@@ -7119,7 +7326,7 @@ var ExportedEntities = class {
|
|
|
7119
7326
|
this.ports.logger.warn("homeassistant-export: exported entities not reconciled — a list was unanswered", { meta: { error: errMsg$1(err) } });
|
|
7120
7327
|
return;
|
|
7121
7328
|
}
|
|
7122
|
-
const { dropped } =
|
|
7329
|
+
const { dropped } = require_ha_instance_settings.pruneDeletedEntities([...this.state.actions.map((e) => ({
|
|
7123
7330
|
kind: "action",
|
|
7124
7331
|
actionId: e.actionId
|
|
7125
7332
|
})), ...this.state.targets.map((e) => ({
|
|
@@ -7191,7 +7398,7 @@ var ExportedEntities = class {
|
|
|
7191
7398
|
const action = this.state.actions.find((e) => actionEntity(e.actionId) === entity);
|
|
7192
7399
|
if (action !== void 0) return {
|
|
7193
7400
|
handled: true,
|
|
7194
|
-
applied: (await
|
|
7401
|
+
applied: (await require_ha_instance_settings.runExportedAction(this.ports.api, this.ports.logger, {
|
|
7195
7402
|
exporter: EXPORTER,
|
|
7196
7403
|
actionId: action.actionId
|
|
7197
7404
|
})).status === "started"
|
|
@@ -7199,7 +7406,7 @@ var ExportedEntities = class {
|
|
|
7199
7406
|
const target = this.state.targets.find((e) => targetEntity(e.targetId) === entity);
|
|
7200
7407
|
if (target !== void 0) return {
|
|
7201
7408
|
handled: true,
|
|
7202
|
-
applied: (await
|
|
7409
|
+
applied: (await require_ha_instance_settings.sendToExportedTarget(this.ports.api, this.ports.logger, {
|
|
7203
7410
|
exporter: EXPORTER,
|
|
7204
7411
|
targetId: target.targetId,
|
|
7205
7412
|
notification: { body: value }
|
|
@@ -7250,7 +7457,7 @@ var SUPPORTED_DEVICE_KINDS = [
|
|
|
7250
7457
|
"image",
|
|
7251
7458
|
"pet-feeder",
|
|
7252
7459
|
"hub",
|
|
7253
|
-
...
|
|
7460
|
+
...require_ha_instance_settings.PLUG_APPLIANCE_TYPES
|
|
7254
7461
|
];
|
|
7255
7462
|
//#endregion
|
|
7256
7463
|
//#region src/ha-export/ha-export.addon.ts
|
|
@@ -7320,7 +7527,7 @@ var DEFAULT_CONFIG = {
|
|
|
7320
7527
|
targets: []
|
|
7321
7528
|
}
|
|
7322
7529
|
};
|
|
7323
|
-
var HaExportAddon = class extends
|
|
7530
|
+
var HaExportAddon = class extends require_ha_instance_settings.BaseAddon {
|
|
7324
7531
|
links = /* @__PURE__ */ new Map();
|
|
7325
7532
|
/** deviceKey → the device it addresses. Rebuilt on every reconcile. */
|
|
7326
7533
|
exported = /* @__PURE__ */ new Map();
|
|
@@ -7426,8 +7633,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7426
7633
|
super({ ...DEFAULT_CONFIG });
|
|
7427
7634
|
}
|
|
7428
7635
|
async onInitialize() {
|
|
7429
|
-
await this.runMigrations(
|
|
7430
|
-
|
|
7636
|
+
await this.runMigrations(require_ha_instance_settings.defineAddonMigrations(ADDON_MIGRATIONS, {
|
|
7637
|
+
readStored: async () => this.resolveGlobalStore(),
|
|
7638
|
+
...this.instancePorts()
|
|
7639
|
+
}));
|
|
7640
|
+
const secret = this.state("haExportMediaSecret", require_ha_instance_settings.string(), "");
|
|
7431
7641
|
const existing = await secret.get();
|
|
7432
7642
|
if (existing.length === 0) {
|
|
7433
7643
|
const minted = randomSecret();
|
|
@@ -7460,7 +7670,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7460
7670
|
this.exportedEntities = exportedEntities;
|
|
7461
7671
|
return [
|
|
7462
7672
|
{
|
|
7463
|
-
capability:
|
|
7673
|
+
capability: require_ha_instance_settings.deviceExportCapability,
|
|
7464
7674
|
provider: {
|
|
7465
7675
|
getStatus: async () => this.buildStatus(),
|
|
7466
7676
|
listSupportedDeviceKinds: async () => [...SUPPORTED_DEVICE_KINDS],
|
|
@@ -7493,8 +7703,8 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7493
7703
|
}
|
|
7494
7704
|
},
|
|
7495
7705
|
{
|
|
7496
|
-
capability:
|
|
7497
|
-
provider:
|
|
7706
|
+
capability: require_ha_instance_settings.addonRoutesCapability,
|
|
7707
|
+
provider: require_ha_instance_settings.buildAddonRouteProvider(ADDON_ID, [{
|
|
7498
7708
|
method: "POST",
|
|
7499
7709
|
path: "/command",
|
|
7500
7710
|
access: "authenticated",
|
|
@@ -7505,27 +7715,56 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7505
7715
|
}])
|
|
7506
7716
|
},
|
|
7507
7717
|
{
|
|
7508
|
-
capability:
|
|
7718
|
+
capability: require_ha_instance_settings.oauthIntegrationCapability,
|
|
7509
7719
|
provider: { getDescriptor: async () => HOME_ASSISTANT_OAUTH_INTEGRATION }
|
|
7510
7720
|
}
|
|
7511
7721
|
];
|
|
7512
7722
|
}
|
|
7723
|
+
/**
|
|
7724
|
+
* The form of the addon shows each instance's own switch and media role: the values sit on its
|
|
7725
|
+
* integration (D985), overlaid on the legacy keys the store may still hold.
|
|
7726
|
+
*/
|
|
7727
|
+
async getGlobalSettings(overlay, cap, nodeId) {
|
|
7728
|
+
const rows = await require_ha_instance_settings.listInstancesOrNothing(this.instancePorts(), (error) => this.ctx.logger.warn("ha-export: the instance settings could not be read for the form", { meta: { error } }));
|
|
7729
|
+
return super.getGlobalSettings({
|
|
7730
|
+
...overlay,
|
|
7731
|
+
...instanceOverlay(rows)
|
|
7732
|
+
}, cap, nodeId);
|
|
7733
|
+
}
|
|
7734
|
+
/**
|
|
7735
|
+
* An operator write: the `exportTo:` / `mediaRole:` keys go to the integration of their broker
|
|
7736
|
+
* (D985), everything else to the addon store as before. A key whose broker has no integration
|
|
7737
|
+
* stays in the store.
|
|
7738
|
+
*/
|
|
7739
|
+
async updateGlobalSettingsAsOperator(patch, nodeId) {
|
|
7740
|
+
const ports = this.instancePorts();
|
|
7741
|
+
const { toInstances, rest } = splitInstanceKeys(patch, await ports.listInstances());
|
|
7742
|
+
for (const [integrationId, flags] of toInstances) {
|
|
7743
|
+
if (await ports.patchInstanceSettings(integrationId, flags)) continue;
|
|
7744
|
+
throw new Error(`ha-export: integration ${integrationId} is gone — the setting was not saved`);
|
|
7745
|
+
}
|
|
7746
|
+
if (Object.keys(rest).length > 0 || toInstances.size === 0) {
|
|
7747
|
+
await super.updateGlobalSettingsAsOperator(rest, nodeId);
|
|
7748
|
+
return;
|
|
7749
|
+
}
|
|
7750
|
+
await this.onConfigChanged();
|
|
7751
|
+
}
|
|
7513
7752
|
async onConfigChanged() {
|
|
7514
7753
|
for (const link of this.links.values()) link.client.resetBackoff();
|
|
7515
7754
|
await this.reconcile("config-changed");
|
|
7516
7755
|
this.startTimer();
|
|
7517
7756
|
}
|
|
7518
7757
|
wireListeners() {
|
|
7519
|
-
this.subscribe({ category:
|
|
7758
|
+
this.subscribe({ category: require_ha_instance_settings.EventCategory.DeviceStateChanged }, (event) => {
|
|
7520
7759
|
this.onSliceChanged(event.data);
|
|
7521
7760
|
});
|
|
7522
|
-
this.subscribe({ category:
|
|
7761
|
+
this.subscribe({ category: require_ha_instance_settings.EventCategory.DeviceOnline }, (event) => {
|
|
7523
7762
|
this.onOnlineChanged(event.data, true);
|
|
7524
7763
|
});
|
|
7525
|
-
this.subscribe({ category:
|
|
7764
|
+
this.subscribe({ category: require_ha_instance_settings.EventCategory.DeviceOffline }, (event) => {
|
|
7526
7765
|
this.onOnlineChanged(event.data, false);
|
|
7527
7766
|
});
|
|
7528
|
-
this.subscribe({ category:
|
|
7767
|
+
this.subscribe({ category: require_ha_instance_settings.EventCategory.MotionOnMotionChanged }, (event) => {
|
|
7529
7768
|
this.onMotion(event.data);
|
|
7530
7769
|
});
|
|
7531
7770
|
/**
|
|
@@ -7534,13 +7773,13 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7534
7773
|
* the entity existed and effectively never moved. It also carries the
|
|
7535
7774
|
* window LEVEL, which nothing else on the export path does.
|
|
7536
7775
|
*/
|
|
7537
|
-
this.subscribe({ category:
|
|
7776
|
+
this.subscribe({ category: require_ha_instance_settings.EventCategory.PipelineAudioInferenceResult }, (event) => {
|
|
7538
7777
|
this.onAudioWindow(event.data);
|
|
7539
7778
|
});
|
|
7540
|
-
this.subscribe({ category:
|
|
7779
|
+
this.subscribe({ category: require_ha_instance_settings.EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {
|
|
7541
7780
|
this.onTrackLifecycle(event.data);
|
|
7542
7781
|
});
|
|
7543
|
-
this.subscribe({ category:
|
|
7782
|
+
this.subscribe({ category: require_ha_instance_settings.EventCategory.ZoneAnalyticsOccupancyChanged }, (event) => {
|
|
7544
7783
|
this.onZoneOccupancy(event.data);
|
|
7545
7784
|
});
|
|
7546
7785
|
/**
|
|
@@ -7549,11 +7788,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7549
7788
|
* rather than a state push.
|
|
7550
7789
|
*/
|
|
7551
7790
|
for (const category of [
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7791
|
+
require_ha_instance_settings.EventCategory.DeviceRegistered,
|
|
7792
|
+
require_ha_instance_settings.EventCategory.DeviceUnregistered,
|
|
7793
|
+
require_ha_instance_settings.EventCategory.DeviceBindingsChanged,
|
|
7794
|
+
require_ha_instance_settings.EventCategory.DeviceMetaChanged,
|
|
7795
|
+
require_ha_instance_settings.EventCategory.BrokerStatusChanged
|
|
7557
7796
|
]) this.subscribe({ category }, () => {
|
|
7558
7797
|
this.reconcileSoon();
|
|
7559
7798
|
});
|
|
@@ -8665,7 +8904,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
8665
8904
|
*/
|
|
8666
8905
|
capSlices: snapshot,
|
|
8667
8906
|
itemEntities: items.entities,
|
|
8668
|
-
...
|
|
8907
|
+
...require_ha_instance_settings.isReadingQuantity(readingQuantity) ? { readingQuantity } : {}
|
|
8669
8908
|
},
|
|
8670
8909
|
heldBack
|
|
8671
8910
|
};
|
|
@@ -8899,7 +9138,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
8899
9138
|
maskEnabled: privacySlice["enabled"] === true,
|
|
8900
9139
|
audioEnabled: typeof privacySlice["audioEnabled"] === "boolean" ? privacySlice["audioEnabled"] : null
|
|
8901
9140
|
};
|
|
8902
|
-
const brokerAudioMuted = sourceNodeId === null ? null : await this.ctx.api.streamBroker.getDeviceAudioMute.query({ deviceId },
|
|
9141
|
+
const brokerAudioMuted = sourceNodeId === null ? null : await this.ctx.api.streamBroker.getDeviceAudioMute.query({ deviceId }, require_ha_instance_settings.nodePin(sourceNodeId)).then((r) => r.muted).catch((err) => {
|
|
8903
9142
|
this.ctx.logger.warn("ha-export: could not read broker audio mute, the broker-audio function degrades", {
|
|
8904
9143
|
tags: { deviceId },
|
|
8905
9144
|
meta: {
|
|
@@ -8950,7 +9189,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
8950
9189
|
* reasoning.
|
|
8951
9190
|
*/
|
|
8952
9191
|
async refreshBrokers() {
|
|
8953
|
-
const haBrokers = await
|
|
9192
|
+
const haBrokers = await require_ha_instance_settings.listHomeAssistantBrokers((input) => this.ctx.api.broker.list.query(input));
|
|
8954
9193
|
const known = haBrokers.map((broker) => ({
|
|
8955
9194
|
id: broker.id,
|
|
8956
9195
|
name: broker.name
|
|
@@ -9149,7 +9388,20 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
9149
9388
|
enabled = [];
|
|
9150
9389
|
async refreshEnabledBrokers(known) {
|
|
9151
9390
|
const store = await this.resolveGlobalStore();
|
|
9152
|
-
this.enabled = enabledBrokerIds(store, known.map((broker) => broker.id));
|
|
9391
|
+
this.enabled = enabledBrokerIds(store, known.map((broker) => broker.id), await this.readInstanceSettings());
|
|
9392
|
+
}
|
|
9393
|
+
/** The hub's `integrations` capability, as the one reader and writer of an instance's settings. */
|
|
9394
|
+
instancePorts() {
|
|
9395
|
+
return require_ha_instance_settings.haInstanceSettingsPortsOver(this.ctx.api.integrations);
|
|
9396
|
+
}
|
|
9397
|
+
/**
|
|
9398
|
+
* Each broker's integration settings (D985): the switch and the media role of an instance.
|
|
9399
|
+
* A read that fails answers nothing, so every broker falls back to its legacy key and the
|
|
9400
|
+
* failure is logged — never read as "everything off".
|
|
9401
|
+
*/
|
|
9402
|
+
async readInstanceSettings() {
|
|
9403
|
+
const rows = await require_ha_instance_settings.listInstancesOrNothing(this.instancePorts(), (error) => this.ctx.logger.warn("ha-export: the instance settings could not be read — the legacy per-broker keys answer", { meta: { error } }));
|
|
9404
|
+
return rows.length === 0 ? NO_INSTANCE_SETTINGS : require_ha_instance_settings.settingsByBroker(rows);
|
|
9153
9405
|
}
|
|
9154
9406
|
enabledBrokerIds() {
|
|
9155
9407
|
return this.enabled;
|
|
@@ -9310,7 +9562,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
9310
9562
|
}
|
|
9311
9563
|
switch (command.kind) {
|
|
9312
9564
|
case "camera-function": {
|
|
9313
|
-
const parsed =
|
|
9565
|
+
const parsed = require_ha_instance_settings.CameraSwitchIdSchema.safeParse(command.switchId);
|
|
9314
9566
|
if (!parsed.success) {
|
|
9315
9567
|
this.ctx.logger.warn("ha-export: dropping a command for an unknown camera function", {
|
|
9316
9568
|
tags: { deviceId: command.deviceId },
|
|
@@ -9550,7 +9802,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
9550
9802
|
async mediaBasesFor(brokerIds, deviceId) {
|
|
9551
9803
|
let bases;
|
|
9552
9804
|
try {
|
|
9553
|
-
bases = mediaBasesByBroker(await this.resolveGlobalStore(), brokerIds, await this.ctx.api.localNetwork.getAddresses.query());
|
|
9805
|
+
bases = mediaBasesByBroker(await this.resolveGlobalStore(), brokerIds, await this.ctx.api.localNetwork.getAddresses.query(), await this.readInstanceSettings());
|
|
9554
9806
|
} catch (err) {
|
|
9555
9807
|
this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
|
|
9556
9808
|
tags: { deviceId },
|