@riddix/hamh 2.1.0-alpha.434 → 2.1.0-alpha.436

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.28 | Production-ready, recommended for most users |
40
+ | **Stable** | `main` | v2.0.29 | 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,18 @@ of port forwarding etc.
52
52
  ## 🎉 What's New
53
53
 
54
54
  <details>
55
- <summary><strong>📦 Stable Features (v2.0.28)</strong> - Click to expand</summary>
55
+ <summary><strong>📦 Stable Features (v2.0.29)</strong> - Click to expand</summary>
56
56
 
57
- **New in v2.0.28:**
57
+ **New in v2.0.29:**
58
+
59
+ | Feature | Description |
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 |
65
+
66
+ **Previously in v2.0.28:**
58
67
 
59
68
  | Feature | Description |
60
69
  |---------|-------------|
@@ -66,30 +75,13 @@ of port forwarding etc.
66
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)) |
67
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)) |
68
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)) |
69
- | **🤖 Dreame Multi-Floor Fix** | Switch floor map before vacuum_clean_segment for multi-floor rooms |
70
- | **🤖 Custom Service Areas Fix** | Register as RvcRunMode room modes for Apple Home zone dispatch |
71
- | **⚡ Optimistic State Updates** | Level and color control commands respond immediately |
72
- | **�️ Frontend Improvements** | Error boundary, 404 page, WCAG contrast, theme-aware UI, HA ingress compatibility |
73
-
74
- **Previously in v2.0.27:**
75
-
76
- | Feature | Description |
77
- |---------|-------------|
78
- | **🤖 Native Valetudo Support** | Auto-detect Valetudo select entities, map segments, use `segment_cleanup` via MQTT for room cleaning ([#205](https://github.com/RiDDiX/home-assistant-matter-hub/issues/205)) |
79
- | **🤖 Custom Service Areas** | Define custom room/zone names for generic zone-based robots without native room support ([#177](https://github.com/RiDDiX/home-assistant-matter-hub/issues/177)) |
80
- | **🚨 Alarm Control Panel** | `alarm_control_panel` entities exposed as Matter ModeSelect — arm/disarm modes available in controllers ([#209](https://github.com/RiDDiX/home-assistant-matter-hub/issues/209)) |
81
- | **🌡️ Composed Air Purifier** | Air purifiers with thermostat/humidity sensors create real Matter Composed Devices (spec 9.4.4) |
82
- | **🏢 Vendor Brand Icons** | 20+ new manufacturer icons (Razer, Roborock, iRobot, Signify, and more) |
83
- | **� linux/arm/v7 Docker** | Added ARM v7 platform for standalone Docker image |
84
- | **🌡️ Thermostat Fixes** | heat_cool-only zones, SmartIR AC conformance fix ([#207](https://github.com/RiDDiX/home-assistant-matter-hub/issues/207), [#28](https://github.com/RiDDiX/home-assistant-matter-hub/issues/28)) |
85
- | **🔧 Air Purifier Fix** | Added Rocking (oscillation) and Wind feature support, removed incorrect Lighting feature |
86
78
 
87
79
  </details>
88
80
 
89
81
  <details>
90
82
  <summary><strong>🧪 Alpha Features (v2.1.0-alpha.x)</strong> - Click to expand</summary>
91
83
 
92
- **Alpha is currently in sync with Stable (v2.0.28).** All alpha features have been promoted to stable. New alpha features will appear here as development continues.
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.
93
85
 
94
86
  </details>
95
87
 
@@ -115,6 +107,9 @@ of port forwarding etc.
115
107
  <details>
116
108
  <summary><strong>📜 Previous Stable Versions</strong> - Click to expand</summary>
117
109
 
110
+ ### v2.0.28
111
+ 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
+
118
113
  ### v2.0.27
119
114
  Valetudo support, Custom Service Areas, ServiceArea Maps, Vacuum Identify/Locate/Charging, Alarm Control Panel, Composed Air Purifier, Dashboard Controls, Vendor Brand Icons, Thermostat fixes, Air Purifier oscillation/wind
120
115
 
@@ -164463,15 +164463,45 @@ import debounce4 from "debounce";
164463
164463
  // src/matter/endpoints/entity-endpoint.ts
164464
164464
  init_esm7();
164465
164465
  var EntityEndpoint = class extends Endpoint {
164466
- constructor(type, entityId, customName) {
164466
+ constructor(type, entityId, customName, mappedEntityIds) {
164467
164467
  super(type, { id: createEndpointId(entityId, customName) });
164468
164468
  this.entityId = entityId;
164469
+ this.mappedEntityIds = mappedEntityIds ?? [];
164470
+ }
164471
+ mappedEntityIds;
164472
+ lastMappedStates = {};
164473
+ hasMappedEntityChanged(states) {
164474
+ let changed = false;
164475
+ for (const mappedId of this.mappedEntityIds) {
164476
+ const mappedState = states[mappedId];
164477
+ if (!mappedState) continue;
164478
+ const fp = mappedState.state;
164479
+ if (fp !== this.lastMappedStates[mappedId]) {
164480
+ this.lastMappedStates[mappedId] = fp;
164481
+ changed = true;
164482
+ }
164483
+ }
164484
+ return changed;
164469
164485
  }
164470
164486
  };
164471
164487
  function createEndpointId(entityId, customName) {
164472
164488
  const baseName = customName || entityId;
164473
164489
  return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
164474
164490
  }
164491
+ function getMappedEntityIds(mapping) {
164492
+ if (!mapping) return [];
164493
+ const ids = [];
164494
+ if (mapping.batteryEntity) ids.push(mapping.batteryEntity);
164495
+ if (mapping.humidityEntity) ids.push(mapping.humidityEntity);
164496
+ if (mapping.pressureEntity) ids.push(mapping.pressureEntity);
164497
+ if (mapping.cleaningModeEntity) ids.push(mapping.cleaningModeEntity);
164498
+ if (mapping.suctionLevelEntity) ids.push(mapping.suctionLevelEntity);
164499
+ if (mapping.mopIntensityEntity) ids.push(mapping.mopIntensityEntity);
164500
+ if (mapping.filterLifeEntity) ids.push(mapping.filterLifeEntity);
164501
+ if (mapping.powerEntity) ids.push(mapping.powerEntity);
164502
+ if (mapping.energyEntity) ids.push(mapping.energyEntity);
164503
+ return ids;
164504
+ }
164475
164505
 
164476
164506
  // src/matter/endpoints/composed/composed-air-purifier-endpoint.ts
164477
164507
  init_dist();
@@ -174790,10 +174820,11 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174790
174820
  return;
174791
174821
  }
174792
174822
  const customName = effectiveMapping?.customName;
174793
- return new _LegacyEndpoint(type, entityId, customName);
174823
+ const mappedIds = getMappedEntityIds(effectiveMapping);
174824
+ return new _LegacyEndpoint(type, entityId, customName, mappedIds);
174794
174825
  }
174795
- constructor(type, entityId, customName) {
174796
- super(type, entityId, customName);
174826
+ constructor(type, entityId, customName, mappedEntityIds) {
174827
+ super(type, entityId, customName, mappedEntityIds);
174797
174828
  this.flushUpdate = debounce4(this.flushPendingUpdate.bind(this), 50);
174798
174829
  }
174799
174830
  lastState;
@@ -174804,9 +174835,15 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174804
174835
  }
174805
174836
  async updateStates(states) {
174806
174837
  const state = states[this.entityId] ?? {};
174807
- if (state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
174838
+ const mappedChanged = this.hasMappedEntityChanged(states);
174839
+ if (!mappedChanged && state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
174808
174840
  return;
174809
174841
  }
174842
+ if (mappedChanged) {
174843
+ logger189.debug(
174844
+ `Mapped entity change detected for ${this.entityId}, forcing update`
174845
+ );
174846
+ }
174810
174847
  logger189.debug(
174811
174848
  `State update received for ${this.entityId}: state=${state.state}`
174812
174849
  );
@@ -175105,12 +175142,26 @@ var BridgeEndpointManager = class extends Service {
175105
175142
  if (!this.entityIds.length) {
175106
175143
  return;
175107
175144
  }
175145
+ const subscriptionIds = this.collectSubscriptionEntityIds();
175108
175146
  this.unsubscribe = subscribeEntities(
175109
175147
  this.client.connection,
175110
175148
  (e) => this.updateStates(e),
175111
- this.entityIds
175149
+ subscriptionIds
175112
175150
  );
175113
175151
  }
175152
+ collectSubscriptionEntityIds() {
175153
+ const ids = new Set(this.entityIds);
175154
+ const endpoints = this.root.parts.map((p) => p);
175155
+ for (const endpoint of endpoints) {
175156
+ const mappedIds = endpoint.mappedEntityIds;
175157
+ if (mappedIds) {
175158
+ for (const mappedId of mappedIds) {
175159
+ ids.add(mappedId);
175160
+ }
175161
+ }
175162
+ }
175163
+ return [...ids];
175164
+ }
175114
175165
  stopObserving() {
175115
175166
  this.unsubscribe?.();
175116
175167
  this.unsubscribe = void 0;
@@ -175235,6 +175286,7 @@ var BridgeEndpointManager = class extends Service {
175235
175286
  }
175236
175287
  }
175237
175288
  async updateStates(states) {
175289
+ this.registry.mergeExternalStates(states);
175238
175290
  const endpoints = this.root.parts.map((p) => p);
175239
175291
  const results = await Promise.allSettled(
175240
175292
  endpoints.map((endpoint) => endpoint.updateStates(states))
@@ -175705,6 +175757,12 @@ var BridgeRegistry = class _BridgeRegistry {
175705
175757
  isEnergyEntityUsed(entityId) {
175706
175758
  return this._usedEnergyEntities.has(entityId);
175707
175759
  }
175760
+ mergeExternalStates(states) {
175761
+ const registryStates = this.registry.states;
175762
+ for (const entityId of Object.keys(states)) {
175763
+ registryStates[entityId] = states[entityId];
175764
+ }
175765
+ }
175708
175766
  /**
175709
175767
  * Get the area name for an entity, resolving from HA area registry.
175710
175768
  * Priority: entity area_id > device area_id > undefined
@@ -176401,12 +176459,18 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176401
176459
  if (!endpointType) {
176402
176460
  return void 0;
176403
176461
  }
176404
- return new _ServerModeVacuumEndpoint(endpointType, entityId, customName);
176462
+ const mappedIds = getMappedEntityIds(effectiveMapping);
176463
+ return new _ServerModeVacuumEndpoint(
176464
+ endpointType,
176465
+ entityId,
176466
+ customName,
176467
+ mappedIds
176468
+ );
176405
176469
  }
176406
176470
  lastState;
176407
176471
  flushUpdate;
176408
- constructor(type, entityId, customName) {
176409
- super(type, entityId, customName);
176472
+ constructor(type, entityId, customName, mappedEntityIds) {
176473
+ super(type, entityId, customName, mappedEntityIds);
176410
176474
  this.flushUpdate = debounce5(this.flushPendingUpdate.bind(this), 50);
176411
176475
  }
176412
176476
  async delete() {
@@ -176415,9 +176479,15 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176415
176479
  }
176416
176480
  async updateStates(states) {
176417
176481
  const state = states[this.entityId] ?? {};
176418
- if (state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
176482
+ const mappedChanged = this.hasMappedEntityChanged(states);
176483
+ if (!mappedChanged && state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
176419
176484
  return;
176420
176485
  }
176486
+ if (mappedChanged) {
176487
+ logger192.debug(
176488
+ `Mapped entity change detected for ${this.entityId}, forcing update`
176489
+ );
176490
+ }
176421
176491
  logger192.debug(
176422
176492
  `State update received for ${this.entityId}: state=${state.state}`
176423
176493
  );
@@ -176498,12 +176568,25 @@ var ServerModeEndpointManager = class extends Service {
176498
176568
  if (!this.entityIds.length) {
176499
176569
  return;
176500
176570
  }
176571
+ const subscriptionIds = this.collectSubscriptionEntityIds();
176501
176572
  this.unsubscribe = subscribeEntities(
176502
176573
  this.client.connection,
176503
176574
  (e) => this.updateStates(e),
176504
- this.entityIds
176575
+ subscriptionIds
176505
176576
  );
176506
176577
  }
176578
+ collectSubscriptionEntityIds() {
176579
+ const ids = new Set(this.entityIds);
176580
+ if (this.deviceEndpoint) {
176581
+ const mappedIds = this.deviceEndpoint.mappedEntityIds;
176582
+ if (mappedIds) {
176583
+ for (const mappedId of mappedIds) {
176584
+ ids.add(mappedId);
176585
+ }
176586
+ }
176587
+ }
176588
+ return [...ids];
176589
+ }
176507
176590
  stopObserving() {
176508
176591
  this.unsubscribe?.();
176509
176592
  this.unsubscribe = void 0;
@@ -176616,6 +176699,7 @@ var ServerModeEndpointManager = class extends Service {
176616
176699
  }
176617
176700
  }
176618
176701
  async updateStates(states) {
176702
+ this.registry.mergeExternalStates(states);
176619
176703
  if (this.deviceEndpoint) {
176620
176704
  try {
176621
176705
  await this.deviceEndpoint.updateStates(states);