@camstack/addon-provider-homeassistant 1.2.646 → 1.2.648
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 +12 -0
- package/README.md +1 -1
- package/dist/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/{ha-broker-source-gQYTcasw.js → ha-broker-source-Ba20mhmb.js} +361 -10
- package/dist/{ha-broker-source-DRIYmtMd.mjs → ha-broker-source-iLA_DsW8.mjs} +350 -11
- package/dist/ha-export/ha-export.addon.js +141 -60
- package/dist/ha-export/ha-export.addon.mjs +141 -60
- package/i18n/en.json +0 -2
- package/i18n/it.json +0 -2
- 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 require_ha_broker_source = require("../ha-broker-source-
|
|
27
|
+
const require_ha_broker_source = require("../ha-broker-source-Ba20mhmb.js");
|
|
28
28
|
let node_crypto = require("node:crypto");
|
|
29
29
|
require("node:zlib");
|
|
30
30
|
let node_fs = require("node:fs");
|
|
@@ -3485,6 +3485,10 @@ function controlEntitySpecs(slice, quantity) {
|
|
|
3485
3485
|
}, kindExtra];
|
|
3486
3486
|
}
|
|
3487
3487
|
var CAPS_NOT_EXPORTED = [
|
|
3488
|
+
{
|
|
3489
|
+
cap: "do-not-disturb",
|
|
3490
|
+
reason: "A vendor switch of a speaker, provided so far only by an imported Echo: its media player is the entity a home-automation front end needs, and no entity kind has been chosen for the Do Not Disturb switch yet."
|
|
3491
|
+
},
|
|
3488
3492
|
{
|
|
3489
3493
|
cap: "timed-run",
|
|
3490
3494
|
reason: "A hub-side timer for a device that has none: a \"run for N minutes\" command and the deadline of the pending switch-off. The switch or pump it drives is already exported through `switch`, and the deadline is a countdown, not a value an entity can hold."
|
|
@@ -4936,6 +4940,49 @@ async function call(slice, capName, make) {
|
|
|
4936
4940
|
}
|
|
4937
4941
|
return { ok: true };
|
|
4938
4942
|
}
|
|
4943
|
+
//#endregion
|
|
4944
|
+
//#region src/ha-export/migrations/drop-public-base-url.ts
|
|
4945
|
+
/**
|
|
4946
|
+
* The media base is no longer a setting of this addon (D986 §5): each Home Assistant
|
|
4947
|
+
* instance chooses a hub address ROLE (`mediaRole:<brokerId>`) and the address is read from
|
|
4948
|
+
* the palette on every use. The `publicBaseUrl` key older versions persisted is deleted.
|
|
4949
|
+
* Every other key is left exactly as stored.
|
|
4950
|
+
*
|
|
4951
|
+
* Idempotent: a store without the key yields an empty plan, which writes nothing.
|
|
4952
|
+
*/
|
|
4953
|
+
/** The settings scope of this addon (`<addonId>:addon-settings`). */
|
|
4954
|
+
var HA_EXPORT_SETTINGS_SCOPE = "homeassistant-export:addon-settings";
|
|
4955
|
+
var RETIRED_URL_KEYS = ["publicBaseUrl"];
|
|
4956
|
+
async function planDropPublicBaseUrl(ports) {
|
|
4957
|
+
const stored = await ports.readStored();
|
|
4958
|
+
const present = RETIRED_URL_KEYS.filter((key) => key in stored);
|
|
4959
|
+
if (present.length === 0) return { edits: [] };
|
|
4960
|
+
return { edits: [{
|
|
4961
|
+
op: "deleteKeys",
|
|
4962
|
+
scope: HA_EXPORT_SETTINGS_SCOPE,
|
|
4963
|
+
keys: [...present],
|
|
4964
|
+
precondition: require_ha_broker_source.preconditionOf(Object.fromEntries(present.map((key) => [key, stored[key]])))
|
|
4965
|
+
}] };
|
|
4966
|
+
}
|
|
4967
|
+
//#endregion
|
|
4968
|
+
//#region src/ha-export/migrations/index.ts
|
|
4969
|
+
/** The `homeassistant-export` catalogue (D987): offered to the engine when the addon initialises. */
|
|
4970
|
+
var ADDON_MIGRATIONS = [{
|
|
4971
|
+
meta: {
|
|
4972
|
+
id: "homeassistant-export/2026-11-02-drop-public-base-url",
|
|
4973
|
+
owner: "homeassistant-export",
|
|
4974
|
+
level: "addon-state",
|
|
4975
|
+
seq: 1,
|
|
4976
|
+
dependsOn: [],
|
|
4977
|
+
store: "settings",
|
|
4978
|
+
scope: "cluster",
|
|
4979
|
+
onFailure: "tolerate",
|
|
4980
|
+
bodyKind: "plan",
|
|
4981
|
+
description: "Delete the retired publicBaseUrl setting; each instance now chooses a hub address role",
|
|
4982
|
+
fixtureMark: "the reconcile interval the migration does not know"
|
|
4983
|
+
},
|
|
4984
|
+
plan: planDropPublicBaseUrl
|
|
4985
|
+
}];
|
|
4939
4986
|
process.platform, process.env["SystemRoot"];
|
|
4940
4987
|
/**
|
|
4941
4988
|
* The ONE derivation of "a signed, expiring URL".
|
|
@@ -5015,15 +5062,26 @@ function verifyExpiringUrl(input) {
|
|
|
5015
5062
|
* an expired link is refused whether or not its signature is valid, so a leaked
|
|
5016
5063
|
* URL stops working on its own.
|
|
5017
5064
|
*/
|
|
5065
|
+
/** The settings key that carries one broker's media role (per-instance, like `exportTo:`). */
|
|
5066
|
+
function mediaRoleKey(brokerId) {
|
|
5067
|
+
return `mediaRole:${brokerId}`;
|
|
5068
|
+
}
|
|
5069
|
+
/** A broker's chosen role; anything stored that is not a known role is `internal`. */
|
|
5070
|
+
function haMediaRoleOf(store, brokerId) {
|
|
5071
|
+
return store[mediaRoleKey(brokerId)] === "publicEndpoint" ? "publicEndpoint" : "internal";
|
|
5072
|
+
}
|
|
5018
5073
|
/**
|
|
5019
|
-
* The base a media link is minted on
|
|
5020
|
-
*
|
|
5021
|
-
*
|
|
5022
|
-
*
|
|
5023
|
-
* endpoint, and never loopback (`null` when nothing qualifies).
|
|
5074
|
+
* The base a media link is minted on: the hub's role (D734) — `internal` unless the
|
|
5075
|
+
* broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
|
|
5076
|
+
* when the role resolves to nothing): a link Home Assistant cannot open is worse
|
|
5077
|
+
* than no image.
|
|
5024
5078
|
*/
|
|
5025
|
-
function haMediaBaseUrl(book) {
|
|
5026
|
-
return book.roles.
|
|
5079
|
+
function haMediaBaseUrl(book, role = "internal") {
|
|
5080
|
+
return book.roles[role].resolved;
|
|
5081
|
+
}
|
|
5082
|
+
/** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
|
|
5083
|
+
function mediaBasesByBroker(store, brokerIds, book) {
|
|
5084
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, haMediaBaseUrl(book, haMediaRoleOf(store, brokerId))]));
|
|
5027
5085
|
}
|
|
5028
5086
|
/** How long a minted link stays valid. Long enough for HA to render it. */
|
|
5029
5087
|
var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
|
|
@@ -7164,8 +7222,7 @@ var DEFAULT_CONFIG = {
|
|
|
7164
7222
|
exportedEntities: {
|
|
7165
7223
|
actions: [],
|
|
7166
7224
|
targets: []
|
|
7167
|
-
}
|
|
7168
|
-
publicBaseUrl: ""
|
|
7225
|
+
}
|
|
7169
7226
|
};
|
|
7170
7227
|
var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
7171
7228
|
links = /* @__PURE__ */ new Map();
|
|
@@ -7174,7 +7231,6 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7174
7231
|
/** numeric deviceId → deviceKey, for the event path. */
|
|
7175
7232
|
keyByDeviceId = /* @__PURE__ */ new Map();
|
|
7176
7233
|
mediaSecret = "";
|
|
7177
|
-
mediaBaseUrl = null;
|
|
7178
7234
|
reconcileTimer = null;
|
|
7179
7235
|
reconcileInFlight = false;
|
|
7180
7236
|
/** A reason asked for while one was running. Re-run, never dropped. */
|
|
@@ -7274,6 +7330,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7274
7330
|
super({ ...DEFAULT_CONFIG });
|
|
7275
7331
|
}
|
|
7276
7332
|
async onInitialize() {
|
|
7333
|
+
await this.runMigrations(require_ha_broker_source.defineAddonMigrations(ADDON_MIGRATIONS, { readStored: async () => this.resolveGlobalStore() }));
|
|
7277
7334
|
const secret = this.state("haExportMediaSecret", require_ha_broker_source.string(), "");
|
|
7278
7335
|
const existing = await secret.get();
|
|
7279
7336
|
if (existing.length === 0) {
|
|
@@ -7674,32 +7731,39 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7674
7731
|
const lastSeen = readNumber(data, "lastSeen") ?? Date.now();
|
|
7675
7732
|
const zonesVisited = readStringArray(data, "zonesVisited");
|
|
7676
7733
|
const fusedIntoTrackId = readString(data, "fusedIntoTrackId");
|
|
7677
|
-
const
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
return;
|
|
7734
|
+
const exported = this.exported.get(deviceKey);
|
|
7735
|
+
if (exported === void 0) return;
|
|
7736
|
+
const bases = await this.mediaBasesFor(exported.brokerIds, deviceId);
|
|
7737
|
+
let produced = false;
|
|
7738
|
+
for (const brokerId of exported.brokerIds) {
|
|
7739
|
+
const link = this.links.get(brokerId);
|
|
7740
|
+
if (link === void 0) continue;
|
|
7741
|
+
const base = bases.get(brokerId) ?? null;
|
|
7742
|
+
const values = projectTrackLifecycle(deviceKey, {
|
|
7743
|
+
deviceId,
|
|
7744
|
+
trackId,
|
|
7745
|
+
phase,
|
|
7746
|
+
classes,
|
|
7747
|
+
...bestClassName !== null ? { bestClassName } : {},
|
|
7748
|
+
...label !== null ? { label } : {},
|
|
7749
|
+
...plateText !== null ? { plateText } : {},
|
|
7750
|
+
lastSeen,
|
|
7751
|
+
zonesVisited,
|
|
7752
|
+
...fusedIntoTrackId !== null ? { fusedIntoTrackId } : {}
|
|
7753
|
+
}, base === null ? null : this.mintTrackMediaUrl(trackId, deviceId, base));
|
|
7754
|
+
if (values.length === 0) continue;
|
|
7755
|
+
produced = true;
|
|
7756
|
+
for (const value of values) link.client.publishState(value.topic, value.value);
|
|
7701
7757
|
}
|
|
7702
|
-
this.
|
|
7758
|
+
if (!produced) this.ctx.logger.debug("ha-export: track produced no exportable state", {
|
|
7759
|
+
tags: { deviceId },
|
|
7760
|
+
meta: {
|
|
7761
|
+
trackId,
|
|
7762
|
+
bestClassName,
|
|
7763
|
+
phase,
|
|
7764
|
+
fusedIntoTrackId
|
|
7765
|
+
}
|
|
7766
|
+
});
|
|
7703
7767
|
}
|
|
7704
7768
|
onAudioWindow(data) {
|
|
7705
7769
|
const deviceId = readNumber(data, "deviceId");
|
|
@@ -7844,7 +7908,6 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7844
7908
|
await this.refreshBrokers();
|
|
7845
7909
|
const fullResync = this.resync.take(reason);
|
|
7846
7910
|
let readsHeld = false;
|
|
7847
|
-
await this.refreshMediaBaseUrl();
|
|
7848
7911
|
await this.refreshComponentSupport();
|
|
7849
7912
|
const enabled = this.enabledBrokerIds();
|
|
7850
7913
|
const devices = await this.ctx.api.deviceManager.listAll.query({});
|
|
@@ -9321,28 +9384,41 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
9321
9384
|
* cannot open is worse than no image, because it renders as a broken
|
|
9322
9385
|
* entity rather than an empty one.
|
|
9323
9386
|
*/
|
|
9324
|
-
mintTrackMediaUrl(trackId, deviceId) {
|
|
9325
|
-
if (this.
|
|
9387
|
+
mintTrackMediaUrl(trackId, deviceId, baseUrl) {
|
|
9388
|
+
if (this.mediaSecret.length === 0) return null;
|
|
9326
9389
|
return buildMediaUrl({
|
|
9327
|
-
baseUrl
|
|
9390
|
+
baseUrl,
|
|
9328
9391
|
routePrefix: MEDIA_ROUTE_PREFIX,
|
|
9329
9392
|
id: `${String(deviceId)}:${trackId}`,
|
|
9330
9393
|
secret: this.mediaSecret,
|
|
9331
9394
|
expMs: Date.now() + MEDIA_URL_TTL_MS
|
|
9332
9395
|
});
|
|
9333
9396
|
}
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9397
|
+
/**
|
|
9398
|
+
* Each broker's media base, read per use: the palette's role the broker chose
|
|
9399
|
+
* (`mediaRole:<brokerId>`, `internal` by default). A role that resolves to nothing
|
|
9400
|
+
* is `null` and says so — the image entities then carry no value, which reads as
|
|
9401
|
+
* empty rather than as a broken link.
|
|
9402
|
+
*/
|
|
9403
|
+
async mediaBasesFor(brokerIds, deviceId) {
|
|
9404
|
+
let bases;
|
|
9339
9405
|
try {
|
|
9340
|
-
|
|
9406
|
+
bases = mediaBasesByBroker(await this.resolveGlobalStore(), brokerIds, await this.ctx.api.localNetwork.getAddresses.query());
|
|
9341
9407
|
} catch (err) {
|
|
9342
|
-
this.
|
|
9343
|
-
|
|
9408
|
+
this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
|
|
9409
|
+
tags: { deviceId },
|
|
9410
|
+
meta: { error: errMsg(err) }
|
|
9411
|
+
});
|
|
9412
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, null]));
|
|
9413
|
+
}
|
|
9414
|
+
for (const brokerId of brokerIds) {
|
|
9415
|
+
if (bases.get(brokerId) !== null) continue;
|
|
9416
|
+
this.ctx.logger.warn("ha-export: the broker media role resolves to no hub address — image entities carry no value until the Network page names one", {
|
|
9417
|
+
tags: { deviceId },
|
|
9418
|
+
meta: { brokerId }
|
|
9419
|
+
});
|
|
9344
9420
|
}
|
|
9345
|
-
|
|
9421
|
+
return bases;
|
|
9346
9422
|
}
|
|
9347
9423
|
/**
|
|
9348
9424
|
* The cap-level expose/unexpose applies to EVERY enabled broker.
|
|
@@ -9396,21 +9472,26 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
9396
9472
|
variant: "warning"
|
|
9397
9473
|
}]
|
|
9398
9474
|
},
|
|
9399
|
-
{
|
|
9475
|
+
...this.config.knownBrokers.length > 0 ? [{
|
|
9400
9476
|
id: "ha-export-transport",
|
|
9401
9477
|
title: "Connection",
|
|
9402
9478
|
titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
|
|
9479
|
+
description: "The hub address each Home Assistant instance fetches image links from. Internal is the address flagged Internal on the Network page; choose Public endpoint for an instance outside your network.",
|
|
9403
9480
|
columns: 1,
|
|
9404
|
-
fields:
|
|
9405
|
-
type: "
|
|
9406
|
-
key:
|
|
9407
|
-
label:
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9481
|
+
fields: this.config.knownBrokers.map((broker) => ({
|
|
9482
|
+
type: "select",
|
|
9483
|
+
key: mediaRoleKey(broker.id),
|
|
9484
|
+
label: `Image links for ${broker.name}`,
|
|
9485
|
+
options: [{
|
|
9486
|
+
value: "internal",
|
|
9487
|
+
label: "Internal (LAN address)"
|
|
9488
|
+
}, {
|
|
9489
|
+
value: "publicEndpoint",
|
|
9490
|
+
label: "Public endpoint (reachable from outside)"
|
|
9491
|
+
}],
|
|
9492
|
+
default: "internal"
|
|
9493
|
+
}))
|
|
9494
|
+
}] : [],
|
|
9414
9495
|
{
|
|
9415
9496
|
id: "ha-export-advanced",
|
|
9416
9497
|
title: "Advanced",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as consumableKindOf,
|
|
1
|
+
import { B as consumableKindOf, Bt as BaseAddon, Dt as sendToExportedTarget, Ht as baseUnitOf, Jt as isReadingQuantity, K as deviceExportCapability, Wt as defineAddonMigrations, Yt as nodePin, Zt as array, _ as PLUG_APPLIANCE_TYPES, _t as pruneDeletedEntities, c as COCO_TO_MACRO, d as ConsumableItemSchema, f as ConsumableStatusSchema, fn as EventCategory, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, in as string, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as object, on as unknown, pt as preconditionOf, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, ut as oauthIntegrationCapability, x as addonRoutesCapability, xt as runExportedAction, z as consumableFormOf } from "../ha-broker-source-iLA_DsW8.mjs";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import "node:zlib";
|
|
4
4
|
import "node:fs";
|
|
@@ -3450,6 +3450,10 @@ function controlEntitySpecs(slice, quantity) {
|
|
|
3450
3450
|
}, kindExtra];
|
|
3451
3451
|
}
|
|
3452
3452
|
var CAPS_NOT_EXPORTED = [
|
|
3453
|
+
{
|
|
3454
|
+
cap: "do-not-disturb",
|
|
3455
|
+
reason: "A vendor switch of a speaker, provided so far only by an imported Echo: its media player is the entity a home-automation front end needs, and no entity kind has been chosen for the Do Not Disturb switch yet."
|
|
3456
|
+
},
|
|
3453
3457
|
{
|
|
3454
3458
|
cap: "timed-run",
|
|
3455
3459
|
reason: "A hub-side timer for a device that has none: a \"run for N minutes\" command and the deadline of the pending switch-off. The switch or pump it drives is already exported through `switch`, and the deadline is a countdown, not a value an entity can hold."
|
|
@@ -4901,6 +4905,49 @@ async function call(slice, capName, make) {
|
|
|
4901
4905
|
}
|
|
4902
4906
|
return { ok: true };
|
|
4903
4907
|
}
|
|
4908
|
+
//#endregion
|
|
4909
|
+
//#region src/ha-export/migrations/drop-public-base-url.ts
|
|
4910
|
+
/**
|
|
4911
|
+
* The media base is no longer a setting of this addon (D986 §5): each Home Assistant
|
|
4912
|
+
* instance chooses a hub address ROLE (`mediaRole:<brokerId>`) and the address is read from
|
|
4913
|
+
* the palette on every use. The `publicBaseUrl` key older versions persisted is deleted.
|
|
4914
|
+
* Every other key is left exactly as stored.
|
|
4915
|
+
*
|
|
4916
|
+
* Idempotent: a store without the key yields an empty plan, which writes nothing.
|
|
4917
|
+
*/
|
|
4918
|
+
/** The settings scope of this addon (`<addonId>:addon-settings`). */
|
|
4919
|
+
var HA_EXPORT_SETTINGS_SCOPE = "homeassistant-export:addon-settings";
|
|
4920
|
+
var RETIRED_URL_KEYS = ["publicBaseUrl"];
|
|
4921
|
+
async function planDropPublicBaseUrl(ports) {
|
|
4922
|
+
const stored = await ports.readStored();
|
|
4923
|
+
const present = RETIRED_URL_KEYS.filter((key) => key in stored);
|
|
4924
|
+
if (present.length === 0) return { edits: [] };
|
|
4925
|
+
return { edits: [{
|
|
4926
|
+
op: "deleteKeys",
|
|
4927
|
+
scope: HA_EXPORT_SETTINGS_SCOPE,
|
|
4928
|
+
keys: [...present],
|
|
4929
|
+
precondition: preconditionOf(Object.fromEntries(present.map((key) => [key, stored[key]])))
|
|
4930
|
+
}] };
|
|
4931
|
+
}
|
|
4932
|
+
//#endregion
|
|
4933
|
+
//#region src/ha-export/migrations/index.ts
|
|
4934
|
+
/** The `homeassistant-export` catalogue (D987): offered to the engine when the addon initialises. */
|
|
4935
|
+
var ADDON_MIGRATIONS = [{
|
|
4936
|
+
meta: {
|
|
4937
|
+
id: "homeassistant-export/2026-11-02-drop-public-base-url",
|
|
4938
|
+
owner: "homeassistant-export",
|
|
4939
|
+
level: "addon-state",
|
|
4940
|
+
seq: 1,
|
|
4941
|
+
dependsOn: [],
|
|
4942
|
+
store: "settings",
|
|
4943
|
+
scope: "cluster",
|
|
4944
|
+
onFailure: "tolerate",
|
|
4945
|
+
bodyKind: "plan",
|
|
4946
|
+
description: "Delete the retired publicBaseUrl setting; each instance now chooses a hub address role",
|
|
4947
|
+
fixtureMark: "the reconcile interval the migration does not know"
|
|
4948
|
+
},
|
|
4949
|
+
plan: planDropPublicBaseUrl
|
|
4950
|
+
}];
|
|
4904
4951
|
process.platform, process.env["SystemRoot"];
|
|
4905
4952
|
/**
|
|
4906
4953
|
* The ONE derivation of "a signed, expiring URL".
|
|
@@ -4980,15 +5027,26 @@ function verifyExpiringUrl(input) {
|
|
|
4980
5027
|
* an expired link is refused whether or not its signature is valid, so a leaked
|
|
4981
5028
|
* URL stops working on its own.
|
|
4982
5029
|
*/
|
|
5030
|
+
/** The settings key that carries one broker's media role (per-instance, like `exportTo:`). */
|
|
5031
|
+
function mediaRoleKey(brokerId) {
|
|
5032
|
+
return `mediaRole:${brokerId}`;
|
|
5033
|
+
}
|
|
5034
|
+
/** A broker's chosen role; anything stored that is not a known role is `internal`. */
|
|
5035
|
+
function haMediaRoleOf(store, brokerId) {
|
|
5036
|
+
return store[mediaRoleKey(brokerId)] === "publicEndpoint" ? "publicEndpoint" : "internal";
|
|
5037
|
+
}
|
|
4983
5038
|
/**
|
|
4984
|
-
* The base a media link is minted on
|
|
4985
|
-
*
|
|
4986
|
-
*
|
|
4987
|
-
*
|
|
4988
|
-
* endpoint, and never loopback (`null` when nothing qualifies).
|
|
5039
|
+
* The base a media link is minted on: the hub's role (D734) — `internal` unless the
|
|
5040
|
+
* broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
|
|
5041
|
+
* when the role resolves to nothing): a link Home Assistant cannot open is worse
|
|
5042
|
+
* than no image.
|
|
4989
5043
|
*/
|
|
4990
|
-
function haMediaBaseUrl(book) {
|
|
4991
|
-
return book.roles.
|
|
5044
|
+
function haMediaBaseUrl(book, role = "internal") {
|
|
5045
|
+
return book.roles[role].resolved;
|
|
5046
|
+
}
|
|
5047
|
+
/** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
|
|
5048
|
+
function mediaBasesByBroker(store, brokerIds, book) {
|
|
5049
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, haMediaBaseUrl(book, haMediaRoleOf(store, brokerId))]));
|
|
4992
5050
|
}
|
|
4993
5051
|
/** How long a minted link stays valid. Long enough for HA to render it. */
|
|
4994
5052
|
var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
|
|
@@ -7129,8 +7187,7 @@ var DEFAULT_CONFIG = {
|
|
|
7129
7187
|
exportedEntities: {
|
|
7130
7188
|
actions: [],
|
|
7131
7189
|
targets: []
|
|
7132
|
-
}
|
|
7133
|
-
publicBaseUrl: ""
|
|
7190
|
+
}
|
|
7134
7191
|
};
|
|
7135
7192
|
var HaExportAddon = class extends BaseAddon {
|
|
7136
7193
|
links = /* @__PURE__ */ new Map();
|
|
@@ -7139,7 +7196,6 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
7139
7196
|
/** numeric deviceId → deviceKey, for the event path. */
|
|
7140
7197
|
keyByDeviceId = /* @__PURE__ */ new Map();
|
|
7141
7198
|
mediaSecret = "";
|
|
7142
|
-
mediaBaseUrl = null;
|
|
7143
7199
|
reconcileTimer = null;
|
|
7144
7200
|
reconcileInFlight = false;
|
|
7145
7201
|
/** A reason asked for while one was running. Re-run, never dropped. */
|
|
@@ -7239,6 +7295,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
7239
7295
|
super({ ...DEFAULT_CONFIG });
|
|
7240
7296
|
}
|
|
7241
7297
|
async onInitialize() {
|
|
7298
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, { readStored: async () => this.resolveGlobalStore() }));
|
|
7242
7299
|
const secret = this.state("haExportMediaSecret", string(), "");
|
|
7243
7300
|
const existing = await secret.get();
|
|
7244
7301
|
if (existing.length === 0) {
|
|
@@ -7639,32 +7696,39 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
7639
7696
|
const lastSeen = readNumber(data, "lastSeen") ?? Date.now();
|
|
7640
7697
|
const zonesVisited = readStringArray(data, "zonesVisited");
|
|
7641
7698
|
const fusedIntoTrackId = readString(data, "fusedIntoTrackId");
|
|
7642
|
-
const
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
return;
|
|
7699
|
+
const exported = this.exported.get(deviceKey);
|
|
7700
|
+
if (exported === void 0) return;
|
|
7701
|
+
const bases = await this.mediaBasesFor(exported.brokerIds, deviceId);
|
|
7702
|
+
let produced = false;
|
|
7703
|
+
for (const brokerId of exported.brokerIds) {
|
|
7704
|
+
const link = this.links.get(brokerId);
|
|
7705
|
+
if (link === void 0) continue;
|
|
7706
|
+
const base = bases.get(brokerId) ?? null;
|
|
7707
|
+
const values = projectTrackLifecycle(deviceKey, {
|
|
7708
|
+
deviceId,
|
|
7709
|
+
trackId,
|
|
7710
|
+
phase,
|
|
7711
|
+
classes,
|
|
7712
|
+
...bestClassName !== null ? { bestClassName } : {},
|
|
7713
|
+
...label !== null ? { label } : {},
|
|
7714
|
+
...plateText !== null ? { plateText } : {},
|
|
7715
|
+
lastSeen,
|
|
7716
|
+
zonesVisited,
|
|
7717
|
+
...fusedIntoTrackId !== null ? { fusedIntoTrackId } : {}
|
|
7718
|
+
}, base === null ? null : this.mintTrackMediaUrl(trackId, deviceId, base));
|
|
7719
|
+
if (values.length === 0) continue;
|
|
7720
|
+
produced = true;
|
|
7721
|
+
for (const value of values) link.client.publishState(value.topic, value.value);
|
|
7666
7722
|
}
|
|
7667
|
-
this.
|
|
7723
|
+
if (!produced) this.ctx.logger.debug("ha-export: track produced no exportable state", {
|
|
7724
|
+
tags: { deviceId },
|
|
7725
|
+
meta: {
|
|
7726
|
+
trackId,
|
|
7727
|
+
bestClassName,
|
|
7728
|
+
phase,
|
|
7729
|
+
fusedIntoTrackId
|
|
7730
|
+
}
|
|
7731
|
+
});
|
|
7668
7732
|
}
|
|
7669
7733
|
onAudioWindow(data) {
|
|
7670
7734
|
const deviceId = readNumber(data, "deviceId");
|
|
@@ -7809,7 +7873,6 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
7809
7873
|
await this.refreshBrokers();
|
|
7810
7874
|
const fullResync = this.resync.take(reason);
|
|
7811
7875
|
let readsHeld = false;
|
|
7812
|
-
await this.refreshMediaBaseUrl();
|
|
7813
7876
|
await this.refreshComponentSupport();
|
|
7814
7877
|
const enabled = this.enabledBrokerIds();
|
|
7815
7878
|
const devices = await this.ctx.api.deviceManager.listAll.query({});
|
|
@@ -9286,28 +9349,41 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
9286
9349
|
* cannot open is worse than no image, because it renders as a broken
|
|
9287
9350
|
* entity rather than an empty one.
|
|
9288
9351
|
*/
|
|
9289
|
-
mintTrackMediaUrl(trackId, deviceId) {
|
|
9290
|
-
if (this.
|
|
9352
|
+
mintTrackMediaUrl(trackId, deviceId, baseUrl) {
|
|
9353
|
+
if (this.mediaSecret.length === 0) return null;
|
|
9291
9354
|
return buildMediaUrl({
|
|
9292
|
-
baseUrl
|
|
9355
|
+
baseUrl,
|
|
9293
9356
|
routePrefix: MEDIA_ROUTE_PREFIX,
|
|
9294
9357
|
id: `${String(deviceId)}:${trackId}`,
|
|
9295
9358
|
secret: this.mediaSecret,
|
|
9296
9359
|
expMs: Date.now() + MEDIA_URL_TTL_MS
|
|
9297
9360
|
});
|
|
9298
9361
|
}
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9362
|
+
/**
|
|
9363
|
+
* Each broker's media base, read per use: the palette's role the broker chose
|
|
9364
|
+
* (`mediaRole:<brokerId>`, `internal` by default). A role that resolves to nothing
|
|
9365
|
+
* is `null` and says so — the image entities then carry no value, which reads as
|
|
9366
|
+
* empty rather than as a broken link.
|
|
9367
|
+
*/
|
|
9368
|
+
async mediaBasesFor(brokerIds, deviceId) {
|
|
9369
|
+
let bases;
|
|
9304
9370
|
try {
|
|
9305
|
-
|
|
9371
|
+
bases = mediaBasesByBroker(await this.resolveGlobalStore(), brokerIds, await this.ctx.api.localNetwork.getAddresses.query());
|
|
9306
9372
|
} catch (err) {
|
|
9307
|
-
this.
|
|
9308
|
-
|
|
9373
|
+
this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
|
|
9374
|
+
tags: { deviceId },
|
|
9375
|
+
meta: { error: errMsg(err) }
|
|
9376
|
+
});
|
|
9377
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, null]));
|
|
9378
|
+
}
|
|
9379
|
+
for (const brokerId of brokerIds) {
|
|
9380
|
+
if (bases.get(brokerId) !== null) continue;
|
|
9381
|
+
this.ctx.logger.warn("ha-export: the broker media role resolves to no hub address — image entities carry no value until the Network page names one", {
|
|
9382
|
+
tags: { deviceId },
|
|
9383
|
+
meta: { brokerId }
|
|
9384
|
+
});
|
|
9309
9385
|
}
|
|
9310
|
-
|
|
9386
|
+
return bases;
|
|
9311
9387
|
}
|
|
9312
9388
|
/**
|
|
9313
9389
|
* The cap-level expose/unexpose applies to EVERY enabled broker.
|
|
@@ -9361,21 +9437,26 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
9361
9437
|
variant: "warning"
|
|
9362
9438
|
}]
|
|
9363
9439
|
},
|
|
9364
|
-
{
|
|
9440
|
+
...this.config.knownBrokers.length > 0 ? [{
|
|
9365
9441
|
id: "ha-export-transport",
|
|
9366
9442
|
title: "Connection",
|
|
9367
9443
|
titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
|
|
9444
|
+
description: "The hub address each Home Assistant instance fetches image links from. Internal is the address flagged Internal on the Network page; choose Public endpoint for an instance outside your network.",
|
|
9368
9445
|
columns: 1,
|
|
9369
|
-
fields:
|
|
9370
|
-
type: "
|
|
9371
|
-
key:
|
|
9372
|
-
label:
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9446
|
+
fields: this.config.knownBrokers.map((broker) => ({
|
|
9447
|
+
type: "select",
|
|
9448
|
+
key: mediaRoleKey(broker.id),
|
|
9449
|
+
label: `Image links for ${broker.name}`,
|
|
9450
|
+
options: [{
|
|
9451
|
+
value: "internal",
|
|
9452
|
+
label: "Internal (LAN address)"
|
|
9453
|
+
}, {
|
|
9454
|
+
value: "publicEndpoint",
|
|
9455
|
+
label: "Public endpoint (reachable from outside)"
|
|
9456
|
+
}],
|
|
9457
|
+
default: "internal"
|
|
9458
|
+
}))
|
|
9459
|
+
}] : [],
|
|
9379
9460
|
{
|
|
9380
9461
|
id: "ha-export-advanced",
|
|
9381
9462
|
title: "Advanced",
|
package/i18n/en.json
CHANGED
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
"globalSettings.s_ha-export-brokers.title": "Home Assistant instances",
|
|
15
15
|
"globalSettings.s_ha-export-brokers.ha-export___no-brokers.label": "No brokers",
|
|
16
16
|
"globalSettings.s_ha-export-transport.title": "Connection",
|
|
17
|
-
"globalSettings.s_ha-export-transport.publicBaseUrl.label": "Hub URL Home Assistant can reach",
|
|
18
|
-
"globalSettings.s_ha-export-transport.publicBaseUrl.description": "Used for the signed image links Home Assistant fetches; leave empty to derive one automatically.",
|
|
19
17
|
"globalSettings.s_ha-export-advanced.title": "Advanced",
|
|
20
18
|
"globalSettings.s_ha-export-advanced.reconcileIntervalSec.label": "Reconcile interval (s)",
|
|
21
19
|
"globalSettings.s_ha-export-advanced.reconcileIntervalSec.description": "Full re-announce and re-push cadence.",
|
package/i18n/it.json
CHANGED
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
"globalSettings.s_ha-export-brokers.title": "Istanze di Home Assistant",
|
|
15
15
|
"globalSettings.s_ha-export-brokers.ha-export___no-brokers.label": "Nessun broker",
|
|
16
16
|
"globalSettings.s_ha-export-transport.title": "Connessione",
|
|
17
|
-
"globalSettings.s_ha-export-transport.publicBaseUrl.label": "URL dell'hub raggiungibile da Home Assistant",
|
|
18
|
-
"globalSettings.s_ha-export-transport.publicBaseUrl.description": "Usato per i link firmati delle immagini che Home Assistant scarica; lascia vuoto per ricavarlo automaticamente.",
|
|
19
17
|
"globalSettings.s_ha-export-advanced.title": "Avanzate",
|
|
20
18
|
"globalSettings.s_ha-export-advanced.reconcileIntervalSec.label": "Intervallo di riconciliazione (s)",
|
|
21
19
|
"globalSettings.s_ha-export-advanced.reconcileIntervalSec.description": "Cadenza di ri-annuncio e ri-invio completo.",
|
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.648",
|
|
4
4
|
"description": "Home Assistant device provider addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
|
|
128
128
|
},
|
|
129
129
|
"requires": {
|
|
130
|
-
"server": ">=1.2.
|
|
130
|
+
"server": ">=1.2.733"
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
"files": [
|