mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
fix: seven small honesty fixes from the 29.08 audit (#369)
(a) documented: deleting a vacuum marker erases its server trail at once and a re-added marker starts from scratch (VACUUM.md + both USER-GUIDEs). (b) smoothVacPath reports dropped non-finite segments — one console warn per call with the count — instead of hiding the whole trail silently on a broken calibration matrix. (c) room climate (#317) now reaches legacy markers whose exported config carries an ABSENT area key rather than an explicit null: `== null` where the placement is decided. (d) the armed furniture preview follows Shift without mouse movement — window keydown/keyup listeners live exactly as long as the palette is armed, detached at every palette teardown. (e) only the primary mouse button places decor/furniture: a right or middle click with an armed tool is a no-op, touch/pen untouched. (f) a device whose registry entities were ALL deliberately disabled by the user no longer glows as an alive controller — the #318 entityless-active rule now requires a genuinely empty roster. (g) furniture-pack author corrected to Sergey Matyunin (Сергей Матюнин) per the owner's decision — LICENSE.md, README.md, pack.json, docs/FURNITURE.md, the provenance check in generate-furniture-assets and its unit; the source archive bytes are unchanged and the README notes the romanisation fix. Proofs: units for (b)/(c)/(f) including the #318 regression pair; new smoke_furniture_polish for (d)/(e) with listener add/remove counters and both mouse buttons; five registry mutants, one per code change. Issue: #369 User-Visible: yes
This commit is contained in:
@@ -1189,3 +1189,48 @@ test('an incidental cover does not hijack another whole-device role on info tap'
|
||||
assert.equal(result.sourceKind, 'device_role');
|
||||
assert.deepEqual(result.visualSources.map((source) => source.eid), ['media_player.screen']);
|
||||
});
|
||||
|
||||
test('issue 369г: устройство с полностью отключёнными сущностями не выглядит живым', () => {
|
||||
const h = hass({
|
||||
'light.wall_group': state('light.wall_group', 'on', { friendly_name: 'Wall lights' }),
|
||||
}, {
|
||||
'light.wall_group': {
|
||||
entity_id: 'light.wall_group', device_id: 'lights', platform: 'demo',
|
||||
},
|
||||
});
|
||||
// (i) истинно безростерный active-контроллер — по-прежнему available (#318)
|
||||
const bare = device({
|
||||
id: 'bare', name: 'Bare controller', entities: [], primary: null,
|
||||
bindingRef: 'bare',
|
||||
bindingStatus: { kind: 'active', enabledEntityIds: [], allEntityIds: [] },
|
||||
marker: { id: 'bare', binding: 'device:bare', tap_action: 'toggle', controls: ['light.wall_group'] },
|
||||
});
|
||||
assert.equal(resolveDevicePresentation(h, bare, options).visual.availability, 'available');
|
||||
// (ii) реестр непуст, но всё отключено пользователем — НЕ available
|
||||
const optedOut = device({
|
||||
id: 'opted', name: 'Opted-out device', entities: [], primary: null,
|
||||
bindingRef: 'opted',
|
||||
bindingStatus: {
|
||||
kind: 'active', enabledEntityIds: [], allEntityIds: ['switch.opted_main'],
|
||||
},
|
||||
marker: { id: 'opted', binding: 'device:opted', tap_action: 'toggle', controls: ['light.wall_group'] },
|
||||
});
|
||||
assert.notEqual(
|
||||
resolveDevicePresentation(h, optedOut, options).visual.availability, 'available',
|
||||
'ростер, сознательно выключенный пользователем, — не свидетельство жизни',
|
||||
);
|
||||
// (iii) ha_disabled — как и раньше, не available
|
||||
const disabled = device({
|
||||
id: 'disabled', name: 'Disabled device', entities: [], primary: null,
|
||||
bindingRef: 'disabled',
|
||||
bindingStatus: {
|
||||
kind: 'ha_disabled', reason: 'device_disabled',
|
||||
enabledEntityIds: [], allEntityIds: ['switch.d_main'],
|
||||
},
|
||||
marker: { id: 'disabled', binding: 'device:disabled', tap_action: 'toggle', controls: ['light.wall_group'] },
|
||||
});
|
||||
// ha_disabled гасится на уровне lifecycle: маркер скрыт и нейтрален целиком
|
||||
const disabledResult = resolveDevicePresentation(h, disabled, options);
|
||||
assert.equal(disabledResult.effectiveHidden, true);
|
||||
assert.equal(disabledResult.visual.status, 'neutral');
|
||||
});
|
||||
|
||||
@@ -2120,3 +2120,26 @@ test('HA-disabled marker is built only as an inert service ghost', () => {
|
||||
assert.deepEqual(ghost.entities, [], 'stale state cannot feed any runtime consumer');
|
||||
assert.deepEqual(ghost.allEntities, ['switch.valve']);
|
||||
});
|
||||
|
||||
test('issue 369в: легаси-маркер без ключа area размещается в комнате как с явным null', () => {
|
||||
const hass = {
|
||||
devices: { legacy: { id: 'legacy', name: 'Legacy thermometer', area_id: null } },
|
||||
entities: { 'sensor.legacy_temperature': { device_id: 'legacy', platform: 'demo' } },
|
||||
states: {
|
||||
'sensor.legacy_temperature': { state: '19', attributes: { device_class: 'temperature' } },
|
||||
},
|
||||
};
|
||||
// ключ area ОТСУТСТВУЕТ (экспорт/легаси), а не явный null
|
||||
const absent = [{ id: 'legacy', binding: 'device:legacy', space: 'f1', room_id: 'shed' }];
|
||||
const map = roomClimateMap(hass, undefined, absent);
|
||||
assert.deepEqual(map.get('@room/f1/shed'), { temp: 19, hum: null },
|
||||
'undefined area == null area: комната получает голос');
|
||||
// явный null даёт тот же ключ (регресс #317)
|
||||
const explicit = [{ id: 'legacy', binding: 'device:legacy', space: 'f1', room_id: 'shed', area: null }];
|
||||
assert.deepEqual(roomClimateMap(hass, undefined, explicit).get('@room/f1/shed'),
|
||||
{ temp: 19, hum: null });
|
||||
// строковая area по-прежнему побеждает (регресс-защита)
|
||||
const areaMarker = [{ id: 'legacy', binding: 'device:legacy', space: 'f1', room_id: 'shed', area: 'kitchen' }];
|
||||
assert.deepEqual(roomClimateMap(hass, undefined, areaMarker).get('kitchen'),
|
||||
{ temp: 19, hum: null });
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ test('front-view menu artwork is reachable only through the lazy editor graph',
|
||||
});
|
||||
|
||||
test('release provenance is normalized to the repository MIT grant', () => {
|
||||
assert.equal(MANIFEST.author, 'Sergey Matyushin (Matysh)');
|
||||
assert.equal(MANIFEST.author, 'Sergey Matyunin (Matysh)');
|
||||
assert.equal(MANIFEST.license, 'MIT');
|
||||
const readme = fs.readFileSync(
|
||||
path.join(ROOT, 'assets', 'furniture', 'houseplan-0.3.0', 'README.md'), 'utf8');
|
||||
|
||||
@@ -458,3 +458,21 @@ test('readVacTelemetry keeps numeric zero map_index as map id (HP-1540-02)', ()
|
||||
const t = readVacTelemetry({ vacuum_position: { x: 1, y: 2 }, map_index: 0 });
|
||||
assert.equal(t.mapId, '0');
|
||||
});
|
||||
|
||||
test('smoothVacPath предупреждает один раз о нефинитных сегментах (#369б)', () => {
|
||||
const warnings = [];
|
||||
const warn = (message) => warnings.push(message);
|
||||
const bad = [
|
||||
[[0, 0], [NaN, 10], [20, 20]],
|
||||
[[0, 0], [10, Infinity]],
|
||||
[[0, 0], [10, 10], [20, 0]],
|
||||
];
|
||||
const out = smoothVacPath(bad, 17.5, warn);
|
||||
assert.equal(out.length, 1, 'валидный сегмент выжил');
|
||||
assert.equal(warnings.length, 1, 'один warn на вызов, не на сегмент');
|
||||
assert.match(warnings[0], /2 segment/);
|
||||
assert.match(warnings[0], /non-finite/);
|
||||
const clean = smoothVacPath([[[0, 0], [10, 10]]], 17.5, warn);
|
||||
assert.equal(clean.length, 1);
|
||||
assert.equal(warnings.length, 1, 'чистый путь не шумит');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user