@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.
|
|
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.
|
|
55
|
+
<summary><strong>📦 Stable Features (v2.0.30)</strong> - Click to expand</summary>
|
|
56
56
|
|
|
57
|
-
**New in v2.0.
|
|
57
|
+
**New in v2.0.30:**
|
|
58
58
|
|
|
59
59
|
| Feature | Description |
|
|
60
60
|
|---------|-------------|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| **🌡️
|
|
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.
|
|
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
|
package/dist/backend/cli.js
CHANGED
|
@@ -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["
|
|
146224
|
-
FanDeviceFeature2[FanDeviceFeature2["
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|