Skip to content

Commit 5d91bf3

Browse files
committed
Add compact HT entity naming pattern without _ams_ prefix (#35)
Support H2C printers where ha-bambulab generates AMS HT entities using compact format (e.g., sensor.h2c_ht1_humidity, sensor.h2c_ht1_tray_1) instead of the standard sensor.h2c_ams_ht_1_humidity pattern.
1 parent db27c44 commit 5d91bf3

File tree

2 files changed

+91
-4
lines changed

2 files changed

+91
-4
lines changed

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ 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 HT naming without _ams_ prefix (e.g., sensor.h2c_ht1_humidity)
75+
{ name: 'Compact HT 1 English', entityId: 'sensor.h2c_ht1_humidity', expected: '128' },
76+
{ name: 'Compact HT 2 English', entityId: 'sensor.h2c_ht2_humidity', expected: '129' },
77+
{ name: 'Compact HT 1 Italian', entityId: 'sensor.h2c_ht1_umidita', expected: '128' },
78+
7479
// Renamed entities with no printer prefix (GitHub Issue #9 comment)
7580
{ name: 'No prefix - ams_humidity', entityId: 'sensor.ams_humidity', expected: '1' },
7681
{ name: 'No prefix - ams_1_humidity', entityId: 'sensor.ams_1_humidity', expected: '1' },
@@ -128,6 +133,11 @@ const trayTestCases: TrayTestCase[] = [
128133
// AMS number-first with ht suffix (ams_1_ht_tray_N)
129134
{ name: 'AMS HT number-first suffix Tray 1', entityId: 'sensor.h2c_ams_1_ht_tray_1', expected: { amsNumber: '128', trayNumber: 1 } },
130135

136+
// Compact HT naming without _ams_ prefix (e.g., sensor.h2c_ht1_tray_1)
137+
{ name: 'Compact HT 1 Tray 1', entityId: 'sensor.h2c_ht1_tray_1', expected: { amsNumber: '128', trayNumber: 1 } },
138+
{ name: 'Compact HT 1 Slot 2 German', entityId: 'sensor.h2c_ht1_slot_2', expected: { amsNumber: '128', trayNumber: 2 } },
139+
{ name: 'Compact HT 2 Tray 1', entityId: 'sensor.h2c_ht2_tray_1', expected: { amsNumber: '129', trayNumber: 1 } },
140+
131141
// Renamed entities with no printer prefix (GitHub Issue #9 comment)
132142
{ name: 'No prefix - ams_tray_1', entityId: 'sensor.ams_tray_1', expected: { amsNumber: '1', trayNumber: 1 } },
133143
{ name: 'No prefix - ams_tray_4', entityId: 'sensor.ams_tray_4', expected: { amsNumber: '1', trayNumber: 4 } },
@@ -164,6 +174,8 @@ const buildAmsPatternTestCases: BuildPatternTestCase[] = [
164174
{ name: 'AMS HT type-first match', prefix: 'h2c', entityId: 'sensor.h2c_ams_ht_1_humidity', shouldMatch: true, expectedAmsNumber: '128' },
165175
{ name: 'AMS HT type-first #2', prefix: 'h2c', entityId: 'sensor.h2c_ams_ht_2_humidity', shouldMatch: true, expectedAmsNumber: '129' },
166176
{ name: 'AMS Lite match', prefix: 'a1_mini', entityId: 'sensor.a1_mini_ams_lite_humidity', shouldMatch: true, expectedAmsNumber: 'lite' },
177+
{ name: 'Compact HT match', prefix: 'h2c', entityId: 'sensor.h2c_ht1_humidity', shouldMatch: true, expectedAmsNumber: '128' },
178+
{ name: 'Compact HT 2 match', prefix: 'h2c', entityId: 'sensor.h2c_ht2_humidity', shouldMatch: true, expectedAmsNumber: '129' },
167179
{ name: 'Wrong prefix no match', prefix: 'x1c', entityId: 'sensor.p1s_ams_1_humidity', shouldMatch: false },
168180
];
169181

@@ -191,6 +203,9 @@ const buildTrayPatternTestCases: BuildTrayPatternTestCase[] = [
191203
{ name: 'AMS HT tray match (ams_ht_1)', prefix: 'h2c', amsNumber: '128', trayNum: 1, entityId: 'sensor.h2c_ams_ht_1_tray_1', shouldMatch: true },
192204
{ name: 'AMS HT tray match (ams_ht standalone)', prefix: 'a1_mini', amsNumber: '128', trayNum: 1, entityId: 'sensor.a1_mini_ams_ht_tray_1', shouldMatch: true },
193205
{ name: 'AMS HT slot match (ams_ht_1)', prefix: 'h2c', amsNumber: '128', trayNum: 2, entityId: 'sensor.h2c_ams_ht_1_slot_2', shouldMatch: true },
206+
// Compact HT naming (ht1_tray_N without _ams_)
207+
{ name: 'Compact HT tray match', prefix: 'h2c', amsNumber: '128', trayNum: 1, entityId: 'sensor.h2c_ht1_tray_1', shouldMatch: true },
208+
{ name: 'Compact HT slot match', prefix: 'h2c', amsNumber: '128', trayNum: 2, entityId: 'sensor.h2c_ht1_slot_2', shouldMatch: true },
194209
// AMS HT should NOT match amsNumber=1
195210
{ name: 'AMS HT should not match amsNumber=1', prefix: 'h2c', amsNumber: '1', trayNum: 1, entityId: 'sensor.h2c_ams_ht_1_tray_1', shouldMatch: false },
196211
{ name: 'AMS Lite no number match', prefix: 'schiller', amsNumber: '1', trayNum: 1, entityId: 'sensor.schiller_ams_tray_1', shouldMatch: true },
@@ -335,8 +350,12 @@ for (const tc of buildAmsPatternTestCases) {
335350
if (tc.expectedAmsNumber) {
336351
// Group 1 = number (number-first), Group 2 = number (type-first pro)
337352
// Group 3 = "lite" or "ht" (standalone), Group 4 = number (type-first ht)
353+
// Group 5 = number (compact ht without _ams_)
338354
let amsNum: string;
339-
if (match[4]) {
355+
if (match[5]) {
356+
// Compact HT: offset by 127
357+
amsNum = String(127 + parseInt(match[5], 10));
358+
} else if (match[4]) {
340359
// HT type-first: offset by 127
341360
amsNum = String(127 + parseInt(match[4], 10));
342361
} else if (match[3] === 'ht') {
@@ -667,6 +686,55 @@ test('H2C: AMS HT trays SHOULD match when querying for amsNumber 128', () => {
667686
if (!match) throw new Error('AMS HT tray should match when querying for amsNumber="128"');
668687
});
669688

689+
// =============================================================================
690+
// H2C Compact HT Entity Naming (GitHub Issue #35 - stogs)
691+
// =============================================================================
692+
693+
console.log('\n=== H2C Compact HT Entity Naming (stogs) ===\n');
694+
695+
// stogs' H2C uses compact HT naming: ht1_ instead of ams_ht_1_ or ams_128_
696+
const stogsEntities = [
697+
'sensor.h2c_ht1_humidity',
698+
'sensor.h2c_ht1_tray_1',
699+
'sensor.h2c_ht1_tray_2',
700+
'sensor.h2c_ht1_tray_3',
701+
'sensor.h2c_ht1_tray_4',
702+
];
703+
704+
test('stogs H2C: Compact HT humidity sensor detected as AMS 128', () => {
705+
const result = matchAmsHumidityEntity('sensor.h2c_ht1_humidity');
706+
assertEqual(result, '128');
707+
});
708+
709+
test('stogs H2C: All 4 compact HT trays detected', () => {
710+
for (let i = 1; i <= 4; i++) {
711+
const trayResult = matchTrayEntity(`sensor.h2c_ht1_tray_${i}`);
712+
if (!trayResult) throw new Error(`Tray ${i} not detected`);
713+
if (trayResult.amsNumber !== '128') throw new Error(`Expected AMS 128, got ${trayResult.amsNumber}`);
714+
if (trayResult.trayNumber !== i) throw new Error(`Expected tray ${i}, got ${trayResult.trayNumber}`);
715+
}
716+
});
717+
718+
test('stogs H2C: buildAmsPattern matches compact HT', () => {
719+
const pattern = buildAmsPattern('h2c');
720+
const match = 'sensor.h2c_ht1_humidity'.match(pattern);
721+
if (!match) throw new Error('buildAmsPattern should match compact HT');
722+
});
723+
724+
test('stogs H2C: buildTrayPattern matches compact HT tray', () => {
725+
const pattern = buildTrayPattern('h2c', '128', 1);
726+
const match = 'sensor.h2c_ht1_tray_1'.match(pattern);
727+
if (!match) throw new Error('buildTrayPattern should match compact HT tray');
728+
});
729+
730+
test('stogs H2C: Compact HT does not collide with regular AMS', () => {
731+
const ams1 = matchAmsHumidityEntity('sensor.h2c_ams_1_humidity');
732+
const compactHt = matchAmsHumidityEntity('sensor.h2c_ht1_humidity');
733+
assertEqual(ams1, '1');
734+
assertEqual(compactHt, '128');
735+
if (ams1 === compactHt) throw new Error('Regular AMS and compact HT should have different numbers');
736+
});
737+
670738
// =============================================================================
671739
// Summary
672740
// =============================================================================

app/src/lib/entity-patterns.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,9 @@ export function buildAmsPattern(prefix: string): RegExp {
409409
// Group 3: "lite" or "ht" when using standalone naming
410410
// Group 4: AMS HT number from type-first format (ams_ht_NUMBER_)
411411
// Group 5: entity version suffix (optional)
412-
return new RegExp(`^sensor\\.${prefix}_ams_(?:(\\d+)(?:_(?:pro|ht))?_|(?:pro)_(\\d+)_|(lite|ht)_|ht_(\\d+)_)?(?:${names})(?:_(\\d+))?$`);
412+
// 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+))?$`);
413415
}
414416

415417
/**
@@ -433,8 +435,8 @@ export function buildTrayPattern(prefix: string, amsNumber: string, trayNum: num
433435
// e.g., ams_128_tray_1, ams_128_ht_tray_1, ams_ht_1_tray_1, ams_ht_tray_1 (when 128)
434436
if (numAms >= 128) {
435437
const htNum = numAms - 127; // 128→1, 129→2, etc.
436-
// Match: ams_128_tray_N, ams_128_ht_tray_N, ams_ht_1_tray_N, ams_ht_tray_N (standalone)
437-
return new RegExp(`^sensor\\.${prefix}_ams_(?:${amsNumber}(?:_ht)?_|ht_${htNum}_|ht_)(?:${names})_${trayNum}(?:_(\\d+))?$`);
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+))?$`);
438440
}
439441

440442
// For A1 with AMS Lite (amsNumber="1"), also match entities without explicit AMS number
@@ -594,6 +596,13 @@ export function getLocalizedEntityName(
594596
export function matchAmsHumidityEntity(entityId: string): string | null {
595597
const names = AMS_HUMIDITY_NAMES.join('|');
596598

599+
// Check for compact HT naming without _ams_ prefix (e.g., sensor.h2c_ht1_humidity)
600+
const compactHtPattern = new RegExp(`^sensor\\.(?:.+_)?ht(\\d+)_(?:${names})(?:_\\d+)?$`);
601+
const compactHtMatch = entityId.match(compactHtPattern);
602+
if (compactHtMatch) {
603+
return String(127 + parseInt(compactHtMatch[1], 10));
604+
}
605+
597606
// Check for type-first "ams_ht_N_humidity" pattern (e.g., ams_ht_1_umidita)
598607
// This MUST be checked before the generic pattern to avoid collision with regular AMS numbers
599608
const htNumberedPattern = new RegExp(`^sensor\\.(?:.+_)?ams_ht_(\\d+)_(?:${names})(?:_\\d+)?$`);
@@ -645,6 +654,16 @@ export function matchAmsHumidityEntity(entityId: string): string | null {
645654
export function matchTrayEntity(entityId: string): { amsNumber: string; trayNumber: number } | null {
646655
const names = TRAY_NAMES.join('|');
647656

657+
// Check for compact HT naming without _ams_ prefix (e.g., sensor.h2c_ht1_tray_1)
658+
const compactHtTrayPattern = new RegExp(`^sensor\\.(?:.+_)?ht(\\d+)_(?:${names})_(\\d+)(?:_\\d+)?$`);
659+
const compactHtTrayMatch = entityId.match(compactHtTrayPattern);
660+
if (compactHtTrayMatch) {
661+
return {
662+
amsNumber: String(127 + parseInt(compactHtTrayMatch[1], 10)),
663+
trayNumber: parseInt(compactHtTrayMatch[2], 10),
664+
};
665+
}
666+
648667
// Check for type-first "ams_ht_N_tray_M" pattern (e.g., ams_ht_1_slot_1)
649668
// This MUST be checked before the generic pattern to avoid collision with regular AMS numbers
650669
const htNumberedPattern = new RegExp(`^sensor\\.(?:.+_)?ams_ht_(\\d+)_(?:${names})_(\\d+)(?:_\\d+)?$`);

0 commit comments

Comments
 (0)