@riddix/hamh 2.1.0-alpha.856 → 2.1.0-alpha.858

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.
@@ -133321,12 +133321,93 @@ function accessLogger(logger253) {
133321
133321
  }
133322
133322
 
133323
133323
  // src/api/backup-api.ts
133324
- import fs from "node:fs";
133325
- import path from "node:path";
133324
+ import fs2 from "node:fs";
133325
+ import path2 from "node:path";
133326
133326
  import archiver from "archiver";
133327
133327
  import express from "express";
133328
133328
  import multer from "multer";
133329
133329
  import unzipper from "unzipper";
133330
+
133331
+ // src/plugins/plugin-storage.ts
133332
+ init_esm();
133333
+ import * as fs from "node:fs";
133334
+ import * as path from "node:path";
133335
+ var logger177 = Logger.get("PluginStorage");
133336
+ var SAVE_DEBOUNCE_MS = 500;
133337
+ var safe = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "_");
133338
+ function pluginStorageFilePath(storageDir, bridgeId, pluginName) {
133339
+ return path.join(
133340
+ storageDir,
133341
+ `plugin-${safe(bridgeId)}-${safe(pluginName)}.json`
133342
+ );
133343
+ }
133344
+ function pluginStateFilePath(storageDir, bridgeId) {
133345
+ return path.join(storageDir, `plugin-state-${safe(bridgeId)}.json`);
133346
+ }
133347
+ var FilePluginStorage = class {
133348
+ data = {};
133349
+ dirty = false;
133350
+ filePath;
133351
+ saveTimer;
133352
+ constructor(storageDir, bridgeId, pluginName) {
133353
+ this.filePath = pluginStorageFilePath(storageDir, bridgeId, pluginName);
133354
+ this.load();
133355
+ }
133356
+ async get(key, defaultValue) {
133357
+ const value = this.data[key];
133358
+ return value ?? defaultValue;
133359
+ }
133360
+ async set(key, value) {
133361
+ this.data[key] = value;
133362
+ this.dirty = true;
133363
+ this.scheduleSave();
133364
+ }
133365
+ async delete(key) {
133366
+ delete this.data[key];
133367
+ this.dirty = true;
133368
+ this.scheduleSave();
133369
+ }
133370
+ async keys() {
133371
+ return Object.keys(this.data);
133372
+ }
133373
+ load() {
133374
+ try {
133375
+ if (fs.existsSync(this.filePath)) {
133376
+ const raw = fs.readFileSync(this.filePath, "utf-8");
133377
+ this.data = JSON.parse(raw);
133378
+ }
133379
+ } catch (e) {
133380
+ logger177.warn(`Failed to load plugin storage from ${this.filePath}:`, e);
133381
+ this.data = {};
133382
+ }
133383
+ }
133384
+ scheduleSave() {
133385
+ if (this.saveTimer) clearTimeout(this.saveTimer);
133386
+ this.saveTimer = setTimeout(() => this.save(), SAVE_DEBOUNCE_MS);
133387
+ }
133388
+ save() {
133389
+ if (!this.dirty) return;
133390
+ if (this.saveTimer) {
133391
+ clearTimeout(this.saveTimer);
133392
+ this.saveTimer = void 0;
133393
+ }
133394
+ try {
133395
+ const dir = path.dirname(this.filePath);
133396
+ if (!fs.existsSync(dir)) {
133397
+ fs.mkdirSync(dir, { recursive: true });
133398
+ }
133399
+ fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
133400
+ this.dirty = false;
133401
+ } catch (e) {
133402
+ logger177.warn(`Failed to save plugin storage to ${this.filePath}:`, e);
133403
+ }
133404
+ }
133405
+ flush() {
133406
+ this.save();
133407
+ }
133408
+ };
133409
+
133410
+ // src/api/backup-api.ts
133330
133411
  var upload = multer({ storage: multer.memoryStorage() });
133331
133412
  function backupApi(bridgeStorage, mappingStorage, storageLocation, backupService, settingsStorage, _bridgeService) {
133332
133413
  const router = express.Router();
@@ -133342,9 +133423,9 @@ function backupApi(bridgeStorage, mappingStorage, storageLocation, backupService
133342
133423
  }
133343
133424
  }
133344
133425
  let includesIcons = false;
