@camstack/addon-import-alexa 0.2.179 → 0.2.181

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -834,7 +834,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
834
834
  return EventCategory;
835
835
  }({});
836
836
  //#endregion
837
- //#region ../types/dist/device-type-CfHw_quR.mjs
837
+ //#region ../types/dist/device-type-D1fH5kaC.mjs
838
838
  /**
839
839
  import { errMsg } from '@camstack/types'
840
840
  * Extract a human-readable message from an unknown error value.
@@ -1065,6 +1065,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
1065
1065
  DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1066
1066
  /** Media player exposes stop distinct from pause. */
1067
1067
  DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1068
+ /** Media player exposes volume up / down in steps (an infrared TV has no level). */
1069
+ DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
1070
+ /** Media player exposes channel up / down in steps. */
1071
+ DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
1072
+ /** Media player exposes power on / off. */
1073
+ DeviceFeature["MediaPlayerPower"] = "media-player-power";
1068
1074
  /** Alarm panel requires a PIN code on arm/disarm. */
1069
1075
  DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1070
1076
  /** Presence device carries GPS coordinates (lat/lng/accuracy) in
@@ -6038,7 +6044,7 @@ function number(params) {
6038
6044
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
6039
6045
  }
6040
6046
  //#endregion
6041
- //#region ../types/dist/sleep-hmSP-eJt.mjs
6047
+ //#region ../types/dist/sleep-DV4qxEyD.mjs
6042
6048
  /**
6043
6049
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6044
6050
  * window to float samples (D455).
@@ -6117,6 +6123,22 @@ function buildAlawTable() {
6117
6123
  }
6118
6124
  buildUlawTable();
6119
6125
  buildAlawTable();
6126
+ /**
6127
+ * An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
6128
+ * so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
6129
+ * co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
6130
+ * bundles its own copy of this module, so the class the runner sees is not the one that threw.
6131
+ */
6132
+ var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
6133
+ var AddonMigrationsBlockedError = class extends Error {
6134
+ /** The ledger ids that did not apply and block the owner. */
6135
+ migrationIds;
6136
+ constructor(migrationIds) {
6137
+ super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
6138
+ this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
6139
+ this.migrationIds = migrationIds;
6140
+ }
6141
+ };
6120
6142
  /** Is this field's stored value a credential? */
6121
6143
  function isSecretConfigField(field) {
6122
6144
  if (field.type === "password") return true;
@@ -7770,7 +7792,7 @@ var BaseAddon = class {
7770
7792
  startMigration: (input) => api.startMigration.mutate(input),
7771
7793
  finishMigration: (input) => api.finishMigration.mutate(input)
7772
7794
  }, migrations, this.ctx.logger);
