@riddix/hamh 2.0.29 → 2.0.31

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.31 | 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,31 @@ 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.31)</strong> - Click to expand</summary>
56
56
 
57
- **New in v2.0.29:**
57
+ **New in v2.0.31:**
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
+ | **🎮 Controller Profiles & Area Setup** | New bridge wizard with controller-specific profiles (Apple Home, Google Home, Alexa) and area-based bridge setup |
62
+ | **🌀 Fan Speed/Preset Fix** | Fix fan speed and preset control not working from Matter controllers ([#233](https://github.com/RiDDiX/home-assistant-matter-hub/issues/233)) |
63
+ | **💡 Optimistic State Fix** | Prevent stale HA state from reverting optimistic brightness/color updates ([#230](https://github.com/RiDDiX/home-assistant-matter-hub/issues/230)) |
64
+ | **🪟 Cover Target Fix** | Route boundary cover targets to open/close regardless of direction ([#240](https://github.com/RiDDiX/home-assistant-matter-hub/issues/240)) |
65
+ | **�️ Humidity Auto-Mapping Default** | Make autoHumidityMapping default-enabled like autoPressureMapping |
65
66
 
66
- **Previously in v2.0.28:**
67
+ **Previously in v2.0.30:**
67
68
 
68
69
  | Feature | Description |
69
70
  |---------|-------------|
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)) |
71
+ | **🔗 Mapped Entity Propagation Fix** | Propagate mapped entity changes (battery, humidity, etc.) to Matter endpoints — fixes stale sensor readings |
72
+ | **🖥️ API Error Surfacing** | Surface API errors instead of silently swallowing them ([#232](https://github.com/RiDDiX/home-assistant-matter-hub/issues/232)) |
78
73
 
79
74
  </details>
80
75
 
81
76
  <details>
82
77
  <summary><strong>🧪 Alpha Features (v2.1.0-alpha.x)</strong> - Click to expand</summary>
83
78
 
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.
79
+ **Alpha is currently in sync with Stable (v2.0.31).** All alpha features have been promoted to stable. New alpha features will appear here as development continues.
85
80
 
86
81
  </details>
87
82
 
@@ -107,6 +102,12 @@ of port forwarding etc.
107
102
  <details>
108
103
  <summary><strong>📜 Previous Stable Versions</strong> - Click to expand</summary>
109
104
 
105
+ ### v2.0.30
106
+ Mapped Entity Propagation Fix, API Error Surfacing
107
+
108
+ ### v2.0.29
109
+ Light currentLevel Fix, Bridge Config Save Fix, Fan Device Feature Fix, Humidity Auto-Mapping Fix
110
+
110
111
  ### v2.0.28
111
112
  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
113
 
@@ -445,6 +446,7 @@ Thank you to everyone who helps improve this project by reporting issues!
445
446
  | [@JRCondat](https://github.com/JRCondat) | 💎 Thank you for your generous support! |
446
447
  | Bonjon | 💎 Thank you for your generous support! |
447
448
  | TobiR | 💎 Thank you for your generous support! |
449
+ | [@d3rby91](https://github.com/d3rby91) | 💎 Thank you for your generous support! |
448
450
  | *Anonymous supporters* | 🙏 Thank you to those who prefer not to be named - your support is equally appreciated! |
449
451
 
450
452
  ### 🌟 Original Author
@@ -146085,6 +146085,13 @@ var init_clusters2 = __esm({
146085
146085
  }
146086
146086
  });
146087
146087
 
146088
+ // ../common/dist/controller-profiles.js
146089
+ var init_controller_profiles = __esm({
146090
+ "../common/dist/controller-profiles.js"() {
146091
+ "use strict";
146092
+ }
146093
+ });
146094
+
146088
146095
  // ../common/dist/diagnostic-event.js
146089
146096
  var init_diagnostic_event = __esm({
146090
146097
  "../common/dist/diagnostic-event.js"() {
@@ -146694,7 +146701,7 @@ var init_bridge_config_schema = __esm({
146694
146701
  title: "Auto Humidity Mapping",
146695
146702
  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
146703
  type: "boolean",
146697
- default: false
146704
+ default: true
146698
146705
  },
146699
146706
  autoPressureMapping: {
146700
146707
  title: "Auto Pressure Mapping",
@@ -146999,6 +147006,7 @@ var init_dist = __esm({
146999
147006
  init_bridge_export();
147000
147007
  init_bridge_templates();
147001
147008
  init_clusters2();
147009
+ init_controller_profiles();
147002
147010
  init_diagnostic_event();
147003
147011
  init_domains();
147004
147012
  init_endpoint_data();
@@ -149030,7 +149038,8 @@ function matterApi(bridgeService, haRegistry) {
149030
149038
  const body = req.body;
149031
149039
  const isValid = ajv.validate(createBridgeRequestSchema, body);
149032
149040
  if (!isValid) {
149033
- res.status(400).json(ajv.errors);
149041
+ const details = ajv.errors?.map((e) => `${e.instancePath || "/"}: ${e.message}`).join("; ") ?? "Unknown";
149042
+ res.status(400).json({ error: `Validation failed: ${details}` });
149034
149043
  } else {
149035
149044
  try {
149036
149045
  const bridge = await bridgeService.create(body);
@@ -149071,7 +149080,8 @@ function matterApi(bridgeService, haRegistry) {
149071
149080
  const body = req.body;
149072
149081
  const isValid = ajv.validate(updateBridgeRequestSchema, body);
149073
149082
  if (!isValid) {
149074
- res.status(400).json(ajv.errors);
149083
+ const details = ajv.errors?.map((e) => `${e.instancePath || "/"}: ${e.message}`).join("; ") ?? "Unknown";
149084
+ res.status(400).json({ error: `Validation failed: ${details}` });
149075
149085
  } else if (bridgeId !== body.id) {
149076
149086
  res.status(400).send("Path variable `bridgeId` does not match `body.id`");
149077
149087
  } else {
@@ -149302,6 +149312,73 @@ function matterApi(bridgeService, haRegistry) {
149302
149312
  areas.sort((a, b) => a.name.localeCompare(b.name));
149303
149313
  res.status(200).json(areas);
149304
149314
  });
149315
+ router.get("/areas/summary", async (_, res) => {
149316
+ if (!haRegistry) {
149317
+ res.status(503).json({ error: "Home Assistant registry not available" });
149318
+ return;
149319
+ }
149320
+ const supportedDomains = /* @__PURE__ */ new Set([
149321
+ "light",
149322
+ "switch",
149323
+ "sensor",
149324
+ "binary_sensor",
149325
+ "climate",
149326
+ "cover",
149327
+ "fan",
149328
+ "lock",
149329
+ "media_player",
149330
+ "vacuum",
149331
+ "valve",
149332
+ "humidifier",
149333
+ "water_heater",
149334
+ "select",
149335
+ "input_select",
149336
+ "input_boolean",
149337
+ "alarm_control_panel",
149338
+ "event",
149339
+ "automation",
149340
+ "script",
149341
+ "scene"
149342
+ ]);
149343
+ const entities = Object.values(haRegistry.entities);
149344
+ const devices = haRegistry.devices;
149345
+ const states = haRegistry.states;
149346
+ const areaSummary = /* @__PURE__ */ new Map();
149347
+ for (const [areaId, areaName] of haRegistry.areas) {
149348
+ areaSummary.set(areaId, { name: areaName, entityCount: 0, domains: {} });
149349
+ }
149350
+ for (const entity of entities) {
149351
+ if (entity.disabled_by != null) continue;
149352
+ const domain = entity.entity_id.split(".")[0];
149353
+ if (!supportedDomains.has(domain)) continue;
149354
+ const state = states[entity.entity_id];
149355
+ if (!state || state.state === "unavailable") continue;
149356
+ let areaId;
149357
+ const entityAreaId = entity.area_id;
149358
+ if (entityAreaId && haRegistry.areas.has(entityAreaId)) {
149359
+ areaId = entityAreaId;
149360
+ } else {
149361
+ const device = entity.device_id ? devices[entity.device_id] : void 0;
149362
+ const deviceAreaId = device?.area_id;
149363
+ if (deviceAreaId && haRegistry.areas.has(deviceAreaId)) {
149364
+ areaId = deviceAreaId;
149365
+ }
149366
+ }
149367
+ if (!areaId) continue;
149368
+ const summary = areaSummary.get(areaId);
149369
+ if (summary) {
149370
+ summary.entityCount++;
149371
+ summary.domains[domain] = (summary.domains[domain] || 0) + 1;
149372
+ }
149373
+ }
149374
+ const result = [...areaSummary.entries()].map(([area_id, data]) => ({
149375
+ area_id,
149376
+ name: data.name,
149377
+ entityCount: data.entityCount,
149378
+ domains: data.domains
149379
+ })).sort((a, b) => a.name.localeCompare(b.name));
149380
+ res.status(200).json(result);
149381
+ });
149305
149382
  router.get("/filter-values", async (_, res) => {
149306
149383
  if (!haRegistry) {
149307
149384
  res.status(503).json({ error: "Home Assistant registry not available" });
@@ -164463,15 +164540,45 @@ import debounce4 from "debounce";
164463
164540
  // src/matter/endpoints/entity-endpoint.ts
164464
164541
  init_esm7();
164465
164542
  var EntityEndpoint = class extends Endpoint {
164466
- constructor(type, entityId, customName) {
164543
+ constructor(type, entityId, customName, mappedEntityIds) {
164467
164544
  super(type, { id: createEndpointId(entityId, customName) });
164468
164545
  this.entityId = entityId;
164546
+ this.mappedEntityIds = mappedEntityIds ?? [];
164547
+ }
164548
+ mappedEntityIds;
164549
+ lastMappedStates = {};
164550
+ hasMappedEntityChanged(states) {
164551
+ let changed = false;
164552
+ for (const mappedId of this.mappedEntityIds) {
164553
+ const mappedState = states[mappedId];
164554
+ if (!mappedState) continue;
164555
+ const fp = mappedState.state;
164556
+ if (fp !== this.lastMappedStates[mappedId]) {
164557
+ this.lastMappedStates[mappedId] = fp;
164558
+ changed = true;
164559
+ }
164560
+ }
164561
+ return changed;
164469
164562
  }
164470
164563
  };
164471
164564
  function createEndpointId(entityId, customName) {
164472
164565
  const baseName = customName || entityId;
164473
164566
  return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
164474
164567
  }
164568
+ function getMappedEntityIds(mapping) {
164569
+ if (!mapping) return [];
164570
+ const ids = [];
164571
+ if (mapping.batteryEntity) ids.push(mapping.batteryEntity);
164572
+ if (mapping.humidityEntity) ids.push(mapping.humidityEntity);
164573
+ if (mapping.pressureEntity) ids.push(mapping.pressureEntity);
164574
+ if (mapping.cleaningModeEntity) ids.push(mapping.cleaningModeEntity);
164575
+ if (mapping.suctionLevelEntity) ids.push(mapping.suctionLevelEntity);
164576
+ if (mapping.mopIntensityEntity) ids.push(mapping.mopIntensityEntity);
164577
+ if (mapping.filterLifeEntity) ids.push(mapping.filterLifeEntity);
164578
+ if (mapping.powerEntity) ids.push(mapping.powerEntity);
164579
+ if (mapping.energyEntity) ids.push(mapping.energyEntity);
164580
+ return ids;
164581
+ }
164475
164582
 
164476
164583
  // src/matter/endpoints/composed/composed-air-purifier-endpoint.ts
164477
164584
  init_dist();
@@ -165901,12 +166008,8 @@ var FanControlServerBase = class extends FeaturedBase3 {
165901
166008
  return;
165902
166009
  }
165903
166010
  this.agent.asLocalActor(() => {
165904
- const percentSetting = Math.floor(speed / this.state.speedMax * 100);
165905
- this.targetPercentSettingChanged(
165906
- percentSetting,
165907
- this.state.percentSetting,
165908
- context
165909
- );
166011
+ const percentage = Math.floor(speed / this.state.speedMax * 100);
166012
+ this.applyPercentageAction(percentage);
165910
166013
  });
165911
166014
  }
165912
166015
  targetFanModeChanged(fanMode, _oldValue, context) {
@@ -165919,16 +166022,12 @@ var FanControlServerBase = class extends FeaturedBase3 {
165919
166022
  return;
165920
166023
  }
165921
166024
  const targetFanMode = FanMode.create(fanMode, this.state.fanModeSequence);
165922
- const config10 = this.state.config;
165923
166025
  if (targetFanMode.mode === FanControl3.FanMode.Auto) {
165924
- homeAssistant.callAction(config10.setAutoMode(void 0, this.agent));
165925
- } else {
165926
- const percentage = targetFanMode.speedPercent();
165927
- this.targetPercentSettingChanged(
165928
- percentage,
165929
- this.state.percentSetting,
165930
- context
166026
+ homeAssistant.callAction(
166027
+ this.state.config.setAutoMode(void 0, this.agent)
165931
166028
  );
166029
+ } else {
166030
+ this.applyPercentageAction(targetFanMode.speedPercent());
165932
166031
  }
165933
166032
  });
165934
166033
  }
@@ -165940,45 +166039,48 @@ var FanControlServerBase = class extends FeaturedBase3 {
165940
166039
  return;
165941
166040
  }
165942
166041
  this.agent.asLocalActor(() => {
165943
- const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
165944
- if (!homeAssistant.isAvailable) {
165945
- return;
165946
- }
165947
- const config10 = this.state.config;
165948
- const supportsPercentage = config10.supportsPercentage(
166042
+ this.applyPercentageAction(percentage);
166043
+ });
166044
+ }
166045
+ applyPercentageAction(percentage) {
166046
+ const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
166047
+ if (!homeAssistant.isAvailable) {
166048
+ return;
166049
+ }
166050
+ const config10 = this.state.config;
166051
+ const supportsPercentage = config10.supportsPercentage(
166052
+ homeAssistant.entity.state,
166053
+ this.agent
166054
+ );
166055
+ if (percentage === 0) {
166056
+ homeAssistant.callAction(config10.turnOff(void 0, this.agent));
166057
+ } else if (supportsPercentage) {
166058
+ const stepSize = config10.getStepSize(
165949
166059
  homeAssistant.entity.state,
165950
166060
  this.agent
165951
166061
  );
165952
- if (percentage === 0) {
165953
- homeAssistant.callAction(config10.turnOff(void 0, this.agent));
165954
- } else if (supportsPercentage) {
165955
- const stepSize = config10.getStepSize(
165956
- homeAssistant.entity.state,
165957
- this.agent
165958
- );
165959
- const roundedPercentage = stepSize && stepSize > 0 ? Math.round(percentage / stepSize) * stepSize : percentage;
165960
- const clampedPercentage = Math.max(
165961
- stepSize ?? 1,
165962
- Math.min(100, roundedPercentage)
166062
+ const roundedPercentage = stepSize && stepSize > 0 ? Math.round(percentage / stepSize) * stepSize : percentage;
166063
+ const clampedPercentage = Math.max(
166064
+ stepSize ?? 1,
166065
+ Math.min(100, roundedPercentage)
166066
+ );
166067
+ homeAssistant.callAction(config10.turnOn(clampedPercentage, this.agent));
166068
+ } else {
166069
+ const presetModes = config10.getPresetModes(homeAssistant.entity.state, this.agent) ?? [];
166070
+ const speedPresets = presetModes.filter(
166071
+ (m) => m.toLowerCase() !== "auto"
166072
+ );
166073
+ if (speedPresets.length > 0) {
166074
+ const presetIndex = Math.min(
166075
+ Math.floor(percentage / 100 * speedPresets.length),
166076
+ speedPresets.length - 1
165963
166077
  );
165964
- homeAssistant.callAction(config10.turnOn(clampedPercentage, this.agent));
165965
- } else {
165966
- const presetModes = config10.getPresetModes(homeAssistant.entity.state, this.agent) ?? [];
165967
- const speedPresets = presetModes.filter(
165968
- (m) => m.toLowerCase() !== "auto"
166078
+ const targetPreset = speedPresets[presetIndex];
166079
+ homeAssistant.callAction(
166080
+ config10.setPresetMode(targetPreset, this.agent)
165969
166081
  );
165970
- if (speedPresets.length > 0) {
165971
- const presetIndex = Math.min(
165972
- Math.floor(percentage / 100 * speedPresets.length),
165973
- speedPresets.length - 1
165974
- );
165975
- const targetPreset = speedPresets[presetIndex];
165976
- homeAssistant.callAction(
165977
- config10.setPresetMode(targetPreset, this.agent)
165978
- );
165979
- }
165980
166082
  }
165981
- });
166083
+ }
165982
166084
  }
165983
166085
  targetAirflowDirectionChanged(airflowDirection, _oldValue, context) {
165984
166086
  if (transactionIsOffline(context)) {
@@ -166118,6 +166220,8 @@ init_home_assistant_entity_behavior();
166118
166220
  init_esm();
166119
166221
  init_home_assistant_entity_behavior();
166120
166222
  var lastTurnOnTimestamps = /* @__PURE__ */ new Map();
166223
+ var optimisticLevelTimestamps = /* @__PURE__ */ new Map();
166224
+ var OPTIMISTIC_LEVEL_COOLDOWN_MS = 2e3;
166121
166225
  function notifyLightTurnedOn(entityId) {
166122
166226
  lastTurnOnTimestamps.set(entityId, Date.now());
166123
166227
  }
@@ -166162,6 +166266,11 @@ var LevelControlServerBase = class extends FeaturedBase4 {
166162
166266
  if (currentLevel != null) {
166163
166267
  currentLevel = Math.min(Math.max(minLevel, currentLevel), maxLevel);
166164
166268
  }
166269
+ const lastOptimistic = optimisticLevelTimestamps.get(entity.entity_id);
166270
+ const inCooldown = lastOptimistic != null && Date.now() - lastOptimistic < OPTIMISTIC_LEVEL_COOLDOWN_MS;
166271
+ if (inCooldown && currentLevel != null) {
166272
+ currentLevel = null;
166273
+ }
166165
166274
  applyPatchState(this.state, {
166166
166275
  minLevel,
166167
166276
  maxLevel,
@@ -166229,6 +166338,7 @@ var LevelControlServerBase = class extends FeaturedBase4 {
166229
166338
  };
166230
166339
  }
166231
166340
  this.state.currentLevel = level;
166341
+ optimisticLevelTimestamps.set(entityId, Date.now());
166232
166342
  homeAssistant.callAction(action);
166233
166343
  }
166234
166344
  };
@@ -168606,9 +168716,9 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
168606
168716
  `handleMovement: type=${MovementType[type]}, direction=${MovementDirection[direction]}, target=${targetPercent100ths}, currentLift=${currentLift}, currentTilt=${currentTilt}, absolutePosition=${this.features.absolutePosition}`
168607
168717
  );
168608
168718
  if (type === MovementType.Lift) {
168609
- if (direction === MovementDirection.Open && (targetPercent100ths == null || targetPercent100ths === 0)) {
168719
+ if (targetPercent100ths === 0) {
168610
168720
  this.handleLiftOpen();
168611
- } else if (direction === MovementDirection.Close && (targetPercent100ths == null || targetPercent100ths === 1e4)) {
168721
+ } else if (targetPercent100ths === 1e4) {
168612
168722
  this.handleLiftClose();
168613
168723
  } else if (targetPercent100ths != null && this.features.absolutePosition) {
168614
168724
  this.handleGoToLiftPosition(targetPercent100ths);
@@ -168618,9 +168728,9 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
168618
168728
  this.handleLiftClose();
168619
168729
  }
168620
168730
  } else if (type === MovementType.Tilt) {
168621
- if (direction === MovementDirection.Open && (targetPercent100ths == null || targetPercent100ths === 0)) {
168731
+ if (targetPercent100ths === 0) {
168622
168732
  this.handleTiltOpen();
168623
- } else if (direction === MovementDirection.Close && (targetPercent100ths == null || targetPercent100ths === 1e4)) {
168733
+ } else if (targetPercent100ths === 1e4) {
168624
168734
  this.handleTiltClose();
168625
168735
  } else if (targetPercent100ths != null && this.features.absolutePosition) {
168626
168736
  this.handleGoToTiltPosition(targetPercent100ths);
@@ -169317,6 +169427,8 @@ init_nodejs();
169317
169427
  // src/matter/behaviors/color-control-server.ts
169318
169428
  init_home_assistant_entity_behavior();
169319
169429
  var logger170 = Logger.get("ColorControlServer");
169430
+ var optimisticColorTimestamps = /* @__PURE__ */ new Map();
169431
+ var OPTIMISTIC_COLOR_COOLDOWN_MS = 2e3;
169320
169432
  var FeaturedBase7 = ColorControlServer.with("ColorTemperature", "HueSaturation");
169321
169433
  var ColorControlServerBase = class extends FeaturedBase7 {
169322
169434
  pendingTransitionTime;
@@ -169395,6 +169507,8 @@ var ColorControlServerBase = class extends FeaturedBase7 {
169395
169507
  const newColorMode = this.getColorModeFromFeatures(
169396
169508
  config10.getCurrentMode(entity.state, this.agent)
169397
169509
  );
169510
+ const lastOptimistic = optimisticColorTimestamps.get(entity.entity_id);
169511
+ const inCooldown = lastOptimistic != null && Date.now() - lastOptimistic < OPTIMISTIC_COLOR_COOLDOWN_MS;
169398
169512
  if (this.features.colorTemperature) {
169399
169513
  const existingMireds = this.state.colorTemperatureMireds;
169400
169514
  if (existingMireds != null) {
@@ -169419,12 +169533,12 @@ var ColorControlServerBase = class extends FeaturedBase7 {
169419
169533
  applyPatchState(this.state, {
169420
169534
  coupleColorTempToLevelMinMireds: minMireds,
169421
169535
  startUpColorTemperatureMireds: startUpMireds,
169422
- colorTemperatureMireds: effectiveMireds
169536
+ ...inCooldown ? {} : { colorTemperatureMireds: effectiveMireds }
169423
169537
  });
169424
169538
  }
169425
169539
  applyPatchState(this.state, {
169426
- colorMode: newColorMode,
169427
- ...this.features.hueSaturation ? {
169540
+ ...inCooldown ? {} : { colorMode: newColorMode },
169541
+ ...this.features.hueSaturation && !inCooldown ? {
169428
169542
  currentHue: hue,
169429
169543
  currentSaturation: saturation
169430
169544
  } : {}
@@ -169451,6 +169565,7 @@ var ColorControlServerBase = class extends FeaturedBase7 {
169451
169565
  colorTemperatureMireds: targetMireds,
169452
169566
  colorMode: ColorControl3.ColorMode.ColorTemperatureMireds
169453
169567
  });
169568
+ optimisticColorTimestamps.set(homeAssistant.entityId, Date.now());
169454
169569
  homeAssistant.callAction(action);
169455
169570
  }
169456
169571
  moveToHueLogic(targetHue) {
@@ -169481,6 +169596,7 @@ var ColorControlServerBase = class extends FeaturedBase7 {
169481
169596
  currentSaturation: targetSaturation,
169482
169597
  colorMode: ColorControl3.ColorMode.CurrentHueAndCurrentSaturation
169483
169598
  });
169599
+ optimisticColorTimestamps.set(homeAssistant.entityId, Date.now());
169484
169600
  homeAssistant.callAction(action);
169485
169601
  }
169486
169602
  applyTransition(action) {
@@ -170784,6 +170900,8 @@ init_home_assistant_entity_behavior();
170784
170900
  init_esm();
170785
170901
  init_home_assistant_entity_behavior();
170786
170902
  var logger175 = Logger.get("SpeakerLevelControlServer");
170903
+ var optimisticLevelTimestamps2 = /* @__PURE__ */ new Map();
170904
+ var OPTIMISTIC_LEVEL_COOLDOWN_MS2 = 2e3;
170787
170905
  var FeaturedBase9 = LevelControlServer.with("OnOff");
170788
170906
  var SpeakerLevelControlServerBase = class extends FeaturedBase9 {
170789
170907
  async initialize() {
@@ -170819,10 +170937,15 @@ var SpeakerLevelControlServerBase = class extends FeaturedBase9 {
170819
170937
  logger175.debug(
170820
170938
  `[${entityId}] Volume update: HA=${currentLevelPercent != null ? Math.round(currentLevelPercent * 100) : "null"}% -> currentLevel=${currentLevel}`
170821
170939
  );
170940
+ const lastOptimistic = optimisticLevelTimestamps2.get(entity.entity_id);
170941
+ const inCooldown = lastOptimistic != null && Date.now() - lastOptimistic < OPTIMISTIC_LEVEL_COOLDOWN_MS2;
170942
+ if (inCooldown && currentLevel != null) {
170943
+ currentLevel = null;
170944
+ }
170822
170945
  applyPatchState(this.state, {
170823
170946
  minLevel,
170824
170947
  maxLevel,
170825
- currentLevel
170948
+ ...currentLevel != null ? { currentLevel } : {}
170826
170949
  });
170827
170950
  }
170828
170951
  async moveToLevel(request) {
@@ -170865,6 +170988,7 @@ var SpeakerLevelControlServerBase = class extends FeaturedBase9 {
170865
170988
  return;
170866
170989
  }
170867
170990
  this.state.currentLevel = level;
170991
+ optimisticLevelTimestamps2.set(entityId, Date.now());
170868
170992
  homeAssistant.callAction(
170869
170993
  config10.moveToLevelPercent(levelPercent, this.agent)
170870
170994
  );
@@ -174790,10 +174914,11 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174790
174914
  return;
174791
174915
  }
174792
174916
  const customName = effectiveMapping?.customName;
174793
- return new _LegacyEndpoint(type, entityId, customName);
174917
+ const mappedIds = getMappedEntityIds(effectiveMapping);
174918
+ return new _LegacyEndpoint(type, entityId, customName, mappedIds);
174794
174919
  }
174795
- constructor(type, entityId, customName) {
174796
- super(type, entityId, customName);
174920
+ constructor(type, entityId, customName, mappedEntityIds) {
174921
+ super(type, entityId, customName, mappedEntityIds);
174797
174922
  this.flushUpdate = debounce4(this.flushPendingUpdate.bind(this), 50);
174798
174923
  }
174799
174924
  lastState;
@@ -174804,9 +174929,15 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
174804
174929
  }
174805
174930
  async updateStates(states) {
174806
174931
  const state = states[this.entityId] ?? {};
174807
- if (state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
174932
+ const mappedChanged = this.hasMappedEntityChanged(states);
174933
+ if (!mappedChanged && state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
174808
174934
  return;
174809
174935
  }
174936
+ if (mappedChanged) {
174937
+ logger189.debug(
174938
+ `Mapped entity change detected for ${this.entityId}, forcing update`
174939
+ );
174940
+ }
174810
174941
  logger189.debug(
174811
174942
  `State update received for ${this.entityId}: state=${state.state}`
174812
174943
  );
@@ -175105,12 +175236,26 @@ var BridgeEndpointManager = class extends Service {
175105
175236
  if (!this.entityIds.length) {
175106
175237
  return;
175107
175238
  }
175239
+ const subscriptionIds = this.collectSubscriptionEntityIds();
175108
175240
  this.unsubscribe = subscribeEntities(
175109
175241
  this.client.connection,
175110
175242
  (e) => this.updateStates(e),
175111
- this.entityIds
175243
+ subscriptionIds
175112
175244
  );
175113
175245
  }
175246
+ collectSubscriptionEntityIds() {
175247
+ const ids = new Set(this.entityIds);
175248
+ const endpoints = this.root.parts.map((p) => p);
175249
+ for (const endpoint of endpoints) {
175250
+ const mappedIds = endpoint.mappedEntityIds;
175251
+ if (mappedIds) {
175252
+ for (const mappedId of mappedIds) {
175253
+ ids.add(mappedId);
175254
+ }
175255
+ }
175256
+ }
175257
+ return [...ids];
175258
+ }
175114
175259
  stopObserving() {
175115
175260
  this.unsubscribe?.();
175116
175261
  this.unsubscribe = void 0;
@@ -175235,6 +175380,7 @@ var BridgeEndpointManager = class extends Service {
175235
175380
  }
175236
175381
  }
175237
175382
  async updateStates(states) {
175383
+ this.registry.mergeExternalStates(states);
175238
175384
  const endpoints = this.root.parts.map((p) => p);
175239
175385
  const results = await Promise.allSettled(
175240
175386
  endpoints.map((endpoint) => endpoint.updateStates(states))
@@ -175391,15 +175537,15 @@ var BridgeRegistry = class _BridgeRegistry {
175391
175537
  }
175392
175538
  /**
175393
175539
  * Check if auto humidity mapping is enabled for this bridge.
175394
- * Default: false (disabled by default, user must explicitly enable).
175540
+ * Default: true (enabled by default).
175395
175541
  * When enabled, humidity sensors on the same device as a temperature sensor
175396
175542
  * are combined into a single TemperatureHumiditySensor endpoint.
175397
175543
  * Note: Apple Home does not display humidity on TemperatureSensorDevice
175398
- * endpoints, so users on Apple Home should keep this disabled.
175544
+ * endpoints, so users on Apple Home should explicitly disable this.
175399
175545
  * See: https://github.com/RiDDiX/home-assistant-matter-hub/issues/133
175400
175546
  */
175401
175547
  isAutoHumidityMappingEnabled() {
175402
- return this.dataProvider.featureFlags?.autoHumidityMapping === true || this.dataProvider.featureFlags?.autoComposedDevices === true;
175548
+ return this.dataProvider.featureFlags?.autoHumidityMapping !== false || this.dataProvider.featureFlags?.autoComposedDevices === true;
175403
175549
  }
175404
175550
  /**
175405
175551
  * Find a humidity sensor entity that belongs to the same HA device.
@@ -175705,6 +175851,12 @@ var BridgeRegistry = class _BridgeRegistry {
175705
175851
  isEnergyEntityUsed(entityId) {
175706
175852
  return this._usedEnergyEntities.has(entityId);
175707
175853
  }
175854
+ mergeExternalStates(states) {
175855
+ const registryStates = this.registry.states;
175856
+ for (const entityId of Object.keys(states)) {
175857
+ registryStates[entityId] = states[entityId];
175858
+ }
175859
+ }
175708
175860
  /**
175709
175861
  * Get the area name for an entity, resolving from HA area registry.
175710
175862
  * Priority: entity area_id > device area_id > undefined
@@ -176401,12 +176553,18 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176401
176553
  if (!endpointType) {
176402
176554
  return void 0;
176403
176555
  }
176404
- return new _ServerModeVacuumEndpoint(endpointType, entityId, customName);
176556
+ const mappedIds = getMappedEntityIds(effectiveMapping);
176557
+ return new _ServerModeVacuumEndpoint(
176558
+ endpointType,
176559
+ entityId,
176560
+ customName,
176561
+ mappedIds
176562
+ );
176405
176563
  }
176406
176564
  lastState;
176407
176565
  flushUpdate;
176408
- constructor(type, entityId, customName) {
176409
- super(type, entityId, customName);
176566
+ constructor(type, entityId, customName, mappedEntityIds) {
176567
+ super(type, entityId, customName, mappedEntityIds);
176410
176568
  this.flushUpdate = debounce5(this.flushPendingUpdate.bind(this), 50);
176411
176569
  }
176412
176570
  async delete() {
@@ -176415,9 +176573,15 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176415
176573
  }
176416
176574
  async updateStates(states) {
176417
176575
  const state = states[this.entityId] ?? {};
176418
- if (state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
176576
+ const mappedChanged = this.hasMappedEntityChanged(states);
176577
+ if (!mappedChanged && state.state === this.lastState?.state && JSON.stringify(state.attributes) === JSON.stringify(this.lastState?.attributes)) {
176419
176578
  return;
176420
176579
  }
176580
+ if (mappedChanged) {
176581
+ logger192.debug(
176582
+ `Mapped entity change detected for ${this.entityId}, forcing update`
176583
+ );
176584
+ }
176421
176585
  logger192.debug(
176422
176586
  `State update received for ${this.entityId}: state=${state.state}`
176423
176587
  );
@@ -176498,12 +176662,25 @@ var ServerModeEndpointManager = class extends Service {
176498
176662
  if (!this.entityIds.length) {
176499
176663
  return;
176500
176664
  }
176665
+ const subscriptionIds = this.collectSubscriptionEntityIds();
176501
176666
  this.unsubscribe = subscribeEntities(
176502
176667
  this.client.connection,
176503
176668
  (e) => this.updateStates(e),
176504
- this.entityIds
176669
+ subscriptionIds
176505
176670
  );
176506
176671
  }
176672
+ collectSubscriptionEntityIds() {
176673
+ const ids = new Set(this.entityIds);
176674
+ if (this.deviceEndpoint) {
176675
+ const mappedIds = this.deviceEndpoint.mappedEntityIds;
176676
+ if (mappedIds) {
176677
+ for (const mappedId of mappedIds) {
176678
+ ids.add(mappedId);
176679
+ }
176680
+ }
176681
+ }
176682
+ return [...ids];
176683
+ }
176507
176684
  stopObserving() {
176508
176685
  this.unsubscribe?.();
176509
176686
  this.unsubscribe = void 0;
@@ -176616,6 +176793,7 @@ var ServerModeEndpointManager = class extends Service {
176616
176793
  }
176617
176794
  }
176618
176795
  async updateStates(states) {
176796
+ this.registry.mergeExternalStates(states);
176619
176797
  if (this.deviceEndpoint) {
176620
176798
  try {
176621
176799
  await this.deviceEndpoint.updateStates(states);