@camstack/addon-google-services 0.2.9 → 0.2.10

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.
@@ -5542,7 +5542,7 @@ var ZodIssueCode = {
5542
5542
  var ZodFirstPartyTypeKind;
5543
5543
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5544
5544
  //#endregion
5545
- //#region ../types/dist/sleep-bZIhUvjd.mjs
5545
+ //#region ../types/dist/sleep-BOjuyoPC.mjs
5546
5546
  /**
5547
5547
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5548
5548
  * window to float samples (D455).
@@ -8771,7 +8771,7 @@ function userError(code, params, englishFallback) {
8771
8771
  return new CamstackError(code, params, englishFallback);
8772
8772
  }
8773
8773
  //#endregion
8774
- //#region ../types/dist/err-msg--2RrYiLO.mjs
8774
+ //#region ../types/dist/err-msg-BXKzRBwg.mjs
8775
8775
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
8776
8776
  DeviceType["Camera"] = "camera";
8777
8777
  DeviceType["Hub"] = "hub";
@@ -8863,6 +8863,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
8863
8863
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
8864
8864
  * integrations sharing the same food/desiccant/hopper surface. */
8865
8865
  DeviceType["PetFeeder"] = "pet-feeder";
8866
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
8867
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
8868
+ * Sources: native Bosch Home Connect, reusable by similar integrations. */
8869
+ DeviceType["Dishwasher"] = "dishwasher";
8866
8870
  return DeviceType;
8867
8871
  }({});
8868
8872
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -9110,7 +9114,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
9110
9114
  return DeviceRole;
9111
9115
  }({});
9112
9116
  //#endregion
9113
- //#region ../types/dist/composition-DOwQB_Ua.mjs
9117
+ //#region ../types/dist/composition-CB6PMT9p.mjs
9114
9118
  /**
9115
9119
  * The ffmpeg build every CamStack node downloads, per platform and arch.
9116
9120
  *
@@ -13632,8 +13636,13 @@ var AlertSeveritySchema = _enum([
13632
13636
  "info",
13633
13637
  "success",
13634
13638
  "warning",
13635
- "error"
13639
+ "error",
13640
+ "critical"
13636
13641
  ]);
13642
+ var AlertActionSchema = object({
13643
+ label: string(),
13644
+ href: string().optional()
13645
+ });
13637
13646
  var AlertStatusSchema = _enum([
13638
13647
  "active",
13639
13648
  "in-progress",
@@ -13651,6 +13660,7 @@ var AlertSchema = object({
13651
13660
  severity: AlertSeveritySchema,
13652
13661
  title: string(),
13653
13662
  message: string(),
13663
+ action: AlertActionSchema.optional(),
13654
13664
  status: AlertStatusSchema,
13655
13665
  progress: number().optional(),
13656
13666
  read: boolean(),
@@ -53647,7 +53657,7 @@ var BatteryStatusSchema = object({
53647
53657
  */
53648
53658
  binary: boolean().optional()
53649
53659
  });
