@camstack/addon-provider-homeassistant 1.2.647 → 1.2.649
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-BFFi4NDH.js → ha-broker-source-CE3nHkyW.js} +472 -101
- package/dist/{ha-broker-source-B6wqfEUz.mjs → ha-broker-source-D3FBA6uG.mjs} +461 -102
- package/dist/ha-export/ha-export.addon.js +148 -60
- package/dist/ha-export/ha-export.addon.mjs +148 -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-CE3nHkyW.js");
|
|
28
28
|
let node_crypto = require("node:crypto");
|
|
29
29
|
require("node:zlib");
|
|
30
30
|
let node_fs = require("node:fs");
|
|
@@ -4940,6 +4940,60 @@ async function call(slice, capName, make) {
|
|
|
4940
4940
|
}
|
|
4941
4941
|
return { ok: true };
|
|
4942
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 addon id, the prefix of its rows in the canonical settings table. */
|
|
4954
|
+
var HA_EXPORT_ADDON_ID = "homeassistant-export";
|
|
4955
|
+
/**
|
|
4956
|
+
* Where the addon's global settings live: the canonical `addon-settings` table, one row per
|
|
4957
|
+
* key, id `<addonId>.<key>`, data `{ addonId, key, value }` (never a per-addon scoped table,
|
|
4958
|
+
* which exists only when something lazily declared it).
|
|
4959
|
+
*/
|
|
4960
|
+
var ADDON_SETTINGS_TABLE = "addon-settings";
|
|
4961
|
+
var RETIRED_URL_KEYS = ["publicBaseUrl"];
|
|
4962
|
+
async function planDropPublicBaseUrl(ports) {
|
|
4963
|
+
const stored = await ports.readStored();
|
|
4964
|
+
const present = RETIRED_URL_KEYS.filter((key) => key in stored);
|
|
4965
|
+
if (present.length === 0) return { edits: [] };
|
|
4966
|
+
const rowId = (key) => `${HA_EXPORT_ADDON_ID}.${key}`;
|
|
4967
|
+
return { edits: [{
|
|
4968
|
+
op: "deleteKeys",
|
|
4969
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
4970
|
+
keys: present.map(rowId),
|
|
4971
|
+
precondition: require_ha_broker_source.preconditionOf(Object.fromEntries(present.map((key) => [rowId(key), {
|
|
4972
|
+
addonId: HA_EXPORT_ADDON_ID,
|
|
4973
|
+
key,
|
|
4974
|
+
value: stored[key]
|
|
4975
|
+
}])))
|
|
4976
|
+
}] };
|
|
4977
|
+
}
|
|
4978
|
+
//#endregion
|
|
4979
|
+
//#region src/ha-export/migrations/index.ts
|
|
4980
|
+
/** The `homeassistant-export` catalogue (D987): offered to the engine when the addon initialises. */
|
|
4981
|
+
var ADDON_MIGRATIONS = [{
|
|
4982
|
+
meta: {
|
|
4983
|
+
id: "homeassistant-export/2026-11-02-drop-public-base-url",
|
|
4984
|
+
owner: "homeassistant-export",
|
|
4985
|
+
level: "addon-state",
|
|
4986
|
+
seq: 1,
|
|
4987
|
+
dependsOn: [],
|
|
4988
|
+
store: "settings",
|
|
4989
|
+
scope: "cluster",
|
|
4990
|
+
onFailure: "tolerate",
|
|
4991
|
+
bodyKind: "plan",
|
|
4992
|
+
description: "Delete the retired publicBaseUrl setting; each instance now chooses a hub address role",
|
|
4993
|
+
fixtureMark: "the reconcile interval the migration does not know"
|
|
4994
|
+
},
|
|
4995
|
+
plan: planDropPublicBaseUrl
|
|
4996
|
+
}];
|
|
4943
4997
|
process.platform, process.env["SystemRoot"];
|
|
4944
4998
|
/**
|
|
4945
4999
|
* The ONE derivation of "a signed, expiring URL".
|
|
@@ -5019,15 +5073,26 @@ function verifyExpiringUrl(input) {
|
|
|
5019
5073
|
* an expired link is refused whether or not its signature is valid, so a leaked
|
|
5020
5074
|
* URL stops working on its own.
|
|
5021
5075
|
*/
|
|
5076
|
+
/** The settings key that carries one broker's media role (per-instance, like `exportTo:`). */
|
|
5077
|
+
function mediaRoleKey(brokerId) {
|
|
5078
|
+
return `mediaRole:${brokerId}`;
|
|
5079
|
+
}
|
|
5080
|
+
/** A broker's chosen role; anything stored that is not a known role is `internal`. */
|
|
5081
|
+
function haMediaRoleOf(store, brokerId) {
|
|
5082
|
+
return store[mediaRoleKey(brokerId)] === "publicEndpoint" ? "publicEndpoint" : "internal";
|
|
5083
|
+
}
|
|
5022
5084
|
/**
|
|
5023
|
-
* The base a media link is minted on
|
|
5024
|
-
*
|
|
5025
|
-
*
|
|
5026
|
-
*
|
|
5027
|
-
* endpoint, and never loopback (`null` when nothing qualifies).
|
|
5085
|
+
* The base a media link is minted on: the hub's role (D734) — `internal` unless the
|
|
5086
|
+
* broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
|
|
5087
|
+
* when the role resolves to nothing): a link Home Assistant cannot open is worse
|
|
5088
|
+
* than no image.
|
|
5028
5089
|
*/
|
|
5029
|
-
function haMediaBaseUrl(book) {
|
|
5030
|
-
return book.roles.
|
|
5090
|
+
function haMediaBaseUrl(book, role = "internal") {
|
|
5091
|
+
return book.roles[role].resolved;
|
|
5092
|
+
}
|
|
5093
|
+
/** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
|
|
5094
|
+
function mediaBasesByBroker(store, brokerIds, book) {
|
|
5095
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, haMediaBaseUrl(book, haMediaRoleOf(store, brokerId))]));
|
|
5031
5096
|
}
|
|
5032
5097
|
/** How long a minted link stays valid. Long enough for HA to render it. */
|
|
5033
5098
|
var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
|
|
@@ -7168,8 +7233,7 @@ var DEFAULT_CONFIG = {
|
|
|
7168
7233
|
exportedEntities: {
|
|
7169
7234
|
actions: [],
|
|
7170
7235
|
targets: []
|
|
7171
|
-
}
|
|
7172
|
-
publicBaseUrl: ""
|
|
7236
|
+
}
|
|
7173
7237
|
};
|
|
7174
7238
|
var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
7175
7239
|
links = /* @__PURE__ */ new Map();
|
|
@@ -7178,7 +7242,6 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7178
7242
|
/** numeric deviceId → deviceKey, for the event path. */
|
|
7179
7243
|
keyByDeviceId = /* @__PURE__ */ new Map();
|
|
7180
7244
|
mediaSecret = "";
|
|
7181
|
-
mediaBaseUrl = null;
|
|
7182
7245
|
reconcileTimer = null;
|
|
7183
7246
|
reconcileInFlight = false;
|
|
7184
7247
|
/** A reason asked for while one was running. Re-run, never dropped. */
|
|
@@ -7278,6 +7341,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7278
7341
|
super({ ...DEFAULT_CONFIG });
|
|
7279
7342
|
}
|
|
7280
7343
|
async onInitialize() {
|
|
7344
|
+
await this.runMigrations(require_ha_broker_source.defineAddonMigrations(ADDON_MIGRATIONS, { readStored: async () => this.resolveGlobalStore() }));
|
|
7281
7345
|
const secret = this.state("haExportMediaSecret", require_ha_broker_source.string(), "");
|
|
7282
7346
|
const existing = await secret.get();
|
|
7283
7347
|
if (existing.length === 0) {
|
|
@@ -7678,32 +7742,39 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7678
7742
|
const lastSeen = readNumber(data, "lastSeen") ?? Date.now();
|
|
7679
7743
|
const zonesVisited = readStringArray(data, "zonesVisited");
|
|
7680
7744
|
const fusedIntoTrackId = readString(data, "fusedIntoTrackId");
|
|
7681
|
-
const
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
return;
|
|
7745
|
+
const exported = this.exported.get(deviceKey);
|
|
7746
|
+
if (exported === void 0) return;
|
|
7747
|
+
const bases = await this.mediaBasesFor(exported.brokerIds, deviceId);
|
|
7748
|
+
let produced = false;
|
|
7749
|
+
for (const brokerId of exported.brokerIds) {
|
|
7750
|
+
const link = this.links.get(brokerId);
|
|
7751
|
+
if (link === void 0) continue;
|
|
7752
|
+
const base = bases.get(brokerId) ?? null;
|
|
7753
|
+
const values = projectTrackLifecycle(deviceKey, {
|
|
7754
|
+
deviceId,
|
|
7755
|
+
trackId,
|
|
7756
|
+
phase,
|
|
7757
|
+
classes,
|
|
7758
|
+
...bestClassName !== null ? { bestClassName } : {},
|
|
7759
|
+
...label !== null ? { label } : {},
|
|
7760
|
+
...plateText !== null ? { plateText } : {},
|
|
7761
|
+
lastSeen,
|
|
7762
|
+
zonesVisited,
|
|
7763
|
+
...fusedIntoTrackId !== null ? { fusedIntoTrackId } : {}
|
|
7764
|
+
}, base === null ? null : this.mintTrackMediaUrl(trackId, deviceId, base));
|
|
7765
|
+
if (values.length === 0) continue;
|
|
7766
|
+
produced = true;
|
|
7767
|
+
for (const value of values) link.client.publishState(value.topic, value.value);
|
|
7705
7768
|
}
|
|
7706
|
-
this.
|
|
7769
|
+
if (!produced) this.ctx.logger.debug("ha-export: track produced no exportable state", {
|
|
7770
|
+
tags: { deviceId },
|
|
7771
|
+
meta: {
|
|
7772
|
+
trackId,
|
|
7773
|
+
bestClassName,
|
|
7774
|
+
phase,
|
|
7775
|
+
fusedIntoTrackId
|
|
7776
|
+
}
|
|
7777
|
+
});
|
|
7707
7778
|
}
|
|
7708
7779
|
onAudioWindow(data) {
|
|
7709
7780
|
const deviceId = readNumber(data, "deviceId");
|
|
@@ -7848,7 +7919,6 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
7848
7919
|
await this.refreshBrokers();
|
|
7849
7920
|
const fullResync = this.resync.take(reason);
|
|
7850
7921
|
let readsHeld = false;
|
|
7851
|
-
await this.refreshMediaBaseUrl();
|
|
7852
7922
|
await this.refreshComponentSupport();
|
|
7853
7923
|
const enabled = this.enabledBrokerIds();
|
|
7854
7924
|
const devices = await this.ctx.api.deviceManager.listAll.query({});
|
|
@@ -9325,28 +9395,41 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
9325
9395
|
* cannot open is worse than no image, because it renders as a broken
|
|
9326
9396
|
* entity rather than an empty one.
|
|
9327
9397
|
*/
|
|
9328
|
-
mintTrackMediaUrl(trackId, deviceId) {
|
|
9329
|
-
if (this.
|
|
9398
|
+
mintTrackMediaUrl(trackId, deviceId, baseUrl) {
|
|
9399
|
+
if (this.mediaSecret.length === 0) return null;
|
|
9330
9400
|
return buildMediaUrl({
|
|
9331
|
-
baseUrl
|
|
9401
|
+
baseUrl,
|
|
9332
9402
|
routePrefix: MEDIA_ROUTE_PREFIX,
|
|
9333
9403
|
id: `${String(deviceId)}:${trackId}`,
|
|
9334
9404
|
secret: this.mediaSecret,
|
|
9335
9405
|
expMs: Date.now() + MEDIA_URL_TTL_MS
|
|
9336
9406
|
});
|
|
9337
9407
|
}
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9408
|
+
/**
|
|
9409
|
+
* Each broker's media base, read per use: the palette's role the broker chose
|
|
9410
|
+
* (`mediaRole:<brokerId>`, `internal` by default). A role that resolves to nothing
|
|
9411
|
+
* is `null` and says so — the image entities then carry no value, which reads as
|
|
9412
|
+
* empty rather than as a broken link.
|
|
9413
|
+
*/
|
|
9414
|
+
async mediaBasesFor(brokerIds, deviceId) {
|
|
9415
|
+
let bases;
|
|
9343
9416
|
try {
|
|
9344
|
-
|
|
9417
|
+
bases = mediaBasesByBroker(await this.resolveGlobalStore(), brokerIds, await this.ctx.api.localNetwork.getAddresses.query());
|
|
9345
9418
|
} catch (err) {
|
|
9346
|
-
this.
|
|
9347
|
-
|
|
9419
|
+
this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
|
|
9420
|
+
tags: { deviceId },
|
|
9421
|
+
meta: { error: errMsg(err) }
|
|
9422
|
+
});
|
|
9423
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, null]));
|
|
9424
|
+
}
|
|
9425
|
+
for (const brokerId of brokerIds) {
|
|
9426
|
+
if (bases.get(brokerId) !== null) continue;
|
|
9427
|
+
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", {
|
|
9428
|
+
tags: { deviceId },
|
|
9429
|
+
meta: { brokerId }
|
|
9430
|
+
});
|
|
9348
9431
|
}
|
|
9349
|
-
|
|
9432
|
+
return bases;
|
|
9350
9433
|
}
|
|
9351
9434
|
/**
|
|
9352
9435
|
* The cap-level expose/unexpose applies to EVERY enabled broker.
|
|
@@ -9400,21 +9483,26 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
9400
9483
|
variant: "warning"
|
|
9401
9484
|
}]
|
|
9402
9485
|
},
|
|
9403
|
-
{
|
|
9486
|
+
...this.config.knownBrokers.length > 0 ? [{
|
|
9404
9487
|
id: "ha-export-transport",
|
|
9405
9488
|
title: "Connection",
|
|
9406
9489
|
titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
|
|
9490
|
+
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.",
|
|
9407
9491
|
columns: 1,
|
|
9408
|
-
fields:
|
|
9409
|
-
type: "
|
|
9410
|
-
key:
|
|
9411
|
-
label:
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9492
|
+
fields: this.config.knownBrokers.map((broker) => ({
|
|
9493
|
+
type: "select",
|
|
9494
|
+
key: mediaRoleKey(broker.id),
|
|
9495
|
+
label: `Image links for ${broker.name}`,
|
|
9496
|
+
options: [{
|
|
9497
|
+
value: "internal",
|
|
9498
|
+
label: "Internal (LAN address)"
|
|
9499
|
+
}, {
|
|
9500
|
+
value: "publicEndpoint",
|
|
9501
|
+
label: "Public endpoint (reachable from outside)"
|
|
9502
|
+
}],
|
|
9503
|
+
default: "internal"
|
|
9504
|
+
}))
|
|
9505
|
+
}] : [],
|
|
9418
9506
|
{
|
|
9419
9507
|
id: "ha-export-advanced",
|
|
9420
9508
|
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-D3FBA6uG.mjs";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import "node:zlib";
|
|
4
4
|
import "node:fs";
|
|
@@ -4905,6 +4905,60 @@ async function call(slice, capName, make) {
|
|
|
4905
4905
|
}
|
|
4906
4906
|
return { ok: true };
|
|
4907
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 addon id, the prefix of its rows in the canonical settings table. */
|
|
4919
|
+
var HA_EXPORT_ADDON_ID = "homeassistant-export";
|
|
4920
|
+
/**
|
|
4921
|
+
* Where the addon's global settings live: the canonical `addon-settings` table, one row per
|
|
4922
|
+
* key, id `<addonId>.<key>`, data `{ addonId, key, value }` (never a per-addon scoped table,
|
|
4923
|
+
* which exists only when something lazily declared it).
|
|
4924
|
+
*/
|
|
4925
|
+
var ADDON_SETTINGS_TABLE = "addon-settings";
|
|
4926
|
+
var RETIRED_URL_KEYS = ["publicBaseUrl"];
|
|
4927
|
+
async function planDropPublicBaseUrl(ports) {
|
|
4928
|
+
const stored = await ports.readStored();
|
|
4929
|
+
const present = RETIRED_URL_KEYS.filter((key) => key in stored);
|
|
4930
|
+
if (present.length === 0) return { edits: [] };
|
|
4931
|
+
const rowId = (key) => `${HA_EXPORT_ADDON_ID}.${key}`;
|
|
4932
|
+
return { edits: [{
|
|
4933
|
+
op: "deleteKeys",
|
|
4934
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
4935
|
+
keys: present.map(rowId),
|
|
4936
|
+
precondition: preconditionOf(Object.fromEntries(present.map((key) => [rowId(key), {
|
|
4937
|
+
addonId: HA_EXPORT_ADDON_ID,
|
|
4938
|
+
key,
|
|
4939
|
+
value: stored[key]
|
|
4940
|
+
}])))
|
|
4941
|
+
}] };
|
|
4942
|
+
}
|
|
4943
|
+
//#endregion
|
|
4944
|
+
//#region src/ha-export/migrations/index.ts
|
|
4945
|
+
/** The `homeassistant-export` catalogue (D987): offered to the engine when the addon initialises. */
|
|
4946
|
+
var ADDON_MIGRATIONS = [{
|
|
4947
|
+
meta: {
|
|
4948
|
+
id: "homeassistant-export/2026-11-02-drop-public-base-url",
|
|
4949
|
+
owner: "homeassistant-export",
|
|
4950
|
+
level: "addon-state",
|
|
4951
|
+
seq: 1,
|
|
4952
|
+
dependsOn: [],
|
|
4953
|
+
store: "settings",
|
|
4954
|
+
scope: "cluster",
|
|
4955
|
+
onFailure: "tolerate",
|
|
4956
|
+
bodyKind: "plan",
|
|
4957
|
+
description: "Delete the retired publicBaseUrl setting; each instance now chooses a hub address role",
|
|
4958
|
+
fixtureMark: "the reconcile interval the migration does not know"
|
|
4959
|
+
},
|
|
4960
|
+
plan: planDropPublicBaseUrl
|
|
4961
|
+
}];
|
|
4908
4962
|
process.platform, process.env["SystemRoot"];
|
|
4909
4963
|
/**
|
|
4910
4964
|
* The ONE derivation of "a signed, expiring URL".
|
|
@@ -4984,15 +5038,26 @@ function verifyExpiringUrl(input) {
|
|
|
4984
5038
|
* an expired link is refused whether or not its signature is valid, so a leaked
|
|
4985
5039
|
* URL stops working on its own.
|
|
4986
5040
|
*/
|
|
5041
|
+
/** The settings key that carries one broker's media role (per-instance, like `exportTo:`). */
|
|
5042
|
+
function mediaRoleKey(brokerId) {
|
|
5043
|
+
return `mediaRole:${brokerId}`;
|
|
5044
|
+
}
|
|
5045
|
+
/** A broker's chosen role; anything stored that is not a known role is `internal`. */
|
|
5046
|
+
function haMediaRoleOf(store, brokerId) {
|
|
5047
|
+
return store[mediaRoleKey(brokerId)] === "publicEndpoint" ? "publicEndpoint" : "internal";
|
|
5048
|
+
}
|
|
4987
5049
|
/**
|
|
4988
|
-
* The base a media link is minted on
|
|
4989
|
-
*
|
|
4990
|
-
*
|
|
4991
|
-
*
|
|
4992
|
-
* endpoint, and never loopback (`null` when nothing qualifies).
|
|
5050
|
+
* The base a media link is minted on: the hub's role (D734) — `internal` unless the
|
|
5051
|
+
* broker chose `publicEndpoint`. Ranked by `local-network`, never loopback (`null`
|
|
5052
|
+
* when the role resolves to nothing): a link Home Assistant cannot open is worse
|
|
5053
|
+
* than no image.
|
|
4993
5054
|
*/
|
|
4994
|
-
function haMediaBaseUrl(book) {
|
|
4995
|
-
return book.roles.
|
|
5055
|
+
function haMediaBaseUrl(book, role = "internal") {
|
|
5056
|
+
return book.roles[role].resolved;
|
|
5057
|
+
}
|
|
5058
|
+
/** brokerId → media base, each broker on ITS role. Pure: the caller reads the palette per use. */
|
|
5059
|
+
function mediaBasesByBroker(store, brokerIds, book) {
|
|
5060
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, haMediaBaseUrl(book, haMediaRoleOf(store, brokerId))]));
|
|
4996
5061
|
}
|
|
4997
5062
|
/** How long a minted link stays valid. Long enough for HA to render it. */
|
|
4998
5063
|
var MEDIA_URL_TTL_MS = 360 * 60 * 1e3;
|
|
@@ -7133,8 +7198,7 @@ var DEFAULT_CONFIG = {
|
|
|
7133
7198
|
exportedEntities: {
|
|
7134
7199
|
actions: [],
|
|
7135
7200
|
targets: []
|
|
7136
|
-
}
|
|
7137
|
-
publicBaseUrl: ""
|
|
7201
|
+
}
|
|
7138
7202
|
};
|
|
7139
7203
|
var HaExportAddon = class extends BaseAddon {
|
|
7140
7204
|
links = /* @__PURE__ */ new Map();
|
|
@@ -7143,7 +7207,6 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
7143
7207
|
/** numeric deviceId → deviceKey, for the event path. */
|
|
7144
7208
|
keyByDeviceId = /* @__PURE__ */ new Map();
|
|
7145
7209
|
mediaSecret = "";
|
|
7146
|
-
mediaBaseUrl = null;
|
|
7147
7210
|
reconcileTimer = null;
|
|
7148
7211
|
reconcileInFlight = false;
|
|
7149
7212
|
/** A reason asked for while one was running. Re-run, never dropped. */
|
|
@@ -7243,6 +7306,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
7243
7306
|
super({ ...DEFAULT_CONFIG });
|
|
7244
7307
|
}
|
|
7245
7308
|
async onInitialize() {
|
|
7309
|
+
await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, { readStored: async () => this.resolveGlobalStore() }));
|
|
7246
7310
|
const secret = this.state("haExportMediaSecret", string(), "");
|
|
7247
7311
|
const existing = await secret.get();
|
|
7248
7312
|
if (existing.length === 0) {
|
|
@@ -7643,32 +7707,39 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
7643
7707
|
const lastSeen = readNumber(data, "lastSeen") ?? Date.now();
|
|
7644
7708
|
const zonesVisited = readStringArray(data, "zonesVisited");
|
|
7645
7709
|
const fusedIntoTrackId = readString(data, "fusedIntoTrackId");
|
|
7646
|
-
const
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
return;
|
|
7710
|
+
const exported = this.exported.get(deviceKey);
|
|
7711
|
+
if (exported === void 0) return;
|
|
7712
|
+
const bases = await this.mediaBasesFor(exported.brokerIds, deviceId);
|
|
7713
|
+
let produced = false;
|
|
7714
|
+
for (const brokerId of exported.brokerIds) {
|
|
7715
|
+
const link = this.links.get(brokerId);
|
|
7716
|
+
if (link === void 0) continue;
|
|
7717
|
+
const base = bases.get(brokerId) ?? null;
|
|
7718
|
+
const values = projectTrackLifecycle(deviceKey, {
|
|
7719
|
+
deviceId,
|
|
7720
|
+
trackId,
|
|
7721
|
+
phase,
|
|
7722
|
+
classes,
|
|
7723
|
+
...bestClassName !== null ? { bestClassName } : {},
|
|
7724
|
+
...label !== null ? { label } : {},
|
|
7725
|
+
...plateText !== null ? { plateText } : {},
|
|
7726
|
+
lastSeen,
|
|
7727
|
+
zonesVisited,
|
|
7728
|
+
...fusedIntoTrackId !== null ? { fusedIntoTrackId } : {}
|
|
7729
|
+
}, base === null ? null : this.mintTrackMediaUrl(trackId, deviceId, base));
|
|
7730
|
+
if (values.length === 0) continue;
|
|
7731
|
+
produced = true;
|
|
7732
|
+
for (const value of values) link.client.publishState(value.topic, value.value);
|
|
7670
7733
|
}
|
|
7671
|
-
this.
|
|
7734
|
+
if (!produced) this.ctx.logger.debug("ha-export: track produced no exportable state", {
|
|
7735
|
+
tags: { deviceId },
|
|
7736
|
+
meta: {
|
|
7737
|
+
trackId,
|
|
7738
|
+
bestClassName,
|
|
7739
|
+
phase,
|
|
7740
|
+
fusedIntoTrackId
|
|
7741
|
+
}
|
|
7742
|
+
});
|
|
7672
7743
|
}
|
|
7673
7744
|
onAudioWindow(data) {
|
|
7674
7745
|
const deviceId = readNumber(data, "deviceId");
|
|
@@ -7813,7 +7884,6 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
7813
7884
|
await this.refreshBrokers();
|
|
7814
7885
|
const fullResync = this.resync.take(reason);
|
|
7815
7886
|
let readsHeld = false;
|
|
7816
|
-
await this.refreshMediaBaseUrl();
|
|
7817
7887
|
await this.refreshComponentSupport();
|
|
7818
7888
|
const enabled = this.enabledBrokerIds();
|
|
7819
7889
|
const devices = await this.ctx.api.deviceManager.listAll.query({});
|
|
@@ -9290,28 +9360,41 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
9290
9360
|
* cannot open is worse than no image, because it renders as a broken
|
|
9291
9361
|
* entity rather than an empty one.
|
|
9292
9362
|
*/
|
|
9293
|
-
mintTrackMediaUrl(trackId, deviceId) {
|
|
9294
|
-
if (this.
|
|
9363
|
+
mintTrackMediaUrl(trackId, deviceId, baseUrl) {
|
|
9364
|
+
if (this.mediaSecret.length === 0) return null;
|
|
9295
9365
|
return buildMediaUrl({
|
|
9296
|
-
baseUrl
|
|
9366
|
+
baseUrl,
|
|
9297
9367
|
routePrefix: MEDIA_ROUTE_PREFIX,
|
|
9298
9368
|
id: `${String(deviceId)}:${trackId}`,
|
|
9299
9369
|
secret: this.mediaSecret,
|
|
9300
9370
|
expMs: Date.now() + MEDIA_URL_TTL_MS
|
|
9301
9371
|
});
|
|
9302
9372
|
}
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9373
|
+
/**
|
|
9374
|
+
* Each broker's media base, read per use: the palette's role the broker chose
|
|
9375
|
+
* (`mediaRole:<brokerId>`, `internal` by default). A role that resolves to nothing
|
|
9376
|
+
* is `null` and says so — the image entities then carry no value, which reads as
|
|
9377
|
+
* empty rather than as a broken link.
|
|
9378
|
+
*/
|
|
9379
|
+
async mediaBasesFor(brokerIds, deviceId) {
|
|
9380
|
+
let bases;
|
|
9308
9381
|
try {
|
|
9309
|
-
|
|
9382
|
+
bases = mediaBasesByBroker(await this.resolveGlobalStore(), brokerIds, await this.ctx.api.localNetwork.getAddresses.query());
|
|
9310
9383
|
} catch (err) {
|
|
9311
|
-
this.
|
|
9312
|
-
|
|
9384
|
+
this.ctx.logger.debug("ha-export: could not resolve the media base URLs", {
|
|
9385
|
+
tags: { deviceId },
|
|
9386
|
+
meta: { error: errMsg(err) }
|
|
9387
|
+
});
|
|
9388
|
+
return new Map(brokerIds.map((brokerId) => [brokerId, null]));
|
|
9389
|
+
}
|
|
9390
|
+
for (const brokerId of brokerIds) {
|
|
9391
|
+
if (bases.get(brokerId) !== null) continue;
|
|
9392
|
+
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", {
|
|
9393
|
+
tags: { deviceId },
|
|
9394
|
+
meta: { brokerId }
|
|
9395
|
+
});
|
|
9313
9396
|
}
|
|
9314
|
-
|
|
9397
|
+
return bases;
|
|
9315
9398
|
}
|
|
9316
9399
|
/**
|
|
9317
9400
|
* The cap-level expose/unexpose applies to EVERY enabled broker.
|
|
@@ -9365,21 +9448,26 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
9365
9448
|
variant: "warning"
|
|
9366
9449
|
}]
|
|
9367
9450
|
},
|
|
9368
|
-
{
|
|
9451
|
+
...this.config.knownBrokers.length > 0 ? [{
|
|
9369
9452
|
id: "ha-export-transport",
|
|
9370
9453
|
title: "Connection",
|
|
9371
9454
|
titleKey: "provider-homeassistant:globalSettings.s_ha-export-transport.title",
|
|
9455
|
+
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.",
|
|
9372
9456
|
columns: 1,
|
|
9373
|
-
fields:
|
|
9374
|
-
type: "
|
|
9375
|
-
key:
|
|
9376
|
-
label:
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9457
|
+
fields: this.config.knownBrokers.map((broker) => ({
|
|
9458
|
+
type: "select",
|
|
9459
|
+
key: mediaRoleKey(broker.id),
|
|
9460
|
+
label: `Image links for ${broker.name}`,
|
|
9461
|
+
options: [{
|
|
9462
|
+
value: "internal",
|
|
9463
|
+
label: "Internal (LAN address)"
|
|
9464
|
+
}, {
|
|
9465
|
+
value: "publicEndpoint",
|
|
9466
|
+
label: "Public endpoint (reachable from outside)"
|
|
9467
|
+
}],
|
|
9468
|
+
default: "internal"
|
|
9469
|
+
}))
|
|
9470
|
+
}] : [],
|
|
9383
9471
|
{
|
|
9384
9472
|
id: "ha-export-advanced",
|
|
9385
9473
|
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.649",
|
|
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.738"
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
"files": [
|