@riddix/hamh 2.0.29 → 2.0.30

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.29 | Production-ready, recommended for most users |
40
+ | **Stable** | `main` | v2.0.30 | 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,36 +52,30 @@ of port forwarding etc.
52
52
  ## 🎉 What's New
53
53
 
54
54
  <details>
55
- <summary><strong>📦 Stable Features (v2.0.29)</strong> - Click to expand</summary>
55
+ <summary><strong>📦 Stable Features (v2.0.30)</strong> - Click to expand</summary>
56
56
 
57
- **New in v2.0.29:**
57
+ **New in v2.0.30:**
58
58
 
59
59
  | Feature | Description |
60
60
  |---------|-------------|
61
- | **💡 Light currentLevel Fix** | Retain light currentLevel when off to prevent Apple Home 100% brightness on turn-on ([#225](https://github.com/RiDDiX/home-assistant-matter-hub/issues/225)) |
62
- | **🖥️ Bridge Config Save Fix** | Decouple save button from RJSF schema validation errors save works even with optional field warnings ([#232](https://github.com/RiDDiX/home-assistant-matter-hub/issues/232)) |
63
- | **🌀 Fan Device Feature Fix** | Correct FanDeviceFeature TURN_ON/TURN_OFF enum values to match Home Assistant |
64
- | **🌡️ Humidity Auto-Mapping Fix** | Correct autoHumidityMapping schema default to match runtime behavior |
61
+ | **� Mapped Entity Propagation Fix** | Propagate mapped entity changes (battery, humidity, etc.) to Matter endpoints fixes stale sensor readings |
62
+ | **🖥️ API Error Surfacing** | Surface API errors instead of silently swallowing them ([#232](https://github.com/RiDDiX/home-assistant-matter-hub/issues/232)) |
65
63
 
66
- **Previously in v2.0.28:**
64
+ **Previously in v2.0.29:**
67
65
 
68
66
  | Feature | Description |
69
67
  |---------|-------------|
70
- | **🖼️ Device Image Support** | Device cards show images from Zigbee2MQTT or custom uploads ([#221](https://github.com/RiDDiX/home-assistant-matter-hub/issues/221)) |
71
- | **🌀 Custom Fan Speed Mapping** | Define custom fan speed levels with named presets ([#226](https://github.com/RiDDiX/home-assistant-matter-hub/pull/226)) |
72
- | **📺 TV Source Selection** | MediaInput cluster added to VideoPlayerDevice for input switching ([#231](https://github.com/RiDDiX/home-assistant-matter-hub/issues/231)) |
73
- | **🔀 Reverse Proxy Base Path** | `--http-base-path` option for subfolder reverse proxy support ([#228](https://github.com/RiDDiX/home-assistant-matter-hub/issues/228)) |
74
- | **🌀 On/Off-Only Fans** | Fans without speed control correctly use OnOffPlugInUnit ([#229](https://github.com/RiDDiX/home-assistant-matter-hub/issues/229)) |
75
- | **💡 Light Brightness Fix** | Prevent brightness reset on turn-on by setting onLevel to null ([#225](https://github.com/RiDDiX/home-assistant-matter-hub/issues/225)) |
76
- | **🌀 Fan Speed Fixes** | speedMax cap raised from 10 to 100 (Matter spec max), retain speed when off ([#225](https://github.com/RiDDiX/home-assistant-matter-hub/issues/225)) |
77
- | **🌡️ Composed Air Purifier Fix** | Flatten to single endpoint for correct Apple Home primary tile ([#218](https://github.com/RiDDiX/home-assistant-matter-hub/issues/218)) |
68
+ | **� Light currentLevel Fix** | Retain light currentLevel when off to prevent Apple Home 100% brightness on turn-on ([#225](https://github.com/RiDDiX/home-assistant-matter-hub/issues/225)) |
69
+ | **�️ Bridge Config Save Fix** | Decouple save button from RJSF schema validation errors ([#232](https://github.com/RiDDiX/home-assistant-matter-hub/issues/232)) |
70
+ | **🌀 Fan Device Feature Fix** | Correct FanDeviceFeature TURN_ON/TURN_OFF enum values to match Home Assistant |
71
+ | **🌡️ Humidity Auto-Mapping Fix** | Correct autoHumidityMapping schema default to match runtime behavior |
78
72
 
79
73
  </details>
80
74
 
81
75
  <details>
82
76
  <summary><strong>🧪 Alpha Features (v2.1.0-alpha.x)</strong> - Click to expand</summary>
83
77
 
84
- **Alpha is currently in sync with Stable (v2.0.29).** All alpha features have been promoted to stable. New alpha features will appear here as development continues.
78
+ **Alpha is currently in sync with Stable (v2.0.30).** All alpha features have been promoted to stable. New alpha features will appear here as development continues.
85
79
 
86
80
  </details>
87
81
 
@@ -107,6 +101,9 @@ of port forwarding etc.
107
101
  <details>
108
102
  <summary><strong>📜 Previous Stable Versions</strong> - Click to expand</summary>
109
103
 
104
+ ### v2.0.29
105
+ Light currentLevel Fix, Bridge Config Save Fix, Fan Device Feature Fix, Humidity Auto-Mapping Fix
106
+
110
107
  ### v2.0.28
111
108
  Device Image Support, Custom Fan Speed Mapping, TV Source Selection, Reverse Proxy Base Path, On/Off-Only Fans, Light Brightness Fix, Fan Speed Fixes, Composed Air Purifier Fix, Dreame Multi-Floor Fix, Optimistic State Updates, Frontend Improvements
112
109
 
@@ -445,6 +442,7 @@ Thank you to everyone who helps improve this project by reporting issues!
445
442
  | [@JRCondat](https://github.com/JRCondat) | 💎 Thank you for your generous support! |
446
443
  | Bonjon | 💎 Thank you for your generous support! |
447
444
  | TobiR | 💎 Thank you for your generous support! |
445
+ | [@d3rby91](https://github.com/d3rby91) | 💎 Thank you for your generous support! |
448
446
  | *Anonymous supporters* | 🙏 Thank you to those who prefer not to be named - your support is equally appreciated! |
449
447
 
450
448
  ### 🌟 Original Author
@@ -149030,7 +149030,8 @@ function matterApi(bridgeService, haRegistry) {
149030
149030
  const body = req.body;
149031
149031
  const isValid = ajv.validate(createBridgeRequestSchema, body);
149032
149032
  if (!isValid) {
149033
- res.status(400).json(ajv.errors);
149033
+ const details = ajv.errors?.map((e) => `${e.instancePath || "/"}: ${e.message}`).join("; ") ?? "Unknown";
149034
+ res.status(400).json({ error: `Validation failed: ${details}` });
149034
149035
  } else {
149035
149036
  try {
149036
149037
  const bridge = await bridgeService.create(body);
@@ -149071,7 +149072,8 @@ function matterApi(bridgeService, haRegistry) {
149071
149072
  const body = req.body;
149072
149073
  const isValid = ajv.validate(updateBridgeRequestSchema, body);
149073
149074
  if (!isValid) {
149074
- res.status(400).json(ajv.errors);
149075
+ const details = ajv.errors?.map((e) => `${e.instancePath || "/"}: ${e.message}`).join("; ") ?? "Unknown";
149076
+ res.status(400).json({ error: `Validation failed: ${details}` });
149075
149077
  } else if (bridgeId !== body.id) {
149076
149078
  res.status(400).send("Path variable `bridgeId` does not match `body.id`");
149077
149079
  } else {
@@ -164463,15 +164465,45 @@ import debounce4 from "debounce";
164463
164465
  // src/matter/endpoints/entity-endpoint.ts
164464
164466
  init_esm7();
164465
164467
  var EntityEndpoint = class extends Endpoint {
164466
- constructor(type, entityId, customName) {
164468
+ constructor(type, entityId, customName, mappedEntityIds) {
164467
164469
  super(type, { id: createEndpointId(entityId, customName) });
164468
164470
  this.entityId = entityId;
164471
+ this.mappedEntityIds = mappedEntityIds ?? [];
164472
+ }
164473
+ mappedEntityIds;
164474
+ lastMappedStates = {};
164475
+ hasMappedEntityChanged(states) {
164476
+ let changed = false;
164477
+ for (const mappedId of this.mappedEntityIds) {
164478
+ const mappedState = states[mappedId];
164479
+ if (!mappedState) continue;
164480
+ const fp = mappedState.state;
164481
+ if (fp !== this.lastMappedStates[mappedId]) {
164482
+ this.lastMappedStates[mappedId] = fp;
164483
+ changed = true;
164484
+ }
164485
+ }
164486
+ return changed;
164469
164487
  }
164470
164488
  };
164471
164489
  function createEndpointId(entityId, customName) {
164472
164490
  const baseName = customName || entityId;
164473
164491
  return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
164474
164492
  }
164493
+ function getMappedEntityIds(mapping) {
164494
+ if (!mapping) return [];
164495
+ const ids = [];
164496
+ if (mapping.batteryEntity) ids.push(mapping.batteryEntity);
164497
+ if (mapping.humidityEntity) ids.push(mapping.humidityEntity);
164498
+ if (mapping.pressureEntity) ids.push(mapping.pressureEntity);
164499
+ if (mapping.cleaningModeEntity) ids.push(mapping.cleaningModeEntity);
164500
+ if (mapping.suctionLevelEntity) ids.push(mapping.suctionLevelEntity);
164501
+ if (mapping.mopIntensityEntity) ids.push(mapping.mopIntensityEntity);
164502
+ if (mapping.filterLifeEntity) ids.push(mapping.filterLifeEntity);
164503
+ if (mapping.powerEntity) ids.push(mapping.powerEntity);
164504
+ if (mapping.energyEntity) ids.push(mapping.energyEntity);
164505
+ return ids;
164506
+ }
164475
164507
 
164476
164508
  // src/matter/endpoints/composed/composed-air-purifier-endpoint.ts
164477
164509
  init_dist();
@@ -174790,10 +174822,11 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174790
174822
  return;
174791
174823
  }
174792
174824
  const customName = effectiveMapping?.customName;
174793
- return new _LegacyEndpoint(type, entityId, customName);
174825
+ const mappedIds = getMappedEntityIds(effectiveMapping);
174826
+ return new _LegacyEndpoint(type, entityId, customName, mappedIds);
174794
174827
  }
174795
- constructor(type, entityId, customName) {
174796
- super(type, entityId, customName);
174828
+ constructor(type, entityId, customName, mappedEntityIds) {
174829
+ super(type, entityId, customName, mappedEntityIds);
174797
174830
  this.flushUpdate = debounce4(this.flushPendingUpdate.bind(this), 50);
174798
174831
  }
174799
174832
  lastState;
@@ -174804,9 +174837,15 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174804
174837
  }
174805
174838
  async updateStates(states) {
174806
174839
  const state = states[this.entityId] ?? {};
174807
- if (state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
174840
+ const mappedChanged = this.hasMappedEntityChanged(states);
174841
+ if (!mappedChanged && state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
174808
174842
  return;
174809
174843
  }
174844
+ if (mappedChanged) {
174845
+ logger189.debug(
174846
+ `Mapped entity change detected for ${this.entityId}, forcing update`
174847
+ );
174848
+ }
174810
174849
  logger189.debug(
174811
174850
  `State update received for ${this.entityId}: state=${state.state}`
174812
174851
  );
@@ -175105,12 +175144,26 @@ var BridgeEndpointManager = class extends Service {
175105
175144
  if (!this.entityIds.length) {
175106
175145
  return;
175107
175146
  }
175147
+ const subscriptionIds = this.collectSubscriptionEntityIds();
175108
175148
  this.unsubscribe = subscribeEntities(
175109
175149
  this.client.connection,
175110
175150
  (e) => this.updateStates(e),
175111
- this.entityIds
175151
+ subscriptionIds
175112
175152
  );
175113
175153
  }
175154
+ collectSubscriptionEntityIds() {
175155
+ const ids = new Set(this.entityIds);
175156
+ const endpoints = this.root.parts.map((p) => p);
175157
+ for (const endpoint of endpoints) {
175158
+ const mappedIds = endpoint.mappedEntityIds;
175159
+ if (mappedIds) {
175160
+ for (const mappedId of mappedIds) {
175161
+ ids.add(mappedId);
175162
+ }
175163
+ }
175164
+ }
175165
+ return [...ids];
175166
+ }
175114
175167
  stopObserving() {
175115
175168
  this.unsubscribe?.();
175116
175169
  this.unsubscribe = void 0;
@@ -175235,6 +175288,7 @@ var BridgeEndpointManager = class extends Service {
175235
175288
  }
175236
175289
  }
175237
175290
  async updateStates(states) {
175291
+ this.registry.mergeExternalStates(states);
175238
175292
  const endpoints = this.root.parts.map((p) => p);
175239
175293
  const results = await Promise.allSettled(
175240
175294
  endpoints.map((endpoint) => endpoint.updateStates(states))
@@ -175705,6 +175759,12 @@ var BridgeRegistry = class _BridgeRegistry {
175705
175759
  isEnergyEntityUsed(entityId) {
175706
175760
  return this._usedEnergyEntities.has(entityId);
175707
175761
  }
175762
+ mergeExternalStates(states) {
175763
+ const registryStates = this.registry.states;
175764
+ for (const entityId of Object.keys(states)) {
175765
+ registryStates[entityId] = states[entityId];
175766
+ }
175767
+ }
175708
175768
  /**
175709
175769
  * Get the area name for an entity, resolving from HA area registry.
175710
175770
  * Priority: entity area_id > device area_id > undefined
@@ -176401,12 +176461,18 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176401
176461
  if (!endpointType) {
176402
176462
  return void 0;
176403
176463
  }
176404
- return new _ServerModeVacuumEndpoint(endpointType, entityId, customName);
176464
+ const mappedIds = getMappedEntityIds(effectiveMapping);
176465
+ return new _ServerModeVacuumEndpoint(
176466
+ endpointType,
176467
+ entityId,
176468
+ customName,
176469
+ mappedIds
176470
+ );
176405
176471
  }
176406
176472
  lastState;
176407
176473
  flushUpdate;
176408
- constructor(type, entityId, customName) {
176409
- super(type, entityId, customName);
176474
+ constructor(type, entityId, customName, mappedEntityIds) {
176475
+ super(type, entityId, customName, mappedEntityIds);
176410
176476
  this.flushUpdate = debounce5(this.flushPendingUpdate.bind(this), 50);
176411
176477
  }
176412
176478
  async delete() {
@@ -176415,9 +176481,15 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176415
176481
  }
176416
176482
  async updateStates(states) {
176417
176483
  const state = states[this.entityId] ?? {};
176418
- if (state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
176484
+ const mappedChanged = this.hasMappedEntityChanged(states);
176485
+ if (!mappedChanged && state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
176419
176486
  return;
176420
176487
  }
176488
+ if (mappedChanged) {
176489
+ logger192.debug(
176490
+ `Mapped entity change detected for ${this.entityId}, forcing update`
176491
+ );
176492
+ }
176421
176493
  logger192.debug(
176422
176494
  `State update received for ${this.entityId}: state=${state.state}`
176423
176495
  );
@@ -176498,12 +176570,25 @@ var ServerModeEndpointManager = class extends Service {
176498
176570
  if (!this.entityIds.length) {
176499
176571
  return;
176500
176572
  }
176573
+ const subscriptionIds = this.collectSubscriptionEntityIds();
176501
176574
  this.unsubscribe = subscribeEntities(
176502
176575
  this.client.connection,
176503
176576
  (e) => this.updateStates(e),
176504
- this.entityIds
176577
+ subscriptionIds
176505
176578
  );
176506
176579
  }
176580
+ collectSubscriptionEntityIds() {
176581
+ const ids = new Set(this.entityIds);
176582
+ if (this.deviceEndpoint) {
176583
+ const mappedIds = this.deviceEndpoint.mappedEntityIds;
176584
+ if (mappedIds) {
176585
+ for (const mappedId of mappedIds) {
176586
+ ids.add(mappedId);
176587
+ }
176588
+ }
176589
+ }
176590
+ return [...ids];
176591
+ }
176507
176592
  stopObserving() {
176508
176593
  this.unsubscribe?.();
176509
176594
  this.unsubscribe = void 0;
@@ -176616,6 +176701,7 @@ var ServerModeEndpointManager = class extends Service {
176616
176701
  }
176617
176702
  }
176618
176703
  async updateStates(states) {
176704
+ this.registry.mergeExternalStates(states);
176619
176705
  if (this.deviceEndpoint) {
176620
176706
  try {
176621
176707
  await this.deviceEndpoint.updateStates(states);