feat: add per-room temperature thresholds

Issue: #487
User-Visible: yes
This commit is contained in:
Matysh
2026-09-08 10:44:13 +03:00
parent d2d9b1ac5d
commit e2751062dc
73 changed files with 1465 additions and 864 deletions
+1 -1
View File
@@ -441,7 +441,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
assert.ok(scenario);
const fixture = prepareGoldenFixture(scenario);
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
assert.equal(GOLDEN_MATRIX_VERSION, 60);
assert.equal(GOLDEN_MATRIX_VERSION, 61);
assert.equal(space.settings.sun_rays, true);
assert.equal(scenario.northDeg, 90,
'the sign-sensitive golden must keep a non-zero north direction');
+1 -1
View File
@@ -94,7 +94,7 @@ test('every i18n key has a literal, dynamic-family or derived help consumer', ()
});
test('help accessibility copy is derived from every literal help consumer', () => {
assert.equal(derivedHelpAria.size, 19, 'the current settings surface has 19 help descriptions');
assert.equal(derivedHelpAria.size, 20, 'the current settings surface has 20 help descriptions');
for (const key of derivedHelpAria) {
assert.equal(typeof dictionary[key], 'string', `${key} must accompany its .help consumer`);
}
+18 -1
View File
@@ -12,7 +12,7 @@ import {
alignGuides, segmentAngle, is45,
swipeTarget, clampScale,
migratePdfUrls,
roomFillModeOf, roomGlowOf, customFillOf, roomCustomFillOf,
roomFillModeOf, roomGlowOf, customFillOf, roomCustomFillOf, roomTempRangeOf,
contentUrl, chunk, referencedContentUrls, MAX_SIGN_PATHS,
interiorPoint,
segmentCm, formatLength, roomEdges, roomPoly, paperRoomShapes, pointOnBoundary, pointStrictlyInside, roomsOverlap,
@@ -627,6 +627,23 @@ test('custom fill projection is safe and follows room -> space -> default inheri
{ c: '#445566', a: 0.7 });
});
test('room temperature range inherits per side and normalises the effective pair (#487)', () => {
assert.deepEqual(roomTempRangeOf(20, 25, null), { min: 20, max: 25 });
assert.deepEqual(roomTempRangeOf(20, 25, { settings: { temp_min: 19, temp_max: 24 } }),
{ min: 19, max: 24 });
assert.deepEqual(roomTempRangeOf(20, 25, { settings: { temp_min: 21 } }),
{ min: 21, max: 25 });
assert.deepEqual(roomTempRangeOf(20, 25, { settings: { temp_max: 19 } }),
{ min: 19, max: 20 });
assert.deepEqual(roomTempRangeOf(20, 25, { settings: { temp_min: 0, temp_max: null } }),
{ min: 0, max: 25 });
assert.deepEqual(roomTempRangeOf(20, 25, { settings: { temp_min: 28, temp_max: 18 } }),
{ min: 18, max: 28 });
assert.deepEqual(roomTempRangeOf(20, 25, { settings: { temp_min: '18', temp_max: Infinity } }),
{ min: 20, max: 25 });
assert.deepEqual(roomTempRangeOf(Number.NaN, Infinity, {}), { min: 20, max: 25 });
});
test('spaceDisplayOf: show_lqi tri-state (null = follow the card option)', () => {
assert.equal(spaceDisplayOf({}).showLqi, null);
assert.equal(spaceDisplayOf({ settings: { show_lqi: false } }).showLqi, false);
+15
View File
@@ -0,0 +1,15 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const read = (name) => readFileSync(new URL(`../src/${name}`, import.meta.url), 'utf8');
test('#487 full and static renderers consume one effective room temperature range', () => {
const full = read('houseplan-card.ts');
const staticCard = read('space-render.ts');
assert.match(full, /const tempRange = roomTempRangeFromDraft\(/);
assert.match(full, /tempRange\.min,\s*tempRange\.max,/);
assert.match(staticCard, /const tempRange = roomTempRangeOf\(/);
assert.match(staticCard, /tempRange\.min,\s*tempRange\.max,/);
assert.doesNotMatch(staticCard, /fill === 'temp'[\s\S]{0,180}disp\.tempMin,[\s\n ]*disp\.tempMax/);
});
+30
View File
@@ -7,8 +7,38 @@ import {
initialSpaceDisplayDraft,
switchSpacePlanSource,
touchSpaceDisplay,
applyRoomTempThresholdDraft,
roomTempRangeFromDraft,
roomTempThresholdDraft,
roomTempThresholdInputValues,
} from '../test-build/space-dialog.js';
test('room temperature draft distinguishes inheritance, zero, reversal and invalid text (#487)', () => {
assert.deepEqual(roomTempThresholdDraft('', ' '), { valid: true, min: null, max: null });
assert.deepEqual(roomTempThresholdDraft('0', '25'), { valid: true, min: 0, max: 25 });
assert.deepEqual(roomTempThresholdDraft('24,5', '18,5'), { valid: true, min: 18.5, max: 24.5 });
assert.deepEqual(roomTempThresholdDraft('', '19'), { valid: true, min: null, max: 19 });
assert.deepEqual(roomTempThresholdDraft('oops', '25'), { valid: false, min: null, max: 25 });
});
test('room temperature settings writer preserves unknown fields and removes only cleared sides (#487)', () => {
const settings = { future: 'keep', temp_min: 18, temp_max: 24 };
assert.equal(applyRoomTempThresholdDraft(settings, '', '0'), true);
assert.deepEqual(settings, { future: 'keep', temp_max: 0 });
assert.deepEqual(roomTempThresholdInputValues(settings), ['', '0']);
assert.equal(applyRoomTempThresholdDraft(settings, 'bad', '25'), false);
assert.deepEqual(settings, { future: 'keep', temp_max: 0 });
assert.deepEqual(roomTempThresholdInputValues({ temp_min: Infinity, temp_max: '20' }), ['', '']);
});
test('room temperature preview uses valid drafts and falls back on invalid drafts (#487)', () => {
const room = { settings: { temp_min: 18, temp_max: 22 } };
assert.deepEqual(roomTempRangeFromDraft(20, 25, room, '19', '21', true), { min: 19, max: 21 });
assert.deepEqual(roomTempRangeFromDraft(20, 25, room, 'bad', '21', true), { min: 18, max: 22 });
assert.deepEqual(roomTempRangeFromDraft(20, 25, room, '', '19', true), { min: 19, max: 20 });
assert.deepEqual(roomTempRangeFromDraft(20, 25, room, '19', '21', false), { min: 18, max: 22 });
});
test('a fresh space is a complete empty v8/v9 wall-model document (#324)', () => {
assert.deepEqual(createEmptySpaceConfig('floor', 'Ground floor'), {
id: 'floor',