Skip to content

Commit c722aa8

Browse files
committed
Add compact H2D AMS entity naming patterns (#45, #47)
H2D printers use compact entity naming where the AMS model type is merged with "ams" without an underscore (e.g., ams2_1_humidity instead of ams_2_pro_humidity, amsht_1_humidity instead of ams_ht_1_humidity). Also handles H2D external spool format with underscore before digit (externalspool_1_ instead of externalspool2_).
1 parent 1778bd9 commit c722aa8

2 files changed

Lines changed: 191 additions & 10 deletions

File tree

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

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ const humidityTestCases: TestCase[] = [
7171
// AMS number-first with ht suffix (ams_1_ht_humidity)
7272
{ name: 'AMS HT number-first suffix', entityId: 'sensor.h2c_ams_1_ht_humidity', expected: '128' },
7373

74+
// Compact AMS model naming (H2D: ams2_1_humidity, amsht_1_humidity)
75+
{ name: 'Compact AMS model H2D unit 1', entityId: 'sensor.h2d_ams2_1_humidity', expected: '1' },
76+
{ name: 'Compact AMS model H2D unit 2', entityId: 'sensor.h2d_ams2_2_humidity', expected: '2' },
77+
{ name: 'Compact AMS HT H2D unit 1', entityId: 'sensor.h2d_amsht_1_humidity', expected: '128' },
78+
{ name: 'Compact AMS HT H2D unit 2', entityId: 'sensor.h2d_amsht_2_humidity', expected: '129' },
79+
7480
// Compact HT naming without _ams_ prefix (e.g., sensor.h2c_ht1_humidity)
7581
{ name: 'Compact HT 1 English', entityId: 'sensor.h2c_ht1_humidity', expected: '128' },
7682
{ name: 'Compact HT 2 English', entityId: 'sensor.h2c_ht2_humidity', expected: '129' },
@@ -133,6 +139,11 @@ const trayTestCases: TrayTestCase[] = [
133139
// AMS number-first with ht suffix (ams_1_ht_tray_N)
134140
{ name: 'AMS HT number-first suffix Tray 1', entityId: 'sensor.h2c_ams_1_ht_tray_1', expected: { amsNumber: '128', trayNumber: 1 } },
135141

142+
// Compact AMS model tray naming (H2D: ams2_1_tray_1, amsht_1_tray_1)
143+
{ name: 'Compact AMS model H2D Tray 1', entityId: 'sensor.h2d_ams2_1_tray_1', expected: { amsNumber: '1', trayNumber: 1 } },
144+
{ name: 'Compact AMS model H2D Tray 4', entityId: 'sensor.h2d_ams2_1_tray_4', expected: { amsNumber: '1', trayNumber: 4 } },
145+
{ name: 'Compact AMS HT H2D Tray 1', entityId: 'sensor.h2d_amsht_1_tray_1', expected: { amsNumber: '128', trayNumber: 1 } },
146+
136147
// Compact HT naming without _ams_ prefix (e.g., sensor.h2c_ht1_tray_1)
137148
{ name: 'Compact HT 1 Tray 1', entityId: 'sensor.h2c_ht1_tray_1', expected: { amsNumber: '128', trayNumber: 1 } },
138149
{ name: 'Compact HT 1 Slot 2 German', entityId: 'sensor.h2c_ht1_slot_2', expected: { amsNumber: '128', trayNumber: 2 } },
@@ -176,6 +187,8 @@ const buildAmsPatternTestCases: BuildPatternTestCase[] = [
176187
{ name: 'AMS Lite match', prefix: 'a1_mini', entityId: 'sensor.a1_mini_ams_lite_humidity', shouldMatch: true, expectedAmsNumber: 'lite' },
177188
{ name: 'Compact HT match', prefix: 'h2c', entityId: 'sensor.h2c_ht1_humidity', shouldMatch: true, expectedAmsNumber: '128' },
178189
{ name: 'Compact HT 2 match', prefix: 'h2c', entityId: 'sensor.h2c_ht2_humidity', shouldMatch: true, expectedAmsNumber: '129' },
190+
{ name: 'Compact AMS model match', prefix: 'h2d', entityId: 'sensor.h2d_ams2_1_humidity', shouldMatch: true, expectedAmsNumber: '1' },
191+
{ name: 'Compact AMS HT match', prefix: 'h2d', entityId: 'sensor.h2d_amsht_1_humidity', shouldMatch: true, expectedAmsNumber: '128' },
179192
{ name: 'Wrong prefix no match', prefix: 'x1c', entityId: 'sensor.p1s_ams_1_humidity', shouldMatch: false },
180193
];
181194

@@ -206,6 +219,9 @@ const buildTrayPatternTestCases: BuildTrayPatternTestCase[] = [
206219
// Compact HT naming (ht1_tray_N without _ams_)
207220
{ name: 'Compact HT tray match', prefix: 'h2c', amsNumber: '128', trayNum: 1, entityId: 'sensor.h2c_ht1_tray_1', shouldMatch: true },
208221
{ name: 'Compact HT slot match', prefix: 'h2c', amsNumber: '128', trayNum: 2, entityId: 'sensor.h2c_ht1_slot_2', shouldMatch: true },
222+
// Compact AMS model naming (ams2_1_tray_N, amsht_1_tray_N)
223+
{ name: 'Compact AMS model tray match', prefix: 'h2d', amsNumber: '1', trayNum: 1, entityId: 'sensor.h2d_ams2_1_tray_1', shouldMatch: true },
224+
{ name: 'Compact AMS HT tray match', prefix: 'h2d', amsNumber: '128', trayNum: 1, entityId: 'sensor.h2d_amsht_1_tray_1', shouldMatch: true },
209225
// AMS HT should NOT match amsNumber=1
210226
{ name: 'AMS HT should not match amsNumber=1', prefix: 'h2c', amsNumber: '1', trayNum: 1, entityId: 'sensor.h2c_ams_ht_1_tray_1', shouldMatch: false },
211227
{ name: 'AMS Lite no number match', prefix: 'schiller', amsNumber: '1', trayNum: 1, entityId: 'sensor.schiller_ams_tray_1', shouldMatch: true },
@@ -351,8 +367,16 @@ for (const tc of buildAmsPatternTestCases) {
351367
// Group 1 = number (number-first), Group 2 = number (type-first pro)
352368
// Group 3 = "lite" or "ht" (standalone), Group 4 = number (type-first ht)
353369
// Group 5 = number (compact ht without _ams_)
370+
// Group 6 = number (compact AMS model: ams\d+_N_)
371+
// Group 7 = number (compact AMS HT: amsht_N_)
354372
let amsNum: string;
355-
if (match[5]) {
373+
if (match[6]) {
374+
// Compact AMS model: use number directly
375+
amsNum = match[6];
376+
} else if (match[7]) {
377+
// Compact AMS HT: offset by 127
378+
amsNum = String(127 + parseInt(match[7], 10));
379+
} else if (match[5]) {
356380
// Compact HT: offset by 127
357381
amsNum = String(127 + parseInt(match[5], 10));
358382
} else if (match[4]) {
@@ -570,6 +594,8 @@ const externalSpoolTestCases: ExternalSpoolTestCase[] = [
570594
{ name: 'H2C English numbered spool 2', entityId: 'sensor.h2c_externalspool2_external_spool', shouldMatch: true },
571595
{ name: 'H2C German numbered spool 2', entityId: 'sensor.h2c_externalspool2_externe_spule', shouldMatch: true },
572596
{ name: 'H2C underscore numbered spool 2', entityId: 'sensor.h2c_external_spool_2_bobina_esterna', shouldMatch: true },
597+
// H2D external spool with underscore+digit (externalspool_1_)
598+
{ name: 'H2D externalspool_1 English', entityId: 'sensor.h2d_externalspool_1_external_spool', shouldMatch: true },
573599
// Edge cases - should NOT match
574600
{ name: 'Invalid - binary sensor', entityId: 'binary_sensor.x1c_external_spool_actief', shouldMatch: false },
575601
{ name: 'Invalid - no external', entityId: 'sensor.x1c_spool', shouldMatch: false },
@@ -652,6 +678,10 @@ test('getExternalSpoolIndex: English external_spool returns 1', () => {
652678
assertEqual(getExternalSpoolIndex('sensor.x1c_external_spool'), 1);
653679
});
654680

681+
test('getExternalSpoolIndex: H2D externalspool_1 returns 1', () => {
682+
assertEqual(getExternalSpoolIndex('sensor.h2d_externalspool_1_external_spool'), 1);
683+
});
684+
655685
// =============================================================================
656686
// H2C Multi-AMS + AMS HT Collision Tests (GitHub Issue #35)
657687
// =============================================================================
@@ -735,6 +765,116 @@ test('stogs H2C: Compact HT does not collide with regular AMS', () => {
735765
if (ams1 === compactHt) throw new Error('Regular AMS and compact HT should have different numbers');
736766
});
737767

768+
// =============================================================================
769+
// H2D Compact AMS Entity Naming (GitHub Issues #45, #47)
770+
// =============================================================================
771+
772+
console.log('\n=== H2D Compact AMS Entity Naming (GitHub Issues #45, #47) ===\n');
773+
774+
// nickangers' H2D entity list from issue #45
775+
const h2dEntities = [
776+
'sensor.h2d_ams2_1_humidity',
777+
'sensor.h2d_ams2_1_tray_1',
778+
'sensor.h2d_ams2_1_tray_2',
779+
'sensor.h2d_ams2_1_tray_3',
780+
'sensor.h2d_ams2_1_tray_4',
781+
'sensor.h2d_amsht_1_humidity',
782+
'sensor.h2d_amsht_1_tray_1',
783+
'sensor.h2d_amsht_1_tray_2',
784+
'sensor.h2d_amsht_1_tray_3',
785+
'sensor.h2d_amsht_1_tray_4',
786+
'sensor.h2d_externalspool_1_external_spool',
787+
];
788+
789+
test('H2D: Compact AMS 2 humidity detected as AMS 1', () => {
790+
const result = matchAmsHumidityEntity('sensor.h2d_ams2_1_humidity');
791+
assertEqual(result, '1');
792+
});
793+
794+
test('H2D: Compact AMS HT humidity detected as AMS 128', () => {
795+
const result = matchAmsHumidityEntity('sensor.h2d_amsht_1_humidity');
796+
assertEqual(result, '128');
797+
});
798+
799+
test('H2D: All 4 compact AMS 2 trays detected', () => {
800+
for (let i = 1; i <= 4; i++) {
801+
const trayResult = matchTrayEntity(`sensor.h2d_ams2_1_tray_${i}`);
802+
if (!trayResult) throw new Error(`Tray ${i} not detected`);
803+
if (trayResult.amsNumber !== '1') throw new Error(`Expected AMS 1, got ${trayResult.amsNumber}`);
804+
if (trayResult.trayNumber !== i) throw new Error(`Expected tray ${i}, got ${trayResult.trayNumber}`);
805+
}
806+
});
807+
808+
test('H2D: All 4 compact AMS HT trays detected', () => {
809+
for (let i = 1; i <= 4; i++) {
810+
const trayResult = matchTrayEntity(`sensor.h2d_amsht_1_tray_${i}`);
811+
if (!trayResult) throw new Error(`Tray ${i} not detected`);
812+
if (trayResult.amsNumber !== '128') throw new Error(`Expected AMS 128, got ${trayResult.amsNumber}`);
813+
if (trayResult.trayNumber !== i) throw new Error(`Expected tray ${i}, got ${trayResult.trayNumber}`);
814+
}
815+
});
816+
817+
test('H2D: External spool with underscore+digit detected', () => {
818+
const result = matchExternalSpoolEntity('sensor.h2d_externalspool_1_external_spool');
819+
assertEqual(result, true);
820+
});
821+
822+
test('H2D: External spool index extracted correctly', () => {
823+
assertEqual(getExternalSpoolIndex('sensor.h2d_externalspool_1_external_spool'), 1);
824+
});
825+
826+
test('H2D: buildAmsPattern matches compact AMS 2', () => {
827+
const pattern = buildAmsPattern('h2d');
828+
const match = 'sensor.h2d_ams2_1_humidity'.match(pattern);
829+
if (!match) throw new Error('buildAmsPattern should match compact AMS model');
830+
});
831+
832+
test('H2D: buildAmsPattern matches compact AMS HT', () => {
833+
const pattern = buildAmsPattern('h2d');
834+
const match = 'sensor.h2d_amsht_1_humidity'.match(pattern);
835+
if (!match) throw new Error('buildAmsPattern should match compact AMS HT');
836+
});
837+
838+
test('H2D: buildTrayPattern matches compact AMS 2 tray', () => {
839+
const pattern = buildTrayPattern('h2d', '1', 1);
840+
const match = 'sensor.h2d_ams2_1_tray_1'.match(pattern);
841+
if (!match) throw new Error('buildTrayPattern should match compact AMS model tray');
842+
});
843+
844+
test('H2D: buildTrayPattern matches compact AMS HT tray', () => {
845+
const pattern = buildTrayPattern('h2d', '128', 1);
846+
const match = 'sensor.h2d_amsht_1_tray_1'.match(pattern);
847+
if (!match) throw new Error('buildTrayPattern should match compact AMS HT tray');
848+
});
849+
850+
test('H2D: buildExternalSpoolPattern matches H2D format', () => {
851+
const pattern = buildExternalSpoolPattern('h2d');
852+
const match = 'sensor.h2d_externalspool_1_external_spool'.match(pattern);
853+
if (!match) throw new Error('buildExternalSpoolPattern should match H2D external spool');
854+
});
855+
856+
test('H2D: Compact AMS and AMS HT do not collide', () => {
857+
const ams = matchAmsHumidityEntity('sensor.h2d_ams2_1_humidity');
858+
const ht = matchAmsHumidityEntity('sensor.h2d_amsht_1_humidity');
859+
assertEqual(ams, '1');
860+
assertEqual(ht, '128');
861+
if (ams === ht) throw new Error('Compact AMS and AMS HT should have different numbers');
862+
});
863+
864+
test('H2D: All entities from issue #45 are discovered', () => {
865+
let humidityCount = 0;
866+
let trayCount = 0;
867+
let externalCount = 0;
868+
for (const entity of h2dEntities) {
869+
if (matchAmsHumidityEntity(entity)) humidityCount++;
870+
if (matchTrayEntity(entity)) trayCount++;
871+
if (matchExternalSpoolEntity(entity)) externalCount++;
872+
}
873+
assertEqual(humidityCount, 2); // ams2 + amsht
874+
assertEqual(trayCount, 8); // 4 ams2 trays + 4 amsht trays
875+
assertEqual(externalCount, 1);
876+
});
877+
738878
// =============================================================================
739879
// Summary
740880
// =============================================================================

app/src/lib/entity-patterns.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -408,10 +408,11 @@ export function buildAmsPattern(prefix: string): RegExp {
408408
// Group 2: AMS number from type-first format (ams_pro_NUMBER_) — NOT ht, handled by group 4
409409
// Group 3: "lite" or "ht" when using standalone naming
410410
// Group 4: AMS HT number from type-first format (ams_ht_NUMBER_)
411-
// Group 5: entity version suffix (optional)
412411
// Group 5: AMS HT number from compact format without _ams_ (ht_NUMBER_)
413-
// Group 6: entity version suffix (optional)
414-
return new RegExp(`^sensor\\.${prefix}_(?:ams_(?:(\\d+)(?:_(?:pro|ht))?_|(?:pro)_(\\d+)_|(lite|ht)_|ht_(\\d+)_)?|ht(\\d+)_)(?:${names})(?:_(\\d+))?$`);
412+
// Group 6: AMS unit number from compact AMS model naming (ams\d+_NUMBER_)
413+
// Group 7: AMS HT unit number from compact AMS HT naming (amsht_NUMBER_)
414+
// Group 8: entity version suffix (optional)
415+
return new RegExp(`^sensor\\.${prefix}_(?:ams_(?:(\\d+)(?:_(?:pro|ht))?_|(?:pro)_(\\d+)_|(lite|ht)_|ht_(\\d+)_)?|ht(\\d+)_|ams\\d+_(\\d+)_|amsht_(\\d+)_)(?:${names})(?:_(\\d+))?$`);
415416
}
416417

417418
/**
@@ -435,17 +436,17 @@ export function buildTrayPattern(prefix: string, amsNumber: string, trayNum: num
435436
// e.g., ams_128_tray_1, ams_128_ht_tray_1, ams_ht_1_tray_1, ams_ht_tray_1 (when 128)
436437
if (numAms >= 128) {
437438
const htNum = numAms - 127; // 128→1, 129→2, etc.
438-
// Match: ams_128_tray_N, ams_128_ht_tray_N, ams_ht_1_tray_N, ams_ht_tray_N (standalone), ht1_tray_N (compact)
439-
return new RegExp(`^sensor\\.${prefix}_(?:ams_(?:${amsNumber}(?:_ht)?_|ht_${htNum}_|ht_)|ht${htNum}_)(?:${names})_${trayNum}(?:_(\\d+))?$`);
439+
// Match: ams_128_tray_N, ams_128_ht_tray_N, ams_ht_1_tray_N, ams_ht_tray_N (standalone), ht1_tray_N (compact), amsht_1_tray_N (compact)
440+
return new RegExp(`^sensor\\.${prefix}_(?:ams_(?:${amsNumber}(?:_ht)?_|ht_${htNum}_|ht_)|ht${htNum}_|amsht_${htNum}_)(?:${names})_${trayNum}(?:_(\\d+))?$`);
440441
}
441442

442443
// For A1 with AMS Lite (amsNumber="1"), also match entities without explicit AMS number
443444
// e.g., "sensor.schiller_ams_tray_1" in addition to "sensor.schiller_ams_1_tray_1"
444445
// Note: "ht" is NOT included in type alternatives — AMS HT is handled above
445446
if (amsNumber === '1') {
446-
return new RegExp(`^sensor\\.${prefix}_ams_(?:1(?:_(?:pro))?_|(?:pro)_1_)?(?:${names})_${trayNum}(?:_(\\d+))?$`);
447+
return new RegExp(`^sensor\\.${prefix}_(?:ams_(?:1(?:_(?:pro))?_|(?:pro)_1_)?|ams\\d+_1_)(?:${names})_${trayNum}(?:_(\\d+))?$`);
447448
}
448-
return new RegExp(`^sensor\\.${prefix}_ams_(?:${amsNumber}(?:_(?:pro))?|(?:pro)_${amsNumber})_(?:${names})_${trayNum}(?:_(\\d+))?$`);
449+
return new RegExp(`^sensor\\.${prefix}_(?:ams_(?:${amsNumber}(?:_(?:pro))?|(?:pro)_${amsNumber})|ams\\d+_${amsNumber})_(?:${names})_${trayNum}(?:_(\\d+))?$`);
449450
}
450451

451452
/**
@@ -461,8 +462,9 @@ export function buildExternalSpoolPattern(prefix: string): RegExp {
461462
// Transform names to handle numbered external spool prefixes
462463
const namePatterns = EXTERNAL_SPOOL_NAMES.map(name => {
463464
// Add optional digit(s) after common prefix patterns
465+
// Supports: externalspool_ (no number), externalspool2_ (H2C), externalspool_1_ (H2D)
464466
return name
465-
.replace(/^externalspool_/, 'externalspool\\d*_')
467+
.replace(/^externalspool_/, 'externalspool(?:\\d+|_\\d+)?_')
466468
.replace(/^external_spool_/, 'external_spool_?\\d*_?');
467469
});
468470
// Deduplicate patterns that became identical after transformation
@@ -596,6 +598,20 @@ export function getLocalizedEntityName(
596598
export function matchAmsHumidityEntity(entityId: string): string | null {
597599
const names = AMS_HUMIDITY_NAMES.join('|');
598600

601+
// Check for compact AMS model naming (e.g., sensor.h2d_ams2_1_humidity)
602+
const compactAmsPattern = new RegExp(`^sensor\\.(?:.+_)?ams\\d+_(\\d+)_(?:${names})(?:_\\d+)?$`);
603+
const compactAmsMatch = entityId.match(compactAmsPattern);
604+
if (compactAmsMatch) {
605+
return compactAmsMatch[1];
606+
}
607+
608+
// Check for compact AMS HT naming (e.g., sensor.h2d_amsht_1_humidity)
609+
const compactAmsHtPattern = new RegExp(`^sensor\\.(?:.+_)?amsht_(\\d+)_(?:${names})(?:_\\d+)?$`);
610+
const compactAmsHtMatch = entityId.match(compactAmsHtPattern);
611+
if (compactAmsHtMatch) {
612+
return String(127 + parseInt(compactAmsHtMatch[1], 10));
613+
}
614+
599615
// Check for compact HT naming without _ams_ prefix (e.g., sensor.h2c_ht1_humidity)
600616
const compactHtPattern = new RegExp(`^sensor\\.(?:.+_)?ht(\\d+)_(?:${names})(?:_\\d+)?$`);
601617
const compactHtMatch = entityId.match(compactHtPattern);
@@ -654,6 +670,26 @@ export function matchAmsHumidityEntity(entityId: string): string | null {
654670
export function matchTrayEntity(entityId: string): { amsNumber: string; trayNumber: number } | null {
655671
const names = TRAY_NAMES.join('|');
656672

673+
// Check for compact AMS model naming (e.g., sensor.h2d_ams2_1_tray_1)
674+
const compactAmsTrayPattern = new RegExp(`^sensor\\.(?:.+_)?ams\\d+_(\\d+)_(?:${names})_(\\d+)(?:_\\d+)?$`);
675+
const compactAmsTrayMatch = entityId.match(compactAmsTrayPattern);
676+
if (compactAmsTrayMatch) {
677+
return {
678+
amsNumber: compactAmsTrayMatch[1],
679+
trayNumber: parseInt(compactAmsTrayMatch[2], 10),
680+
};
681+
}
682+
683+
// Check for compact AMS HT naming (e.g., sensor.h2d_amsht_1_tray_1)
684+
const compactAmsHtTrayPattern = new RegExp(`^sensor\\.(?:.+_)?amsht_(\\d+)_(?:${names})_(\\d+)(?:_\\d+)?$`);
685+
const compactAmsHtTrayMatch = entityId.match(compactAmsHtTrayPattern);
686+
if (compactAmsHtTrayMatch) {
687+
return {
688+
amsNumber: String(127 + parseInt(compactAmsHtTrayMatch[1], 10)),
689+
trayNumber: parseInt(compactAmsHtTrayMatch[2], 10),
690+
};
691+
}
692+
657693
// Check for compact HT naming without _ams_ prefix (e.g., sensor.h2c_ht1_tray_1)
658694
const compactHtTrayPattern = new RegExp(`^sensor\\.(?:.+_)?ht(\\d+)_(?:${names})_(\\d+)(?:_\\d+)?$`);
659695
const compactHtTrayMatch = entityId.match(compactHtTrayPattern);
@@ -721,7 +757,7 @@ export function matchTrayEntity(entityId: string): { amsNumber: string; trayNumb
721757
export function matchExternalSpoolEntity(entityId: string): boolean {
722758
const namePatterns = EXTERNAL_SPOOL_NAMES.map(name => {
723759
return name
724-
.replace(/^externalspool_/, 'externalspool\\d*_')
760+
.replace(/^externalspool_/, 'externalspool(?:\\d+|_\\d+)?_')
725761
.replace(/^external_spool_/, 'external_spool_?\\d*_?');
726762
});
727763
const uniquePatterns = [...new Set(namePatterns)];
@@ -743,6 +779,11 @@ export function getExternalSpoolIndex(entityId: string): number {
743779
if (numberedMatch) {
744780
return parseInt(numberedMatch[1], 10);
745781
}
782+
// Check for H2D-style: externalspool_1_, externalspool_2_
783+
const underscoreNumberedMatch = entityId.match(/_externalspool_(\d+)_/);
784+
if (underscoreNumberedMatch) {
785+
return parseInt(underscoreNumberedMatch[1], 10);
786+
}
746787
// Check for numbered underscore pattern: external_spool_2_, etc.
747788
const underscoredMatch = entityId.match(/_external_spool_(\d+)_/);
748789
if (underscoredMatch) {

0 commit comments

Comments
 (0)