this._markerDialog=null}>
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 3332f4a8..9bc56330 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1132,6 +1132,13 @@ its configured space.
group has no executable unavailable/missing/HA-disabled target produces the
card's standard local explanatory toast and no service/press feedback;
partial groups keep executing their available subset.
+ The persisted external-target list also keeps the physical-controller role
+ when every runtime target was filtered by another marker's deletion
+ tombstone: own live diagnostics still decide availability instead of an
+ event-primary fallback. Marker-dialog drafts are projected through
+ `buildDevices` with the complete persisted marker roster, replacing only the
+ edited marker. Consequently target ownership/tombstones and controller
+ presentation are identical on the committed plan and in preview.
- **Universal device action** (#94): `src/device-toggle.ts` is the only authority
for toggle origin, exact target, capability/security filtering, next effect
and service command. The dialog hint, click path, confirmation re-resolution
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 20b48e99..14a45316 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -2,6 +2,12 @@
## Unreleased
+- Wireless controllers now keep the same available/working state on the plan
+ and in the device-dialog preview when a separately deleted target marker
+ filters the configured light from the runtime roster. Live controller
+ battery/LQI/update diagnostics continue to prove that the controller is
+ online, while only an active controlled target makes it yellow
+ ([#274](https://github.com/Matysh/houseplan-card/issues/274)).
- “Optimize plans” now removes a microscopic wall-thickness step beside a
T-junction when the short fragment is bounded by two equal-thickness pieces
of the same straight wall. The T-node and its perpendicular wall stay in
diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md
index 8e400fab..ab042e65 100755
--- a/docs/CHANGELOG.ru.md
+++ b/docs/CHANGELOG.ru.md
@@ -8,6 +8,12 @@
## Не выпущено
+- Беспроводной контроллер теперь одинаково показывает доступность и работу на
+ плане и в preview диалога устройства, даже если отдельно удалённый маркер
+ цели отфильтровывает настроенный свет из runtime-реестра. Живые диагностические
+ сущности battery/LQI/update по-прежнему подтверждают связь с контроллером, а
+ жёлтым его делает только активная управляемая цель
+ ([#274](https://github.com/Matysh/houseplan-card/issues/274)).
- «Оптимизировать планы» теперь удаляет микроскопическую ступень толщины рядом
с T-стыком, если короткий фрагмент ограничен двумя участками одинаковой
толщины одной прямой стены. T-узел и перпендикулярная стена остаются на
diff --git a/docs/TESTING.md b/docs/TESTING.md
index c59872a1..bf3c30c4 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -1369,15 +1369,22 @@ separately promised workflows:
controller is encountered first. Controller availability is independent
(#251): live battery/LQI/update keeps it neutral and opaque when every
target is unavailable, all unavailable own entities fade it even if a
- target is on, and a virtual controller remains available. A fully
+ target is on, and a virtual controller remains available. A separately
+ deleted target marker also cannot turn a live wireless controller into
+ `unavailable` or make dialog preview disagree with the saved plan (#274):
+ both projections consume the complete marker roster, preserve the live
+ LQI value and become yellow only after an active target returns. A fully
unavailable configured group shows the named singular/plural local toast
without service, confirmation or press feedback; partial groups still
execute silently, and a target lost after confirmation uses the same
unavailable toast [auto: smoke_controls; unit: devices.test.mjs,
device-presentation.test.mjs, device-toggle.test.mjs; golden:
device-icon-state-table light/dark; mutation: controller-availability-follows-target,
- controller-diagnostics-do-not-prove-online, unavailable-toggle-stays-silent,
- partial-group-shows-noop-toast]
+ controller-diagnostics-do-not-prove-online,
+ wireless-controller-loses-filtered-target-role,
+ wireless-controller-preview-drops-sibling-markers,
+ unavailable-toggle-stays-silent, partial-group-shows-noop-toast; #274:
+ smoke_wireless_controller_parity]
- [ ] Linked manual virtual light (#174): an exact #107 virtual Always-light
with an incoming controller follows the real HA driver despite a saved
manual off-bit. Clicking either marker operates the real relay and one HA
diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md
index 0bf9381e..df322332 100644
--- a/docs/USER-GUIDE.md
+++ b/docs/USER-GUIDE.md
@@ -500,6 +500,9 @@ but only the controller's own active entities decide whether it fades. A live
battery, Zigbee LQI or update entity therefore keeps a wireless switch neutral
and opaque when all of its lamps are unavailable. A controller with no live own
entity fades even if a target is on; a virtual controller is always available.
+This remains true when the same target was separately removed from the plan:
+the removed marker is not restored, but it cannot make the controller look
+offline or make its editor preview disagree with the plan.
For a composite appliance with a dedicated Power switch, Power=`on` alone
remains neutral. If Home Assistant also exposes a strict lifecycle entity such
diff --git a/docs/USER-GUIDE.ru.md b/docs/USER-GUIDE.ru.md
index 317d7666..d53908e0 100644
--- a/docs/USER-GUIDE.ru.md
+++ b/docs/USER-GUIDE.ru.md
@@ -884,6 +884,9 @@ hover → состояние → нейтрально**. Если все сущ
собственная сущность не имеет живого состояния, контроллер бледнеет даже при
включённой цели. Виртуальный контроллер не имеет физического HA-устройства и
считается доступным.
+То же правило действует, если маркер этой цели отдельно удалён с плана:
+удалённый маркер не возвращается, но он не может сделать контроллер визуально
+недоступным или вызвать расхождение плана и preview в редакторе устройства.
Hover включается только после события от настоящей мыши на устройстве, которое
поддерживает точный hover. Касание пальцем или пером сразу снимает подсказку и
diff --git a/docs/images/01-view-desktop.png b/docs/images/01-view-desktop.png
index 3f959b61..d1947abb 100644
Binary files a/docs/images/01-view-desktop.png and b/docs/images/01-view-desktop.png differ
diff --git a/docs/images/02-view-touch.png b/docs/images/02-view-touch.png
index fbc37414..8d4589d8 100644
Binary files a/docs/images/02-view-touch.png and b/docs/images/02-view-touch.png differ
diff --git a/docs/images/03-space-create.png b/docs/images/03-space-create.png
index 1e683d3a..09ceed64 100644
Binary files a/docs/images/03-space-create.png and b/docs/images/03-space-create.png differ
diff --git a/docs/images/04-room-contour-close.png b/docs/images/04-room-contour-close.png
index 792e207e..45639865 100644
Binary files a/docs/images/04-room-contour-close.png and b/docs/images/04-room-contour-close.png differ
diff --git a/docs/images/05-plan-context-tray.png b/docs/images/05-plan-context-tray.png
index 53c160d1..3d3ee26c 100644
Binary files a/docs/images/05-plan-context-tray.png and b/docs/images/05-plan-context-tray.png differ
diff --git a/docs/images/06-device-display-preview.png b/docs/images/06-device-display-preview.png
index 7ebc07a2..e97464df 100644
Binary files a/docs/images/06-device-display-preview.png and b/docs/images/06-device-display-preview.png differ
diff --git a/docs/images/06-device-editor.png b/docs/images/06-device-editor.png
index 27e45eb7..42398476 100644
Binary files a/docs/images/06-device-editor.png and b/docs/images/06-device-editor.png differ
diff --git a/docs/images/07-background-editor.png b/docs/images/07-background-editor.png
index ec9fb8f5..c14479c0 100644
Binary files a/docs/images/07-background-editor.png and b/docs/images/07-background-editor.png differ
diff --git a/docs/images/08-room-card.png b/docs/images/08-room-card.png
index a50b3912..179689ce 100644
Binary files a/docs/images/08-room-card.png and b/docs/images/08-room-card.png differ
diff --git a/docs/images/09-device-info.png b/docs/images/09-device-info.png
index 8767664f..e2c9d1e0 100644
Binary files a/docs/images/09-device-info.png and b/docs/images/09-device-info.png differ
diff --git a/docs/images/screenshots.json b/docs/images/screenshots.json
index 1456b559..29d3f958 100644
--- a/docs/images/screenshots.json
+++ b/docs/images/screenshots.json
@@ -2,7 +2,7 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
- "sourceFingerprint": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
+ "sourceFingerprint": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -14,8 +14,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "abc6ca91ef76fe4892b95414c24b5d4452a403aaa42de89c7f8559cf3cf60ec6"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "aaab24546dac767fc434c627c0aaecd69458b97872cf7a97ce417686946902d1"
},
"view-touch": {
"file": "02-view-touch.png",
@@ -25,8 +25,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "367d0ff05c14f945d60ff6ab1369a8747fc72dac5f2d599266ac0a2e848587bb"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "3cbdf7aa2105d72b2dea0d644862f36118e268d8c8d2e285f1edcffc49648a93"
},
"space-create": {
"file": "03-space-create.png",
@@ -36,8 +36,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "6df7753f00b7d87cd35fd18fb8fbac0a2a7fa2e549902b3afee38f38c93a2ac0"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "c33a7279165a4cec6fa6fadb6fd08cd967e082a17fe101ef442d27d36ae59b6b"
},
"room-contour-close": {
"file": "04-room-contour-close.png",
@@ -47,8 +47,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "24ff960f77cd79e0d760dabab93d52f0df5d22cadaeabd3228e6ec4e3db0d59a"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "2c67fe47a3627e7c4fef3202630dd40ac975cfbe36766b5b4264f22fe43cc49a"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -58,8 +58,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "5b9ecd237a141a50538bb3a41a0d6416947a41fb3e88aca29a93238224a739ff"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "c21bdc2e92da0aaba27a909b0c00d9aeca3ab075ccef5f94bb074cf85d173757"
},
"device-editor": {
"file": "06-device-editor.png",
@@ -69,8 +69,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "25b0e13e28ec406b0202440d10fb51b48ad9889269533e5789812ec0011a2da3"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "b68e560bbc3744a911cbf9d37b0eee765d1ef437b27fe65402c38b13921da1fe"
},
"device-display-preview": {
"file": "06-device-display-preview.png",
@@ -80,8 +80,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "4dcd28f886af304219d4ad6762c398cfdf3af85c1cb2aec31a5fa9c12d8033be"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "7393c93fd60666703531f8882fac9b421452aaf04e47f1a12997afae39cfb7e7"
},
"background-editor": {
"file": "07-background-editor.png",
@@ -91,8 +91,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "7f3595184ada0ebbaab49e73124eaccde93ba21c5b4513a99a3182c43ad60ef9"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "01b6245826e70b256312ffb20219e61a8246bdcace87f848419dd9246df297ba"
},
"room-card": {
"file": "08-room-card.png",
@@ -102,8 +102,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "66d8b0a509909822ff6b891a483bad9d2c72b20f08e287355883aa5205c6cee0"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
"file": "09-device-info.png",
@@ -113,8 +113,8 @@
},
"theme": "dark",
"language": "en",
- "sourceSha256": "4db4677b72a4166904a853837eed8014e587d85afc8811c7dd10e0d3365ae54d",
- "imageSha256": "094aaea598ea1d8f96bcaa059e66922a94adc27340baa28c3c275cc2efa400c1"
+ "sourceSha256": "85ce7cdb75b81740bc94a37ad3e35af37c0eb836e5ccd6ad8c010219f3a67137",
+ "imageSha256": "b49138235d348ca6a2d74da5c9891298f44546fd902efcdea49ef89605654b33"
}
}
}
diff --git a/docs/specs/274-wireless-controller-presentation-parity.md b/docs/specs/274-wireless-controller-presentation-parity.md
index b6e896cb..ab293d09 100644
--- a/docs/specs/274-wireless-controller-presentation-parity.md
+++ b/docs/specs/274-wireless-controller-presentation-parity.md
@@ -6,7 +6,7 @@
сложность 5/10 · риск 5/10
- Issue: [#274](https://github.com/Matysh/houseplan-card/issues/274)
- Ветка: `issue/274-wireless-controller-parity`
-- Статус ТЗ: готово к ревью
+- Статус ТЗ: реализовано, готово к code review
Канонические документы: `docs/SCOPE.md`, `docs/ARCHITECTURE.md`,
`docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`, `docs/TESTING.md` и
@@ -314,4 +314,3 @@ provenance принимается только из полного Linux workflo
draft после пользовательской правки вправе показывать будущий результат.
4. Точная раскладка новых lifecycle tests техническая и может быть изменена
ревьюером при сохранении AC2/AC4 и исполняемых мутантов.
-
diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs
index 73b4619d..436f23a2 100644
--- a/scripts/mutation-gate.mjs
+++ b/scripts/mutation-gate.mjs
@@ -1306,8 +1306,9 @@ export const MUTANTS = [
+ 'the focused matrix keeps own availability and target working as separate facts (#251)',
patches: [{
file: 'src/device-presentation.ts',
- find: " ? { ...combined, availability: controllerAvailability(hass, d) }",
- replace: ' ? combined',
+ find: " let visual = controllerFace\n"
+ + " ? { ...combined, availability: controllerAvailability(hass, d) }",
+ replace: " let visual = controllerFace\n ? combined",
}],
},
{
@@ -1324,6 +1325,36 @@ export const MUTANTS = [
+ "&& !eid.startsWith('update.')).some((eid) => {",
}],
},
+ {
+ id: 'wireless-controller-loses-filtered-target-role',
+ guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ + '&& node --test --test-name-pattern="issue 274 keeps a wireless controller" '
+ + 'test/device-presentation.test.mjs',
+ because: 'a saved target tombstone may empty the runtime light graph without turning the '
+ + 'physical controller into its event-only primary; live diagnostics still prove online (#274)',
+ patches: [{
+ file: 'src/device-presentation.ts',
+ find: " const controllerFace = sources.sourceKind === 'controls'\n"
+ + " || (configuredController && sources.sourceKind !== 'light' && sources.sourceKind !== 'cover');",
+ replace: " const controllerFace = sources.sourceKind === 'controls';",
+ }],
+ },
+ {
+ id: 'wireless-controller-preview-drops-sibling-markers',
+ guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ + '&& node --test --test-name-pattern="issue 274 keeps a wireless controller" '
+ + 'test/device-presentation.test.mjs',
+ because: 'the draft must inherit the same tombstones and ownership roster as the saved plan; '
+ + 'isolating it restores the plan/preview contradiction (#274)',
+ patches: [{
+ file: 'src/devices.ts',
+ find: ' const markers = [\n'
+ + ' ...siblingMarkers.filter((item) => item.id !== marker.id),\n'
+ + ' marker,\n'
+ + ' ];',
+ replace: ' const markers = [marker];',
+ }],
+ },
{
id: 'unavailable-toggle-stays-silent',
guard: 'node demo/smoke_controls.mjs',
diff --git a/src/device-presentation.ts b/src/device-presentation.ts
index bcac6945..f6ecbd0f 100644
--- a/src/device-presentation.ts
+++ b/src/device-presentation.ts
@@ -624,7 +624,18 @@ export function resolveDevicePresentation(
// A controller mirrors target work, not target connectivity. Its faded
// state describes only the bound controller; Glow/fill/statistics continue
// to consume the unchanged target graph.
- let visual = sources.sourceKind === 'controls'
+ // A configured controller remains a controller when every saved target is
+ // currently filtered out by a plan tombstone. In that case the source graph
+ // is deliberately empty/device-led, but falling back to the event primary
+ // would undo #251 and can produce the impossible `unavail` + live LQI face.
+ // Manual/owned light and cover faces keep their dedicated availability.
+ const configuredController = !isManualVirtualLightMarker(d.marker)
+ && persistedExternalControls(
+ d.marker?.binding, d.marker?.controls ?? d.controls, d.entities,
+ ).length > 0;
+ const controllerFace = sources.sourceKind === 'controls'
+ || (configuredController && sources.sourceKind !== 'light' && sources.sourceKind !== 'cover');
+ let visual = controllerFace
? { ...combined, availability: controllerAvailability(hass, d) }
: combined;
if (effectiveHidden) visual = { availability: 'available', status: 'neutral', activity: 'none' };
diff --git a/src/devices.ts b/src/devices.ts
index da7fcda2..65ad28a1 100644
--- a/src/devices.ts
+++ b/src/devices.ts
@@ -1284,9 +1284,19 @@ export function buildDevices(ctx: BuildCtx): DevItem[] {
* projection over buildDevices prevents a second, subtly different preview
* implementation from growing beside the runtime one.
*/
-export function deviceFromMarkerDraft(ctx: Omit & { marker: Marker }): DevItem | null {
- const { marker, ...base } = ctx;
- return buildDevices({ ...base, markers: [marker] })
+export function deviceFromMarkerDraft(
+ ctx: Omit & {
+ marker: Marker;
+ /** The persisted roster whose tombstones/ownership also constrain the plan. */
+ siblingMarkers?: readonly Marker[];
+ },
+): DevItem | null {
+ const { marker, siblingMarkers = [], ...base } = ctx;
+ const markers = [
+ ...siblingMarkers.filter((item) => item.id !== marker.id),
+ marker,
+ ];
+ return buildDevices({ ...base, markers })
.find((device) => device.id === marker.id) || null;
}
diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts
index bfbd6bed..702bdecc 100755
--- a/src/houseplan-card.ts
+++ b/src/houseplan-card.ts
@@ -19899,7 +19899,7 @@ class HouseplanCard extends LitElement {
private _markerPreviewDevice(d: NonNullable): DevItem | null {
const marker = this._markerDraft(d);
if (!marker) return null;
- const key = `${this._haRegistry.revision}\n${JSON.stringify(marker)}`;
+ const key = `${this._haRegistry.revision}\n${contentFingerprint(this._markers)}\n${JSON.stringify(marker)}`;
if (this._markerPreviewMemo?.key === key) return this._markerPreviewMemo.device;
const device = deviceFromMarkerDraft({
hass: this.hass,
@@ -19908,6 +19908,7 @@ class HouseplanCard extends LitElement {
Object.entries(this._areaToSpace).map(([area, value]) => [area, value.space]),
),
marker,
+ siblingMarkers: this._markers,
settings: this._settings,
excluded: this._excluded,
showAll: this._showAll,
diff --git a/test/device-presentation.test.mjs b/test/device-presentation.test.mjs
index 1e15405c..56b37f86 100644
--- a/test/device-presentation.test.mjs
+++ b/test/device-presentation.test.mjs
@@ -16,7 +16,9 @@ import {
resolveEntityProvider,
} from '../test-build/integration-provider.js';
import { valueBadgeTitle, valueBadgeWriteFields } from '../test-build/device-value-badge.js';
-import { resolvedLightSources } from '../test-build/devices.js';
+import {
+ buildDevices, deviceFromMarkerDraft, resolvedLightSources,
+} from '../test-build/devices.js';
const state = (entity_id, value, attributes = {}) => ({ entity_id, state: value, attributes });
const hass = (states, entities = {}) => ({
@@ -350,6 +352,96 @@ test('issue 251 separates controller availability from controlled target status'
assert.ok(alarm.classes.includes('alarm'));
});
+test('issue 274 keeps a wireless controller available and gives its draft the full marker roster', () => {
+ const own = [
+ 'event.wall_action', 'sensor.wall_battery', 'sensor.wall_linkquality', 'update.wall',
+ ];
+ const h = hass({
+ 'event.wall_action': state('event.wall_action', 'unknown'),
+ 'sensor.wall_battery': state('sensor.wall_battery', '100'),
+ 'sensor.wall_linkquality': state('sensor.wall_linkquality', '164'),
+ 'update.wall': state('update.wall', 'off'),
+ 'light.wall_group': state('light.wall_group', 'off', { friendly_name: 'Wall lights' }),
+ }, {
+ 'event.wall_action': {
+ entity_id: 'event.wall_action', device_id: 'wall', platform: 'mqtt',
+ },
+ 'sensor.wall_battery': {
+ entity_id: 'sensor.wall_battery', device_id: 'wall', platform: 'mqtt',
+ entity_category: 'diagnostic',
+ },
+ 'sensor.wall_linkquality': {
+ entity_id: 'sensor.wall_linkquality', device_id: 'wall', platform: 'mqtt',
+ entity_category: 'diagnostic',
+ },
+ 'update.wall': {
+ entity_id: 'update.wall', device_id: 'wall', platform: 'mqtt',
+ entity_category: 'config',
+ },
+ 'light.wall_group': {
+ entity_id: 'light.wall_group', platform: 'group', area_id: 'kitchen',
+ },
+ });
+ h.devices.wall = {
+ id: 'wall', name: 'Wall switch', model: 'Wireless switch', area_id: 'kitchen',
+ identifiers: [['mqtt', 'wall']], entry_type: null, via_device_id: null,
+ };
+ h.areas = { kitchen: { area_id: 'kitchen', name: 'Kitchen' } };
+ const marker = {
+ id: 'wall-marker', binding: 'device:wall', tap_action: 'toggle',
+ controls: ['light.wall_group'],
+ };
+ const markers = [
+ marker,
+ { id: 'removed-group', binding: 'entity:light.wall_group', removed: true, hidden: true },
+ ];
+ const context = {
+ hass: h,
+ areaToSpace: { kitchen: 'floor' },
+ settings: { filter_seeded: true },
+ excluded: new Set(),
+ showAll: false,
+ firstSpaceId: 'floor',
+ loc: (key) => key,
+ };
+ const saved = buildDevices({ ...context, markers })
+ .find((item) => item.id === marker.id);
+ const draft = deviceFromMarkerDraft({ ...context, marker, siblingMarkers: markers });
+ assert.ok(saved);
+ assert.ok(draft);
+ assert.deepEqual(saved.entities, own);
+ assert.deepEqual(draft.entities, own);
+ assert.deepEqual(draft.controls, saved.controls, 'draft honours the same tombstones as the plan');
+
+ const savedPresentation = resolveDevicePresentation(h, saved, {
+ ...options, lightDevices: [saved],
+ });
+ const draftPresentation = resolveDevicePresentation(h, draft, {
+ ...options, designPreview: true, lightDevices: [draft],
+ });
+ for (const result of [savedPresentation, draftPresentation]) {
+ assert.deepEqual(result.visual, {
+ availability: 'available', status: 'neutral', activity: 'none',
+ });
+ assert.ok(!result.classes.includes('unavail'));
+ assert.equal(result.lqiText, '164');
+ }
+ assert.deepEqual(
+ {
+ sourceKind: draftPresentation.sourceKind,
+ visual: draftPresentation.visual,
+ classes: draftPresentation.classes,
+ lqiText: draftPresentation.lqiText,
+ },
+ {
+ sourceKind: savedPresentation.sourceKind,
+ visual: savedPresentation.visual,
+ classes: savedPresentation.classes,
+ lqiText: savedPresentation.lqiText,
+ },
+ );
+});
+
test('passive sensor source keeps its normal scalar value and never probes marker ids', () => {
const hits = [];
const h = hass(new Proxy({