7773
- if (report.blockOwner.length > 0) throw new Error(`migration(s) ${report.blockOwner.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
7795
+ if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
7774
7796
  return report;
7775
7797
  }
7776
7798
  /**
@@ -8110,6 +8132,10 @@ var BaseAddon = class {
8110
8132
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
8111
8133
  const patch = {};
8112
8134
  for (const key of keys) {
8135
+ if (tree.get(key)?.kind === "array") {
8136
+ patch[key] = [];
8137
+ continue;
8138
+ }
8113
8139
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8114
8140
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8115
8141
  }
@@ -11766,7 +11792,7 @@ object({
11766
11792
  params: record(string(), union([string(), number$1()])).optional()
11767
11793
  });
11768
11794
  //#endregion
11769
- //#region ../types/dist/composition-Dh3RfMx5.mjs
11795
+ //#region ../types/dist/composition-CD1GdFuv.mjs
11770
11796
  /**
11771
11797
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11772
11798
  *
@@ -12903,7 +12929,14 @@ var CompositionForwardCommandSchema = object({
12903
12929
  method: string().min(1),
12904
12930
  args: record(string().min(1), CompositionForwardArgSchema).optional()
12905
12931
  });
12906
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
12932
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [
12933
+ CompositionForwardCommandSchema,
12934
+ CompositionCodeSourceSchema,
12935
+ object({
12936
+ kind: literal("extension"),
12937
+ target: literal("remote")
12938
+ })
12939
+ ]);
12907
12940
  /** An item key inside an item-array cap (`consumables.items[<key>]`). */
12908
12941
  var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
12909
12942
  /**
@@ -12948,7 +12981,7 @@ var CompositionSchema = object({
12948
12981
  kind: literal("existing"),
12949
12982
  device: CompositionSourceRefSchema
12950
12983
  })]),
12951
- features: array(CompositionFeatureSchema).min(1).max(16),
12984
+ features: array(CompositionFeatureSchema).min(0).max(16),
12952
12985
  /**
12953
12986
  * Present when the block is a helper template. Absent on a free
12954
12987
  * composition (the room example, device 4603). The editor reads this,
@@ -19527,6 +19560,48 @@ var APPLIANCE_CAP_VOCABULARY = {
19527
19560
  scope: "device",
19528
19561
  noun: t("Media player", "Lettore multimediale"),
19529
19562
  actions: [
19563
+ {
19564
+ id: "media.turnOn",
19565
+ method: "turnOn",
19566
+ label: t("Turn on", "Accendi"),
19567
+ phrase: t("Turn on {device}", "Accendi {device}"),
19568
+ icon: "power"
19569
+ },
19570
+ {
19571
+ id: "media.turnOff",
19572
+ method: "turnOff",
19573
+ label: t("Turn off", "Spegni"),
19574
+ phrase: t("Turn off {device}", "Spegni {device}"),
19575
+ icon: "power"
19576
+ },
19577
+ {
19578
+ id: "media.volumeUp",
19579
+ method: "volumeUp",
19580
+ label: t("Volume up", "Alza il volume"),
19581
+ phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
19582
+ icon: "volume-2"
19583
+ },
19584
+ {
19585
+ id: "media.volumeDown",
19586
+ method: "volumeDown",
19587
+ label: t("Volume down", "Abbassa il volume"),
19588
+ phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
19589
+ icon: "volume-2"
19590
+ },
19591
+ {
19592
+ id: "media.channelUp",
19593
+ method: "channelUp",
19594
+ label: t("Channel up", "Canale successivo"),
19595
+ phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
19596
+ icon: "skip-forward"
19597
+ },
19598
+ {
19599
+ id: "media.channelDown",
19600
+ method: "channelDown",
19601
+ label: t("Channel down", "Canale precedente"),
19602
+ phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
19603
+ icon: "skip-back"
19604
+ },
19530
19605
  {
19531
19606
  id: "media.play",
19532
19607
  method: "play",
@@ -49154,6 +49229,244 @@ var controlCapability = {
49154
49229
  */
49155
49230
  history: "none"
49156
49231
  };
49232
+ /**
49233
+ * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
49234
+ * Apple TV, Spotify, Roku, generic OTT receivers, …) on
49235
+ * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
49236
+ * surface — those use `DeviceType.Camera` + the broker pipeline.
49237
+ *
49238
+ * The cap declares a wide method surface; not every backend supports
49239
+ * every method. Drivers signal sub-capability via the
49240
+ * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
49241
+ * corresponding method) so the UI gates buttons without a slice
49242
+ * fetch — same pattern as `PanTiltZoom` for cameras.
49243
+ */
49244
+ var MediaPlayerStateSchema = _enum([
49245
+ "off",
49246
+ "on",
49247
+ "idle",
49248
+ "playing",
49249
+ "paused",
49250
+ "buffering",
49251
+ "standby"
49252
+ ]);
49253
+ var MediaPlayerRepeatSchema = _enum([
49254
+ "off",
49255
+ "all",
49256
+ "one"
49257
+ ]);
49258
+ var MediaInfoSchema = object({
49259
+ /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
49260
+ * `podcast`, …). */
49261
+ type: string(),
49262
+ /** Human-readable title. */
49263
+ title: string(),
49264
+ /** Optional artist / channel / station label. */
49265
+ artist: string().optional(),
49266
+ /** Optional album / season / show name. */
49267
+ album: string().optional(),
49268
+ /** Optional cover-art / thumbnail URL. */
49269
+ imageUrl: string().optional()
49270
+ });
49271
+ /**
49272
+ * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
49273
+ * the device said so. `assumed`: the driver has no feedback (infrared) and
49274
+ * tracks what it last SENT; it moves only after a send was accepted, and a
49275
+ * human at the real remote makes it wrong. A UI shows an assumed value as a
49276
+ * guess, never as a reading.
49277
+ */
49278
+ var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
49279
+ /** The fields of a media-player status a driver can only guess. */
49280
+ var MediaPlayerAssumedFieldSchema = _enum([
49281
+ "state",
49282
+ "volumeLevel",
49283
+ "isMuted",
49284
+ "source"
49285
+ ]);
49286
+ var MediaPlayerStatusSchema = object({
49287
+ /** Playback lifecycle state. */
49288
+ state: MediaPlayerStateSchema,
49289
+ /**
49290
+ * Whether the state fields are readings or the driver's own bookkeeping.
49291
+ * A slice written before this field existed parses as `reported`.
49292
+ */
49293
+ stateSource: MediaPlayerStateSourceSchema.default("reported"),
49294
+ /**
49295
+ * Which fields are the driver's own guess. `stateSource` is `assumed` when
49296
+ * this is not empty; a field not listed here is a reading even then (power
49297
+ * from a running-state source, mute guessed from the last send).
49298
+ */
49299
+ assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
49300
+ /** Volume as 0..100 inclusive. Null when the device has no volume
49301
+ * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
49302
+ volumeLevel: number$1().min(0).max(100).nullable().meta(readingMeta("percentage")),
49303
+ /** Mute toggle distinct from volume=0. Null when no mute surface.
49304
+ * Pair with `DeviceFeature.MediaPlayerMute`. */
49305
+ isMuted: boolean().nullable(),
49306
+ /** Active source / input. Empty when no source surface. */
49307
+ source: string(),
49308
+ /** Selectable sources. Empty when no source surface.
49309
+ * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
49310
+ availableSources: array(string()),
49311
+ /** Currently-playing media info. Null when nothing is playing. */
49312
+ currentMedia: MediaInfoSchema.nullable(),
49313
+ /** Current playback position in ms. Null when not seekable or
49314
+ * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
49315
+ position: number$1().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49316
+ /** Total duration of the current media in ms. Null when unknown
49317
+ * (live stream) or nothing is playing. */
49318
+ duration: number$1().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
49319
+ /** Shuffle toggle. Null when no shuffle surface.
49320
+ * Pair with `DeviceFeature.MediaPlayerShuffle`. */
49321
+ shuffle: boolean().nullable(),
49322
+ /** Repeat mode. Null when no repeat surface.
49323
+ * Pair with `DeviceFeature.MediaPlayerRepeat`. */
49324
+ repeat: MediaPlayerRepeatSchema.nullable(),
49325
+ /** Ms epoch when the slice was last updated. */
49326
+ lastChangedAt: number$1()
49327
+ });
49328
+ var stepInput = object({
49329
+ deviceId: number$1().int().nonnegative(),
49330
+ /** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
49331
+ steps: number$1().int().min(1).max(50).default(1)
49332
+ });
49333
+ var deviceOnly = object({ deviceId: number$1().int().nonnegative() });
49334
+ var mediaPlayerCapability = {
49335
+ name: "media-player",
49336
+ scope: "device",
49337
+ deviceNative: true,
49338
+ mode: "singleton",
49339
+ deviceTypes: [DeviceType.MediaPlayer],
49340
+ methods: {
49341
+ turnOn: method(deviceOnly, _void(), {
49342
+ kind: "mutation",
49343
+ auth: "protected"
49344
+ }),
49345
+ turnOff: method(deviceOnly, _void(), {
49346
+ kind: "mutation",
49347
+ auth: "protected"
49348
+ }),
49349
+ volumeUp: method(stepInput, _void(), {
49350
+ kind: "mutation",
49351
+ auth: "protected"
49352
+ }),
49353
+ volumeDown: method(stepInput, _void(), {
49354
+ kind: "mutation",
49355
+ auth: "protected"
49356
+ }),
49357
+ channelUp: method(stepInput, _void(), {
49358
+ kind: "mutation",
49359
+ auth: "protected"
49360
+ }),
49361
+ channelDown: method(stepInput, _void(), {
49362
+ kind: "mutation",
49363
+ auth: "protected"
49364
+ }),
49365
+ play: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
49366
+ kind: "mutation",
49367
+ auth: "protected"
49368
+ }),
49369
+ pause: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
49370
+ kind: "mutation",
49371
+ auth: "protected"
49372
+ }),
49373
+ stop: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
49374
+ kind: "mutation",
49375
+ auth: "protected"
49376
+ }),
49377
+ next: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
49378
+ kind: "mutation",
49379
+ auth: "protected"
49380
+ }),
49381
+ previous: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
49382
+ kind: "mutation",
49383
+ auth: "protected"
49384
+ }),
49385
+ seek: method(object({
49386
+ deviceId: number$1().int().nonnegative(),
49387
+ position: number$1().int().nonnegative()
49388
+ }), _void(), {
49389
+ kind: "mutation",
49390
+ auth: "protected"
49391
+ }),
49392
+ setVolume: method(object({
49393
+ deviceId: number$1().int().nonnegative(),
49394
+ volumeLevel: number$1().min(0).max(100)
49395
+ }), _void(), {
49396
+ kind: "mutation",
49397
+ auth: "protected"
49398
+ }),
49399
+ setMute: method(object({
49400
+ deviceId: number$1().int().nonnegative(),
49401
+ muted: boolean()
49402
+ }), _void(), {
49403
+ kind: "mutation",
49404
+ auth: "protected"
49405
+ }),
49406
+ setShuffle: method(object({
49407
+ deviceId: number$1().int().nonnegative(),
49408
+ shuffle: boolean()
49409
+ }), _void(), {
49410
+ kind: "mutation",
49411
+ auth: "protected"
49412
+ }),
49413
+ setRepeat: method(object({
49414
+ deviceId: number$1().int().nonnegative(),
49415
+ repeat: MediaPlayerRepeatSchema
49416
+ }), _void(), {
49417
+ kind: "mutation",
49418
+ auth: "protected"
49419
+ }),
49420
+ selectSource: method(object({
49421
+ deviceId: number$1().int().nonnegative(),
49422
+ source: string().min(1)
49423
+ }), _void(), {
49424
+ kind: "mutation",
49425
+ auth: "protected"
49426
+ }),
49427
+ playMedia: method(object({
49428
+ deviceId: number$1().int().nonnegative(),
49429
+ /** Media identifier / URL. */
49430
+ mediaId: string().min(1),
49431
+ /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
49432
+ * passes it through to the upstream service. */
49433
+ mediaType: string().min(1)
49434
+ }), _void(), {
49435
+ kind: "mutation",
49436
+ auth: "protected"
49437
+ })
49438
+ },
49439
+ status: {
49440
+ schema: MediaPlayerStatusSchema,
49441
+ kind: "push"
49442
+ },
49443
+ /**
49444
+ * Runtime-state slice — mirrored by the kernel. UI players read the
49445
+ * full slice for live now-playing, volume, and progress updates
49446
+ * without polling.
49447
+ */
49448
+ runtimeState: MediaPlayerStatusSchema,
49449
+ /**
49450
+ * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
49451
+ *
49452
+ * See `RuntimeStateDurability`. Enforced by
49453
+ * `scripts/check-runtime-state-durability.ts`.
49454
+ */
49455
+ durability: "session",
49456
+ /**
49457
+ * History (D850): volume (a level an operator or an automation sets; the
49458
+ * default 10 s floor folds a slider drag into its end value) and mute.
49459
+ * `position` is not recorded: it moves continuously while playing.
49460
+ */
49461
+ history: { fields: {
49462
+ volumeLevel: {
49463
+ kind: "gauge",
49464
+ unit: "%",
49465
+ deadband: 1
49466
+ },
49467
+ isMuted: { kind: "boolean" }
49468
+ } }
49469
+ };
49157
49470
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
49158
49471
  var NUMBER = {
49159
49472
  kind: "fixed",
@@ -57847,214 +58160,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
57847
58160
  ]);
57848
58161
  method(_void(), array(LoginMethodContributionSchema).readonly());
57849
58162
  /**
57850
- * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
57851
- * Apple TV, Spotify, Roku, generic OTT receivers, …) on
57852
- * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
57853
- * surface — those use `DeviceType.Camera` + the broker pipeline.
57854
- *
57855
- * The cap declares a wide method surface; not every backend supports
57856
- * every method. Drivers signal sub-capability via the
57857
- * `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
57858
- * corresponding method) so the UI gates buttons without a slice
57859
- * fetch — same pattern as `PanTiltZoom` for cameras.
57860
- */
57861
- var MediaPlayerStateSchema = _enum([
57862
- "off",
57863
- "on",
57864
- "idle",
57865
- "playing",
57866
- "paused",
57867
- "buffering",
57868
- "standby"
57869
- ]);
57870
- var MediaPlayerRepeatSchema = _enum([
57871
- "off",
57872
- "all",
57873
- "one"
57874
- ]);
57875
- var MediaInfoSchema = object({
57876
- /** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
57877
- * `podcast`, …). */
57878
- type: string(),
57879
- /** Human-readable title. */
57880
- title: string(),
57881
- /** Optional artist / channel / station label. */
57882
- artist: string().optional(),
57883
- /** Optional album / season / show name. */
57884
- album: string().optional(),
57885
- /** Optional cover-art / thumbnail URL. */
57886
- imageUrl: string().optional()
57887
- });
57888
- /**
57889
- * Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
57890
- * the device said so. `assumed`: the driver has no feedback (infrared) and
57891
- * tracks what it last SENT; it moves only after a send was accepted, and a
57892
- * human at the real remote makes it wrong. A UI shows an assumed value as a
57893
- * guess, never as a reading.
57894
- */
57895
- var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
57896
- /** The fields of a media-player status a driver can only guess. */
57897
- var MediaPlayerAssumedFieldSchema = _enum([
57898
- "state",
57899
- "volumeLevel",
57900
- "isMuted",
57901
- "source"
57902
- ]);
57903
- var MediaPlayerStatusSchema = object({
57904
- /** Playback lifecycle state. */
57905
- state: MediaPlayerStateSchema,
57906
- /**
57907
- * Whether the state fields are readings or the driver's own bookkeeping.
57908
- * A slice written before this field existed parses as `reported`.
57909
- */
57910
- stateSource: MediaPlayerStateSourceSchema.default("reported"),
57911
- /**
57912
- * Which fields are the driver's own guess. `stateSource` is `assumed` when
57913
- * this is not empty; a field not listed here is a reading even then (power
57914
- * from a running-state source, mute guessed from the last send).
57915
- */
57916
- assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
57917
- /** Volume as 0..100 inclusive. Null when the device has no volume
57918
- * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
57919
- volumeLevel: number$1().min(0).max(100).nullable().meta(readingMeta("percentage")),
57920
- /** Mute toggle distinct from volume=0. Null when no mute surface.
57921
- * Pair with `DeviceFeature.MediaPlayerMute`. */
57922
- isMuted: boolean().nullable(),
57923
- /** Active source / input. Empty when no source surface. */
57924
- source: string(),
57925
- /** Selectable sources. Empty when no source surface.
57926
- * Pair with `DeviceFeature.MediaPlayerSelectSource`. */
57927
- availableSources: array(string()),
57928
- /** Currently-playing media info. Null when nothing is playing. */
57929
- currentMedia: MediaInfoSchema.nullable(),
57930
- /** Current playback position in ms. Null when not seekable or
57931
- * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
57932
- position: number$1().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57933
- /** Total duration of the current media in ms. Null when unknown
57934
- * (live stream) or nothing is playing. */
57935
- duration: number$1().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
57936
- /** Shuffle toggle. Null when no shuffle surface.
57937
- * Pair with `DeviceFeature.MediaPlayerShuffle`. */
57938
- shuffle: boolean().nullable(),
57939
- /** Repeat mode. Null when no repeat surface.
57940
- * Pair with `DeviceFeature.MediaPlayerRepeat`. */
57941
- repeat: MediaPlayerRepeatSchema.nullable(),
57942
- /** Ms epoch when the slice was last updated. */
57943
- lastChangedAt: number$1()
57944
- });
57945
- var mediaPlayerCapability = {
57946
- name: "media-player",
57947
- scope: "device",
57948
- deviceNative: true,
57949
- mode: "singleton",
57950
- deviceTypes: [DeviceType.MediaPlayer],
57951
- methods: {
57952
- play: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
57953
- kind: "mutation",
57954
- auth: "protected"
57955
- }),
57956
- pause: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
57957
- kind: "mutation",
57958
- auth: "protected"
57959
- }),
57960
- stop: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
57961
- kind: "mutation",
57962
- auth: "protected"
57963
- }),
57964
- next: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
57965
- kind: "mutation",
57966
- auth: "protected"
57967
- }),
57968
- previous: method(object({ deviceId: number$1().int().nonnegative() }), _void(), {
57969
- kind: "mutation",
57970
- auth: "protected"
57971
- }),
57972
- seek: method(object({
57973
- deviceId: number$1().int().nonnegative(),
57974
- position: number$1().int().nonnegative()
57975
- }), _void(), {
57976
- kind: "mutation",
57977
- auth: "protected"
57978
- }),
57979
- setVolume: method(object({
57980
- deviceId: number$1().int().nonnegative(),
57981
- volumeLevel: number$1().min(0).max(100)
57982
- }), _void(), {
57983
- kind: "mutation",
57984
- auth: "protected"
57985
- }),
57986
- setMute: method(object({
57987
- deviceId: number$1().int().nonnegative(),
57988
- muted: boolean()
57989
- }), _void(), {
57990
- kind: "mutation",
57991
- auth: "protected"
57992
- }),
57993
- setShuffle: method(object({
57994
- deviceId: number$1().int().nonnegative(),
57995
- shuffle: boolean()
57996
- }), _void(), {
57997
- kind: "mutation",
57998
- auth: "protected"
57999
- }),
58000
- setRepeat: method(object({
58001
- deviceId: number$1().int().nonnegative(),
58002
- repeat: MediaPlayerRepeatSchema
58003
- }), _void(), {
58004
- kind: "mutation",
58005
- auth: "protected"
58006
- }),
58007
- selectSource: method(object({
58008
- deviceId: number$1().int().nonnegative(),
58009
- source: string().min(1)
58010
- }), _void(), {
58011
- kind: "mutation",
58012
- auth: "protected"
58013
- }),
58014
- playMedia: method(object({
58015
- deviceId: number$1().int().nonnegative(),
58016
- /** Media identifier / URL. */
58017
- mediaId: string().min(1),
58018
- /** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
58019
- * passes it through to the upstream service. */
58020
- mediaType: string().min(1)
58021
- }), _void(), {
58022
- kind: "mutation",
58023
- auth: "protected"
58024
- })
58025
- },
58026
- status: {
58027
- schema: MediaPlayerStatusSchema,
58028
- kind: "push"
58029
- },
58030
- /**
58031
- * Runtime-state slice — mirrored by the kernel. UI players read the
58032
- * full slice for live now-playing, volume, and progress updates
58033
- * without polling.
58034
- */
58035
- runtimeState: MediaPlayerStatusSchema,
58036
- /**
58037
- * Runtime-state durability: **session** — a restored transport position describes a playback that stopped when the hub did.
58038
- *
58039
- * See `RuntimeStateDurability`. Enforced by
58040
- * `scripts/check-runtime-state-durability.ts`.
58041
- */
58042
- durability: "session",
58043
- /**
58044
- * History (D850): volume (a level an operator or an automation sets; the
58045
- * default 10 s floor folds a slider drag into its end value) and mute.
58046
- * `position` is not recorded: it moves continuously while playing.
58047
- */
58048
- history: { fields: {
58049
- volumeLevel: {
58050
- kind: "gauge",
58051
- unit: "%",
58052
- deadband: 1
58053
- },
58054
- isMuted: { kind: "boolean" }
58055
- } }
58056
- };
58057
- /**
58058
58163
  * mesh-network — collection cap for mesh-VPN providers.
58059
58164
  *
58060
58165
  * Different shape from `network-access` (public tunnels):
@@ -66813,6 +66918,21 @@ var NcTemplateVarKindSchema = _enum([
66813
66918
  "sound",
66814
66919
  "system"
66815
66920
  ]);
66921
+ /**
66922
+ * The default title/body a rule of one SITUATION is delivered with when the
66923
+ * operator wrote no template — served so an editor can pre-fill it (and the
66924
+ * operator can rebuild it from placeholders) instead of starting from a blank
66925
+ * box that, once saved, silently replaces it. `id` names the situation inside
66926
+ * a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
66927
+ * appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
66928
+ * the hub's current language and with its overrides applied.
66929
+ */
66930
+ var NcTemplateDefaultSchema = object({
66931
+ id: _enum(["appliance-cycle"]),
66932
+ kind: NcRuleKindSchema,
66933
+ title: string(),
66934
+ body: string()
66935
+ });
66816
66936
  /** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
66817
66937
  var NcTemplateFamilySchema = _enum([
66818
66938
  "rule",
@@ -67668,7 +67788,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
67668
67788
  }), method(object({}), object({
67669
67789
  catalog: array(NcConditionDescriptorSchema),
67670
67790
  taxonomy: NcTaxonomySchema.optional()
67671
- })), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
67791
+ })), method(object({}), object({
67792
+ vars: array(NcTemplateVarDescriptorSchema),
67793
+ /** Per-situation default texts; absent on a hub that predates them. */
67794
+ defaults: array(NcTemplateDefaultSchema)
67795
+ })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
67672
67796
  kind: "mutation",