133345
- const iconsDir = path.join(storageLocation, "bridge-icons");
133346
- if (includeIdentity && fs.existsSync(iconsDir)) {
133347
- const iconFiles = fs.readdirSync(iconsDir);
133426
+ const iconsDir = path2.join(storageLocation, "bridge-icons");
133427
+ if (includeIdentity && fs2.existsSync(iconsDir)) {
133428
+ const iconFiles = fs2.readdirSync(iconsDir);
133348
133429
  includesIcons = iconFiles.some((iconFile) => {
133349
133430
  const bridgeId = iconFile.split(".")[0];
133350
133431
  return bridges.some((b) => b.id === bridgeId);
@@ -133381,19 +133462,25 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133381
133462
  `,
133382
133463
  { name: "README.txt" }
133383
133464
  );
133465
+ for (const bridge of bridges) {
133466
+ const pluginState = pluginStateFilePath(storageLocation, bridge.id);
133467
+ if (fs2.existsSync(pluginState)) {
133468
+ archive.file(pluginState, { name: `plugin-state/${bridge.id}.json` });
133469
+ }
133470
+ }
133384
133471
  if (includeIdentity) {
133385
133472
  for (const bridge of bridges) {
133386
- const bridgeStoragePath = path.join(storageLocation, bridge.id);
133387
- if (fs.existsSync(bridgeStoragePath)) {
133473
+ const bridgeStoragePath = path2.join(storageLocation, bridge.id);
133474
+ if (fs2.existsSync(bridgeStoragePath)) {
133388
133475
  archive.directory(bridgeStoragePath, `identity/${bridge.id}`);
133389
133476
  }
133390
133477
  }
133391
133478
  if (includesIcons) {
133392
- const iconFiles = fs.readdirSync(iconsDir);
133479
+ const iconFiles = fs2.readdirSync(iconsDir);
133393
133480
  for (const iconFile of iconFiles) {
133394
133481
  const bridgeId = iconFile.split(".")[0];
133395
133482
  if (bridges.some((b) => b.id === bridgeId)) {
133396
- const iconPath = path.join(iconsDir, iconFile);
133483
+ const iconPath = path2.join(iconsDir, iconFile);
133397
133484
  archive.file(iconPath, { name: `bridge-icons/${iconFile}` });
133398
133485
  }
133399
133486
  }
@@ -133515,7 +133602,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133515
133602
  fanRestoreSpeedOnPowerOn: config8.fanRestoreSpeedOnPowerOn,
133516
133603
  composedEntities: config8.composedEntities,
133517
133604
  disableMomentaryFlip: config8.disableMomentaryFlip,
133518
- vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder
133605
+ vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder,
133606
+ vacuumRoomSwitches: config8.vacuumRoomSwitches
133519
133607
  });
133520
133608
  mappingsRestored++;
133521
133609
  }
@@ -133541,6 +133629,7 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133541
133629
  iconsRestored++;
133542
133630
  }
133543
133631
  }
133632
+ await restorePluginState(zipDirectory, bridge.id, storageLocation);
133544
133633
  } catch (e) {
133545
133634
  errors.push({
133546
133635
  bridgeId: bridge.id,
@@ -133599,7 +133688,7 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133599
133688
  "Content-Disposition",
133600
133689
  `attachment; filename="${req.params.filename}"`
133601
133690
  );
133602
- const stream = fs.createReadStream(filepath);
133691
+ const stream = fs2.createReadStream(filepath);
133603
133692
  stream.pipe(res);
133604
133693
  } catch (error) {
133605
133694
  const message = error instanceof Error ? error.message : "Failed to download backup";
@@ -133613,7 +133702,7 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133613
133702
  res.status(404).json({ error: "Backup not found" });
133614
133703
  return;
133615
133704
  }
133616
- const buffer = fs.readFileSync(filepath);
133705
+ const buffer = fs2.readFileSync(filepath);
133617
133706
  const options = req.body || {};
133618
133707
  const { backupData, zipDirectory } = await extractBackupData(buffer);
133619
133708
  const existingIds = new Set(bridgeStorage.bridges.map((b) => b.id));
@@ -133681,7 +133770,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133681
133770
  fanWindPresets: config8.fanWindPresets,
133682
133771
  composedEntities: config8.composedEntities,
133683
133772
  disableMomentaryFlip: config8.disableMomentaryFlip,
133684
- vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder
133773
+ vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder,
133774
+ vacuumRoomSwitches: config8.vacuumRoomSwitches
133685
133775
  });
133686
133776
  mappingsRestored++;
133687
133777
  }
@@ -133707,6 +133797,7 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133707
133797
  iconsRestored++;
133708
133798
  }
133709
133799
  }
133800
+ await restorePluginState(zipDirectory, bridge.id, storageLocation);
133710
133801
  } catch (e) {
133711
133802
  errors.push({
133712
133803
  bridgeId: bridge.id,
@@ -133774,12 +133865,12 @@ async function extractBackupData(buffer) {
133774
133865
  return { backupData: data, zipDirectory: directory };
133775
133866
  }
133776
133867
  function resolveWithin(baseDir, relative) {
133777
- if (relative.length === 0 || path.isAbsolute(relative)) {
133868
+ if (relative.length === 0 || path2.isAbsolute(relative)) {
133778
133869
  return null;
133779
133870
  }
133780
- const resolvedBase = path.resolve(baseDir);
133781
- const resolvedTarget = path.resolve(resolvedBase, relative);
133782
- if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path.sep)) {
133871
+ const resolvedBase = path2.resolve(baseDir);
133872
+ const resolvedTarget = path2.resolve(resolvedBase, relative);
133873
+ if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path2.sep)) {
133783
133874
  return null;
133784
133875
  }
133785
133876
  return resolvedTarget;
@@ -133792,8 +133883,8 @@ async function restoreIdentityFiles(zipDirectory, bridgeId, storageLocation) {
133792
133883
  if (identityFiles.length === 0) {
133793
133884
  return false;
133794
133885
  }
133795
- const targetDir = path.join(storageLocation, bridgeId);
133796
- fs.mkdirSync(targetDir, { recursive: true });
133886
+ const targetDir = path2.join(storageLocation, bridgeId);
133887
+ fs2.mkdirSync(targetDir, { recursive: true });
133797
133888
  for (const file of identityFiles) {
133798
133889
  const relativePath = file.path.substring(identityPrefix.length);
133799
133890
  const targetPath = resolveWithin(targetDir, relativePath);
@@ -133802,13 +133893,24 @@ async function restoreIdentityFiles(zipDirectory, bridgeId, storageLocation) {
133802
133893
  `Refusing to restore identity file with unsafe path: ${file.path}`
133803
133894
  );
133804
133895
  }
133805
- const targetDirPath = path.dirname(targetPath);
133806
- fs.mkdirSync(targetDirPath, { recursive: true });
133896
+ const targetDirPath = path2.dirname(targetPath);
133897
+ fs2.mkdirSync(targetDirPath, { recursive: true });
133807
133898
  const content = await file.buffer();
133808
- fs.writeFileSync(targetPath, content);
133899
+ fs2.writeFileSync(targetPath, content);
133809
133900
  }
133810
133901
  return true;
133811
133902
  }
133903
+ async function restorePluginState(zipDirectory, bridgeId, storageLocation) {
133904
+ const entry = zipDirectory.files.find(
133905
+ (f) => f.path === `plugin-state/${bridgeId}.json` && f.type === "File"
133906
+ );
133907
+ if (!entry) {
133908
+ return false;
133909
+ }
133910
+ const content = await entry.buffer();
133911
+ fs2.writeFileSync(pluginStateFilePath(storageLocation, bridgeId), content);
133912
+ return true;
133913
+ }
133812
133914
  async function restoreBridgeIcon(zipDirectory, bridgeId, storageLocation) {
133813
133915
  const iconPrefix = "bridge-icons/";
133814
133916
  const iconFiles = zipDirectory.files.filter(
@@ -133817,8 +133919,8 @@ async function restoreBridgeIcon(zipDirectory, bridgeId, storageLocation) {
133817
133919
  if (iconFiles.length === 0) {
133818
133920
  return false;
133819
133921
  }
133820
- const iconsDir = path.join(storageLocation, "bridge-icons");
133821
- fs.mkdirSync(iconsDir, { recursive: true });
133922
+ const iconsDir = path2.join(storageLocation, "bridge-icons");
133923
+ fs2.mkdirSync(iconsDir, { recursive: true });
133822
133924
  for (const file of iconFiles) {
133823
133925
  const fileName = file.path.substring(iconPrefix.length);
133824
133926
  const targetPath = resolveWithin(iconsDir, fileName);
@@ -133828,7 +133930,7 @@ async function restoreBridgeIcon(zipDirectory, bridgeId, storageLocation) {
133828
133930
  );
133829
133931
  }
133830
133932
  const content = await file.buffer();
133831
- fs.writeFileSync(targetPath, content);
133933
+ fs2.writeFileSync(targetPath, content);
133832
133934
  }
133833
133935
  return true;
133834
133936
  }
@@ -133837,7 +133939,7 @@ async function restoreBridgeIcon(zipDirectory, bridgeId, storageLocation) {
133837
133939
  init_dist();
133838
133940
  init_esm();
133839
133941
  import express2 from "express";
133840
- var logger177 = Logger.get("BridgeExportApi");
133942
+ var logger178 = Logger.get("BridgeExportApi");
133841
133943
  function migrateFilter(legacyFilter) {
133842
133944
  if (!legacyFilter) {
133843
133945
  return { include: [], exclude: [] };
@@ -133988,7 +134090,7 @@ function bridgeExportApi(bridgeStorage) {
133988
134090
  res.json(result);
133989
134091
  } catch (e) {
133990
134092
  const message = e instanceof Error ? e.message : String(e);
133991
- logger177.warn(`Failed to import bridges: ${message}`, e);
134093
+ logger178.warn(`Failed to import bridges: ${message}`, e);
133992
134094
  res.status(400).json({ error: `Failed to import bridges: ${message}` });
133993
134095
  }
133994
134096
  });
@@ -133996,16 +134098,16 @@ function bridgeExportApi(bridgeStorage) {
133996
134098
  }
133997
134099
 
133998
134100
  // src/api/bridge-icon-api.ts
133999
- import fs2 from "node:fs";
134000
- import path2 from "node:path";
134101
+ import fs3 from "node:fs";
134102
+ import path3 from "node:path";
134001
134103
  import express3 from "express";
134002
134104
  import multer2 from "multer";
134003
134105
  var ALLOWED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"];
134004
134106
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
134005
134107
  function bridgeIconApi(storageLocation) {
134006
- const iconsDir = path2.join(storageLocation, "bridge-icons");
134007
- if (!fs2.existsSync(iconsDir)) {
134008
- fs2.mkdirSync(iconsDir, { recursive: true });
134108
+ const iconsDir = path3.join(storageLocation, "bridge-icons");
134109
+ if (!fs3.existsSync(iconsDir)) {
134110
+ fs3.mkdirSync(iconsDir, { recursive: true });
134009
134111
  }
134010
134112
  const storage2 = multer2.diskStorage({
134011
134113
  destination: (_req, _file, cb) => {
@@ -134013,12 +134115,12 @@ function bridgeIconApi(storageLocation) {
134013
134115
  },
134014
134116
  filename: (req, file, cb) => {
134015
134117
  const bridgeId = req.params.bridgeId;
134016
- const ext = path2.extname(file.originalname).toLowerCase();
134118
+ const ext = path3.extname(file.originalname).toLowerCase();
134017
134119
  cb(null, `${bridgeId}${ext}`);
134018
134120
  }
134019
134121
  });
134020
134122
  const fileFilter = (_req, file, cb) => {
134021
- const ext = path2.extname(file.originalname).toLowerCase();
134123
+ const ext = path3.extname(file.originalname).toLowerCase();
134022
134124
  if (ALLOWED_EXTENSIONS.includes(ext)) {
134023
134125
  cb(null, true);
134024
134126
  } else {
@@ -134051,40 +134153,40 @@ function bridgeIconApi(storageLocation) {
134051
134153
  "/:bridgeId/exists",
134052
134154
  (req, res) => {
134053
134155
  const bridgeId = req.params.bridgeId;
134054
- const files = fs2.readdirSync(iconsDir);
134156
+ const files = fs3.readdirSync(iconsDir);
134055
134157
  const exists = files.some((f) => f.startsWith(`${bridgeId}.`));
134056
134158
  res.json({ exists });
134057
134159
  }
134058
134160
  );
134059
134161
  router.get("/:bridgeId", (req, res) => {
134060
134162
  const bridgeId = req.params.bridgeId;
134061
- const files = fs2.readdirSync(iconsDir);
134163
+ const files = fs3.readdirSync(iconsDir);
134062
134164
  const iconFile = files.find((f) => f.startsWith(`${bridgeId}.`));
134063
134165
  if (!iconFile) {
134064
134166
  res.status(404).json({ error: "Icon not found" });
134065
134167
  return;
134066
134168
  }
134067
- const filePath = path2.join(iconsDir, iconFile);
134169
+ const filePath = path3.join(iconsDir, iconFile);
134068
134170
  res.sendFile(filePath);
134069
134171
  });
134070
134172
  router.delete("/:bridgeId", (req, res) => {
134071
134173
  const bridgeId = req.params.bridgeId;
134072
- const files = fs2.readdirSync(iconsDir);
134174
+ const files = fs3.readdirSync(iconsDir);
134073
134175
  const iconFile = files.find((f) => f.startsWith(`${bridgeId}.`));
134074
134176
  if (!iconFile) {
134075
134177
  res.status(404).json({ error: "Icon not found" });
134076
134178
  return;
134077
134179
  }
134078
- const filePath = path2.join(iconsDir, iconFile);
134079
- fs2.unlinkSync(filePath);
134180
+ const filePath = path3.join(iconsDir, iconFile);
134181
+ fs3.unlinkSync(filePath);
134080
134182
  res.json({ success: true });
134081
134183
  });
134082
134184
  return router;
134083
134185
  }
134084
134186
 
134085
134187
  // src/api/device-image-api.ts
134086
- import fs3 from "node:fs";
134087
- import path3 from "node:path";
134188
+ import fs4 from "node:fs";
134189
+ import path4 from "node:path";
134088
134190
  import express4 from "express";
134089
134191
  import multer3 from "multer";
134090
134192
  var ALLOWED_EXTENSIONS2 = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"];
@@ -134095,10 +134197,10 @@ function sanitizeEntityId(entityId) {
134095
134197
  }
134096
134198
  function findCustomImage(imagesDir, entityId) {
134097
134199
  const sanitized = sanitizeEntityId(entityId);
134098
- if (!fs3.existsSync(imagesDir)) return void 0;
134099
- const files = fs3.readdirSync(imagesDir);
134100
- const imageFile = files.find((f) => path3.parse(f).name === sanitized);
134101
- return imageFile ? path3.join(imagesDir, imageFile) : void 0;
134200
+ if (!fs4.existsSync(imagesDir)) return void 0;
134201
+ const files = fs4.readdirSync(imagesDir);
134202
+ const imageFile = files.find((f) => path4.parse(f).name === sanitized);
134203
+ return imageFile ? path4.join(imagesDir, imageFile) : void 0;
134102
134204
  }
134103
134205
  function resolveZ2mImageUrl(haRegistry, entityId) {
134104
134206
  const entity = haRegistry.entities[entityId];
@@ -134108,9 +134210,9 @@ function resolveZ2mImageUrl(haRegistry, entityId) {
134108
134210
  return `${Z2M_IMAGE_BASE}/${encodeURIComponent(device.model)}.png`;
134109
134211
  }
134110
134212
  function deviceImageApi(storageLocation, haRegistry) {
134111
- const imagesDir = path3.join(storageLocation, "device-images");
134112
- if (!fs3.existsSync(imagesDir)) {
134113
- fs3.mkdirSync(imagesDir, { recursive: true });
134213
+ const imagesDir = path4.join(storageLocation, "device-images");
134214
+ if (!fs4.existsSync(imagesDir)) {
134215
+ fs4.mkdirSync(imagesDir, { recursive: true });
134114
134216
  }
134115
134217
  const storage2 = multer3.diskStorage({
134116
134218
  destination: (_req, _file, cb) => {
@@ -134118,12 +134220,12 @@ function deviceImageApi(storageLocation, haRegistry) {
134118
134220
  },
134119
134221
  filename: (req, file, cb) => {
134120
134222
  const entityId = sanitizeEntityId(req.params.entityId);
134121
- const ext = path3.extname(file.originalname).toLowerCase();
134223
+ const ext = path4.extname(file.originalname).toLowerCase();
134122
134224
  cb(null, `${entityId}${ext}`);
134123
134225
  }
134124
134226
  });
134125
134227
  const fileFilter = (_req, file, cb) => {
134126
- const ext = path3.extname(file.originalname).toLowerCase();
134228
+ const ext = path4.extname(file.originalname).toLowerCase();
134127
134229
  if (ALLOWED_EXTENSIONS2.includes(ext)) {
134128
134230
  cb(null, true);
134129
134231
  } else {
@@ -134171,10 +134273,10 @@ function deviceImageApi(storageLocation, haRegistry) {
134171
134273
  return;
134172
134274
  }
134173
134275
  const sanitized = sanitizeEntityId(req.params.entityId);
134174
- const files = fs3.readdirSync(imagesDir);
134276
+ const files = fs4.readdirSync(imagesDir);
134175
134277
  for (const f of files) {
134176
- if (path3.parse(f).name === sanitized && f !== req.file.filename) {
134177
- fs3.unlinkSync(path3.join(imagesDir, f));
134278
+ if (path4.parse(f).name === sanitized && f !== req.file.filename) {
134279
+ fs4.unlinkSync(path4.join(imagesDir, f));
134178
134280
  }
134179
134281
  }
134180
134282
  res.json({ success: true });
@@ -134201,7 +134303,7 @@ function deviceImageApi(storageLocation, haRegistry) {
134201
134303
  res.status(404).json({ error: "No custom image found" });
134202
134304
  return;
134203
134305
  }
134204
- fs3.unlinkSync(customImage);
134306
+ fs4.unlinkSync(customImage);
134205
134307
  res.json({ success: true });
134206
134308
  });
134207
134309
  router.head("/:entityId", (req, res) => {
@@ -134533,6 +134635,7 @@ function entityMappingApi(mappingStorage, identityStorage) {
134533
134635
  currentRoomEntity: body.currentRoomEntity,
134534
134636
  cleanedAreaEntity: body.cleanedAreaEntity,
134535
134637
  vacuumAscendingRoomOrder: body.vacuumAscendingRoomOrder,
134638
+ vacuumRoomSwitches: body.vacuumRoomSwitches,
134536
134639
  disableCustomAreaRoomModes: body.disableCustomAreaRoomModes,
134537
134640
  valetudoIdentifier: body.valetudoIdentifier,
134538
134641
  coverSwapOpenClose: body.coverSwapOpenClose,
@@ -135045,7 +135148,8 @@ function configToProfileEntry(config8) {
135045
135148
  climateExposeFan: config8.climateExposeFan,
135046
135149
  climateAutoMode: config8.climateAutoMode,
135047
135150
  disableMomentaryFlip: config8.disableMomentaryFlip,
135048
- vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder
135151
+ vacuumAscendingRoomOrder: config8.vacuumAscendingRoomOrder,
135152
+ vacuumRoomSwitches: config8.vacuumRoomSwitches
135049
135153
  };
135050
135154
  }
135051
135155
  function mappingProfileApi(mappingStorage) {
@@ -135189,7 +135293,8 @@ function mappingProfileApi(mappingStorage) {
135189
135293
  climateExposeFan: entry.climateExposeFan,
135190
135294
  climateAutoMode: entry.climateAutoMode,
135191
135295
  disableMomentaryFlip: entry.disableMomentaryFlip,
135192
- vacuumAscendingRoomOrder: entry.vacuumAscendingRoomOrder
135296
+ vacuumAscendingRoomOrder: entry.vacuumAscendingRoomOrder,
135297
+ vacuumRoomSwitches: entry.vacuumRoomSwitches
135193
135298
  });
135194
135299
  applied++;
135195
135300
  } catch (e) {
@@ -136593,26 +136698,26 @@ var BUILTIN_PLUGIN_NAMES = ["camera", "security"];
136593
136698
  // src/plugins/plugin-installer.ts
136594
136699
  init_esm();
136595
136700
  import { execFile } from "node:child_process";
136596
- import * as fs4 from "node:fs";
136597
- import * as path4 from "node:path";
136598
- var logger178 = Logger.get("PluginInstaller");
136701
+ import * as fs5 from "node:fs";
136702
+ import * as path5 from "node:path";
136703
+ var logger179 = Logger.get("PluginInstaller");
136599
136704
  var VALID_PACKAGE_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[^@\s]+)?$/;
136600
136705
  var PluginInstaller = class {
136601
136706
  pluginDir;
136602
136707
  constructor(storageLocation) {
136603
- this.pluginDir = path4.join(storageLocation, "plugin-packages");
136708
+ this.pluginDir = path5.join(storageLocation, "plugin-packages");
136604
136709
  this.ensurePluginDir();
136605
136710
  }
136606
136711
  get installDir() {
136607
136712
  return this.pluginDir;
136608
136713
  }
136609
136714
  ensurePluginDir() {
136610
- if (!fs4.existsSync(this.pluginDir)) {
136611
- fs4.mkdirSync(this.pluginDir, { recursive: true });
136715
+ if (!fs5.existsSync(this.pluginDir)) {
136716
+ fs5.mkdirSync(this.pluginDir, { recursive: true });
136612
136717
  }
136613
- const pkgJson = path4.join(this.pluginDir, "package.json");
136614
- if (!fs4.existsSync(pkgJson)) {
136615
- fs4.writeFileSync(
136718
+ const pkgJson = path5.join(this.pluginDir, "package.json");
136719
+ if (!fs5.existsSync(pkgJson)) {
136720
+ fs5.writeFileSync(
136616
136721
  pkgJson,
136617
136722
  JSON.stringify(
136618
136723
  {
@@ -136635,7 +136740,7 @@ var PluginInstaller = class {
136635
136740
  error: `Invalid package name: "${packageName}"`
136636
136741
  };
136637
136742
  }
136638
- logger178.info(`Installing plugin: ${packageName}`);
136743
+ logger179.info(`Installing plugin: ${packageName}`);
136639
136744
  return new Promise((resolve11) => {
136640
136745
  execFile(
136641
136746
  "npm",
@@ -136647,7 +136752,7 @@ var PluginInstaller = class {
136647
136752
  },
136648
136753
  (error, _stdout, stderr) => {
136649
136754
  if (error) {
136650
- logger178.error(
136755
+ logger179.error(
136651
136756
  `Failed to install ${packageName}:`,
136652
136757
  stderr || error.message
136653
136758
  );
@@ -136659,7 +136764,7 @@ var PluginInstaller = class {
136659
136764
  return;
136660
136765
  }
136661
136766
  const version2 = this.getInstalledVersion(packageName);
136662
- logger178.info(`Installed ${packageName}@${version2 || "unknown"}`);
136767
+ logger179.info(`Installed ${packageName}@${version2 || "unknown"}`);
136663
136768
  resolve11({
136664
136769
  success: true,
136665
136770
  packageName,
@@ -136677,7 +136782,7 @@ var PluginInstaller = class {
136677
136782
  error: `Invalid package name: "${packageName}"`
136678
136783
  };
136679
136784
  }
136680
- logger178.info(`Uninstalling plugin: ${packageName}`);
136785
+ logger179.info(`Uninstalling plugin: ${packageName}`);
136681
136786
  return new Promise((resolve11) => {
136682
136787
  execFile(
136683
136788
  "npm",
@@ -136688,7 +136793,7 @@ var PluginInstaller = class {
136688
136793
  },
136689
136794
  (error, _stdout, stderr) => {
136690
136795
  if (error) {
136691
- logger178.error(
136796
+ logger179.error(
136692
136797
  `Failed to uninstall ${packageName}:`,
136693
136798
  stderr || error.message
136694
136799
  );
@@ -136699,7 +136804,7 @@ var PluginInstaller = class {
136699
136804
  });
136700
136805
  return;
136701
136806
  }
136702
- logger178.info(`Uninstalled ${packageName}`);
136807
+ logger179.info(`Uninstalled ${packageName}`);
136703
136808
  resolve11({ success: true, packageName });
136704
136809
  }
136705
136810
  );
@@ -136710,16 +136815,16 @@ var PluginInstaller = class {
136710
136815
  * This is used by PluginManager.loadExternal() to import the plugin.
136711
136816
  */
136712
136817
  getPluginPath(packageName) {
136713
- return path4.join(this.pluginDir, "node_modules", packageName);
136818
+ return path5.join(this.pluginDir, "node_modules", packageName);
136714
136819
  }
136715
136820
  /**
136716
136821
  * List all installed plugin packages from the plugin directory's package.json.
136717
136822
  */
136718
136823
  listInstalled() {
136719
136824
  try {
136720
- const pkgJson = path4.join(this.pluginDir, "package.json");
136721
- if (!fs4.existsSync(pkgJson)) return [];
136722
- const pkg = JSON.parse(fs4.readFileSync(pkgJson, "utf-8"));
136825
+ const pkgJson = path5.join(this.pluginDir, "package.json");
136826
+ if (!fs5.existsSync(pkgJson)) return [];
136827
+ const pkg = JSON.parse(fs5.readFileSync(pkgJson, "utf-8"));
136723
136828
  const deps = pkg.dependencies ?? {};
136724
136829
  return Object.entries(deps).map(([name, ver]) => ({
136725
136830
  name,
@@ -136730,9 +136835,9 @@ var PluginInstaller = class {
136730
136835
  }
136731
136836
  }
136732
136837
  async installFromTgz(tgzBuffer) {
136733
- const tgzPath = path4.join(this.pluginDir, `.upload-${Date.now()}.tgz`);
136838
+ const tgzPath = path5.join(this.pluginDir, `.upload-${Date.now()}.tgz`);
136734
136839
  try {
136735
- fs4.writeFileSync(tgzPath, tgzBuffer);
136840
+ fs5.writeFileSync(tgzPath, tgzBuffer);
136736
136841
  const depsBefore = new Set(Object.keys(this.readDeps()));
136737
136842
  const result = await this.installFromNpm(tgzPath);
136738
136843
  if (!result.success) return result;
@@ -136749,11 +136854,11 @@ var PluginInstaller = class {
136749
136854
  return result;
136750
136855
  } catch (e) {
136751
136856
  const msg = e instanceof Error ? e.message : String(e);
136752
- logger178.error("Failed to install from tgz:", msg);
136857
+ logger179.error("Failed to install from tgz:", msg);
136753
136858
  return { success: false, packageName: "unknown", error: msg };
136754
136859
  } finally {
136755
136860
  try {
136756
- if (fs4.existsSync(tgzPath)) fs4.unlinkSync(tgzPath);
136861
+ if (fs5.existsSync(tgzPath)) fs5.unlinkSync(tgzPath);
136757
136862
  } catch {
136758
136863
  }
136759
136864
  }
@@ -136772,14 +136877,14 @@ var PluginInstaller = class {
136772
136877
  if (error) {
136773
136878
  resolve11({
136774
136879
  success: false,
136775
- packageName: path4.basename(target),
136880
+ packageName: path5.basename(target),
136776
136881
  error: stderr || error.message
136777
136882
  });
136778
136883
  return;
136779
136884
  }
136780
136885
  resolve11({
136781
136886
  success: true,
136782
- packageName: path4.basename(target)
136887
+ packageName: path5.basename(target)
136783
136888
  });
136784
136889
  }
136785
136890
  );
@@ -136787,25 +136892,25 @@ var PluginInstaller = class {
136787
136892
  }
136788
136893
  readDeps() {
136789
136894
  try {
136790
- const pkgJson = path4.join(this.pluginDir, "package.json");
136791
- if (!fs4.existsSync(pkgJson)) return {};
136792
- const pkg = JSON.parse(fs4.readFileSync(pkgJson, "utf-8"));
136895
+ const pkgJson = path5.join(this.pluginDir, "package.json");
136896
+ if (!fs5.existsSync(pkgJson)) return {};
136897
+ const pkg = JSON.parse(fs5.readFileSync(pkgJson, "utf-8"));
136793
136898
  return pkg.dependencies ?? {};
136794
136899
  } catch {
136795
136900
  return {};
136796
136901
  }
136797
136902
  }
136798
136903
  installFromLocal(localPath) {
136799
- const resolvedPath = path4.resolve(localPath);
136800
- if (!fs4.existsSync(resolvedPath)) {
136904
+ const resolvedPath = path5.resolve(localPath);
136905
+ if (!fs5.existsSync(resolvedPath)) {
136801
136906
  return {
136802
136907
  success: false,
136803
136908
  packageName: "unknown",
136804
136909
  error: `Path does not exist: ${resolvedPath}`
136805
136910
  };
136806
136911
  }
136807
- const pkgJsonPath = path4.join(resolvedPath, "package.json");
136808
- if (!fs4.existsSync(pkgJsonPath)) {
136912
+ const pkgJsonPath = path5.join(resolvedPath, "package.json");
136913
+ if (!fs5.existsSync(pkgJsonPath)) {
136809
136914
  return {
136810
136915
  success: false,
136811
136916
  packageName: "unknown",
@@ -136814,7 +136919,7 @@ var PluginInstaller = class {
136814
136919
  }
136815
136920
  let pkg;
136816
136921
  try {
136817
- pkg = JSON.parse(fs4.readFileSync(pkgJsonPath, "utf-8"));
136922
+ pkg = JSON.parse(fs5.readFileSync(pkgJsonPath, "utf-8"));
136818
136923
  } catch {
136819
136924
  return {
136820
136925
  success: false,
@@ -136830,13 +136935,13 @@ var PluginInstaller = class {
136830
136935
  error: "Invalid package.json: missing 'name' field"
136831
136936
  };
136832
136937
  }
136833
- const targetLink = path4.join(this.pluginDir, "node_modules", packageName);
136834
- if (fs4.existsSync(targetLink)) {
136835
- fs4.rmSync(targetLink, { recursive: true, force: true });
136938
+ const targetLink = path5.join(this.pluginDir, "node_modules", packageName);
136939
+ if (fs5.existsSync(targetLink)) {
136940
+ fs5.rmSync(targetLink, { recursive: true, force: true });
136836
136941
  }
136837
- fs4.mkdirSync(path4.dirname(targetLink), { recursive: true });
136838
- fs4.symlinkSync(resolvedPath, targetLink, "dir");
136839
- logger178.info(
136942
+ fs5.mkdirSync(path5.dirname(targetLink), { recursive: true });
136943
+ fs5.symlinkSync(resolvedPath, targetLink, "dir");
136944
+ logger179.info(
136840
136945
  `Linked local plugin: ${packageName}@${pkg.version || "unknown"} \u2192 ${resolvedPath}`
136841
136946
  );
136842
136947
  return {
@@ -136847,14 +136952,14 @@ var PluginInstaller = class {
136847
136952
  }
136848
136953
  getInstalledVersion(packageName) {
136849
136954
  try {
136850
- const pkgPath = path4.join(
136955
+ const pkgPath = path5.join(
136851
136956
  this.pluginDir,
136852
136957
  "node_modules",
136853
136958
  packageName,
136854
136959
  "package.json"
136855
136960
  );
136856
- if (fs4.existsSync(pkgPath)) {
136857
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
136961
+ if (fs5.existsSync(pkgPath)) {
136962
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
136858
136963
  return pkg.version ?? null;
136859
136964
  }
136860
136965
  } catch {
@@ -136865,40 +136970,40 @@ var PluginInstaller = class {
136865
136970
 
136866
136971
  // src/plugins/plugin-registry.ts
136867
136972
  init_esm();
136868
- import * as fs5 from "node:fs";
136869
- import * as path5 from "node:path";
136870
- var logger179 = Logger.get("PluginRegistry");
136973
+ import * as fs6 from "node:fs";
136974
+ import * as path6 from "node:path";
136975
+ var logger180 = Logger.get("PluginRegistry");
136871
136976
  var PluginRegistry = class {
136872
136977
  plugins = [];
136873
136978
  filePath;
136874
136979
  constructor(storageLocation) {
136875
- this.filePath = path5.join(storageLocation, "installed-plugins.json");
136980
+ this.filePath = path6.join(storageLocation, "installed-plugins.json");
136876
136981
  this.load();
136877
136982
  }
136878
136983
  load() {
136879
136984
  try {
136880
- if (fs5.existsSync(this.filePath)) {
136881
- const raw = fs5.readFileSync(this.filePath, "utf-8");
136985
+ if (fs6.existsSync(this.filePath)) {
136986
+ const raw = fs6.readFileSync(this.filePath, "utf-8");
136882
136987
  this.plugins = JSON.parse(raw);
136883
136988
  }
136884
136989
  } catch (e) {
136885
- logger179.warn("Failed to load plugin registry:", e);
136990
+ logger180.warn("Failed to load plugin registry:", e);
136886
136991
  this.plugins = [];
136887
136992
  }
136888
136993
  }
136889
136994
  save() {
136890
136995
  try {
136891
- const dir = path5.dirname(this.filePath);
136892
- if (!fs5.existsSync(dir)) {
136893
- fs5.mkdirSync(dir, { recursive: true });
136996
+ const dir = path6.dirname(this.filePath);
136997
+ if (!fs6.existsSync(dir)) {
136998
+ fs6.mkdirSync(dir, { recursive: true });
136894
136999
  }
136895
- fs5.writeFileSync(
137000
+ fs6.writeFileSync(
136896
137001
  this.filePath,
136897
137002
  JSON.stringify(this.plugins, null, 2),
136898
137003
  "utf-8"
136899
137004
  );
136900
137005
  } catch (e) {
136901
- logger179.error("Failed to save plugin registry:", e);
137006
+ logger180.error("Failed to save plugin registry:", e);
136902
137007
  }
136903
137008
  }
136904
137009
  getAll() {
@@ -137031,19 +137136,35 @@ function pluginApi(bridgeService, storageLocation) {
137031
137136
  }
137032
137137
  return bridge;
137033
137138
  }
137034
- router.post("/:bridgeId/:pluginName/enable", (req, res) => {
137139
+ router.post("/:bridgeId/:pluginName/enable", async (req, res) => {
137035
137140
  const bridge = pluginBridge(req.params.bridgeId, res);
137036
137141
  if (!bridge) return;
137037
137142
  const { pluginName } = req.params;
137038
- bridge.enablePlugin(pluginName);
137039
- res.json({ success: true, pluginName, enabled: true });
137143
+ const metadata = await bridge.enablePlugin(pluginName);
137144
+ if (!metadata) {
137145
+ res.status(404).json({ error: "Plugin not found" });
137146
+ return;
137147
+ }
137148
+ res.json({
137149
+ success: metadata.enabled === true,
137150
+ pluginName,
137151
+ enabled: metadata.enabled
137152
+ });
137040
137153
  });
137041
- router.post("/:bridgeId/:pluginName/disable", (req, res) => {
137154
+ router.post("/:bridgeId/:pluginName/disable", async (req, res) => {
137042
137155
  const bridge = pluginBridge(req.params.bridgeId, res);
137043
137156
  if (!bridge) return;
137044
137157
  const { pluginName } = req.params;
137045
- bridge.disablePlugin(pluginName);
137046
- res.json({ success: true, pluginName, enabled: false });
137158
+ const metadata = await bridge.disablePlugin(pluginName);
137159
+ if (!metadata) {
137160
+ res.status(404).json({ error: "Plugin not found" });
137161
+ return;
137162
+ }
137163
+ res.json({
137164
+ success: metadata.enabled === false,
137165
+ pluginName,
137166
+ enabled: metadata.enabled
137167
+ });
137047
137168
  });
137048
137169
  router.get("/:bridgeId/:pluginName/config-schema", (req, res) => {
137049
137170
  const bridge = pluginBridge(req.params.bridgeId, res);
@@ -137244,7 +137365,7 @@ function pluginApi(bridgeService, storageLocation) {
137244
137365
  }
137245
137366
 
137246
137367
  // src/api/proxy-support.ts
137247
- import path6 from "node:path";
137368
+ import path7 from "node:path";
137248
137369
  var ingressPath = "x-ingress-path";
137249
137370
  var forwardedPrefix = "x-forwarded-prefix";
137250
137371
  function supportIngress(req, _, next) {
@@ -137283,7 +137404,7 @@ function supportProxyLocation(req, res, next) {
137283
137404
  next();
137284
137405
  }
137285
137406
  function buildPath(...paths) {
137286
- let result = path6.posix.join(...paths);
137407
+ let result = path7.posix.join(...paths);
137287
137408
  if (!result.startsWith("/")) {
137288
137409
  result = `/${result}`;
137289
137410
  }
@@ -137555,7 +137676,7 @@ import { promisify } from "node:util";
137555
137676
  import v8 from "node:v8";
137556
137677
  import express15 from "express";
137557
137678
  var execAsync = promisify(exec);
137558
- var logger180 = Logger.get("SystemApi");
137679
+ var logger181 = Logger.get("SystemApi");
137559
137680
  function detectEnvironment2() {
137560
137681
  if (process.env.SUPERVISOR_TOKEN || process.env.HASSIO_TOKEN) {
137561
137682
  return "Home Assistant Add-on";
@@ -137602,7 +137723,7 @@ function systemApi(version2) {
137602
137723
  const data = await response.json();
137603
137724
  res.json(toUpdateCheckResponse(version2, data, detectEnvironment2()));
137604
137725
  } catch (error) {
137605
- logger180.error("Failed to check for updates:", error);
137726
+ logger181.error("Failed to check for updates:", error);
137606
137727
  res.status(500).json({ error: "Failed to check for updates" });
137607
137728
  }
137608
137729
  });
@@ -137651,7 +137772,7 @@ function systemApi(version2) {
137651
137772
  };
137652
137773
  res.json(systemInfo);
137653
137774
  } catch (error) {
137654
- logger180.error("Failed to get system info:", error);
137775
+ logger181.error("Failed to get system info:", error);
137655
137776
  res.status(500).json({ error: "Failed to get system info" });
137656
137777
  }
137657
137778
  });
@@ -137694,7 +137815,7 @@ async function getStorageInfo() {
137694
137815
  return await getUnixStorageInfo(pathToCheck);
137695
137816
  }
137696
137817
  } catch (error) {
137697
- logger180.error("Failed to get storage info:", error);
137818
+ logger181.error("Failed to get storage info:", error);
137698
137819
  return { total: 0, used: 0, free: 0 };
137699
137820
  }
137700
137821
  }
@@ -137763,8 +137884,8 @@ async function getUnixStorageInfo(path14) {
137763
137884
  }
137764
137885
 
137765
137886
  // src/api/web-ui.ts
137766
- import fs6 from "node:fs";
137767
- import path7 from "node:path";
137887
+ import fs7 from "node:fs";
137888
+ import path8 from "node:path";
137768
137889
  import express16 from "express";
137769
137890
  function webUi(dist) {
137770
137891
  const router = express16.Router();
@@ -137783,7 +137904,7 @@ function replaceBase(dist) {
137783
137904
  if (!baseUrl.endsWith("/")) {
137784
137905
  baseUrl += "/";
137785
137906
  }
137786
- const content = fs6.readFileSync(path7.join(dist, "index.html"), "utf8").replace(
137907
+ const content = fs7.readFileSync(path8.join(dist, "index.html"), "utf8").replace(
137787
137908
  /<!-- BASE -->[\s\S]*<!-- \/BASE -->/,
137788
137909
  `<base href='${baseUrl}' />`
137789
137910
  );
@@ -138379,7 +138500,7 @@ var FilteredNetwork = class extends NodeJsNetwork {
138379
138500
  };
138380
138501
 
138381
138502
  // src/core/app/mdns.ts
138382
- var logger181 = Logger.get("Mdns");
138503
+ var logger182 = Logger.get("Mdns");
138383
138504
  function mdns(env, options) {
138384
138505
  if (options.stripGlobalIpv6) {
138385
138506
  env.set(Network, new FilteredNetwork());
@@ -138394,7 +138515,7 @@ function mdns(env, options) {
138394
138515
  function warnAboutAdvertising(options) {
138395
138516
  const choice = selectMdnsInterface(os5.networkInterfaces());
138396
138517
  if (choice.hasGlobalIpv6) {
138397
- logger181.warn(
138518
+ logger182.warn(
138398
138519
  "Matter mDNS is advertising a global IPv6 address that controllers may not reach on the LAN, so devices can show No Response (#361). Set mdns-strip-global-ipv6 if devices stay unreachable."
138399
138520
  );
138400
138521
  }
@@ -138403,7 +138524,7 @@ function warnAboutAdvertising(options) {
138403
138524
  }
138404
138525
  const suggestion = choice.selected ? ` Likely LAN interface: ${choice.selected}.` : "";
138405
138526
  if (choice.hasThreadInterface) {
138406
- logger181.warn(
138527
+ logger182.warn(
138407
138528
  `Matter mDNS is advertising on an OpenThread/OTBR interface (wpan/otbr) whose mesh-local address controllers cannot reach, so devices may show offline (#388). Set mdns-network-interface to your LAN interface.${suggestion}`
138408
138529
  );
138409
138530
  }
@@ -138411,16 +138532,16 @@ function warnAboutAdvertising(options) {
138411
138532
  return;
138412
138533
  }
138413
138534
  const list3 = choice.external.map((i) => `${i.name} (${i.ipv4[0] ?? i.ipv6[0] ?? "?"})`).join(", ");
138414
- logger181.warn(
138535
+ logger182.warn(
138415
138536
  `Matter mDNS is advertising on several interfaces including likely Docker-internal ones, so controllers may show devices as offline (#361). Set mdns-network-interface to your LAN interface.${suggestion} Interfaces: ${list3}.`
138416
138537
  );
138417
138538
  }
138418
138539
 
138419
138540
  // src/core/app/storage.ts
138420
138541
  init_esm7();
138421
- import fs7 from "node:fs";
138542
+ import fs8 from "node:fs";
138422
138543
  import os6 from "node:os";
138423
- import path8 from "node:path";
138544
+ import path9 from "node:path";
138424
138545
 
138425
138546
  // src/core/app/storage/custom-storage.ts
138426
138547
  init_dist();
@@ -138440,7 +138561,7 @@ var CustomStorage = class extends FileStorageDriver {
138440
138561
  // src/core/app/storage.ts
138441
138562
  function storage(environment, options) {
138442
138563
  const location = resolveStorageLocation(options.location);
138443
- fs7.mkdirSync(location, { recursive: true });
138564
+ fs8.mkdirSync(location, { recursive: true });
138444
138565
  environment.get(VariableService).set("storage.path", location);
138445
138566
  const storageService = environment.get(StorageService);
138446
138567
  storageService.registerDriver({
@@ -138455,7 +138576,7 @@ function storage(environment, options) {
138455
138576
  }
138456
138577
  function resolveStorageLocation(storageLocation) {
138457
138578
  const homedir = os6.homedir();
138458
- return storageLocation ? path8.resolve(storageLocation.replace(/^~\//, `${homedir}/`)) : path8.join(homedir, ".home-assistant-matter-hub");
138579
+ return storageLocation ? path9.resolve(storageLocation.replace(/^~\//, `${homedir}/`)) : path9.join(homedir, ".home-assistant-matter-hub");
138459
138580
  }
138460
138581
 
138461
138582
  // src/core/app/configure-default-environment.ts
@@ -138475,7 +138596,7 @@ function configureDefaultEnvironment(options) {
138475
138596
  init_esm7();
138476
138597
  import { createRequire } from "node:module";
138477
138598
  import os7 from "node:os";
138478
- import path9 from "node:path";
138599
+ import path10 from "node:path";
138479
138600
  function resolveAppVersion() {
138480
138601
  try {
138481
138602
  const require2 = createRequire(import.meta.url);
@@ -138545,7 +138666,7 @@ var Options = class {
138545
138666
  resolveStorageLocation() {
138546
138667
  const storageLocation = notEmpty(this.startOptions.storageLocation);
138547
138668
  const homedir = os7.homedir();
138548
- return storageLocation ? path9.resolve(storageLocation.replace(/^~\//, `${homedir}/`)) : path9.join(homedir, ".home-assistant-matter-hub");
138669
+ return storageLocation ? path10.resolve(storageLocation.replace(/^~\//, `${homedir}/`)) : path10.join(homedir, ".home-assistant-matter-hub");
138549
138670
  }
138550
138671
  get bridgeService() {
138551
138672
  return {
@@ -138585,8 +138706,8 @@ init_esm7();
138585
138706
 
138586
138707
  // src/services/backup/backup-service.ts
138587
138708
  init_esm();
138588
- import fs8 from "node:fs";
138589
- import path10 from "node:path";
138709
+ import fs9 from "node:fs";
138710
+ import path11 from "node:path";
138590
138711
  import archiver2 from "archiver";
138591
138712
  var BackupService = class {
138592
138713
  constructor(bridgeStorage, mappingStorage, settingsStorage, props) {
@@ -138594,8 +138715,8 @@ var BackupService = class {
138594
138715
  this.mappingStorage = mappingStorage;
138595
138716
  this.settingsStorage = settingsStorage;
138596
138717
  this.props = props;
138597
- this.backupDir = path10.join(props.storageLocation, "backups");
138598
- fs8.mkdirSync(this.backupDir, { recursive: true });
138718
+ this.backupDir = path11.join(props.storageLocation, "backups");
138719
+ fs9.mkdirSync(this.backupDir, { recursive: true });
138599
138720
  }
138600
138721
  bridgeStorage;
138601
138722
  mappingStorage;
@@ -138609,7 +138730,7 @@ var BackupService = class {
138609
138730
  const dateStr = now.toISOString().replace(/T/, "_").replace(/:/g, "-").replace(/\.\d+Z$/, "");
138610
138731
  const prefix = auto ? "auto" : "manual";
138611
138732
  const filename = `hamh-${prefix}-${version2}-${dateStr}.zip`;
138612
- const filepath = path10.join(this.backupDir, filename);
138733
+ const filepath = path11.join(this.backupDir, filename);
138613
138734
  const bridges = this.bridgeStorage.bridges;
138614
138735
  const entityMappings = {};
138615
138736
  for (const bridge of bridges) {
@@ -138619,9 +138740,9 @@ var BackupService = class {
138619
138740
  }
138620
138741
  }
138621
138742
  let includesIcons = false;
138622
- const iconsDir = path10.join(this.props.storageLocation, "bridge-icons");
138623
- if (fs8.existsSync(iconsDir)) {
138624
- const iconFiles = fs8.readdirSync(iconsDir);
138743
+ const iconsDir = path11.join(this.props.storageLocation, "bridge-icons");
138744
+ if (fs9.existsSync(iconsDir)) {
138745
+ const iconFiles = fs9.readdirSync(iconsDir);
138625
138746
  includesIcons = iconFiles.some((f) => {
138626
138747
  const bridgeId = f.split(".")[0];
138627
138748
  return bridges.some((b) => b.id === bridgeId);
@@ -138638,7 +138759,7 @@ var BackupService = class {
138638
138759
  auto
138639
138760
  };
138640
138761
  await new Promise((resolve11, reject) => {
138641
- const output = fs8.createWriteStream(filepath);
138762
+ const output = fs9.createWriteStream(filepath);
138642
138763
  const archive = archiver2("zip", { zlib: { level: 9 } });
138643
138764
  output.on("close", () => resolve11());
138644
138765
  archive.on("error", (err) => reject(err));
@@ -138662,20 +138783,27 @@ var BackupService = class {
138662
138783
  { name: "README.txt" }
138663
138784
  );
138664
138785
  for (const bridge of bridges) {
138665
- const bridgeStoragePath = path10.join(
138786
+ const bridgeStoragePath = path11.join(
138666
138787
  this.props.storageLocation,
138667
138788
  bridge.id
138668
138789
  );
138669
- if (fs8.existsSync(bridgeStoragePath)) {
138790
+ if (fs9.existsSync(bridgeStoragePath)) {
138670
138791
  archive.directory(bridgeStoragePath, `identity/${bridge.id}`);
138671
138792
  }
138793
+ const pluginState = pluginStateFilePath(
138794
+ this.props.storageLocation,
138795
+ bridge.id
138796
+ );
138797
+ if (fs9.existsSync(pluginState)) {
138798
+ archive.file(pluginState, { name: `plugin-state/${bridge.id}.json` });
138799
+ }
138672
138800
  }
138673
138801
  if (includesIcons) {
138674
- const iconFiles = fs8.readdirSync(iconsDir);
138802
+ const iconFiles = fs9.readdirSync(iconsDir);
138675
138803
  for (const iconFile of iconFiles) {
138676
138804
  const bridgeId = iconFile.split(".")[0];
138677
138805
  if (bridges.some((b) => b.id === bridgeId)) {
138678
- archive.file(path10.join(iconsDir, iconFile), {
138806
+ archive.file(path11.join(iconsDir, iconFile), {
138679
138807
  name: `bridge-icons/${iconFile}`
138680
138808
  });
138681
138809
  }
@@ -138683,7 +138811,7 @@ var BackupService = class {
138683
138811
  }
138684
138812
  archive.finalize();
138685
138813
  });
138686
- const stat4 = fs8.statSync(filepath);
138814
+ const stat4 = fs9.statSync(filepath);
138687
138815
  const metadata = {
138688
138816
  filename,
138689
138817
  version: version2,
@@ -138711,13 +138839,13 @@ var BackupService = class {
138711
138839
  }
138712
138840
  }
138713
138841
  listBackups() {
138714
- if (!fs8.existsSync(this.backupDir)) {
138842
+ if (!fs9.existsSync(this.backupDir)) {
138715
138843
  return [];
138716
138844
  }
138717
- const files = fs8.readdirSync(this.backupDir).filter((f) => f.startsWith("hamh-") && f.endsWith(".zip"));
138845
+ const files = fs9.readdirSync(this.backupDir).filter((f) => f.startsWith("hamh-") && f.endsWith(".zip"));
138718
138846
  return files.map((filename) => {
138719
138847
  try {
138720
- const stat4 = fs8.statSync(path10.join(this.backupDir, filename));
138848
+ const stat4 = fs9.statSync(path11.join(this.backupDir, filename));
138721
138849
  const parsed = this.parseFilename(filename);
138722
138850
  return {
138723
138851
  filename,
@@ -138737,8 +138865,8 @@ var BackupService = class {
138737
138865
  if (filename.includes("..") || filename.includes("/")) {
138738
138866
  return null;
138739
138867
  }
138740
- const filepath = path10.join(this.backupDir, filename);
138741
- if (!fs8.existsSync(filepath)) {
138868
+ const filepath = path11.join(this.backupDir, filename);
138869
+ if (!fs9.existsSync(filepath)) {
138742
138870
  return null;
138743
138871
  }
138744
138872
  return filepath;
@@ -138747,7 +138875,7 @@ var BackupService = class {
138747
138875
  const filepath = this.getBackupPath(filename);
138748
138876
  if (!filepath) return false;
138749
138877
  try {
138750
- fs8.unlinkSync(filepath);
138878
+ fs9.unlinkSync(filepath);
138751
138879
  this.log.info(`Backup deleted: ${filename}`);
138752
138880
  return true;
138753
138881
  } catch (e) {
@@ -139327,7 +139455,7 @@ async function getAreaRegistry(connection, timeoutMs) {
139327
139455
  }
139328
139456
 
139329
139457
  // src/services/home-assistant/home-assistant-registry.ts
139330
- var logger182 = Logger.get("HomeAssistantRegistry");
139458
+ var logger183 = Logger.get("HomeAssistantRegistry");
139331
139459
  var HomeAssistantRegistry = class extends Service {
139332
139460
  constructor(client, options) {
139333
139461
  super("HomeAssistantRegistry");
@@ -139362,7 +139490,7 @@ var HomeAssistantRegistry = class extends Service {
139362
139490
  try {
139363
139491
  await this.reload();
139364
139492
  } catch (e) {
139365
- logger182.warn(
139493
+ logger183.warn(
139366
139494
  "Initial registry fetch failed, starting empty and relying on auto-refresh:",
139367
139495
  e
139368
139496
  );
@@ -139376,7 +139504,7 @@ var HomeAssistantRegistry = class extends Service {
139376
139504
  let refreshing = false;
139377
139505
  this.autoRefresh = setInterval(async () => {
139378
139506
  if (refreshing) {
139379
- logger182.debug("Skipping registry refresh, previous tick still running");
139507
+ logger183.debug("Skipping registry refresh, previous tick still running");
139380
139508
  return;
139381
139509
  }
139382
139510
  refreshing = true;
@@ -139386,7 +139514,7 @@ var HomeAssistantRegistry = class extends Service {
139386
139514
  await onRefresh();
139387
139515
  }
139388
139516
  } catch (e) {
139389
- logger182.warn("Failed to refresh registry, will retry next interval:", e);
139517
+ logger183.warn("Failed to refresh registry, will retry next interval:", e);
139390
139518
  } finally {
139391
139519
  refreshing = false;
139392
139520
  }
@@ -139404,7 +139532,7 @@ var HomeAssistantRegistry = class extends Service {
139404
139532
  baseDelayMs: 2e3,
139405
139533
  maxDelayMs: 3e4,
139406
139534
  onRetry: (attempt, error, delayMs) => {
139407
- logger182.warn(
139535
+ logger183.warn(
139408
139536
  `Registry fetch failed (attempt ${attempt}), retrying in ${delayMs}ms:`,
139409
139537
  error
139410
139538
  );
@@ -139417,7 +139545,7 @@ var HomeAssistantRegistry = class extends Service {
139417
139545
  return await this.runRegistryQueries();
139418
139546
  } catch (e) {
139419
139547
  if (!isConnectionLost(e)) throw e;
139420
- logger182.debug("Registry fetch hit connection drop, waiting for reconnect");
139548
+ logger183.debug("Registry fetch hit connection drop, waiting for reconnect");
139421
139549
  await this.waitForConnection(6e4);
139422
139550
  return await this.runRegistryQueries();
139423
139551
  }
@@ -139425,7 +139553,7 @@ var HomeAssistantRegistry = class extends Service {
139425
139553
  async waitForConnection(timeoutMs) {
139426
139554
  const connection = this.client.connection;
139427
139555
  if (connection.connected) return;
139428
- logger182.debug("Connection not ready, waiting for reconnect...");
139556
+ logger183.debug("Connection not ready, waiting for reconnect...");
139429
139557
  await new Promise((resolve11) => {
139430
139558
  const timeout = setTimeout(() => {
139431
139559
  connection.removeEventListener("ready", onReady);
@@ -139486,7 +139614,7 @@ var HomeAssistantRegistry = class extends Service {
139486
139614
  const fingerprint = hash2.digest("hex");
139487
139615
  this._states = keyBy(statesList, "entity_id");
139488
139616
  if (fingerprint === this.lastRegistryFingerprint) {
139489
- logger182.debug("Registry unchanged, skipping full refresh");
139617
+ logger183.debug("Registry unchanged, skipping full refresh");
139490
139618
  return false;
139491
139619
  }
139492
139620
  this.lastRegistryFingerprint = fingerprint;
@@ -139507,10 +139635,10 @@ var HomeAssistantRegistry = class extends Service {
139507
139635
  const missingDevices = fromPairs(missingDeviceIds.map((d) => [d, { id: d }]));
139508
139636
  this._devices = { ...missingDevices, ...realDevices };
139509
139637
  this._entities = allEntities;
139510
- logger182.debug(
139638
+ logger183.debug(
139511
139639
  `Loaded HA registry: ${keys(allEntities).length} entities, ${keys(realDevices).length} devices, ${keys(this._states).length} states`
139512
139640
  );
139513
- logMemoryUsage(logger182, "after HA registry load");
139641
+ logMemoryUsage(logger183, "after HA registry load");
139514
139642
  this._labels = labels;
139515
139643
  this._areas = new Map(areas.map((a) => [a.area_id, a.name]));
139516
139644
  return true;
@@ -139966,6 +140094,7 @@ var EntityMappingStorage = class extends Service {
139966
140094
  currentRoomEntity: request.currentRoomEntity?.trim() || void 0,
139967
140095
  cleanedAreaEntity: request.cleanedAreaEntity?.trim() || void 0,
139968
140096
  vacuumAscendingRoomOrder: request.vacuumAscendingRoomOrder || void 0,
140097
+ vacuumRoomSwitches: request.vacuumRoomSwitches || void 0,
139969
140098
  disableCustomAreaRoomModes: request.disableCustomAreaRoomModes || void 0,
139970
140099
  valetudoIdentifier: request.valetudoIdentifier?.trim() || void 0,
139971
140100
  coverSwapOpenClose: request.coverSwapOpenClose || void 0,
@@ -139983,7 +140112,7 @@ var EntityMappingStorage = class extends Service {
139983
140112
  composedEntities: request.composedEntities?.filter((e) => e.entityId?.trim()) ?? void 0,
139984
140113
  disableMomentaryFlip: request.disableMomentaryFlip || void 0
139985
140114
  };
139986
- if (!config8.matterDeviceType && !config8.customName && !config8.customProductName && !config8.customVendorName && !config8.customSerialNumber && config8.customVendorId === void 0 && config8.disabled !== true && !config8.filterLifeEntity && !config8.cleaningModeEntity && !config8.temperatureEntity && !config8.humidityEntity && !config8.batteryEntity && !config8.disableBatteryMapping && !config8.chargingStateEntity && !config8.roomEntities && !config8.disableLockPin && !config8.lockUsercodeService && config8.lockUsercodeSlot === void 0 && config8.lockPinMinLength === void 0 && config8.lockPinMaxLength === void 0 && !config8.powerEntity && !config8.energyEntity && !config8.meterSerialNumber && !config8.pointOfDelivery && !config8.voltageEntity && !config8.currentEntity && !config8.batteryPowerEntity && !config8.batteryEnergyEntity && !config8.chargingSwitchEntity && !config8.currentLimitEntity && !config8.pressureEntity && !config8.suctionLevelEntity && !config8.mopIntensityEntity && (!config8.customServiceAreas || config8.customServiceAreas.length === 0) && (!config8.customFanSpeedTags || Object.keys(config8.customFanSpeedTags).length === 0) && (!config8.fanWindPresets || (config8.fanWindPresets.natural?.length ?? 0) === 0 && (config8.fanWindPresets.sleep?.length ?? 0) === 0) && !config8.fanRestoreSpeedOnPowerOn && !config8.currentRoomEntity && !config8.cleanedAreaEntity && !config8.vacuumAscendingRoomOrder && !config8.disableCustomAreaRoomModes && !config8.valetudoIdentifier && !config8.coverSwapOpenClose && !config8.coverExposeAsDimmableLight && !config8.selectExposeAsSwitch && !config8.selectSwitchOnOption && !config8.selectSwitchOffOption && !config8.coverSliderDebounceMs && !config8.updateThrottleMs && !config8.disableClimateOnOff && !config8.disableClimateFanControl && !config8.climateKeepModeOnIdle && !config8.climateExposeFan && !config8.climateAutoMode && (!config8.composedEntities || config8.composedEntities.length === 0) && !config8.disableMomentaryFlip) {
140115
+ if (!config8.matterDeviceType && !config8.customName && !config8.customProductName && !config8.customVendorName && !config8.customSerialNumber && config8.customVendorId === void 0 && config8.disabled !== true && !config8.filterLifeEntity && !config8.cleaningModeEntity && !config8.temperatureEntity && !config8.humidityEntity && !config8.batteryEntity && !config8.disableBatteryMapping && !config8.chargingStateEntity && !config8.roomEntities && !config8.disableLockPin && !config8.lockUsercodeService && config8.lockUsercodeSlot === void 0 && config8.lockPinMinLength === void 0 && config8.lockPinMaxLength === void 0 && !config8.powerEntity && !config8.energyEntity && !config8.meterSerialNumber && !config8.pointOfDelivery && !config8.voltageEntity && !config8.currentEntity && !config8.batteryPowerEntity && !config8.batteryEnergyEntity && !config8.chargingSwitchEntity && !config8.currentLimitEntity && !config8.pressureEntity && !config8.suctionLevelEntity && !config8.mopIntensityEntity && (!config8.customServiceAreas || config8.customServiceAreas.length === 0) && (!config8.customFanSpeedTags || Object.keys(config8.customFanSpeedTags).length === 0) && (!config8.fanWindPresets || (config8.fanWindPresets.natural?.length ?? 0) === 0 && (config8.fanWindPresets.sleep?.length ?? 0) === 0) && !config8.fanRestoreSpeedOnPowerOn && !config8.currentRoomEntity && !config8.cleanedAreaEntity && !config8.vacuumAscendingRoomOrder && !config8.vacuumRoomSwitches && !config8.disableCustomAreaRoomModes && !config8.valetudoIdentifier && !config8.coverSwapOpenClose && !config8.coverExposeAsDimmableLight && !config8.selectExposeAsSwitch && !config8.selectSwitchOnOption && !config8.selectSwitchOffOption && !config8.coverSliderDebounceMs && !config8.updateThrottleMs && !config8.disableClimateOnOff && !config8.disableClimateFanControl && !config8.climateKeepModeOnIdle && !config8.climateExposeFan && !config8.climateAutoMode && (!config8.composedEntities || config8.composedEntities.length === 0) && !config8.disableMomentaryFlip) {
139987
140116
  bridgeMap.delete(request.entityId);
139988
140117
  } else {
139989
140118
  bridgeMap.set(request.entityId, config8);
@@ -140382,7 +140511,7 @@ var __privateIn5 = (member, obj) => Object(obj) !== obj ? __typeError50('Cannot
140382
140511
  var __privateGet5 = (obj, member, getter) => (__accessCheck5(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
140383
140512
  var __privateSet5 = (obj, member, value, setter) => (__accessCheck5(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
140384
140513
  var __privateMethod5 = (obj, member, method) => (__accessCheck5(obj, member, "access private method"), method);
140385
- var logger183 = Logger.get("ScenesManagementServer");
140514
+ var logger184 = Logger.get("ScenesManagementServer");
140386
140515
  var UNDEFINED_SCENE_ID = 255;
140387
140516
  function constraintErrorWithSceneId(groupId22, sceneId) {
140388
140517
  const response = { status: Status2.ConstraintError, groupId: groupId22, sceneId };
@@ -140509,11 +140638,11 @@ var ScenesManagementServer = class extends ScenesManagementBase {
140509
140638
  return { status: Status2.ResourceExhausted, groupId: groupId22, sceneId };
140510
140639
  }
140511
140640
  this.state.sceneTable.push(sceneData);
140512
- logger183.debug(`Added scene ${sceneId} in group ${groupId22} for fabric ${fabricIndex}`);
140641
+ logger184.debug(`Added scene ${sceneId} in group ${groupId22} for fabric ${fabricIndex}`);
140513
140642
  this.#updateFabricSceneInfoCountsForFabric(fabricIndex);
140514
140643
  } else {
140515
140644
  this.state.sceneTable[existingSceneIndex] = sceneData;
140516
- logger183.debug(`Updated scene ${sceneId} in group ${groupId22} for fabric ${fabricIndex}`);
140645
+ logger184.debug(`Updated scene ${sceneId} in group ${groupId22} for fabric ${fabricIndex}`);
140517
140646
  }
140518
140647
  return { status: Status2.Success, groupId: groupId22, sceneId };
140519
140648
  }
@@ -140816,20 +140945,20 @@ var ScenesManagementServer = class extends ScenesManagementBase {
140816
140945
  }
140817
140946
  }
140818
140947
  if (fieldCount !== 2) {
140819
- logger183.warn(
140948
+ logger184.warn(
140820
140949
  `AttributeValuePair has invalid number (${fieldCount}) of fields (${serialize(attributeValuePair)})`
140821
140950
  );
140822
140951
  return void 0;
140823
140952
  }
140824
140953
  const value = attributeValuePair[mappedType];
140825
140954
  if (value === void 0) {
140826
- logger183.warn(
140955
+ logger184.warn(
140827
140956
  `AttributeValuePair missing value for mappedType ${mappedType} (${serialize(attributeValuePair)})`
140828
140957
  );
140829
140958
  return void 0;
140830
140959
  }
140831
140960
  if (typeof value !== "number" && typeof value !== "bigint") {
140832
- logger183.warn(
140961
+ logger184.warn(
140833
140962
  `AttributeValuePair has invalid non-numeric value for mappedType ${mappedType} (${serialize(attributeValuePair)})`
140834
140963
  // Should never happen
140835
140964
  );
@@ -140927,7 +141056,7 @@ var ScenesManagementServer = class extends ScenesManagementBase {
140927
141056
  } else if (schema6.schema.baseTypeMin < 0 && schema6.schema.min > schema6.schema.baseTypeMin) {
140928
141057
  return { attributeId, [mappedType]: schema6.schema.baseTypeMin };
140929
141058
  } else {
140930
- logger183.warn(
141059
+ logger184.warn(
140931
141060
  `Cannot determine out-of-bounds value for attribute schema, returning min value of datatype schema`
140932
141061
  );
140933
141062
  }
@@ -140948,7 +141077,7 @@ var ScenesManagementServer = class extends ScenesManagementBase {
140948
141077
  }
140949
141078
  }
140950
141079
  });
140951
- logger183.debug(`Collected scene attribute values on Endpoint ${this.endpoint.id}: ${serialize(sceneValues)}`);
141080
+ logger184.debug(`Collected scene attribute values on Endpoint ${this.endpoint.id}: ${serialize(sceneValues)}`);
140952
141081
  return sceneValues;
140953
141082
  }
140954
141083
  /**
@@ -140987,7 +141116,7 @@ var ScenesManagementServer = class extends ScenesManagementBase {
140987
141116
  }
140988
141117
  const attrType = attribute.primitiveBase?.name;
140989
141118
  if (attrType === void 0 || DataTypeToSceneAttributeDataMap[attrType] === void 0) {
140990
- logger183.warn(
141119
+ logger184.warn(
140991
141120
  `Scene Attribute ${attribute.name} on Cluster ${clusterName} has unsupported datatype ${attrType} for scene management on Endpoint ${this.endpoint.id}`
140992
141121
  );
140993
141122
  continue;
@@ -141002,7 +141131,7 @@ var ScenesManagementServer = class extends ScenesManagementBase {
141002
141131
  });
141003
141132
  }
141004
141133
  if (sceneClusterDetails) {
141005
- logger183.info(
141134
+ logger184.info(
141006
141135
  `Registered ${sceneClusterDetails.attributes.size} scene attributes for Cluster ${clusterName} on Endpoint ${this.endpoint.id}`
141007
141136
  );
141008
141137
  this.internal.endpointSceneableBehaviors.add(sceneClusterDetails);
@@ -141010,7 +141139,7 @@ var ScenesManagementServer = class extends ScenesManagementBase {
141010
141139
  }
141011
141140
  /** Apply scene attribute values in the various clusters on the endpoint. */
141012
141141
  #applySceneAttributeValues(sceneValues, transitionTime = null) {
141013
- logger183.debug(`Recalling scene on Endpoint ${this.endpoint.id} with values: ${serialize(sceneValues)}`);
141142
+ logger184.debug(`Recalling scene on Endpoint ${this.endpoint.id} with values: ${serialize(sceneValues)}`);
141014
141143
  const agent = this.endpoint.agentFor(this.context);
141015
141144
  const promises = [];
141016
141145
  for (const [clusterName, clusterAttributes] of Object.entries(sceneValues)) {
@@ -141021,7 +141150,7 @@ var ScenesManagementServer = class extends ScenesManagementBase {
141021
141150
  promises.push(result);
141022
141151
  }
141023
141152
  } else {
141024
- logger183.warn(
141153
+ logger184.warn(
141025
141154
  `No scenes implementation found for cluster ${clusterName} on Endpoint ${this.endpoint.id} during scene recall. Values are ignored`
141026
141155
  );
141027
141156
  }
@@ -141029,7 +141158,7 @@ var ScenesManagementServer = class extends ScenesManagementBase {
141029
141158
  if (promises.length) {
141030
141159
  return Promise.all(promises).then(
141031
141160
  () => void 0,
141032
- (error) => logger183.warn(`Error applying scene attribute values on Endpoint ${this.endpoint.id}:`, error)
141161
+ (error) => logger184.warn(`Error applying scene attribute values on Endpoint ${this.endpoint.id}:`, error)
141033
141162
  );
141034
141163
  }
141035
141164
  }
@@ -141222,7 +141351,7 @@ var GroupsBehaviorConstructor = ClusterBehavior.for(Groups4);
141222
141351
  var GroupsBehavior = GroupsBehaviorConstructor;
141223
141352
 
141224
141353
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/groups/GroupsServer.js
141225
- var logger184 = Logger.get("GroupsServer");
141354
+ var logger185 = Logger.get("GroupsServer");
141226
141355
  var { commands: commands3 } = Groups4.schema;
141227
141356
  var addGroup = commands3.require("AddGroup");
141228
141357
  var addGroupIfIdentifying = commands3.require("AddGroupIfIdentifying");
@@ -141301,7 +141430,7 @@ var GroupsServer = class extends GroupsBase {
141301
141430
  (fabric2, gkm) => gkm.addEndpointForGroup(fabric2, groupId3, endpointNumber, groupName)
141302
141431
  );
141303
141432
  } catch (error) {
141304
- logger184.debug("Could not add group", error);
141433
+ logger185.debug("Could not add group", error);
141305
141434
  StatusResponseError.accept(error);
141306
141435
  return { status: error.code, groupId: groupId3 };
141307
141436
  }
@@ -141978,7 +142107,7 @@ var SwitchBehavior = SwitchBehaviorConstructor;
141978
142107
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/switch/SwitchServer.js
141979
142108
  var DEFAULT_MULTIPRESS_DELAY = Millis(300);
141980
142109
  var DEFAULT_LONG_PRESS_DELAY = Seconds(2);
141981
- var logger185 = Logger.get("SwitchServer");
142110
+ var logger186 = Logger.get("SwitchServer");
141982
142111
  var SwitchServerBase = SwitchBehavior.with(
141983
142112
  Switch3.Feature.LatchingSwitch,
141984
142113
  Switch3.Feature.MomentarySwitch,
@@ -142031,7 +142160,7 @@ var SwitchBaseServer = class extends SwitchServerBase {
142031
142160
  this.internal.currentIsLongPress = false;
142032
142161
  this.internal.multiPressTimer?.stop();
142033
142162
  this.internal.longPressTimer?.stop();
142034
- logger185.info("State of Switch got reset");
142163
+ logger186.info("State of Switch got reset");
142035
142164
  }
142036
142165
  // TODO remove when Validator logic can assess that with 1.3 introduction
142037
142166
  #assertPositionInRange(position) {
@@ -142269,11 +142398,11 @@ var WebRtcTransportRequestorBehaviorConstructor = ClusterBehavior.for(WebRtcTran
142269
142398
  var WebRtcTransportRequestorBehavior = WebRtcTransportRequestorBehaviorConstructor;
142270
142399
 
142271
142400
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/web-rtc-transport-requestor/WebRtcTransportRequestorServer.js
142272
- var logger186 = Logger.get("WebRtcTransportRequestorServer");
142401
+ var logger187 = Logger.get("WebRtcTransportRequestorServer");
142273
142402
  var WebRtcTransportRequestorServer = class extends WebRtcTransportRequestorBehavior {
142274
142403
  async initialize() {
142275
142404
  const node = Node.forEndpoint(this.endpoint);
142276
- logger186.info(
142405
+ logger187.info(
142277
142406
  `WebRtcTransportRequestor initialized on endpoint=${this.endpoint.number} (id="${this.endpoint.id}")`
142278
142407
  );
142279
142408
  this.reactTo(node.lifecycle.online, this.#nodeOnline);
@@ -142339,7 +142468,7 @@ var WebRtcTransportRequestorServer = class extends WebRtcTransportRequestorBehav
142339
142468
  * `offer` event.
142340
142469
  */
142341
142470
  async offer(request) {
142342
- logger186.debug(`incoming Offer webRtcSessionId=${request.webRtcSessionId} sdpLen=${request.sdp.length}`);
142471
+ logger187.debug(`incoming Offer webRtcSessionId=${request.webRtcSessionId} sdpLen=${request.sdp.length}`);
142343
142472
  const session = this.#findSessionStrict(request.webRtcSessionId);
142344
142473
  this.events.offer.emit(session, request);
142345
142474
  }
@@ -142348,7 +142477,7 @@ var WebRtcTransportRequestorServer = class extends WebRtcTransportRequestorBehav
142348
142477
  * `answer` event.
142349
142478
  */
142350
142479
  async answer(request) {
142351
- logger186.debug(`incoming Answer webRtcSessionId=${request.webRtcSessionId} sdpLen=${request.sdp.length}`);
142480
+ logger187.debug(`incoming Answer webRtcSessionId=${request.webRtcSessionId} sdpLen=${request.sdp.length}`);
142352
142481
  const session = this.#findSessionStrict(request.webRtcSessionId);
142353
142482
  this.events.answer.emit(session, request.sdp);
142354
142483
  }
@@ -142356,7 +142485,7 @@ var WebRtcTransportRequestorServer = class extends WebRtcTransportRequestorBehav
142356
142485
  * `iceCandidates` event.
142357
142486
  */
142358
142487
  async iceCandidates(request) {
142359
- logger186.debug(
142488
+ logger187.debug(
142360
142489
  `incoming ICECandidates webRtcSessionId=${request.webRtcSessionId} count=${request.iceCandidates.length}`
142361
142490
  );
142362
142491
  if (request.iceCandidates.length === 0) {
@@ -142369,7 +142498,7 @@ var WebRtcTransportRequestorServer = class extends WebRtcTransportRequestorBehav
142369
142498
  * {@link WebRtcTransportRequestorServer.Events} `end` event.
142370
142499
  */
142371
142500
  async end(request) {
142372
- logger186.debug(`incoming End webRtcSessionId=${request.webRtcSessionId} reason=${request.reason}`);
142501
+ logger187.debug(`incoming End webRtcSessionId=${request.webRtcSessionId} reason=${request.reason}`);
142373
142502
  const session = this.#findSessionStrict(request.webRtcSessionId);
142374
142503
  this.removeSession(request.webRtcSessionId);
142375
142504
  this.events.end.emit(session, request.reason);
@@ -142814,11 +142943,11 @@ var OccupancySensingBehaviorConstructor = ClusterBehavior.for(OccupancySensing3)
142814
142943
  var OccupancySensingBehavior = OccupancySensingBehaviorConstructor;
142815
142944
 
142816
142945
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/occupancy-sensing/OccupancySensingServer.js
142817
- var logger187 = Logger.get("OccupancySensingServer");
142946
+ var logger188 = Logger.get("OccupancySensingServer");
142818
142947
  var OccupancySensingServer = class extends OccupancySensingBehavior {
142819
142948
  initialize() {
142820
142949
  if (!Object.values(this.features).some((feature) => feature)) {
142821
- logger187.error(
142950
+ logger188.error(
142822
142951
  `OccupancySensingServer: Since revision 5 of the cluster features need to be set based on the detector type. Currently no features are enabled.`
142823
142952
  );
142824
142953
  } else if (!Object.values(this.state.occupancySensorTypeBitmap).some((feature) => feature) || this.state.occupancySensorType === void 0) {
@@ -142845,7 +142974,7 @@ var OccupancySensingServer = class extends OccupancySensingBehavior {
142845
142974
  } else if (this.state.occupancySensorTypeBitmap.physicalContact) {
142846
142975
  this.state.occupancySensorType = OccupancySensing3.OccupancySensorType.PhysicalContact;
142847
142976
  }
142848
- logger187.debug(
142977
+ logger188.debug(
142849
142978
  "Sync occupancySensorType to",
142850
142979
  OccupancySensing3.OccupancySensorType[this.state.occupancySensorType],
142851
142980
  "and occupancySensorTypeBitmap to",
@@ -142858,7 +142987,7 @@ var OccupancySensingServer = class extends OccupancySensingBehavior {
142858
142987
  if (this.features.occupancyEvent) {
142859
142988
  this.reactTo(this.events.occupancy$Changed, this.#emitOccupancyChanged);
142860
142989
  } else {
142861
- logger187.info(
142990
+ logger188.info(
142862
142991
  'OccupancySensingServer: enable the OccupancyEvent feature (e.g. OccupancySensingServer.with("<DetectorType>", "OccupancyEvent")) to emit the OccupancyChanged event.'
142863
142992
  );
142864
142993
  }
@@ -145454,7 +145583,7 @@ function miredsToXy(mireds) {
145454
145583
  }
145455
145584
 
145456
145585
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/color-control/ColorControlServer.js
145457
- var logger188 = Logger.get("ColorControlServer");
145586
+ var logger189 = Logger.get("ColorControlServer");
145458
145587
  var ColorControlBase = ColorControlBehavior.with(
145459
145588
  ColorControl3.Feature.HueSaturation,
145460
145589
  ColorControl3.Feature.EnhancedHue,
@@ -146635,7 +146764,7 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146635
146764
  switch (oldMode) {
146636
146765
  case ColorControl3.ColorMode.CurrentHueAndCurrentSaturation:
146637
146766
  if (this.state.currentHue === void 0 || this.state.currentSaturation === void 0) {
146638
- logger188.warn("Could not convert from hue/saturation because one of them is undefined");
146767
+ logger189.warn("Could not convert from hue/saturation because one of them is undefined");
146639
146768
  break;
146640
146769
  }
146641
146770
  switch (newMode) {
@@ -146647,7 +146776,7 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146647
146776
  case ColorControl3.ColorMode.ColorTemperatureMireds:
146648
146777
  const mireds = hsvToMireds(this.hue, this.saturation);
146649
146778
  if (mireds === void 0) {
146650
- logger188.warn(
146779
+ logger189.warn(
146651
146780
  `Could not convert hue/saturation (${this.hue}/${this.saturation}) to color temperature`
146652
146781
  );
146653
146782
  } else {
@@ -146658,7 +146787,7 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146658
146787
  break;
146659
146788
  case ColorControl3.ColorMode.CurrentXAndCurrentY:
146660
146789
  if (this.state.currentX === void 0 || this.state.currentY === void 0) {
146661
- logger188.warn("Could not convert from xy because one of them is undefined");
146790
+ logger189.warn("Could not convert from xy because one of them is undefined");
146662
146791
  break;
146663
146792
  }
146664
146793
  switch (newMode) {
@@ -146670,7 +146799,7 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146670
146799
  case ColorControl3.ColorMode.ColorTemperatureMireds:
146671
146800
  const mireds = xyToMireds(this.x, this.y);
146672
146801
  if (mireds === void 0) {
146673
- logger188.warn(`Could not convert xy ${this.x / this.y} to color temperature`);
146802
+ logger189.warn(`Could not convert xy ${this.x / this.y} to color temperature`);
146674
146803
  } else {
146675
146804
  this.mireds = mireds;
146676
146805
  }
@@ -146679,14 +146808,14 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146679
146808
  break;
146680
146809
  case ColorControl3.ColorMode.ColorTemperatureMireds:
146681
146810
  if (this.state.colorTemperatureMireds === void 0) {
146682
- logger188.warn("Could not convert from color temperature because it is undefined");
146811
+ logger189.warn("Could not convert from color temperature because it is undefined");
146683
146812
  break;
146684
146813
  }
146685
146814
  switch (newMode) {
146686
146815
  case ColorControl3.ColorMode.CurrentHueAndCurrentSaturation:
146687
146816
  const hsvResult = miredsToHsv(this.mireds);
146688
146817
  if (hsvResult === void 0) {
146689
- logger188.warn(`Could not convert color temperature ${this.mireds} to hue/saturation`);
146818
+ logger189.warn(`Could not convert color temperature ${this.mireds} to hue/saturation`);
146690
146819
  } else {
146691
146820
  const [hue, saturation] = hsvResult;
146692
146821
  this.hue = hue;
@@ -146696,7 +146825,7 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146696
146825
  case ColorControl3.ColorMode.CurrentXAndCurrentY:
146697
146826
  const xyResult = miredsToXy(this.mireds);
146698
146827
  if (xyResult === void 0) {
146699
- logger188.warn("Could not convert color temperature to xy");
146828
+ logger189.warn("Could not convert color temperature to xy");
146700
146829
  } else {
146701
146830
  const [x, y] = xyResult;
146702
146831
  this.x = x;
@@ -146745,7 +146874,7 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146745
146874
  );
146746
146875
  newColorTemp = tempPhysMax - tempDelta;
146747
146876
  }
146748
- logger188.debug(`Synced color temperature with level: ${level}, new color temperature: ${newColorTemp}`);
146877
+ logger189.debug(`Synced color temperature with level: ${level}, new color temperature: ${newColorTemp}`);
146749
146878
  return this.moveToColorTemperatureLogic(newColorTemp, 0);
146750
146879
  }
146751
146880
  #assertRate(mode, rate) {
@@ -146949,7 +147078,7 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146949
147078
  targetEnhancedColorMode = values4.enhancedColorMode;
146950
147079
  }
146951
147080
  if (!this.#supportsColorMode(targetEnhancedColorMode)) {
146952
- logger188.info(
147081
+ logger189.info(
146953
147082
  `Can not apply scene with unsupported color mode: ${ColorControl3.EnhancedColorMode[targetEnhancedColorMode]} (${targetEnhancedColorMode})`
146954
147083
  );
146955
147084
  }
@@ -146991,7 +147120,7 @@ var ColorControlBaseServer = class _ColorControlBaseServer extends ColorControlB
146991
147120
  }
146992
147121
  break;
146993
147122
  default:
146994
- logger188.info(
147123
+ logger189.info(
146995
147124
  `No supported color mode found to apply scene: ${ColorControl3.EnhancedColorMode[targetEnhancedColorMode]} (${targetEnhancedColorMode})`
146996
147125
  );
146997
147126
  break;
@@ -147086,7 +147215,7 @@ var LevelControlBehaviorConstructor = ClusterBehavior.for(LevelControl3);
147086
147215
  var LevelControlBehavior = LevelControlBehaviorConstructor;
147087
147216
 
147088
147217
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/level-control/LevelControlServer.js
147089
- var logger189 = Logger.get("LevelControlServer");
147218
+ var logger190 = Logger.get("LevelControlServer");
147090
147219
  var LevelControlBase = LevelControlBehavior.with(LevelControl3.Feature.OnOff, LevelControl3.Feature.Lighting);
147091
147220
  var LevelControlBaseServer = class _LevelControlBaseServer extends LevelControlBase {
147092
147221
  /** Returns the minimum level, including feature specific fallback value handling. */
@@ -147179,17 +147308,17 @@ var LevelControlBaseServer = class _LevelControlBaseServer extends LevelControlB
147179
147308
  */
147180
147309
  initializeLighting() {
147181
147310
  if (this.state.currentLevel === 0) {
147182
- logger189.warn(
147311
+ logger190.warn(
147183
147312
  `The currentLevel value of ${this.state.currentLevel} is invalid according to Matter specification. The value must not be 0.`
147184
147313
  );
147185
147314
  }
147186
147315
  if (this.minLevel !== 1) {
147187
- logger189.warn(
147316
+ logger190.warn(
147188
147317
  `The minLevel value of ${this.minLevel} is invalid according to Matter specification. The value should be 1.`
147189
147318
  );
147190
147319
  }
147191
147320
  if (this.maxLevel !== 254) {
147192
- logger189.warn(
147321
+ logger190.warn(
147193
147322
  `The maxLevel value of ${this.maxLevel} is invalid according to Matter specification. The value should be 254.`
147194
147323
  );
147195
147324
  }
@@ -147500,7 +147629,7 @@ var LevelControlBaseServer = class _LevelControlBaseServer extends LevelControlB
147500
147629
  if (!onOff || this.state.onLevel === null) {
147501
147630
  return;
147502
147631
  }
147503
- logger189.debug(`OnOff changed to ON, setting level to onLevel value of ${this.state.onLevel}`);
147632
+ logger190.debug(`OnOff changed to ON, setting level to onLevel value of ${this.state.onLevel}`);
147504
147633
  this.state.currentLevel = this.state.onLevel;
147505
147634
  }
147506
147635
  #calculateEffectiveOptions(optionsMask, optionsOverride) {
@@ -150563,7 +150692,7 @@ var ModeSelectBehaviorConstructor = ClusterBehavior.for(ModeSelect3);
150563
150692
  var ModeSelectBehavior = ModeSelectBehaviorConstructor;
150564
150693
 
150565
150694
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/mode-select/ModeSelectServer.js
150566
- var logger190 = Logger.get("ModeSelectServer");
150695
+ var logger191 = Logger.get("ModeSelectServer");
150567
150696
  var ModeSelectBase = ModeSelectBehavior.with(ModeSelect3.Feature.OnOff);
150568
150697
  var ModeSelectBaseServer = class extends ModeSelectBase {
150569
150698
  initialize() {
@@ -150580,7 +150709,7 @@ var ModeSelectBaseServer = class extends ModeSelectBase {
150580
150709
  }
150581
150710
  this.reactTo(onOffServer.events.onOff$Changed, this.#handleOnOffDependency);
150582
150711
  } else {
150583
- logger190.warn("OnOffServer not found on endpoint, but OnMode is set.");
150712
+ logger191.warn("OnOffServer not found on endpoint, but OnMode is set.");
150584
150713
  }
150585
150714
  }
150586
150715
  if (!currentModeOverridden && this.state.startUpMode !== void 0 && this.state.startUpMode !== null && this.#getBootReason() !== GeneralDiagnostics3.BootReason.SoftwareUpdateCompleted) {
@@ -151781,7 +151910,7 @@ init_esm3();
151781
151910
 
151782
151911
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/thermostat/AtomicWriteState.js
151783
151912
  init_esm();
151784
- var logger191 = Logger.get("AtomicWriteState");
151913
+ var logger192 = Logger.get("AtomicWriteState");
151785
151914
  var MAXIMUM_ALLOWED_TIMEOUT = Seconds(9);
151786
151915
  var AtomicWriteState = class {
151787
151916
  peerAddress;
@@ -151816,19 +151945,19 @@ var AtomicWriteState = class {
151816
151945
  });
151817
151946
  }
151818
151947
  start() {
151819
- logger191.debug(
151948
+ logger192.debug(
151820
151949
  `Starting atomic write state for peer ${this.peerAddress.toString()} on endpoint ${this.endpoint.id}`
151821
151950
  );
151822
151951
  this.#timer.start();
151823
151952
  }
151824
151953
  #timeoutTriggered() {
151825
- logger191.debug(
151954
+ logger192.debug(
151826
151955
  `Atomic write state for peer ${this.peerAddress.toString()} on endpoint ${this.endpoint.id} timed out`
151827
151956
  );
151828
151957
  this.close();
151829
151958
  }
151830
151959
  close() {
151831
- logger191.debug(
151960
+ logger192.debug(
151832
151961
  `Closing atomic write state for peer ${this.peerAddress.toString()} on endpoint ${this.endpoint.id}`
151833
151962
  );
151834
151963
  if (this.#timer.isRunning) {
@@ -151839,7 +151968,7 @@ var AtomicWriteState = class {
151839
151968
  };
151840
151969
 
151841
151970
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/thermostat/AtomicWriteHandler.js
151842
- var logger192 = Logger.get("AtomicWriteHandler");
151971
+ var logger193 = Logger.get("AtomicWriteHandler");
151843
151972
  var AtomicWriteHandler = class _AtomicWriteHandler {
151844
151973
  #observers = new ObserverGroup();
151845
151974
  #pendingWrites = new BasicSet();
@@ -151901,7 +152030,7 @@ var AtomicWriteHandler = class _AtomicWriteHandler {
151901
152030
  this.#pendingWrites.add(state);
151902
152031
  state.closed.on(() => void this.#pendingWrites.delete(state));
151903
152032
  state.start();
151904
- logger192.debug("Added atomic write state:", state);
152033
+ logger193.debug("Added atomic write state:", state);
151905
152034
  return state;
151906
152035
  }
151907
152036
  if (existingState === void 0) {
@@ -151976,14 +152105,14 @@ var AtomicWriteHandler = class _AtomicWriteHandler {
151976
152105
  writeAttribute(context, endpoint, cluster2, attribute, value) {
151977
152106
  const state = this.#assertPendingWriteForAttributeAndPeer(context, endpoint, cluster2, attribute);
151978
152107
  const attributeName = state.attributeNames.get(attribute);
151979
- logger192.debug(`Writing pending value for attribute ${attributeName}, ${attribute} in atomic write`, value);
152108
+ logger193.debug(`Writing pending value for attribute ${attributeName}, ${attribute} in atomic write`, value);
151980
152109
  endpoint.eventsOf(cluster2.id)[`${attributeName}$AtomicChanging`]?.emit(
151981
152110
  value,
151982
152111
  state.pendingAttributeValues[attribute] !== void 0 ? state.pendingAttributeValues[attribute] : state.initialValues[attribute],
151983
152112
  context
151984
152113
  );
151985
152114
  state.pendingAttributeValues[attribute] = value;
151986
- logger192.debug("Atomic write state after current write:", state);
152115
+ logger193.debug("Atomic write state after current write:", state);
151987
152116
  }
151988
152117
  /**
151989
152118
  * Implements the commit logic for an atomic write.
@@ -152002,7 +152131,7 @@ var AtomicWriteHandler = class _AtomicWriteHandler {
152002
152131
  await context.transaction?.commit();
152003
152132
  } catch (error) {
152004
152133
  await context.transaction?.rollback();
152005
- logger192.info(
152134
+ logger193.info(
152006
152135
  `Failed to write attribute ${attr} during atomic write commit:`,
152007
152136
  Diagnostic.errorMessage(asError(error))
152008
152137
  );
@@ -152041,7 +152170,7 @@ var AtomicWriteHandler = class _AtomicWriteHandler {
152041
152170
  const fabricIndex = fabric.fabricIndex;
152042
152171
  for (const writeState of Array.from(this.#pendingWrites)) {
152043
152172
  if (writeState.peerAddress.fabricIndex === fabricIndex) {
152044
- logger192.debug(
152173
+ logger193.debug(
152045
152174
  `Closing atomic write state for peer ${writeState.peerAddress.toString()} on endpoint ${writeState.endpoint.id} due to fabric removal`
152046
152175
  );
152047
152176
  writeState.close();
@@ -152090,7 +152219,7 @@ var AtomicWriteHandler = class _AtomicWriteHandler {
152090
152219
  if (!PeerAddress.is(attrWriteState.peerAddress, peerAddress)) {
152091
152220
  return void 0;
152092
152221
  }
152093
- logger192.debug(
152222
+ logger193.debug(
152094
152223
  `Found pending value for attribute ${attribute} for peer ${peerAddress.nodeId}`,
152095
152224
  serialize(attrWriteState.pendingAttributeValues[attribute])
152096
152225
  );
@@ -152131,7 +152260,7 @@ var ThermostatBehaviorConstructor = ClusterBehavior.for(Thermostat3);
152131
152260
  var ThermostatBehavior = ThermostatBehaviorConstructor;
152132
152261
 
152133
152262
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/thermostat/ThermostatServer.js
152134
- var logger193 = Logger.get("ThermostatServer");
152263
+ var logger194 = Logger.get("ThermostatServer");
152135
152264
  var ThermostatBehaviorLogicBase = ThermostatBehavior.with(
152136
152265
  Thermostat3.Feature.Heating,
152137
152266
  Thermostat3.Feature.Cooling,
@@ -152160,7 +152289,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152160
152289
  throw new ImplementationError("Setback feature is deprecated and not allowed to be enabled");
152161
152290
  }
152162
152291
  if (this.features.matterScheduleConfiguration) {
152163
- logger193.warn("MatterScheduleConfiguration feature is not yet implemented. Please do not activate it");
152292
+ logger194.warn("MatterScheduleConfiguration feature is not yet implemented. Please do not activate it");
152164
152293
  }
152165
152294
  if (!this.features.presets && !this.features.matterScheduleConfiguration) {
152166
152295
  this.atomicRequest = Behavior.unimplemented;
@@ -152281,7 +152410,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152281
152410
  throw new StatusResponse.InvalidCommandError("Requested PresetHandle not found");
152282
152411
  }
152283
152412
  }
152284
- logger193.info(`Setting active preset handle to`, presetHandle);
152413
+ logger194.info(`Setting active preset handle to`, presetHandle);
152285
152414
  this.state.activePresetHandle = presetHandle;
152286
152415
  return preset;
152287
152416
  }
@@ -152351,7 +152480,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152351
152480
  }
152352
152481
  if (this.state.setpointHoldExpiryTimestamp === void 0) {
152353
152482
  } else {
152354
- logger193.warn(
152483
+ logger194.warn(
152355
152484
  "Handling for setpointHoldExpiryTimestamp is not yet implemented. To use this attribute you need to install the needed logic yourself"
152356
152485
  );
152357
152486
  }
@@ -152424,7 +152553,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152424
152553
  "RemoteSensing cannot be set to LocalTemperature when LocalTemperatureNotExposed feature is enabled"
152425
152554
  );
152426
152555
  }
152427
- logger193.debug("LocalTemperatureNotExposed feature is enabled, ignoring local temperature measurement");
152556
+ logger194.debug("LocalTemperatureNotExposed feature is enabled, ignoring local temperature measurement");
152428
152557
  this.state.localTemperature = null;
152429
152558
  }
152430
152559
  let localTemperature = null;
@@ -152433,11 +152562,11 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152433
152562
  const endpoints = this.env.get(ServerNode).endpoints;
152434
152563
  const endpoint = endpoints.has(localTempEndpoint) ? endpoints.for(localTempEndpoint) : void 0;
152435
152564
  if (endpoint !== void 0 && endpoint.behaviors.has(TemperatureMeasurementServer)) {
152436
- logger193.debug(
152565
+ logger194.debug(
152437
152566
  `Using existing TemperatureMeasurement cluster on endpoint #${localTempEndpoint} for local temperature measurement`
152438
152567
  );
152439
152568
  if (this.state.externalMeasuredIndoorTemperature !== void 0) {
152440
- logger193.warn(
152569
+ logger194.warn(
152441
152570
  "Both local TemperatureMeasurement cluster and externalMeasuredIndoorTemperature state are set, using local cluster"
152442
152571
  );
152443
152572
  }
@@ -152447,19 +152576,19 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152447
152576
  );
152448
152577
  localTemperature = endpoint.stateOf(TemperatureMeasurementServer).measuredValue;
152449
152578
  } else {
152450
- logger193.warn(
152579
+ logger194.warn(
152451
152580
  `No TemperatureMeasurement cluster found on endpoint #${localTempEndpoint}, falling back to externalMeasuredIndoorTemperature state if set`
152452
152581
  );
152453
152582
  }
152454
152583
  } else {
152455
152584
  if (this.state.externalMeasuredIndoorTemperature === void 0) {
152456
152585
  if (this.state.localTemperatureCalibration !== void 0) {
152457
- logger193.warn(
152586
+ logger194.warn(
152458
152587
  "No local TemperatureMeasurement cluster available, externalMeasuredIndoorTemperature state not set but localTemperatureCalibration is used: Ensure to correctly consider the calibration when updating the localTemperature value"
152459
152588
  );
152460
152589
  }
152461
152590
  } else {
152462
- logger193.info("Using measured temperature via externalMeasuredIndoorTemperature state");
152591
+ logger194.info("Using measured temperature via externalMeasuredIndoorTemperature state");
152463
152592
  localTemperature = this.state.externalMeasuredIndoorTemperature ?? null;
152464
152593
  }
152465
152594
  this.reactTo(this.events.externalMeasuredIndoorTemperature$Changed, this.#handleMeasuredTemperatureChange);
@@ -152499,28 +152628,28 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152499
152628
  const endpoints = this.env.get(ServerNode).endpoints;
152500
152629
  const endpoint = endpoints.has(localOccupancyEndpoint) ? endpoints.for(localOccupancyEndpoint) : void 0;
152501
152630
  if (endpoint !== void 0 && endpoint.behaviors.has(OccupancySensingServer)) {
152502
- logger193.debug(
152631
+ logger194.debug(
152503
152632
  `Using existing OccupancySensing cluster on endpoint ${localOccupancyEndpoint} for local occupancy sensing`
152504
152633
  );
152505
152634
  if (this.state.externallyMeasuredOccupancy !== void 0) {
152506
- logger193.warn(
152635
+ logger194.warn(
152507
152636
  "Both local OccupancySensing cluster and externallyMeasuredOccupancy state are set, using local cluster"
152508
152637
  );
152509
152638
  }
152510
152639
  this.reactTo(endpoint.eventsOf(OccupancySensingServer).occupancy$Changed, this.#handleOccupancyChange);
152511
152640
  currentOccupancy = !!endpoint.stateOf(OccupancySensingServer).occupancy.occupied;
152512
152641
  } else {
152513
- logger193.warn(
152642
+ logger194.warn(
152514
152643
  `No OccupancySensing cluster found on endpoint ${localOccupancyEndpoint}, falling back to externallyMeasuredOccupancy state if set`
152515
152644
  );
152516
152645
  }
152517
152646
  } else {
152518
152647
  if (this.state.externallyMeasuredOccupancy === void 0) {
152519
- logger193.warn(
152648
+ logger194.warn(
152520
152649
  "No local OccupancySensing cluster available and externallyMeasuredOccupancy state not set"
152521
152650
  );
152522
152651
  } else {
152523
- logger193.info("Using occupancy via externallyMeasuredOccupancy state");
152652
+ logger194.info("Using occupancy via externallyMeasuredOccupancy state");
152524
152653
  currentOccupancy = this.state.externallyMeasuredOccupancy;
152525
152654
  }
152526
152655
  this.reactTo(this.events.externallyMeasuredOccupancy$Changed, this.#handleExternalOccupancyChange);
@@ -152763,7 +152892,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152763
152892
  max = this.state[`max${scope}`] ?? defaults.absMax,
152764
152893
  absMax = this.state[`absMax${scope}`] ?? defaults.absMax
152765
152894
  } = details;
152766
- logger193.debug(
152895
+ logger194.debug(
152767
152896
  `Validating user setpoint limits for ${scope}: absMin=${absMin}, min=${min}, max=${max}, absMax=${absMax}`
152768
152897
  );
152769
152898
  if (absMin > min) {
@@ -152810,7 +152939,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
152810
152939
  const limitMax = scope === "Heat" ? this.heatSetpointMaximum : this.coolSetpointMaximum;
152811
152940
  const result = cropValueRange(setpoint, limitMin, limitMax);
152812
152941
  if (result !== setpoint) {
152813
- logger193.debug(
152942
+ logger194.debug(
152814
152943
  `${scope} setpoint (${setpoint}) is out of limits [${limitMin}, ${limitMax}], clamping to ${result}`
152815
152944
  );
152816
152945
  }
@@ -153290,7 +153419,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
153290
153419
  */
153291
153420
  #handlePersistedPresetsChanged(newPresets, oldPresets) {
153292
153421
  if (oldPresets === void 0) {
153293
- logger193.debug(
153422
+ logger194.debug(
153294
153423
  "Old presets is undefined, skipping some checks. This should only happen on setup of the behavior."
153295
153424
  );
153296
153425
  }
@@ -153299,7 +153428,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
153299
153428
  const newPresetHandles = /* @__PURE__ */ new Set();
153300
153429
  for (const preset of newPresets) {
153301
153430
  if (preset.presetHandle === null) {
153302
- logger193.debug("Preset is missing presetHandle, generating a new one");
153431
+ logger194.debug("Preset is missing presetHandle, generating a new one");
153303
153432
  preset.presetHandle = entropy.randomBytes(16);
153304
153433
  changed = true;
153305
153434
  }
@@ -153348,7 +153477,7 @@ var ThermostatBaseServer = class _ThermostatBaseServer extends ThermostatBehavio
153348
153477
  throw new StatusResponse.InvalidInStateError(`ActivePresetHandle references non-existing presetHandle`);
153349
153478
  }
153350
153479
  if (changed) {
153351
- logger193.debug("PresetHandles or BuiltIn flags were updated, updating persistedPresets");
153480
+ logger194.debug("PresetHandles or BuiltIn flags were updated, updating persistedPresets");
153352
153481
  this.state.persistedPresets = newPresets;
153353
153482
  }
153354
153483
  }
@@ -154437,7 +154566,7 @@ var WindowCoveringBehaviorConstructor = ClusterBehavior.for(WindowCovering3);
154437
154566
  var WindowCoveringBehavior = WindowCoveringBehaviorConstructor;
154438
154567
 
154439
154568
  // ../../node_modules/.pnpm/@matter+node@0.17.9_patch_hash=e3376f1c2fdd0a0df09b8d04625a4d7af65b47d7277cc88cdb80baf7fb32bd0d/node_modules/@matter/node/dist/esm/behaviors/window-covering/WindowCoveringServer.js
154440
- var logger194 = Logger.get("WindowCoveringServer");
154569
+ var logger195 = Logger.get("WindowCoveringServer");
154441
154570
  var WindowCoveringBase = WindowCoveringBehavior.with(
154442
154571
  WindowCovering3.Feature.Lift,
154443
154572
  WindowCovering3.Feature.Tilt,
@@ -154522,7 +154651,7 @@ var WindowCoveringBaseServer = class extends WindowCoveringBase {
154522
154651
  this.state.configStatus = configStatus;
154523
154652
  });
154524
154653
  }
154525
- logger194.debug(
154654
+ logger195.debug(
154526
154655
  `Mode changed to ${Diagnostic.json(mode)} and config status to ${Diagnostic.json(configStatus)} and internal calibration mode to ${this.internal.calibrationMode}`
154527
154656
  );
154528
154657
  }
@@ -154530,7 +154659,7 @@ var WindowCoveringBaseServer = class extends WindowCoveringBase {
154530
154659
  #handleOperationalStatusChanging(operationalStatus) {
154531
154660
  const globalStatus = operationalStatus.lift !== WindowCovering3.MovementStatus.Stopped ? operationalStatus.lift : operationalStatus.tilt;
154532
154661
  operationalStatus.global = globalStatus;
154533
- logger194.debug(
154662
+ logger195.debug(
154534
154663
  `Operational status changed to ${Diagnostic.json(operationalStatus)} with new global status ${globalStatus}`
154535
154664
  );
154536
154665
  this.state.operationalStatus = operationalStatus;
@@ -154559,10 +154688,10 @@ var WindowCoveringBaseServer = class extends WindowCoveringBase {
154559
154688
  this.state.currentPositionLiftPercentage = percent100ths3 === null ? percent100ths3 : Math.floor(percent100ths3 / WC_PERCENT100THS_COEFFICIENT);
154560
154689
  if (this.state.operationalStatus.lift !== WindowCovering3.MovementStatus.Stopped && percent100ths3 === this.state.targetPositionLiftPercent100ths) {
154561
154690
  this.state.operationalStatus.lift = WindowCovering3.MovementStatus.Stopped;
154562
- logger194.debug("Lift movement stopped, target value reached");
154691
+ logger195.debug("Lift movement stopped, target value reached");
154563
154692
  }
154564
154693
  }
154565
- logger194.debug(
154694
+ logger195.debug(
154566
154695
  `Syncing lift position ${this.state.currentPositionLiftPercent100ths === null ? null : (this.state.currentPositionLiftPercent100ths / 100).toFixed(2)} to ${this.state.currentPositionLiftPercentage}%`
154567
154696
  );
154568
154697
  }
@@ -154572,10 +154701,10 @@ var WindowCoveringBaseServer = class extends WindowCoveringBase {
154572
154701
  this.state.currentPositionTiltPercentage = percent100ths3 === null ? percent100ths3 : Math.floor(percent100ths3 / WC_PERCENT100THS_COEFFICIENT);
154573
154702
  if (this.state.operationalStatus.tilt !== WindowCovering3.MovementStatus.Stopped && percent100ths3 === this.state.targetPositionTiltPercent100ths) {
154574
154703
  this.state.operationalStatus.tilt = WindowCovering3.MovementStatus.Stopped;
154575
- logger194.debug("Tilt movement stopped, target value reached");
154704
+ logger195.debug("Tilt movement stopped, target value reached");
154576
154705
  }
154577
154706
  }
154578
- logger194.debug(
154707
+ logger195.debug(
154579
154708
  `Syncing tilt position ${this.state.currentPositionTiltPercent100ths === null ? null : (this.state.currentPositionTiltPercent100ths / 100).toFixed(2)} to ${this.state.currentPositionTiltPercentage}%`
154580
154709
  );
154581
154710
  }
@@ -154663,7 +154792,7 @@ var WindowCoveringBaseServer = class extends WindowCoveringBase {
154663
154792
  }
154664
154793
  const directionInfo = direction === 2 ? ` in direction by position` : ` in direction ${direction === 1 ? "Close" : "Open"}`;
154665
154794
  const targetInfo = targetPercent100ths === void 0 ? "" : ` to target position ${(targetPercent100ths / 100).toFixed(2)}`;
154666
- logger194.debug(
154795
+ logger195.debug(
154667
154796
  `Moving the device ${type === 0 ? "Lift" : "Tilt"}${directionInfo} (reversed=${reversed})${targetInfo}`
154668
154797
  );
154669
154798
  }
@@ -154685,7 +154814,7 @@ var WindowCoveringBaseServer = class extends WindowCoveringBase {
154685
154814
  );
154686
154815
  }
154687
154816
  if (type === 0 && this.state.configStatus.liftMovementReversed) {
154688
- logger194.debug("Lift movement is reversed");
154817
+ logger195.debug("Lift movement is reversed");
154689
154818
  }
154690
154819
  switch (type) {
154691
154820
  case 0:
@@ -154941,7 +155070,7 @@ function matterSubscriptionOptions() {
154941
155070
  }
154942
155071
 
154943
155072
  // src/matter/endpoints/server-mode-server-node.ts
154944
- var logger195 = Logger.get("ServerModeServerNode");
155073
+ var logger196 = Logger.get("ServerModeServerNode");
154945
155074
  var ServerModeServerNode = class extends ServerNode {
154946
155075
  deviceEndpoints = /* @__PURE__ */ new Map();
154947
155076
  featureFlags;
@@ -155038,7 +155167,7 @@ var ServerModeServerNode = class extends ServerNode {
155038
155167
  await this.set({ basicInformation });
155039
155168
  } catch (e) {
155040
155169
  const msg = e instanceof Error ? e.message : String(e);
155041
- logger195.warn(
155170
+ logger196.warn(
155042
155171
  `Failed to apply server-mode identity for ${entityId}: ${msg}`
155043
155172
  );
155044
155173
  }
@@ -155049,7 +155178,7 @@ var ServerModeServerNode = class extends ServerNode {
155049
155178
  await this.set({ productDescription: { deviceType } });
155050
155179
  } catch (e) {
155051
155180
  const msg = e instanceof Error ? e.message : String(e);
155052
- logger195.warn(`Failed to set server-mode device type: ${msg}`);
155181
+ logger196.warn(`Failed to set server-mode device type: ${msg}`);
155053
155182
  }
155054
155183
  }
155055
155184
  async factoryReset() {
@@ -155069,84 +155198,6 @@ function dropUndefined(obj) {
155069
155198
 
155070
155199
  // src/plugins/builtin/camera/camera-tcp-requirement.ts
155071
155200
  import { readFileSync as readFileSync6 } from "node:fs";
155072
-
155073
- // src/plugins/plugin-storage.ts
155074
- init_esm();
155075
- import * as fs9 from "node:fs";
155076
- import * as path11 from "node:path";
155077
- var logger196 = Logger.get("PluginStorage");
155078
- var SAVE_DEBOUNCE_MS = 500;
155079
- function pluginStorageFilePath(storageDir, bridgeId, pluginName) {
155080
- const safe = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "_");
155081
- return path11.join(
155082
- storageDir,
155083
- `plugin-${safe(bridgeId)}-${safe(pluginName)}.json`
155084
- );
155085
- }
155086
- var FilePluginStorage = class {
155087
- data = {};
155088
- dirty = false;
155089
- filePath;
155090
- saveTimer;
155091
- constructor(storageDir, bridgeId, pluginName) {
155092
- this.filePath = pluginStorageFilePath(storageDir, bridgeId, pluginName);
155093
- this.load();
155094
- }
155095
- async get(key, defaultValue) {
155096
- const value = this.data[key];
155097
- return value ?? defaultValue;
155098
- }
155099
- async set(key, value) {
155100
- this.data[key] = value;
155101
- this.dirty = true;
155102
- this.scheduleSave();
155103
- }
155104
- async delete(key) {
155105
- delete this.data[key];
155106
- this.dirty = true;
155107
- this.scheduleSave();
155108
- }
155109
- async keys() {
155110
- return Object.keys(this.data);
155111
- }
155112
- load() {
155113
- try {
155114
- if (fs9.existsSync(this.filePath)) {
155115
- const raw = fs9.readFileSync(this.filePath, "utf-8");
155116
- this.data = JSON.parse(raw);
155117
- }
155118
- } catch (e) {
155119
- logger196.warn(`Failed to load plugin storage from ${this.filePath}:`, e);
155120
- this.data = {};
155121
- }
155122
- }
155123
- scheduleSave() {
155124
- if (this.saveTimer) clearTimeout(this.saveTimer);
155125
- this.saveTimer = setTimeout(() => this.save(), SAVE_DEBOUNCE_MS);
155126
- }
155127
- save() {
155128
- if (!this.dirty) return;
155129
- if (this.saveTimer) {
155130
- clearTimeout(this.saveTimer);
155131
- this.saveTimer = void 0;
155132
- }
155133
- try {
155134
- const dir = path11.dirname(this.filePath);
155135
- if (!fs9.existsSync(dir)) {
155136
- fs9.mkdirSync(dir, { recursive: true });
155137
- }
155138
- fs9.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
155139
- this.dirty = false;
155140
- } catch (e) {
155141
- logger196.warn(`Failed to save plugin storage to ${this.filePath}:`, e);
155142
- }
155143
- }
155144
- flush() {
155145
- this.save();
155146
- }
155147
- };
155148
-
155149
- // src/plugins/builtin/camera/camera-tcp-requirement.ts
155150
155201
  var CAMERA_TCP_CONFIG = { incoming: true, outgoing: false };
155151
155202
  function parseCameraList(cameras) {
155152
155203
  if (typeof cameras !== "string") return [];
@@ -156830,6 +156881,28 @@ var SafePluginRunner = class {
156830
156881
  return void 0;
156831
156882
  }
156832
156883
  }
156884
+ /**
156885
+ * Run a shutdown-class hook with timeout only. An open breaker never blocks
156886
+ * it and its failures never count toward the breaker: cleanup has to run
156887
+ * exactly when a plugin is at its most broken, or timers and sockets leak.
156888
+ */
156889
+ async runCleanup(pluginName, operation, fn, timeoutMs = DEFAULT_TIMEOUT_MS) {
156890
+ const timeout = this.createTimeout(pluginName, operation, timeoutMs);
156891
+ try {
156892
+ const result = await Promise.race([
156893
+ Promise.resolve().then(fn),
156894
+ timeout.promise
156895
+ ]);
156896
+ timeout.clear();
156897
+ return result;
156898
+ } catch (error) {
156899
+ timeout.clear();
156900
+ logger201.error(
156901
+ `Plugin "${pluginName}" failed during ${operation}: ${error instanceof Error ? error.message : String(error)}`
156902
+ );
156903
+ return void 0;
156904
+ }
156905
+ }
156833
156906
  /**
156834
156907
  * Run a synchronous plugin function with try/catch + circuit breaker.
156835
156908
  */
@@ -156880,6 +156953,8 @@ var SafePluginRunner = class {
156880
156953
  var logger202 = Logger.get("PluginManager");
156881
156954
  var PLUGIN_API_VERSION = 1;
156882
156955
  var MAX_PLUGIN_DEVICE_ID_LENGTH = 100;
156956
+ var LEGACY_ENABLED_KEY = "__enabled";
156957
+ var PLUGIN_CONFIG_KEY = "config";
156883
156958
  function validatePluginDevice(device) {
156884
156959
  if (!device || typeof device !== "object") return "device must be an object";
156885
156960
  const d = device;
@@ -156913,18 +156988,23 @@ var PluginManager = class {
156913
156988
  domainMappingOwners = /* @__PURE__ */ new Map();
156914
156989
  storageDir;
156915
156990
  bridgeId;
156991
+ stateFile;
156916
156992
  homeAssistant;
156917
156993
  runner = new SafePluginRunner();
156918
156994
  registry;
156919
156995
  /** Callback invoked when a plugin registers a new device */
156920
156996
  onDeviceRegistered;
156921
- /** Callback invoked when a plugin removes a device */
156997
+ /**
156998
+ * Callback invoked when a plugin removes a device. keepIdentity marks a
156999
+ * reversible stop: the endpoint closes but keeps its persisted number.
157000
+ */
156922
157001
  onDeviceUnregistered;
156923
157002
  /** Callback invoked when a plugin updates device state */
156924
157003
  onDeviceStateUpdated;
156925
157004
  constructor(bridgeId, storageDir, homeAssistant) {
156926
157005
  this.bridgeId = bridgeId;
156927
157006
  this.storageDir = storageDir;
157007
+ this.stateFile = pluginStateFilePath(storageDir, bridgeId);
156928
157008
  this.homeAssistant = homeAssistant;
156929
157009
  }
156930
157010
  setRegistry(registry3) {
@@ -157009,35 +157089,59 @@ var PluginManager = class {
157009
157089
  this.bridgeId,
157010
157090
  plugin.name
157011
157091
  );
157092
+ let enabled = this.readEnabledState()[plugin.name];
157093
+ const legacy = await storage2.get(LEGACY_ENABLED_KEY);
157094
+ if (typeof legacy === "boolean") {
157095
+ if (enabled === void 0) {
157096
+ enabled = legacy;
157097
+ this.persistEnabled(plugin.name, legacy);
157098
+ }
157099
+ await storage2.delete(LEGACY_ENABLED_KEY);
157100
+ storage2.flush();
157101
+ }
157102
+ if (enabled === false) {
157103
+ metadata.enabled = false;
157104
+ logger202.info(`Plugin "${plugin.name}" stays disabled (persisted)`);
157105
+ }
157012
157106
  const devices = /* @__PURE__ */ new Map();
157013
157107
  const pluginLogger = Logger.get(`Plugin:${plugin.name}`);
157108
+ const registerDeviceAt = (epoch) => async (device) => {
157109
+ const live = this.instances.get(plugin.name);
157110
+ if (!live || !live.metadata.enabled || live.epoch !== epoch) {
157111
+ pluginLogger.warn(
157112
+ `Dropped a device registration from a disabled or superseded start of "${plugin.name}"`
157113
+ );
157114
+ return;
157115
+ }
157116
+ const validationError = validatePluginDevice(device);
157117
+ if (validationError) {
157118
+ pluginLogger.warn(`Rejected device registration: ${validationError}`);
157119
+ return;
157120
+ }
157121
+ if (devices.has(device.id)) {
157122
+ pluginLogger.warn(
157123
+ `Device "${device.id}" already registered, updating`
157124
+ );
157125
+ }
157126
+ devices.set(device.id, device);
157127
+ await this.onDeviceRegistered?.(plugin.name, device);
157128
+ pluginLogger.debug(`Registered device: ${device.name} (${device.id})`);
157129
+ };
157014
157130
  const context = {
157015
157131
  bridgeId: this.bridgeId,
157016
157132
  storage: storage2,
157017
157133
  log: pluginLogger,
157018
157134
  homeAssistant: this.homeAssistant,
157019
- registerDevice: async (device) => {
157020
- const validationError = validatePluginDevice(device);
157021
- if (validationError) {
157022
- pluginLogger.warn(`Rejected device registration: ${validationError}`);
157023
- return;
157024
- }
157025
- if (devices.has(device.id)) {
157026
- pluginLogger.warn(
157027
- `Device "${device.id}" already registered, updating`
157028
- );
157029
- }
157030
- devices.set(device.id, device);
157031
- await this.onDeviceRegistered?.(plugin.name, device);
157032
- pluginLogger.debug(`Registered device: ${device.name} (${device.id})`);
157033
- },
157135
+ registerDevice: registerDeviceAt(0),
157034
157136
  unregisterDevice: async (deviceId) => {
157035
157137
  if (!devices.has(deviceId)) {
157036
157138
  pluginLogger.warn(`Device "${deviceId}" not found`);
157037
157139
  return;
157038
157140
  }
157039
157141
  devices.delete(deviceId);
157040
- await this.onDeviceUnregistered?.(plugin.name, deviceId);
157142
+ await this.onDeviceUnregistered?.(plugin.name, deviceId, {
157143
+ keepIdentity: this.instances.get(plugin.name)?.suspending === true
157144
+ });
157041
157145
  pluginLogger.debug(`Unregistered device: ${deviceId}`);
157042
157146
  },
157043
157147
  updateDeviceState: (deviceId, clusterId3, attributes9) => {
@@ -157076,7 +157180,14 @@ var PluginManager = class {
157076
157180
  context,
157077
157181
  metadata,
157078
157182
  devices,
157079
- started: false
157183
+ started: false,
157184
+ queue: Promise.resolve(),
157185
+ epoch: 0,
157186
+ contextAt: (epoch) => ({
157187
+ ...context,
157188
+ registerDevice: registerDeviceAt(epoch)
157189
+ }),
157190
+ suspending: false
157080
157191
  });
157081
157192
  logger202.info(
157082
157193
  `Registered plugin: ${plugin.name} v${plugin.version} (${metadata.source})`
@@ -157087,28 +157198,42 @@ var PluginManager = class {
157087
157198
  */
157088
157199
  async startAll() {
157089
157200
  for (const [name, instance] of this.instances) {
157090
- if (!instance.metadata.enabled) continue;
157091
- if (this.runner.isDisabled(name)) {
157092
- logger202.warn(
157093
- `Plugin "${name}" is disabled (circuit breaker), skipping start`
157094
- );
157095
- instance.metadata.enabled = false;
157096
- continue;
157097
- }
157098
- logger202.info(`Starting plugin: ${name}`);
157099
- await this.runner.run(
157100
- name,
157101
- "onStart",
157102
- () => instance.plugin.onStart(instance.context)
157201
+ await this.inTransition(instance, () => this.startPlugin(name, instance));
157202
+ }
157203
+ }
157204
+ // One transition at a time per plugin: a disable during a slow start waits
157205
+ // for the start to settle instead of interleaving with it.
157206
+ inTransition(instance, fn) {
157207
+ const run = instance.queue.then(fn, fn);
157208
+ instance.queue = run.then(
157209
+ () => void 0,
157210
+ () => void 0
157211
+ );
157212
+ return run;
157213
+ }
157214
+ async startPlugin(name, instance) {
157215
+ if (!instance.metadata.enabled) return;
157216
+ if (this.runner.isDisabled(name)) {
157217
+ logger202.warn(
157218
+ `Plugin "${name}" is disabled (circuit breaker), skipping start`
157103
157219
  );
157104
- if (this.runner.isDisabled(name)) {
157105
- instance.metadata.enabled = false;
157106
- } else if (this.runner.getState(name).failures === 0) {
157107
- instance.started = true;
157108
- }
157109
- if (instance.plugin.getCurrentConfig) {
157110
- instance.metadata.config = instance.plugin.getCurrentConfig();
157111
- }
157220
+ instance.metadata.enabled = false;
157221
+ return;
157222
+ }
157223
+ logger202.info(`Starting plugin: ${name}`);
157224
+ const epoch = ++instance.epoch;
157225
+ await this.runner.run(
157226
+ name,
157227
+ "onStart",
157228
+ () => instance.plugin.onStart(instance.contextAt(epoch))
157229
+ );
157230
+ if (this.runner.isDisabled(name)) {
157231
+ instance.metadata.enabled = false;
157232
+ } else if (this.runner.getState(name).failures === 0) {
157233
+ instance.started = true;
157234
+ }
157235
+ if (instance.plugin.getCurrentConfig) {
157236
+ instance.metadata.config = instance.plugin.getCurrentConfig();
157112
157237
  }
157113
157238
  }
157114
157239
  /**
@@ -157130,23 +157255,32 @@ var PluginManager = class {
157130
157255
  }
157131
157256
  }
157132
157257
  /**
157133
- * Shut down all plugins via SafePluginRunner.
157258
+ * Shut down all plugins. Runs outside the circuit breaker: cleanup must
157259
+ * happen even for a plugin the breaker took down.
157134
157260
  */
157135
157261
  async shutdownAll(reason) {
157136
157262
  for (const [name, instance] of this.instances) {
157137
- if (instance.started && instance.plugin.onShutdown) {
157138
- await this.runner.run(
157139
- name,
157140
- "onShutdown",
157141
- () => instance.plugin.onShutdown(reason)
157142
- );
157143
- }
157144
- const storage2 = instance.context.storage;
157145
- if (storage2 instanceof FilePluginStorage) {
157146
- storage2.flush();
157147
- }
157148
- instance.started = false;
157149
- logger202.info(`Plugin "${name}" shut down`);
157263
+ await this.inTransition(instance, async () => {
157264
+ instance.epoch++;
157265
+ instance.suspending = true;
157266
+ try {
157267
+ if (instance.started && instance.plugin.onShutdown) {
157268
+ await this.runner.runCleanup(
157269
+ name,
157270
+ "onShutdown",
157271
+ () => instance.plugin.onShutdown(reason)
157272
+ );
157273
+ }
157274
+ } finally {
157275
+ instance.suspending = false;
157276
+ }
157277
+ const storage2 = instance.context.storage;
157278
+ if (storage2 instanceof FilePluginStorage) {
157279
+ storage2.flush();
157280
+ }
157281
+ instance.started = false;
157282
+ logger202.info(`Plugin "${name}" shut down`);
157283
+ });
157150
157284
  }
157151
157285
  this.instances.clear();
157152
157286
  }
@@ -157179,23 +157313,122 @@ var PluginManager = class {
157179
157313
  instance.metadata.enabled = true;
157180
157314
  }
157181
157315
  }
157182
- disablePlugin(pluginName) {
157316
+ /**
157317
+ * Disable a plugin: stop it, unmount its devices, and persist the choice so
157318
+ * a bridge restart does not bring it back (#439). The shutdown runs outside
157319
+ * the circuit breaker, a broken plugin still has to release its resources.
157320
+ * Returns the resulting metadata, or undefined for an unknown name.
157321
+ */
157322
+ async disablePlugin(pluginName) {
157183
157323
  const instance = this.instances.get(pluginName);
157184
- if (instance) {
157324
+ if (!instance) return void 0;
157325
+ return this.inTransition(instance, async () => {
157185
157326
  instance.metadata.enabled = false;
157186
- }
157187
- for (const [domain, owner] of this.domainMappingOwners) {
157188
- if (owner === pluginName) {
157189
- this.domainMappings.delete(domain);
157190
- this.domainMappingOwners.delete(domain);
157327
+ instance.epoch++;
157328
+ this.persistEnabled(pluginName, false);
157329
+ instance.suspending = true;
157330
+ try {
157331
+ if (instance.started && instance.plugin.onShutdown) {
157332
+ await this.runner.runCleanup(
157333
+ pluginName,
157334
+ "onShutdown",
157335
+ () => instance.plugin.onShutdown("Plugin disabled")
157336
+ );
157337
+ }
157338
+ instance.started = false;
157339
+ for (const deviceId of [...instance.devices.keys()]) {
157340
+ instance.devices.delete(deviceId);
157341
+ await this.onDeviceUnregistered?.(pluginName, deviceId, {
157342
+ keepIdentity: true
157343
+ });
157344
+ }
157345
+ } finally {
157346
+ instance.suspending = false;
157191
157347
  }
157192
- }
157348
+ for (const [domain, owner] of this.domainMappingOwners) {
157349
+ if (owner === pluginName) {
157350
+ this.domainMappings.delete(domain);
157351
+ this.domainMappingOwners.delete(domain);
157352
+ }
157353
+ }
157354
+ return instance.metadata;
157355
+ });
157193
157356
  }
157194
- enablePlugin(pluginName) {
157195
- this.runner.resetCircuitBreaker(pluginName);
157357
+ /**
157358
+ * Enable a plugin, persist the choice, and start it right away so its
157359
+ * devices come back without a bridge restart. Returns the resulting
157360
+ * metadata, or undefined for an unknown name.
157361
+ */
157362
+ async enablePlugin(pluginName) {
157196
157363
  const instance = this.instances.get(pluginName);
157197
- if (instance) {
157364
+ if (!instance) return void 0;
157365
+ return this.inTransition(instance, async () => {
157366
+ this.runner.resetCircuitBreaker(pluginName);
157198
157367
  instance.metadata.enabled = true;
157368
+ this.persistEnabled(pluginName, true);
157369
+ if (instance.started) return instance.metadata;
157370
+ const pending = instance.pendingConfig;
157371
+ if (pending) {
157372
+ try {
157373
+ await instance.context.storage.set(PLUGIN_CONFIG_KEY, pending);
157374
+ await instance.context.storage.flush?.();
157375
+ instance.metadata.config = pending;
157376
+ instance.pendingConfig = void 0;
157377
+ } catch (e) {
157378
+ logger202.warn(
157379
+ `Failed to persist the parked config for "${pluginName}", it stays parked:`,
157380
+ e
157381
+ );
157382
+ }
157383
+ }
157384
+ logger202.info(`Starting plugin: ${pluginName}`);
157385
+ const epoch = ++instance.epoch;
157386
+ await this.runner.run(
157387
+ pluginName,
157388
+ "onStart",
157389
+ () => instance.plugin.onStart(instance.contextAt(epoch))
157390
+ );
157391
+ if (this.runner.isDisabled(pluginName)) {
157392
+ instance.metadata.enabled = false;
157393
+ return instance.metadata;
157394
+ }
157395
+ if (this.runner.getState(pluginName).failures === 0) {
157396
+ instance.started = true;
157397
+ }
157398
+ if (instance.plugin.getCurrentConfig) {
157399
+ instance.metadata.config = instance.plugin.getCurrentConfig();
157400
+ }
157401
+ if (instance.started && instance.plugin.onConfigure) {
157402
+ await this.runner.run(
157403
+ pluginName,
157404
+ "onConfigure",
157405
+ () => instance.plugin.onConfigure()
157406
+ );
157407
+ }
157408
+ return instance.metadata;
157409
+ });
157410
+ }
157411
+ readEnabledState() {
157412
+ try {
157413
+ if (fs10.existsSync(this.stateFile)) {
157414
+ return JSON.parse(fs10.readFileSync(this.stateFile, "utf-8"));
157415
+ }
157416
+ } catch (e) {
157417
+ logger202.warn(`Failed to read the plugin state file:`, e);
157418
+ }
157419
+ return {};
157420
+ }
157421
+ persistEnabled(pluginName, enabled) {
157422
+ try {
157423
+ const state = this.readEnabledState();
157424
+ state[pluginName] = enabled;
157425
+ fs10.mkdirSync(path12.dirname(this.stateFile), { recursive: true });
157426
+ fs10.writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
157427
+ } catch (e) {
157428
+ logger202.warn(
157429
+ `Failed to persist enabled=${enabled} for "${pluginName}":`,
157430
+ e
157431
+ );
157199
157432
  }
157200
157433
  }
157201
157434
  getConfigSchema(pluginName) {
@@ -157209,27 +157442,36 @@ var PluginManager = class {
157209
157442
  async updateConfig(pluginName, config8) {
157210
157443
  const instance = this.instances.get(pluginName);
157211
157444
  if (!instance) return false;
157212
- config8 = { ...config8 };
157213
- const schema6 = instance.plugin.getConfigSchema?.();
157214
- if (schema6) {
157215
- for (const [key, prop] of Object.entries(schema6.properties)) {
157216
- if (prop.secret && config8[key] === SECRET_UNCHANGED) {
157217
- const stored = instance.metadata.config[key];
157218
- if (stored == null) delete config8[key];
157219
- else config8[key] = stored;
157445
+ return this.inTransition(instance, async () => {
157446
+ config8 = { ...config8 };
157447
+ const schema6 = instance.plugin.getConfigSchema?.();
157448
+ if (schema6) {
157449
+ for (const [key, prop] of Object.entries(schema6.properties)) {
157450
+ if (prop.secret && config8[key] === SECRET_UNCHANGED) {
157451
+ const stored = instance.metadata.config[key];
157452
+ if (stored == null) delete config8[key];
157453
+ else config8[key] = stored;
157454
+ }
157455
+ }
157456
+ }
157457
+ instance.metadata.config = config8;
157458
+ this.registry?.updateConfig(pluginName, config8);
157459
+ if (instance.plugin.onConfigChanged) {
157460
+ if (!instance.metadata.enabled) {
157461
+ instance.pendingConfig = config8;
157462
+ logger202.info(
157463
+ `Plugin "${pluginName}" is disabled, the config applies on enable`
157464
+ );
157465
+ } else {
157466
+ await this.runner.run(
157467
+ pluginName,
157468
+ "onConfigChanged",
157469
+ () => instance.plugin.onConfigChanged(config8)
157470
+ );
157220
157471
  }
157221
157472
  }
157222
- }
157223
- instance.metadata.config = config8;
157224
- this.registry?.updateConfig(pluginName, config8);
157225
- if (instance.plugin.onConfigChanged) {
157226
- await this.runner.run(
157227
- pluginName,
157228
- "onConfigChanged",
157229
- () => instance.plugin.onConfigChanged(config8)
157230
- );
157231
- }
157232
- return true;
157473
+ return true;
157474
+ });
157233
157475
  }
157234
157476
  };
157235
157477
 
@@ -157980,11 +158222,11 @@ var Bridge = class {
157980
158222
  get pluginInfo() {
157981
158223
  return this.endpointManager.getPluginInfo();
157982
158224
  }
157983
- enablePlugin(pluginName) {
157984
- this.endpointManager.enablePlugin(pluginName);
158225
+ async enablePlugin(pluginName) {
158226
+ return await this.endpointManager.enablePlugin(pluginName);
157985
158227
  }
157986
- disablePlugin(pluginName) {
157987
- this.endpointManager.disablePlugin(pluginName);
158228
+ async disablePlugin(pluginName) {
158229
+ return await this.endpointManager.disablePlugin(pluginName);
157988
158230
  }
157989
158231
  resetPlugin(pluginName) {
157990
158232
  this.endpointManager.resetPlugin(pluginName);
@@ -171139,6 +171381,31 @@ function createCleanAreaServiceAreaServer(cleanAreaRooms) {
171139
171381
  currentArea: null
171140
171382
  });
171141
171383
  }
171384
+ function getVacuumServiceAreas(attributes9, mapping) {
171385
+ const cleanAreaRooms = mapping?.cleanAreaRooms;
171386
+ if (cleanAreaRooms && cleanAreaRooms.length > 0) {
171387
+ return cleanAreaRooms.map((room) => ({
171388
+ areaId: room.areaId,
171389
+ name: room.name
171390
+ }));
171391
+ }
171392
+ const customAreas = mapping?.customServiceAreas;
171393
+ if (customAreas && customAreas.length > 0) {
171394
+ return customAreas.map((area, index) => ({
171395
+ areaId: index + 1,
171396
+ name: area.name
171397
+ }));
171398
+ }
171399
+ const roomEntities = mapping?.roomEntities;
171400
+ const rooms = roomEntities && roomEntities.length > 0 ? buttonEntitiesToRooms(roomEntities, attributes9) : parseVacuumRooms(attributes9);
171401
+ if (rooms.length > 0) {
171402
+ return roomsToAreas(rooms).map((area) => ({
171403
+ areaId: area.areaId,
171404
+ name: area.areaInfo.locationInfo?.locationName ?? ""
171405
+ }));
171406
+ }
171407
+ return [];
171408
+ }
171142
171409
 
171143
171410
  // src/matter/endpoints/legacy/vacuum/behaviors/vacuum-rvc-run-mode-server.ts
171144
171411
  var logger236 = Logger.get("VacuumRvcRunModeServer");
@@ -171313,6 +171580,135 @@ var cleaningStates = [
171313
171580
  function vacuumIsCleaning(state) {
171314
171581
  return state != null && cleaningStates.includes(state);
171315
171582
  }
171583
+ function dispatchRoomClean(attributes9, mapping, entityId, selectedAreas, seam) {
171584
+ const customAreas = mapping?.customServiceAreas;
171585
+ if (customAreas && customAreas.length > 0) {
171586
+ const ephemeral = {
171587
+ completedAreas: /* @__PURE__ */ new Set(),
171588
+ lastCurrentArea: null,
171589
+ activeAreas: [],
171590
+ loggedShortCircuits: /* @__PURE__ */ new Set(),
171591
+ observedCleaning: false,
171592
+ pendingDispatches: [],
171593
+ cleanedAreaBaseline: null
171594
+ };
171595
+ const action = handleCustomServiceAreas(
171596
+ selectedAreas,
171597
+ customAreas,
171598
+ ephemeral
171599
+ );
171600
+ return { action, pending: ephemeral.pendingDispatches };
171601
+ }
171602
+ const cleanAreaRooms = mapping?.cleanAreaRooms;
171603
+ if (cleanAreaRooms && cleanAreaRooms.length > 0) {
171604
+ const haAreaIds = resolveCleanAreaIds(selectedAreas, cleanAreaRooms);
171605
+ if (haAreaIds.length > 0) {
171606
+ logger236.info(`CLEAN_AREA: cleaning HA areas: ${haAreaIds.join(", ")}`);
171607
+ return {
171608
+ action: {
171609
+ action: "vacuum.clean_area",
171610
+ data: { cleaning_area_id: haAreaIds }
171611
+ },
171612
+ pending: []
171613
+ };
171614
+ }
171615
+ }
171616
+ const roomEntities = mapping?.roomEntities;
171617
+ if (roomEntities && roomEntities.length > 0) {
171618
+ const matched = [];
171619
+ for (const areaId of selectedAreas) {
171620
+ const buttonId = roomEntities.find((id) => toAreaId(id) === areaId);
171621
+ if (buttonId) {
171622
+ matched.push({ areaId, entityId: buttonId });
171623
+ }
171624
+ }
171625
+ if (matched.length > 0) {
171626
+ logger236.info(
171627
+ `Roborock: ${matched.length} room button(s) queued: ${matched.map((m) => m.entityId).join(", ")}`
171628
+ );
171629
+ return {
171630
+ action: { action: "button.press", target: matched[0].entityId },
171631
+ pending: matched.slice(1).map(({ areaId, entityId: buttonId }) => ({
171632
+ areaId,
171633
+ action: { action: "button.press", target: buttonId }
171634
+ }))
171635
+ };
171636
+ }
171637
+ }
171638
+ if (entityId.startsWith("vacuum.valetudo_")) {
171639
+ return {
171640
+ action: buildValetudoSegmentAction(
171641
+ entityId,
171642
+ selectedAreas,
171643
+ mapping?.valetudoIdentifier
171644
+ ),
171645
+ pending: []
171646
+ };
171647
+ }
171648
+ const rooms = parseVacuumRooms(attributes9);
171649
+ const roomIds = [];
171650
+ let targetMapName;
171651
+ for (const areaId of selectedAreas) {
171652
+ const room = rooms.find((r) => toAreaId(r.id) === areaId);
171653
+ if (room) {
171654
+ roomIds.push(room.originalId ?? room.id);
171655
+ if (room.mapName && !targetMapName) {
171656
+ targetMapName = room.mapName;
171657
+ }
171658
+ }
171659
+ }
171660
+ if (roomIds.length > 0) {
171661
+ logger236.info(`Starting cleaning with selected areas: ${roomIds.join(", ")}`);
171662
+ if (isDreameVacuum(attributes9)) {
171663
+ if (targetMapName) {
171664
+ const vacName = entityId.replace("vacuum.", "");
171665
+ const selectedMapEntity = `select.${vacName}_selected_map`;
171666
+ logger236.info(
171667
+ `Dreame multi-floor: switching to map "${targetMapName}" via ${selectedMapEntity}`
171668
+ );
171669
+ seam.callAction({
171670
+ action: "select.select_option",
171671
+ target: selectedMapEntity,
171672
+ data: { option: targetMapName }
171673
+ });
171674
+ }
171675
+ return {
171676
+ action: {
171677
+ action: "dreame_vacuum.vacuum_clean_segment",
171678
+ data: { segments: roomIds.length === 1 ? roomIds[0] : roomIds }
171679
+ },
171680
+ pending: []
171681
+ };
171682
+ }
171683
+ if (isRoborockVacuum(attributes9) || isXiaomiMiotVacuum(attributes9)) {
171684
+ return {
171685
+ action: {
171686
+ action: "vacuum.send_command",
171687
+ data: { command: "app_segment_clean", params: roomIds }
171688
+ },
171689
+ pending: []
171690
+ };
171691
+ }
171692
+ if (isEcovacsVacuum(attributes9)) {
171693
+ const roomIdStr = roomIds.join(",");
171694
+ logger236.info(`Ecovacs vacuum: Using spot_area for rooms: ${roomIdStr}`);
171695
+ return {
171696
+ action: {
171697
+ action: "vacuum.send_command",
171698
+ data: {
171699
+ command: "spot_area",
171700
+ params: { mapID: 0, cleanings: 1, rooms: roomIdStr }
171701
+ }
171702
+ },
171703
+ pending: []
171704
+ };
171705
+ }
171706
+ logger236.warn(
171707
+ `Room cleaning via send_command not supported for this vacuum type. Rooms: ${roomIds.join(", ")}. Falling back to vacuum.start`
171708
+ );
171709
+ }
171710
+ return { action: { action: "vacuum.start" }, pending: [] };
171711
+ }
171316
171712
  var vacuumRvcRunModeConfig = {
171317
171713
  getCurrentMode: (entity) => {
171318
171714
  const isCleaning = vacuumIsCleaning(entity.state);
@@ -171339,123 +171735,19 @@ var vacuumRvcRunModeConfig = {
171339
171735
  const selectedAreas = [...serviceArea.state.selectedAreas];
171340
171736
  if (selectedAreas.length > 0) {
171341
171737
  const homeAssistant = agent.get(HomeAssistantEntityBehavior);
171342
- const entity = homeAssistant.entity;
171343
- const attributes9 = entity.state.attributes;
171738
+ const effective = homeAssistant.endpoint.vacuumEffective;
171739
+ const attributes9 = (effective?.state ?? homeAssistant.entity.state).attributes;
171740
+ const mapping = effective ? effective.mapping : homeAssistant.state.mapping;
171344
171741
  const session = getSession(homeAssistant.endpoint);
171345
- const customAreas = homeAssistant.state.mapping?.customServiceAreas;
171346
- if (customAreas && customAreas.length > 0) {
171347
- return handleCustomServiceAreas(selectedAreas, customAreas, session);
171348
- }
171349
- const cleanAreaRooms = homeAssistant.state.mapping?.cleanAreaRooms;
171350
- if (cleanAreaRooms && cleanAreaRooms.length > 0) {
171351
- const haAreaIds = resolveCleanAreaIds(selectedAreas, cleanAreaRooms);
171352
- if (haAreaIds.length > 0) {
171353
- logger236.info(
171354
- `CLEAN_AREA: cleaning HA areas: ${haAreaIds.join(", ")}`
171355
- );
171356
- return {
171357
- action: "vacuum.clean_area",
171358
- data: { cleaning_area_id: haAreaIds }
171359
- };
171360
- }
171361
- }
171362
- const roomEntities = homeAssistant.state.mapping?.roomEntities;
171363
- if (roomEntities && roomEntities.length > 0) {
171364
- const matched = [];
171365
- for (const areaId of selectedAreas) {
171366
- const entityId = roomEntities.find((id) => toAreaId(id) === areaId);
171367
- if (entityId) {
171368
- matched.push({ areaId, entityId });
171369
- }
171370
- }
171371
- if (matched.length > 0) {
171372
- logger236.info(
171373
- `Roborock: ${matched.length} room button(s) queued: ${matched.map((m) => m.entityId).join(", ")}`
171374
- );
171375
- session.pendingDispatches = matched.slice(1).map(({ areaId, entityId }) => ({
171376
- areaId,
171377
- action: { action: "button.press", target: entityId }
171378
- }));
171379
- return {
171380
- action: "button.press",
171381
- target: matched[0].entityId
171382
- };
171383
- }
171384
- }
171385
- const vacuumEntityId = homeAssistant.entityId;
171386
- if (vacuumEntityId.startsWith("vacuum.valetudo_")) {
171387
- return buildValetudoSegmentAction(
171388
- vacuumEntityId,
171389
- selectedAreas,
171390
- homeAssistant.state.mapping?.valetudoIdentifier
171391
- );
171392
- }
171393
- const rooms = parseVacuumRooms(attributes9);
171394
- const roomIds = [];
171395
- let targetMapName;
171396
- for (const areaId of selectedAreas) {
171397
- const room = rooms.find((r) => toAreaId(r.id) === areaId);
171398
- if (room) {
171399
- roomIds.push(room.originalId ?? room.id);
171400
- if (room.mapName && !targetMapName) {
171401
- targetMapName = room.mapName;
171402
- }
171403
- }
171404
- }
171405
- if (roomIds.length > 0) {
171406
- logger236.info(
171407
- `Starting cleaning with selected areas: ${roomIds.join(", ")}`
171408
- );
171409
- if (isDreameVacuum(attributes9)) {
171410
- if (targetMapName) {
171411
- const vacName = vacuumEntityId.replace("vacuum.", "");
171412
- const selectedMapEntity = `select.${vacName}_selected_map`;
171413
- logger236.info(
171414
- `Dreame multi-floor: switching to map "${targetMapName}" via ${selectedMapEntity}`
171415
- );
171416
- homeAssistant.callAction({
171417
- action: "select.select_option",
171418
- target: selectedMapEntity,
171419
- data: { option: targetMapName }
171420
- });
171421
- }
171422
- return {
171423
- action: "dreame_vacuum.vacuum_clean_segment",
171424
- data: {
171425
- segments: roomIds.length === 1 ? roomIds[0] : roomIds
171426
- }
171427
- };
171428
- }
171429
- if (isRoborockVacuum(attributes9) || isXiaomiMiotVacuum(attributes9)) {
171430
- return {
171431
- action: "vacuum.send_command",
171432
- data: {
171433
- command: "app_segment_clean",
171434
- params: roomIds
171435
- }
171436
- };
171437
- }
171438
- if (isEcovacsVacuum(attributes9)) {
171439
- const roomIdStr = roomIds.join(",");
171440
- logger236.info(
171441
- `Ecovacs vacuum: Using spot_area for rooms: ${roomIdStr}`
171442
- );
171443
- return {
171444
- action: "vacuum.send_command",
171445
- data: {
171446
- command: "spot_area",
171447
- params: {
171448
- mapID: 0,
171449
- cleanings: 1,
171450
- rooms: roomIdStr
171451
- }
171452
- }
171453
- };
171454
- }
171455
- logger236.warn(
171456
- `Room cleaning via send_command not supported for this vacuum type. Rooms: ${roomIds.join(", ")}. Falling back to vacuum.start`
171457
- );
171458
- }
171742
+ const result = dispatchRoomClean(
171743
+ attributes9,
171744
+ mapping,
171745
+ homeAssistant.entityId,
171746
+ selectedAreas,
171747
+ { callAction: (a) => homeAssistant.callAction(a) }
171748
+ );
171749
+ session.pendingDispatches = result.pending;
171750
+ return result.action;
171459
171751
  }
171460
171752
  } catch {
171461
171753
  }
@@ -173922,6 +174214,12 @@ function asStandaloneEndpointType(type) {
173922
174214
  // src/matter/endpoints/legacy/legacy-endpoint.ts
173923
174215
  var logger245 = Logger.get("LegacyEndpoint");
173924
174216
  var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174217
+ constructor(type, entityId, customName, mappedEntityIds, throttleMs, endpointId, vacuumEffective) {
174218
+ super(type, entityId, customName, mappedEntityIds, endpointId);
174219
+ this.vacuumEffective = vacuumEffective;
174220
+ this.flushUpdate = throttleMs && throttleMs > 50 ? throttleLatest(this.flushPendingUpdate.bind(this), throttleMs) : debounce6(this.flushPendingUpdate.bind(this), 50);
174221
+ }
174222
+ vacuumEffective;
173925
174223
  static async create(registry3, entityId, mapping, pluginDomainMappings, standalone = false, endpointId, identityAnchor) {
173926
174224
  const deviceRegistry = registry3.deviceOf(entityId);
173927
174225
  let state = registry3.initialState(entityId);
@@ -174295,19 +174593,17 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174295
174593
  }
174296
174594
  const customName = effectiveMapping?.customName;
174297
174595
  const mappedIds = getMappedEntityIds(effectiveMapping);
174596
+ const vacuumEffective = entityId.startsWith("vacuum.") ? { mapping: effectiveMapping, state } : void 0;
174298
174597
  return new _LegacyEndpoint(
174299
174598
  type,
174300
174599
  entityId,
174301
174600
  customName,
174302
174601
  mappedIds,
174303
174602
  effectiveMapping?.updateThrottleMs,
174304
- endpointId
174603
+ endpointId,
174604
+ vacuumEffective
174305
174605
  );
174306
174606
  }
174307
- constructor(type, entityId, customName, mappedEntityIds, throttleMs, endpointId) {
174308
- super(type, entityId, customName, mappedEntityIds, endpointId);
174309
- this.flushUpdate = throttleMs && throttleMs > 50 ? throttleLatest(this.flushPendingUpdate.bind(this), throttleMs) : debounce6(this.flushPendingUpdate.bind(this), 50);
174310
- }
174311
174607
  lastState;
174312
174608
  pendingMappedChange = false;
174313
174609
  flushUpdate;
@@ -174374,6 +174670,96 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174374
174670
  }
174375
174671
  };
174376
174672
 
174673
+ // src/matter/endpoints/legacy/vacuum/vacuum-area-switch.ts
174674
+ init_esm();
174675
+ init_home_assistant_entity_behavior();
174676
+ var VacuumAreaSwitchEndpoint = class _VacuumAreaSwitchEndpoint extends EntityEndpoint {
174677
+ constructor(type, entityId, endpointId, areaId, vacuumEndpointId, parentEffective) {
174678
+ super(type, entityId, void 0, [], endpointId);
174679
+ this.areaId = areaId;
174680
+ this.vacuumEndpointId = vacuumEndpointId;
174681
+ this.parentEffective = parentEffective;
174682
+ }
174683
+ areaId;
174684
+ vacuumEndpointId;
174685
+ parentEffective;
174686
+ static create(params) {
174687
+ const { vacuumEndpointId, entity, mapping, area, parentEffective } = params;
174688
+ const endpointId = `${vacuumEndpointId}_roomsw_${area.areaId}`;
174689
+ const areaId = area.areaId;
174690
+ const switchMapping = mapping?.customSerialNumber ? { ...mapping, customSerialNumber: void 0 } : mapping;
174691
+ const switchEntity = entity.deviceRegistry?.serial_number ? {
174692
+ ...entity,
174693
+ deviceRegistry: {
174694
+ ...entity.deviceRegistry,
174695
+ serial_number: void 0
174696
+ }
174697
+ } : entity;
174698
+ const type = OnOffPlugInUnitDevice.with(
174699
+ BasicInformationServer2,
174700
+ IdentifyServer2,
174701
+ HomeAssistantEntityBehavior,
174702
+ OnOffServer2({
174703
+ isOn: () => false,
174704
+ // off is a no-op (momentary auto-reset only, script pattern).
174705
+ turnOff: null,
174706
+ turnOn: (_value, agent) => {
174707
+ const ha = agent.get(HomeAssistantEntityBehavior);
174708
+ const { action } = dispatchRoomClean(
174709
+ parentEffective.state.attributes,
174710
+ parentEffective.mapping,
174711
+ ha.entityId,
174712
+ [areaId],
174713
+ { callAction: (a) => ha.callAction(a) }
174714
+ );
174715
+ return action;
174716
+ }
174717
+ })
174718
+ ).set({
174719
+ homeAssistantEntity: {
174720
+ entity: switchEntity,
174721
+ mapping: switchMapping,
174722
+ customName: area.name,
174723
+ // Distinct anchor so uniqueId/serial don't collide with the vacuum's,
174724
+ // and stay frozen across HA renames.
174725
+ identityAnchor: endpointId
174726
+ }
174727
+ });
174728
+ return new _VacuumAreaSwitchEndpoint(
174729
+ type,
174730
+ entity.entity_id,
174731
+ endpointId,
174732
+ areaId,
174733
+ vacuumEndpointId,
174734
+ parentEffective
174735
+ );
174736
+ }
174737
+ async updateStates(states) {
174738
+ const state = states[this.entityId];
174739
+ if (!state) return;
174740
+ try {
174741
+ await this.construction.ready;
174742
+ } catch {
174743
+ return;
174744
+ }
174745
+ try {
174746
+ const current = this.stateOf(HomeAssistantEntityBehavior).entity;
174747
+ await this.setStateOf(HomeAssistantEntityBehavior, {
174748
+ entity: { ...current, state }
174749
+ });
174750
+ } catch (error) {
174751
+ if (error instanceof TransactionDestroyedError || error instanceof DestroyedDependencyError) {
174752
+ return;
174753
+ }
174754
+ const message = error instanceof Error ? error.message : String(error);
174755
+ if (message.includes("Endpoint storage inaccessible")) {
174756
+ return;
174757
+ }
174758
+ throw error;
174759
+ }
174760
+ }
174761
+ };
174762
+
174377
174763
  // src/plugins/builtin/camera/camera-plugin.ts
174378
174764
  init_esm();
174379
174765
 
@@ -174482,10 +174868,11 @@ function unregisterRequestor(sessionId) {
174482
174868
  pendingDeliveries.delete(sessionId);
174483
174869
  }
174484
174870
  }
174485
- function unregisterAllRequestors() {
174486
- for (const id of [...registry2.keys()]) unregisterRequestor(id);
174487
- for (const timer of pendingDeliveries.values()) clearTimeout(timer);
174488
- pendingDeliveries.clear();
174871
+ function unregisterRequestorsByOwner(owner) {
174872
+ if (owner == null) return;
174873
+ for (const [id, registration] of [...registry2]) {
174874
+ if (registration.owner === owner) unregisterRequestor(id);
174875
+ }
174489
174876
  }
174490
174877
  function deliverAnswerDeferred(sessionId, sdp, onGiveUp) {
174491
174878
  const prior = pendingDeliveries.get(sessionId);
@@ -174653,7 +175040,9 @@ var CameraWebRtcProviderServer = class extends WebRtcTransportProviderServer {
174653
175040
  registerRequestor(id, {
174654
175041
  session,
174655
175042
  requestorEndpoint,
174656
- env: this.env
175043
+ env: this.env,
175044
+ // The bridge instance scopes this session to its camera plugin.
175045
+ owner: this.state.bridge
174657
175046
  });
174658
175047
  }
174659
175048
  let answerSdp;
@@ -175358,10 +175747,11 @@ var CameraPlugin = class {
175358
175747
  });
175359
175748
  }
175360
175749
  this.deviceIds = [];
175361
- await this.bridge?.close().catch(() => {
175362
- });
175750
+ const bridge = this.bridge;
175363
175751
  this.bridge = void 0;
175364
- unregisterAllRequestors();
175752
+ await bridge?.close().catch(() => {
175753
+ });
175754
+ if (bridge) unregisterRequestorsByOwner(bridge);
175365
175755
  }
175366
175756
  };
175367
175757
 
@@ -175719,6 +176109,9 @@ var SecurityPlugin = class {
175719
176109
  // coalesced per entity so a hung call cannot pile up work behind it.
175720
176110
  tasks = [];
175721
176111
  draining = false;
176112
+ // Bumped on every teardown and bring-up; queued tasks from an older
176113
+ // generation never dispatch.
176114
+ effectGeneration = 0;
175722
176115
  connection;
175723
176116
  unsubscribeEvents;
175724
176117
  retryTimer;
@@ -175731,6 +176124,21 @@ var SecurityPlugin = class {
175731
176124
  const stored = await context.storage.get(CONFIG_KEY2);
175732
176125
  this.config = { ...this.config, ...stored ?? {} };
175733
176126
  this.applyLists();
176127
+ if (!this.isConfigured()) {
176128
+ this.log.info(
176129
+ "no trigger entities configured, the security devices stay unregistered"
176130
+ );
176131
+ return;
176132
+ }
176133
+ await this.bringUp();
176134
+ }
176135
+ isConfigured() {
176136
+ return this.watched.size > 0;
176137
+ }
176138
+ async bringUp() {
176139
+ const context = this.context;
176140
+ if (!context) return;
176141
+ this.effectGeneration++;
175734
176142
  this.machine = new SecurityStateMachine(
175735
176143
  this.machineConfig(),
175736
176144
  this.effects()
@@ -175753,21 +176161,38 @@ var SecurityPlugin = class {
175753
176161
  this.pushDeviceStates();
175754
176162
  this.startConnection();
175755
176163
  }
176164
+ async tearDown() {
176165
+ this.machine?.shutdown();
176166
+ this.machine = void 0;
176167
+ this.tasks = [];
176168
+ this.effectGeneration++;
176169
+ await this.stopConnection();
176170
+ for (const id of [...Object.values(MODE_DEVICE_IDS), ALARM_DEVICE_ID]) {
176171
+ await this.context?.unregisterDevice(id).catch(() => {
176172
+ });
176173
+ }
176174
+ }
175756
176175
  async onConfigChanged(config8) {
175757
176176
  this.config = config8;
175758
176177
  await this.context?.storage.set(CONFIG_KEY2, this.config);
175759
176178
  this.applyLists();
175760
- this.machine?.setConfig(this.machineConfig());
176179
+ if (!this.isConfigured()) {
176180
+ if (this.machine) {
176181
+ this.log.info("trigger lists emptied, removing the security devices");
176182
+ await this.tearDown();
176183
+ }
176184
+ return;
176185
+ }
176186
+ if (!this.machine) {
176187
+ await this.bringUp();
176188
+ return;
176189
+ }
176190
+ this.machine.setConfig(this.machineConfig());
175761
176191
  await this.stopConnection();
175762
176192
  this.startConnection();
175763
176193
  }
175764
176194
  async onShutdown() {
175765
- this.machine?.shutdown();
175766
- await this.stopConnection();
175767
- for (const id of [...Object.values(MODE_DEVICE_IDS), ALARM_DEVICE_ID]) {
175768
- await this.context?.unregisterDevice(id).catch(() => {
175769
- });
175770
- }
176195
+ await this.tearDown();
175771
176196
  }
175772
176197
  getCurrentConfig() {
175773
176198
  return { ...this.config };
@@ -176005,6 +176430,7 @@ var SecurityPlugin = class {
176005
176430
  });
176006
176431
  }
176007
176432
  pushTask(task) {
176433
+ task.gen = this.effectGeneration;
176008
176434
  this.tasks.push(task);
176009
176435
  queueMicrotask(() => void this.drain());
176010
176436
  }
@@ -176052,6 +176478,7 @@ var SecurityPlugin = class {
176052
176478
  for (; ; ) {
176053
176479
  const task = this.tasks.shift();
176054
176480
  if (!task) return;
176481
+ if (task.gen !== this.effectGeneration) continue;
176055
176482
  try {
176056
176483
  await this.runTask(task);
176057
176484
  } catch (e) {
@@ -176677,7 +177104,7 @@ var BridgeEndpointManager = class extends Service {
176677
177104
  );
176678
177105
  }
176679
177106
  };
176680
- this.pluginManager.onDeviceUnregistered = async (pluginName, deviceId) => {
177107
+ this.pluginManager.onDeviceUnregistered = async (pluginName, deviceId, options) => {
176681
177108
  const listeners = this.pluginListeners.get(deviceId);
176682
177109
  if (listeners) {
176683
177110
  for (const { observable, listener } of listeners) {
@@ -176691,7 +177118,11 @@ var BridgeEndpointManager = class extends Service {
176691
177118
  const endpoint = this.pluginEndpoints.get(deviceId);
176692
177119
  if (endpoint) {
176693
177120
  try {
176694
- await endpoint.delete();
177121
+ if (options?.keepIdentity) {
177122
+ await endpoint.close();
177123
+ } else {
177124
+ await endpoint.delete();
177125
+ }
176695
177126
  } catch (e) {
176696
177127
  this.log.warn(
176697
177128
  `Plugin "${pluginName}": failed to remove device "${deviceId}":`,
@@ -176827,11 +177258,11 @@ var BridgeEndpointManager = class extends Service {
176827
177258
  circuitBreakers
176828
177259
  };
176829
177260
  }
176830
- enablePlugin(pluginName) {
176831
- this.pluginManager?.enablePlugin(pluginName);
177261
+ async enablePlugin(pluginName) {
177262
+ return await this.pluginManager?.enablePlugin(pluginName);
176832
177263
  }
176833
- disablePlugin(pluginName) {
176834
- this.pluginManager?.disablePlugin(pluginName);
177264
+ async disablePlugin(pluginName) {
177265
+ return await this.pluginManager?.disablePlugin(pluginName);
176835
177266
  }
176836
177267
  resetPlugin(pluginName) {
176837
177268
  this.pluginManager?.resetPlugin(pluginName);
@@ -176849,8 +177280,8 @@ var BridgeEndpointManager = class extends Service {
176849
177280
  async isolateEntity(entityName) {
176850
177281
  const endpoints = this.root.parts.map((p) => p);
176851
177282
  const endpoint = endpoints.find(
176852
- (e) => e.id === entityName || e.entityId === entityName
176853
- );
177283
+ (e) => !(e instanceof VacuumAreaSwitchEndpoint) && (e.id === entityName || e.entityId === entityName)
177284
+ ) ?? endpoints.find((e) => e.id === entityName);
176854
177285
  if (endpoint) {
176855
177286
  this.log.warn(
176856
177287
  `Isolating entity ${endpoint.entityId} due to runtime error`
@@ -176862,6 +177293,17 @@ var BridgeEndpointManager = class extends Service {
176862
177293
  }
176863
177294
  this.pendingRemovals.delete(endpoint.entityId);
176864
177295
  this.mappingFingerprints.delete(endpoint.entityId);
177296
+ if (!(endpoint instanceof VacuumAreaSwitchEndpoint)) {
177297
+ for (const sw of endpoints) {
177298
+ if (!(sw instanceof VacuumAreaSwitchEndpoint)) continue;
177299
+ if (sw.vacuumEndpointId !== endpoint.id) continue;
177300
+ try {
177301
+ await sw.delete();
177302
+ } catch (e) {
177303
+ this.log.warn(`Failed to remove area switch ${sw.id}:`, e);
177304
+ }
177305
+ }
177306
+ }
176865
177307
  }
176866
177308
  }
176867
177309
  // refreshDevices only runs on registry-fingerprint changes, which may not
@@ -176947,7 +177389,7 @@ var BridgeEndpointManager = class extends Service {
176947
177389
  async refreshDevices() {
176948
177390
  this.registry.refresh();
176949
177391
  this._failedEntities = [];
176950
- const endpoints = this.root.parts.map((p) => p);
177392
+ const endpoints = this.root.parts.map((p) => p).filter((p) => !(p instanceof VacuumAreaSwitchEndpoint));
176951
177393
  this.entityIds = this.registry.entityIds;
176952
177394
  if (this.registry.isAutoComposedDevicesEnabled()) {
176953
177395
  for (const eid of this.entityIds) {
@@ -177010,6 +177452,19 @@ var BridgeEndpointManager = class extends Service {
177010
177452
  this.bridgeId,
177011
177453
  buildPresentEntityIds(fullEntities)
177012
177454
  );
177455
+ for (const part of [...this.root.parts]) {
177456
+ if (!(part instanceof VacuumAreaSwitchEndpoint)) continue;
177457
+ const claimant = endpointIdToEntity.get(part.id);
177458
+ if (claimant == null) continue;
177459
+ this.log.info(
177460
+ `Area switch ${part.id} collides with entity ${claimant}, removing the switch`
177461
+ );
177462
+ try {
177463
+ await part.delete();
177464
+ } catch (e) {
177465
+ this.log.warn(`Failed to remove colliding area switch ${part.id}:`, e);
177466
+ }
177467
+ }
177013
177468
  const existingEndpoints = [];
177014
177469
  const now = Date.now();
177015
177470
  for (const endpoint of endpoints) {
@@ -177169,10 +177624,117 @@ var BridgeEndpointManager = class extends Service {
177169
177624
  }
177170
177625
  }
177171
177626
  }
177627
+ await this.reconcileAreaSwitches();
177172
177628
  if (this.unsubscribe) {
177173
177629
  this.startObserving();
177174
177630
  }
177175
177631
  }
177632
+ // Opt-in per-area room switches (#355). One momentary OnOffPlugInUnit sibling
177633
+ // per configured service area, mounted alongside its vacuum with a stable
177634
+ // derived id. Areas and mapping come from the parent's vacuumEffective, the
177635
+ // exact config its ServiceArea cluster was built from, never from raw storage
177636
+ // (raw can be a different id space: injected Valetudo/Roborock rooms,
177637
+ // auto-resolved CLEAN_AREA). The vacuum's own endpoint is never touched here.
177638
+ // Switches are kept while their parent endpoint survives with the flag on
177639
+ // (the parent's removal grace is mirrored for free), rebuilt via close() when
177640
+ // the parent was recreated for a mapping change so numbers survive, and
177641
+ // deleted when the flag goes off, the area is gone, or the parent is gone.
177642
+ async reconcileAreaSwitches() {
177643
+ const parts = this.root.parts.map((p) => p);
177644
+ const switches = parts.filter(
177645
+ (p) => p instanceof VacuumAreaSwitchEndpoint
177646
+ );
177647
+ const vacuumById = /* @__PURE__ */ new Map();
177648
+ for (const part of parts) {
177649
+ if (part instanceof VacuumAreaSwitchEndpoint) continue;
177650
+ vacuumById.set(part.id, part);
177651
+ }
177652
+ const isolated = new Set(
177653
+ EntityIsolationService.getIsolatedEntities(this.bridgeId).map(
177654
+ (f) => f.entityId
177655
+ )
177656
+ );
177657
+ const parentIsolated = (parent) => isolated.has(parent.id) || parent.entityId != null && isolated.has(parent.entityId);
177658
+ const kept = /* @__PURE__ */ new Set();
177659
+ for (const sw of switches) {
177660
+ const parent = vacuumById.get(sw.vacuumEndpointId);
177661
+ const mapping = parent ? this.getEntityMapping(parent.entityId) : void 0;
177662
+ const effective = parent instanceof LegacyEndpoint ? parent.vacuumEffective : void 0;
177663
+ let keep = false;
177664
+ let rebuild = false;
177665
+ if (parent && !parentIsolated(parent) && mapping?.vacuumRoomSwitches && effective) {
177666
+ const areas = getVacuumServiceAreas(
177667
+ effective.state.attributes,
177668
+ effective.mapping
177669
+ );
177670
+ if (areas.some((a) => a.areaId === sw.areaId)) {
177671
+ if (sw.parentEffective === effective) {
177672
+ keep = true;
177673
+ } else {
177674
+ rebuild = true;
177675
+ }
177676
+ }
177677
+ }
177678
+ if (keep) {
177679
+ kept.add(sw.id);
177680
+ continue;
177681
+ }
177682
+ try {
177683
+ if (rebuild) {
177684
+ await sw.close();
177685
+ } else {
177686
+ await sw.delete();
177687
+ }
177688
+ } catch (e) {
177689
+ this.log.warn(`Failed to remove area switch ${sw.id}:`, e);
177690
+ }
177691
+ }
177692
+ for (const part of vacuumById.values()) {
177693
+ const entityId = part.entityId;
177694
+ if (!entityId?.startsWith("vacuum.")) continue;
177695
+ if (parentIsolated(part)) continue;
177696
+ const mapping = this.getEntityMapping(entityId);
177697
+ if (!mapping?.vacuumRoomSwitches) continue;
177698
+ const effective = part instanceof LegacyEndpoint ? part.vacuumEffective : void 0;
177699
+ if (!effective) continue;
177700
+ const areas = getVacuumServiceAreas(
177701
+ effective.state.attributes,
177702
+ effective.mapping
177703
+ );
177704
+ const entity = {
177705
+ entity_id: entityId,
177706
+ state: effective.state,
177707
+ registry: this.registry.entity(entityId),
177708
+ deviceRegistry: this.registry.deviceOf(entityId)
177709
+ };
177710
+ for (const area of areas) {
177711
+ const switchId = `${part.id}_roomsw_${area.areaId}`;
177712
+ if (kept.has(switchId)) continue;
177713
+ const holder = vacuumById.get(switchId);
177714
+ if (holder) {
177715
+ this.log.warn(
177716
+ `Skipping area switch ${switchId} for ${entityId}: id taken by entity ${holder.entityId}`
177717
+ );
177718
+ continue;
177719
+ }
177720
+ try {
177721
+ const endpoint = VacuumAreaSwitchEndpoint.create({
177722
+ vacuumEndpointId: part.id,
177723
+ entity,
177724
+ mapping: effective.mapping,
177725
+ area,
177726
+ parentEffective: effective
177727
+ });
177728
+ await this.root.add(endpoint);
177729
+ } catch (e) {
177730
+ this.log.warn(
177731
+ `Failed to add area switch ${switchId} for ${entityId}:`,
177732
+ e
177733
+ );
177734
+ }
177735
+ }
177736
+ }
177737
+ }
177176
177738
  updateInFlight;
177177
177739
  pendingStates;
177178
177740
  pendingChanged;
@@ -180453,9 +181015,9 @@ function startCommand(webDist) {
180453
181015
  }
180454
181016
 
180455
181017
  // src/cli.ts
180456
- var dirname7 = import.meta.dirname ?? url.fileURLToPath(new URL(".", import.meta.url));
181018
+ var dirname8 = import.meta.dirname ?? url.fileURLToPath(new URL(".", import.meta.url));
180457
181019
  async function cli(argv) {
180458
- const webDist = process.env.NODE_ENV === "development" ? void 0 : path13.join(dirname7, "../frontend");
181020
+ const webDist = process.env.NODE_ENV === "development" ? void 0 : path13.join(dirname8, "../frontend");
180459
181021
  const cli2 = yargs(hideBin(argv));
180460
181022
  cli2.scriptName("home-assistant-matter-hub").version().strict().recommendCommands().detectLocale(false).help().command(startCommand(webDist)).demandCommand().wrap(Math.min(140, cli2.terminalWidth())).parse();
180461
181023
  }