53650
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, method(object({
53660
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, method(object({
53651
53661
  deviceId: number(),
53652
53662
  /** Bound on the wait. Sensible range 3000–10000ms. */
53653
53663
  timeoutMs: number().int().min(500).max(3e4).default(8e3)
@@ -54533,6 +54543,98 @@ object({
54533
54543
  deviceId: number(),
54534
54544
  status: DeviceStatusSchema
54535
54545
  });
54546
+ /**
54547
+ * Smart-dishwasher cap — program lifecycle, door / remote-start gates,
54548
+ * consumable levels (salt, rinse aid) and common program options.
54549
+ *
54550
+ * Sources: native Bosch Home Connect (`dishwasher` device type). Reusable
54551
+ * by any integration that exposes the same program / option surface.
54552
+ *
54553
+ * `kind: 'poll'` — cloud REST APIs typically have no push channel; the
54554
+ * provider refreshes the slice on an interval and eagerly after commands.
54555
+ */
54556
+ var DishwasherStateSchema = _enum([
54557
+ "idle",
54558
+ "running",
54559
+ "paused",
54560
+ "finished",
54561
+ "error",
54562
+ "unavailable"
54563
+ ]);
54564
+ var DishwasherConsumableLevelSchema = _enum([
54565
+ "ok",
54566
+ "low",
54567
+ "empty"
54568
+ ]);
54569
+ object({
54570
+ state: DishwasherStateSchema,
54571
+ /** Active program id/name, or null when none selected / unknown. */
54572
+ program: string().nullable(),
54573
+ /** Program tokens the hardware accepts — drives the UI selector. */
54574
+ availablePrograms: array(string()),
54575
+ /** Minutes remaining on the active cycle, or null when not running. */
54576
+ remainingMinutes: number().int().nonnegative().nullable(),
54577
+ /** Cycle completion 0..100, or null when not applicable. */
54578
+ progressPercent: number().min(0).max(100).nullable(),
54579
+ /** True when the door is reported open. */
54580
+ doorOpen: boolean(),
54581
+ /** True when remote start is allowed (door closed, power on, etc.). */
54582
+ remoteStartAllowed: boolean(),
54583
+ /** Main power / switched-on state. */
54584
+ powerOn: boolean(),
54585
+ saltLevel: DishwasherConsumableLevelSchema.nullable(),
54586
+ rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
54587
+ halfLoad: boolean(),
54588
+ hygienePlus: boolean(),
54589
+ errorCode: number().nullable(),
54590
+ errorLabel: string().nullable(),
54591
+ lastFetchedAt: number()
54592
+ });
54593
+ DeviceType.Dishwasher, method(object({ deviceId: number().int().nonnegative() }), _void(), {
54594
+ kind: "mutation",
54595
+ auth: "protected"
54596
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54597
+ kind: "mutation",
54598
+ auth: "protected"
54599
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54600
+ kind: "mutation",
54601
+ auth: "protected"
54602
+ }), method(object({
54603
+ deviceId: number().int().nonnegative(),
54604
+ program: string().min(1)
54605
+ }), _void(), {
54606
+ kind: "mutation",
54607
+ auth: "protected"
54608
+ }), method(object({
54609
+ deviceId: number().int().nonnegative(),
54610
+ on: boolean()
54611
+ }), _void(), {
54612
+ kind: "mutation",
54613
+ auth: "protected"
54614
+ }), method(object({
54615
+ deviceId: number().int().nonnegative(),
54616
+ on: boolean()
54617
+ }), _void(), {
54618
+ kind: "mutation",
54619
+ auth: "protected"
54620
+ }), method(object({
54621
+ deviceId: number().int().nonnegative(),
54622
+ on: boolean()
54623
+ }), _void(), {
54624
+ kind: "mutation",
54625
+ auth: "protected"
54626
+ }), method(object({
54627
+ deviceId: number().int().nonnegative(),
54628
+ key: string().min(1),
54629
+ value: union([
54630
+ boolean(),
54631
+ string(),
54632
+ number()
54633
+ ])
54634
+ }), _void(), {
54635
+ kind: "mutation",
54636
+ auth: "protected"
54637
+ });
54536
54638
  object({
54537
54639
  /** Ms epoch of the last press. null = never observed since this provider started. */
54538
54640
  lastPressedAt: number().nullable(),
@@ -55098,6 +55200,13 @@ object({
55098
55200
  lastChangedAt: number()
55099
55201
  });
55100
55202
  DeviceType.Sensor;
55203
+ object({
55204
+ /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
55205
+ position: number().min(0).max(100).nullable(),
55206
+ /** Ms epoch when the slice was last updated (push or poll). */
55207
+ lastFetchedAt: number()
55208
+ });
55209
+ DeviceType.Thermostat;
55101
55210
  object({
55102
55211
  /** Whether the humidifier is currently on. */
55103
55212
  on: boolean(),
@@ -62164,7 +62273,7 @@ var AccessoryKind = {
62164
62273
  PrivacyMask: DeviceRole.PrivacyMask
62165
62274
  };
62166
62275
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
62167
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
62276
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
62168
62277
  new Set(Object.values(DeviceType));
62169
62278
  Object.freeze({
62170
62279
  "accessories.getStatus": {
@@ -64585,6 +64694,60 @@ Object.freeze({
64585
64694
  addonId: null,
64586
64695
  access: "view"
64587
64696
  },
64697
+ "dishwasher.getStatus": {
64698
+ capName: "dishwasher",
64699
+ capScope: "device",
64700
+ addonId: null,
64701
+ access: "view"
64702
+ },
64703
+ "dishwasher.pause": {
64704
+ capName: "dishwasher",
64705
+ capScope: "device",
64706
+ addonId: null,
64707
+ access: "create"
64708
+ },
64709
+ "dishwasher.setHalfLoad": {
64710
+ capName: "dishwasher",
64711
+ capScope: "device",
64712
+ addonId: null,
64713
+ access: "create"
64714
+ },
64715
+ "dishwasher.setHygienePlus": {
64716
+ capName: "dishwasher",
64717
+ capScope: "device",
64718
+ addonId: null,
64719
+ access: "create"
64720
+ },
64721
+ "dishwasher.setOption": {
64722
+ capName: "dishwasher",
64723
+ capScope: "device",
64724
+ addonId: null,
64725
+ access: "create"
64726
+ },
64727
+ "dishwasher.setPower": {
64728
+ capName: "dishwasher",
64729
+ capScope: "device",
64730
+ addonId: null,
64731
+ access: "create"
64732
+ },
64733
+ "dishwasher.setProgram": {
64734
+ capName: "dishwasher",
64735
+ capScope: "device",
64736
+ addonId: null,
64737
+ access: "create"
64738
+ },
64739
+ "dishwasher.start": {
64740
+ capName: "dishwasher",
64741
+ capScope: "device",
64742
+ addonId: null,
64743
+ access: "create"
64744
+ },
64745
+ "dishwasher.stop": {
64746
+ capName: "dishwasher",
64747
+ capScope: "device",
64748
+ addonId: null,
64749
+ access: "create"
64750
+ },
64588
64751
  "doorbell.getStatus": {
64589
64752
  capName: "doorbell",
64590
64753
  capScope: "device",
@@ -64795,6 +64958,12 @@ Object.freeze({
64795
64958
  addonId: null,
64796
64959
  access: "view"
64797
64960
  },
64961
+ "heatingValve.getStatus": {
64962
+ capName: "heating-valve",
64963
+ capScope: "device",
64964
+ addonId: null,
64965
+ access: "view"
64966
+ },
64798
64967
  "humidifier.getStatus": {
64799
64968
  capName: "humidifier",
64800
64969
  capScope: "device",
@@ -70382,7 +70551,7 @@ Object.freeze({
70382
70551
  access: "create"
70383
70552
  }
70384
70553
  });
70385
- 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;
70554
+ 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.Dishwasher;
70386
70555
  DeviceFeature.BatteryOperated;
70387
70556
  Object.freeze({
70388
70557
  "accessories.getStatus": [{
@@ -71206,6 +71375,51 @@ Object.freeze({
71206
71375
  form: "single",
71207
71376
  optional: false
71208
71377
  }],
71378
+ "dishwasher.getStatus": [{
71379
+ name: "deviceId",
71380
+ form: "single",
71381
+ optional: false
71382
+ }],
71383
+ "dishwasher.pause": [{
71384
+ name: "deviceId",
71385
+ form: "single",
71386
+ optional: false
71387
+ }],
71388
+ "dishwasher.setHalfLoad": [{
71389
+ name: "deviceId",
71390
+ form: "single",
71391
+ optional: false
71392
+ }],
71393
+ "dishwasher.setHygienePlus": [{
71394
+ name: "deviceId",
71395
+ form: "single",
71396
+ optional: false
71397
+ }],
71398
+ "dishwasher.setOption": [{
71399
+ name: "deviceId",
71400
+ form: "single",
71401
+ optional: false
71402
+ }],
71403
+ "dishwasher.setPower": [{
71404
+ name: "deviceId",
71405
+ form: "single",
71406
+ optional: false
71407
+ }],
71408
+ "dishwasher.setProgram": [{
71409
+ name: "deviceId",
71410
+ form: "single",
71411
+ optional: false
71412
+ }],
71413
+ "dishwasher.start": [{
71414
+ name: "deviceId",
71415
+ form: "single",
71416
+ optional: false
71417
+ }],
71418
+ "dishwasher.stop": [{
71419
+ name: "deviceId",
71420
+ form: "single",
71421
+ optional: false
71422
+ }],
71209
71423
  "doorbell.getStatus": [{
71210
71424
  name: "deviceId",
71211
71425
  form: "single",
@@ -71275,6 +71489,11 @@ Object.freeze({
71275
71489
  form: "single",
71276
71490
  optional: false
71277
71491
  }],
71492
+ "heatingValve.getStatus": [{
71493
+ name: "deviceId",
71494
+ form: "single",
71495
+ optional: false
71496
+ }],
71278
71497
  "humidifier.getStatus": [{
71279
71498
  name: "deviceId",
71280
71499
  form: "single",
@@ -74970,6 +75189,40 @@ function createGmailSmtpProvider(deps) {
74970
75189
  };
74971
75190
  }
74972
75191
  //#endregion
75192
+ //#region src/google-broker-reconnect-alert.ts
75193
+ function googleBrokerReconnectAlertId(brokerId) {
75194
+ return `google-broker:${brokerId}`;
75195
+ }
75196
+ function isInvalidGrantRefreshError(err) {
75197
+ return (err instanceof Error ? err.message : String(err)).includes("invalid_grant");
75198
+ }
75199
+ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75200
+ const pushLine = `${brokerName}: Google session expired — sign in again`;
75201
+ return {
75202
+ id: googleBrokerReconnectAlertId(brokerId),
75203
+ category: "google.broker-reconnect",
75204
+ severity: "critical",
75205
+ title: "Google needs a new sign-in",
75206
+ message: `${brokerName} needs you to sign in to Google again.`,
75207
+ action: {
75208
+ label: "Sign in again",
75209
+ href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`
75210
+ },
75211
+ status: "active",
75212
+ read: false,
75213
+ createdAt: Date.now(),
75214
+ updatedAt: Date.now(),
75215
+ source: {
75216
+ type: "google-broker",
75217
+ id: brokerId
75218
+ },
75219
+ metadata: {
75220
+ episodeKey: "reconnect",
75221
+ pushLine
75222
+ }
75223
+ };
75224
+ }
75225
+ //#endregion
74973
75226
  //#region src/google-oauth.ts
74974
75227
  /**
74975
75228
  * Google OAuth2 authorization-code + refresh for Calendar API.
@@ -75585,6 +75838,7 @@ var GoogleServicesAddon = class extends BaseAddon {
75585
75838
  };
75586
75839
  this.registry.setOauth(next);
75587
75840
  await this.persistOauthAccounts();
75841
+ if (next.refreshToken.length > 0) this.resolveGoogleBrokerReconnectAlert(brokerId);
75588
75842
  if (next.accountEmail && (entry.name === "Google account" || entry.name.startsWith("google_"))) {
75589
75843
  const renamed = {
75590
75844
  ...entry,
@@ -75717,6 +75971,39 @@ var GoogleServicesAddon = class extends BaseAddon {
75717
75971
  }
75718
75972
  return out.toSorted((a, b) => a.label.localeCompare(b.label));
75719
75973
  }
75974
+ async emitGoogleBrokerReconnectAlert(brokerId, brokerName) {
75975
+ const emit = this.ctx.api.alerts?.emit;
75976
+ if (emit === void 0) {
75977
+ this.ctx.logger.warn("google broker reconnect alert skipped — alerts capability missing", { meta: { brokerId } });
75978
+ return;
75979
+ }
75980
+ try {
75981
+ await emit.mutate(buildGoogleBrokerReconnectAlert(brokerId, brokerName));
75982
+ } catch (err) {
75983
+ this.ctx.logger.warn("google broker reconnect alert emit failed", { meta: {
75984
+ brokerId,
75985
+ error: err instanceof Error ? err.message : String(err)
75986
+ } });
75987
+ }
75988
+ }
75989
+ async resolveGoogleBrokerReconnectAlert(brokerId) {
75990
+ const update = this.ctx.api.alerts?.update;
75991
+ if (update === void 0) return;
75992
+ try {
75993
+ await update.mutate({
75994
+ alertId: googleBrokerReconnectAlertId(brokerId),
75995
+ patch: {
75996
+ status: "completed",
75997
+ read: true
75998
+ }
75999
+ });
76000
+ } catch (err) {
76001
+ this.ctx.logger.warn("google broker reconnect alert resolve failed", { meta: {
76002
+ brokerId,
76003
+ error: err instanceof Error ? err.message : String(err)
76004
+ } });
76005
+ }
76006
+ }
75720
76007
  async ensureAccessToken(brokerId, opts = {}) {
75721
76008
  const entry = this.registry.getEntry(brokerId);
75722
76009
  const oauth = this.registry.getOauth(brokerId);
@@ -75729,10 +76016,16 @@ var GoogleServicesAddon = class extends BaseAddon {
75729
76016
  accountEmail: oauth.accountEmail
75730
76017
  };
75731
76018
  if (!opts.forceRefresh && !needsRefresh(state)) return state.accessToken;
75732
- const tokens = await refreshAccessToken({
75733
- clientId: entry.clientId.trim(),
75734
- clientSecret: entry.clientSecret.trim()
75735
- }, state.refreshToken);
76019
+ let tokens;
76020
+ try {
76021
+ tokens = await refreshAccessToken({
76022
+ clientId: entry.clientId.trim(),
76023
+ clientSecret: entry.clientSecret.trim()
76024
+ }, state.refreshToken);
76025
+ } catch (err) {
76026
+ if (isInvalidGrantRefreshError(err)) this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
76027
+ throw err;
76028
+ }
75736
76029
  if (typeof tokens.access_token !== "string" || tokens.access_token.trim().length === 0) throw new Error("Google token refresh returned an empty access_token — Sign out and Sign in again");
75737
76030
  const nextState = applyTokenResponse(state, tokens);
75738
76031
  this.registry.setOauth({
@@ -5538,7 +5538,7 @@ var ZodIssueCode = {
5538
5538
  var ZodFirstPartyTypeKind;
5539
5539
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5540
5540
  //#endregion
5541
- //#region ../types/dist/sleep-bZIhUvjd.mjs
5541
+ //#region ../types/dist/sleep-BOjuyoPC.mjs
5542
5542
  /**
5543
5543
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5544
5544
  * window to float samples (D455).
@@ -8767,7 +8767,7 @@ function userError(code, params, englishFallback) {
8767
8767
  return new CamstackError(code, params, englishFallback);
8768
8768
  }
8769
8769
  //#endregion
8770
- //#region ../types/dist/err-msg--2RrYiLO.mjs
8770
+ //#region ../types/dist/err-msg-BXKzRBwg.mjs
8771
8771
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
8772
8772
  DeviceType["Camera"] = "camera";
8773
8773
  DeviceType["Hub"] = "hub";
@@ -8859,6 +8859,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
8859
8859
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
8860
8860
  * integrations sharing the same food/desiccant/hopper surface. */
8861
8861
  DeviceType["PetFeeder"] = "pet-feeder";
8862
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
8863
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
8864
+ * Sources: native Bosch Home Connect, reusable by similar integrations. */
8865
+ DeviceType["Dishwasher"] = "dishwasher";
8862
8866
  return DeviceType;
8863
8867
  }({});
8864
8868
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -9106,7 +9110,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
9106
9110
  return DeviceRole;
9107
9111
  }({});
9108
9112
  //#endregion
9109
- //#region ../types/dist/composition-DOwQB_Ua.mjs
9113
+ //#region ../types/dist/composition-CB6PMT9p.mjs
9110
9114
  /**
9111
9115
  * The ffmpeg build every CamStack node downloads, per platform and arch.
9112
9116
  *
@@ -13628,8 +13632,13 @@ var AlertSeveritySchema = _enum([
13628
13632
  "info",
13629
13633
  "success",
13630
13634
  "warning",
13631
- "error"
13635
+ "error",
13636
+ "critical"
13632
13637
  ]);
13638
+ var AlertActionSchema = object({
13639
+ label: string(),
13640
+ href: string().optional()
13641
+ });
13633
13642
  var AlertStatusSchema = _enum([
13634
13643
  "active",
13635
13644
  "in-progress",
@@ -13647,6 +13656,7 @@ var AlertSchema = object({
13647
13656
  severity: AlertSeveritySchema,
13648
13657
  title: string(),
13649
13658
  message: string(),
13659
+ action: AlertActionSchema.optional(),
13650
13660
  status: AlertStatusSchema,
13651
13661
  progress: number().optional(),
13652
13662
  read: boolean(),
@@ -53643,7 +53653,7 @@ var BatteryStatusSchema = object({
53643
53653
  */
53644
53654
  binary: boolean().optional()
53645
53655
  });
53646
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, method(object({
53656
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, method(object({
53647
53657
  deviceId: number(),
53648
53658
  /** Bound on the wait. Sensible range 3000–10000ms. */
53649
53659
  timeoutMs: number().int().min(500).max(3e4).default(8e3)
@@ -54529,6 +54539,98 @@ object({
54529
54539
  deviceId: number(),
54530
54540
  status: DeviceStatusSchema
54531
54541
  });
54542
+ /**
54543
+ * Smart-dishwasher cap — program lifecycle, door / remote-start gates,
54544
+ * consumable levels (salt, rinse aid) and common program options.
54545
+ *
54546
+ * Sources: native Bosch Home Connect (`dishwasher` device type). Reusable
54547
+ * by any integration that exposes the same program / option surface.
54548
+ *
54549
+ * `kind: 'poll'` — cloud REST APIs typically have no push channel; the
54550
+ * provider refreshes the slice on an interval and eagerly after commands.
54551
+ */
54552
+ var DishwasherStateSchema = _enum([
54553
+ "idle",
54554
+ "running",
54555
+ "paused",
54556
+ "finished",
54557
+ "error",
54558
+ "unavailable"
54559
+ ]);
54560
+ var DishwasherConsumableLevelSchema = _enum([
54561
+ "ok",
54562
+ "low",
54563
+ "empty"
54564
+ ]);
54565
+ object({
54566
+ state: DishwasherStateSchema,
54567
+ /** Active program id/name, or null when none selected / unknown. */
54568
+ program: string().nullable(),
54569
+ /** Program tokens the hardware accepts — drives the UI selector. */
54570
+ availablePrograms: array(string()),
54571
+ /** Minutes remaining on the active cycle, or null when not running. */
54572
+ remainingMinutes: number().int().nonnegative().nullable(),
54573
+ /** Cycle completion 0..100, or null when not applicable. */
54574
+ progressPercent: number().min(0).max(100).nullable(),
54575
+ /** True when the door is reported open. */
54576
+ doorOpen: boolean(),
54577
+ /** True when remote start is allowed (door closed, power on, etc.). */
54578
+ remoteStartAllowed: boolean(),
54579
+ /** Main power / switched-on state. */
54580
+ powerOn: boolean(),
54581
+ saltLevel: DishwasherConsumableLevelSchema.nullable(),
54582
+ rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
54583
+ halfLoad: boolean(),
54584
+ hygienePlus: boolean(),
54585
+ errorCode: number().nullable(),
54586
+ errorLabel: string().nullable(),
54587
+ lastFetchedAt: number()
54588
+ });
54589
+ DeviceType.Dishwasher, method(object({ deviceId: number().int().nonnegative() }), _void(), {
54590
+ kind: "mutation",
54591
+ auth: "protected"
54592
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54593
+ kind: "mutation",
54594
+ auth: "protected"
54595
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54596
+ kind: "mutation",
54597
+ auth: "protected"
54598
+ }), method(object({
54599
+ deviceId: number().int().nonnegative(),
54600
+ program: string().min(1)
54601
+ }), _void(), {
54602
+ kind: "mutation",
54603
+ auth: "protected"
54604
+ }), method(object({
54605
+ deviceId: number().int().nonnegative(),
54606
+ on: boolean()
54607
+ }), _void(), {
54608
+ kind: "mutation",
54609
+ auth: "protected"
54610
+ }), method(object({
54611
+ deviceId: number().int().nonnegative(),
54612
+ on: boolean()
54613
+ }), _void(), {
54614
+ kind: "mutation",
54615
+ auth: "protected"
54616
+ }), method(object({
54617
+ deviceId: number().int().nonnegative(),
54618
+ on: boolean()
54619
+ }), _void(), {
54620
+ kind: "mutation",
54621
+ auth: "protected"
54622
+ }), method(object({
54623
+ deviceId: number().int().nonnegative(),
54624
+ key: string().min(1),
54625
+ value: union([
54626
+ boolean(),
54627
+ string(),
54628
+ number()
54629
+ ])
54630
+ }), _void(), {
54631
+ kind: "mutation",
54632
+ auth: "protected"
54633
+ });
54532
54634
  object({
54533
54635
  /** Ms epoch of the last press. null = never observed since this provider started. */
54534
54636
  lastPressedAt: number().nullable(),
@@ -55094,6 +55196,13 @@ object({
55094
55196
  lastChangedAt: number()
55095
55197
  });
55096
55198
  DeviceType.Sensor;
55199
+ object({
55200
+ /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
55201
+ position: number().min(0).max(100).nullable(),
55202
+ /** Ms epoch when the slice was last updated (push or poll). */
55203
+ lastFetchedAt: number()
55204
+ });
55205
+ DeviceType.Thermostat;
55097
55206
  object({
55098
55207
  /** Whether the humidifier is currently on. */
55099
55208
  on: boolean(),
@@ -62160,7 +62269,7 @@ var AccessoryKind = {
62160
62269
  PrivacyMask: DeviceRole.PrivacyMask
62161
62270
  };
62162
62271
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
62163
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
62272
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
62164
62273
  new Set(Object.values(DeviceType));
62165
62274
  Object.freeze({
62166
62275
  "accessories.getStatus": {
@@ -64581,6 +64690,60 @@ Object.freeze({
64581
64690
  addonId: null,
64582
64691
  access: "view"
64583
64692
  },
64693
+ "dishwasher.getStatus": {
64694
+ capName: "dishwasher",
64695
+ capScope: "device",
64696
+ addonId: null,
64697
+ access: "view"
64698
+ },
64699
+ "dishwasher.pause": {
64700
+ capName: "dishwasher",
64701
+ capScope: "device",
64702
+ addonId: null,
64703
+ access: "create"
64704
+ },
64705
+ "dishwasher.setHalfLoad": {
64706
+ capName: "dishwasher",
64707
+ capScope: "device",
64708
+ addonId: null,
64709
+ access: "create"
64710
+ },
64711
+ "dishwasher.setHygienePlus": {
64712
+ capName: "dishwasher",
64713
+ capScope: "device",
64714
+ addonId: null,
64715
+ access: "create"
64716
+ },
64717
+ "dishwasher.setOption": {
64718
+ capName: "dishwasher",
64719
+ capScope: "device",
64720
+ addonId: null,
64721
+ access: "create"
64722
+ },
64723
+ "dishwasher.setPower": {
64724
+ capName: "dishwasher",
64725
+ capScope: "device",
64726
+ addonId: null,
64727
+ access: "create"
64728
+ },
64729
+ "dishwasher.setProgram": {
64730
+ capName: "dishwasher",
64731
+ capScope: "device",
64732
+ addonId: null,
64733
+ access: "create"
64734
+ },
64735
+ "dishwasher.start": {
64736
+ capName: "dishwasher",
64737
+ capScope: "device",
64738
+ addonId: null,
64739
+ access: "create"
64740
+ },
64741
+ "dishwasher.stop": {
64742
+ capName: "dishwasher",
64743
+ capScope: "device",
64744
+ addonId: null,
64745
+ access: "create"
64746
+ },
64584
64747
  "doorbell.getStatus": {
64585
64748
  capName: "doorbell",
64586
64749
  capScope: "device",
@@ -64791,6 +64954,12 @@ Object.freeze({
64791
64954
  addonId: null,
64792
64955
  access: "view"
64793
64956
  },
64957
+ "heatingValve.getStatus": {
64958
+ capName: "heating-valve",
64959
+ capScope: "device",
64960
+ addonId: null,
64961
+ access: "view"
64962
+ },
64794
64963
  "humidifier.getStatus": {
64795
64964
  capName: "humidifier",
64796
64965
  capScope: "device",
@@ -70378,7 +70547,7 @@ Object.freeze({
70378
70547
  access: "create"
70379
70548
  }
70380
70549
  });
70381
- 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;
70550
+ 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.Dishwasher;
70382
70551
  DeviceFeature.BatteryOperated;
70383
70552
  Object.freeze({
70384
70553
  "accessories.getStatus": [{
@@ -71202,6 +71371,51 @@ Object.freeze({
71202
71371
  form: "single",
71203
71372
  optional: false
71204
71373
  }],
71374
+ "dishwasher.getStatus": [{
71375
+ name: "deviceId",
71376
+ form: "single",
71377
+ optional: false
71378
+ }],
71379
+ "dishwasher.pause": [{
71380
+ name: "deviceId",
71381
+ form: "single",
71382
+ optional: false
71383
+ }],
71384
+ "dishwasher.setHalfLoad": [{
71385
+ name: "deviceId",
71386
+ form: "single",
71387
+ optional: false
71388
+ }],
71389
+ "dishwasher.setHygienePlus": [{
71390
+ name: "deviceId",
71391
+ form: "single",
71392
+ optional: false
71393
+ }],
71394
+ "dishwasher.setOption": [{
71395
+ name: "deviceId",
71396
+ form: "single",
71397
+ optional: false
71398
+ }],
71399
+ "dishwasher.setPower": [{
71400
+ name: "deviceId",
71401
+ form: "single",
71402
+ optional: false
71403
+ }],
71404
+ "dishwasher.setProgram": [{
71405
+ name: "deviceId",
71406
+ form: "single",
71407
+ optional: false
71408
+ }],
71409
+ "dishwasher.start": [{
71410
+ name: "deviceId",
71411
+ form: "single",
71412
+ optional: false
71413
+ }],
71414
+ "dishwasher.stop": [{
71415
+ name: "deviceId",
71416
+ form: "single",
71417
+ optional: false
71418
+ }],
71205
71419
  "doorbell.getStatus": [{
71206
71420
  name: "deviceId",
71207
71421
  form: "single",
@@ -71271,6 +71485,11 @@ Object.freeze({
71271
71485
  form: "single",
71272
71486
  optional: false
71273
71487
  }],
71488
+ "heatingValve.getStatus": [{
71489
+ name: "deviceId",
71490
+ form: "single",
71491
+ optional: false
71492
+ }],
71274
71493
  "humidifier.getStatus": [{
71275
71494
  name: "deviceId",
71276
71495
  form: "single",
@@ -74966,6 +75185,40 @@ function createGmailSmtpProvider(deps) {
74966
75185
  };
74967
75186
  }
74968
75187
  //#endregion
75188
+ //#region src/google-broker-reconnect-alert.ts
75189
+ function googleBrokerReconnectAlertId(brokerId) {
75190
+ return `google-broker:${brokerId}`;
75191
+ }
75192
+ function isInvalidGrantRefreshError(err) {
75193
+ return (err instanceof Error ? err.message : String(err)).includes("invalid_grant");
75194
+ }
75195
+ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75196
+ const pushLine = `${brokerName}: Google session expired — sign in again`;
75197
+ return {
75198
+ id: googleBrokerReconnectAlertId(brokerId),
75199
+ category: "google.broker-reconnect",
75200
+ severity: "critical",
75201
+ title: "Google needs a new sign-in",
75202
+ message: `${brokerName} needs you to sign in to Google again.`,
75203
+ action: {
75204
+ label: "Sign in again",
75205
+ href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`
75206
+ },
75207
+ status: "active",
75208
+ read: false,
75209
+ createdAt: Date.now(),
75210
+ updatedAt: Date.now(),
75211
+ source: {
75212
+ type: "google-broker",
75213
+ id: brokerId
75214
+ },
75215
+ metadata: {
75216
+ episodeKey: "reconnect",
75217
+ pushLine
75218
+ }
75219
+ };
75220
+ }
75221
+ //#endregion
74969
75222
  //#region src/google-oauth.ts
74970
75223
  /**
74971
75224
  * Google OAuth2 authorization-code + refresh for Calendar API.
@@ -75581,6 +75834,7 @@ var GoogleServicesAddon = class extends BaseAddon {
75581
75834
  };
75582
75835
  this.registry.setOauth(next);
75583
75836
  await this.persistOauthAccounts();
75837
+ if (next.refreshToken.length > 0) this.resolveGoogleBrokerReconnectAlert(brokerId);
75584
75838
  if (next.accountEmail && (entry.name === "Google account" || entry.name.startsWith("google_"))) {
75585
75839
  const renamed = {
75586
75840
  ...entry,
@@ -75713,6 +75967,39 @@ var GoogleServicesAddon = class extends BaseAddon {
75713
75967
  }
75714
75968
  return out.toSorted((a, b) => a.label.localeCompare(b.label));
75715
75969
  }
75970
+ async emitGoogleBrokerReconnectAlert(brokerId, brokerName) {
75971
+ const emit = this.ctx.api.alerts?.emit;
75972
+ if (emit === void 0) {
75973
+ this.ctx.logger.warn("google broker reconnect alert skipped — alerts capability missing", { meta: { brokerId } });
75974
+ return;
75975
+ }
75976
+ try {
75977
+ await emit.mutate(buildGoogleBrokerReconnectAlert(brokerId, brokerName));
75978
+ } catch (err) {
75979
+ this.ctx.logger.warn("google broker reconnect alert emit failed", { meta: {
75980
+ brokerId,
75981
+ error: err instanceof Error ? err.message : String(err)
75982
+ } });
75983
+ }
75984
+ }
75985
+ async resolveGoogleBrokerReconnectAlert(brokerId) {
75986
+ const update = this.ctx.api.alerts?.update;
75987
+ if (update === void 0) return;
75988
+ try {
75989
+ await update.mutate({
75990
+ alertId: googleBrokerReconnectAlertId(brokerId),
75991
+ patch: {
75992
+ status: "completed",
75993
+ read: true
75994
+ }
75995
+ });
75996
+ } catch (err) {
75997
+ this.ctx.logger.warn("google broker reconnect alert resolve failed", { meta: {
75998
+ brokerId,
75999
+ error: err instanceof Error ? err.message : String(err)
76000
+ } });
76001
+ }
76002
+ }
75716
76003
  async ensureAccessToken(brokerId, opts = {}) {
75717
76004
  const entry = this.registry.getEntry(brokerId);
75718
76005
  const oauth = this.registry.getOauth(brokerId);
@@ -75725,10 +76012,16 @@ var GoogleServicesAddon = class extends BaseAddon {
75725
76012
  accountEmail: oauth.accountEmail
75726
76013
  };
75727
76014
  if (!opts.forceRefresh && !needsRefresh(state)) return state.accessToken;
75728
- const tokens = await refreshAccessToken({
75729
- clientId: entry.clientId.trim(),
75730
- clientSecret: entry.clientSecret.trim()
75731
- }, state.refreshToken);
76015
+ let tokens;
76016
+ try {
76017
+ tokens = await refreshAccessToken({
76018
+ clientId: entry.clientId.trim(),
76019
+ clientSecret: entry.clientSecret.trim()
76020
+ }, state.refreshToken);
76021
+ } catch (err) {
76022
+ if (isInvalidGrantRefreshError(err)) this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
76023
+ throw err;
76024
+ }
75732
76025
  if (typeof tokens.access_token !== "string" || tokens.access_token.trim().length === 0) throw new Error("Google token refresh returned an empty access_token — Sign out and Sign in again");
75733
76026
  const nextState = applyTokenResponse(state, tokens);
75734
76027
  this.registry.setOauth({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-google-services",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Google Services — Google brokers (OAuth2): Calendar as calendar-source, Gmail send as smtp-provider.",
5
5
  "keywords": [
6
6
  "camstack",