67673
67797
  access: "view"
67674
67798
  }), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), NcRuleLastRunsSchema, { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
@@ -70259,6 +70383,14 @@ var powerMeterCapability = {
70259
70383
  deviceNative: true,
70260
70384
  mode: "singleton",
70261
70385
  deviceTypes: POWER_METER_DEVICE_TYPES,
70386
+ deviceConfig: { ui: {
70387
+ kind: "widget",
70388
+ widgetId: "host/power-meter-line",
70389
+ tab: "power",
70390
+ placement: "hero",
70391
+ label: "Power",
70392
+ order: 4
70393
+ } },
70262
70394
  methods: {},
70263
70395
  status: {
70264
70396
  schema: PowerMeterStatusSchema,
@@ -73820,7 +73952,13 @@ var RemoteMediaCommandSchema = _enum([
73820
73952
  "next",
73821
73953
  "previous",
73822
73954
  "setMute",
73823
- "selectSource"
73955
+ "selectSource",
73956
+ "turnOn",
73957
+ "turnOff",
73958
+ "volumeUp",
73959
+ "volumeDown",
73960
+ "channelUp",
73961
+ "channelDown"
73824
73962
  ]);
73825
73963
  /** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
73826
73964
  var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
@@ -73841,7 +73979,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
73841
73979
  }), RemoteSendResultSchema, {
73842
73980
  kind: "mutation",
73843
73981
  auth: "protected"
73844
- }), method(deviceInput.extend({
73982
+ }), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
73845
73983
  command: RemoteMediaCommandSchema,
73846
73984
  muted: boolean().optional(),
73847
73985
  source: string().optional(),
@@ -76087,7 +76225,11 @@ listSignals: method(_void(), array(StreamSignalSchema).readonly()) }
76087
76225
  * `getSettingsUISchema()` surface.
76088
76226
  */
76089
76227
  var SwitchStatusSchema = object({
76090
- on: boolean().meta({ label: "On" }),
76228
+ /**
76229
+ * `null` = the device has never reported its state (a measurement that
76230
+ * has not happened is unknown, never `false`).
76231
+ */
76232
+ on: boolean().nullable().meta({ label: "On" }),
76091
76233
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
76092
76234
  lastChangedAt: number$1()
76093
76235
  });
@@ -76112,7 +76254,7 @@ var switchCapability = {
76112
76254
  }) },
76113
76255
  events: { onStateChanged: { data: object({
76114
76256
  deviceId: number$1(),
76115
- on: boolean(),
76257
+ on: boolean().nullable(),
76116
76258
  lastChangedAt: number$1()
76117
76259
  }) } },
76118
76260
  status: {
@@ -79669,6 +79811,7 @@ function buildDescriptor(capName, kind) {
79669
79811
  * contributes one entry per bound cap present in this map.
79670
79812
  */
79671
79813
  var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
79814
+ DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
79672
79815
  var btn = (idSuffix, row, col, init) => ({
79673
79816
  idSuffix,
79674
79817
  row,
@@ -85847,6 +85990,20 @@ Object.freeze({
85847
85990
  access: "view",
85848
85991
  role: "admin"
85849
85992
  },
85993
+ "mediaPlayer.channelDown": {
85994
+ capName: "media-player",
85995
+ capScope: "device",
85996
+ addonId: null,
85997
+ access: "create",
85998
+ role: "viewer"
85999
+ },
86000
+ "mediaPlayer.channelUp": {
86001
+ capName: "media-player",
86002
+ capScope: "device",
86003
+ addonId: null,
86004
+ access: "create",
86005
+ role: "viewer"
86006
+ },
85850
86007
  "mediaPlayer.getStatus": {
85851
86008
  capName: "media-player",
85852
86009
  capScope: "device",
@@ -85938,6 +86095,34 @@ Object.freeze({
85938
86095
  access: "create",
85939
86096
  role: "viewer"
85940
86097
  },
86098
+ "mediaPlayer.turnOff": {
86099
+ capName: "media-player",
86100
+ capScope: "device",
86101
+ addonId: null,
86102
+ access: "create",
86103
+ role: "viewer"
86104
+ },
86105
+ "mediaPlayer.turnOn": {
86106
+ capName: "media-player",
86107
+ capScope: "device",
86108
+ addonId: null,
86109
+ access: "create",
86110
+ role: "viewer"
86111
+ },
86112
+ "mediaPlayer.volumeDown": {
86113
+ capName: "media-player",
86114
+ capScope: "device",
86115
+ addonId: null,
86116
+ access: "create",
86117
+ role: "viewer"
86118
+ },
86119
+ "mediaPlayer.volumeUp": {
86120
+ capName: "media-player",
86121
+ capScope: "device",
86122
+ addonId: null,
86123
+ access: "create",
86124
+ role: "viewer"
86125
+ },
85941
86126
  "meshNetwork.getStatus": {
85942
86127
  capName: "mesh-network",
85943
86128
  capScope: "system",
@@ -89494,6 +89679,13 @@ Object.freeze({
89494
89679
  access: "create",
89495
89680
  role: "admin"
89496
89681
  },
89682
+ "remote.getAssumedStatus": {
89683
+ capName: "remote",
89684
+ capScope: "system",
89685
+ addonId: null,
89686
+ access: "view",
89687
+ role: "admin"
89688
+ },
89497
89689
  "remote.getConfig": {
89498
89690
  capName: "remote",
89499
89691
  capScope: "system",
@@ -93160,6 +93352,16 @@ Object.freeze({
93160
93352
  form: "single",
93161
93353
  optional: true
93162
93354
  }],
93355
+ "mediaPlayer.channelDown": [{
93356
+ name: "deviceId",
93357
+ form: "single",
93358
+ optional: false
93359
+ }],
93360
+ "mediaPlayer.channelUp": [{
93361
+ name: "deviceId",
93362
+ form: "single",
93363
+ optional: false
93364
+ }],
93163
93365
  "mediaPlayer.getStatus": [{
93164
93366
  name: "deviceId",
93165
93367
  form: "single",
@@ -93225,6 +93427,26 @@ Object.freeze({
93225
93427
  form: "single",
93226
93428
  optional: false
93227
93429
  }],
93430
+ "mediaPlayer.turnOff": [{
93431
+ name: "deviceId",
93432
+ form: "single",
93433
+ optional: false
93434
+ }],
93435
+ "mediaPlayer.turnOn": [{
93436
+ name: "deviceId",
93437
+ form: "single",
93438
+ optional: false
93439
+ }],
93440
+ "mediaPlayer.volumeDown": [{
93441
+ name: "deviceId",
93442
+ form: "single",
93443
+ optional: false
93444
+ }],
93445
+ "mediaPlayer.volumeUp": [{
93446
+ name: "deviceId",
93447
+ form: "single",
93448
+ optional: false
93449
+ }],
93228
93450
  "meter.getStatus": [{
93229
93451
  name: "deviceId",
93230
93452
  form: "single",
@@ -94390,6 +94612,11 @@ Object.freeze({
94390
94612
  form: "single",
94391
94613
  optional: true
94392
94614
  }],
94615
+ "remote.getAssumedStatus": [{
94616
+ name: "deviceId",
94617
+ form: "single",
94618
+ optional: false
94619
+ }],
94393
94620
  "remote.getConfig": [{
94394
94621
  name: "deviceId",
94395
94622
  form: "single",
@@ -95852,6 +96079,7 @@ var STATE_SUMMARY_VARS = [
95852
96079
  [...RULE_TEXT],
95853
96080
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
95854
96081
  ([...RULE_TEXT], [...COMBINED_ALARM]),
96082
+ ([...RULE_TEXT], [...COMBINED_ALARM]),
95855
96083
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
95856
96084
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
95857
96085
  ([...RULE_TEXT], [...CONSUMABLE]),
@@ -141978,6 +142206,7 @@ function isKnownCap(cap) {
141978
142206
  var EchoSessionRegistry = class {
141979
142207
  #resolve = null;
141980
142208
  #dnd = /* @__PURE__ */ new Map();
142209
+ #volumes = /* @__PURE__ */ new Map();
141981
142210
  setResolver(fn) {
141982
142211
  this.#resolve = fn;
141983
142212
  }
@@ -142000,9 +142229,26 @@ var EchoSessionRegistry = class {
142000
142229
  });
142001
142230
  return pending;
142002
142231
  }
142232
+ /** Volume and mute of every device of the account. A failed read is not kept for the next device. */
142233
+ volumes(brokerId, now) {
142234
+ const shared = this.#volumes.get(brokerId);
142235
+ if (shared !== void 0 && now - shared.at < 25e3) return shared.pending;
142236
+ const session = this.get(brokerId);
142237
+ if (session === null) return Promise.reject(/* @__PURE__ */ new Error("no Echo session for this account"));
142238
+ const pending = session.deviceVolumes();
142239
+ this.#volumes.set(brokerId, {
142240
+ at: now,
142241
+ pending
142242
+ });
142243
+ pending.catch(() => {
142244
+ if (this.#volumes.get(brokerId)?.pending === pending) this.#volumes.delete(brokerId);
142245
+ });
142246
+ return pending;
142247
+ }
142003
142248
  clear() {
142004
142249
  this.#resolve = null;
142005
142250
  this.#dnd.clear();
142251
+ this.#volumes.clear();
142006
142252
  }
142007
142253
  };
142008
142254
  /** The single in-process registry shared across the addon. */
@@ -142085,6 +142331,18 @@ function mapPlayerInfoToStatus(info, input) {
142085
142331
  repeat: repeatOf(info.transport?.repeat)
142086
142332
  };
142087
142333
  }
142334
+ /**
142335
+ * Fill the volume and mute the player read left null from the account's volume
142336
+ * list. The player read wins when it has a value; a value neither source has stays null.
142337
+ */
142338
+ function withListedVolume(status, listed) {
142339
+ if (listed === void 0) return status;
142340
+ return {
142341
+ ...status,
142342
+ volumeLevel: status.volumeLevel ?? percent(listed.volume),
142343
+ isMuted: status.isMuted ?? listed.muted
142344
+ };
142345
+ }
142088
142346
  //#endregion
142089
142347
  //#region src/speak/worker-protocol.ts
142090
142348
  /** Line-delimited JSON the long-lived Python worker speaks. One object per line. */
@@ -142202,6 +142460,19 @@ function parseDndState(result) {
142202
142460
  if (!parsed.success) throw new Error("worker returned an unexpected Do Not Disturb list");
142203
142461
  return new Map(parsed.data.devices.map((entry) => [entry.serial, entry.enabled]));
142204
142462
  }
142463
+ var DeviceVolumeEntrySchema = object({
142464
+ serial: string().min(1),
142465
+ volume: number$1().finite().optional(),
142466
+ muted: boolean().optional()
142467
+ });
142468
+ function parseDeviceVolumes(result) {
142469
+ const parsed = object({ devices: array(DeviceVolumeEntrySchema) }).safeParse(result);
142470
+ if (!parsed.success) throw new Error("worker returned an unexpected device volume list");
142471
+ return new Map(parsed.data.devices.map((entry) => [entry.serial, {
142472
+ volume: entry.volume ?? null,
142473
+ muted: entry.muted ?? null
142474
+ }]));
142475
+ }
142205
142476
  //#endregion
142206
142477
  //#region src/devices/alexa-echo-device.ts
142207
142478
  /**
@@ -142216,6 +142487,8 @@ var alexaEchoDeviceSchema = object({
142216
142487
  system: literal("alexa").optional(),
142217
142488
  integrationId: string().optional()
142218
142489
  });
142490
+ /** Percent points one volume-up / volume-down moves an Echo. */
142491
+ var VOLUME_STEP = 10;
142219
142492
  var COLD_DND = {
142220
142493
  enabled: null,
142221
142494
  lastChangedAt: 0
@@ -142236,6 +142509,7 @@ var AlexaEchoDevice = class extends BaseDevice {
142236
142509
  rereadTimer = null;
142237
142510
  polling = false;
142238
142511
  failureLogged = false;
142512
+ volumeFailureLogged = false;
142239
142513
  health = "ok";
142240
142514
  constructor(ctx) {
142241
142515
  const persisted = alexaEchoDeviceSchema.parse(ctx.persistedConfig ?? {});
@@ -142308,12 +142582,11 @@ var AlexaEchoDevice = class extends BaseDevice {
142308
142582
  }
142309
142583
  const deviceType = this.amazonDeviceType !== "" ? this.amazonDeviceType : listed?.deviceType ?? "";
142310
142584
  try {
142311
- const info = await session.playerState(this.serial, deviceType);
142312
- const now = Date.now();
142313
- this.runtimeState.setCapState("media-player", mapPlayerInfoToStatus(info, {
142585
+ const fromPlayer = mapPlayerInfoToStatus(await session.playerState(this.serial, deviceType), {
142314
142586
  online: true,
142315
- now
142316
- }));
142587
+ now: Date.now()
142588
+ });
142589
+ this.runtimeState.setCapState("media-player", await this.completeVolume(fromPlayer));
142317
142590
  this.markOnline(true);
142318
142591
  this.noteRecovered();
142319
142592
  } catch (err) {
@@ -142322,6 +142595,29 @@ var AlexaEchoDevice = class extends BaseDevice {
142322
142595
  }
142323
142596
  await this.readDoNotDisturb();
142324
142597
  }
142598
+ /**
142599
+ * An idle Echo's player read carries no volume. Ask the account's volume list
142600
+ * (shared by every Echo of the account in this cycle); a failed read leaves null.
142601
+ */
142602
+ async completeVolume(status) {
142603
+ if (status.volumeLevel !== null && status.isMuted !== null) return status;
142604
+ try {
142605
+ const all = await alexaEchoSessions.volumes(this.brokerId, Date.now());
142606
+ if (this.volumeFailureLogged) this.ctx.logger.info("alexa echo: the volume list reads again", { tags: { deviceId: this.id } });
142607
+ this.volumeFailureLogged = false;
142608
+ return withListedVolume(status, all.get(this.serial));
142609
+ } catch (err) {
142610
+ if (err instanceof RateLimitedError) return status;
142611
+ if (!this.volumeFailureLogged) {
142612
+ this.volumeFailureLogged = true;
142613
+ this.ctx.logger.warn("alexa echo: could not read the volume list, volume stays unknown", {
142614
+ tags: { deviceId: this.id },
142615
+ meta: { error: err instanceof Error ? err.message : String(err) }
142616
+ });
142617
+ }
142618
+ return status;
142619
+ }
142620
+ }
142325
142621
  async readDoNotDisturb() {
142326
142622
  try {
142327
142623
  const enabled = (await alexaEchoSessions.dnd(this.brokerId, Date.now())).get(this.serial);
@@ -142357,6 +142653,14 @@ var AlexaEchoDevice = class extends BaseDevice {
142357
142653
  await this.session().npCommand(this.serial, command);
142358
142654
  this.rereadSoon();
142359
142655
  }
142656
+ /** Amazon has no step command: move from the last known level, refuse when there is none. */
142657
+ async stepVolume(delta) {
142658
+ const level = this.runtimeState.getCapState("media-player")?.volumeLevel;
142659
+ if (level === null || level === void 0) throw new Error("the Echo volume is not known yet");
142660
+ const next = Math.max(0, Math.min(100, level + delta));
142661
+ await this.session().setVolume(this.serial, next);
142662
+ this.rereadSoon();
142663
+ }
142360
142664
  registerMediaPlayer() {
142361
142665
  this.ctx.registerNativeCap(mediaPlayerCapability, {
142362
142666
  getStatus: async () => this.runtimeState.getCapState("media-player") ?? emptyEchoStatus("off", 0),
@@ -142369,6 +142673,12 @@ var AlexaEchoDevice = class extends BaseDevice {
142369
142673
  this.rereadSoon();
142370
142674
  },
142371
142675
  stop: async () => unsupported("stop"),
142676
+ turnOn: async () => unsupported("power"),
142677
+ turnOff: async () => unsupported("power"),
142678
+ volumeUp: async () => this.stepVolume(VOLUME_STEP),
142679
+ volumeDown: async () => this.stepVolume(-10),
142680
+ channelUp: async () => unsupported("channel steps"),
142681
+ channelDown: async () => unsupported("channel steps"),
142372
142682
  seek: async () => unsupported("seek"),
142373
142683
  setMute: async () => unsupported("mute"),
142374
142684
  setShuffle: async () => unsupported("shuffle"),
@@ -142873,6 +143183,11 @@ var BrokerEcho = class {
142873
143183
  await this.requireReady();
142874
143184
  return this.limited(async () => parseDndState(await this.deps.worker.request({ op: "dnd_state" })));
142875
143185
  }
143186
+ /** Speaker volume and mute of every device of the account, by serial, in one call. */
143187
+ async deviceVolumes() {
143188
+ await this.requireReady();
143189
+ return this.limited(async () => parseDeviceVolumes(await this.deps.worker.request({ op: "device_volumes" })));
143190
+ }
142876
143191
  async setDnd(serial, enabled) {
142877
143192
  await this.requireReady();
142878
143193
  await this.limited(() => this.deps.worker.request({