mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
fix: support registryless opening entities
Issue: #117 User-Visible: yes
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,163 @@
|
||||
// #117: exact YAML contact/lock references without Entity Registry rows must
|
||||
// survive the immutable render projection without weakening disabled/security rules.
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch();
|
||||
const out = await page.evaluate(async () => {
|
||||
const result = {};
|
||||
const card = window.__card;
|
||||
const root = () => card.shadowRoot || card.renderRoot;
|
||||
const settle = async () => {
|
||||
card.requestUpdate();
|
||||
await card.updateComplete;
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
};
|
||||
const contactId = 'binary_sensor.hp117_yaml_contact';
|
||||
const lockId = 'lock.hp117_yaml_lock';
|
||||
const space = card._serverCfg.spaces.find((item) => item.id === 'f1');
|
||||
space.openings = [
|
||||
{
|
||||
id: 'hp117-window', type: 'window', x: 0.22, y: 0.14,
|
||||
angle: 0, length: 0.09, contact: contactId,
|
||||
},
|
||||
{
|
||||
id: 'hp117-door', type: 'door', x: 0.40, y: 0.14,
|
||||
angle: 0, length: 0.09, contact: contactId, lock: lockId,
|
||||
},
|
||||
];
|
||||
card._serverCfg = {
|
||||
...card._serverCfg,
|
||||
markers: [
|
||||
{ id: 'hp117-contact-gone', binding: `entity:${contactId}`, removed: true, hidden: true },
|
||||
{ id: 'hp117-lock-gone', binding: `entity:${lockId}`, removed: true, hidden: true },
|
||||
],
|
||||
};
|
||||
card._cfgEpoch++;
|
||||
card._setMode('view');
|
||||
const serviceCalls = [];
|
||||
card.hass = {
|
||||
...card.hass,
|
||||
states: {
|
||||
...card.hass.states,
|
||||
[contactId]: {
|
||||
entity_id: contactId, state: 'off',
|
||||
attributes: { friendly_name: 'YAML opening contact', device_class: 'window' },
|
||||
},
|
||||
[lockId]: {
|
||||
entity_id: lockId, state: 'locked',
|
||||
attributes: { friendly_name: 'YAML opening lock' },
|
||||
},
|
||||
},
|
||||
callService: async (domain, service, data) => {
|
||||
serviceCalls.push({ domain, service, entityId: data.entity_id });
|
||||
},
|
||||
};
|
||||
await settle();
|
||||
|
||||
const rendered = () => card._openingsR;
|
||||
const yamlWindow = () => rendered().find((opening) => opening.id === 'hp117-window');
|
||||
const yamlDoor = () => rendered().find((opening) => opening.id === 'hp117-door');
|
||||
result.noRegistryRowsExist = card.hass.entities?.[contactId] == null
|
||||
&& card.hass.entities?.[lockId] == null;
|
||||
result.pickerOffersExactYamlEntities = card._contactCandidates()
|
||||
.some((item) => item.value === contactId)
|
||||
&& card._lockCandidates().some((item) => item.value === lockId);
|
||||
result.frozenProjectionKeepsYamlStates = card._renderPlanHass.entities?.[contactId] == null
|
||||
&& card._renderPlanHass.entities?.[lockId] == null
|
||||
&& card._renderPlanHass.states?.[contactId]?.state === 'off'
|
||||
&& card._renderPlanHass.states?.[lockId]?.state === 'locked';
|
||||
result.markerTombstonesDoNotBlockOpening = card._renderOpeningEntityAvailable(contactId)
|
||||
&& card._renderOpeningEntityAvailable(lockId)
|
||||
&& !card._planEntityAvailable(contactId)
|
||||
&& !card._planEntityAvailable(lockId);
|
||||
result.closedContactControlsPresentation = card._openingAmt(yamlWindow()) === 0
|
||||
&& card._openingAmt(yamlDoor()) === 0;
|
||||
result.yamlLockBadgeRendersLocked = root().querySelectorAll('.oplock.locked').length === 1;
|
||||
|
||||
const callsBeforePlanTap = serviceCalls.length;
|
||||
root().querySelector('[data-hp="opening"][data-id="hp117-door"] .op-hit')
|
||||
?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||
root().querySelector('.oplock')?.dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true, composed: true }),
|
||||
);
|
||||
await settle();
|
||||
result.planOpeningAndBadgeNeverActuate = serviceCalls.length === callsBeforePlanTap;
|
||||
result.badgeOpensInfoWithBothRows = card._openingInfo?.id === 'hp117-door'
|
||||
&& root().querySelectorAll('.oprow').length === 2
|
||||
&& root().querySelectorAll('.lockact').length === 1;
|
||||
|
||||
window.confirm = () => true;
|
||||
card._lockAction(lockId, 'unlock');
|
||||
result.explicitInfoActionStillWorks = serviceCalls.at(-1)?.domain === 'lock'
|
||||
&& serviceCalls.at(-1)?.service === 'unlock'
|
||||
&& serviceCalls.at(-1)?.entityId === lockId;
|
||||
card._openingInfo = null;
|
||||
await settle();
|
||||
|
||||
card._physicalBodiesR();
|
||||
const frameBefore = card._renderPlanHass;
|
||||
const physicalBefore = card._physicalBodiesCache;
|
||||
const epochBefore = card._cfgEpoch;
|
||||
const configBefore = JSON.stringify(card._serverCfg);
|
||||
card.hass = {
|
||||
...card.hass,
|
||||
states: {
|
||||
...card.hass.states,
|
||||
[contactId]: { ...card.hass.states[contactId], state: 'on' },
|
||||
[lockId]: { ...card.hass.states[lockId], state: 'unlocked' },
|
||||
},
|
||||
};
|
||||
const oldFrameHeldUntilUpdate = card._renderPlanHass === frameBefore
|
||||
&& card._openingAmt(yamlWindow()) === 0;
|
||||
await settle();
|
||||
result.stateTickSwapsOneAtomicFrame = oldFrameHeldUntilUpdate
|
||||
&& card._renderPlanHass !== frameBefore
|
||||
&& card._openingAmt(yamlWindow()) === 1
|
||||
&& card._openingAmt(yamlDoor()) === 1
|
||||
&& root().querySelectorAll('.oplock.unlocked').length === 1;
|
||||
result.stateTickDoesNotRebuildGeometryOrConfig = card._physicalBodiesCache === physicalBefore
|
||||
&& card._cfgEpoch === epochBefore
|
||||
&& JSON.stringify(card._serverCfg) === configBefore;
|
||||
|
||||
card.hass = {
|
||||
...card.hass,
|
||||
states: {
|
||||
...card.hass.states,
|
||||
[contactId]: { ...card.hass.states[contactId], state: 'unknown' },
|
||||
[lockId]: { ...card.hass.states[lockId], state: 'unknown' },
|
||||
},
|
||||
};
|
||||
await settle();
|
||||
result.unknownKeepsExistingTypeSemantics = card._renderOpeningEntityAvailable(contactId)
|
||||
&& card._openingAmt(yamlWindow()) === 0
|
||||
&& card._openingAmt(yamlDoor()) === 1
|
||||
&& root().querySelectorAll('.oplock.unknown').length === 1;
|
||||
|
||||
space.openings = [
|
||||
{
|
||||
id: 'hp117-disabled-window', type: 'window', x: 0.22, y: 0.14,
|
||||
angle: 0, length: 0.09, contact: 'binary_sensor.window',
|
||||
},
|
||||
{
|
||||
id: 'hp117-disabled-lock', type: 'door', x: 0.40, y: 0.14,
|
||||
angle: 0, length: 0.09, lock: 'lock.front_door',
|
||||
},
|
||||
];
|
||||
card._cfgEpoch++;
|
||||
window.__setRegistryDisabled('entity', 'binary_sensor.window', 'user');
|
||||
window.__setRegistryDisabled('entity', 'lock.front_door', 'user');
|
||||
await new Promise((resolve) => setTimeout(resolve, 220));
|
||||
await settle();
|
||||
const disabledWindow = card._openingsR.find((opening) => opening.id === 'hp117-disabled-window');
|
||||
result.explicitDisabledRowsRemoveStaleStates = card._renderPlanHass.states?.['binary_sensor.window'] == null
|
||||
&& card._renderPlanHass.states?.['lock.front_door'] == null
|
||||
&& !card._renderOpeningEntityAvailable('binary_sensor.window')
|
||||
&& !card._renderOpeningEntityAvailable('lock.front_door')
|
||||
&& card._openingAmt(disabledWindow) === 0
|
||||
&& root().querySelectorAll('.oplock').length === 0;
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
checkAll(out);
|
||||
await finish(browser, out);
|
||||
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -519,8 +519,11 @@ double click → properties dialog. In markup mode the "Opening" tool handles cl
|
||||
Contact and lock are exact HA references owned by the opening, not aliases of
|
||||
standalone markers. Their candidate/action path follows HA binding status while
|
||||
their render path follows the frozen active-registry projection; neither path
|
||||
consults marker tombstones. Plan-level consumers keep the tombstone policy
|
||||
described above.
|
||||
consults marker tombstones. For that projection, the presence of an exact state
|
||||
is sufficient: registry-less YAML entities have no row, while explicit
|
||||
disabled/orphan rows have already been stripped together with their states.
|
||||
The render helper must never receive raw live hass. Plan-level consumers keep
|
||||
the tombstone policy described above.
|
||||
|
||||
For a wall with thickness, one `OpeningWallIndex` resolves the atomic wall
|
||||
interval and adjacent room on each side of the centreline. Opening symbols,
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
([#166](https://github.com/Matysh/houseplan-card/issues/166)).
|
||||
- Small fixes and improvements.
|
||||
|
||||
- Door, window and gate contacts and locks now keep working when they are live
|
||||
YAML entities without a `unique_id` and therefore have no Entity Registry
|
||||
row. The picker, View animation, lock badge and opening info card now follow
|
||||
the same exact reference, while disabled, orphaned and missing entities
|
||||
remain unavailable ([#117](https://github.com/Matysh/houseplan-card/issues/117)).
|
||||
|
||||
## v1.64.0-beta.3 — 2026-08-14
|
||||
|
||||
- Large plans no longer recompute an unused physical-wall union on every floor
|
||||
|
||||
@@ -80,6 +80,13 @@
|
||||
([#166](https://github.com/Matysh/houseplan-card/issues/166)).
|
||||
- Мелкие исправления и улучшения.
|
||||
|
||||
- Датчики и замки дверей, окон и ворот теперь работают и для живых
|
||||
YAML-сущностей без `unique_id`, у которых поэтому нет строки в Entity
|
||||
Registry. Список выбора, анимация в Просмотре, значок замка и карточка проёма
|
||||
используют одну точную ссылку; отключённые, потерянные и отсутствующие
|
||||
сущности по-прежнему недоступны
|
||||
([#117](https://github.com/Matysh/houseplan-card/issues/117)).
|
||||
|
||||
## v1.64.0-beta.3 — 2026-08-14
|
||||
|
||||
- Большие планы больше не пересчитывают неиспользуемое объединение физических
|
||||
|
||||
@@ -1095,6 +1095,13 @@ separately promised workflows:
|
||||
locked / Lock when unlocked; button calls the lock service; disabled while
|
||||
locking/unlocking; hidden when unavailable; plan-icon tap still never
|
||||
toggles a lock [auto: smoke_gear_tabs / smoke_gs_always]
|
||||
- [ ] Registry-less opening binding (#117): a live YAML contact/lock without
|
||||
`unique_id` is offered by the picker and drives the frozen View frame,
|
||||
badge and info card; marker tombstones do not block it, an HA tick swaps
|
||||
the frame without rebuilding geometry/config, and explicit disabled rows
|
||||
with stale states remain hidden. Only the confirmed info-card lock action
|
||||
may call a service [auto: ha-binding-status, render-device-snapshot,
|
||||
smoke_registryless_opening, mutation-gate]
|
||||
- [ ] New-device flag (v1.29.0): a device added to HA after install gets a big red
|
||||
dot top-right of its icon (all clients); opening its editor clears it
|
||||
everywhere; upgrade/first-run seeds the baseline silently — no dot flood [auto: smoke_new_device]
|
||||
|
||||
@@ -472,7 +472,10 @@ Glow и солнечные лучи, но при Resize комнаты оста
|
||||
датчика анимирует створку/полотно. Инверсия нужна, если интеграция отдаёт
|
||||
противоположную логику. Контактный датчик и замок — самостоятельные точные
|
||||
привязки проёма: удаление отдельного маркера той же сущности с плана не убирает
|
||||
её из этих списков и не выключает уже сохранённую связь.
|
||||
её из этих списков и не выключает уже сохранённую связь. Можно выбрать живую
|
||||
YAML-сущность без `unique_id` и строки в Entity Registry: пока Home Assistant
|
||||
передаёт её точное состояние, она так же управляет проёмом и значком замка в
|
||||
Просмотре. Явно отключённая, потерянная или отсутствующая сущность не работает.
|
||||
|
||||
### Поведение в разных режимах
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"sourceFingerprint": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceFingerprint": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"captureScriptSha256": "34f2219790d46efd8250e7a1bd829cb8fc0b0547e1260635fefa52407551b41b",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "d36b6f9f8139f31ef73a780c6511a640a26055efd9d7a24c24fd48b1d8379bf0"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "358e25ff9984d0fb0c03cfbb848df40c425fdfe64cd5f4f613b1e754ca9d4249"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "c53db2e5c642a5549c13f3c93a5b359fed69bdb2621bf71a243a877ffcb95e6b"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "5ca7f24642926072ccbc5bf48341c5310329918631b147c116f9c29e20e4927e"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -57,7 +57,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "164b79fd9075a1425cffe3c85c88b689dfda90b79856bb4ed6655507b166b3e0"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "7a22a81e2abedf5a520bd36b7509d953cbfa9962b48c0ffbc233666a6215c04b"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "f6014caed7b7d28790b8548996ba09d806a4e4d51fbb7e8d3fb3c582ebe49167"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "d7cfe70551d9260169df8efd832e32ed7df99c7f60b55d1fd4a8322bd4c47175"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -101,7 +101,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "176abba71d41cfb045a33f82a794d9fbb2a5d3c48e6df66e6ec3a8e448311b11"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -112,7 +112,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "3523bebdb36912c484e50c1a31b37bcc12aec09a6eff8d9dd3558a6f037de4a0",
|
||||
"sourceSha256": "2b4a6de3340e1c264c45724b79f9e618cda943509b8d4cb746f8ecace57d2bc5",
|
||||
"imageSha256": "2199ed88b215bf2bff63bf665028f78b2aa6a92c3032b803931790f2ef071893"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,18 @@ export const MUTANTS = [
|
||||
replace: 'cuts.push([opening.x, opening.y, opening.x, opening.y]);',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'registryless-opening-requires-registry-row',
|
||||
guard: 'node demo/smoke_registryless_opening.mjs',
|
||||
because: 'возврат требования Entity Registry row снова делает выбранную YAML-сущность '
|
||||
+ 'невидимой в painted frame; smoke обязан доказать contact, lock и frozen-snapshot parity',
|
||||
patches: [{
|
||||
file: 'src/ha-binding-status.ts',
|
||||
find: ' return !!entityId\n && !!projectedHass?.states?.[entityId];',
|
||||
replace: ' return !!entityId\n && !!projectedHass?.entities?.[entityId]\n'
|
||||
+ ' && !!projectedHass?.states?.[entityId];',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'column-shadow-removed',
|
||||
guard: 'node demo/smoke_glow.mjs',
|
||||
|
||||
@@ -532,15 +532,17 @@ export function openingEntityAvailable(
|
||||
|
||||
/**
|
||||
* Frame-local counterpart of `openingEntityAvailable`. The caller supplies an
|
||||
* immutable active-registry projection, so both the registry row and state
|
||||
* must belong to the same painted frame. Registry-less render parity is #117.
|
||||
* immutable active-registry projection. State presence in that projection is
|
||||
* sufficient evidence for an exact opening reference: YAML entities without
|
||||
* unique_id have no registry row, while explicit disabled/orphan rows have
|
||||
* already had their states removed by `activeRegistryHass`. Never call this
|
||||
* helper with raw live hass; every read must belong to one painted frame.
|
||||
*/
|
||||
export function renderOpeningEntityAvailable(
|
||||
projectedHass: any,
|
||||
entityId: string | null | undefined,
|
||||
): boolean {
|
||||
return !!entityId
|
||||
&& !!projectedHass?.entities?.[entityId]
|
||||
&& !!projectedHass?.states?.[entityId];
|
||||
}
|
||||
|
||||
|
||||
@@ -247,19 +247,59 @@ test('opening entity availability ignores marker lifecycle and follows exact HA
|
||||
assert.equal(openingEntityAvailable(hass, null, snapshot), false);
|
||||
});
|
||||
|
||||
test('opening render availability requires one frozen registry-backed frame', () => {
|
||||
const frame = {
|
||||
entities: {
|
||||
'binary_sensor.door': { entity_id: 'binary_sensor.door' },
|
||||
'lock.no_state': { entity_id: 'lock.no_state' },
|
||||
test('opening render availability trusts one frozen active projection', () => {
|
||||
const devices = {
|
||||
active: { id: 'active', disabled_by: null },
|
||||
disabled: { id: 'disabled', disabled_by: 'user' },
|
||||
};
|
||||
const entities = {
|
||||
'binary_sensor.door': {
|
||||
entity_id: 'binary_sensor.door', device_id: 'active', disabled_by: null,
|
||||
},
|
||||
states: {
|
||||
'binary_sensor.door': { state: 'unknown' },
|
||||
'binary_sensor.yaml_only': { state: 'on' },
|
||||
'lock.no_state': { entity_id: 'lock.no_state', device_id: 'active', disabled_by: null },
|
||||
'lock.disabled_entity': {
|
||||
entity_id: 'lock.disabled_entity', device_id: 'active', disabled_by: 'user',
|
||||
},
|
||||
'lock.disabled_parent': {
|
||||
entity_id: 'lock.disabled_parent', device_id: 'disabled', disabled_by: null,
|
||||
},
|
||||
'lock.orphan': {
|
||||
entity_id: 'lock.orphan', device_id: 'missing-device', disabled_by: null,
|
||||
},
|
||||
};
|
||||
const states = {
|
||||
'binary_sensor.door': { entity_id: 'binary_sensor.door', state: 'unknown' },
|
||||
'binary_sensor.yaml_only': { entity_id: 'binary_sensor.yaml_only', state: 'on' },
|
||||
'lock.yaml_unknown': { entity_id: 'lock.yaml_unknown', state: 'unavailable' },
|
||||
'lock.disabled_entity': { entity_id: 'lock.disabled_entity', state: 'locked' },
|
||||
'lock.disabled_parent': { entity_id: 'lock.disabled_parent', state: 'locked' },
|
||||
'lock.orphan': { entity_id: 'lock.orphan', state: 'locked' },
|
||||
};
|
||||
const frame = activeRegistryHass({ devices, entities, states }, full(devices, entities));
|
||||
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'binary_sensor.door'), true);
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'lock.no_state'), false);
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'binary_sensor.yaml_only'), false, '#117 owns YAML parity');
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'binary_sensor.yaml_only'), true);
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'lock.yaml_unknown'), true,
|
||||
'unknown/unavailable remains an existing exact reference, not an open state');
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'lock.disabled_entity'), false);
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'lock.disabled_parent'), false);
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'lock.orphan'), false);
|
||||
assert.equal(renderOpeningEntityAvailable(frame, 'lock.missing'), false);
|
||||
assert.equal(renderOpeningEntityAvailable(frame, ''), false);
|
||||
|
||||
const limitedFrame = activeRegistryHass({
|
||||
devices: {}, entities: {},
|
||||
states: { 'binary_sensor.limited_yaml': { state: 'off' } },
|
||||
}, limited({}, {}));
|
||||
assert.equal(renderOpeningEntityAvailable(limitedFrame, 'binary_sensor.limited_yaml'), true);
|
||||
|
||||
const markerTombstoneIsNotAnInput = {
|
||||
...frame,
|
||||
markers: [{ binding: 'entity:binary_sensor.yaml_only', removed: true }],
|
||||
};
|
||||
assert.equal(
|
||||
renderOpeningEntityAvailable(markerTombstoneIsNotAnInput, 'binary_sensor.yaml_only'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -114,6 +114,14 @@ test('opening references use their own availability policy without weakening pla
|
||||
assert.match(renderAvailability, /isRemovedPlanEntity/);
|
||||
assert.doesNotMatch(planAvailability, /openingEntityAvailable/);
|
||||
assert.doesNotMatch(renderAvailability, /renderOpeningEntityAvailable/);
|
||||
|
||||
const openingRenderAvailability = methodBody(source, '_renderOpeningEntityAvailable');
|
||||
assert.match(
|
||||
openingRenderAvailability,
|
||||
/renderOpeningEntityAvailable\(this\._renderPlanHass, eid\)/,
|
||||
'opening render availability receives only the immutable painted-frame projection',
|
||||
);
|
||||
assert.doesNotMatch(openingRenderAvailability, /this\.hass\b/);
|
||||
});
|
||||
|
||||
test('lock actuation remains guarded inside the one sanctioned opening-card method', () => {
|
||||
|
||||
Reference in New Issue
Block a user