@riddix/hamh 2.0.28 → 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.28 | 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,44 +52,30 @@ 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.30)</strong> - Click to expand</summary>
56
56
 
57
- **New in v2.0.28:**
57
+ **New in v2.0.30:**
58
58
 
59
59
  | Feature | Description |
60
60
  |---------|-------------|
61
- | **🖼️ Device Image Support** | Device cards show images from Zigbee2MQTT or custom uploads ([#221](https://github.com/RiDDiX/home-assistant-matter-hub/issues/221)) |
62
- | **🌀 Custom Fan Speed Mapping** | Define custom fan speed levels with named presets ([#226](https://github.com/RiDDiX/home-assistant-matter-hub/pull/226)) |
63
- | **📺 TV Source Selection** | MediaInput cluster added to VideoPlayerDevice for input switching ([#231](https://github.com/RiDDiX/home-assistant-matter-hub/issues/231)) |
64
- | **🔀 Reverse Proxy Base Path** | `--http-base-path` option for subfolder reverse proxy support ([#228](https://github.com/RiDDiX/home-assistant-matter-hub/issues/228)) |
65
- | **🌀 On/Off-Only Fans** | Fans without speed control correctly use OnOffPlugInUnit ([#229](https://github.com/RiDDiX/home-assistant-matter-hub/issues/229)) |
66
- | **💡 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
- | **🌀 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
- | **🌡️ 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:**
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)) |
63
+
64
+ **Previously in v2.0.29:**
75
65
 
76
66
  | Feature | Description |
77
67
  |---------|-------------|
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 |
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 |
86
72
 
87
73
  </details>
88
74
 
89
75
  <details>
90
76
  <summary><strong>🧪 Alpha Features (v2.1.0-alpha.x)</strong> - Click to expand</summary>
91
77
 
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.
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.
93
79
 
94
80
  </details>
95
81
 
@@ -115,6 +101,12 @@ of port forwarding etc.
115
101
  <details>
116
102
  <summary><strong>📜 Previous Stable Versions</strong> - Click to expand</summary>
117
103
 
104
+ ### v2.0.29
105
+ Light currentLevel Fix, Bridge Config Save Fix, Fan Device Feature Fix, Humidity Auto-Mapping Fix
106
+
107
+ ### v2.0.28
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
109
+
118
110
  ### v2.0.27
119
111
  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
112
 
@@ -450,6 +442,7 @@ Thank you to everyone who helps improve this project by reporting issues!
450
442
  | [@JRCondat](https://github.com/JRCondat) | 💎 Thank you for your generous support! |
451
443
  | Bonjon | 💎 Thank you for your generous support! |
452
444
  | TobiR | 💎 Thank you for your generous support! |
445
+ | [@d3rby91](https://github.com/d3rby91) | 💎 Thank you for your generous support! |
453
446
  | *Anonymous supporters* | 🙏 Thank you to those who prefer not to be named - your support is equally appreciated! |
454
447
 
455
448
  ### 🌟 Original Author
@@ -146220,8 +146220,8 @@ var init_fan = __esm({
146220
146220
  FanDeviceFeature2[FanDeviceFeature2["OSCILLATE"] = 2] = "OSCILLATE";
146221
146221
  FanDeviceFeature2[FanDeviceFeature2["DIRECTION"] = 4] = "DIRECTION";
146222
146222
  FanDeviceFeature2[FanDeviceFeature2["PRESET_MODE"] = 8] = "PRESET_MODE";
146223
- FanDeviceFeature2[FanDeviceFeature2["TURN_OFF"] = 16] = "TURN_OFF";
146224
- FanDeviceFeature2[FanDeviceFeature2["TURN_ON"] = 32] = "TURN_ON";
146223
+ FanDeviceFeature2[FanDeviceFeature2["TURN_ON"] = 16] = "TURN_ON";
146224
+ FanDeviceFeature2[FanDeviceFeature2["TURN_OFF"] = 32] = "TURN_OFF";
146225
146225
  })(FanDeviceFeature || (FanDeviceFeature = {}));
146226
146226
  }
146227
146227
  });
@@ -146694,7 +146694,7 @@ var init_bridge_config_schema = __esm({
146694
146694
  title: "Auto Humidity Mapping",
146695
146695
  description: "Automatically combine humidity sensors with temperature sensors from the same Home Assistant device. When enabled, humidity sensors will be merged into temperature sensors to create combined TemperatureHumiditySensor devices.",
146696
146696
  type: "boolean",
146697
- default: true
146697
+ default: false
146698
146698
  },
146699
146699
  autoPressureMapping: {
146700
146700
  title: "Auto Pressure Mapping",
@@ -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();
@@ -169567,7 +169599,7 @@ var config8 = {
169567
169599
  if (brightness != null) {
169568
169600
  return brightness / 255;
169569
169601
  }
169570
- return 0;
169602
+ return null;
169571
169603
  },
169572
169604
  moveToLevelPercent: (brightnessPercent) => ({
169573
169605
  action: "light.turn_on",
@@ -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);