Skip to content

Commit 567ddd1

Browse files
committed
Add catch-all for user-renamed AMS devices (#47)
Users who rename their AMS devices in HA (e.g., "AMS links"/"AMS rechts" for left/right) get non-standard entity IDs like ams_links_humidity instead of ams_1_humidity. Add catch-all patterns to matchAmsHumidityEntity and matchTrayEntity that accept any single alphabetic word as an AMS identifier, enabling device-based fallback discovery to find these entities.
1 parent 5180b31 commit 567ddd1

2 files changed

Lines changed: 125 additions & 0 deletions

File tree

app/src/lib/entity-patterns.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ const humidityTestCases: TestCase[] = [
9292
{ name: 'No prefix - ams_ht_humidity', entityId: 'sensor.ams_ht_humidity', expected: '128' },
9393
{ name: 'No prefix - ams_luftfeuchtigkeit', entityId: 'sensor.ams_luftfeuchtigkeit', expected: '1' },
9494

95+
// User-renamed AMS devices (e.g., German left/right)
96+
{ name: 'Renamed AMS "links" (left)', entityId: 'sensor.h2d_ams_links_humidity', expected: 'links' },
97+
{ name: 'Renamed AMS "rechts" (right)', entityId: 'sensor.h2d_ams_rechts_humidity', expected: 'rechts' },
98+
{ name: 'Renamed AMS "left"', entityId: 'sensor.h2d_ams_left_humidity', expected: 'left' },
99+
{ name: 'Renamed AMS "right"', entityId: 'sensor.h2d_ams_right_humidity', expected: 'right' },
100+
95101
// Edge cases - should NOT match
96102
{ name: 'Invalid - no ams', entityId: 'sensor.x1c_humidity', expected: null },
97103
{ name: 'Invalid - wrong type', entityId: 'sensor.x1c_ams_1_temperature', expected: null },
@@ -159,6 +165,11 @@ const trayTestCases: TrayTestCase[] = [
159165
{ name: 'No prefix - ams_ht_tray_2', entityId: 'sensor.ams_ht_tray_2', expected: { amsNumber: '128', trayNumber: 2 } },
160166
{ name: 'No prefix - ams_slot_1 German', entityId: 'sensor.ams_slot_1', expected: { amsNumber: '1', trayNumber: 1 } },
161167

168+
// User-renamed AMS devices (e.g., German left/right)
169+
{ name: 'Renamed AMS "links" Tray 1', entityId: 'sensor.h2d_ams_links_tray_1', expected: { amsNumber: 'links', trayNumber: 1 } },
170+
{ name: 'Renamed AMS "rechts" Tray 4', entityId: 'sensor.h2d_ams_rechts_tray_4', expected: { amsNumber: 'rechts', trayNumber: 4 } },
171+
{ name: 'Renamed AMS "links" Slot 2', entityId: 'sensor.h2d_ams_links_slot_2', expected: { amsNumber: 'links', trayNumber: 2 } },
172+
162173
// Edge cases - should NOT match
163174
{ name: 'Invalid - no tray number', entityId: 'sensor.x1c_ams_1_tray', expected: null },
164175
{ name: 'Invalid - humidity not tray', entityId: 'sensor.x1c_ams_1_humidity', expected: null },
@@ -875,6 +886,97 @@ test('H2D: All entities from issue #45 are discovered', () => {
875886
assertEqual(externalCount, 1);
876887
});
877888

889+
// =============================================================================
890+
// User-Renamed AMS Devices (GitHub Issue #47 - Somaxax)
891+
// =============================================================================
892+
893+
console.log('\n=== User-Renamed AMS Devices (GitHub Issue #47 - Somaxax) ===\n');
894+
895+
// Somaxax's H2D entity list from issue #47 — AMS devices renamed to "links"/"rechts"
896+
const somaxaxEntities = [
897+
'sensor.h2d_ams_links_humidity',
898+
'sensor.h2d_ams_links_humidity_index',
899+
'sensor.h2d_ams_links_remaining_drying_time',
900+
'sensor.h2d_ams_links_temperature',
901+
'sensor.h2d_ams_links_tray_1',
902+
'sensor.h2d_ams_links_tray_2',
903+
'sensor.h2d_ams_links_tray_3',
904+
'sensor.h2d_ams_links_tray_4',
905+
'sensor.h2d_ams_rechts_humidity',
906+
'sensor.h2d_ams_rechts_humidity_index',
907+
'sensor.h2d_ams_rechts_remaining_drying_time',
908+
'sensor.h2d_ams_rechts_temperature',
909+
'sensor.h2d_ams_rechts_tray_1',
910+
'sensor.h2d_ams_rechts_tray_2',
911+
'sensor.h2d_ams_rechts_tray_3',
912+
'sensor.h2d_ams_rechts_tray_4',
913+
'sensor.h2d_externalspool_external_spool',
914+
'sensor.h2d_externalspool2_external_spool',
915+
];
916+
917+
test('Somaxax H2D: "links" AMS humidity detected', () => {
918+
assertEqual(matchAmsHumidityEntity('sensor.h2d_ams_links_humidity'), 'links');
919+
});
920+
921+
test('Somaxax H2D: "rechts" AMS humidity detected', () => {
922+
assertEqual(matchAmsHumidityEntity('sensor.h2d_ams_rechts_humidity'), 'rechts');
923+
});
924+
925+
test('Somaxax H2D: Non-humidity sensors should NOT match', () => {
926+
assertEqual(matchAmsHumidityEntity('sensor.h2d_ams_links_humidity_index'), null);
927+
assertEqual(matchAmsHumidityEntity('sensor.h2d_ams_links_remaining_drying_time'), null);
928+
assertEqual(matchAmsHumidityEntity('sensor.h2d_ams_links_temperature'), null);
929+
});
930+
931+
test('Somaxax H2D: All 4 "links" trays detected', () => {
932+
for (let i = 1; i <= 4; i++) {
933+
const result = matchTrayEntity(`sensor.h2d_ams_links_tray_${i}`);
934+
if (!result) throw new Error(`Tray ${i} not detected`);
935+
assertEqual(result.amsNumber, 'links');
936+
assertEqual(result.trayNumber, i);
937+
}
938+
});
939+
940+
test('Somaxax H2D: All 4 "rechts" trays detected', () => {
941+
for (let i = 1; i <= 4; i++) {
942+
const result = matchTrayEntity(`sensor.h2d_ams_rechts_tray_${i}`);
943+
if (!result) throw new Error(`Tray ${i} not detected`);
944+
assertEqual(result.amsNumber, 'rechts');
945+
assertEqual(result.trayNumber, i);
946+
}
947+
});
948+
949+
test('Somaxax H2D: "links" and "rechts" do not collide', () => {
950+
const links = matchAmsHumidityEntity('sensor.h2d_ams_links_humidity');
951+
const rechts = matchAmsHumidityEntity('sensor.h2d_ams_rechts_humidity');
952+
if (links === rechts) throw new Error('links and rechts should have different AMS identifiers');
953+
});
954+
955+
test('Somaxax H2D: External spools detected', () => {
956+
assertEqual(matchExternalSpoolEntity('sensor.h2d_externalspool_external_spool'), true);
957+
assertEqual(matchExternalSpoolEntity('sensor.h2d_externalspool2_external_spool'), true);
958+
});
959+
960+
test('Somaxax H2D: buildTrayPattern works with non-numeric amsNumber', () => {
961+
const pattern = buildTrayPattern('h2d', 'links', 1);
962+
const match = 'sensor.h2d_ams_links_tray_1'.match(pattern);
963+
if (!match) throw new Error('buildTrayPattern should match renamed AMS tray');
964+
});
965+
966+
test('Somaxax H2D: All entities from issue #47 are discovered', () => {
967+
let humidityCount = 0;
968+
let trayCount = 0;
969+
let externalCount = 0;
970+
for (const entity of somaxaxEntities) {
971+
if (matchAmsHumidityEntity(entity)) humidityCount++;
972+
if (matchTrayEntity(entity)) trayCount++;
973+
if (matchExternalSpoolEntity(entity)) externalCount++;
974+
}
975+
assertEqual(humidityCount, 2); // links + rechts
976+
assertEqual(trayCount, 8); // 4 links trays + 4 rechts trays
977+
assertEqual(externalCount, 2); // externalspool + externalspool2
978+
});
979+
878980
// =============================================================================
879981
// Summary
880982
// =============================================================================

app/src/lib/entity-patterns.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,16 @@ export function matchAmsHumidityEntity(entityId: string): string | null {
653653
// Return AMS number, or "lite", or default to "1" for A1 AMS Lite without explicit naming
654654
return match[1] || match[2] || match[3] || '1';
655655
}
656+
657+
// Catch-all: match any single alphabetic word between _ams_ and a humidity name
658+
// Handles user-renamed AMS devices (e.g., ams_links_, ams_rechts_, ams_left_, ams_right_)
659+
// Only reached if no specific pattern matched above
660+
const catchAllPattern = new RegExp(`^sensor\\.(?:.+_)?ams_([a-z]+)_(?:${names})(?:_\\d+)?$`);
661+
const catchAllMatch = entityId.match(catchAllPattern);
662+
if (catchAllMatch) {
663+
return catchAllMatch[1];
664+
}
665+
656666
return null;
657667
}
658668

@@ -747,6 +757,19 @@ export function matchTrayEntity(entityId: string): { amsNumber: string; trayNumb
747757
trayNumber: parseInt(match[4], 10),
748758
};
749759
}
760+
761+
// Catch-all: match any single alphabetic word between _ams_ and a tray name
762+
// Handles user-renamed AMS devices (e.g., ams_links_tray_1, ams_rechts_slot_2)
763+
// Only reached if no specific pattern matched above
764+
const catchAllTrayPattern = new RegExp(`^sensor\\.(?:.+_)?ams_([a-z]+)_(?:${names})_(\\d+)(?:_\\d+)?$`);
765+
const catchAllTrayMatch = entityId.match(catchAllTrayPattern);
766+
if (catchAllTrayMatch) {
767+
return {
768+
amsNumber: catchAllTrayMatch[1],
769+
trayNumber: parseInt(catchAllTrayMatch[2], 10),
770+
};
771+
}
772+
750773
return null;
751774
}
752775

0 commit comments

Comments
 (0)