@riddix/hamh 2.1.0-alpha.647 → 2.1.0-alpha.649

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,7 +37,7 @@ of port forwarding etc.
37
37
 
38
38
  | Channel | Branch | Current Version | Description |
39
39
  |---------|--------|-----------------|-------------|
40
- | **Stable** | `main` | v2.0.42 | Production-ready, recommended for most users |
40
+ | **Stable** | `main` | v2.0.43 | Production-ready, recommended for most users |
41
41
  | **Alpha** | `alpha` | v2.1.0-alpha.x | Pre-release with new features, for early adopters |
42
42
  | **Testing** | `testing` | v4.1.0-testing.x | ⚠️ **Highly unstable!** Experimental features, may break |
43
43
 
@@ -52,9 +52,23 @@ of port forwarding etc.
52
52
  ## 🎉 What's New
53
53
 
54
54
  <details>
55
- <summary><strong>📦 Stable Features (v2.0.42)</strong> - Click to expand</summary>
55
+ <summary><strong>📦 Stable Features (v2.0.43)</strong> - Click to expand</summary>
56
56
 
57
- **New in v2.0.42 (hotfix release):**
57
+ **New in v2.0.43:**
58
+
59
+ - 🤖 Vacuum `currentArea` updates when cleaning is started outside HAMH ([#281](https://github.com/RiDDiX/home-assistant-matter-hub/issues/281))
60
+ - 📡 Sensor reactors mark themselves offline when HA disconnects, so updates reach controllers on reconnect ([#327](https://github.com/RiDDiX/home-assistant-matter-hub/issues/327))
61
+ - 🪟 Lift+tilt window coverings pick a valid Matter Type ([#323](https://github.com/RiDDiX/home-assistant-matter-hub/issues/323))
62
+ - 🪟 Cover `device_class=window` maps to Rollershade ([#312](https://github.com/RiDDiX/home-assistant-matter-hub/issues/312))
63
+ - 🧹 UWANT and Xiaomi sweep/mop labels recognised, mop usage routed via `mode.vacuum_mop` ([#322](https://github.com/RiDDiX/home-assistant-matter-hub/issues/322))
64
+ - 🤖 Vacuum identify falls back to a sibling identify button when `vacuum.locate` is unsupported ([#320](https://github.com/RiDDiX/home-assistant-matter-hub/issues/320))
65
+ - ❄️ HA-auto AC `systemMode` stays put when `hvac_action` is idle, ha-auto-only ACs no longer expose Matter Auto ([#309](https://github.com/RiDDiX/home-assistant-matter-hub/issues/309))
66
+ - 🌡️ Climate setpoints snap to the entity `target_temp_step` ([#321](https://github.com/RiDDiX/home-assistant-matter-hub/issues/321))
67
+ - 🛰️ matter.js controller traffic captured in `/api/logs`
68
+ - 🇯🇵 Japanese translation by [@kimera257](https://github.com/kimera257) ([#325](https://github.com/RiDDiX/home-assistant-matter-hub/pull/325))
69
+ - 📝 Docs note for the iPhone-only stuck-on-updating vacuum workaround ([#287](https://github.com/RiDDiX/home-assistant-matter-hub/issues/287))
70
+
71
+ **Previously in v2.0.42 (hotfix release):**
58
72
 
59
73
  - 🇯🇵 Aqara bridge registration no longer stalls, root `softwareVersionString` now matches the numeric `softwareVersion` ([#316](https://github.com/RiDDiX/home-assistant-matter-hub/issues/316))
60
74
  - ❄️ Climate `auto` mode is clamped to `heat`/`cool` on devices without an `AutoMode` base ([#319](https://github.com/RiDDiX/home-assistant-matter-hub/issues/319))
@@ -134,7 +148,7 @@ of port forwarding etc.
134
148
  <details>
135
149
  <summary><strong>🧪 Alpha Features (v2.1.0-alpha.x)</strong> - Click to expand</summary>
136
150
 
137
- **Alpha is currently level with Stable (v2.0.42).** All alpha work through `v2.1.0-alpha.633` has been promoted into v2.0.42. New alpha work continues from `v2.1.0-alpha.634` onward and will appear here as development progresses.
151
+ **Alpha is currently level with Stable (v2.0.43).** All alpha work up to the latest pre-release has been promoted into v2.0.43. New alpha work continues from the next pre-release tag onward and will appear here as development progresses.
138
152
 
139
153
  </details>
140
154
 
@@ -146813,6 +146813,12 @@ var init_bridge_config_schema = __esm({
146813
146813
  description: "Workaround for Alexa resetting light brightness to 100% after subscription renewal. When enabled, the bridge ignores brightness commands that set lights to 100% within 200ms of a turn-on command for the same light. WARNING: breaks Apple Home's 'set room to 100%' Siri commands, which use the same on() + moveToLevel(254) pattern. Only enable on Alexa-only bridges.",
146814
146814
  type: "boolean",
146815
146815
  default: false
146816
+ },
146817
+ useHaRegistrySerial: {
146818
+ title: "Use HA Registry Serial Number",
146819
+ description: "Fall back to the Home Assistant device registry serial_number when no per-entity customSerialNumber is configured. Default off because changing serialNumber after commissioning can confuse controllers. A per-entity customSerialNumber still takes precedence.",
146820
+ type: "boolean",
146821
+ default: false
146816
146822
  }
146817
146823
  }
146818
146824
  };
@@ -148842,6 +148848,7 @@ function entityMappingApi(mappingStorage) {
148842
148848
  customProductName: body.customProductName,
148843
148849
  customVendorName: body.customVendorName,
148844
148850
  customSerialNumber: body.customSerialNumber,
148851
+ customVendorId: body.customVendorId,
148845
148852
  disabled: body.disabled,
148846
148853
  filterLifeEntity: body.filterLifeEntity,
148847
148854
  cleaningModeEntity: body.cleaningModeEntity,
@@ -153329,6 +153336,7 @@ var EntityMappingStorage = class extends Service {
153329
153336
  customProductName: request.customProductName?.trim() || void 0,
153330
153337
  customVendorName: request.customVendorName?.trim() || void 0,
153331
153338
  customSerialNumber: request.customSerialNumber?.trim() || void 0,
153339
+ customVendorId: sanitizeVendorId(request.customVendorId),
153332
153340
  disabled: request.disabled,
153333
153341
  filterLifeEntity: request.filterLifeEntity?.trim() || void 0,
153334
153342
  cleaningModeEntity: request.cleaningModeEntity?.trim() || void 0,
@@ -153353,7 +153361,7 @@ var EntityMappingStorage = class extends Service {
153353
153361
  disableClimateFanControl: request.disableClimateFanControl || void 0,
153354
153362
  composedEntities: request.composedEntities?.filter((e) => e.entityId?.trim()) ?? void 0
153355
153363
  };
153356
- if (!config10.matterDeviceType && !config10.customName && !config10.customProductName && !config10.customVendorName && !config10.customSerialNumber && config10.disabled !== true && !config10.filterLifeEntity && !config10.cleaningModeEntity && !config10.temperatureEntity && !config10.humidityEntity && !config10.batteryEntity && !config10.roomEntities && !config10.disableLockPin && !config10.powerEntity && !config10.energyEntity && !config10.pressureEntity && !config10.suctionLevelEntity && !config10.mopIntensityEntity && (!config10.customServiceAreas || config10.customServiceAreas.length === 0) && (!config10.customFanSpeedTags || Object.keys(config10.customFanSpeedTags).length === 0) && !config10.currentRoomEntity && !config10.valetudoIdentifier && !config10.coverSwapOpenClose && !config10.disableClimateOnOff && !config10.disableClimateFanControl && (!config10.composedEntities || config10.composedEntities.length === 0)) {
153364
+ if (!config10.matterDeviceType && !config10.customName && !config10.customProductName && !config10.customVendorName && !config10.customSerialNumber && config10.customVendorId === void 0 && config10.disabled !== true && !config10.filterLifeEntity && !config10.cleaningModeEntity && !config10.temperatureEntity && !config10.humidityEntity && !config10.batteryEntity && !config10.roomEntities && !config10.disableLockPin && !config10.powerEntity && !config10.energyEntity && !config10.pressureEntity && !config10.suctionLevelEntity && !config10.mopIntensityEntity && (!config10.customServiceAreas || config10.customServiceAreas.length === 0) && (!config10.customFanSpeedTags || Object.keys(config10.customFanSpeedTags).length === 0) && !config10.currentRoomEntity && !config10.valetudoIdentifier && !config10.coverSwapOpenClose && !config10.disableClimateOnOff && !config10.disableClimateFanControl && (!config10.composedEntities || config10.composedEntities.length === 0)) {
153357
153365
  bridgeMap.delete(request.entityId);
153358
153366
  } else {
153359
153367
  bridgeMap.set(request.entityId, config10);
@@ -153373,6 +153381,16 @@ var EntityMappingStorage = class extends Service {
153373
153381
  await this.persist();
153374
153382
  }
153375
153383
  };
153384
+ function sanitizeVendorId(value) {
153385
+ if (value === void 0 || value === null || value === "") {
153386
+ return void 0;
153387
+ }
153388
+ const n = typeof value === "string" ? Number(value) : value;
153389
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 65534) {
153390
+ return void 0;
153391
+ }
153392
+ return n;
153393
+ }
153376
153394
 
153377
153395
  // src/services/storage/lock-credential-storage.ts
153378
153396
  init_service();
@@ -165393,7 +165411,8 @@ var ServerModeServerNode = class extends ServerNode {
165393
165411
  async updateDeviceIdentity(entityId, device, mapping, friendlyName) {
165394
165412
  const nodeLabel = trimToLength(mapping?.customName, 32, "...") ?? trimToLength(friendlyName, 32, "...") ?? trimToLength(entityId, 32, "...");
165395
165413
  const productNameFromNodeLabel = this.featureFlags?.productNameFromNodeLabel === true ? trimToLength(sanitizeMatterString(nodeLabel ?? ""), 32, "...") ?? void 0 : void 0;
165396
- const rawSerial = trimToLength(mapping?.customSerialNumber, 32, "...");
165414
+ const registrySerial = this.featureFlags?.useHaRegistrySerial ? trimToLength(device?.serial_number, 32, "...") : void 0;
165415
+ const rawSerial = trimToLength(mapping?.customSerialNumber, 32, "...") ?? registrySerial;
165397
165416
  const serialNumber = rawSerial && this.serialNumberSuffix ? trimToLength(`${rawSerial}${this.serialNumberSuffix}`, 32, "...") : rawSerial;
165398
165417
  const basicInformation = dropUndefined({
165399
165418
  vendorName: trimToLength(mapping?.customVendorName, 32, "...") ?? trimToLength(device?.manufacturer, 32, "..."),
@@ -168415,10 +168434,13 @@ var BasicInformationServer2 = class extends BridgedDeviceBasicInformationServer
168415
168434
  const nodeLabel = ellipse(32, homeAssistant.state.customName) ?? ellipse(32, registryName) ?? ellipse(32, entity.state?.attributes?.friendly_name) ?? ellipse(32, entity.entity_id);
168416
168435
  const productNameFromNodeLabel = featureFlags?.productNameFromNodeLabel === true ? ellipse(32, sanitizeMatterString(nodeLabel ?? "")) ?? void 0 : void 0;
168417
168436
  const serialNumberSuffix = this.env.get(BridgeDataProvider).serialNumberSuffix;
168418
- const rawSerial = ellipse(32, mapping?.customSerialNumber) ?? hash(32, entity.entity_id);
168437
+ const registrySerial = featureFlags?.useHaRegistrySerial ? ellipse(32, device?.serial_number) : void 0;
168438
+ const rawSerial = ellipse(32, mapping?.customSerialNumber) ?? registrySerial ?? hash(32, entity.entity_id);
168419
168439
  const serialNumber = serialNumberSuffix ? ellipse(32, `${rawSerial}${serialNumberSuffix}`) : rawSerial;
168440
+ const customVendorId = mapping?.customVendorId;
168441
+ const vendorId3 = isValidVendorId(customVendorId) ? customVendorId : basicInformation.vendorId;
168420
168442
  applyPatchState(this.state, {
168421
- vendorId: VendorId(basicInformation.vendorId),
168443
+ vendorId: VendorId(vendorId3),
168422
168444
  vendorName: ellipse(32, mapping?.customVendorName) ?? ellipse(32, device?.manufacturer) ?? hash(32, basicInformation.vendorName),
168423
168445
  productName: ellipse(32, mapping?.customProductName) ?? productNameFromNodeLabel ?? ellipse(32, device?.model_id) ?? ellipse(32, device?.model) ?? hash(32, basicInformation.productName),
168424
168446
  productLabel: ellipse(64, device?.model) ?? hash(64, basicInformation.productLabel),
@@ -168443,6 +168465,9 @@ function hash(maxLength, value) {
168443
168465
  const suffix = crypto6.createHash("md5").update(value ?? "").digest("hex").substring(0, hashLength);
168444
168466
  return trimToLength(value, maxLength, suffix);
168445
168467
  }
168468
+ function isValidVendorId(value) {
168469
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65534;
168470
+ }
168446
168471
 
168447
168472
  // src/matter/behaviors/electrical-energy-measurement-server.ts
168448
168473
  init_esm();
@@ -177002,26 +177027,22 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
177002
177027
  );
177003
177028
  return;
177004
177029
  }
177005
- if (s.activeAreas.length === 0) {
177006
- this.logShortCircuitOnce(
177007
- "no-active-areas",
177008
- `currentRoom sensor: activeAreas empty while cleaning, sensor=${currentRoomEntityId} state="${roomState.state}"`
177009
- );
177010
- return;
177011
- }
177012
177030
  const serviceArea = this.agent.get(ServiceAreaBehavior);
177031
+ const externalSession = s.activeAreas.length === 0;
177032
+ const supportedAreaIds = serviceArea.state.supportedAreas.map(
177033
+ (a) => a.areaId
177034
+ );
177035
+ const isAllowedArea = (id) => externalSession ? supportedAreaIds.includes(id) : s.activeAreas.includes(id);
177013
177036
  const sensorAttrs = roomState.attributes;
177014
177037
  const segmentId = sensorAttrs.segment_id ?? sensorAttrs.room_id;
177015
177038
  const roomName = roomState.state;
177016
177039
  let matchedAreaId = null;
177017
- if (segmentId != null) {
177018
- if (s.activeAreas.includes(segmentId)) {
177019
- matchedAreaId = segmentId;
177020
- }
177040
+ if (segmentId != null && isAllowedArea(segmentId)) {
177041
+ matchedAreaId = segmentId;
177021
177042
  }
177022
177043
  if (matchedAreaId === null && segmentId != null) {
177023
177044
  for (const area of serviceArea.state.supportedAreas) {
177024
- if (s.activeAreas.includes(area.areaId) && area.areaId % 1e4 === segmentId) {
177045
+ if (isAllowedArea(area.areaId) && area.areaId % 1e4 === segmentId) {
177025
177046
  matchedAreaId = area.areaId;
177026
177047
  break;
177027
177048
  }
@@ -177031,7 +177052,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
177031
177052
  const area = serviceArea.state.supportedAreas.find(
177032
177053
  (a) => a.areaInfo.locationInfo?.locationName?.toLowerCase() === roomName.toLowerCase()
177033
177054
  );
177034
- if (area && s.activeAreas.includes(area.areaId)) {
177055
+ if (area && isAllowedArea(area.areaId)) {
177035
177056
  matchedAreaId = area.areaId;
177036
177057
  }
177037
177058
  }