Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda25d26c2 | ||
|
|
e49083d6ec | ||
|
|
8cff278c78 | ||
|
|
a1a404e419 | ||
|
|
7dc5a69ec2 | ||
|
|
2e1661b23d | ||
|
|
8c726d753e | ||
|
|
f398b50037 | ||
|
|
16b4197409 | ||
|
|
16f5a30f62 | ||
|
|
3bfd3f4161 | ||
|
|
1d2d23f104 | ||
|
|
b83dd2021f | ||
|
|
7812a26f18 | ||
|
|
8a6f523b1a | ||
|
|
ab9dc1fb46 | ||
|
|
705889cc37 | ||
|
|
e12c04568a | ||
|
|
d50efcb56e | ||
|
|
25f463a933 | ||
|
|
63b6b48d03 | ||
|
|
323fa70f96 | ||
|
|
3e98ac4dde | ||
|
|
3ee8663bfa | ||
|
|
7015a2c454 | ||
|
|
aa62145621 | ||
|
|
602305417b | ||
|
|
97e6504e9c | ||
|
|
88bdd513ec | ||
|
|
b94112b8c4 | ||
|
|
3c091e7ef8 | ||
|
|
9951e6ba10 | ||
|
|
d4d48bb040 | ||
|
|
d787c02687 | ||
|
|
b9145d3e85 | ||
|
|
6651d854d0 | ||
|
|
e8e5718ecd | ||
|
|
b3f5aefa20 | ||
|
|
de2029b7f9 | ||
|
|
370df0708c | ||
|
|
bcc68f0da2 | ||
|
|
574ae94840 | ||
|
|
90e8ea4685 | ||
|
|
ee351dcb84 | ||
|
|
2b841d94e4 | ||
|
|
c175a2ab1a | ||
|
|
020739ebca | ||
|
|
ffdd69e277 | ||
|
|
319ee70010 | ||
|
|
5c893c7ff3 | ||
|
|
077e9c1b1b | ||
|
|
0471482c2e | ||
|
|
db887cd7f0 | ||
|
|
7feda5b6c8 | ||
|
|
285a6dc436 | ||
|
|
52876b0573 | ||
|
|
cdfe02e64d | ||
|
|
07c885435a | ||
|
|
67af859e8c | ||
|
|
4a7abf531b | ||
|
|
379de706d2 | ||
|
|
1faed2af7d | ||
|
|
343449e694 | ||
|
|
9722153e01 | ||
|
|
0f77b921f6 | ||
|
|
e69b3f2009 | ||
|
|
7341ecf9c5 | ||
|
|
fd406acea2 | ||
|
|
33022ff946 | ||
|
|
55ac10be3c | ||
|
|
22828495bb | ||
|
|
370c082f31 | ||
|
|
0d64547643 | ||
|
|
2ac221d28b | ||
|
|
d19bac3966 | ||
|
|
aef02229c6 | ||
|
|
865312e0b0 | ||
|
|
841bad4e79 | ||
|
|
9ebc430983 | ||
|
|
0a098c1e51 | ||
|
|
cf1c90fb5b | ||
|
|
7fb20c658b | ||
|
|
05c8e164c2 | ||
|
|
9deb1dcfd6 | ||
|
|
a5e06b0527 | ||
|
|
14f5b90c24 | ||
|
|
4de49e0db2 | ||
|
|
80cba30895 | ||
|
|
96f89717bd | ||
|
|
d8949cfffc | ||
|
|
7777b8a0a8 | ||
|
|
45aeadb38e | ||
|
|
b07ef67dcb | ||
|
|
d58a5d0dad | ||
|
|
181b61ae8e |
@@ -8,15 +8,14 @@ name: "Мутационный гейт · тело (#623)"
|
||||
# Прогон дорогой и проверяет не продукт, а тесты, поэтому он не входит ни в
|
||||
# Validate, ни в цикл разработки, ни в релизный гейт (#513, решение владельца
|
||||
# 09.09): каждую ночь по расписанию, отказ — issue с отчётом (#472).
|
||||
# Мутанты, задетые диффом, конвейер ревью и слияние гоняют отдельно на своём
|
||||
# кандидате (#510); кандидат беты, `full=true` и ночной Validate их не
|
||||
# запрашивают (#601) — ночью достаточно этого полного реестра.
|
||||
# С #709 ревью, слияние, кандидат беты, `full=true` и ночной Validate
|
||||
# мутантов не запрашивают — ночью достаточно этого полного реестра.
|
||||
# Дешёвая половина — «якоря патчей живы, guard-файлы существуют» — идёт с
|
||||
# обычными юнитами: test/mutation-gate.test.mjs.
|
||||
#
|
||||
# #332: бандл собирается только мутантам с браузерным гвардом (guardNeedsBundle),
|
||||
# компиляция тестов в worktree стартует с тёплого test-build (инкрементальный
|
||||
# tsc), а реестр режется на четыре чересполосных шарда — полный прогон
|
||||
# tsc), а реестр режется на чересполосные шарды — полный прогон
|
||||
# укладывается в десятки минут вместо часов. Локальный дифф-режим:
|
||||
# node scripts/mutation-gate.mjs --changed origin/dev..HEAD
|
||||
#
|
||||
@@ -110,20 +109,20 @@ jobs:
|
||||
fi
|
||||
|
||||
mutants:
|
||||
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 6)"
|
||||
name: "Мутанты: каждый обязан красить тесты (шард ${{ matrix.shard }} из 10)"
|
||||
needs: material
|
||||
if: needs.material.outputs.reuse != 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6]
|
||||
# Шесть чересполосных шардов (#604): при четырёх шард нёс ~203 мутанта из
|
||||
# 810 и рос с реестром — 42 мин 10.09, 57 мин 20.09, 61 мин 21.09, и шард
|
||||
# 2/4 был снят по потолку без единого FAIL. Делитель тот же, что у
|
||||
# `changed_mutants` в Validate; число шардов повторяется в `--shard=i/6`,
|
||||
# `--shards=6` и имени job — тест `mutation-gate.test.mjs` держит их
|
||||
# равными. Бандл собирают только браузерные гварды. Час — потолок против
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
# #795: ночь 05.10 (1164 мутанта) заняла 42–60 мин на шести шардах;
|
||||
# четвёртый отменён по потолку. Десять шардов оставляют 116–117 мутантов
|
||||
# вместо 194. Оценка по длительностям той ночи — максимум около 39 мин
|
||||
# с запасом на setup; это прогноз, фактическое время проверяет следующая ночь.
|
||||
# Матрицу, делители runner/evidence и имя job держит mutation-gate.test.mjs.
|
||||
# Бандл собирают только браузерные гварды. Час — потолок против
|
||||
# зависшего Chromium, а не бюджет шарда: шард, упёршийся в него, — сигнал
|
||||
# снова делить, и отчёт (#472) называет такой шард прерванным, не «ok».
|
||||
timeout-minutes: 60
|
||||
@@ -178,7 +177,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p artifacts/mutation-shard-${{ matrix.shard }}
|
||||
set -o pipefail
|
||||
node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/6 2>&1 | tee artifacts/mutation-shard-${{ matrix.shard }}/mutation-shard-${{ matrix.shard }}.log
|
||||
node scripts/mutation-gate.mjs --shard=${{ matrix.shard }}/10 2>&1 | tee artifacts/mutation-shard-${{ matrix.shard }}/mutation-shard-${{ matrix.shard }}.log
|
||||
# Исход шага едет в evidence (#604): снятый по timeout-minutes шаг даёт
|
||||
# `cancelled`, и агрегатор отвергает такой шард как неполный — лог без
|
||||
# строк FAIL сам по себе зелёным не считается.
|
||||
@@ -191,7 +190,7 @@ jobs:
|
||||
--tree=${{ needs.material.outputs.tree }} \
|
||||
--workflow-sha=${{ job.workflow_sha }} \
|
||||
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }} \
|
||||
--shard=${{ matrix.shard }} --shards=6 \
|
||||
--shard=${{ matrix.shard }} --shards=10 \
|
||||
--outcome=${{ steps.gate.outcome }}
|
||||
- name: Сохранить лог и identity шарда
|
||||
if: always()
|
||||
@@ -202,7 +201,7 @@ jobs:
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
# Результат нельзя приписывать material, пока не доказаны все шесть шардов —
|
||||
# Результат нельзя приписывать material, пока не доказаны все десять шардов —
|
||||
# каждый с identity и с дошедшим до конца прогоном (#549, #604).
|
||||
# always() нужен при красном мутанте: лог красного шарда всё равно evidence.
|
||||
evidence:
|
||||
@@ -228,13 +227,13 @@ jobs:
|
||||
- name: Проверить полноту и identity
|
||||
run: |
|
||||
node scripts/mutation-gate-report.mjs --verify-only \
|
||||
--logs=artifacts/mutation-logs --shards=6 \
|
||||
--logs=artifacts/mutation-logs --shards=10 \
|
||||
--sha=${{ needs.material.outputs.sha }} \
|
||||
--tree=${{ needs.material.outputs.tree }} \
|
||||
--workflow-sha=${{ job.workflow_sha }} \
|
||||
--run-id=${{ github.run_id }} --run-attempt=${{ github.run_attempt }}
|
||||
|
||||
# #620: маркер пишется ТОЛЬКО после зелёного агрегатора — все шесть шардов
|
||||
# #620: маркер пишется ТОЛЬКО после зелёного агрегатора — все десять шардов
|
||||
# доказаны на одном material. Красный или неполный прогон маркера не оставляет,
|
||||
# и следующая ночь гонит реестр заново.
|
||||
green_marker:
|
||||
@@ -306,7 +305,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
node scripts/mutation-gate-report.mjs \
|
||||
--require-evidence --logs=artifacts/mutation-logs --shards=6 \
|
||||
--require-evidence --logs=artifacts/mutation-logs --shards=10 \
|
||||
--sha=${{ needs.material.outputs.sha }} \
|
||||
--tree=${{ needs.material.outputs.tree }} \
|
||||
--workflow-sha=${{ job.workflow_sha }} \
|
||||
|
||||
@@ -83,7 +83,9 @@ jobs:
|
||||
# снимок события об этом не знает, а исполнять отозванный запрос нельзя.
|
||||
current=$(gh issue view "$NUM" --repo "$REPO" --json labels --jq '.labels[].name')
|
||||
labels=$(printf '%s\n' "$current" | paste -sd, -)
|
||||
has() { printf '%s\n' "$current" | grep -qx -- "$1"; }
|
||||
# Не grep -q: раннее закрытие pipe даёт printf SIGPIPE, а pipefail
|
||||
# превращает найденную метку в ложный отказ (#793). Читаем до конца.
|
||||
has() { printf '%s\n' "$current" | grep -Fx -- "$1" > /dev/null; }
|
||||
BLOCKED=false; EXHAUSTED=false
|
||||
has blocked && BLOCKED=true
|
||||
has review-4 && EXHAUSTED=true
|
||||
@@ -1029,8 +1031,8 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
needs: [guard, prepare]
|
||||
# #696: `ship` в рамках сливается без модели — пакетное ревью диапазона
|
||||
# перед бетой (PROCESS.md §11.7, ship-review.yml).
|
||||
# #696/#727: `ship` в рамках сливается без модели — пакетное ревью ночью;
|
||||
# перед бетой проверка покрытия и непрочитанная дельта (PROCESS.md §11.7).
|
||||
if: needs.prepare.outputs.proceed == 'true' && needs.prepare.outputs.reuse != 'true' && needs.prepare.outputs.ship != 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
concurrency:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
name: "Бета: пакетное ревью ship · тело (#716)"
|
||||
|
||||
# #696, PROCESS.md §11.7: задачи `track:ship` сливаются без ревью модели —
|
||||
# правка в механических рамках и зелёный лёгкий Validate. Их код читает одна
|
||||
# сессия модели перед бетой: все ship-задачи диапазона «прошлый тег..кандидат».
|
||||
# Гейт публикации беты (`ship-review.mjs check`) требует этот документ, если в
|
||||
# диапазоне есть ship-задачи.
|
||||
# правка в механических рамках и зелёный лёгкий Validate. Их код читается
|
||||
# пакетно ночью; перед бетой — только непрочитанные или изменённые задачи.
|
||||
# Гейт публикации беты (`ship-review.mjs check`) требует покрытие всех ship-задач
|
||||
# диапазона «прошлый тег..кандидат»; ночного документа с тем же патч-набором
|
||||
# достаточно, отдельный документ тега беты нужен не всегда (#727).
|
||||
#
|
||||
# Три job, как у release-review.yml (#551, #556): детерминированный вход,
|
||||
# модель без права записи, детерминированная публикация документа в `dev`.
|
||||
@@ -375,7 +376,11 @@ jobs:
|
||||
commit -q -F "$msg"
|
||||
git diff --name-only "origin/dev...HEAD" | node scripts/review-doc-guard.mjs
|
||||
if git push -q "https://x-access-token:$TOKEN@github.com/${{ github.repository }}" HEAD:dev 2> "$push_err"; then
|
||||
echo "### Пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ "$MODE" = "nightly" ]; then
|
||||
echo "### Ночное пакетное ревью ship" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "### Предрелизное пакетное ревью ship $TAG" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "Задачи $ISSUES · High $high · Medium $medium · Low $low — \`$DOC\` в dev." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "::notice::$DOC опубликован: High $high · Medium $medium · Low $low"
|
||||
exit 0
|
||||
|
||||
@@ -608,14 +608,15 @@ patch-id кандидата слияния: вердикт к работе за
|
||||
| ТЗ | строка «что меняется и чем проверить» в теле issue под `## ТЗ` | «Оценка» (§7.2) и до трёх AC в теле issue под `## ТЗ` | полное ТЗ по §7.1 |
|
||||
| Ревью ТЗ | нет | нет | да, лимит 4 цикла |
|
||||
| Локальный гейт | `npm run gate:small` | `npm run gate:small` плюс смоуки `smoke-select` и AC | §8 |
|
||||
| Код-ревью | до слияния нет; пакетное ревью диапазона перед бетой | модель: корректность и AC | документ ревью, как в §2.7 |
|
||||
| Код-ревью | до слияния нет; пакетное ревью ночью, перед бетой — проверка покрытия и непрочитанная дельта (§11.7) | модель: корректность и AC | документ ревью, как в §2.7 |
|
||||
| Лимит циклов код-ревью | — | 2 | 4 |
|
||||
|
||||
**Рамки `track:ship`** механические: дифф `src/**` не больше 30 строк, без новых
|
||||
файлов в `src/**`, без ключей i18n, без полей конфига и без Python. Выход за
|
||||
рамки переводит задачу в `track:show`. Рамки и слияние `ship` без ревью модели
|
||||
проверяет и исполняет конвейер (§10.4, #696); код `ship` читает пакетное ревью
|
||||
диапазона перед бетой (§11.7).
|
||||
ночью заранее; перед бетой проверяется покрытие и читается непрочитанная дельта
|
||||
(§11.7).
|
||||
|
||||
**Проверка риска по изменённым участкам** (#707). Рамки считают строки и файлы,
|
||||
но не видят, что именно тронуто: обработчик `pointerdown` в двенадцать строк
|
||||
@@ -675,7 +676,8 @@ patch-id кандидата слияния: вердикт к работе за
|
||||
`ux-contract`, `perf-touch`, `undocumented`. Шаг решения по вердикту ставит
|
||||
`track:ask`, снимает `track:show` и переводит задачу в `S3-spec` комментарием:
|
||||
критерий словами, документы код-ревью, бюджет. Код остаётся в ветке, полное ТЗ
|
||||
пишется в теле issue, код класса A не пушится до `S5`; бюджет код-ревью не
|
||||
пишется в теле issue; до `S5` запрещён push всей ветки, включая тесты и документы
|
||||
(§11.8), а не только кода класса A; бюджет код-ревью не
|
||||
обнуляется (§4). Подтверждённый владельцем `show` конвейер не повышает: задача
|
||||
получает `blocked` и вопрос владельцу с вариантом по умолчанию «повысить до
|
||||
`ask`». `reclassify` без критерия из списка, на `ask` или на ревью ТЗ —
|
||||
@@ -1411,7 +1413,7 @@ limit`) и своей логики трека не держит; ответ comp
|
||||
|---|---|---|---|
|
||||
| Validate на материале | лёгкий; годится завершённый push-прогон на этом SHA | лёгкий, как у `ship` | лёгкий, как у `ship` (#709) |
|
||||
| Ребейз до ревью | нет, если `git merge-tree` с `dev` чистый | нет, если чистый | да (#257) |
|
||||
| Ревью модели | нет — пакетное ревью перед бетой (§11.7) | корректность и AC; окружение — по нужде | полное |
|
||||
| Ревью модели | нет — пакетное ревью ночью; перед бетой проверка покрытия и непрочитанная дельта (§11.7) | корректность и AC; окружение — по нужде | полное |
|
||||
| Проверка кандидата слияния | ждёт push-прогон кандидата; dispatch — только если его нет | как у `ship` | как у `ship` (#709) |
|
||||
|
||||
Мутантов по диффу нет ни на одном треке (#709, §2.7). Рамки `ship`
|
||||
@@ -1715,7 +1717,8 @@ Golden, браузерные смоки, performance и полный HA-харн
|
||||
|
||||
Это исключение из правила «каждое изменение проходит код-ревью» (§5, §7.1).
|
||||
Второе такое место — трек `ship` (§5): его ревью не отменяется, а переносится
|
||||
на пакетное ревью диапазона перед бетой. Здесь же исключение относится только к
|
||||
на пакетное ревью ночью и проверку покрытия с чтением непрочитанной дельты перед
|
||||
бетой (§11.7). Здесь же исключение относится только к
|
||||
окну между `S8-merged` и выпуском.
|
||||
|
||||
### 11.5 Независимое ревью линии перед стабильным релизом
|
||||
@@ -1784,7 +1787,9 @@ action ревью пускает ровно этого бота (`allowed_bots`,
|
||||
выпуск после такого отказа, исправляют причину и получают новый совместимый
|
||||
полный зелёный прогон на том же финальном SHA либо на новом SHA кандидата.
|
||||
|
||||
### 11.7 Пакетное ревью `ship` перед бетой
|
||||
<a id="117-пакетное-ревью-ship-перед-бетой"></a>
|
||||
|
||||
### 11.7 Пакетное ревью `ship`: ночью и перед бетой
|
||||
|
||||
Решение владельца 2026-09-28, issue #696.
|
||||
|
||||
@@ -1793,7 +1798,9 @@ action ревью пускает ровно этого бота (`allowed_bots`,
|
||||
до того, как она уйдёт пользователям беты. Одна сессия на все ship-задачи
|
||||
диапазона дешевле ревью на каждую, а рамки ship держат объём малым.
|
||||
|
||||
**Шаг.** Перед публикацией беты — `ship-review.yml`
|
||||
**Шаг.** Ночью код читается заранее (режим ниже). Перед публикацией беты
|
||||
проверяется покрытие; непрочитанные или изменённые после ревью задачи читает
|
||||
`ship-review.yml`
|
||||
(`gh workflow run ship-review.yml --ref dev -f tag=vX.Y.Z-beta.N` или кнопка
|
||||
«Run workflow» в Actions). Это тонкий вызывающий файл, он лежит и в `main`:
|
||||
без этого GitHub запуск не даёт. Тело `_ship-review.yml` читается из `dev` при
|
||||
|
||||
@@ -49,6 +49,10 @@ async def _async_flush_runtime(
|
||||
writers are independently guarded so one failed store cannot prevent the
|
||||
other from reaching disk during shutdown.
|
||||
"""
|
||||
zigbee = getattr(entry.runtime_data, "zigbee_coordinator", None)
|
||||
if zigbee is not None:
|
||||
await zigbee.async_close()
|
||||
|
||||
recorder = hass.data.get(DOMAIN, {}).get("trail_recorder")
|
||||
if recorder is not None:
|
||||
try:
|
||||
@@ -111,6 +115,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: HouseplanConfigEntry) ->
|
||||
_LOGGER.exception("House Plan: virtual-light storage is not readable; using default on")
|
||||
entry.runtime_data = data
|
||||
|
||||
from .zigbee_topology import ZigbeeScanCoordinator
|
||||
data.zigbee_coordinator = ZigbeeScanCoordinator(hass, entry)
|
||||
|
||||
from .radar import RadarCoordinator
|
||||
data.radar_coordinator = RadarCoordinator(hass, data)
|
||||
await data.radar_coordinator.async_setup()
|
||||
|
||||
@@ -57,7 +57,7 @@ SCHEDULED_GRACE_S = 30 * 24 * 3600
|
||||
FILES_DIR = "houseplan/files"
|
||||
ASSETS_DIR = "houseplan/assets"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.79.0"
|
||||
VERSION = "1.80.0-beta.6"
|
||||
DECOR_ASSETS_API_VERSION = 1
|
||||
SUMMARY_PANEL_API_VERSION = 1
|
||||
|
||||
|
||||
@@ -1,156 +1,170 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172",
|
||||
"fingerprint": "e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90",
|
||||
"entry": "houseplan-card.js",
|
||||
"panelEntry": "houseplan-panel.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 300133,
|
||||
"initialViewGzipBytes": 302803,
|
||||
"initialPanelFiles": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-panel.js"
|
||||
],
|
||||
"initialPanelGzipBytes": 301657,
|
||||
"initialPanelGzipBytes": 304326,
|
||||
"initialPanelOnlyFiles": [
|
||||
"houseplan-panel.js"
|
||||
],
|
||||
"initialPanelOnlyGzipBytes": 2328,
|
||||
"initialPanelOnlyGzipBytes": 2329,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-DHF8oaNH.js",
|
||||
"houseplan-assets/de-C4xCvmYr.js",
|
||||
"houseplan-assets/editor-CFo6BxSe.js",
|
||||
"houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"houseplan-assets/fr-BJJefRIG.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-XIg9ZqQe.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CqreRULk.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DOfjrx0M.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-BAaZNyAl.js",
|
||||
"houseplan-assets/iso-scene-render-CR7lxY-w.js",
|
||||
"houseplan-assets/led-strip-editor-kcNyoue_.js",
|
||||
"houseplan-assets/led-strip-geometry-DaLifxqY.js",
|
||||
"houseplan-assets/led-strip-runtime-Bx6qpdWk.js",
|
||||
"houseplan-assets/live-interaction-runtime-C1983oH8.js",
|
||||
"houseplan-assets/moon-runtime-Ca1mxhJl.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js",
|
||||
"houseplan-assets/pdf-export-C-Y4BSVu.js",
|
||||
"houseplan-assets/space-editor-EDOQT48H.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-CcLP5B5v.js",
|
||||
"houseplan-assets/zigbee-topology-BG_a6cIb.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-CynQ2m3K.js"
|
||||
"houseplan-assets/backdrop-pick-BAVj_WGz.js",
|
||||
"houseplan-assets/de-BwAuYefh.js",
|
||||
"houseplan-assets/editor-BDUl4r6P.js",
|
||||
"houseplan-assets/floating-surface-CJR0BNYY.js",
|
||||
"houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"houseplan-assets/fr-Tme1AtiE.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-CAzEkrsH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DDQB4TOP.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-UkuAhCnX.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-BRNEuIj9.js",
|
||||
"houseplan-assets/iso-scene-render-BrGpMlLk.js",
|
||||
"houseplan-assets/led-strip-editor-B6xkVxnD.js",
|
||||
"houseplan-assets/led-strip-geometry-DmI_yO1O.js",
|
||||
"houseplan-assets/led-strip-runtime-C5hDyvEc.js",
|
||||
"houseplan-assets/live-interaction-runtime-COtedrta.js",
|
||||
"houseplan-assets/live-tip-placement-CHOp_byC.js",
|
||||
"houseplan-assets/moon-runtime-Q0R6n-SW.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js",
|
||||
"houseplan-assets/pdf-export-MsPKGIGi.js",
|
||||
"houseplan-assets/space-editor-BDXGcIyy.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-CKq56nJ2.js",
|
||||
"houseplan-assets/zigbee-topology-Dv1GstKE.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-DXZC0gSe.js"
|
||||
],
|
||||
"lazyGzipBytes": 478817,
|
||||
"lazyGzipBytes": 486970,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-DHF8oaNH.js",
|
||||
"houseplan-assets/editor-CFo6BxSe.js",
|
||||
"houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-XIg9ZqQe.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CqreRULk.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js",
|
||||
"houseplan-assets/zigbee-topology-BG_a6cIb.js"
|
||||
"houseplan-assets/backdrop-pick-BAVj_WGz.js",
|
||||
"houseplan-assets/editor-BDUl4r6P.js",
|
||||
"houseplan-assets/floating-surface-CJR0BNYY.js",
|
||||
"houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-CAzEkrsH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DDQB4TOP.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js",
|
||||
"houseplan-assets/zigbee-topology-Dv1GstKE.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 240843,
|
||||
"lazyEditorGzipBytes": 244872,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-DHF8oaNH.js",
|
||||
"houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DOfjrx0M.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js"
|
||||
"houseplan-assets/backdrop-pick-BAVj_WGz.js",
|
||||
"houseplan-assets/floating-surface-CJR0BNYY.js",
|
||||
"houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-UkuAhCnX.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 28402,
|
||||
"lazyOnboardingGzipBytes": 28788,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-C4xCvmYr.js",
|
||||
"houseplan-assets/fr-BJJefRIG.js"
|
||||
"houseplan-assets/de-BwAuYefh.js",
|
||||
"houseplan-assets/fr-Tme1AtiE.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 53876,
|
||||
"lazyLocaleGzipBytes": 53914,
|
||||
"lazyIsometricFiles": [
|
||||
"houseplan-assets/iso-scene-render-CR7lxY-w.js"
|
||||
"houseplan-assets/iso-scene-render-BrGpMlLk.js"
|
||||
],
|
||||
"lazyIsometricGzipBytes": 12348,
|
||||
"lazyFurnitureArtFiles": [
|
||||
"houseplan-assets/furniture-plan-art.generated-XIg9ZqQe.js"
|
||||
"houseplan-assets/furniture-plan-art.generated-CAzEkrsH.js"
|
||||
],
|
||||
"lazyFurnitureArtGzipBytes": 16936,
|
||||
"lazyFurnitureArtGzipBytes": 16938,
|
||||
"lazyPdfFiles": [
|
||||
"houseplan-assets/furniture-plan-art.generated-XIg9ZqQe.js",
|
||||
"houseplan-assets/pdf-export-C-Y4BSVu.js"
|
||||
"houseplan-assets/furniture-plan-art.generated-CAzEkrsH.js",
|
||||
"houseplan-assets/pdf-export-MsPKGIGi.js"
|
||||
],
|
||||
"lazyPdfGzipBytes": 126934,
|
||||
"lazyPdfGzipBytes": 126936,
|
||||
"lazyMoonFiles": [
|
||||
"houseplan-assets/moon-runtime-Ca1mxhJl.js"
|
||||
"houseplan-assets/moon-runtime-Q0R6n-SW.js"
|
||||
],
|
||||
"lazyMoonGzipBytes": 11713,
|
||||
"lazyMoonGzipBytes": 11717,
|
||||
"lazyLedFiles": [
|
||||
"houseplan-assets/led-strip-field-ObbckCkp.js",
|
||||
"houseplan-assets/led-strip-geometry-DaLifxqY.js",
|
||||
"houseplan-assets/led-strip-runtime-Bx6qpdWk.js"
|
||||
"houseplan-assets/led-strip-field-3OQ_dqlz.js",
|
||||
"houseplan-assets/led-strip-geometry-DmI_yO1O.js",
|
||||
"houseplan-assets/led-strip-runtime-C5hDyvEc.js"
|
||||
],
|
||||
"lazyLedGzipBytes": 10613,
|
||||
"lazyLedGzipBytes": 10690,
|
||||
"lazyLedEditorFiles": [
|
||||
"houseplan-assets/led-strip-editor-kcNyoue_.js",
|
||||
"houseplan-assets/led-strip-geometry-DaLifxqY.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js"
|
||||
"houseplan-assets/led-strip-editor-B6xkVxnD.js",
|
||||
"houseplan-assets/led-strip-geometry-DmI_yO1O.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js"
|
||||
],
|
||||
"lazyLedEditorGzipBytes": 10707,
|
||||
"lazyLedEditorGzipBytes": 10708,
|
||||
"lazyNamespaceLocaleFiles": [
|
||||
"houseplan-assets/led-de-yp1GefDC.js",
|
||||
"houseplan-assets/led-fr-CHODsRJm.js",
|
||||
"houseplan-assets/led-ru-D2-9Yy6l.js",
|
||||
"houseplan-assets/settings-de-ZP6F6dIF.js",
|
||||
"houseplan-assets/settings-fr-doqd_TsB.js",
|
||||
"houseplan-assets/settings-ru-CQ1W2gik.js",
|
||||
"houseplan-assets/support-de-haZQ7q8i.js",
|
||||
"houseplan-assets/support-fr-DnRyZ_Ga.js",
|
||||
"houseplan-assets/support-ru-B7zzXsuL.js",
|
||||
"houseplan-assets/tools-de-CYXPPfUH.js",
|
||||
"houseplan-assets/tools-fr-DvygRB10.js",
|
||||
"houseplan-assets/tools-ru-2xmu2w_8.js",
|
||||
"houseplan-assets/topology-de-COda8n9n.js",
|
||||
"houseplan-assets/topology-fr-BT9yhzay.js",
|
||||
"houseplan-assets/topology-ru-C-d06yvD.js"
|
||||
"houseplan-assets/led-de-y6w9EwPN.js",
|
||||
"houseplan-assets/led-fr-Df8x1RGB.js",
|
||||
"houseplan-assets/led-ru-CVnmfAoy.js",
|
||||
"houseplan-assets/settings-de-fkMgRuOS.js",
|
||||
"houseplan-assets/settings-fr-BurubDQp.js",
|
||||
"houseplan-assets/settings-ru-DbsAT3jO.js",
|
||||
"houseplan-assets/support-de-DZK2ffma.js",
|
||||
"houseplan-assets/support-fr-rWwBzb8X.js",
|
||||
"houseplan-assets/support-ru-W1JCLO00.js",
|
||||
"houseplan-assets/tools-de-kcoefG9C.js",
|
||||
"houseplan-assets/tools-fr-DFLiNVPb.js",
|
||||
"houseplan-assets/tools-ru-uyo2DKol.js",
|
||||
"houseplan-assets/topology-de-C1i-yRwz.js",
|
||||
"houseplan-assets/topology-fr-bwG9CpyJ.js",
|
||||
"houseplan-assets/topology-ru-frHZA64p.js"
|
||||
],
|
||||
"lazyNamespaceLocaleGzipBytes": 30082,
|
||||
"lazyNamespaceLocaleGzipBytes": 32106,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-DHF8oaNH.js",
|
||||
"sha256": "7262c67e3f88f44ee6c6a0973073d1f7c3c9f177ff8254758a21c4e3867c5b06",
|
||||
"rawBytes": 55329,
|
||||
"gzipBytes": 18027,
|
||||
"path": "houseplan-assets/backdrop-pick-BAVj_WGz.js",
|
||||
"sha256": "7da5c7ce96085d80aaeb04d571f1d5f5ab53e65086be5a03edbf0d9e5afb1665",
|
||||
"rawBytes": 54713,
|
||||
"gzipBytes": 17804,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js"
|
||||
"houseplan-assets/floating-surface-CJR0BNYY.js",
|
||||
"houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/settings-de-ZP6F6dIF.js",
|
||||
"houseplan-assets/settings-fr-doqd_TsB.js",
|
||||
"houseplan-assets/settings-ru-CQ1W2gik.js"
|
||||
"houseplan-assets/settings-de-fkMgRuOS.js",
|
||||
"houseplan-assets/settings-fr-BurubDQp.js",
|
||||
"houseplan-assets/settings-ru-DbsAT3jO.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-C4xCvmYr.js",
|
||||
"sha256": "e3778d3139c05e075281f96460e15a6aaafe8dd9200306b0a3799a1bc5710828",
|
||||
"rawBytes": 89873,
|
||||
"gzipBytes": 27169,
|
||||
"path": "houseplan-assets/de-BwAuYefh.js",
|
||||
"sha256": "9115fe627d6b411ccb7d1335f4f0af3f088d04ff2d030f712abbca2121bc26f7",
|
||||
"rawBytes": 89941,
|
||||
"gzipBytes": 27190,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-CFo6BxSe.js",
|
||||
"sha256": "81d90441a06989a01a09c19e8c73567b80cd4d6ba52e4ea4796e096e1e5b7aa4",
|
||||
"path": "houseplan-assets/editor-BDUl4r6P.js",
|
||||
"sha256": "4b5fb130f259bc625a30f320fd773f7c6072b10214f354c0f44d99b8de8fbfe7",
|
||||
"rawBytes": 3833,
|
||||
"gzipBytes": 1584,
|
||||
"gzipBytes": 1587,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"sha256": "d18d3eb8bf1dd9dda6d6fdeed84edc16d3945e8a7e23b1d88e5f2edcef0a0af6",
|
||||
"path": "houseplan-assets/floating-surface-CJR0BNYY.js",
|
||||
"sha256": "b9f73ed7626f3701d300db6c979db5ddc65e421dd3be3780365a56187449d28c",
|
||||
"rawBytes": 1099,
|
||||
"gzipBytes": 578,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"sha256": "3bf6f059e1d882dee88c84babe6aed25b94f3a23930bddfba768b844ca2c861b",
|
||||
"rawBytes": 25605,
|
||||
"gzipBytes": 5059,
|
||||
"isEntry": false,
|
||||
@@ -158,114 +172,117 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-BJJefRIG.js",
|
||||
"sha256": "4932ff13f6fdc339e9d67123105f9bd97f051395a3c3310240fc07b119d87024",
|
||||
"rawBytes": 92266,
|
||||
"gzipBytes": 26707,
|
||||
"path": "houseplan-assets/fr-Tme1AtiE.js",
|
||||
"sha256": "850997cfc29fcabe2002d36d6e493166ca5a43e2f45748d7fd07b9c9d5106948",
|
||||
"rawBytes": 92335,
|
||||
"gzipBytes": 26724,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/furniture-plan-art.generated-XIg9ZqQe.js",
|
||||
"sha256": "10a0b09e4b1513bd02ffba047d6931add1d1106c453af03f6b3c778b5bb55e15",
|
||||
"path": "houseplan-assets/furniture-plan-art.generated-CAzEkrsH.js",
|
||||
"sha256": "4cb06d9a135afe113e8fdd555cc5dce36268b44927b9326fca10384005d769ce",
|
||||
"rawBytes": 52035,
|
||||
"gzipBytes": 16936,
|
||||
"gzipBytes": 16938,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"sha256": "37c784960d53789a66988a4e4ab8a8afe88aeddb80c5f244f3d453ef86c3ba8c",
|
||||
"rawBytes": 1056405,
|
||||
"gzipBytes": 299329,
|
||||
"path": "houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"sha256": "f3ad7abf8d6b2896edd0f85517b5a490dc17c4fc7e3b65a0396fc417c9ea8af2",
|
||||
"rawBytes": 1064618,
|
||||
"gzipBytes": 301997,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-C4xCvmYr.js",
|
||||
"houseplan-assets/editor-CFo6BxSe.js",
|
||||
"houseplan-assets/fr-BJJefRIG.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-XIg9ZqQe.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CqreRULk.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DOfjrx0M.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-BAaZNyAl.js",
|
||||
"houseplan-assets/iso-scene-render-CR7lxY-w.js",
|
||||
"houseplan-assets/led-strip-editor-kcNyoue_.js",
|
||||
"houseplan-assets/led-strip-runtime-Bx6qpdWk.js",
|
||||
"houseplan-assets/live-interaction-runtime-C1983oH8.js",
|
||||
"houseplan-assets/moon-runtime-Ca1mxhJl.js",
|
||||
"houseplan-assets/pdf-export-C-Y4BSVu.js",
|
||||
"houseplan-assets/space-editor-EDOQT48H.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-CcLP5B5v.js"
|
||||
"houseplan-assets/de-BwAuYefh.js",
|
||||
"houseplan-assets/editor-BDUl4r6P.js",
|
||||
"houseplan-assets/fr-Tme1AtiE.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-CAzEkrsH.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DDQB4TOP.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-UkuAhCnX.js",
|
||||
"houseplan-assets/hp-zigbee-topology-overlay-BRNEuIj9.js",
|
||||
"houseplan-assets/iso-scene-render-BrGpMlLk.js",
|
||||
"houseplan-assets/led-strip-editor-B6xkVxnD.js",
|
||||
"houseplan-assets/led-strip-runtime-C5hDyvEc.js",
|
||||
"houseplan-assets/live-interaction-runtime-COtedrta.js",
|
||||
"houseplan-assets/moon-runtime-Q0R6n-SW.js",
|
||||
"houseplan-assets/pdf-export-MsPKGIGi.js",
|
||||
"houseplan-assets/space-editor-BDXGcIyy.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-CKq56nJ2.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CqreRULk.js",
|
||||
"sha256": "772b538d5b87265a0e88117f01034a3ccadd5f4e928ced77cb99254ab74e299a",
|
||||
"rawBytes": 695353,
|
||||
"gzipBytes": 194530,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-DDQB4TOP.js",
|
||||
"sha256": "dc43fa8b5eb649a196eb4b64a2d27a24f7ac41638f66bb6c570d7b9b5cdb1b4d",
|
||||
"rawBytes": 701599,
|
||||
"gzipBytes": 196538,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-DHF8oaNH.js",
|
||||
"houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-XIg9ZqQe.js",
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js",
|
||||
"houseplan-assets/zigbee-topology-BG_a6cIb.js"
|
||||
"houseplan-assets/backdrop-pick-BAVj_WGz.js",
|
||||
"houseplan-assets/floating-surface-CJR0BNYY.js",
|
||||
"houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"houseplan-assets/furniture-plan-art.generated-CAzEkrsH.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js",
|
||||
"houseplan-assets/zigbee-topology-Dv1GstKE.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/support-de-haZQ7q8i.js",
|
||||
"houseplan-assets/support-fr-DnRyZ_Ga.js",
|
||||
"houseplan-assets/support-ru-B7zzXsuL.js",
|
||||
"houseplan-assets/tools-de-CYXPPfUH.js",
|
||||
"houseplan-assets/tools-fr-DvygRB10.js",
|
||||
"houseplan-assets/tools-ru-2xmu2w_8.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-CynQ2m3K.js"
|
||||
"houseplan-assets/support-de-DZK2ffma.js",
|
||||
"houseplan-assets/support-fr-rWwBzb8X.js",
|
||||
"houseplan-assets/support-ru-W1JCLO00.js",
|
||||
"houseplan-assets/tools-de-kcoefG9C.js",
|
||||
"houseplan-assets/tools-fr-DFLiNVPb.js",
|
||||
"houseplan-assets/tools-ru-uyo2DKol.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-DXZC0gSe.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DOfjrx0M.js",
|
||||
"sha256": "242ae570916ec2a000a8a0079a948826faeb45c0968cc1006e9670f9d3c4088b",
|
||||
"rawBytes": 15886,
|
||||
"gzipBytes": 4907,
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-UkuAhCnX.js",
|
||||
"sha256": "988ea120db3de7adde8cbb55da77fd01866dd08329cb45516193f06e157589d0",
|
||||
"rawBytes": 15930,
|
||||
"gzipBytes": 4934,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-DHF8oaNH.js",
|
||||
"houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js"
|
||||
"houseplan-assets/backdrop-pick-BAVj_WGz.js",
|
||||
"houseplan-assets/floating-surface-CJR0BNYY.js",
|
||||
"houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/hp-zigbee-topology-overlay-BAaZNyAl.js",
|
||||
"sha256": "8cb7c3f010cf7ef792a676bfd994c4498f31222dd12ded79e3e2f37dfae4f52e",
|
||||
"rawBytes": 10958,
|
||||
"gzipBytes": 3768,
|
||||
"path": "houseplan-assets/hp-zigbee-topology-overlay-BRNEuIj9.js",
|
||||
"sha256": "ec3c46d13bd8db52b49b6c38385836508374dcff0678ff4325bf1eeb8778b015",
|
||||
"rawBytes": 16748,
|
||||
"gzipBytes": 5536,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js",
|
||||
"houseplan-assets/zigbee-topology-BG_a6cIb.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-CynQ2m3K.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/live-tip-placement-CHOp_byC.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js",
|
||||
"houseplan-assets/zigbee-topology-Dv1GstKE.js",
|
||||
"houseplan-assets/zigbee-topology-runtime-DXZC0gSe.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/iso-scene-render-CR7lxY-w.js",
|
||||
"sha256": "1986dd43c8b76580f893c1629d494f7747f02aa8d544baff20d56fe10b470bb5",
|
||||
"rawBytes": 36700,
|
||||
"path": "houseplan-assets/iso-scene-render-BrGpMlLk.js",
|
||||
"sha256": "bbac5bbbda816178018163d5f4857851dd16e3ec5a98366e14ba66e44cd9ce1d",
|
||||
"rawBytes": 36702,
|
||||
"gzipBytes": 12348,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-de-yp1GefDC.js",
|
||||
"sha256": "eb9ce8bb536ac20e11cf4fe925d68f28b1ba8a5ea02c61abf9c3bb939de0a4c4",
|
||||
"path": "houseplan-assets/led-de-y6w9EwPN.js",
|
||||
"sha256": "4923f6632899f7a2b22797cad5cb4dc47290b4c991e5d57805ac1f4f1baa0d52",
|
||||
"rawBytes": 1907,
|
||||
"gzipBytes": 921,
|
||||
"isEntry": false,
|
||||
@@ -273,17 +290,17 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-fr-CHODsRJm.js",
|
||||
"sha256": "adb428bd63b6157ef0a5d749badf94e95204c9c3b83e5f3022accf65d304435e",
|
||||
"path": "houseplan-assets/led-fr-Df8x1RGB.js",
|
||||
"sha256": "a3cb61d1db7726ebe86ce304f74d037ab6b8467b8abe763cfd2cf5438a72750d",
|
||||
"rawBytes": 1886,
|
||||
"gzipBytes": 906,
|
||||
"gzipBytes": 905,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-ru-D2-9Yy6l.js",
|
||||
"sha256": "bc5575139b30d6e93e07673f47865c332e71011796929a52043645a904d6df31",
|
||||
"path": "houseplan-assets/led-ru-CVnmfAoy.js",
|
||||
"sha256": "a6cc4f59f9427953ccc395984591f7b216b2797e21314e7c63082e327cb16e2c",
|
||||
"rawBytes": 2525,
|
||||
"gzipBytes": 1106,
|
||||
"isEntry": false,
|
||||
@@ -291,191 +308,202 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-strip-editor-kcNyoue_.js",
|
||||
"sha256": "dac9fdcaae6444caa1773a07384155cc92b916d2ebf3498e1dfc1b3525a84753",
|
||||
"path": "houseplan-assets/led-strip-editor-B6xkVxnD.js",
|
||||
"sha256": "ea5ca1a3d83eb684f90605e4341c5fd403220d789ab07a093511c90bd9132aea",
|
||||
"rawBytes": 21704,
|
||||
"gzipBytes": 7381,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/led-strip-geometry-DaLifxqY.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/led-strip-geometry-DmI_yO1O.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/led-de-yp1GefDC.js",
|
||||
"houseplan-assets/led-fr-CHODsRJm.js",
|
||||
"houseplan-assets/led-ru-D2-9Yy6l.js"
|
||||
"houseplan-assets/led-de-y6w9EwPN.js",
|
||||
"houseplan-assets/led-fr-Df8x1RGB.js",
|
||||
"houseplan-assets/led-ru-CVnmfAoy.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-strip-field-ObbckCkp.js",
|
||||
"sha256": "b85d8de01906a7e70f28baff534cf1a211216c99b72005b6d77a0d19cbb5ce8f",
|
||||
"rawBytes": 7920,
|
||||
"gzipBytes": 3321,
|
||||
"path": "houseplan-assets/led-strip-field-3OQ_dqlz.js",
|
||||
"sha256": "c3eb37227352c845f79c09622652d145e62484f59aad5ea69182476db679674d",
|
||||
"rawBytes": 8066,
|
||||
"gzipBytes": 3384,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/led-strip-geometry-DaLifxqY.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/led-strip-geometry-DmI_yO1O.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-strip-geometry-DaLifxqY.js",
|
||||
"sha256": "2dfbe1509da809008c9a525a7ed02226c026aaa5c0dafcb9f3dc1c44151ed63e",
|
||||
"path": "houseplan-assets/led-strip-geometry-DmI_yO1O.js",
|
||||
"sha256": "f45162dab9bd519f797c0cfc29c80f16bb072caff3eed8f5040a75492f69120f",
|
||||
"rawBytes": 7055,
|
||||
"gzipBytes": 2917,
|
||||
"gzipBytes": 2914,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/led-strip-runtime-Bx6qpdWk.js",
|
||||
"sha256": "6fd2d3578e550dfbe8c38308923e21f9fddeb469f64e8fcf295dc292949ddf32",
|
||||
"rawBytes": 11179,
|
||||
"gzipBytes": 4375,
|
||||
"path": "houseplan-assets/led-strip-runtime-C5hDyvEc.js",
|
||||
"sha256": "2350809b6bce23cc94d4a1611185be1d8aea1a01d8e6fd03fe7c78a63551b0d4",
|
||||
"rawBytes": 11248,
|
||||
"gzipBytes": 4392,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/led-strip-geometry-DaLifxqY.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/led-strip-geometry-DmI_yO1O.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/led-strip-field-ObbckCkp.js"
|
||||
"houseplan-assets/led-strip-field-3OQ_dqlz.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/live-interaction-runtime-C1983oH8.js",
|
||||
"sha256": "04be552ac07f99c03713f162ce0b6129c8491dfd89da9cd8e07cd71866fe309c",
|
||||
"rawBytes": 8370,
|
||||
"gzipBytes": 3281,
|
||||
"path": "houseplan-assets/live-interaction-runtime-COtedrta.js",
|
||||
"sha256": "1ca98d6e0625543f35b06a495cc361e98ac524c6a8d6cf4e8299a39bd4038c0f",
|
||||
"rawBytes": 9694,
|
||||
"gzipBytes": 3782,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/floating-surface-CJR0BNYY.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/live-tip-placement-CHOp_byC.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/moon-runtime-Ca1mxhJl.js",
|
||||
"sha256": "97b9f55cb8e2915acb5eaa260f21e2103bc08f0e654c1c0b03c3000604319730",
|
||||
"path": "houseplan-assets/live-tip-placement-CHOp_byC.js",
|
||||
"sha256": "edaa20153bf41febe6878287e41d8e0976709d8b4be31f57d3562804be12e91c",
|
||||
"rawBytes": 1191,
|
||||
"gzipBytes": 683,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/moon-runtime-Q0R6n-SW.js",
|
||||
"sha256": "34932bced050fbc8256fa46ac6927027e296aee456ac0555ed3f7b00d2c37627",
|
||||
"rawBytes": 30292,
|
||||
"gzipBytes": 11713,
|
||||
"gzipBytes": 11717,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/namespace-language-D86tJUeD.js",
|
||||
"sha256": "012aa30cbf8bbbb2eff6b7ef05970ff90fb2b4b576910578e3527b75e68ffda6",
|
||||
"path": "houseplan-assets/namespace-language-DCOgYCJW.js",
|
||||
"sha256": "d9921ce868bf6038a88c86fdf7acf8192df4f945dbde68394e7a0b67ced1a4d8",
|
||||
"rawBytes": 675,
|
||||
"gzipBytes": 409,
|
||||
"gzipBytes": 413,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/pdf-export-C-Y4BSVu.js",
|
||||
"sha256": "fba700bc14c66c695f1b3de3e82abdf5fb6b14b70773c3d0f0e185dcf2989491",
|
||||
"path": "houseplan-assets/pdf-export-MsPKGIGi.js",
|
||||
"sha256": "b6c875721cb17c948119b935dbcbc748bd475229cbb57c1e643f6b74bf595c17",
|
||||
"rawBytes": 236714,
|
||||
"gzipBytes": 109998,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/furniture-plan-art.generated-XIg9ZqQe.js",
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/furniture-plan-art.generated-CAzEkrsH.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-de-ZP6F6dIF.js",
|
||||
"sha256": "12dd3028d2de2e4fc3bfcf2299e45e253075ca23b95235436ef1cf9156738a3e",
|
||||
"rawBytes": 8886,
|
||||
"gzipBytes": 3411,
|
||||
"path": "houseplan-assets/settings-de-fkMgRuOS.js",
|
||||
"sha256": "cd88052db22e6a38d1be593fa7b36db11a1904e8583d04c4ea3c6ad8f92acd6d",
|
||||
"rawBytes": 9199,
|
||||
"gzipBytes": 3519,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-fr-doqd_TsB.js",
|
||||
"sha256": "0c1f01b69c2b1704ddd29e99dd7609ad679655b5241b3e2b8921057198005ff3",
|
||||
"rawBytes": 9297,
|
||||
"gzipBytes": 3385,
|
||||
"path": "houseplan-assets/settings-fr-BurubDQp.js",
|
||||
"sha256": "96524242f4bb186e8b03c4074b14c7e855ec3431d0b2ced3b0cde344c00da7f5",
|
||||
"rawBytes": 9644,
|
||||
"gzipBytes": 3495,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/settings-ru-CQ1W2gik.js",
|
||||
"sha256": "bad5c925aac84429d0831a1036ea3162ead8d4431db35ea577b0e806c8fbdbf1",
|
||||
"rawBytes": 12284,
|
||||
"gzipBytes": 3900,
|
||||
"path": "houseplan-assets/settings-ru-DbsAT3jO.js",
|
||||
"sha256": "cceb53637a06b08e30cad5f65c576647913eed554220bed9a77fd048f96e6956",
|
||||
"rawBytes": 12691,
|
||||
"gzipBytes": 4020,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/space-editor-EDOQT48H.js",
|
||||
"sha256": "47339ab7329ffd1a0cbd21980c1cf2a2041d7a514ed5992e73a07abdce6fb764",
|
||||
"path": "houseplan-assets/space-editor-BDXGcIyy.js",
|
||||
"sha256": "81eef57aaa35af23f5f9b26e6cd1376f9337ad01c7d4cbd80c232c6c1f7fec44",
|
||||
"rawBytes": 2619,
|
||||
"gzipBytes": 1130,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-editor-BZnGwuVh.js",
|
||||
"sha256": "516e3462d3896a9ca782ee0f2f3266a4da6e6b6664e92a619a23b8991e600795",
|
||||
"rawBytes": 14862,
|
||||
"gzipBytes": 3427,
|
||||
"path": "houseplan-assets/summary-panel-editor-YvarhxkQ.js",
|
||||
"sha256": "c1add22aaddf1992f7ba8a48894c51f3d45243f40d13d378ca463fe438c861c3",
|
||||
"rawBytes": 14867,
|
||||
"gzipBytes": 3431,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-CcLP5B5v.js"
|
||||
"houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/summary-panel-runtime-loaded-CKq56nJ2.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-metrics-D_ZXPJxz.js",
|
||||
"sha256": "3be26833b041416cef359e4c7086a0b0e6788680dda8f1583ae76d9256f944fa",
|
||||
"path": "houseplan-assets/summary-panel-metrics-CRyrp3Mg.js",
|
||||
"sha256": "285c0d9e3fa1895c1c6a7f7031638593aae1ca6f638f1743be7cbaac75a66b2c",
|
||||
"rawBytes": 2701,
|
||||
"gzipBytes": 1348,
|
||||
"gzipBytes": 1346,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/summary-panel-runtime-loaded-CcLP5B5v.js",
|
||||
"sha256": "7db94205b54996355041487465d6bade0530e5ac0d13a0eb9b7645086171ce58",
|
||||
"path": "houseplan-assets/summary-panel-runtime-loaded-CKq56nJ2.js",
|
||||
"sha256": "8a49df291ab60682540c857ec8490ac809b9f9430a05fc1c7bd99c7dbd2b4492",
|
||||
"rawBytes": 73476,
|
||||
"gzipBytes": 20491,
|
||||
"gzipBytes": 20490,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/form-kit.styles-B6HsmMPA.js",
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/form-kit.styles-DLwU23Kk.js",
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/summary-panel-editor-BZnGwuVh.js",
|
||||
"houseplan-assets/summary-panel-metrics-D_ZXPJxz.js"
|
||||
"houseplan-assets/summary-panel-editor-YvarhxkQ.js",
|
||||
"houseplan-assets/summary-panel-metrics-CRyrp3Mg.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-de-haZQ7q8i.js",
|
||||
"sha256": "f377e9d552978c850f069eeb05b20c7572f81c7a8c7b2d60838a7907f1c2b3ef",
|
||||
"path": "houseplan-assets/support-de-DZK2ffma.js",
|
||||
"sha256": "206b9e96a87a78befee2935491261281267eb1b92bce58d3b1819ece6d92cbde",
|
||||
"rawBytes": 6167,
|
||||
"gzipBytes": 2400,
|
||||
"gzipBytes": 2398,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-fr-DnRyZ_Ga.js",
|
||||
"sha256": "bb66f839f3b63a74212dc83d273db535dd4597e5f43c5c1b35f0e1fac401e424",
|
||||
"path": "houseplan-assets/support-fr-rWwBzb8X.js",
|
||||
"sha256": "d969c3f68547508bc15e0bf4025b2e59565371fa57dcc13bc7a0d9c7057eb19d",
|
||||
"rawBytes": 6333,
|
||||
"gzipBytes": 2433,
|
||||
"isEntry": false,
|
||||
@@ -483,8 +511,8 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/support-ru-B7zzXsuL.js",
|
||||
"sha256": "63ec59059f3a10c994e4292c4c2b125bb7b2916326dbae27f9975c14be008822",
|
||||
"path": "houseplan-assets/support-ru-W1JCLO00.js",
|
||||
"sha256": "119e9ae1edecf07ea915bafa810a98b566f4de5f50cd0cf7d6450fadbd1d525a",
|
||||
"rawBytes": 8376,
|
||||
"gzipBytes": 2878,
|
||||
"isEntry": false,
|
||||
@@ -492,17 +520,17 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/tools-de-CYXPPfUH.js",
|
||||
"sha256": "411c98cf876e42d7255882d0faa486602453f871d443b0c2b59c22e3b2552efb",
|
||||
"path": "houseplan-assets/tools-de-kcoefG9C.js",
|
||||
"sha256": "103c95c7c817e3220f9481ea4c2558b99b5d3237b2f014809640f97f79231e24",
|
||||
"rawBytes": 4130,
|
||||
"gzipBytes": 1378,
|
||||
"gzipBytes": 1376,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/tools-fr-DvygRB10.js",
|
||||
"sha256": "f7d35402d5f3504baa8c00dc8bd5170487535c1b87f9a8367a28e332cda92bf2",
|
||||
"path": "houseplan-assets/tools-fr-DFLiNVPb.js",
|
||||
"sha256": "c5a0c5bba87a892a1b8e155b6e6ebbab46dc9c4f6d85fcddd38fc28e0f8e2b5b",
|
||||
"rawBytes": 4136,
|
||||
"gzipBytes": 1360,
|
||||
"isEntry": false,
|
||||
@@ -510,8 +538,8 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/tools-ru-2xmu2w_8.js",
|
||||
"sha256": "14e6c4c22d615293903fc4f25d831f0af042b4e48234d4f2e114ef7d8817d57a",
|
||||
"path": "houseplan-assets/tools-ru-uyo2DKol.js",
|
||||
"sha256": "6517d93766e36a0a3aa26d757a69e04df340ea4d0e57c8b410f459fd59417821",
|
||||
"rawBytes": 5633,
|
||||
"gzipBytes": 1697,
|
||||
"isEntry": false,
|
||||
@@ -519,80 +547,80 @@
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-de-COda8n9n.js",
|
||||
"sha256": "88ff4a8cccc94e3eba040af628d9a85db664cbf20586f95704e0823a6215c1f9",
|
||||
"rawBytes": 2698,
|
||||
"gzipBytes": 1336,
|
||||
"path": "houseplan-assets/topology-de-C1i-yRwz.js",
|
||||
"sha256": "c034120a336f1118b43972f9daea6a0d3942cf7544aca519e6f8cdc183ff5e87",
|
||||
"rawBytes": 4237,
|
||||
"gzipBytes": 1889,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-fr-BT9yhzay.js",
|
||||
"sha256": "60cb68c5c9086ec5f4901627a6baa3e3c8cdfda430b0977622b260984122fcfa",
|
||||
"rawBytes": 2718,
|
||||
"gzipBytes": 1356,
|
||||
"path": "houseplan-assets/topology-fr-bwG9CpyJ.js",
|
||||
"sha256": "c419184f8be8c2c3e342b767af95bb6d23ef8655d50fe759f926d4747ed7623b",
|
||||
"rawBytes": 4321,
|
||||
"gzipBytes": 1871,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/topology-ru-C-d06yvD.js",
|
||||
"sha256": "ca9a68f252b8275c954ae7ff7ea56d07a8ea5cf14f18eaac285fd0c63a7bad8f",
|
||||
"rawBytes": 3669,
|
||||
"gzipBytes": 1615,
|
||||
"path": "houseplan-assets/topology-ru-frHZA64p.js",
|
||||
"sha256": "61bc5e1c3255fbd7ce70909b20c02446b2cee81bde432b58db84a4fb1d3d9030",
|
||||
"rawBytes": 5880,
|
||||
"gzipBytes": 2238,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/zigbee-topology-BG_a6cIb.js",
|
||||
"sha256": "98d8e54ec59e2d1810b124a0a0c6c2f528c112ab17d12bc3c0a129e13c456c72",
|
||||
"rawBytes": 10958,
|
||||
"gzipBytes": 4298,
|
||||
"path": "houseplan-assets/zigbee-topology-Dv1GstKE.js",
|
||||
"sha256": "0cefc405c44a0fa547feb5f1d2472f2fbb0360069c9d6f42e703dc78271f3cdd",
|
||||
"rawBytes": 15905,
|
||||
"gzipBytes": 5955,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/topology-de-COda8n9n.js",
|
||||
"houseplan-assets/topology-fr-BT9yhzay.js",
|
||||
"houseplan-assets/topology-ru-C-d06yvD.js"
|
||||
"houseplan-assets/topology-de-C1i-yRwz.js",
|
||||
"houseplan-assets/topology-fr-bwG9CpyJ.js",
|
||||
"houseplan-assets/topology-ru-frHZA64p.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/zigbee-topology-runtime-CynQ2m3K.js",
|
||||
"sha256": "966ef9fa87888f0ed561d1061b36f0192c27700f040305ae038fe9509c7a7e33",
|
||||
"rawBytes": 3983,
|
||||
"gzipBytes": 1789,
|
||||
"path": "houseplan-assets/zigbee-topology-runtime-DXZC0gSe.js",
|
||||
"sha256": "0c4c56c1c8be79334933880c6b3b2bcd1fac208fffd3e9b67daec6ab248a7f97",
|
||||
"rawBytes": 7883,
|
||||
"gzipBytes": 2879,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js",
|
||||
"houseplan-assets/namespace-language-D86tJUeD.js",
|
||||
"houseplan-assets/zigbee-topology-BG_a6cIb.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js",
|
||||
"houseplan-assets/namespace-language-DCOgYCJW.js",
|
||||
"houseplan-assets/zigbee-topology-Dv1GstKE.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "80338f4a64b84431f95438482aeabe1cc6b0f399ccb0c600b318c0f6e6cccc38",
|
||||
"sha256": "161e09bbe9c34771f5f48c8cb857d9bcdfae97eee181105d3d3c5961be6634e8",
|
||||
"rawBytes": 1205,
|
||||
"gzipBytes": 804,
|
||||
"gzipBytes": 806,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-panel.js",
|
||||
"sha256": "ce8c313af85073d1c54ccc74e2111c1f14c5d8e3e8539bc59ec436d6d3e0d574",
|
||||
"sha256": "2be7e9e89681077323cc8f6f39ad7df4ca88c2d38d34174b3618da98a466966b",
|
||||
"rawBytes": 6165,
|
||||
"gzipBytes": 2328,
|
||||
"gzipBytes": 2329,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-BOdiRDFR.js"
|
||||
"houseplan-assets/houseplan-card-DrYGKEMS.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";import{aG as e,l as o,aJ as t,eE as s,A as a,eF as i,eG as l,eH as n,E as r,c}from"./houseplan-card-BOdiRDFR.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},_=this._schema,f=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),u=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";import{aJ as e,l as o,aM as t,eK as s,A as a,eL as i,eM as l,eN as n,E as r,c}from"./houseplan-card-DrYGKEMS.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${u(_.slice(0,3))}
|
||||
${f?c`<div class="default-floor-error" role="alert"
|
||||
${d(f.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:f})}
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${u(_.slice(3))}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
@@ -0,0 +1 @@
|
||||
function t(t){const h=t.visualViewport;return h&&Number.isFinite(h.width)&&Number.isFinite(h.height)?{left:Number(h.offsetLeft)||0,top:Number(h.offsetTop)||0,width:Math.max(0,Number(h.width)||0),height:Math.max(0,Number(h.height)||0)}:{left:0,top:0,width:t.innerWidth,height:t.innerHeight}}function h(t,h,a,e=7,i=8){const o=a.left+i,n=a.top+i,m=Math.max(o,a.left+a.width-i),r=Math.max(n,a.top+a.height-i),M=Math.max(0,m-o),f=Math.max(0,r-n),d=Math.min(Math.max(0,h.width),M),u=Math.min(Math.max(0,h.height),f);let p=t.left;p+d>m&&(p=t.right-d),p=Math.min(Math.max(o,p),Math.max(o,m-d));const x=t.bottom+e,b=t.top-e-u;let s="bottom",c=x;return x+u>r&&b>=n?(s="top",c=Math.min(Math.max(n,b),Math.max(n,r-u))):c=Math.min(Math.max(n,x),Math.max(n,r-u)),{left:Math.round(p),top:Math.round(c),side:s,maxWidth:Math.round(M),maxHeight:Math.round(f)}}function a(t){const h=t.HTMLElement?.prototype;return"function"==typeof h?.showPopover&&"function"==typeof h?.hidePopover}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";export{a,t as f,h as p};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";var e={"led.tool":"LED-Streifen","led.hint":"Zeichnen Sie den Streifen Punkt für Punkt entlang der Wandflächen oder über den Boden; er rastet am Raster und an Wandflächen ein. Danach wählen Sie das Gerät, das er darstellt.","led.start":"Klicken, um den Streifen zu beginnen","led.start_touch":"Tippen, um den Streifen zu beginnen","led.points":"Punkte: {n} · Umschalt — 45°-Schritte · Esc — fertig · Strg+Z — Punkt entfernen","led.points_touch":"Punkte: {n} · Werkzeug wechseln zum Beenden","led.finish":"Fertig","led.stopped":"Der Streifen stößt an eine Wand","led.unbound":"Streifen ist mit keinem Gerät verbunden","led.bind":"Gerät verbinden","led.change":"Gerät wechseln","led.later":"Später","led.unbind":"Trennen","led.delete":"Streifen löschen","led.settings":"Geräteeinstellungen","led.type_note":"LED-Streifen: Form im Geräte-Editor auf dem Plan bearbeiten","led.show_strip":"Als LED-Streifen zeigen","led.show_icon":"Als Symbol zeigen","led.cancel":"Abbrechen","led.taken":"Bereits mit einem anderen LED-Streifen verbunden","led.space":"Zuerst den LED-Streifen lösen: Er gehört zu seinem Bereich","led.limit_points":"Höchstens 50 Punkte pro Streifen","led.limit_strips":"Höchstens 50 Streifen pro Bereich","led.pick_title":"Gerät für den Streifen wählen","led.pick_new":"Neues Gerät…","led.pick_empty":"Auf diesem Plan gibt es noch keine freien Geräte.","led.saved":"LED-Streifen gespeichert","led.save_failed":"Streifen nicht gespeichert: {err}","led.history_draw":"LED-Streifen","led.history_shape":"Form des LED-Streifens","led.history_view":"Darstellung des Geräts","led.optimize_walls":"Streifen durch Wände: {n} ({spaces})"};const n="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";export{e as dictionary,n as fingerprint};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";var e={"led.tool":"LED-Streifen","led.hint":"Zeichnen Sie den Streifen Punkt für Punkt entlang der Wandflächen oder über den Boden; er rastet am Raster und an Wandflächen ein. Danach wählen Sie das Gerät, das er darstellt.","led.start":"Klicken, um den Streifen zu beginnen","led.start_touch":"Tippen, um den Streifen zu beginnen","led.points":"Punkte: {n} · Umschalt — 45°-Schritte · Esc — fertig · Strg+Z — Punkt entfernen","led.points_touch":"Punkte: {n} · Werkzeug wechseln zum Beenden","led.finish":"Fertig","led.stopped":"Der Streifen stößt an eine Wand","led.unbound":"Streifen ist mit keinem Gerät verbunden","led.bind":"Gerät verbinden","led.change":"Gerät wechseln","led.later":"Später","led.unbind":"Trennen","led.delete":"Streifen löschen","led.settings":"Geräteeinstellungen","led.type_note":"LED-Streifen: Form im Geräte-Editor auf dem Plan bearbeiten","led.show_strip":"Als LED-Streifen zeigen","led.show_icon":"Als Symbol zeigen","led.cancel":"Abbrechen","led.taken":"Bereits mit einem anderen LED-Streifen verbunden","led.space":"Zuerst den LED-Streifen lösen: Er gehört zu seinem Bereich","led.limit_points":"Höchstens 50 Punkte pro Streifen","led.limit_strips":"Höchstens 50 Streifen pro Bereich","led.pick_title":"Gerät für den Streifen wählen","led.pick_new":"Neues Gerät…","led.pick_empty":"Auf diesem Plan gibt es noch keine freien Geräte.","led.saved":"LED-Streifen gespeichert","led.save_failed":"Streifen nicht gespeichert: {err}","led.history_draw":"LED-Streifen","led.history_shape":"Form des LED-Streifens","led.history_view":"Darstellung des Geräts","led.optimize_walls":"Streifen durch Wände: {n} ({spaces})"};const n="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";export{e as dictionary,n as fingerprint};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";var e={"led.tool":"Ruban LED","led.hint":"Tracez le ruban point par point le long des faces des murs ou sur le sol ; il s’aimante à la grille et aux faces des murs. Ensuite, choisissez l’appareil qu’il représente.","led.start":"Cliquez pour commencer le ruban","led.start_touch":"Touchez pour commencer le ruban","led.points":"Points : {n} · Maj — pas de 45° · Échap — terminer · Ctrl+Z — retirer le point","led.points_touch":"Points : {n} · changez d’outil pour terminer","led.finish":"Terminer","led.stopped":"Le ruban bute contre un mur","led.unbound":"Ruban non lié à un appareil","led.bind":"Lier un appareil","led.change":"Changer d’appareil","led.later":"Plus tard","led.unbind":"Délier","led.delete":"Supprimer le ruban","led.settings":"Réglages de l’appareil","led.type_note":"Ruban LED : modifiez sa forme sur le plan dans l’éditeur d’appareils","led.show_strip":"Afficher en ruban LED","led.show_icon":"Afficher en icône","led.cancel":"Annuler","led.taken":"Déjà lié à un autre ruban LED","led.space":"Détachez d’abord le ruban LED : il appartient à son espace","led.limit_points":"50 points au maximum par ruban","led.limit_strips":"50 rubans au maximum par espace","led.pick_title":"Choisissez l’appareil du ruban","led.pick_new":"Nouvel appareil…","led.pick_empty":"Aucun appareil libre sur ce plan pour l’instant.","led.saved":"Ruban LED enregistré","led.save_failed":"Ruban non enregistré : {err}","led.history_draw":"Ruban LED","led.history_shape":"Forme du ruban LED","led.history_view":"Représentation de l’appareil","led.optimize_walls":"Rubans traversant des murs : {n} ({spaces})"};const a="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";export{e as dictionary,a as fingerprint};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";var e={"led.tool":"Ruban LED","led.hint":"Tracez le ruban point par point le long des faces des murs ou sur le sol ; il s’aimante à la grille et aux faces des murs. Ensuite, choisissez l’appareil qu’il représente.","led.start":"Cliquez pour commencer le ruban","led.start_touch":"Touchez pour commencer le ruban","led.points":"Points : {n} · Maj — pas de 45° · Échap — terminer · Ctrl+Z — retirer le point","led.points_touch":"Points : {n} · changez d’outil pour terminer","led.finish":"Terminer","led.stopped":"Le ruban bute contre un mur","led.unbound":"Ruban non lié à un appareil","led.bind":"Lier un appareil","led.change":"Changer d’appareil","led.later":"Plus tard","led.unbind":"Délier","led.delete":"Supprimer le ruban","led.settings":"Réglages de l’appareil","led.type_note":"Ruban LED : modifiez sa forme sur le plan dans l’éditeur d’appareils","led.show_strip":"Afficher en ruban LED","led.show_icon":"Afficher en icône","led.cancel":"Annuler","led.taken":"Déjà lié à un autre ruban LED","led.space":"Détachez d’abord le ruban LED : il appartient à son espace","led.limit_points":"50 points au maximum par ruban","led.limit_strips":"50 rubans au maximum par espace","led.pick_title":"Choisissez l’appareil du ruban","led.pick_new":"Nouvel appareil…","led.pick_empty":"Aucun appareil libre sur ce plan pour l’instant.","led.saved":"Ruban LED enregistré","led.save_failed":"Ruban non enregistré : {err}","led.history_draw":"Ruban LED","led.history_shape":"Forme du ruban LED","led.history_view":"Représentation de l’appareil","led.optimize_walls":"Rubans traversant des murs : {n} ({spaces})"};const a="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";export{e as dictionary,a as fingerprint};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";var e={"led.tool":"LED-лента","led.hint":"Нарисуйте ленту по точкам вдоль граней стен или по полу — она прилипает к сетке и граням. После завершения выберите устройство, которое она показывает.","led.start":"Нажмите, чтобы начать ленту","led.start_touch":"Коснитесь, чтобы начать ленту","led.points":"Точек: {n} · Shift — шаг 45° · Esc — завершить · Ctrl+Z — убрать точку","led.points_touch":"Точек: {n} · смените инструмент, чтобы завершить","led.finish":"Завершить","led.stopped":"Лента упёрлась в стену","led.unbound":"Лента не привязана к устройству","led.bind":"Привязать устройство","led.change":"Сменить устройство","led.later":"Позже","led.unbind":"Отвязать","led.delete":"Удалить ленту","led.settings":"Настройки устройства","led.type_note":"LED-лента: форма редактируется на плане в редакторе устройств","led.show_strip":"Показывать LED-лентой","led.show_icon":"Показывать значком","led.cancel":"Отмена","led.taken":"Уже привязано к другой LED-ленте","led.space":"Сначала отвяжите LED-ленту: она принадлежит своему пространству","led.limit_points":"Не более 50 точек в ленте","led.limit_strips":"Не более 50 лент в пространстве","led.pick_title":"Выберите устройство для ленты","led.pick_new":"Новое устройство…","led.pick_empty":"На этом плане пока нет свободных устройств.","led.saved":"LED-лента сохранена","led.save_failed":"Лента не сохранена: {err}","led.history_draw":"LED-лента","led.history_shape":"Форма LED-ленты","led.history_view":"Вид устройства","led.optimize_walls":"Лент, проходящих сквозь стены: {n} ({spaces})"};const l="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";export{e as dictionary,l as fingerprint};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";var e={"led.tool":"LED-лента","led.hint":"Нарисуйте ленту по точкам вдоль граней стен или по полу — она прилипает к сетке и граням. После завершения выберите устройство, которое она показывает.","led.start":"Нажмите, чтобы начать ленту","led.start_touch":"Коснитесь, чтобы начать ленту","led.points":"Точек: {n} · Shift — шаг 45° · Esc — завершить · Ctrl+Z — убрать точку","led.points_touch":"Точек: {n} · смените инструмент, чтобы завершить","led.finish":"Завершить","led.stopped":"Лента упёрлась в стену","led.unbound":"Лента не привязана к устройству","led.bind":"Привязать устройство","led.change":"Сменить устройство","led.later":"Позже","led.unbind":"Отвязать","led.delete":"Удалить ленту","led.settings":"Настройки устройства","led.type_note":"LED-лента: форма редактируется на плане в редакторе устройств","led.show_strip":"Показывать LED-лентой","led.show_icon":"Показывать значком","led.cancel":"Отмена","led.taken":"Уже привязано к другой LED-ленте","led.space":"Сначала отвяжите LED-ленту: она принадлежит своему пространству","led.limit_points":"Не более 50 точек в ленте","led.limit_strips":"Не более 50 лент в пространстве","led.pick_title":"Выберите устройство для ленты","led.pick_new":"Новое устройство…","led.pick_empty":"На этом плане пока нет свободных устройств.","led.saved":"LED-лента сохранена","led.save_failed":"Лента не сохранена: {err}","led.history_draw":"LED-лента","led.history_shape":"Форма LED-ленты","led.history_view":"Вид устройства","led.optimize_walls":"Лент, проходящих сквозь стены: {n} ({spaces})"};const d="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";export{e as dictionary,d as fingerprint};
|
||||
@@ -1,47 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";import{eu as e,eC as t,c6 as o,eJ as s,eK as i,ad as n,ee as a,O as r,eL as l,az as d,ef as c,aL as p,eM as u,eN as h,eO as f,A as m,eP as v}from"./houseplan-card-BOdiRDFR.js";import{L as g,v as w,s as $,a as _,b as y,p as k,c as b,d as C,e as P}from"./led-strip-geometry-DaLifxqY.js";const M="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";let F,L=null,U=null;function x(e,t){return L||U||F===e||""===F||(U=(void 0===F?import("./led-strip-field-ObbckCkp.js"):import(new URL(`./led-strip-field-ObbckCkp.js?${Date.now()}`,import.meta.url).href)).then(e=>{e.LED_FIELD_FINGERPRINT===s?L=e:F=""},()=>{F=e}).finally(()=>{U=null})),L||U?.then(()=>L&&t()),L}const D=e=>e.some(e=>e.glow&&"on"===e.state),R=e=>e.map(e=>[e[0],e[1]]),E="#383838";function A(s,i){if(!s)return null;const n=Array.isArray(s.masonryGeometry)?s.masonryGeometry:[],a=[...e(s.masonryGeometry),...s.opaqueBodies],r=[];for(const e of a)for(let t=0;t<e.length;t++){const o=e[t],s=e[(t+1)%e.length];o&&s&&r.push({a:[o[0],o[1]],b:[s[0],s[1]]})}const l=25,d=new Map;r.forEach((e,t)=>{const o=Math.floor(Math.min(e.a[0],e.b[0])/l),s=Math.floor(Math.max(e.a[0],e.b[0])/l),i=Math.floor(Math.min(e.a[1],e.b[1])/l),n=Math.floor(Math.max(e.a[1],e.b[1])/l);for(let e=o;e<=s;e++)for(let o=i;o<=n;o++){const s=`${e},${o}`,i=d.get(s);i?i.push(t):d.set(s,[t])}});const c=e=>{let t=1/0,o=1/0,s=-1/0,i=-1/0;for(const n of e)t=Math.min(t,n[0]),o=Math.min(o,n[1]),s=Math.max(s,n[0]),i=Math.max(i,n[1]);return[t,o,s,i]},p=(e,t)=>e[0]>=t[0]&&e[0]<=t[2]&&e[1]>=t[1]&&e[1]<=t[3],u=n.filter(e=>e?.[0]?.length).map(e=>({box:c(e[0]),poly:e})),h=s.opaqueBodies.map(e=>({box:c(e),body:e}));return{faces:r,inside:e=>u.some(({box:o,poly:s})=>p(e,o)&&t([e[0],e[1]],[s]))||h.some(({box:t,body:s})=>p(e,t)&&o([e[0],e[1]],s)),epsilon:i,near:e=>{const t=new Set;for(let o=Math.floor(e[0]/l);o<=Math.floor(e[2]/l);o++)for(let s=Math.floor(e[1]/l);s<=Math.floor(e[3]/l);s++)for(const e of d.get(`${o},${s}`)||[])t.add(e);return[...t].map(e=>r[e])}}}function I(e){const t=e.candidate?.sourceEid||e.device.primary||"",o=t?e.hass?.states?.[t]?.state:void 0,s="unavailable"===o||"unknown"===o||!!t&&!!e.hass&&!e.hass.states?.[t]&&!e.device.virtual,i=Number(e.device.marker?.glow_radius_cm),n=Number.isFinite(i)&&i>0?i/e.cellCm*e.gridPitch:e.defaultRadius,a=s?null:e.candidate?.appearance??null;return{strip:e.strip,device:e.device,state:s?"unavailable":a?"on":"off",glow:e.glow,appearance:a,radius:n}}const T=new WeakMap;function j(e,t,o){const s=P*o;const i=T.get(e);if(i&&i.faces===t&&i.epsilon===t?.epsilon&&i.value.t===s&&i.points.length===e.points.length&&i.points.every((t,o)=>t[0]===e.points[o][0]&&t[1]===e.points[o][1]))return i.value;const n=R(e.points),a=y(n,t,s/2),r={t:s,path:a,d:k(a)};return T.set(e,{faces:t,epsilon:t?.epsilon,points:n,value:r}),r}function G(e){if(!e.views.length)return d``;const t=e.handlers,o=[...e.views].sort((e,t)=>e.strip.id<t.strip.id?1:e.strip.id>t.strip.id?-1:0),s=e.iso;return d`<g class="led-strips" data-hp-led-strips="${o.length}"
|
||||
data-hp-iso=${s?"raised":m}>
|
||||
${s?d`<defs><filter id="hp-led-iso-shadow" filterUnits="userSpaceOnUse"
|
||||
x="-100000" y="-100000" width="200000" height="200000">
|
||||
<feGaussianBlur stdDeviation="${s.shadow.sigma}"></feGaussianBlur></filter></defs>`:m}
|
||||
<style>
|
||||
.led-strip .led-focus { fill: none; stroke: transparent; }
|
||||
.led-strip:has(.led-hit:focus-visible) .led-focus { stroke: var(--primary-color, #03a9f4); }
|
||||
.led-strip .led-hit { fill: none; stroke: transparent; cursor: pointer; outline: none; }
|
||||
</style>
|
||||
${v(o,e=>e.strip.id,o=>{const{t:i,path:n,d:a}=j(o.strip,e.faces,e.d),r="unavailable"===o.state,l=r?"#9e9e9e":"on"===o.state&&!o.glow&&o.appearance?o.appearance.c:"#FFFFFF",c=2*b(i*e.perUnit)/(e.perUnit||1),p=o.device,u=t=>function(e,t,o){const s=e.currentTarget,i=s?.getScreenCTM?.();if(!i||o.views.length<2)return t;const n=i.inverse(),a=[n.a*e.clientX+n.c*e.clientY+n.e,n.b*e.clientX+n.d*e.clientY+n.f],r=o.views.map(e=>{const{t:t,path:s}=j(e.strip,o.faces,o.d);return{id:e.strip.id,points:s.points.map(e=>[e[0]*o.perUnit,e[1]*o.perUnit]),closed:s.closed,thicknessPx:t*o.perUnit}}),l=C([a[0]*o.perUnit,a[1]*o.perUnit],r);return o.views.find(e=>e.strip.id===l)||t}(t,o,e).device,h=s?`translate(0 ${-s.lift})`:m;return d`<g class="led-strip state-${o.state}" data-led-strip="${o.strip.id}"
|
||||
data-marker="${p.id}" data-state="${o.state}" data-closed="${n.closed?"true":"false"}">
|
||||
${s?d`<path class="led-iso-shadow" d="${a}" fill="none" stroke="black"
|
||||
stroke-opacity="${s.shadow.opacity}" stroke-width="${i}" stroke-linecap="round"
|
||||
stroke-linejoin="round" filter="url(#hp-led-iso-shadow)" pointer-events="none"
|
||||
transform="translate(${s.shadow.dx} ${s.shadow.dy-s.lift})"></path>
|
||||
${[1,.5].map(e=>d`<path class="led-iso-edge" d="${a}" fill="none" stroke="${s.edge}"
|
||||
stroke-width="${i}" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"
|
||||
transform="translate(0 ${-s.lift+s.depth*e})"></path>`)}`:m}
|
||||
<g transform=${h}>
|
||||
<path class="led-outline" d="${a}" fill="none" stroke="${E}" stroke-width="${i}"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray=${r?`${2*i} ${1.5*i}`:m}></path>
|
||||
<path class="led-core" d="${a}" fill="none" stroke="${l}" stroke-width="${i/2}"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray=${r?`${2*i} ${1.5*i}`:m}></path>
|
||||
<path class="led-focus" d="${a}" stroke-width="${1.6*i}" stroke-linecap="round"
|
||||
stroke-linejoin="round" aria-hidden="true"></path>
|
||||
${t?d`<path class="led-hit" d="${a}" stroke-width="${c}"
|
||||
stroke-linecap="round" stroke-linejoin="round" pointer-events="stroke"
|
||||
role="button" tabindex="0" aria-label="${t.label(p)}"
|
||||
data-hp="device" data-id="${p.id}"
|
||||
@click=${e=>t.click(e,u(e))}
|
||||
@keydown=${e=>t.keydown(e,p)}
|
||||
@contextmenu=${e=>t.contextmenu(e,u(e))}
|
||||
@pointerdown=${e=>t.pointerdown(e,u(e))}
|
||||
@pointermove=${e=>t.pointermove(e,u(e))}
|
||||
@pointerup=${e=>t.pointerup(e,u(e))}
|
||||
@pointercancel=${e=>t.pointercancel(e,p)}
|
||||
@lostpointercapture=${e=>t.pointercancel(e,p)}
|
||||
@pointerover=${e=>t.pointerover(e,u(e))}
|
||||
@pointerleave=${()=>t.pointerleave()}
|
||||
@focus=${e=>t.focus(e,p)}
|
||||
@blur=${()=>t.blur(p)}></path>`:m}
|
||||
</g>
|
||||
</g>`})}
|
||||
</g>`}function H(e,t,o,s){return(t?e.find(e=>e.id===t):void 0)??(o?e.find(e=>s(o,e)):void 0)}function N(e){const t=new Map(e.devices.map(e=>[e.id,e])),o=g/e.cellCm*e.gridPitch,s=new Map(i({hass:e.hass,devices:e.devices,virtualLights:e.virtualLights,spaceId:e.space.id,defaultColor:e.defaultColor,paletteAlpha:e.paletteAlpha,defaultRadiusUnits:o,cellCm:e.cellCm,gridPitch:e.gridPitch,position:()=>({x:0,y:0})}).map(e=>[e.key,e])),r=[];for(const i of e.space.led_strips||[]){if(!1===i?.active||!i.marker||!w(i.points))continue;const a={...i,points:i.points.map(e=>[e[0]*n,e[1]*n])},l=t.get(a.marker);if(!l||l.space!==e.space.id)continue;if(l.hidden&&!e.showHidden)continue;const d=H(e.space.rooms,l.marker?.room_id,$(R(a.points)),e.inRoom);r.push(I({strip:a,device:l,candidate:s.get(`${e.space.id}|${l.id}`)??null,hass:e.hass,glow:!!d&&e.glowFor(d),defaultRadius:o,cellCm:e.cellCm,gridPitch:e.gridPitch}))}const l=_/e.cellCm*e.gridPitch;return{views:r,faces:A(e.scene,l),d:e.iconPct/100*a(e.space),scene:e.scene,polygons:e.polygons}}const q=new WeakMap;function B(e){for(const t of q.get(e)?.frame.views??[])T.delete(t.strip);q.delete(e),L?.releaseLedField(e)}function S(e){return{shapes:q.get(e)?.frame.views.length??0,...L?.ledFieldStats(e)??{visibility:0,sources:0,visibilityPaths:0,pathChars:0,recomputes:0}}}function O(e,t,o){const s=t.rooms.flatMap(e=>{const t=r(e);return t?[{room:e,poly:t}]:[]}),i=s.length?e._lightBarriers(t,s.map(({room:e,poly:t})=>({r:e,poly:t}))):null,n=[t,e._renderDevices,e._renderPlanHass,o,i,e._mode,e._showAll],a=q.get(e);if(a&&a.key.every((e,t)=>e===n[t]))return a.frame;for(const e of a?.frame.views??[])T.delete(e.strip);const d=e._config?.icon_size??2.5,c=N({space:t,devices:e._renderDevices,hass:e._renderPlanHass,virtualLights:e._virtualLights,defaultColor:e._fillColors.glow_light.c,paletteAlpha:e._fillColors.glow_light.a,cellCm:e._cellCm,gridPitch:e._gridPitch,iconPct:d>8?2.5:d,scene:i,polygons:s,glowFor:e=>l(o,e),inRoom:(t,o)=>e._pointInRoom(t,o),showHidden:"devices"===e._mode&&e._showAll});return!1!==e.isConnected&&q.set(e,{key:n,frame:c}),c}function W(e,t,o,s){const i=O(e,t,o),n=e._stageEl?.clientWidth,a="iso"===e._renderProjection&&"view"===e._mode,r=a?i.d*c:i.d,l="theme-dark"===p(e._renderPlanHass)?"dark":"light",m=!!e._isoLightFloors?.size,v=u(l,m),g=G({views:i.views,d:r,iso:a?{lift:f.lift*r,depth:f.depth*r,edge:h(E,l,m),shadow:{dx:v.dx*r,dy:v.dy*r,sigma:v.sigma*r,opacity:v.opacityWhite}}:null,faces:i.faces,perUnit:n&&s.w?n/s.w:1,handlers:"view"===e._mode?{click:(t,o)=>e._clickDevice(t,o),keydown:(t,o)=>e._keyDevice(t,o),contextmenu:(t,o)=>e._ctxDevice(t,o),pointerdown:(t,o)=>e._pointerDown(t,o),pointermove:(t,o)=>{const s=e._pointerMove(t,o);s&&e._showDeviceTip(t,s)},pointerup:(t,o)=>e._pointerUp(t,o),pointercancel:(t,o)=>e._pointerCancel(t,o),pointerover:(t,o)=>e._showDeviceTip(t,o),pointerleave:()=>e._clearPointerHover(),focus:(t,o)=>e._showDeviceFocusTip(t,o),blur:t=>e._hideDeviceFocusTip(t.id),label:e=>e.name}:null});return"plan"===e._mode||"decor"===e._mode?d`<g class="led-passive" opacity="0.45" pointer-events="none">${g}</g>`:g}function z(e,t,o){if(!1===e.isConnected)return d``;const s=O(e,t,o),i=s.scene&&(D(s.views)||L?.hasLedField(e))?L||x(t.id,()=>!1!==e.isConnected&&e.requestUpdate()):null;return i?i.renderLedField({views:s.views,scene:s.scene,polygons:s.polygons,faces:s.faces,spaceId:t.id,owner:e,requestUpdate:()=>e.requestUpdate(),isConnected:()=>!1!==e.isConnected}):d``}function Y(e){const t=e.space.rooms.flatMap(e=>{const t=r(e);return t?[{room:e,poly:t}]:[]}),o=N({space:e.space,devices:e.devices,hass:e.hass,virtualLights:e.virtualLights,defaultColor:e.defaultColor,paletteAlpha:e.paletteAlpha,cellCm:e.cellCm,gridPitch:e.gridPitch,iconPct:e.iconPct,scene:e.scene,polygons:t,glowFor:e.glowFor,inRoom:e.inRoom,showHidden:!1}),s=e.live?o.views:o.views.map(e=>({...e,state:"off",appearance:null,glow:!1})),i=A({masonryGeometry:e.bodies.masonryGeometry,opaqueBodies:e.bodies.opaqueBodies},_/e.cellCm*e.gridPitch),n=e.scene&&e.live&&(D(s)||L?.hasLedField(e.owner))?x(e.space.id,e.ready):null;return d`${n?n.renderLedField({views:s,scene:e.scene,polygons:t,faces:i,spaceId:e.space.id,owner:e.owner,requestUpdate:e.ready,isConnected:()=>!0}):m}${G({views:s,d:o.d,faces:i,perUnit:e.perUnit,handlers:null})}`}export{M as LED_RUNTIME_FINGERPRINT,A as faceContext,x as ledField,N as ledFrame,O as ledFrameFor,S as ledStats,I as ledStripView,j as ledStripePath,B as releaseLed,z as renderLedFieldFor,W as renderLedLayerFor,G as renderLedStripes,Y as renderStaticLed,H as stripRoom};
|
||||
@@ -0,0 +1,47 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";import{eA as e,eI as t,cc as s,eP as o,eQ as i,ag as n,em as a,R as r,eR as l,aC as d,en as c,aQ as p,eS as u,eT as h,eU as f,A as m,eV as v}from"./houseplan-card-DrYGKEMS.js";import{L as g,v as w,s as $,a as y,b as _,p as k,c as b,d as C,e as P}from"./led-strip-geometry-DmI_yO1O.js";const F="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";let M,U=null,L=null;function x(e,t){return U||L||M===e||""===M||(L=(void 0===M?import("./led-strip-field-3OQ_dqlz.js"):import(new URL(`./led-strip-field-3OQ_dqlz.js?${Date.now()}`,import.meta.url).href)).then(e=>{e.LED_FIELD_FINGERPRINT===o?U=e:M=""},()=>{M=e}).finally(()=>{L=null})),U||L?.then(()=>U&&t()),U}const D=e=>e.some(e=>e.glow&&"on"===e.state),R=e=>e.map(e=>[e[0],e[1]]),A="#383838";function E(o,i){if(!o)return null;const n=Array.isArray(o.masonryGeometry)?o.masonryGeometry:[],a=[...e(o.masonryGeometry),...o.opaqueBodies],r=[];for(const e of a)for(let t=0;t<e.length;t++){const s=e[t],o=e[(t+1)%e.length];s&&o&&r.push({a:[s[0],s[1]],b:[o[0],o[1]]})}const l=25,d=new Map;r.forEach((e,t)=>{const s=Math.floor(Math.min(e.a[0],e.b[0])/l),o=Math.floor(Math.max(e.a[0],e.b[0])/l),i=Math.floor(Math.min(e.a[1],e.b[1])/l),n=Math.floor(Math.max(e.a[1],e.b[1])/l);for(let e=s;e<=o;e++)for(let s=i;s<=n;s++){const o=`${e},${s}`,i=d.get(o);i?i.push(t):d.set(o,[t])}});const c=e=>{let t=1/0,s=1/0,o=-1/0,i=-1/0;for(const n of e)t=Math.min(t,n[0]),s=Math.min(s,n[1]),o=Math.max(o,n[0]),i=Math.max(i,n[1]);return[t,s,o,i]},p=(e,t)=>e[0]>=t[0]&&e[0]<=t[2]&&e[1]>=t[1]&&e[1]<=t[3],u=n.filter(e=>e?.[0]?.length).map(e=>({box:c(e[0]),poly:e})),h=o.opaqueBodies.map(e=>({box:c(e),body:e}));return{faces:r,inside:e=>u.some(({box:s,poly:o})=>p(e,s)&&t([e[0],e[1]],[o]))||h.some(({box:t,body:o})=>p(e,t)&&s([e[0],e[1]],o)),epsilon:i,near:e=>{const t=new Set;for(let s=Math.floor(e[0]/l);s<=Math.floor(e[2]/l);s++)for(let o=Math.floor(e[1]/l);o<=Math.floor(e[3]/l);o++)for(const e of d.get(`${s},${o}`)||[])t.add(e);return[...t].map(e=>r[e])}}}function I(e){const t=e.candidate?.sourceEid||e.device.primary||"",s=t?e.hass?.states?.[t]?.state:void 0,o="unavailable"===s||"unknown"===s||!!t&&!!e.hass&&!e.hass.states?.[t]&&!e.device.virtual,i=Number(e.device.marker?.glow_radius_cm),n=Number.isFinite(i)&&i>0?i/e.cellCm*e.gridPitch:e.defaultRadius,a=o?null:e.candidate?.appearance??null;return{strip:e.strip,device:e.device,state:o?"unavailable":a?"on":"off",glow:e.glow,appearance:a,radius:n}}const T=new WeakMap;function j(e,t,s){const o=P*s;const i=T.get(e);if(i&&i.faces===t&&i.epsilon===t?.epsilon&&i.value.t===o&&i.points.length===e.points.length&&i.points.every((t,s)=>t[0]===e.points[s][0]&&t[1]===e.points[s][1]))return i.value;const n=R(e.points),a=_(n,t,o/2),r={t:o,path:a,d:k(a)};return T.set(e,{faces:t,epsilon:t?.epsilon,points:n,value:r}),r}function G(e){if(!e.views.length)return d``;const t=e.handlers,s=[...e.views].sort((e,t)=>e.strip.id<t.strip.id?1:e.strip.id>t.strip.id?-1:0),o=e.iso;return d`<g class="led-strips" data-hp-led-strips="${s.length}"
|
||||
data-hp-iso=${o?"raised":m}>
|
||||
${o?d`<defs><filter id="hp-led-iso-shadow" filterUnits="userSpaceOnUse"
|
||||
x="-100000" y="-100000" width="200000" height="200000">
|
||||
<feGaussianBlur stdDeviation="${o.shadow.sigma}"></feGaussianBlur></filter></defs>`:m}
|
||||
<style>
|
||||
.led-strip .led-focus { fill: none; stroke: transparent; }
|
||||
.led-strip:has(.led-hit:focus-visible) .led-focus { stroke: var(--primary-color, #03a9f4); }
|
||||
.led-strip .led-hit { fill: none; stroke: transparent; cursor: pointer; outline: none; }
|
||||
</style>
|
||||
${v(s,e=>e.strip.id,s=>{const{t:i,path:n,d:a}=j(s.strip,e.faces,e.d),r="unavailable"===s.state,l=r?"#9e9e9e":"on"===s.state&&s.appearance?s.appearance.c:"#FFFFFF",c=2*b(i*e.perUnit)/(e.perUnit||1),p=s.device,u=t=>function(e,t,s){const o=e.currentTarget,i=o?.getScreenCTM?.();if(!i||s.views.length<2)return t;const n=i.inverse(),a=[n.a*e.clientX+n.c*e.clientY+n.e,n.b*e.clientX+n.d*e.clientY+n.f],r=s.views.map(e=>{const{t:t,path:o}=j(e.strip,s.faces,s.d);return{id:e.strip.id,points:o.points.map(e=>[e[0]*s.perUnit,e[1]*s.perUnit]),closed:o.closed,thicknessPx:t*s.perUnit}}),l=C([a[0]*s.perUnit,a[1]*s.perUnit],r);return s.views.find(e=>e.strip.id===l)||t}(t,s,e).device,h=o?`translate(0 ${-o.lift})`:m;return d`<g class="led-strip state-${s.state}" data-led-strip="${s.strip.id}"
|
||||
data-marker="${p.id}" data-state="${s.state}" data-closed="${n.closed?"true":"false"}">
|
||||
${o?d`<path class="led-iso-shadow" d="${a}" fill="none" stroke="black"
|
||||
stroke-opacity="${o.shadow.opacity}" stroke-width="${i}" stroke-linecap="round"
|
||||
stroke-linejoin="round" filter="url(#hp-led-iso-shadow)" pointer-events="none"
|
||||
transform="translate(${o.shadow.dx} ${o.shadow.dy-o.lift})"></path>
|
||||
${[1,.5].map(e=>d`<path class="led-iso-edge" d="${a}" fill="none" stroke="${o.edge}"
|
||||
stroke-width="${i}" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"
|
||||
transform="translate(0 ${-o.lift+o.depth*e})"></path>`)}`:m}
|
||||
<g transform=${h}>
|
||||
<path class="led-outline" d="${a}" fill="none" stroke="${A}" stroke-width="${i}"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray=${r?`${2*i} ${1.5*i}`:m}></path>
|
||||
<path class="led-core" d="${a}" fill="none" stroke="${l}" stroke-width="${i/2}"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-dasharray=${r?`${2*i} ${1.5*i}`:m}></path>
|
||||
<path class="led-focus" d="${a}" stroke-width="${1.6*i}" stroke-linecap="round"
|
||||
stroke-linejoin="round" aria-hidden="true"></path>
|
||||
${t?d`<path class="led-hit" d="${a}" stroke-width="${c}"
|
||||
stroke-linecap="round" stroke-linejoin="round" pointer-events="stroke"
|
||||
role="button" tabindex="0" aria-label="${t.label(p)}"
|
||||
data-hp="device" data-id="${p.id}"
|
||||
@click=${e=>t.click(e,u(e))}
|
||||
@keydown=${e=>t.keydown(e,p)}
|
||||
@contextmenu=${e=>t.contextmenu(e,u(e))}
|
||||
@pointerdown=${e=>t.pointerdown(e,u(e))}
|
||||
@pointermove=${e=>t.pointermove(e,u(e))}
|
||||
@pointerup=${e=>t.pointerup(e,u(e))}
|
||||
@pointercancel=${e=>t.pointercancel(e,p)}
|
||||
@lostpointercapture=${e=>t.pointercancel(e,p)}
|
||||
@pointerover=${e=>t.pointerover(e,u(e))}
|
||||
@pointerleave=${()=>t.pointerleave()}
|
||||
@focus=${e=>t.focus(e,p)}
|
||||
@blur=${()=>t.blur(p)}></path>`:m}
|
||||
</g>
|
||||
</g>`})}
|
||||
</g>`}function H(e,t,s,o){return(t?e.find(e=>e.id===t):void 0)??(s?e.find(e=>o(s,e)):void 0)}function S(e){const t=new Map(e.devices.map(e=>[e.id,e])),s=g/e.cellCm*e.gridPitch,o=new Map(i({hass:e.hass,devices:e.devices,virtualLights:e.virtualLights,spaceId:e.space.id,defaultColor:e.defaultColor,paletteAlpha:e.paletteAlpha,defaultRadiusUnits:s,cellCm:e.cellCm,gridPitch:e.gridPitch,position:()=>({x:0,y:0})}).map(e=>[e.key,e])),r=[];for(const i of e.space.led_strips||[]){if(!1===i?.active||!i.marker||!w(i.points))continue;const a={...i,points:i.points.map(e=>[e[0]*n,e[1]*n])},l=t.get(a.marker);if(!l||l.space!==e.space.id)continue;if(l.hidden&&!e.showHidden)continue;const d=H(e.space.rooms,l.marker?.room_id,$(R(a.points)),e.inRoom);r.push(I({strip:a,device:l,candidate:o.get(`${e.space.id}|${l.id}`)??null,hass:e.hass,glow:!!d&&e.glowFor(d),defaultRadius:s,cellCm:e.cellCm,gridPitch:e.gridPitch}))}const l=y/e.cellCm*e.gridPitch;return{views:r,faces:E(e.scene,l),d:e.iconPct/100*a(e.space),scene:e.scene,polygons:e.polygons}}const q=new WeakMap;function B(e){for(const t of q.get(e)?.frame.views??[])T.delete(t.strip);q.delete(e),U?.releaseLedField(e)}function N(e){return{shapes:q.get(e)?.frame.views.length??0,...U?.ledFieldStats(e)??{visibility:0,sources:0,visibilityPaths:0,pathChars:0,recomputes:0}}}function W(e,t,s){const o=t.rooms.flatMap(e=>{const t=r(e);return t?[{room:e,poly:t}]:[]}),i=o.length?e._lightBarriers(t,o.map(({room:e,poly:t})=>({r:e,poly:t}))):null,n=[t,e._renderDevices,e._renderPlanHass,s,i,e._mode,e._showAll],a=q.get(e);if(a&&a.key.every((e,t)=>e===n[t]))return a.frame;for(const e of a?.frame.views??[])T.delete(e.strip);const d=e._config?.icon_size??2.5,c=S({space:t,devices:e._renderDevices,hass:e._renderPlanHass,virtualLights:e._virtualLights,defaultColor:e._fillColors.glow_light.c,paletteAlpha:e._fillColors.glow_light.a,cellCm:e._cellCm,gridPitch:e._gridPitch,iconPct:d>8?2.5:d,scene:i,polygons:o,glowFor:e=>l(s,e),inRoom:(t,s)=>e._pointInRoom(t,s),showHidden:"devices"===e._mode&&e._showAll});return!1!==e.isConnected&&q.set(e,{key:n,frame:c}),c}function O(e,t,s,o){const i=W(e,t,s),n=e._stageEl?.clientWidth,a="iso"===e._renderProjection&&"view"===e._mode,r=a?i.d*c:i.d,l="theme-dark"===p(e._renderPlanHass)?"dark":"light",m=!!e._isoLightFloors?.size,v=u(l,m),g=G({views:i.views,d:r,iso:a?{lift:f.lift*r,depth:f.depth*r,edge:h(A,l,m),shadow:{dx:v.dx*r,dy:v.dy*r,sigma:v.sigma*r,opacity:v.opacityWhite}}:null,faces:i.faces,perUnit:n&&o.w?n/o.w:1,handlers:"view"===e._mode?{click:(t,s)=>e._clickDevice(t,s),keydown:(t,s)=>e._keyDevice(t,s),contextmenu:(t,s)=>e._ctxDevice(t,s),pointerdown:(t,s)=>e._pointerDown(t,s),pointermove:(t,s)=>{const o=e._pointerMove(t,s);o&&e._showDeviceTip(t,o)},pointerup:(t,s)=>e._pointerUp(t,s),pointercancel:(t,s)=>e._pointerCancel(t,s),pointerover:(t,s)=>e._showDeviceTip(t,s),pointerleave:()=>e._clearPointerHover(),focus:(t,s)=>e._showDeviceFocusTip(t,s),blur:t=>e._hideDeviceFocusTip(t.id),label:e=>e.name}:null});return"plan"===e._mode||"decor"===e._mode?d`<g class="led-passive" opacity="0.45" pointer-events="none">${g}</g>`:g}function Y(e,t,s){if(!1===e.isConnected)return d``;const o=W(e,t,s),i=o.scene&&(D(o.views)||U?.hasLedField(e))?U||x(t.id,()=>!1!==e.isConnected&&e.requestUpdate()):null;return i?i.renderLedField({views:o.views,scene:o.scene,polygons:o.polygons,faces:o.faces,spaceId:t.id,owner:e,requestUpdate:()=>e.requestUpdate(),isConnected:()=>!1!==e.isConnected}):d``}function z(e){const t=e.space.rooms.flatMap(e=>{const t=r(e);return t?[{room:e,poly:t}]:[]}),s=S({space:e.space,devices:e.devices,hass:e.hass,virtualLights:e.virtualLights,defaultColor:e.defaultColor,paletteAlpha:e.paletteAlpha,cellCm:e.cellCm,gridPitch:e.gridPitch,iconPct:e.iconPct,scene:e.scene,polygons:t,glowFor:e.glowFor,inRoom:e.inRoom,showHidden:!1}),o=e.live?s.views:s.views.map(e=>({...e,state:"off",appearance:null,glow:!1})),i=E({masonryGeometry:e.bodies.masonryGeometry,opaqueBodies:e.bodies.opaqueBodies},y/e.cellCm*e.gridPitch),n=e.isConnected()&&e.scene&&e.live&&(D(o)||U?.hasLedField(e.owner))?x(e.space.id,()=>e.isConnected()&&e.ready()):null;return n||U?.releaseLedField(e.owner),d`${n?n.renderLedField({views:o,scene:e.scene,polygons:t,faces:i,spaceId:e.space.id,owner:e.owner,requestUpdate:e.ready,isConnected:e.isConnected}):m}${G({views:o,d:s.d,faces:i,perUnit:e.perUnit,handlers:null})}`}export{F as LED_RUNTIME_FINGERPRINT,E as faceContext,x as ledField,S as ledFrame,W as ledFrameFor,N as ledStats,I as ledStripView,j as ledStripePath,B as releaseLed,Y as renderLedFieldFor,O as renderLedLayerFor,G as renderLedStripes,z as renderStaticLed,H as stripRoom};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";const t=t=>[t.left,t.top,t.right,t.bottom,t.width,t.height].every(Number.isFinite)&&t.right>=t.left&&t.bottom>=t.top&&t.width>=0&&t.height>=0;function o(o){const{preferred:e,size:i,bounds:h,blockers:r,gap:n=8,edge:f=8}=o;if(![e.x,e.y,i.width,i.height,n,f].every(Number.isFinite)||i.width<=0||i.height<=0||n<0||f<0||!t(h)||r.some(o=>!t(o)))return null;const d=h.left+f,s=h.right-f-i.width,b=h.top+f,l=h.bottom-f-i.height;if(![d,s,b,l].every(Number.isFinite)||s<d||l<b)return null;const c=r.filter(t=>t.right>t.left&&t.bottom>t.top).map(t=>({left:t.left-n-i.width,right:t.right+n,top:t.top-n-i.height,bottom:t.bottom+n})),g=(t,o,e)=>Math.max(o,Math.min(e,t)),m=new Set([d,s,g(e.x,d,s)]),p=new Set([b,l,g(e.y,b,l)]);for(const t of c)m.add(g(t.left,d,s)),m.add(g(t.right,d,s)),p.add(g(t.top,b,l)),p.add(g(t.bottom,b,l));const a=[...m].sort((t,o)=>t-o),u=[...p].sort((t,o)=>t-o);let w=null,y=1/0;for(const t of u)for(const o of a){if(c.some(e=>o>e.left&&o<e.right&&t>e.top&&t<e.bottom))continue;const i=Math.hypot(o-e.x,t-e.y);(!w||i<y)&&(w={left:o,top:t},y=i)}return w}export{o as p};
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";import{eR as e,eS as a,eG as n}from"./houseplan-card-BOdiRDFR.js";function o(n,o){return new e([{code:"en",dictionary:n},{code:"ru",loadDictionary:o.ru},{code:"de",loadDictionary:o.de},{code:"fr",loadDictionary:o.fr}],"ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172",console.warn,a)}function r(e){return function(e,a){return{state:n=>{const o=e.state(n);return"pending"!==o&&a.some(e=>"pending"===e.state(n))?"pending":o},dictionary:a=>e.dictionary(a),ensure:n=>Promise.all([e,...a].map(e=>e.ensure(n))).then(()=>{})}}(n,e)}export{o as n,r as s};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";import{eX as e,eY as n,eM as a}from"./houseplan-card-DrYGKEMS.js";function c(a,c){return new e([{code:"en",dictionary:a},{code:"ru",loadDictionary:c.ru},{code:"de",loadDictionary:c.de},{code:"fr",loadDictionary:c.fr}],"e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90",console.warn,n)}function d(e){return function(e,n){return{state:a=>{const c=e.state(a);return"pending"!==c&&n.some(e=>"pending"===e.state(a))?"pending":c},dictionary:n=>e.dictionary(n),ensure:a=>Promise.all([e,...n].map(e=>e.ensure(a))).then(()=>{})}}(a,e)}export{c as n,d as s};
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";import{aG as e,l as t,aJ as s,A as a,eF as o,eG as i,eH as n,E as l,eQ as c,c as r}from"./houseplan-card-BOdiRDFR.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return t(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"fit",selector:{select:{mode:"dropdown",options:[{value:"content",label:s(this._lang,"editor.fit_content")},{value:"house",label:s(this._lang,"editor.fit_house")}]}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"light_pools",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}}]}render(){if(!this.hass||!this._config)return a;const e=o(this,i,t(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return l;this._loadSpaces();const h=this._lang,_={space:s(h,"editor.space"),title:s(h,"editor.title"),fit:s(h,"editor.framing"),show_button:s(h,"editor.show_button"),button_label:s(h,"editor.button_label"),button_target:s(h,"editor.button_target"),icon_size:s(h,"editor.icon_size"),show_temperature:s(h,"editor.show_temperature"),live_states:s(h,"editor.live_states"),light_pools:s(h,"editor.light_pools"),show_signal:s(h,"editor.show_signal")};return r`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";import{aJ as e,l as t,aM as s,A as a,eL as o,eM as i,eN as n,E as l,eW as c,c as r}from"./houseplan-card-DrYGKEMS.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return t(this.hass,this._config?.language)}get _schema(){const e=this._spaces||[];return[e.length?{name:"space",selector:{select:{mode:"dropdown",options:e}}}:{name:"space",selector:{text:{}}},{name:"title",selector:{text:{}}},{name:"fit",selector:{select:{mode:"dropdown",options:[{value:"content",label:s(this._lang,"editor.fit_content")},{value:"house",label:s(this._lang,"editor.fit_house")}]}}},{name:"show_button",selector:{boolean:{}}},{name:"button_label",selector:{text:{}}},{name:"button_target",selector:{text:{}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"light_pools",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}}]}render(){if(!this.hass||!this._config)return a;const e=o(this,i,t(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return l;this._loadSpaces();const h=this._lang,_={space:s(h,"editor.space"),title:s(h,"editor.title"),fit:s(h,"editor.framing"),show_button:s(h,"editor.show_button"),button_label:s(h,"editor.button_label"),button_target:s(h,"editor.button_target"),icon_size:s(h,"editor.icon_size"),show_temperature:s(h,"editor.show_temperature"),live_states:s(h,"editor.live_states"),light_pools:s(h,"editor.light_pools"),show_signal:s(h,"editor.show_signal")};return r`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${{...this._config,fit:c(this._config.fit)}}
|
||||
.schema=${this._schema}
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";import{c as a,A as e}from"./houseplan-card-BOdiRDFR.js";import{s,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-CcLP5B5v.js";import"./form-kit.styles-B6HsmMPA.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!s(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(e=>e.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";import{c as a,A as s}from"./houseplan-card-DrYGKEMS.js";import{s as e,n as t,a as l,m as r,b as o,c as i,d as u}from"./summary-panel-runtime-loaded-CKq56nJ2.js";import"./form-kit.styles-DLwU23Kk.js";const m=m=>{const{host:c,dialog:d,problems:n,t:b}=m,y=n.find(a=>"error"===a.kind),$=d.localShow!==d.baseLocalShow||!d.localOnly&&!e(t(d.draft),d.base),p=$&&!d.busy&&!y,v=a=>n.find(s=>s.path===a),h=a=>a?b(`summary.problem.${a.code}`):"",k=()=>m.close(),g=a`<label class="summary-switch" for="summary-local-show">
|
||||
<span class="summary-switch-caption"><strong>${b("summary.show_local")}</strong>
|
||||
<small id="summary-local-show-hint">${b("summary.show_local_hint")}</small></span>
|
||||
<input id="summary-local-show" data-summary-local-show type="checkbox"
|
||||
@@ -10,31 +10,31 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580
|
||||
dismiss-on-scrim aria-busy=${String(d.busy)}
|
||||
@hp-close=${k}>
|
||||
<div class="body summary-editor" @click=${()=>m.closeSource()}>
|
||||
${d.localOnly?a`<p class="summary-local-hint">${b(d.localOnlyHint)}</p>`:e}
|
||||
${d.localOnly?a`<p class="summary-local-hint">${b(d.localOnlyHint)}</p>`:s}
|
||||
<section class="summary-general" aria-labelledby="summary-general-title">
|
||||
<h3 id="summary-general-title">${b("summary.general_settings")}</h3>
|
||||
<div class="summary-general-grid">
|
||||
${d.localOnly?e:a`<div class="summary-field">
|
||||
${d.localOnly?s:a`<div class="summary-field">
|
||||
<label for="summary-panel-title">${b("summary.panel_title")}</label>
|
||||
<input id="summary-panel-title" type="text" .value=${d.draft.title}
|
||||
?disabled=${d.busy}
|
||||
data-summary-error=${String(d.attempted&&"title"===y?.path)}
|
||||
aria-invalid=${"error"===v("title")?.kind?"true":"false"}
|
||||
@input=${a=>m.mutate(e=>{e.title=a.target.value})} />
|
||||
${v("title")?a`<div class="summary-problem error">${h(v("title"))}</div>`:e}
|
||||
@input=${a=>m.mutate(s=>{s.title=a.target.value})} />
|
||||
${v("title")?a`<div class="summary-problem error">${h(v("title"))}</div>`:s}
|
||||
</div>`}
|
||||
${g}
|
||||
${d.localOnly?e:a`<label class="summary-switch" for="summary-mobile-show">
|
||||
${d.localOnly?s:a`<label class="summary-switch" for="summary-mobile-show">
|
||||
<span class="summary-switch-caption"><strong>${b("summary.show_mobile")}</strong>
|
||||
<small id="summary-mobile-show-hint">${b("summary.show_mobile_hint")}</small></span>
|
||||
<input id="summary-mobile-show" data-summary-mobile-show type="checkbox"
|
||||
.checked=${d.draft.show_on_mobile} ?disabled=${!d.localShow||d.busy}
|
||||
aria-label=${b("summary.show_mobile")} aria-describedby="summary-mobile-show-hint"
|
||||
@change=${a=>m.mutate(e=>{e.show_on_mobile=a.target.checked})} /></label>`}
|
||||
@change=${a=>m.mutate(s=>{s.show_on_mobile=a.target.checked})} /></label>`}
|
||||
</div>
|
||||
${m.storageUnavailable?a`<div class="summary-problem warning">${b("summary.storage_unavailable")}</div>`:e}
|
||||
${m.storageUnavailable?a`<div class="summary-problem warning">${b("summary.storage_unavailable")}</div>`:s}
|
||||
</section>
|
||||
${d.localOnly?e:a`
|
||||
${d.localOnly?s:a`
|
||||
<section class="summary-blocks-card" aria-labelledby="summary-blocks-title">
|
||||
<header class="summary-blocks-heading">
|
||||
<div><h3 id="summary-blocks-title">${b("summary.blocks")}</h3>
|
||||
@@ -42,7 +42,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580
|
||||
<span class="summary-block-count">${b("summary.block_count").replace("{count}",String(d.draft.blocks.length)).replace("{limit}","10")}</span>
|
||||
</header>
|
||||
<div class="summary-editor-blocks">
|
||||
${d.draft.blocks.map((s,t)=>{const u=`blocks.${t}`,n=v(`${u}.scope`);return a`<article class="summary-editor-block"
|
||||
${d.draft.blocks.map((e,t)=>{const u=`blocks.${t}`,n=v(`${u}.scope`);return a`<article class="summary-editor-block"
|
||||
@dragover=${a=>a.preventDefault()}
|
||||
@drop=${a=>m.drop(a,`block:${t}`)}>
|
||||
<div class="summary-block-head">
|
||||
@@ -57,40 +57,40 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580
|
||||
?disabled=${t===d.draft.blocks.length-1||d.busy}
|
||||
@click=${()=>m.mutate(a=>{a.blocks=r(a.blocks,t,t+1)})}>${l("down")}</button>
|
||||
</div>
|
||||
<input class="summary-block-title" type="text" .value=${s.title}
|
||||
<input class="summary-block-title" type="text" .value=${e.title}
|
||||
placeholder=${b("summary.block_title")} aria-label=${b("summary.block_title")}
|
||||
?disabled=${d.busy}
|
||||
data-summary-error=${String(d.attempted&&y?.path===`${u}.title`)}
|
||||
aria-invalid=${"error"===v(`${u}.title`)?.kind?"true":"false"}
|
||||
@input=${a=>m.mutate(e=>{e.blocks[t].title=a.target.value})} />
|
||||
@input=${a=>m.mutate(s=>{s.blocks[t].title=a.target.value})} />
|
||||
<button class="summary-icon-button summary-visibility" type="button"
|
||||
?disabled=${d.busy} aria-pressed=${String(s.visible)}
|
||||
title=${b(s.visible?"summary.hide_block":"summary.show_block")}
|
||||
aria-label=${b(s.visible?"summary.hide_block":"summary.show_block")}
|
||||
@click=${()=>m.mutate(a=>{a.blocks[t].visible=!a.blocks[t].visible})}>${l(s.visible?"eye":"eyeOff")}</button>
|
||||
${v(`${u}.title`)?a`<div class="summary-problem error summary-block-title-error">${h(v(`${u}.title`))}</div>`:e}
|
||||
?disabled=${d.busy} aria-pressed=${String(e.visible)}
|
||||
title=${b(e.visible?"summary.hide_block":"summary.show_block")}
|
||||
aria-label=${b(e.visible?"summary.hide_block":"summary.show_block")}
|
||||
@click=${()=>m.mutate(a=>{a.blocks[t].visible=!a.blocks[t].visible})}>${l(e.visible?"eye":"eyeOff")}</button>
|
||||
${v(`${u}.title`)?a`<div class="summary-problem error summary-block-title-error">${h(v(`${u}.title`))}</div>`:s}
|
||||
<div class="summary-scope">
|
||||
<select .value=${s.scope.type} ?disabled=${d.busy}
|
||||
<select .value=${e.scope.type} ?disabled=${d.busy}
|
||||
aria-label=${b("summary.scope")}
|
||||
@change=${a=>m.mutate(e=>{e.blocks[t].scope="space"===a.target.value?{type:"space",space_id:c._space}:{type:"all"}})}>
|
||||
<option value="all" ?selected=${"all"===s.scope.type}>${b("summary.scope_all")}</option>
|
||||
<option value="space" ?selected=${"space"===s.scope.type}>${b("summary.scope_space")}</option>
|
||||
@change=${a=>m.mutate(s=>{s.blocks[t].scope="space"===a.target.value?{type:"space",space_id:c._space}:{type:"all"}})}>
|
||||
<option value="all" ?selected=${"all"===e.scope.type}>${b("summary.scope_all")}</option>
|
||||
<option value="space" ?selected=${"space"===e.scope.type}>${b("summary.scope_space")}</option>
|
||||
</select>
|
||||
${"space"===s.scope.type?a`<select .value=${s.scope.space_id}
|
||||
${"space"===e.scope.type?a`<select .value=${e.scope.space_id}
|
||||
?disabled=${d.busy} aria-label=${b("summary.scope_space")}
|
||||
data-summary-error=${String(d.attempted&&y?.path===`${u}.scope`)}
|
||||
aria-invalid=${"error"===n?.kind?"true":"false"}
|
||||
@change=${a=>m.mutate(e=>{const s=e.blocks[t].scope;"space"===s.type&&(s.space_id=a.target.value)})}>
|
||||
${c._model.some(a=>a.id===s.scope.space_id)?e:a`<option value=${s.scope.space_id} selected>${s.scope.space_id}</option>`}
|
||||
${c._model.map(e=>a`<option value=${e.id}
|
||||
?selected=${e.id===s.scope.space_id}>${e.title}</option>`)}
|
||||
</select>`:e}
|
||||
@change=${a=>m.mutate(s=>{const e=s.blocks[t].scope;"space"===e.type&&(e.space_id=a.target.value)})}>
|
||||
${c._model.some(a=>a.id===e.scope.space_id)?s:a`<option value=${e.scope.space_id} selected>${e.scope.space_id}</option>`}
|
||||
${c._model.map(s=>a`<option value=${s.id}
|
||||
?selected=${s.id===e.scope.space_id}>${s.title}</option>`)}
|
||||
</select>`:s}
|
||||
</div>
|
||||
${n?a`<div class="summary-problem summary-scope-problem ${n.kind}">${h(n)}</div>`:e}
|
||||
${n?a`<div class="summary-problem summary-scope-problem ${n.kind}">${h(n)}</div>`:s}
|
||||
</div>
|
||||
<div class="summary-block-body">
|
||||
<div class="summary-editor-values">
|
||||
${s.values.map((i,c)=>{const n=`${u}.values.${c}`,$=v(`${n}.source`),p=m.sourceToken(i.source),k=d.activeSource?.blockId===s.id&&d.activeSource.valueId===i.id,g="system"===i.source.type?b(`summary.system.${i.source.key}`):i.source.entity_id?m.entityIndex.labels.get(i.source.entity_id)||i.source.entity_id:b("summary.select_source"),_="system"===i.source.type?b("summary.system_source"):i.source.entity_id,f=k?o(m.entityIndex,d.entityFilter):null,w="entity"===i.source.type&&!!i.source.entity_id&&!m.entityIndex.labels.has(i.source.entity_id),S=`${s.id}\n${i.id}`;return a`<div class="summary-editor-value"
|
||||
${e.values.map((i,c)=>{const n=`${u}.values.${c}`,$=v(`${n}.source`),p=m.sourceToken(i.source),k=d.activeSource?.blockId===e.id&&d.activeSource.valueId===i.id,g="system"===i.source.type?b(`summary.system.${i.source.key}`):i.source.entity_id?m.entityIndex.labels.get(i.source.entity_id)||i.source.entity_id:b("summary.select_source"),_="system"===i.source.type?b("summary.system_source"):i.source.entity_id,f=k?o(m.entityIndex,d.entityFilter):null,w="entity"===i.source.type&&!!i.source.entity_id&&!m.entityIndex.labels.has(i.source.entity_id),S=`${e.id}\n${i.id}`;return a`<div class="summary-editor-value"
|
||||
@dragover=${a=>{a.preventDefault(),a.stopPropagation()}}
|
||||
@drop=${a=>m.drop(a,`value:${t}:${c}`)}>
|
||||
<span class="summary-drag" draggable=${String(!d.busy)} title=${b("summary.drag")}
|
||||
@@ -101,7 +101,7 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580
|
||||
@click=${()=>m.mutate(a=>{a.blocks[t].values=r(a.blocks[t].values,c,c-1)})}>${l("up")}</button>
|
||||
<button class="summary-icon-button" type="button" title=${b("summary.down")}
|
||||
aria-label=${b("summary.down")}
|
||||
?disabled=${c===s.values.length-1||d.busy}
|
||||
?disabled=${c===e.values.length-1||d.busy}
|
||||
@click=${()=>m.mutate(a=>{a.blocks[t].values=r(a.blocks[t].values,c,c+1)})}>${l("down")}</button>
|
||||
</div>
|
||||
<div class="summary-value-label">
|
||||
@@ -109,8 +109,8 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580
|
||||
placeholder=${b("summary.value_name")} aria-label=${b("summary.value_name")}
|
||||
data-summary-error=${String(d.attempted&&y?.path===`${n}.label`)}
|
||||
aria-invalid=${"error"===v(`${n}.label`)?.kind?"true":"false"}
|
||||
@input=${a=>m.mutate(e=>{e.blocks[t].values[c].label=a.target.value})} />
|
||||
${v(`${n}.label`)?a`<div class="summary-problem error">${h(v(`${n}.label`))}</div>`:e}
|
||||
@input=${a=>m.mutate(s=>{s.blocks[t].values[c].label=a.target.value})} />
|
||||
${v(`${n}.label`)?a`<div class="summary-problem error">${h(v(`${n}.label`))}</div>`:s}
|
||||
</div>
|
||||
<div class="summary-source-field">
|
||||
<button class="summary-source" type="button"
|
||||
@@ -119,9 +119,9 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580
|
||||
data-summary-error=${String(d.attempted&&y?.path===`${n}.source`)}
|
||||
aria-invalid=${"error"===$?.kind?"true":"false"}
|
||||
aria-haspopup="listbox" aria-expanded=${k?"true":"false"}
|
||||
@click=${a=>{a.stopPropagation(),k?m.closeSource():m.openSource(s.id,i.id)}}>
|
||||
@click=${a=>{a.stopPropagation(),k?m.closeSource():m.openSource(e.id,i.id)}}>
|
||||
<span class="summary-source-caption"><strong>${g}</strong>
|
||||
${_?a`<small>${_}</small>`:e}</span>
|
||||
${_?a`<small>${_}</small>`:s}</span>
|
||||
${l("chevron")}
|
||||
</button>
|
||||
${k&&f?a`<div class="summary-source-picker"
|
||||
@@ -135,39 +135,39 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580
|
||||
<div class="summary-source-results" role="listbox"
|
||||
aria-label=${b("summary.select_source")}>
|
||||
<div class="summary-source-group">${b("summary.system_group")}</div>
|
||||
${["device_count","total_area","datetime"].map(e=>a`
|
||||
${["device_count","total_area","datetime"].map(s=>a`
|
||||
<button type="button" role="option"
|
||||
?disabled=${d.busy}
|
||||
aria-selected=${p===`system:${e}`?"true":"false"}
|
||||
@click=${()=>m.setSource(s.id,i.id,`system:${e}`)}>
|
||||
${b(`summary.system.${e}`)}
|
||||
aria-selected=${p===`system:${s}`?"true":"false"}
|
||||
@click=${()=>m.setSource(e.id,i.id,`system:${s}`)}>
|
||||
${b(`summary.system.${s}`)}
|
||||
</button>`)}
|
||||
${w?a`<div class="summary-source-group">${b("summary.current_source")}</div>
|
||||
<button type="button" role="option" class="broken" aria-selected="true"
|
||||
?disabled=${d.busy}
|
||||
@click=${()=>m.setSource(s.id,i.id,p)}>
|
||||
@click=${()=>m.setSource(e.id,i.id,p)}>
|
||||
${g}
|
||||
</button>`:e}
|
||||
</button>`:s}
|
||||
<div class="summary-source-group">${b("summary.entities_group")}</div>
|
||||
${f.entries.map(e=>a`<button type="button" role="option"
|
||||
${f.entries.map(s=>a`<button type="button" role="option"
|
||||
?disabled=${d.busy}
|
||||
aria-selected=${p===`entity:${e.id}`?"true":"false"}
|
||||
@click=${()=>m.setSource(s.id,i.id,`entity:${e.id}`)}>
|
||||
<span>${e.label}</span><small>${e.id}</small>
|
||||
aria-selected=${p===`entity:${s.id}`?"true":"false"}
|
||||
@click=${()=>m.setSource(e.id,i.id,`entity:${s.id}`)}>
|
||||
<span>${s.label}</span><small>${s.id}</small>
|
||||
</button>`)}
|
||||
${f.total?e:a`<div class="summary-empty">${b("summary.no_search_results")}</div>`}
|
||||
${f.truncated?a`<div class="summary-refine">${b("summary.refine_search")}</div>`:e}
|
||||
${f.total?s:a`<div class="summary-empty">${b("summary.no_search_results")}</div>`}
|
||||
${f.truncated?a`<div class="summary-refine">${b("summary.refine_search")}</div>`:s}
|
||||
</div>
|
||||
</div>`:e}
|
||||
${$?a`<div class="summary-problem ${$.kind}">${h($)}</div>`:e}
|
||||
</div>`:s}
|
||||
${$?a`<div class="summary-problem ${$.kind}">${h($)}</div>`:s}
|
||||
</div>
|
||||
<button class="summary-icon-button summary-value-remove" type="button"
|
||||
title=${b("btn.delete")} aria-label=${b("btn.delete")} ?disabled=${d.busy}
|
||||
@click=${()=>m.mutate(a=>{a.blocks[t].values.splice(c,1)})}>${l("close")}</button>
|
||||
</div>`})}
|
||||
<button class="summary-add summary-add-value" type="button"
|
||||
?disabled=${s.values.length>=20||d.busy}
|
||||
title=${s.values.length>=20?b("summary.limit_values"):""}
|
||||
?disabled=${e.values.length>=20||d.busy}
|
||||
title=${e.values.length>=20?b("summary.limit_values"):""}
|
||||
@click=${()=>m.mutate(a=>{a.blocks[t].values.push(i())})}>
|
||||
${l("plus")}${b("summary.add_value")}
|
||||
</button>
|
||||
@@ -186,11 +186,11 @@ globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580
|
||||
${l("plus")}${b("summary.add_block")}
|
||||
</button>
|
||||
</section>
|
||||
${d.error?a`<div class="summary-problem error" role="alert">${d.error}</div>`:e}
|
||||
${d.error?a`<div class="summary-problem error" role="alert">${d.error}</div>`:s}
|
||||
${d.conflict?a`<button class="btn ghost summary-reload" type="button"
|
||||
?disabled=${d.busy} @click=${()=>m.reload()}>
|
||||
<ha-icon icon="mdi:reload"></ha-icon>${b("summary.reload_current")}
|
||||
</button>`:e}
|
||||
</button>`:s}
|
||||
`}
|
||||
</div>
|
||||
<div class="row summary-editor-footer" slot="footer">
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";import{f5 as e,R as t,bb as n,P as i,ai as o,ag as r,cl as s,f6 as a,ck as c,ba as l,b9 as d,bc as u,f7 as f,f8 as m,cn as g,f9 as v}from"./houseplan-card-DrYGKEMS.js";function y(e){if(!e.registry.authoritative)return null;const t=new Set,n=u(e.markers);for(const i of Object.values(e.registry.devices||{}))i?.id&&"service"!==i.entry_type&&!n.devices.has(i.id)&&i.area_id&&e.areaToSpace[i.area_id]&&t.add(i.id);for(const i of e.markers||[]){if(i.removed||"virtual"===i.binding)continue;const o=String(i.binding||"").indexOf(":");if(o<1)continue;const r=i.binding.slice(0,o),s=i.binding.slice(o+1),a="device"===r?e.registry.devices?.[s]:null,c="entity"===r?e.registry.entities?.[s]:null,l="device"===r?s:c?.device_id,d="device"===r?a?.area_id:c?.area_id||l&&e.registry.devices?.[l]?.area_id,u="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||d||"",f=u&&e.areaToSpace[u]||i.space||e.firstSpaceId;l&&e.registry.devices?.[l]&&e.spaceIds.has(f)&&(!n.devices.has(l)||"entity"===r&&n.liveEntities.has(s))&&t.add(l)}return t}function _(e,t){return e?.length?t?.length?v(e,t):e:t}function b(e,t){const n=l(e.rooms,t.walls,t.openCuts,[],i,t.cellCm,o,r);return{roomGeom:"ok"===n.status||"degraded-extra"===n.status?n.roomGeom:void 0,multiWallNodes:d(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*h(l,d,u=b){try{let f=0;for(const m of d){const d=l.spaces.find(e=>String(e?.id)===m.id);if(!d)continue;const g=e(d,m),v=u(m,g);let y=null;for(const e of m.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const c=n(m.rooms,e.id,g.walls,g.openCuts,i,g.cellCm,o,r,v.roomGeom,v.multiWallNodes)||a;y=_(y,s(c,g.physicalBodies)),yield}const b=a(y||[],m.stairs);let h=b.next();for(;!h.done;)yield,h=b.next();y=h.value;const p=g.cellCm/o;f+=c(y)*p*p/1e4}return f}catch{return null}}function p(e,t,n=b){const i=h(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function S(e,t,n,i){if("system"!==e.type)return null;if("device_count"===e.key)return null===t.deviceCount?null:String(t.deviceCount);if("total_area"===e.key)return null===t.areaM2?null:g(t.areaM2,"mi"===n?.config?.unit_system?.length);try{return new Intl.DateTimeFormat(i||void 0,{dateStyle:"short",timeStyle:"short",timeZone:n?.config?.time_zone||void 0}).format(t.now)}catch{return t.now.toLocaleString()}}function C(e,t){const n=e?.states?.[t];if(!n)return null;const i=f(e,t);return i?m(i,String(n.attributes?.unit_of_measurement||"")):null}export{h as cleanFloorAreaSteps,y as representedHaDeviceIds,b as spaceWallGeometry,C as summaryEntityValue,S as summarySystemValue,p as totalCleanFloorAreaM2};
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";import{e$ as e,O as t,b5 as n,M as i,af as o,ad as r,cf as s,f0 as a,ce as l,b4 as c,b3 as u,b6 as d,f1 as f,f2 as m,ch as g,f3 as v}from"./houseplan-card-BOdiRDFR.js";function y(e){if(!e.registry.authoritative)return null;const t=new Set,n=d(e.markers);for(const i of Object.values(e.registry.devices||{}))i?.id&&"service"!==i.entry_type&&!n.devices.has(i.id)&&i.area_id&&e.areaToSpace[i.area_id]&&t.add(i.id);for(const i of e.markers||[]){if(i.removed||"virtual"===i.binding)continue;const o=String(i.binding||"").indexOf(":");if(o<1)continue;const r=i.binding.slice(0,o),s=i.binding.slice(o+1),a="device"===r?e.registry.devices?.[s]:null,l="entity"===r?e.registry.entities?.[s]:null,c="device"===r?s:l?.device_id,u="device"===r?a?.area_id:l?.area_id||c&&e.registry.devices?.[c]?.area_id,d="string"==typeof i.room_id&&i.room_id.length>0&&null===i.area?"":i.area||u||"",f=d&&e.areaToSpace[d]||i.space||e.firstSpaceId;c&&e.registry.devices?.[c]&&e.spaceIds.has(f)&&(!n.devices.has(c)||"entity"===r&&n.liveEntities.has(s))&&t.add(c)}return t}function _(e,t){return e?.length?t?.length?v(e,t):e:t}function h(e,t){const n=c(e.rooms,t.walls,t.openCuts,[],i,t.cellCm,o,r);return{roomGeom:"ok"===n.status||"degraded-extra"===n.status?n.roomGeom:void 0,multiWallNodes:u(e.rooms,t.walls,t.openCuts,i,t.cellCm,o,r)}}function*p(c,u,d=h){try{let f=0;for(const m of u){const u=c.spaces.find(e=>String(e?.id)===m.id);if(!u)continue;const g=e(u,m),v=d(m,g);let y=null;for(const e of m.rooms){if(!e.id)continue;const a=t(e);if(!a)continue;const l=n(m.rooms,e.id,g.walls,g.openCuts,i,g.cellCm,o,r,v.roomGeom,v.multiWallNodes)||a;y=_(y,s(l,g.physicalBodies)),yield}const h=a(y||[],m.stairs);let p=h.next();for(;!p.done;)yield,p=h.next();y=p.value;const b=g.cellCm/o;f+=l(y)*b*b/1e4}return f}catch{return null}}function b(e,t,n=h){const i=p(e,t,n);let o=i.next();for(;!o.done;)o=i.next();return o.value}function S(e,t,n,i){if("system"!==e.type)return null;if("device_count"===e.key)return null===t.deviceCount?null:String(t.deviceCount);if("total_area"===e.key)return null===t.areaM2?null:g(t.areaM2,"mi"===n?.config?.unit_system?.length);try{return new Intl.DateTimeFormat(i||void 0,{dateStyle:"short",timeStyle:"short",timeZone:n?.config?.time_zone||void 0}).format(t.now)}catch{return t.now.toLocaleString()}}function C(e,t){const n=e?.states?.[t];if(!n)return null;const i=f(e,t);return i?m(i,String(n.attributes?.unit_of_measurement||"")):null}export{p as cleanFloorAreaSteps,y as representedHaDeviceIds,h as spaceWallGeometry,C as summaryEntityValue,S as summarySystemValue,b as totalCleanFloorAreaM2};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";var e={"furn.title":"Möbelbibliothek","furn.symbol":"Symbol","furn.group_furniture":"Möbel","furn.group_appliance":"Geräte","furn.group_sanitary":"Sanitär","furn.group_other":"Sonstiges","furn.width":"Breite","furn.depth":"Tiefe","furn.back_to_categories":"Alle Kategorien","furn.pick_hint":"Wählen Sie eine Kategorie und dann ein Symbol.","furn.place_hint":"Klicken Sie auf den Plan — das Element wird an der nächsten Wand platziert. Mit Shift wird es frei platziert.","furn.cat_air_conditioner":"Klimaanlagen","furn.cat_armchair":"Sessel","furn.cat_bathtub":"Badewannen","furn.cat_bed":"Betten","furn.cat_bidet":"Bidets","furn.cat_boiler":"Warmwasserbereiter","furn.cat_chair":"Stühle","furn.cat_coffee_table":"Couchtische","furn.cat_computer":"Computer","furn.cat_cooktop":"Kochfelder","furn.cat_dining_table":"Esstische","furn.cat_dishwasher":"Geschirrspüler","furn.cat_dryer":"Wäschetrockner","furn.cat_exercise":"Fitnessgerät","furn.cat_fireplace":"Kamine","furn.cat_fridge":"Kühlschränke","furn.cat_hood":"Dunstabzugshaube","furn.cat_kitchen_cabinet":"Küchenschränke","furn.cat_kitchen_sink":"Küchenspülen","furn.cat_nightstand":"Schränke","furn.cat_oven":"Backofen","furn.cat_plant":"Pflanzen","furn.cat_rug":"Teppiche","furn.cat_shelving":"Regale","furn.cat_shower":"Duschen","furn.cat_sink":"Waschbecken","furn.cat_sofa":"Sofas","furn.cat_stairs":"Treppen","furn.cat_toilet":"Toiletten","furn.cat_tv":"Fernseher","furn.cat_wardrobe":"Kleiderschränke","furn.cat_washer":"Waschmaschinen","furn.cat_work_table":"Schreibtische","furn.sym_exercise":"Fitnessgerät","furn.sym_computer":"Computer","furn.sym_hood":"Dunstabzugshaube","furn.sym_oven":"Backofen","furn.sym_sofa":"Zweisitzer-Sofa","furn.sym_sofa_three_seat":"Dreisitzer-Sofa","furn.sym_sofa_corner_right":"Ecksofa rechts","furn.sym_armchair":"Polstersessel","furn.sym_armchair_office":"Bürostuhl","furn.sym_coffee_table":"Rechteckiger Couchtisch","furn.sym_coffee_table_round":"Runder Couchtisch","furn.sym_coffee_table_oval":"Ovaler Couchtisch","furn.sym_coffee_table_rounded":"Abgerundeter Couchtisch","furn.sym_table_dining":"Rechteckiger Esstisch","furn.sym_table_round":"Rundtisch","furn.sym_table_dining_oval":"Ovaler Esstisch","furn.sym_table_dining_rounded":"Abgerundeter Esstisch","furn.sym_chair":"Stuhl","furn.sym_chair_bar":"Barhocker","furn.sym_desk":"Rechteckiger Schreibtisch","furn.sym_desk_corner":"Eckschreibtisch","furn.sym_bed_double":"Doppelbett","furn.sym_bed_single":"Einzelbett","furn.sym_nightstand":"Nachttisch","furn.sym_cabinet_tv":"TV-Schrank","furn.sym_cabinet_shoe":"Schuhschrank","furn.sym_cabinet_sink":"Waschtischunterschrank","furn.sym_wardrobe":"Kleiderschrank","furn.sym_bookshelf":"Bücherschrank","furn.sym_wall_unit":"Schrankwand","furn.sym_kitchen_floor":"Küchen-Unterschrank","furn.sym_kitchen_floor_corner":"Küchen-Eckunterschrank","furn.sym_kitchen_wall":"Küchen-Hängeschrank","furn.sym_kitchen_wall_corner":"Küchen-Eckhängeschrank","furn.sym_shelf_floor":"Standregal","furn.sym_shelf_wall":"Wandregal","furn.sym_fridge":"Kühlschrank","furn.sym_stove":"Vierflammiges Kochfeld","furn.sym_cooktop_two":"Zweiflammiges Kochfeld","furn.sym_dishwasher":"Geschirrspüler","furn.sym_washer":"Waschmaschine","furn.sym_dryer":"Wäschetrockner","furn.sym_tv":"Fernseher mit Standfuß","furn.sym_tv_wall":"Wandfernseher","furn.sym_ac":"Klimaanlage","furn.sym_water_heater":"Wassererhitzer","furn.sym_toilet":"Stand-WC","furn.sym_toilet_built_in":"Wand-WC","furn.sym_bathtub":"Rechteckige Badewanne","furn.sym_bathtub_corner":"Eckbadewanne","furn.sym_shower":"Dusche","furn.sym_sink":"Waschbecken","furn.sym_kitchen_sink":"Einzel-Küchenspüle","furn.sym_kitchen_sink_double":"Doppel-Küchenspüle","furn.sym_bidet":"Stand-Bidet","furn.sym_bidet_built_in":"Wand-Bidet","furn.sym_stairs":"Treppe","furn.sym_fireplace":"Kamin","furn.sym_plant":"Pflanze","furn.sym_rug":"Teppich"};const r="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";export{e as dictionary,r as fingerprint};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";var e={"furn.title":"Möbelbibliothek","furn.symbol":"Symbol","furn.group_furniture":"Möbel","furn.group_appliance":"Geräte","furn.group_sanitary":"Sanitär","furn.group_other":"Sonstiges","furn.width":"Breite","furn.depth":"Tiefe","furn.back_to_categories":"Alle Kategorien","furn.pick_hint":"Wählen Sie eine Kategorie und dann ein Symbol.","furn.place_hint":"Klicken Sie auf den Plan — das Element wird an der nächsten Wand platziert. Mit Shift wird es frei platziert.","furn.cat_air_conditioner":"Klimaanlagen","furn.cat_armchair":"Sessel","furn.cat_bathtub":"Badewannen","furn.cat_bed":"Betten","furn.cat_bidet":"Bidets","furn.cat_boiler":"Warmwasserbereiter","furn.cat_chair":"Stühle","furn.cat_coffee_table":"Couchtische","furn.cat_computer":"Computer","furn.cat_cooktop":"Kochfelder","furn.cat_dining_table":"Esstische","furn.cat_dishwasher":"Geschirrspüler","furn.cat_dryer":"Wäschetrockner","furn.cat_exercise":"Fitnessgerät","furn.cat_fireplace":"Kamine","furn.cat_fridge":"Kühlschränke","furn.cat_hood":"Dunstabzugshaube","furn.cat_kitchen_cabinet":"Küchenschränke","furn.cat_kitchen_sink":"Küchenspülen","furn.cat_nightstand":"Schränke","furn.cat_oven":"Backofen","furn.cat_plant":"Pflanzen","furn.cat_rug":"Teppiche","furn.cat_shelving":"Regale","furn.cat_shower":"Duschen","furn.cat_sink":"Waschbecken","furn.cat_sofa":"Sofas","furn.cat_stairs":"Treppen","furn.cat_toilet":"Toiletten","furn.cat_tv":"Fernseher","furn.cat_wardrobe":"Kleiderschränke","furn.cat_washer":"Waschmaschinen","furn.cat_work_table":"Schreibtische","furn.sym_exercise":"Fitnessgerät","furn.sym_computer":"Computer","furn.sym_hood":"Dunstabzugshaube","furn.sym_oven":"Backofen","furn.sym_sofa":"Zweisitzer-Sofa","furn.sym_sofa_three_seat":"Dreisitzer-Sofa","furn.sym_sofa_corner_right":"Ecksofa rechts","furn.sym_armchair":"Polstersessel","furn.sym_armchair_office":"Bürostuhl","furn.sym_coffee_table":"Rechteckiger Couchtisch","furn.sym_coffee_table_round":"Runder Couchtisch","furn.sym_coffee_table_oval":"Ovaler Couchtisch","furn.sym_coffee_table_rounded":"Abgerundeter Couchtisch","furn.sym_table_dining":"Rechteckiger Esstisch","furn.sym_table_round":"Rundtisch","furn.sym_table_dining_oval":"Ovaler Esstisch","furn.sym_table_dining_rounded":"Abgerundeter Esstisch","furn.sym_chair":"Stuhl","furn.sym_chair_bar":"Barhocker","furn.sym_desk":"Rechteckiger Schreibtisch","furn.sym_desk_corner":"Eckschreibtisch","furn.sym_bed_double":"Doppelbett","furn.sym_bed_single":"Einzelbett","furn.sym_nightstand":"Nachttisch","furn.sym_cabinet_tv":"TV-Schrank","furn.sym_cabinet_shoe":"Schuhschrank","furn.sym_cabinet_sink":"Waschtischunterschrank","furn.sym_wardrobe":"Kleiderschrank","furn.sym_bookshelf":"Bücherschrank","furn.sym_wall_unit":"Schrankwand","furn.sym_kitchen_floor":"Küchen-Unterschrank","furn.sym_kitchen_floor_corner":"Küchen-Eckunterschrank","furn.sym_kitchen_wall":"Küchen-Hängeschrank","furn.sym_kitchen_wall_corner":"Küchen-Eckhängeschrank","furn.sym_shelf_floor":"Standregal","furn.sym_shelf_wall":"Wandregal","furn.sym_fridge":"Kühlschrank","furn.sym_stove":"Vierflammiges Kochfeld","furn.sym_cooktop_two":"Zweiflammiges Kochfeld","furn.sym_dishwasher":"Geschirrspüler","furn.sym_washer":"Waschmaschine","furn.sym_dryer":"Wäschetrockner","furn.sym_tv":"Fernseher mit Standfuß","furn.sym_tv_wall":"Wandfernseher","furn.sym_ac":"Klimaanlage","furn.sym_water_heater":"Wassererhitzer","furn.sym_toilet":"Stand-WC","furn.sym_toilet_built_in":"Wand-WC","furn.sym_bathtub":"Rechteckige Badewanne","furn.sym_bathtub_corner":"Eckbadewanne","furn.sym_shower":"Dusche","furn.sym_sink":"Waschbecken","furn.sym_kitchen_sink":"Einzel-Küchenspüle","furn.sym_kitchen_sink_double":"Doppel-Küchenspüle","furn.sym_bidet":"Stand-Bidet","furn.sym_bidet_built_in":"Wand-Bidet","furn.sym_stairs":"Treppe","furn.sym_fireplace":"Kamin","furn.sym_plant":"Pflanze","furn.sym_rug":"Teppich"};const r="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";export{e as dictionary,r as fingerprint};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";var e={"furn.title":"Bibliothèque de mobilier","furn.symbol":"Symbole","furn.group_furniture":"Mobilier","furn.group_appliance":"Électroménager","furn.group_sanitary":"Sanitaires","furn.group_other":"Autre","furn.width":"Largeur","furn.depth":"Profondeur","furn.back_to_categories":"Toutes les catégories","furn.pick_hint":"Choisissez un symbole, puis cliquez sur le plan.","furn.place_hint":"Cliquez sur le plan — le meuble se place contre le mur le plus proche. Maj permet un placement libre.","furn.cat_air_conditioner":"Climatiseurs","furn.cat_armchair":"Fauteuils","furn.cat_bathtub":"Baignoires","furn.cat_bed":"Lits","furn.cat_bidet":"Bidets","furn.cat_boiler":"Chauffe-eau","furn.cat_chair":"Chaises","furn.cat_coffee_table":"Tables basses","furn.cat_computer":"Ordinateur","furn.cat_cooktop":"Plaques de cuisson","furn.cat_dining_table":"Tables à manger","furn.cat_dishwasher":"Lave-vaisselle","furn.cat_dryer":"Sèche-linge","furn.cat_exercise":"Équipement de sport","furn.cat_fireplace":"Cheminées","furn.cat_fridge":"Réfrigérateurs","furn.cat_hood":"Hotte","furn.cat_kitchen_cabinet":"Meubles de cuisine","furn.cat_kitchen_sink":"Éviers de cuisine","furn.cat_nightstand":"Meubles","furn.cat_oven":"Four","furn.cat_plant":"Plantes","furn.cat_rug":"Tapis","furn.cat_shelving":"Étagères","furn.cat_shower":"Douches","furn.cat_sink":"Lavabos","furn.cat_sofa":"Canapés","furn.cat_stairs":"Escaliers","furn.cat_toilet":"Toilettes","furn.cat_tv":"Téléviseurs","furn.cat_wardrobe":"Armoires","furn.cat_washer":"Lave-linge","furn.cat_work_table":"Bureaux","furn.sym_exercise":"Équipement de sport","furn.sym_computer":"Ordinateur","furn.sym_hood":"Hotte","furn.sym_oven":"Four","furn.sym_sofa":"Canapé","furn.sym_sofa_three_seat":"Canapé trois places","furn.sym_sofa_corner_right":"Canapé d'angle droit","furn.sym_armchair":"Fauteuil","furn.sym_armchair_office":"Chaise de bureau","furn.sym_coffee_table":"Table basse","furn.sym_coffee_table_round":"Table basse ronde","furn.sym_coffee_table_oval":"Table basse ovale","furn.sym_coffee_table_rounded":"Table basse arrondie","furn.sym_table_dining":"Table à manger","furn.sym_table_round":"Table ronde","furn.sym_table_dining_oval":"Table à manger ovale","furn.sym_table_dining_rounded":"Table à manger arrondie","furn.sym_chair":"Chaise","furn.sym_chair_bar":"Tabouret de bar","furn.sym_desk":"Bureau","furn.sym_desk_corner":"Bureau d'angle","furn.sym_bed_double":"Lit double","furn.sym_bed_single":"Lit simple","furn.sym_nightstand":"Table de chevet","furn.sym_cabinet_tv":"Meuble TV","furn.sym_cabinet_shoe":"Meuble à chaussures","furn.sym_cabinet_sink":"Meuble sous lavabo","furn.sym_wardrobe":"Armoire","furn.sym_bookshelf":"Bibliothèque","furn.sym_wall_unit":"Élément mural","furn.sym_kitchen_floor":"Module de cuisine bas","furn.sym_kitchen_floor_corner":"Module de cuisine bas d'angle","furn.sym_kitchen_wall":"Module de cuisine haut","furn.sym_kitchen_wall_corner":"Module de cuisine haut d'angle","furn.sym_shelf_floor":"Étagère au sol","furn.sym_shelf_wall":"Étagère murale","furn.sym_fridge":"Réfrigérateur","furn.sym_stove":"Cuisinière","furn.sym_cooktop_two":"Plaque à deux feux","furn.sym_dishwasher":"Lave-vaisselle","furn.sym_washer":"Lave-linge","furn.sym_dryer":"Sèche-linge","furn.sym_tv":"Téléviseur","furn.sym_tv_wall":"Téléviseur mural","furn.sym_ac":"Climatiseur","furn.sym_water_heater":"Chauffe-eau","furn.sym_toilet":"Toilettes","furn.sym_toilet_built_in":"Toilettes suspendues","furn.sym_bathtub":"Baignoire","furn.sym_bathtub_corner":"Baignoire d'angle","furn.sym_shower":"Douche","furn.sym_sink":"Lavabo","furn.sym_kitchen_sink":"Évier de cuisine","furn.sym_kitchen_sink_double":"Évier de cuisine double","furn.sym_bidet":"Bidet","furn.sym_bidet_built_in":"Bidet suspendu","furn.sym_stairs":"Escalier","furn.sym_fireplace":"Cheminée","furn.sym_plant":"Plante","furn.sym_rug":"Tapis"};const r="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";export{e as dictionary,r as fingerprint};
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";var e={"furn.title":"Bibliothèque de mobilier","furn.symbol":"Symbole","furn.group_furniture":"Mobilier","furn.group_appliance":"Électroménager","furn.group_sanitary":"Sanitaires","furn.group_other":"Autre","furn.width":"Largeur","furn.depth":"Profondeur","furn.back_to_categories":"Toutes les catégories","furn.pick_hint":"Choisissez un symbole, puis cliquez sur le plan.","furn.place_hint":"Cliquez sur le plan — le meuble se place contre le mur le plus proche. Maj permet un placement libre.","furn.cat_air_conditioner":"Climatiseurs","furn.cat_armchair":"Fauteuils","furn.cat_bathtub":"Baignoires","furn.cat_bed":"Lits","furn.cat_bidet":"Bidets","furn.cat_boiler":"Chauffe-eau","furn.cat_chair":"Chaises","furn.cat_coffee_table":"Tables basses","furn.cat_computer":"Ordinateur","furn.cat_cooktop":"Plaques de cuisson","furn.cat_dining_table":"Tables à manger","furn.cat_dishwasher":"Lave-vaisselle","furn.cat_dryer":"Sèche-linge","furn.cat_exercise":"Équipement de sport","furn.cat_fireplace":"Cheminées","furn.cat_fridge":"Réfrigérateurs","furn.cat_hood":"Hotte","furn.cat_kitchen_cabinet":"Meubles de cuisine","furn.cat_kitchen_sink":"Éviers de cuisine","furn.cat_nightstand":"Meubles","furn.cat_oven":"Four","furn.cat_plant":"Plantes","furn.cat_rug":"Tapis","furn.cat_shelving":"Étagères","furn.cat_shower":"Douches","furn.cat_sink":"Lavabos","furn.cat_sofa":"Canapés","furn.cat_stairs":"Escaliers","furn.cat_toilet":"Toilettes","furn.cat_tv":"Téléviseurs","furn.cat_wardrobe":"Armoires","furn.cat_washer":"Lave-linge","furn.cat_work_table":"Bureaux","furn.sym_exercise":"Équipement de sport","furn.sym_computer":"Ordinateur","furn.sym_hood":"Hotte","furn.sym_oven":"Four","furn.sym_sofa":"Canapé","furn.sym_sofa_three_seat":"Canapé trois places","furn.sym_sofa_corner_right":"Canapé d'angle droit","furn.sym_armchair":"Fauteuil","furn.sym_armchair_office":"Chaise de bureau","furn.sym_coffee_table":"Table basse","furn.sym_coffee_table_round":"Table basse ronde","furn.sym_coffee_table_oval":"Table basse ovale","furn.sym_coffee_table_rounded":"Table basse arrondie","furn.sym_table_dining":"Table à manger","furn.sym_table_round":"Table ronde","furn.sym_table_dining_oval":"Table à manger ovale","furn.sym_table_dining_rounded":"Table à manger arrondie","furn.sym_chair":"Chaise","furn.sym_chair_bar":"Tabouret de bar","furn.sym_desk":"Bureau","furn.sym_desk_corner":"Bureau d'angle","furn.sym_bed_double":"Lit double","furn.sym_bed_single":"Lit simple","furn.sym_nightstand":"Table de chevet","furn.sym_cabinet_tv":"Meuble TV","furn.sym_cabinet_shoe":"Meuble à chaussures","furn.sym_cabinet_sink":"Meuble sous lavabo","furn.sym_wardrobe":"Armoire","furn.sym_bookshelf":"Bibliothèque","furn.sym_wall_unit":"Élément mural","furn.sym_kitchen_floor":"Module de cuisine bas","furn.sym_kitchen_floor_corner":"Module de cuisine bas d'angle","furn.sym_kitchen_wall":"Module de cuisine haut","furn.sym_kitchen_wall_corner":"Module de cuisine haut d'angle","furn.sym_shelf_floor":"Étagère au sol","furn.sym_shelf_wall":"Étagère murale","furn.sym_fridge":"Réfrigérateur","furn.sym_stove":"Cuisinière","furn.sym_cooktop_two":"Plaque à deux feux","furn.sym_dishwasher":"Lave-vaisselle","furn.sym_washer":"Lave-linge","furn.sym_dryer":"Sèche-linge","furn.sym_tv":"Téléviseur","furn.sym_tv_wall":"Téléviseur mural","furn.sym_ac":"Climatiseur","furn.sym_water_heater":"Chauffe-eau","furn.sym_toilet":"Toilettes","furn.sym_toilet_built_in":"Toilettes suspendues","furn.sym_bathtub":"Baignoire","furn.sym_bathtub_corner":"Baignoire d'angle","furn.sym_shower":"Douche","furn.sym_sink":"Lavabo","furn.sym_kitchen_sink":"Évier de cuisine","furn.sym_kitchen_sink_double":"Évier de cuisine double","furn.sym_bidet":"Bidet","furn.sym_bidet_built_in":"Bidet suspendu","furn.sym_stairs":"Escalier","furn.sym_fireplace":"Cheminée","furn.sym_plant":"Plante","furn.sym_rug":"Tapis"};const r="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";export{e as dictionary,r as fingerprint};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";var e={title:"Zigbee-Verbindungen",toggle:"Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",hint:"Zeigt Eltern und nächste Hops laut Integration. Beim Darüberfahren werden keine Daten abgerufen.",help:"Zeigt den Elternknoten eines Endgeräts oder den aktiven nächsten Hop eines Routers zum Koordinator laut Integration. Es gibt höchstens einen ausgehenden Pfeil; eingehende Pfeile zeigen Geräte, die über diesen Knoten routen. Gewöhnliche Nachbarn werden ausgeblendet. Alle Linien sind durchgezogen. Linie und Pfeil verwenden LQI 0–255: rot bei 0, gelb bei 128, grün bei 255. Ohne LQI sind sie grau mit schwarzer Kontur. Eine Beschriftung am Pfeilende nennt ein Ziel in einem anderen Bereich oder außerhalb des Plans. Keine Routendaten bedeutet fehlende eindeutige Angaben, nicht dass das Gerät offline ist. Dies sind zuletzt empfangene Integrationsdaten, keine Live-Paketverfolgung oder Garantie des gesamten Pfads. Der ZHA-Cache kann beim Lesen bereits alt sein; veraltete und unvollständige Daten werden markiert. Verbindungen sind nur mit der Maus sichtbar.",help_aria:"Hilfe: Zigbee-Verbindungen",admin_only:"Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",save_first:"Speichere diese Einstellung, bevor Topologiedaten geladen werden.",zha:"ZHA",zha_read:"ZHA-Daten lesen",zha_hint:"Liest den ZHA-Topologiecache; es wird kein Funkscan gestartet.",z2m:"Zigbee2MQTT",z2m_topics:"Basistopics (eines pro Zeile)",z2m_update:"Karte aktualisieren",z2m_warning:"Ein Netzwerkscan mit Routen kann länger als 10 Minuten dauern. House Plan wartet im Hintergrund weiter. Der Scan kann Zigbee vorübergehend verlangsamen.",scan_connecting:"Verbindung zu Zigbee2MQTT wird hergestellt",scan_waiting:"Warten auf die Karte von Zigbee2MQTT",scan_elapsed:"Vergangen: {time}",scan_background_hint:"Du kannst die Einstellungen schließen — das Warten wird im Hintergrund fortgesetzt.",scan_long_wait:"Zigbee2MQTT hat noch nicht geantwortet. Das Warten wird fortgesetzt.",scan_cancel:"Anfrage abbrechen",scan_cancel_hint:"Nur das Warten von House Plan wird beendet. Zigbee2MQTT kann den Scan fortsetzen.",scan_cancelled:"Warten abgebrochen. Der Scan von Zigbee2MQTT ist möglicherweise noch nicht beendet.",scan_backend_required:"Aktualisieren Sie die House-Plan-Integration und starten Sie Home Assistant neu, um Kartenscans im Hintergrund zu nutzen.",scan_connection_lost:"Die MQTT-Verbindung wurde unterbrochen; das Ergebnis ist möglicherweise nicht angekommen. Das Warten wurde beendet, Zigbee2MQTT scannt möglicherweise weiter.",scan_frontend_disconnected:"Die Verbindung zu Home Assistant wurde unterbrochen. Das Warten wird im Hintergrund fortgesetzt. Stelle die Verbindung wieder her, um den Status zu sehen.",scan_unavailable:"MQTT ist in Home Assistant nicht verfügbar oder wird nicht unterstützt.",status_idle:"Daten nicht geladen",status_loading:"Wird geladen…",status_ready:"Empfangen: {time}",status_stale:"Empfangen: {time} · veraltet",status_partial:"Empfangen: {time} · unvollständige Daten",status_no_links:"Empfangen: {time} · keine bestätigten Routen",error_permission:"Administratorrechte sind erforderlich.",error_unsupported:"Dieser Anbieter oder die Home-Assistant-API ist nicht verfügbar.",error_timeout:"Der Anbieter hat nicht rechtzeitig geantwortet.",error_invalid_topic:"Prüfe das Zigbee2MQTT-Basistopic.",error_invalid_payload:"Der Anbieter hat nicht unterstützte Topologiedaten geliefert.",error_provider:"Topologiedaten konnten nicht geladen werden.",remote_count:"+{n} in anderen Bereichen",route_device_not_on_plan:"Gerät ist nicht im Plan",route_coordinator_not_on_plan:"Koordinator ist nicht im Plan",route_other_space:"anderer Bereich",route_other_space_named:"{space} ({name})",route_device_not_on_plan_named:"Gerät ist nicht im Plan ({name})",route_unknown:"Keine Routendaten",route_stale:"Veraltete Daten",route_partial:"Unvollständige Daten",status_cache:"Integrationscache; Zeitpunkt des Funkscans unbekannt"};const n="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";export{e as dictionary,n as fingerprint};
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";var e={title:"Zigbee-Verbindungen",toggle:"Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",hint:"Es werden nur beobachtete direkte Nachbarn angezeigt. Beim Darüberfahren werden keine Daten abgerufen.",help:"Zeigt, mit welchen Zigbee-Geräten jedes Gerät direkt verbunden ist. Die Linienfarbe ist die Verbindungsqualität (LQI) auf derselben Skala wie der LQI-Wert des Geräts: rot bei 40 und darunter, grün bei 180 und darüber. Eine gestrichelte Linie bedeutet, dass die Qualität nicht gemeldet wurde. Ein Pfeil zeigt auf das nächste Gerät auf dem Weg zum Koordinator: ein Pfeil führt hinaus, eingehende Pfeile sind die Geräte, die über dieses Gerät routen. Eine Linie ohne Pfeil ist ein Ersatznachbar. Das ist der Routenbaum, den House Plan bildet, nicht der Weg eines Pakets in diesem Moment: eine Beschriftung am Pfeilende bedeutet, dass das Ziel nicht auf diesem Plan liegt, und ein fehlender Pfeil, dass die Route unbekannt ist. Die Daten stammen aus dem letzten Laden und veralten; Verbindungen sind nur mit der Maus sichtbar.",help_aria:"Hilfe: Zigbee-Verbindungen",admin_only:"Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",save_first:"Speichere diese Einstellung, bevor Topologiedaten geladen werden.",zha:"ZHA",zha_read:"ZHA-Daten lesen",zha_hint:"Liest den ZHA-Topologiecache; es wird kein Funkscan gestartet.",z2m:"Zigbee2MQTT",z2m_topics:"Basistopics (eines pro Zeile)",z2m_update:"Karte aktualisieren",z2m_warning:"Ein Netzwerkscan kann 10 Sekunden bis 2 Minuten dauern und Zigbee vorübergehend verlangsamen.",status_idle:"Daten nicht geladen",status_loading:"Wird geladen…",status_ready:"Empfangen: {time}",status_stale:"Empfangen: {time} · veraltet",status_partial:"Empfangen: {time} · einige Knoten fehlen",status_no_links:"Empfangen: {time} · keine beobachteten Verbindungen gefunden",error_permission:"Administratorrechte sind erforderlich.",error_unsupported:"Dieser Anbieter oder die Home-Assistant-API ist nicht verfügbar.",error_timeout:"Der Anbieter hat nicht rechtzeitig geantwortet.",error_invalid_topic:"Prüfe das Zigbee2MQTT-Basistopic.",error_invalid_payload:"Der Anbieter hat nicht unterstützte Topologiedaten geliefert.",error_provider:"Topologiedaten konnten nicht geladen werden.",remote_count:"+{n} in anderen Bereichen",route_device_not_on_plan:"Gerät ist nicht im Plan",route_coordinator_not_on_plan:"Koordinator ist nicht im Plan",route_other_space:"anderer Bereich"};const n="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";export{e as dictionary,n as fingerprint};
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";var e={title:"Liens Zigbee",toggle:"Afficher les liens Zigbee au survol d’un appareil",hint:"Seuls les voisins directs observés sont affichés. Le survol ne lance aucune requête.",help:"Montre avec quels appareils Zigbee chaque appareil communique directement. La couleur de la ligne est la qualité du lien (LQI) sur la même échelle que l'indicateur LQI de l'appareil : rouge à 40 et en dessous, vert à 180 et au-dessus. Une ligne pointillée signifie que la qualité n'a pas été communiquée. Une flèche pointe vers l'appareil suivant sur le chemin du coordinateur : une flèche part, les flèches entrantes sont les appareils qui passent par celui-ci. Une ligne sans flèche est un voisin de secours. C'est l'arbre de routes que construit House Plan, pas le trajet d'un paquet à cet instant : une étiquette au bout d'une flèche signifie que sa cible n'est pas sur ce plan, et l'absence de flèche que la route est inconnue. Les données proviennent du dernier chargement et vieillissent ; les liens ne sont visibles qu'à la souris.",help_aria:"Aide : liens Zigbee",admin_only:"Les données de topologie sont réservées aux administrateurs Home Assistant.",save_first:"Enregistrez ce réglage avant de charger les données de topologie.",zha:"ZHA",zha_read:"Lire les données ZHA",zha_hint:"Lit la topologie en cache de ZHA sans lancer d’analyse radio.",z2m:"Zigbee2MQTT",z2m_topics:"Topics de base (un par ligne)",z2m_update:"Actualiser la carte",z2m_warning:"L’analyse de la carte réseau peut durer de 10 secondes à 2 minutes et réduire temporairement la réactivité de Zigbee.",status_idle:"Données non chargées",status_loading:"Chargement…",status_ready:"Reçu : {time}",status_stale:"Reçu : {time} · obsolète",status_partial:"Reçu : {time} · certains nœuds sont omis",status_no_links:"Reçu : {time} · aucun lien observé trouvé",error_permission:"Les droits administrateur sont requis.",error_unsupported:"Ce fournisseur ou l’API Home Assistant n’est pas disponible.",error_timeout:"Le fournisseur n’a pas répondu à temps.",error_invalid_topic:"Vérifiez le topic de base Zigbee2MQTT.",error_invalid_payload:"Le fournisseur a renvoyé des données de topologie non prises en charge.",error_provider:"Impossible de charger les données de topologie.",remote_count:"+{n} dans d’autres espaces",route_device_not_on_plan:"appareil absent du plan",route_coordinator_not_on_plan:"coordinateur absent du plan",route_other_space:"autre espace"};const s="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";export{e as dictionary,s as fingerprint};
|
||||
@@ -0,0 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";var e={title:"Liens Zigbee",toggle:"Afficher les liens Zigbee au survol d’un appareil",hint:"Affiche les parents et prochains sauts signalés par l’intégration. Le survol ne lance aucune requête.",help:"Affiche le parent d’un appareil terminal ou le prochain saut actif d’un routeur vers le coordinateur selon l’intégration. Il y a au plus une flèche sortante ; les flèches entrantes indiquent les appareils passant par ce nœud. Les simples voisins sont masqués. Toutes les lignes sont continues. Ligne et flèche utilisent le LQI 0–255 : rouge à 0, jaune à 128, vert à 255. Sans LQI, elles sont grises avec un contour noir. Une étiquette au bout de la flèche nomme une cible dans un autre espace ou absente du plan. Aucune donnée de route indique l’absence d’information univoque, pas un appareil hors ligne. Il s’agit des derniers relevés de l’intégration, pas du trajet des paquets en direct ni d’une garantie du chemin entier. Le cache ZHA peut déjà être ancien à la lecture ; les données anciennes et incomplètes sont signalées. Les liens ne sont visibles qu’à la souris.",help_aria:"Aide : liens Zigbee",admin_only:"Les données de topologie sont réservées aux administrateurs Home Assistant.",save_first:"Enregistrez ce réglage avant de charger les données de topologie.",zha:"ZHA",zha_read:"Lire les données ZHA",zha_hint:"Lit la topologie en cache de ZHA sans lancer d’analyse radio.",z2m:"Zigbee2MQTT",z2m_topics:"Topics de base (un par ligne)",z2m_update:"Actualiser la carte",z2m_warning:"L’analyse de la carte réseau avec les routes peut prendre plus de 10 minutes. House Plan continue d’attendre en arrière-plan. L’analyse peut réduire temporairement la réactivité de Zigbee.",scan_connecting:"Connexion à Zigbee2MQTT",scan_waiting:"En attente de la carte de Zigbee2MQTT",scan_elapsed:"Temps écoulé : {time}",scan_background_hint:"Vous pouvez fermer les réglages — l’attente continuera en arrière-plan.",scan_long_wait:"Zigbee2MQTT n’a pas encore répondu. L’attente continue.",scan_cancel:"Annuler la demande",scan_cancel_hint:"Seule l’attente de House Plan est annulée. L’analyse de Zigbee2MQTT peut continuer.",scan_cancelled:"Attente annulée. L’analyse de Zigbee2MQTT n’est peut-être pas encore terminée.",scan_backend_required:"Mettez à jour l’intégration House Plan et redémarrez Home Assistant pour utiliser les analyses de carte en arrière-plan.",scan_connection_lost:"La connexion MQTT a été perdue ; le résultat n’est peut-être pas arrivé. L’attente est terminée, mais Zigbee2MQTT peut encore analyser le réseau.",scan_frontend_disconnected:"La connexion à Home Assistant a été interrompue. L’attente continue en arrière-plan. Reconnectez-vous pour retrouver l’état de la demande.",scan_unavailable:"MQTT n’est pas disponible ou n’est pas pris en charge dans Home Assistant.",status_idle:"Données non chargées",status_loading:"Chargement…",status_ready:"Reçu : {time}",status_stale:"Reçu : {time} · obsolète",status_partial:"Reçu : {time} · données incomplètes",status_no_links:"Reçu : {time} · aucune route confirmée",error_permission:"Les droits administrateur sont requis.",error_unsupported:"Ce fournisseur ou l’API Home Assistant n’est pas disponible.",error_timeout:"Le fournisseur n’a pas répondu à temps.",error_invalid_topic:"Vérifiez le topic de base Zigbee2MQTT.",error_invalid_payload:"Le fournisseur a renvoyé des données de topologie non prises en charge.",error_provider:"Impossible de charger les données de topologie.",remote_count:"+{n} dans d’autres espaces",route_device_not_on_plan:"appareil absent du plan",route_coordinator_not_on_plan:"coordinateur absent du plan",route_other_space:"autre espace",route_other_space_named:"{space} ({name})",route_device_not_on_plan_named:"Appareil absent du plan ({name})",route_unknown:"Aucune donnée de route",route_stale:"Données anciennes",route_partial:"Données incomplètes",status_cache:"Cache de l’intégration ; date de l’analyse radio inconnue"};const n="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";export{e as dictionary,n as fingerprint};
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";var e={title:"Связи Zigbee",toggle:"Показывать связи Zigbee при наведении на устройство",hint:"Показываются только наблюдаемые прямые соседи. Наведение ничего не запрашивает.",help:"Показывает, с какими устройствами Zigbee каждое связано напрямую. Цвет линии — качество связи (LQI) по той же шкале, что у показателя LQI устройства: красный при 40 и ниже, зелёный при 180 и выше. Пунктир — качество не сообщено. Стрелка ведёт к следующему устройству по пути к координатору: исходящая одна, входящие — те, кто ходит через это устройство. Линия без стрелки — запасной сосед. Это дерево маршрутов, которое строит House Plan, а не путь пакета в эту секунду: подпись на конце стрелки значит, что цель не на этом плане, а отсутствие стрелки — что путь неизвестен. Данные из последней загрузки и устаревают, связи видны только при работе мышью.",help_aria:"Справка: связи Zigbee",admin_only:"Данные топологии доступны только администраторам Home Assistant.",save_first:"Сохраните настройку перед загрузкой данных топологии.",zha:"ZHA",zha_read:"Прочитать данные ZHA",zha_hint:"Читает сохранённую топологию ZHA и не запускает радио-сканирование.",z2m:"Zigbee2MQTT",z2m_topics:"Базовые топики (по одному в строке)",z2m_update:"Обновить карту",z2m_warning:"Сканирование карты сети занимает от 10 секунд до 2 минут и временно может снизить отзывчивость Zigbee.",status_idle:"Данные не загружены",status_loading:"Загрузка…",status_ready:"Получено: {time}",status_stale:"Получено: {time} · данные устарели",status_partial:"Получено: {time} · часть узлов не показана",status_no_links:"Получено: {time} · наблюдаемые связи не найдены",error_permission:"Нужны права администратора.",error_unsupported:"Провайдер или API Home Assistant недоступен.",error_timeout:"Провайдер не ответил вовремя.",error_invalid_topic:"Проверьте базовый топик Zigbee2MQTT.",error_invalid_payload:"Провайдер вернул неподдерживаемые данные топологии.",error_provider:"Не удалось загрузить данные топологии.",remote_count:"+{n} в других пространствах",route_device_not_on_plan:"устройства нет на плане",route_coordinator_not_on_plan:"координатора нет на плане",route_other_space:"другое пространство"};const a="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";export{e as dictionary,a as fingerprint};
|
||||
@@ -1 +0,0 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";import{n as t,c as n}from"./zigbee-topology-BG_a6cIb.js";import{aP as e}from"./houseplan-card-BOdiRDFR.js";import"./namespace-language-D86tJUeD.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function m(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function g(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function h(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,h=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,h=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&m(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=m(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&h?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await g(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await g(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,h as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab07da35681c15ab6136c323500b69766580ea4a788e399ea88f9e7497549172";try{await import("./houseplan-assets/houseplan-card-BOdiRDFR.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale houseplan-card.js: the implementation chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="e3b1c6ebac6804dc8b326f09ec0914bd28773bb07df9074a9c8d958852619d90";try{await import("./houseplan-assets/houseplan-card-DrYGKEMS.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale houseplan-card.js: the implementation chunk is unavailable",e)}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"domain": "houseplan",
|
||||
"name": "House Plan",
|
||||
"after_dependencies": [
|
||||
"lovelace"
|
||||
"lovelace",
|
||||
"mqtt"
|
||||
],
|
||||
"codeowners": [
|
||||
"@Matysh"
|
||||
@@ -20,5 +21,5 @@
|
||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.79.0"
|
||||
"version": "1.80.0-beta.6"
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ class HouseplanData:
|
||||
junction_baseline: tuple[int, dict[str, dict[str, int]]] | None = None
|
||||
# Runtime-only #485 authority; never serialized by HouseplanStore.
|
||||
radar_coordinator: Any | None = None
|
||||
# #800: MQTT jobs and last-good maps live only for this loaded entry.
|
||||
zigbee_coordinator: Any | None = None
|
||||
|
||||
|
||||
HouseplanConfigEntry = ConfigEntry[HouseplanData]
|
||||
|
||||
@@ -156,6 +156,9 @@ def _global_settings(value: object) -> dict[str, Any]:
|
||||
show_room_tooltip = value.get("show_room_tooltip")
|
||||
if isinstance(show_room_tooltip, bool):
|
||||
out["show_room_tooltip"] = show_room_tooltip
|
||||
show_device_battery = value.get("show_device_battery")
|
||||
if isinstance(show_device_battery, bool) or show_device_battery == "low":
|
||||
out["show_device_battery"] = show_device_battery
|
||||
volumetric_view = value.get("volumetric_view")
|
||||
if isinstance(volumetric_view, bool):
|
||||
out["volumetric_view"] = volumetric_view
|
||||
@@ -286,6 +289,9 @@ def _project_marker(ids: _Pseudonyms, marker: dict[str, Any]) -> dict[str, Any]:
|
||||
"ripple_size", "size", "angle", "glow_radius_cm", "glow_color", "is_light",
|
||||
"use_climate_temp",
|
||||
)))
|
||||
hide_battery = marker.get("hide_battery")
|
||||
if isinstance(hide_battery, bool):
|
||||
out["hide_battery"] = hide_battery
|
||||
icon = marker.get("icon")
|
||||
if isinstance(icon, str) and _SAFE_ICON.fullmatch(icon):
|
||||
out["icon"] = icon
|
||||
|
||||
@@ -1402,6 +1402,9 @@ def _north_deg(value):
|
||||
|
||||
_BG_MODE = vol.In(["static", "daynight"])
|
||||
_SUN_RAY_ORIGIN = vol.In(["inner", "outer"])
|
||||
# #792/#807: absent = every battery device, exact False = none, "low" = only
|
||||
# the red low state. Strings other than "low" and truthy numbers are refused.
|
||||
_DEVICE_BATTERY = vol.Any(bool, "low")
|
||||
|
||||
SPACE_DISPLAY_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -2000,6 +2003,8 @@ MARKER_SCHEMA = vol.Schema(
|
||||
),
|
||||
# climate current_temperature: badge + room-average vote (off unless True)
|
||||
vol.Optional("use_climate_temp"): vol.Any(bool, None),
|
||||
# Per-marker battery indicator override; absence/False keeps global policy.
|
||||
vol.Optional("hide_battery"): bool,
|
||||
vol.Optional("room_id"): vol.Any(str, None),
|
||||
# Keep in sync with DISPLAY_MODES in src/logic.ts. `ripple` is no longer
|
||||
# offered, but remains accepted while old stores migrate to icon_ripple.
|
||||
@@ -2321,6 +2326,7 @@ CONFIG_SCHEMA = vol.All(
|
||||
vol.Optional("sun_rays"): bool,
|
||||
vol.Optional("sun_ray_origin"): _SUN_RAY_ORIGIN,
|
||||
vol.Optional("show_room_tooltip"): bool,
|
||||
vol.Optional("show_device_battery"): _DEVICE_BATTERY,
|
||||
# #649: installation-wide 2.5D View; absent/false keeps Flat.
|
||||
vol.Optional("volumetric_view"): bool,
|
||||
# #661: the moon on the "Follow the Sun" background; absent/false = off.
|
||||
|
||||
@@ -258,6 +258,7 @@ async def _resolved_write_pair(
|
||||
def async_register(hass: HomeAssistant) -> None:
|
||||
"""Register the WS commands."""
|
||||
from .radar_websocket import async_register as async_register_radar
|
||||
from .zigbee_websocket import async_register as async_register_zigbee
|
||||
|
||||
websocket_api.async_register_command(hass, ws_layout_get)
|
||||
websocket_api.async_register_command(hass, ws_trail_get)
|
||||
@@ -288,6 +289,7 @@ def async_register(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, ws_support_preview_discard)
|
||||
websocket_api.async_register_command(hass, ws_support_submit)
|
||||
async_register_radar(hass)
|
||||
async_register_zigbee(hass)
|
||||
|
||||
|
||||
def _runtime(hass: HomeAssistant, connection, msg_id: int) -> HouseplanData | None:
|
||||
@@ -1481,6 +1483,8 @@ async def ws_config_get(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
|
||||
"decor_assets_api": DECOR_ASSETS_API_VERSION,
|
||||
"summary_panel_api": SUMMARY_PANEL_API_VERSION,
|
||||
**({"radar_stage1_api": 1} if rt.radar_coordinator is not None else {}),
|
||||
**({"zigbee_scan_api": 1} if rt.zigbee_coordinator is not None
|
||||
and not rt.zigbee_coordinator.closed else {}),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
"""Bounded, entry-owned Zigbee2MQTT requests; never a polling radio scanner."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from time import monotonic, time
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
MAX_TOPICS = 8
|
||||
MAX_LISTENERS = 32
|
||||
MAX_PAYLOAD_BYTES = 2 * 1024 * 1024
|
||||
MAX_NODES = 1000
|
||||
MAX_LINKS = 6000
|
||||
CANCEL_AFTER_MS = 600_000
|
||||
TRANSPORT_TIMEOUT = 10.0
|
||||
INFO_TIMEOUT = 4.0
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZigbeeScanError(Exception):
|
||||
"""A stable, already localized WebSocket command refusal."""
|
||||
|
||||
def __init__(self, code: str) -> None:
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
|
||||
|
||||
def normalize_base_topic(value: object) -> str:
|
||||
"""Match the frontend's exact-topic normalization, without a saved-topic gate."""
|
||||
if not isinstance(value, str):
|
||||
raise ZigbeeScanError("invalid_data")
|
||||
topic = re.sub(r"/{2,}", "/", value.strip().strip("/"))
|
||||
# JS counts UTF-16 code units; reject lone surrogates as invalid MQTT UTF-8.
|
||||
try:
|
||||
length = len(topic.encode("utf-16-le")) // 2
|
||||
topic.encode("utf-8")
|
||||
except UnicodeError as err:
|
||||
raise ZigbeeScanError("invalid_data") from err
|
||||
if not topic or length > 180 or re.search(r"[#+\x00-\x1f\x7f]", topic):
|
||||
raise ZigbeeScanError("invalid_data")
|
||||
return topic
|
||||
|
||||
|
||||
def _parse_payload(payload: object) -> dict[str, Any] | None:
|
||||
"""Bound bytes before decoding; uncorrelatable garbage must not end a job."""
|
||||
try:
|
||||
if isinstance(payload, str):
|
||||
payload = payload.encode("utf-8")
|
||||
if not isinstance(payload, bytes) or len(payload) > MAX_PAYLOAD_BYTES:
|
||||
return None
|
||||
value = json.loads(payload)
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
pending = [(value, 0)]
|
||||
inspected = 0
|
||||
while pending:
|
||||
item, depth = pending.pop()
|
||||
inspected += 1
|
||||
if depth > 32 or inspected > 100_000:
|
||||
return None
|
||||
if isinstance(item, dict):
|
||||
pending.extend((child, depth + 1) for child in item.values())
|
||||
elif isinstance(item, list):
|
||||
pending.extend((child, depth + 1) for child in item)
|
||||
elif isinstance(item, float) and not math.isfinite(item):
|
||||
return None
|
||||
return value
|
||||
except (ValueError, UnicodeError, RecursionError):
|
||||
return None
|
||||
|
||||
|
||||
def _valid_map(message: dict[str, Any]) -> bool:
|
||||
"""Check the raw-map shape, not routing evidence (that remains TypeScript)."""
|
||||
data = message.get("data")
|
||||
value = data.get("value", data) if isinstance(data, dict) else data
|
||||
if value is None:
|
||||
value = message.get("value", message)
|
||||
if isinstance(value, str):
|
||||
value = _parse_payload(value)
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
nodes, links = value.get("nodes"), value.get("links")
|
||||
if not isinstance(nodes, list) or len(nodes) > MAX_NODES:
|
||||
return False
|
||||
if not isinstance(links, list) or len(links) > MAX_LINKS:
|
||||
return False
|
||||
for node in nodes:
|
||||
if not isinstance(node, dict):
|
||||
return False
|
||||
ieee = node.get("ieeeAddr", node.get("ieee_address", node.get("ieee")))
|
||||
if not isinstance(ieee, str) or not re.fullmatch(
|
||||
r"(?:0x)?[0-9a-fA-F]{16}|(?:[0-9a-fA-F]{2}[:-]){7}[0-9a-fA-F]{2}", ieee,
|
||||
):
|
||||
return False
|
||||
routes_count = 0
|
||||
for link in links:
|
||||
if not isinstance(link, dict):
|
||||
return False
|
||||
for side in ("source", "target"):
|
||||
endpoint = link.get(f"{side}IeeeAddr", link.get(side))
|
||||
if isinstance(endpoint, bool) or not isinstance(endpoint, (dict, str, int)):
|
||||
return False
|
||||
if not endpoint and endpoint != 0:
|
||||
return False
|
||||
routes = link.get("routes", [])
|
||||
if not isinstance(routes, list) or any(not isinstance(route, dict) for route in routes):
|
||||
return False
|
||||
routes_count += len(routes)
|
||||
if routes_count > MAX_LINKS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Job:
|
||||
topic: str
|
||||
job_id: str
|
||||
started_at: int
|
||||
started: float
|
||||
info: asyncio.Future[None]
|
||||
phase: str = "loading"
|
||||
stage: str = "connecting"
|
||||
finished: float | None = None
|
||||
error: str | None = None
|
||||
stale: bool = False
|
||||
result: dict[str, Any] | None = None
|
||||
obtained_at: int | None = None
|
||||
response_active: bool = False
|
||||
task: asyncio.Task[None] | None = None
|
||||
cleanups: list[Callable[[], None]] = field(default_factory=list)
|
||||
|
||||
|
||||
class ZigbeeScanCoordinator:
|
||||
"""One in-memory slot per topic, shared by all browsers of the HA entry."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
self.hass = hass
|
||||
self.entry = entry
|
||||
self.session_id = str(uuid4())
|
||||
self.revision = 0
|
||||
self.closed = False
|
||||
self._jobs: dict[str, _Job] = {}
|
||||
self._listeners: set[Callable[[dict[str, Any]], None]] = set()
|
||||
self._operations: set[asyncio.Task] = set()
|
||||
|
||||
def _active(self, job: _Job) -> bool:
|
||||
return not self.closed and self._jobs.get(job.topic) is job and job.phase == "loading"
|
||||
|
||||
def _state(self, job: _Job) -> dict[str, Any]:
|
||||
provider: dict[str, Any] = {
|
||||
"topic": job.topic, "job_id": job.job_id, "phase": job.phase,
|
||||
"stage": job.stage, "started_at": job.started_at,
|
||||
"elapsed_ms": max(0, int(((job.finished if job.finished is not None else monotonic()) - job.started) * 1000)),
|
||||
"cancel_after_ms": CANCEL_AFTER_MS,
|
||||
}
|
||||
if job.error is not None:
|
||||
provider["error"] = job.error
|
||||
if job.result is not None:
|
||||
provider["result"] = job.result
|
||||
provider["obtained_at"] = job.obtained_at
|
||||
provider["stale"] = job.stale
|
||||
return {"kind": "state", "session_id": self.session_id,
|
||||
"revision": self.revision, "provider": provider}
|
||||
|
||||
def snapshot(self, topic: str) -> dict[str, Any]:
|
||||
"""Current public state; no MQTT work, no timestamp reset."""
|
||||
if self.closed:
|
||||
raise ZigbeeScanError("not_ready")
|
||||
job = self._jobs.get(normalize_base_topic(topic))
|
||||
if job is None:
|
||||
raise ZigbeeScanError("invalid_data")
|
||||
return self._state(job)
|
||||
|
||||
def initial_events(self) -> list[dict[str, Any]]:
|
||||
"""Bound each event to one map; never aggregate eight maps into one frame."""
|
||||
return [{"kind": "reset", "session_id": self.session_id,
|
||||
"revision": self.revision, "topics": list(self._jobs)},
|
||||
*(self._state(job) for job in self._jobs.values())]
|
||||
|
||||
def add_listener(self, listener: Callable[[dict[str, Any]], None]) -> Callable[[], None]:
|
||||
if self.closed:
|
||||
raise ZigbeeScanError("not_ready")
|
||||
if len(self._listeners) >= MAX_LISTENERS:
|
||||
raise ZigbeeScanError("capacity_exceeded")
|
||||
self._listeners.add(listener)
|
||||
return lambda: self._listeners.discard(listener)
|
||||
|
||||
def _emit(self, event: dict[str, Any]) -> None:
|
||||
for listener in tuple(self._listeners):
|
||||
try:
|
||||
listener(event)
|
||||
except Exception: # noqa: BLE001 - one disconnected UI must not interrupt a shared job
|
||||
self._listeners.discard(listener)
|
||||
|
||||
def _changed(self, job: _Job) -> None:
|
||||
self.revision += 1
|
||||
self._emit(self._state(job))
|
||||
|
||||
def start(self, base_topic: str) -> dict[str, Any]:
|
||||
"""Reserve before creating any task: concurrent WS clients join one job."""
|
||||
if self.closed:
|
||||
raise ZigbeeScanError("not_ready")
|
||||
topic = normalize_base_topic(base_topic)
|
||||
previous = self._jobs.get(topic)
|
||||
if previous is not None and previous.phase == "loading":
|
||||
return self._state(previous)
|
||||
# A transport which acknowledges cancellation late still owns a bounded
|
||||
# slot until its cleanup returns; repeated failed starts cannot grow it.
|
||||
if len(self._operations) >= MAX_TOPICS:
|
||||
raise ZigbeeScanError("capacity_exceeded")
|
||||
if previous is None and len(self._jobs) >= MAX_TOPICS:
|
||||
candidate = next((item for item in self._jobs.values() if item.phase != "loading"), None)
|
||||
if candidate is None:
|
||||
raise ZigbeeScanError("capacity_exceeded")
|
||||
del self._jobs[candidate.topic]
|
||||
self.revision += 1
|
||||
self._emit({"kind": "removed", "session_id": self.session_id,
|
||||
"revision": self.revision, "topic": candidate.topic})
|
||||
job = _Job(topic, f"houseplan-{uuid4()}", int(time() * 1000), monotonic(),
|
||||
self.hass.loop.create_future(),
|
||||
stale=previous.stale if previous else False,
|
||||
result=previous.result if previous else None,
|
||||
obtained_at=previous.obtained_at if previous else None)
|
||||
self._jobs.pop(topic, None)
|
||||
self._jobs[topic] = job
|
||||
job.task = self.entry.async_create_background_task(
|
||||
self.hass, self._run(job), "House Plan Zigbee map", eager_start=False,
|
||||
)
|
||||
self._changed(job)
|
||||
return self._state(job)
|
||||
|
||||
def cancel(self, base_topic: str, job_id: str) -> dict[str, Any]:
|
||||
if self.closed:
|
||||
raise ZigbeeScanError("not_ready")
|
||||
topic = normalize_base_topic(base_topic)
|
||||
job = self._jobs.get(topic)
|
||||
if job is None or job.job_id != job_id:
|
||||
raise ZigbeeScanError("conflict")
|
||||
# A repeated cancellation or a success/cancel race never rolls back a final state.
|
||||
if job.phase != "loading":
|
||||
return self._state(job)
|
||||
if (monotonic() - job.started) * 1000 < CANCEL_AFTER_MS:
|
||||
raise ZigbeeScanError("conflict")
|
||||
self._finish(job, "cancelled")
|
||||
return self._state(job)
|
||||
|
||||
@staticmethod
|
||||
def _cleanup(cleanup: Callable[[], None]) -> None:
|
||||
try:
|
||||
cleanup()
|
||||
except Exception: # noqa: BLE001 - cleanup remains idempotent across unload and transport failure
|
||||
_LOGGER.debug("House Plan Zigbee transport cleanup failed")
|
||||
|
||||
def _add_cleanup(self, job: _Job, cleanup: Callable[[], None]) -> None:
|
||||
if self._active(job):
|
||||
job.cleanups.append(cleanup)
|
||||
else:
|
||||
self._cleanup(cleanup)
|
||||
|
||||
def _release(self, job: _Job) -> None:
|
||||
job.response_active = False
|
||||
cleanups, job.cleanups = job.cleanups, []
|
||||
for cleanup in cleanups:
|
||||
self._cleanup(cleanup)
|
||||
if not job.info.done():
|
||||
job.info.cancel()
|
||||
|
||||
def _finish(self, job: _Job, phase: str, error: str | None = None,
|
||||
result: dict[str, Any] | None = None) -> None:
|
||||
if not self._active(job):
|
||||
return
|
||||
job.phase, job.error, job.finished = phase, error, monotonic()
|
||||
if result is not None:
|
||||
job.result, job.obtained_at = result, int(time() * 1000)
|
||||
job.stale = False
|
||||
elif job.result is not None:
|
||||
job.stale = True
|
||||
self._release(job)
|
||||
# Including when called synchronously inside publish: a terminal provider
|
||||
# reply must not wait another ten seconds for the publish acknowledgement.
|
||||
if job.task is not None and not job.task.done():
|
||||
job.task.cancel()
|
||||
self._changed(job)
|
||||
|
||||
async def _operation(self, job: _Job, action: Callable[[], Awaitable],
|
||||
seconds: float, *, subscription: bool = False):
|
||||
"""Bound transport even if a cancelled subscribe later returns an unsubscribe."""
|
||||
async def perform():
|
||||
result = await action()
|
||||
if subscription and callable(result):
|
||||
self._add_cleanup(job, result)
|
||||
return result
|
||||
|
||||
task = self.entry.async_create_background_task(
|
||||
self.hass, perform(), "House Plan Zigbee transport", eager_start=False,
|
||||
)
|
||||
self._operations.add(task)
|
||||
|
||||
@callback
|
||||
def settled(done: asyncio.Task) -> None:
|
||||
self._operations.discard(done)
|
||||
if not done.cancelled():
|
||||
done.exception() # consume a late transport exception after the job ended
|
||||
|
||||
task.add_done_callback(settled)
|
||||
try:
|
||||
done, _pending = await asyncio.wait({task}, timeout=max(0, seconds))
|
||||
if not done:
|
||||
raise TimeoutError
|
||||
return task.result()
|
||||
finally:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
|
||||
async def _subscribe(self, job: _Job, topic: str, handler: Callable) -> None:
|
||||
deadline = self.hass.loop.time() + TRANSPORT_TIMEOUT
|
||||
await self._operation(job, lambda: mqtt.async_subscribe(
|
||||
self.hass, topic, handler, qos=0, encoding=None,
|
||||
), TRANSPORT_TIMEOUT, subscription=True)
|
||||
# Optional on the declared HA floor (2024.6). This acknowledges HA's
|
||||
# processing of subscriptions, not provider acceptance or radio progress.
|
||||
processed = getattr(mqtt, "async_on_subscribe_done", None)
|
||||
if callable(processed) and self._active(job):
|
||||
ready = self.hass.loop.create_future()
|
||||
|
||||
@callback
|
||||
def on_ready() -> None:
|
||||
if not ready.done():
|
||||
ready.set_result(None)
|
||||
|
||||
remove = processed(self.hass, topic, 0, on_ready)
|
||||
self._add_cleanup(job, remove)
|
||||
async with asyncio.timeout(max(0, deadline - self.hass.loop.time())):
|
||||
await ready
|
||||
|
||||
async def _run(self, job: _Job) -> None:
|
||||
try:
|
||||
if not mqtt.mqtt_config_entry_enabled(self.hass):
|
||||
self._finish(job, "error", "unsupported")
|
||||
return
|
||||
if not mqtt.is_connected(self.hass):
|
||||
self._finish(job, "error", "connection")
|
||||
return
|
||||
|
||||
@callback
|
||||
def connection_changed(connected: bool) -> None:
|
||||
if not connected:
|
||||
self._finish(job, "error", "connection")
|
||||
|
||||
self._add_cleanup(job, mqtt.async_subscribe_connection_status(
|
||||
self.hass, connection_changed,
|
||||
))
|
||||
response_topic = f"{job.topic}/bridge/response/networkmap"
|
||||
info_topic = f"{job.topic}/bridge/info"
|
||||
|
||||
@callback
|
||||
def receive_response(message) -> None:
|
||||
if (not self._active(job) or not job.response_active
|
||||
or message.topic != response_topic or message.retain):
|
||||
return
|
||||
value = _parse_payload(message.payload)
|
||||
if value is None:
|
||||
return
|
||||
data = value.get("data")
|
||||
transaction = value.get("transaction", data.get("transaction") if isinstance(data, dict) else None)
|
||||
if transaction != job.job_id:
|
||||
return
|
||||
if value.get("status") not in (None, "ok"):
|
||||
self._finish(job, "error", "provider")
|
||||
elif not _valid_map(value):
|
||||
self._finish(job, "error", "invalid_payload")
|
||||
else:
|
||||
self._finish(job, "ready", result=value)
|
||||
|
||||
@callback
|
||||
def receive_info(message) -> None:
|
||||
if (self._active(job) and message.topic == info_topic and message.retain
|
||||
and not job.info.done() and _parse_payload(message.payload) is not None):
|
||||
job.info.set_result(None)
|
||||
|
||||
# Response first also improves the old-HA retained-info preflight;
|
||||
# retained info alone is not claimed to be a strict broker SUBACK.
|
||||
await self._subscribe(job, response_topic, receive_response)
|
||||
await self._subscribe(job, info_topic, receive_info)
|
||||
async with asyncio.timeout(INFO_TIMEOUT):
|
||||
await job.info
|
||||
if not self._active(job):
|
||||
return
|
||||
job.response_active = True
|
||||
await self._operation(job, lambda: mqtt.async_publish(
|
||||
self.hass, f"{job.topic}/bridge/request/networkmap",
|
||||
json.dumps({"type": "raw", "routes": True, "transaction": job.job_id}),
|
||||
qos=0, retain=False,
|
||||
), TRANSPORT_TIMEOUT)
|
||||
if self._active(job):
|
||||
job.stage = "waiting"
|
||||
self._changed(job)
|
||||
# Only MQTT, explicit cancel, transport loss or entry teardown
|
||||
# ends this wait. 600 seconds is a cancel threshold, not a deadline.
|
||||
await self.hass.loop.create_future()
|
||||
except asyncio.CancelledError:
|
||||
if self._active(job):
|
||||
self._finish(job, "error", "connection")
|
||||
raise
|
||||
except TimeoutError:
|
||||
self._finish(job, "error", "timeout")
|
||||
except Exception: # noqa: BLE001 - contain optional MQTT errors without logging network payloads
|
||||
self._finish(job, "error", "connection")
|
||||
finally:
|
||||
self._release(job)
|
||||
|
||||
async def async_close(self) -> None:
|
||||
"""Stop entry-owned work, clear volatile maps, never restart a radio scan."""
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
# Reload does not disconnect HA WebSockets. Tell their observers that
|
||||
# this session and its volatile cache are gone before dropping them.
|
||||
self.revision += 1
|
||||
self._emit({"kind": "closed", "session_id": self.session_id,
|
||||
"revision": self.revision})
|
||||
tasks = [job.task for job in self._jobs.values() if job.task is not None]
|
||||
for job in self._jobs.values():
|
||||
self._release(job)
|
||||
for task in (*tasks, *self._operations):
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
self._jobs.clear()
|
||||
self._listeners.clear()
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Admin-only WebSocket observers and explicit actions for background Z2M jobs."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .store import get_data
|
||||
from .zigbee_topology import ZigbeeScanCoordinator, ZigbeeScanError
|
||||
|
||||
|
||||
def _coordinator(hass: HomeAssistant, connection, msg_id: int) -> ZigbeeScanCoordinator | None:
|
||||
runtime = get_data(hass)
|
||||
coordinator = getattr(runtime, "zigbee_coordinator", None)
|
||||
if not isinstance(coordinator, ZigbeeScanCoordinator) or coordinator.closed:
|
||||
connection.send_error(msg_id, "not_ready", "not_ready")
|
||||
return None
|
||||
return coordinator
|
||||
|
||||
|
||||
@callback
|
||||
def async_register(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, ws_zigbee_subscribe)
|
||||
websocket_api.async_register_command(hass, ws_zigbee_start)
|
||||
websocket_api.async_register_command(hass, ws_zigbee_cancel)
|
||||
|
||||
|
||||
@websocket_api.websocket_command({vol.Required("type"): "houseplan/zigbee/subscribe"})
|
||||
@websocket_api.require_admin
|
||||
def ws_zigbee_subscribe(hass: HomeAssistant, connection, msg: dict[str, Any]) -> None:
|
||||
coordinator = _coordinator(hass, connection, msg["id"])
|
||||
if coordinator is None:
|
||||
return
|
||||
|
||||
@callback
|
||||
def publish(event: dict[str, Any]) -> None:
|
||||
# Permission can be revoked during a long-lived browser session.
|
||||
if connection.user is None or not connection.user.is_admin:
|
||||
remove()
|
||||
connection.subscriptions.pop(msg["id"], None)
|
||||
return
|
||||
connection.send_event(msg["id"], event)
|
||||
|
||||
try:
|
||||
remove = coordinator.add_listener(publish)
|
||||
except ZigbeeScanError as err:
|
||||
connection.send_error(msg["id"], err.code, err.code)
|
||||
return
|
||||
# HA calls this on unsubscribe/disconnect. It never cancels the shared job.
|
||||
connection.subscriptions[msg["id"]] = remove
|
||||
connection.send_result(msg["id"], {"session_id": coordinator.session_id,
|
||||
"revision": coordinator.revision})
|
||||
for event in coordinator.initial_events():
|
||||
publish(event)
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
vol.Required("type"): "houseplan/zigbee/start",
|
||||
vol.Required("base_topic"): vol.All(str, vol.Length(min=1, max=1024)),
|
||||
})
|
||||
@websocket_api.require_admin
|
||||
def ws_zigbee_start(hass: HomeAssistant, connection, msg: dict[str, Any]) -> None:
|
||||
coordinator = _coordinator(hass, connection, msg["id"])
|
||||
if coordinator is None:
|
||||
return
|
||||
try:
|
||||
result = coordinator.start(msg["base_topic"])
|
||||
except ZigbeeScanError as err:
|
||||
connection.send_error(msg["id"], err.code, err.code)
|
||||
return
|
||||
connection.send_result(msg["id"], result)
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
vol.Required("type"): "houseplan/zigbee/cancel",
|
||||
vol.Required("base_topic"): vol.All(str, vol.Length(min=1, max=1024)),
|
||||
vol.Required("job_id"): vol.All(str, vol.Length(min=1, max=80)),
|
||||
})
|
||||
@websocket_api.require_admin
|
||||
def ws_zigbee_cancel(hass: HomeAssistant, connection, msg: dict[str, Any]) -> None:
|
||||
coordinator = _coordinator(hass, connection, msg["id"])
|
||||
if coordinator is None:
|
||||
return
|
||||
try:
|
||||
result = coordinator.cancel(msg["base_topic"], msg["job_id"])
|
||||
except ZigbeeScanError as err:
|
||||
connection.send_error(msg["id"], err.code, err.code)
|
||||
return
|
||||
connection.send_result(msg["id"], result)
|
||||
@@ -21,6 +21,10 @@
|
||||
* profile's camera scenario, benchmark_large_house.mjs) and the longest Long
|
||||
* Task of the camera — the scenario and the 100-step series, retained heap
|
||||
* after 20 A→B→C→A cycles over a warm cycle (same GC protocol).
|
||||
* #789 additionally observes cameraFullCycleLongTaskMaxMs continuously through
|
||||
* full-quality restoration, immediate restart and a 500 ms tail, with the same
|
||||
* camera ceiling. Historical camera windows remain unchanged; each row retains
|
||||
* raw full-cycle Long Tasks and phase boundaries for audit.
|
||||
*
|
||||
* Counters (exact, every sample): geometry/visibility recomputes over 100
|
||||
* unrelated HA ticks, 100 pan/zoom steps and a colour-only change (0); the
|
||||
@@ -44,6 +48,7 @@ import { dirname, resolve } from 'node:path';
|
||||
import { launch } from './serve.mjs';
|
||||
import { makeLedStripsFixture } from './performance/led-strips-fixture.mjs';
|
||||
import { ledChunkRequestName } from './performance/led-chunk-request.mjs';
|
||||
import { cameraCycleFailures, cameraCycleSampleFailures, finishLedCameraCycle } from './performance/led-camera-cycle.mjs';
|
||||
|
||||
const valueArg = (name) => process.argv.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
|
||||
const warmupOnly = process.argv.includes('--warmup-only');
|
||||
@@ -118,6 +123,7 @@ function installLedLifecycleTracker() {
|
||||
/** The page side of one mount: the same fixture host as before, shared by the sample and the late-import run. */
|
||||
function pageHost() {
|
||||
return `
|
||||
window.__finishLedCameraCycle = ${finishLedCameraCycle.toString()};
|
||||
window.__ledHost = (fixture) => {
|
||||
const card = document.createElement('houseplan-card');
|
||||
card.setConfig({ type: 'custom:houseplan-card', title: 'LED strips', icon_size: 3.4 });
|
||||
@@ -182,7 +188,7 @@ async function sample() {
|
||||
await sleep(5);
|
||||
}
|
||||
};
|
||||
const longTasks = () => {
|
||||
const longTasks = (onEntries) => {
|
||||
const entries = [];
|
||||
const observer = new PerformanceObserver((list) => entries.push(...list.getEntries()));
|
||||
observer.observe({ type: 'longtask', buffered: false });
|
||||
@@ -190,6 +196,7 @@ async function sample() {
|
||||
await sleep(0);
|
||||
entries.push(...observer.takeRecords());
|
||||
observer.disconnect();
|
||||
onEntries?.(entries.map(({ startTime, duration }) => ({ startTime, duration })));
|
||||
return Number(Math.max(0, ...entries.map((entry) => entry.duration)).toFixed(2));
|
||||
};
|
||||
};
|
||||
@@ -250,6 +257,10 @@ async function sample() {
|
||||
const wheel = (deltaY) => stage.dispatchEvent(new WheelEvent('wheel', {
|
||||
deltaY, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, bubbles: true, cancelable: true,
|
||||
}));
|
||||
// Keep the old pan/series windows intact. This separate observer also
|
||||
// covers the first quality switch, both restores and the quick restart.
|
||||
const cameraFullCycle = { startTime: performance.now(), entries: [], phases: {} };
|
||||
const stopFullCycle = longTasks(entries => { cameraFullCycle.entries = entries; });
|
||||
const stopPan = longTasks();
|
||||
const panStarted = performance.now();
|
||||
wheel(-120);
|
||||
@@ -266,6 +277,14 @@ async function sample() {
|
||||
}
|
||||
await frame();
|
||||
const cameraSeriesLongTaskMaxMs = await stopSeries();
|
||||
cameraFullCycle.phases.seriesEnd = performance.now();
|
||||
Object.assign(cameraFullCycle.phases, await window.__finishLedCameraCycle({
|
||||
fullQuality: () => !card.hasAttribute('data-led-zoom-quality'),
|
||||
input: async () => { wheel(-60); await card.updateComplete; },
|
||||
frame, now: () => performance.now(), sleep,
|
||||
}));
|
||||
const cameraFullCycleLongTaskMaxMs = await stopFullCycle();
|
||||
cameraFullCycle.endTime = performance.now();
|
||||
const afterCamera = await stats();
|
||||
// A colour-only change rebuilds no geometry or visibility.
|
||||
host.setStates({ ...host.states, ...Object.fromEntries(floorLights.map((id) => [id,
|
||||
@@ -301,6 +320,7 @@ async function sample() {
|
||||
const key = (entry) => `${entry.shapes}/${entry.visibility}/${entry.sources}/${entry.visibilityPaths}/${entry.pathChars}`;
|
||||
return {
|
||||
firstStableRenderMs, warmSpaceReadyMs, stateUpdateMs, panZoomMs, panZoomLongTaskMaxMs, cameraSeriesLongTaskMaxMs,
|
||||
cameraFullCycleLongTaskMaxMs, cameraFullCycle,
|
||||
retainedHeapBytes: heapBefore == null || heapAfter == null ? null : Math.max(0, heapAfter - heapBefore),
|
||||
counters: strips ? {
|
||||
recomputesOnHaTicks: afterTicks.recomputes - before.recomputes,
|
||||
@@ -410,7 +430,7 @@ const metric = (name) => {
|
||||
return { median: values.length ? at(0.5) : null, p95: values.length ? at(0.95) : null, samples: values };
|
||||
};
|
||||
const METRICS = ['firstStableRenderMs', 'warmSpaceReadyMs', 'stateUpdateMs', 'panZoomMs', 'panZoomLongTaskMaxMs',
|
||||
'cameraSeriesLongTaskMaxMs', 'retainedHeapBytes'];
|
||||
'cameraSeriesLongTaskMaxMs', 'cameraFullCycleLongTaskMaxMs', 'retainedHeapBytes'];
|
||||
const report = {
|
||||
profile: PROFILE, size, strips: STRIPS, points: POINTS, sourceSha, chromium,
|
||||
samples: rows.length, warmups: discarded, cycles: CYCLES, viewport: { width: 1440, height: 1000 }, dpr: 1,
|
||||
@@ -429,6 +449,8 @@ if (!partial && (report.samples < MIN_SAMPLES || report.warmups < 1)) {
|
||||
failures.push(`${report.samples} samples after ${report.warmups} warmups: the profile needs ≥ ${MIN_SAMPLES} after ≥ 1`);
|
||||
}
|
||||
const limits = budgets.sizes[size] || {};
|
||||
failures.push(...cameraCycleSampleFailures(rows));
|
||||
failures.push(...cameraCycleFailures(report.metrics.cameraFullCycleLongTaskMaxMs, limits.cameraSeriesLongTaskMaxMs));
|
||||
for (const [name, limit] of Object.entries(limits)) {
|
||||
for (const stat of ['median', 'p95']) {
|
||||
const value = report.metrics[name]?.[stat];
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/** #54: bounded provider normalization and incident-only hover performance. */
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { mapTopologies, normalizeZhaTopology, resolveMappedTopologyHover } from '../test-build/zigbee-topology.js';
|
||||
import { mapTopologies, normalizeZhaTopology, resolveMappedTopologyHover, resolveProviderUplinks } from '../test-build/zigbee-topology.js';
|
||||
|
||||
const NODES = 500;
|
||||
const SAMPLES = 9;
|
||||
const BUDGETS = { normalizeMs: 80, mapMs: 160, firstHoverMs: 180, repeatedHoverMs: 120 };
|
||||
const hex = (index) => index.toString(16).padStart(16, '0');
|
||||
const rows = Array.from({ length: NODES }, (_, index) => ({
|
||||
ieee: hex(index + 1), device_reg_id: `d${index}`, device_type: index ? 'Router' : 'Coordinator',
|
||||
ieee: hex(index + 1), nwk: index, device_reg_id: `d${index}`, device_type: index ? 'Router' : 'Coordinator',
|
||||
routes: index ? [{ dest_nwk: '0x0000', next_hop: index - 1, route_status: 'Active' }] : [],
|
||||
neighbors: [1, 2, 3, 4, 5, 6].flatMap((offset) => {
|
||||
const other = index + offset;
|
||||
return other < NODES ? [{ ieee: hex(other + 1), lqi: (index * 17 + offset * 13) % 256 }] : [];
|
||||
@@ -39,7 +40,13 @@ const firstHoverMs = measured(() => resolveMappedTopologyHover(mapped, 'main', '
|
||||
const repeatedHoverMs = measured(() => {
|
||||
for (let i = 1; i <= 20; i++) resolveMappedTopologyHover(mapped, 'main', `m${i}`);
|
||||
});
|
||||
const result = { nodes: topology.nodes.length, links: topology.links.length,
|
||||
const confirmedRoutes = [...resolveProviderUplinks(topology).values()].filter((route) => route.kind === 'known').length;
|
||||
const witness = resolveMappedTopologyHover(mapped, 'main', 'm1');
|
||||
if (confirmedRoutes !== NODES - 1 || witness.outgoing !== 'known'
|
||||
|| witness.parentTargets.length !== 1 || witness.lines.length !== 1) {
|
||||
throw new Error('benchmark must exercise a nonempty provider-confirmed graph, outgoing remote target and incoming edge');
|
||||
}
|
||||
const result = { nodes: topology.nodes.length, links: topology.links.length, confirmedRoutes,
|
||||
normalizeMs, mapMs, firstHoverMs, repeatedHoverMs };
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
for (const [name, budget] of Object.entries(BUDGETS)) {
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
{
|
||||
"schema": 3,
|
||||
"matrixVersion": 71,
|
||||
"acceptedAt": "2026-10-03T15:57:17.589Z",
|
||||
"sourceFingerprint": "6cd33bbad3c9a6a101afc1d501a8959df6ad58b693802489873660c043677fa7",
|
||||
"matrixVersion": 73,
|
||||
"acceptedAt": "2026-10-06T17:21:15.454Z",
|
||||
"sourceFingerprint": "542656cfacffc36a77b28cf329140d08b6f9bff447c092d54689c300d606f75d",
|
||||
"chromium": "151.0.7922.34",
|
||||
"capturedOn": "linux",
|
||||
"acceptedOn": "win32",
|
||||
"acceptedOn": "linux",
|
||||
"capture": {
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"chromium": "151.0.7922.34",
|
||||
"buildFingerprint": "6cd33bbad3c9a6a101afc1d501a8959df6ad58b693802489873660c043677fa7",
|
||||
"ci": {
|
||||
"buildFingerprint": "542656cfacffc36a77b28cf329140d08b6f9bff447c092d54689c300d606f75d",
|
||||
"ci": null
|
||||
},
|
||||
"localAttestation": {
|
||||
"schema": "houseplan-golden-wsl/v1",
|
||||
"sha256": "d5e785e772731545943419b719df10f952592b82bbfb65db61a6727892d8b61c",
|
||||
"artifactSha256": "ebce2dff59e2fca809ea4d8a0efbcb3663aeed57bf38f4f821387228f6eba09a",
|
||||
"createdAt": "2026-10-06T17:20:50.497Z",
|
||||
"source": {
|
||||
"repository": "Matysh/houseplan-card",
|
||||
"run": 37134548622,
|
||||
"attempt": 1,
|
||||
"sha": "c142e4f7b5b3ffb46d3a2b88222a9d697e8144b1"
|
||||
"branch": "issue/806-battery-shadow-optout",
|
||||
"commit": "3bfd3f416192892e768534c83b4117f8615a7042",
|
||||
"tree": "a3959db430b8e57704fed4da397d6fe99fec4674",
|
||||
"remoteSha": "3bfd3f416192892e768534c83b4117f8615a7042",
|
||||
"clean": true,
|
||||
"status": ""
|
||||
},
|
||||
"environment": {
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"kernel": "6.18.33.2-microsoft-standard-WSL2",
|
||||
"wsl": true,
|
||||
"distro": "Ubuntu",
|
||||
"filesystem": "ext2/ext3"
|
||||
},
|
||||
"toolchain": {
|
||||
"node": "22.23.2",
|
||||
"npm": "10.9.8",
|
||||
"playwright": "1.62.0",
|
||||
"chromiumExecutableSha256": "0b20b130e7edd9dd51873be867761295fe0cfad490c2b9a64f95bd3cfc08fa71"
|
||||
}
|
||||
},
|
||||
"localAttestation": null,
|
||||
"foreignCapture": {
|
||||
"reason": "Owner-authorized #788 acceptance on Windows of Linux CI artifact Validate 37134548622; capture is not Windows (#455)"
|
||||
},
|
||||
"foreignCapture": null,
|
||||
"witnesses": {
|
||||
"count": 101,
|
||||
"count": 110,
|
||||
"floor": 10
|
||||
},
|
||||
"scenarios": {
|
||||
@@ -142,6 +163,11 @@
|
||||
"openings-hidden-view-dark": "209a6a52993a6840d85126f59f408fac68dcb9b7117e489161de96d63c834fae",
|
||||
"lighting-glow-sun-dark": "9021fa10f6606d2307f75f8024473be3e2d6e94d1c48ab149551c2bbdf129578",
|
||||
"device-value-badge-positions-dark": "f4ae3548d65e8fd0377b7c2b446065d922473c40f85537a8ce99d3219978223c",
|
||||
"device-battery-board-light": "ca8d98e6fa4f634c920f4d184277cfee2133b1b906b3cfb78e160a89ab4d5c12",
|
||||
"device-battery-board-dark": "d0f4d338a34eef50965c89716df285344bcfb6a60a078372d3a8b1bb9855d733",
|
||||
"device-battery-board-medium": "25d924e54d0d0c09569678560c00869ac3a39dfc38c59910adc86ad221e8b2e7",
|
||||
"device-battery-mobile-dark": "f968127a092cd5081a18864ce883f791e398136b5cbc0fcd319eab6a855c7cc8",
|
||||
"device-battery-zigbee-overlap-dark": "69e6a6da65f3aa144d9e4ec56f9d97820a6926e0fff58faa598d9ca0bff8420a",
|
||||
"device-value-face-cover-source-dark": "18aa90881fe7794c98c52142b73fb1db33bf57d390c24b6286b7b7db6312bcfa",
|
||||
"device-icon-state-table-light": "0e426d536bd89354d51bd90ef62c08bdf8d36f27132d5f1f22b64c79850266df",
|
||||
"device-icon-state-table-dark": "c993ee850b38af1325f1290d45e516a19c4bc6c3e4db864cdc1cc61276ce6d18",
|
||||
@@ -199,7 +225,7 @@
|
||||
"device-help-popover-light-ru": "7645a419f8214dacc6b97be894643d12b2a0486342e568b0b3ee42d74b8cf878",
|
||||
"decor-color-popover-mobile-ru": "bdf90e0e265d41d032db948a737eeedd6e60cdacc9f85c9356aa809894654960",
|
||||
"decor-color-popover-desktop-en": "46b38745da10d4467b510c5a257102395a797e0d3229e7ce8c89e4bbf3b937e7",
|
||||
"general-color-popover-desktop-en": "b54ef50909c2e7b667d321457b17205d5de38dd39b219eef05bee5ef8fb6bef2",
|
||||
"general-color-popover-desktop-en": "8ffdaaff5c859539985cf77304ee0acd4044d8b13eae0d80ea9b5ce1ef9f346f",
|
||||
"settings-help-zoom-200-en-light": "5539d8df2691eee0b8e90fa1fe086380870eeca5d945a34bb71e65f18bb3c22f",
|
||||
"settings-help-zoom-200-ru-dark": "7be1e0c156ae3a8f71d6b55fc75e5a20e5a4404efee674fa4bebdb82004daa80",
|
||||
"support-desktop-empty-light-en": "55ffd7a63b8e4d65b8cb695a7b166e0c4bb2e719ce925b8d28b8e8595897750d",
|
||||
@@ -208,7 +234,7 @@
|
||||
"support-phone-success-dark-en": "dcf2cb53e904dd3196052f5e0001b8aee712db9c9c0e4f8cd2982645e4bf7dbd",
|
||||
"support-relay-error-light-en": "adf30f96a926a1730c9ad34d18b4234d9b19140f11d56c8fb7508976f366c11d",
|
||||
"support-tablet-preview-dark-ru": "a69ba86bf069d9beba94eedf6cc3b385b026caeee66348e4ef42c1a072b6df6a",
|
||||
"device-ripple-color-popover-mobile-ru": "8fc31025d32d87e5d3d3df896937d182577cd5169d520e7cf5fc267e7d080776",
|
||||
"device-ripple-color-popover-mobile-ru": "3f74934eac123e25ef0e9aed18b470e06271f250e53a26cac15d8882fa260f43",
|
||||
"space-room-color-popover-desktop-ru": "0e4654978e004cb9242ccf4add8022b57459b6a8524f141160ae49aa46787fe9",
|
||||
"backup-full-preview-desktop-en": "2a71dd52bce193a5ddca1ee0404f91754787206cbe01af0b0bfb3871016ed289",
|
||||
"backup-plan-only-export-desktop-en": "d7ae8031e1147ee8d3d15f35d8cd5bc65aa24fc880ad2feafdd7c8713b76c0c5",
|
||||
@@ -220,12 +246,12 @@
|
||||
"card-editor-invalid-default-floor-light-en": "dbf251ece014cfee252e0dcb2d7c00eda66aa4b5bd125e4c83e248d4319d24b3",
|
||||
"card-editor-invalid-default-floor-dark-ru": "422b74bb391b87e383350247ef1bc1f10ca48c8f6fe418f75bde94e62e3b8bc0",
|
||||
"led-strip-design-reference-off-light": "1a558111dbeb1962b10f47baad20b4689248a347510261152f0d6196906044fd",
|
||||
"led-strip-design-reference-on-light": "16bfb46f2a90c57e95f1307e375e04e5809d41ae677e3b1e1eb56d7e0bc1d8ec",
|
||||
"lighting-led-strip-glow-dark": "254fe1e39c5bb7bbb7e67527f69c767c8bdb6ed959986a8085b18adb152001d6",
|
||||
"led-strip-long-zigzag-glow-light": "cd0847c03b6b397c93fa923dcd06b30d7d4a8f6c6db2f006b9f3e20de0e66e91",
|
||||
"led-strip-endcaps-zoom-light": "57703b53bd9d2eaae1b3142903a8b574e5d12b761b9b7240543ee2463c44c74d",
|
||||
"led-strip-rectangle-door-zoom-light": "549e9e9110cc3b01537d07bcf6ab89896069a49183a27c22904b89c99e35347c",
|
||||
"led-strip-design-reference-on-light": "1d6b4c2cfac2e1aeb9ce17ae2b3ddb62707e3f46c2ad3967b3669712a8e577fd",
|
||||
"lighting-led-strip-glow-dark": "e6cdec5a16fde53bd1eca255eff554a5923f710d2b6e24904e46e39d8e5ab4ab",
|
||||
"led-strip-long-zigzag-glow-light": "342749c29bac93a9a53f58921a40a10731fbcfdfff4345e71af81ce4de4dfa61",
|
||||
"led-strip-endcaps-zoom-light": "468293d57b96b4b17f61027467c7c43b25d5a0f91972ba0451998cdc61ea3980",
|
||||
"led-strip-rectangle-door-zoom-light": "00e8d937924bff957ef413f5d8b7c6964e44c96e90b9209ea24158217328b2ac",
|
||||
"led-strip-off-light": "cbb4f4794c871d592917653e8b5713d87a6be6dfe470b2161da760c2bb3cebdc",
|
||||
"iso-led-strip-dark": "572728e34f77f911f0ece247c5b7de6653e9c0dfafe04a0ab198b88644081a58"
|
||||
"iso-led-strip-dark": "6605a62b15a47da09920743793b5c7f8cb2768610b6d57b739fa91eeef883676"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,390 @@
|
||||
/** #792: synthetic, data-driven reference boards; no renderer/CSS substitution. */
|
||||
const STATES = ['normal', 'warning', 'low', 'unknown'];
|
||||
const VALUES = { normal: '80', warning: '40', low: '10', unknown: 'unavailable' };
|
||||
const COLORS = { normal: [29, 194, 29], warning: [240, 160, 12], low: [240, 65, 12], unknown: [112, 119, 129] };
|
||||
const SPACE = 'golden-battery';
|
||||
// Independent visual oracle for the owner's four-state MDI choice.
|
||||
export const BATTERY_BOARD_ICONS = Object.freeze({
|
||||
normal: 'mdi:battery', warning: 'mdi:battery-30',
|
||||
low: 'mdi:battery-outline', unknown: 'mdi:battery-unknown',
|
||||
});
|
||||
|
||||
export function batteryBoardSamples(kind) {
|
||||
if (!['desktop', 'mobile'].includes(kind)) throw new Error(`unknown battery board: ${kind}`);
|
||||
if (kind === 'mobile') return [
|
||||
['normal', 'none'], ['warning', 'right'], ['low', 'left'], ['unknown', 'top'],
|
||||
['normal', 'bottom'], ['warning', 'text'], ['low', 'binary'], ['unknown', 'binary'],
|
||||
].map(([state, arrangement], index) => ({
|
||||
id: `battery-mobile-${index}`, state, arrangement, diameter: index < 6 ? 32 : 56,
|
||||
x: index % 2 ? 0.66 : 0.22, y: 0.16 + Math.floor(index / 2) * 0.22,
|
||||
}));
|
||||
return [
|
||||
...[32, 56, 96].flatMap((diameter, row) => STATES.map((state, column) => ({
|
||||
id: `battery-${diameter}-${state}`, state, diameter, arrangement: 'none',
|
||||
x: 0.14 + column * 0.23, y: 0.12 + row * 0.19,
|
||||
}))),
|
||||
...['none', 'right', 'left', 'top', 'bottom'].map((arrangement, index) => ({
|
||||
id: `battery-layout-${arrangement}`, state: STATES[index % 4], diameter: 56, arrangement,
|
||||
// Opposite horizontal capsules extend toward each other. Leave room for
|
||||
// both complete shells plus the first capsule's independent battery.
|
||||
x: [0.08, 0.22, 0.56, 0.73, 0.91][index], y: 0.70,
|
||||
})),
|
||||
...['text', 'legacy', 'binary', 'binary'].map((arrangement, index) => ({
|
||||
id: `battery-extra-${index}`, state: STATES[index], diameter: 56, arrangement,
|
||||
x: 0.13 + index * 0.23, y: 0.88,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
export function makeBatteryBoardFixture(kind) {
|
||||
const fixture = {
|
||||
config: {
|
||||
model_version: 7,
|
||||
spaces: [{ id: SPACE, title: 'Battery reference', plan_url: null, view_box: [0, 0, 1, 1],
|
||||
cell_cm: 5, rooms: [{ id: 'battery-room', name: '', area: 'battery_area',
|
||||
poly: [[0, 0], [1, 0], [1, 1], [0, 1]] }],
|
||||
walls: [], wall_segments: [], partitions: [], openings: [], wall_columns: [], decor: [],
|
||||
settings: { fill_mode: 'none', show_names: false, show_borders: false, glow_enabled: false },
|
||||
}],
|
||||
markers: [], settings: { bg_mode: 'static', sun_rays: false, marker_area_snapshot: {} },
|
||||
},
|
||||
devices: {}, entities: {}, states: {}, layout: {},
|
||||
areas: { battery_area: { area_id: 'battery_area', name: 'Battery reference' } },
|
||||
};
|
||||
const addEntity = (id, entityId, state, attributes, diagnostic = false) => {
|
||||
fixture.entities[entityId] = { entity_id: entityId, device_id: id, platform: 'houseplan_golden',
|
||||
config_entry_id: 'golden_entry', disabled_by: null,
|
||||
original_device_class: attributes.device_class,
|
||||
...(diagnostic ? { entity_category: 'diagnostic' } : {}),
|
||||
};
|
||||
fixture.states[entityId] = { entity_id: entityId, state, attributes };
|
||||
};
|
||||
for (const sample of batteryBoardSamples(kind)) {
|
||||
const { id, diameter, arrangement, state } = sample;
|
||||
const stem = id.replaceAll('-', '_');
|
||||
const valueId = `sensor.${stem}_measurement`;
|
||||
const batteryId = `${arrangement === 'binary' ? 'binary_sensor' : 'sensor'}.${stem}_battery`;
|
||||
fixture.devices[id] = { id, name: id, model: 'GOLDEN-BATTERY', area_id: 'battery_area',
|
||||
identifiers: [['houseplan_golden', id]], config_entries: ['golden_entry'], disabled_by: null,
|
||||
};
|
||||
addEntity(id, valueId, arrangement === 'text' ? '498' : '21.5', {
|
||||
device_class: arrangement === 'legacy' ? 'humidity' : 'temperature',
|
||||
unit_of_measurement: arrangement === 'text' ? 'ppm' : arrangement === 'legacy' ? '%' : '°C',
|
||||
});
|
||||
addEntity(id, batteryId, arrangement === 'binary'
|
||||
? state === 'low' ? 'on' : state === 'normal' ? 'off' : 'unavailable'
|
||||
: VALUES[state], { device_class: 'battery', ...(arrangement === 'binary' ? {} : { unit_of_measurement: '%' }) }, true);
|
||||
const marker = { id, binding: `device:${id}`, space: SPACE, size: diameter / 16,
|
||||
icon: 'mdi:thermometer', value_badge: { enabled: false },
|
||||
};
|
||||
if (['right', 'left', 'top', 'bottom'].includes(arrangement)) marker.value_badge = {
|
||||
enabled: true, position: arrangement, source: { kind: 'entity_state', entity_id: valueId },
|
||||
};
|
||||
if (arrangement === 'text') {
|
||||
marker.display = 'value';
|
||||
marker.value_source = { kind: 'entity_state', entity_id: valueId };
|
||||
}
|
||||
if (arrangement === 'legacy') {
|
||||
// A humidity-led legacy marker with a diagnostic climate temperature:
|
||||
// the preserved second metric widens the same shell as the primary badge.
|
||||
marker.icon = 'mdi:water-percent';
|
||||
marker.use_climate_temp = true;
|
||||
delete marker.value_badge;
|
||||
addEntity(id, `climate.${stem}_diagnostic`, 'idle', { current_temperature: 22.4 }, true);
|
||||
}
|
||||
fixture.config.markers.push(marker);
|
||||
fixture.config.settings.marker_area_snapshot[id] = { binding: marker.binding, area: 'battery_area' };
|
||||
fixture.layout[id] = { s: SPACE, x: sample.x, y: sample.y };
|
||||
}
|
||||
return fixture;
|
||||
}
|
||||
|
||||
/** Calibrate the public icon-size option once; never force a face's CSS size. */
|
||||
export async function prepareBatteryBoard(page, scenario) {
|
||||
return page.evaluate(async ({ samples, scenario, colors, icons }) => {
|
||||
const card = window.__goldenCard;
|
||||
const frame = () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
|
||||
const marker = (id) => card.renderRoot.querySelector(`[data-hp="device"][data-id="${CSS.escape(id)}"]`);
|
||||
const reference = samples[0];
|
||||
const initial = marker(reference.id)?.querySelector('.device-shell-frame')?.getBoundingClientRect().width;
|
||||
if (!(initial > 0)) throw new Error('battery golden calibration has no round shell');
|
||||
const iconSize = (scenario.iconSize || 3.4) * reference.diameter / initial;
|
||||
if (!(iconSize > 0 && iconSize <= 8)) throw new Error(`battery golden invalid icon size ${iconSize}`);
|
||||
card.setConfig({ type: 'custom:houseplan-card', title: `Golden ${scenario.id}`,
|
||||
icon_size: iconSize, language: scenario.language || 'en', show_temperature: true, show_signal: false,
|
||||
});
|
||||
await card.updateComplete;
|
||||
await frame();
|
||||
const stage = card.renderRoot.querySelector('.stage').getBoundingClientRect();
|
||||
const near = (actual, expected, detail) => {
|
||||
if (!Number.isFinite(actual) || Math.abs(actual - expected) > 0.5)
|
||||
throw new Error(`battery golden ${detail}: ${actual} != ${expected}`);
|
||||
};
|
||||
const loadedIcons = new Set();
|
||||
const measured = [];
|
||||
for (const sample of samples) {
|
||||
const device = marker(sample.id);
|
||||
const shell = device?.querySelector('.device-shell-frame');
|
||||
const battery = device?.querySelector('.device-battery');
|
||||
if (!shell || !battery) throw new Error(`battery golden missing face: ${sample.id}`);
|
||||
const iconNodes = battery.querySelectorAll('ha-icon.device-battery-icon');
|
||||
if (iconNodes.length !== 1) throw new Error(`battery golden icon count: ${sample.id}`);
|
||||
const icon = iconNodes[0];
|
||||
await icon.updateComplete;
|
||||
const expectedIcon = icons[sample.state];
|
||||
if (icon.icon !== expectedIcon) throw new Error(`battery golden wrong MDI: ${sample.id}`);
|
||||
const artwork = icon.shadowRoot?.querySelector('svg');
|
||||
const paths = artwork?.querySelectorAll('path');
|
||||
const expectedPath = window.__ICONS?.[expectedIcon];
|
||||
if (!artwork || artwork.getAttribute('viewBox') !== '0 0 24 24'
|
||||
|| paths.length !== 1 || typeof expectedPath !== 'string' || !expectedPath
|
||||
|| paths[0].getAttribute('d') !== expectedPath)
|
||||
throw new Error(`battery golden missing MDI path: ${sample.id}`);
|
||||
const inkBounds = paths[0].getBBox();
|
||||
if (!(inkBounds.width > 0 && inkBounds.height > 0))
|
||||
throw new Error(`battery golden empty MDI geometry: ${sample.id}`);
|
||||
loadedIcons.add(expectedIcon);
|
||||
const expectedFrame = { 32: 19, 56: 33, 96: 56 }[sample.diameter];
|
||||
const expectedGap = { 32: 2, 56: 4, 96: 7 }[sample.diameter];
|
||||
const rect = battery.getBoundingClientRect();
|
||||
const iconRect = icon.getBoundingClientRect();
|
||||
const artworkRect = artwork.getBoundingClientRect();
|
||||
const whole = shell.getBoundingClientRect();
|
||||
const core = device.querySelector('.device-core').getBoundingClientRect();
|
||||
near(core.height * 101.5 / 80, sample.diameter, `${sample.id} base diameter`);
|
||||
near(rect.width, expectedFrame, `${sample.id} frame width`);
|
||||
near(rect.height, expectedFrame, `${sample.id} frame height`);
|
||||
near(iconRect.width, expectedFrame, `${sample.id} MDI host width`);
|
||||
near(iconRect.height, expectedFrame, `${sample.id} MDI host height`);
|
||||
near(artworkRect.width, expectedFrame, `${sample.id} painted SVG width`);
|
||||
near(artworkRect.height, expectedFrame, `${sample.id} painted SVG height`);
|
||||
near(rect.left - whole.right, expectedGap, `${sample.id} whole-shell gap`);
|
||||
near(rect.top + rect.height / 2, whole.top + whole.height / 2, `${sample.id} whole-shell center`);
|
||||
if (battery.dataset.state !== sample.state) throw new Error(`battery golden wrong state: ${sample.id}`);
|
||||
if (rect.left < stage.left || rect.right > stage.right || rect.top < stage.top || rect.bottom > stage.bottom)
|
||||
throw new Error(`battery golden clipped battery: ${sample.id}`);
|
||||
if (sample.arrangement === 'legacy' && !device.querySelector('.legacy-secondary'))
|
||||
throw new Error('battery golden legacy second metric is absent');
|
||||
if (sample.arrangement === 'text' && !(whole.width > sample.diameter))
|
||||
throw new Error('battery golden Text shell is not expanded');
|
||||
measured.push({ id: sample.id, state: sample.state, color: colors[sample.state],
|
||||
x: rect.left, y: rect.top, width: rect.width, height: rect.height });
|
||||
}
|
||||
await frame();
|
||||
return { iconSize, loadedIcons: loadedIcons.size, samples: measured };
|
||||
}, { samples: batteryBoardSamples(scenario.batteryBoard), scenario, colors: COLORS, icons: BATTERY_BOARD_ICONS });
|
||||
}
|
||||
|
||||
/** Real screenshot ink, not DOM presence: empty/broken/invisible SVG fails. */
|
||||
export async function inspectBatteryBoardPixels(page, png, clip, board) {
|
||||
const samples = await page.evaluate(async ({ png64, clip, samples }) => {
|
||||
const bytes = Uint8Array.from(atob(png64), (char) => char.charCodeAt(0));
|
||||
const image = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const context = canvas.getContext('2d', { willReadFrequently: true });
|
||||
context.drawImage(image, 0, 0);
|
||||
const pixels = context.getImageData(0, 0, image.width, image.height).data;
|
||||
image.close();
|
||||
return samples.map((sample) => {
|
||||
const left = Math.floor(sample.x - (clip?.x || 0));
|
||||
const top = Math.floor(sample.y - (clip?.y || 0));
|
||||
const right = Math.ceil(left + sample.width);
|
||||
const bottom = Math.ceil(top + sample.height);
|
||||
let matching = 0;
|
||||
for (let y = Math.max(0, top); y < Math.min(canvas.height, bottom); y++) {
|
||||
for (let x = Math.max(0, left); x < Math.min(canvas.width, right); x++) {
|
||||
const offset = (y * canvas.width + x) * 4;
|
||||
if (sample.color.every((channel, index) => Math.abs(pixels[offset + index] - channel) <= 12)) matching++;
|
||||
}
|
||||
}
|
||||
return { id: sample.id, matching, minimum: Math.max(8, Math.floor(sample.width * sample.height * 0.035)) };
|
||||
});
|
||||
}, { png64: png.toString('base64'), clip, samples: board.samples });
|
||||
const failures = samples.filter((sample) => sample.matching < sample.minimum);
|
||||
if (failures.length) throw new Error(`semantic battery golden ink missing: ${JSON.stringify(failures)}`);
|
||||
return samples;
|
||||
}
|
||||
|
||||
export const BATTERY_ZIGBEE_MARKER = 'battery-56-normal';
|
||||
|
||||
/** Same natural unplaced-parent arrangement as smoke_device_battery_zigbee. */
|
||||
export function makeBatteryZigbeeFixture() {
|
||||
const fixture = makeBatteryBoardFixture('desktop');
|
||||
fixture.config.markers = fixture.config.markers.filter((marker) => marker.id === BATTERY_ZIGBEE_MARKER);
|
||||
fixture.config.markers[0].size = 1.4;
|
||||
fixture.config.settings = { ...fixture.config.settings, volumetric_view: false,
|
||||
show_device_battery: true, moon_enabled: false, zigbee_topology: { enabled: true },
|
||||
marker_area_snapshot: { [BATTERY_ZIGBEE_MARKER]: fixture.config.settings.marker_area_snapshot[BATTERY_ZIGBEE_MARKER] },
|
||||
};
|
||||
fixture.devices = { [BATTERY_ZIGBEE_MARKER]: fixture.devices[BATTERY_ZIGBEE_MARKER] };
|
||||
fixture.entities = Object.fromEntries(Object.entries(fixture.entities)
|
||||
.filter(([, entity]) => entity.device_id === BATTERY_ZIGBEE_MARKER));
|
||||
fixture.states = Object.fromEntries(Object.entries(fixture.states).filter(([id]) => fixture.entities[id]));
|
||||
fixture.layout = { [BATTERY_ZIGBEE_MARKER]: { s: SPACE, x: 0.34, y: 0.34 } };
|
||||
fixture.zhaDevices = [
|
||||
{ ieee: '00124b0000000001', nwk: 1, device_reg_id: BATTERY_ZIGBEE_MARKER, device_type: 'EndDevice',
|
||||
neighbors: [{ ieee: '00124b0000000002', relationship: 'Parent', lqi: 50 }] },
|
||||
{ ieee: '00124b0000000002', nwk: 2, device_reg_id: 'not_on_plan', device_type: 'Router',
|
||||
name: 'Upstairs parent relay',
|
||||
neighbors: [{ ieee: '00124b0000000001', relationship: 'Child', lqi: 50 }] },
|
||||
];
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export async function batteryZigbeeProbe(page) {
|
||||
return page.evaluate((id) => {
|
||||
const root = window.__goldenCard.renderRoot;
|
||||
const marker = root.querySelector(`[data-hp="device"][data-id="${CSS.escape(id)}"]`);
|
||||
const overlay = root.querySelector('hp-zigbee-topology-overlay')?.shadowRoot;
|
||||
const rect = (node) => {
|
||||
if (!node) throw new Error('battery Zigbee golden missing rendered geometry');
|
||||
const r = node.getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
|
||||
};
|
||||
const battery = rect(marker?.querySelector('.device-battery'));
|
||||
const caption = rect(overlay?.querySelector('[data-hp="zigbee-topology-parent-bubble"]'));
|
||||
const core = rect(marker?.querySelector('.device-core'));
|
||||
return { battery, caption, core, overlap: {
|
||||
x: Math.max(battery.x, caption.x), y: Math.max(battery.y, caption.y),
|
||||
right: Math.min(battery.right, caption.right), bottom: Math.min(battery.bottom, caption.bottom),
|
||||
} };
|
||||
}, BATTERY_ZIGBEE_MARKER);
|
||||
}
|
||||
|
||||
/** Fixed, core-anchored focus: tiny layering changes cannot hide in a full page. */
|
||||
export function batteryZigbeeClip(probe, viewport) {
|
||||
const clip = { x: Math.floor(probe.core.x - 16),
|
||||
y: Math.floor(probe.core.y + probe.core.height / 2 - 70), width: 420, height: 140 };
|
||||
if (clip.x < 0 || clip.y < 0 || clip.x + clip.width > viewport.width
|
||||
|| clip.y + clip.height > viewport.height || [probe.core, probe.battery, probe.caption].some((rect) =>
|
||||
rect.x < clip.x || rect.y < clip.y || rect.right > clip.x + clip.width || rect.bottom > clip.y + clip.height))
|
||||
throw new Error(`battery Zigbee golden focused clip truncates the witness: ${JSON.stringify({ clip, probe, viewport })}`);
|
||||
return clip;
|
||||
}
|
||||
|
||||
/** Executable pixel oracle shared with unit negative cases, not a z-index check. */
|
||||
export function inspectBatteryZigbeePixels(images, probe, clip) {
|
||||
if (probe.overlap.right - probe.overlap.x < 4 || probe.overlap.bottom - probe.overlap.y < 6)
|
||||
throw new Error('battery Zigbee golden caption does not overlap battery');
|
||||
const at = (image, x, y) => {
|
||||
const ix = Math.floor(x - clip.x), iy = Math.floor(y - clip.y);
|
||||
if (ix < 0 || iy < 0 || ix >= image.width || iy >= image.height)
|
||||
throw new Error('battery Zigbee golden pixel outside capture');
|
||||
const index = (iy * image.width + ix) * 4;
|
||||
return image.data.slice(index, index + 3);
|
||||
};
|
||||
const changed = (a, b) => a.some((value, index) => Math.abs(value - b[index]) > 12);
|
||||
const green = (pixel) => COLORS.normal.every((value, index) => Math.abs(value - pixel[index]) < 8);
|
||||
let ink = 0, covered = 0, coreChanged = 0, exposedRoute = 0;
|
||||
const centreY = probe.caption.y + probe.caption.height / 2;
|
||||
for (let y = Math.ceil(Math.max(probe.overlap.y + 2, centreY - 5));
|
||||
y < Math.min(probe.overlap.bottom - 2, centreY + 5); y++) {
|
||||
for (let x = Math.ceil(probe.overlap.x + 2); x < probe.overlap.right - 2; x++) {
|
||||
const baseline = at(images.captionHidden, x, y), painted = at(images.active, x, y);
|
||||
if (!green(baseline)) continue;
|
||||
ink++;
|
||||
if (changed(baseline, painted) && painted[1] - Math.max(painted[0], painted[2]) < 50) covered++;
|
||||
}
|
||||
}
|
||||
const cx = probe.core.x + probe.core.width / 2, cy = probe.core.y + probe.core.height / 2;
|
||||
for (let x = Math.ceil(cx); x < probe.caption.x - 3; x++) {
|
||||
for (let dy = -2; dy <= 2; dy++) {
|
||||
if (!changed(at(images.active, x, cy + dy), at(images.routesHidden, x, cy + dy))) continue;
|
||||
if (x < probe.core.right - 5) coreChanged++;
|
||||
else if (x > probe.core.right + 3) exposedRoute++;
|
||||
}
|
||||
}
|
||||
const evidence = { ink, covered, coreChanged, exposedRoute };
|
||||
// Keep the smoke's differential thresholds; the focused zero-diff golden
|
||||
// additionally catches a one-pixel edge leak that a ratio alone could miss.
|
||||
if (ink < 3 || covered / ink < 0.9)
|
||||
throw new Error(`battery Zigbee golden caption does not cover real battery ink: ${JSON.stringify(evidence)}`);
|
||||
if (coreChanged > 2 || exposedRoute < 3)
|
||||
throw new Error(`battery Zigbee golden route/core paint order is missing: ${JSON.stringify(evidence)}`);
|
||||
return evidence;
|
||||
}
|
||||
|
||||
export async function prepareBatteryZigbeeOverlap(page) {
|
||||
// Open/refresh/close the real General Settings UI; __hpTest still points at
|
||||
// the old demo card after golden remounts and must not seed this scene.
|
||||
await page.evaluate(() => window.__goldenCard.renderRoot.querySelector('[data-hp="settings"]').click());
|
||||
await page.waitForFunction(() => [...(window.__goldenCard.renderRoot
|
||||
.querySelector('hp-zigbee-topology-settings')?.shadowRoot?.querySelectorAll('button') || [])]
|
||||
.some((button) => button.querySelector('ha-icon[icon="mdi:access-point-network"]') && !button.disabled));
|
||||
await page.evaluate(() => [...window.__goldenCard.renderRoot.querySelector('hp-zigbee-topology-settings')
|
||||
.shadowRoot.querySelectorAll('button')]
|
||||
.find((button) => button.querySelector('ha-icon[icon="mdi:access-point-network"]')).click());
|
||||
await page.waitForFunction(() => /Received/i.test(window.__goldenCard.renderRoot
|
||||
.querySelector('hp-zigbee-topology-settings')?.shadowRoot?.textContent || ''));
|
||||
await page.evaluate(() => window.__goldenCard.renderRoot.querySelector('hp-dialog [data-hp="dialog-cancel"]').click());
|
||||
await page.waitForFunction(() => !window.__goldenCard.renderRoot.querySelector('hp-dialog'));
|
||||
const face = await page.evaluate(async (id) => {
|
||||
const marker = window.__goldenCard.renderRoot.querySelector(`[data-hp="device"][data-id="${CSS.escape(id)}"]`);
|
||||
const battery = marker?.querySelector('.device-battery');
|
||||
const icon = battery?.querySelector('ha-icon.device-battery-icon');
|
||||
await icon?.updateComplete;
|
||||
const svg = icon?.shadowRoot?.querySelector('svg'), path = svg?.querySelector('path');
|
||||
const rect = battery?.getBoundingClientRect(), core = marker?.querySelector('.device-core')?.getBoundingClientRect();
|
||||
if (battery?.dataset.state !== 'normal' || icon?.icon !== 'mdi:battery'
|
||||
|| !path || path.getAttribute('d') !== window.__ICONS?.['mdi:battery']
|
||||
|| !(path.getBBox().width > 0) || !(path.getBBox().height > 0)
|
||||
|| svg.getAttribute('viewBox') !== '0 0 24 24'
|
||||
|| !(rect.width > 0) || Math.abs(svg.getBoundingClientRect().width - rect.width) > 0.5
|
||||
|| Math.abs(svg.getBoundingClientRect().height - rect.height) > 0.5)
|
||||
throw new Error('battery Zigbee golden lacks a full-size official MDI battery');
|
||||
return { x: core.x + core.width / 2, y: core.y + core.height / 2,
|
||||
battery: { id, state: 'normal', color: [29, 194, 29], x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
||||
}, BATTERY_ZIGBEE_MARKER);
|
||||
// Before hover the very same MDI must paint. Empty glyphs cannot make the
|
||||
// subsequent caption control vacuously green.
|
||||
await inspectBatteryBoardPixels(page, await page.screenshot({ scale: 'css', animations: 'disabled' }),
|
||||
null, { samples: [face.battery] });
|
||||
await page.mouse.move(face.x, face.y);
|
||||
await page.waitForFunction(() => window.__goldenCard.renderRoot.querySelector('hp-zigbee-topology-overlay')
|
||||
?.shadowRoot?.querySelector('[data-hp="zigbee-topology-parent-bubble"]')?.getBoundingClientRect().width > 0);
|
||||
await page.evaluate(() => {
|
||||
const caption = window.__goldenCard.renderRoot.querySelector('hp-zigbee-topology-overlay').shadowRoot
|
||||
.querySelector('[data-hp="zigbee-topology-parent-bubble"]');
|
||||
if (!caption.textContent.includes('Upstairs parent relay'))
|
||||
throw new Error('battery Zigbee golden expected real parent caption text');
|
||||
});
|
||||
await page.evaluate(async () => {
|
||||
await window.__goldenCard.updateComplete;
|
||||
await new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
|
||||
});
|
||||
const probe = await batteryZigbeeProbe(page), clip = batteryZigbeeClip(probe, page.viewportSize());
|
||||
const screenshot = () => page.screenshot({ clip, scale: 'css', animations: 'disabled', caret: 'hide' });
|
||||
const controls = async (hidden) => page.evaluate((hidden) => {
|
||||
const overlay = window.__goldenCard.renderRoot.querySelector('hp-zigbee-topology-overlay').shadowRoot;
|
||||
for (const [name, selector] of [['caption', 'zigbee-topology-parent-bubble'], ['routes', 'zigbee-topology-lines']]) {
|
||||
const node = overlay.querySelector(`[data-hp="${selector}"]`);
|
||||
if (!node) throw new Error(`battery Zigbee golden missing ${name}`);
|
||||
if (hidden === name) node.style.visibility = 'hidden';
|
||||
else node.style.removeProperty('visibility');
|
||||
}
|
||||
}, hidden);
|
||||
let active, captionHidden, routesHidden;
|
||||
try {
|
||||
active = await screenshot();
|
||||
await controls('caption');
|
||||
captionHidden = await screenshot();
|
||||
await controls('routes');
|
||||
routesHidden = await screenshot();
|
||||
} finally {
|
||||
await controls(null);
|
||||
}
|
||||
const images = await page.evaluate(async (encoded) => Object.fromEntries(await Promise.all(
|
||||
Object.entries(encoded).map(async ([name, base64]) => {
|
||||
const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
||||
const bitmap = await createImageBitmap(new Blob([bytes], { type: 'image/png' }));
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height), context = canvas.getContext('2d');
|
||||
context.drawImage(bitmap, 0, 0);
|
||||
const image = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
||||
bitmap.close();
|
||||
return [name, { width: image.width, height: image.height, data: Array.from(image.data) }];
|
||||
}))), { active: active.toString('base64'), captionHidden: captionHidden.toString('base64'),
|
||||
routesHidden: routesHidden.toString('base64') });
|
||||
return { ...inspectBatteryZigbeePixels(images, probe, clip), clip, overlap: probe.overlap };
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { makeLargeHouseFixture } from '../fixtures/large-house.mjs';
|
||||
import { fixtureWallKey, makeVisualMatrixFixture } from '../fixtures/visual-matrix.mjs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBatteryBoardFixture, prepareBatteryBoard, makeBatteryZigbeeFixture,
|
||||
prepareBatteryZigbeeOverlap, batteryZigbeeProbe, batteryZigbeeClip } from './device-battery.mjs';
|
||||
|
||||
const junctionArtifactsFixture = JSON.parse(readFileSync(
|
||||
new URL('../../test/fixtures/302-junction-artifacts.json', import.meta.url), 'utf8',
|
||||
@@ -41,7 +43,9 @@ const wallUnionIsolationFixture = JSON.parse(readFileSync(
|
||||
const cardVersion = '0.0.0-golden';
|
||||
const VERSION_RELOAD_ATTEMPT_KEY = 'houseplan-card:version-reload-target:v1';
|
||||
|
||||
const fixtureFor = (scenario) => scenario.fixture === 'large'
|
||||
const fixtureFor = (scenario) => scenario.batteryBoard ? makeBatteryBoardFixture(scenario.batteryBoard)
|
||||
: scenario.batteryZigbeeOverlap ? makeBatteryZigbeeFixture()
|
||||
: scenario.fixture === 'large'
|
||||
? makeLargeHouseFixture({ includeStairs: false })
|
||||
: makeVisualMatrixFixture({
|
||||
applianceLifecycle: !!scenario.applianceLifecycle, ledStrips: !!scenario.ledStrips,
|
||||
@@ -1011,6 +1015,7 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
three: { floor_id: 'three', name: 'Three', level: 2 },
|
||||
},
|
||||
callWS: async (message) => {
|
||||
if (message.type === 'zha/devices' && fixture.zhaDevices) return structuredClone(fixture.zhaDevices);
|
||||
if (message.type === 'houseplan/config/get') return {
|
||||
config: structuredClone(fixture.config), rev: 1,
|
||||
can_write: scenario.canWrite !== false,
|
||||
@@ -2491,11 +2496,14 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
if (!point) throw new Error(`golden hover stair missing: ${scenario.hoverStair}`);
|
||||
await page.mouse.move(point.x, point.y);
|
||||
}
|
||||
if (scenario.batteryBoard) result.batteryBoard = await prepareBatteryBoard(page, scenario);
|
||||
if (scenario.batteryZigbeeOverlap) result.batteryZigbeeOverlap = await prepareBatteryZigbeeOverlap(page);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function goldenClip(page, capture) {
|
||||
if (capture === 'page') return null;
|
||||
if (capture === 'battery-zigbee-overlap') return batteryZigbeeClip(await batteryZigbeeProbe(page), page.viewportSize());
|
||||
return page.evaluate((captureKind) => {
|
||||
const card = window.__goldenCard;
|
||||
const target = card?.renderRoot?.querySelector('.stage');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
|
||||
|
||||
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
|
||||
export const GOLDEN_MATRIX_VERSION = 71;
|
||||
export const GOLDEN_MATRIX_VERSION = 73;
|
||||
|
||||
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
|
||||
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
|
||||
@@ -888,6 +888,26 @@ export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
'golden-climate': { s: 'golden-lighting', x: 0.80, y: 0.28 },
|
||||
},
|
||||
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
// #792: six exact designer variants (Light/Dark × D32/56/96), all four
|
||||
// states, every badge side, expanded Text and legacy multi-metric shells.
|
||||
// Each rendered battery must render its MDI path and paint matching ink.
|
||||
...['light', 'dark'].map((theme) => ({
|
||||
id: `device-battery-board-${theme}`, fixture: 'visual', space: 'golden-battery', mode: 'view',
|
||||
batteryBoard: 'desktop', theme, viewport: { width: 1120, height: 1120 }, ...stage,
|
||||
})),
|
||||
// #806 AC1: an explicit neutral mid-tone plan surface. Theme is kept light
|
||||
// so this proves the shadow against a third background, not a third UI skin.
|
||||
{ id: 'device-battery-board-medium', fixture: 'visual', space: 'golden-battery', mode: 'view',
|
||||
batteryBoard: 'desktop', bgMode: 'static', bgColor: '#808080',
|
||||
fillMode: 'custom', customFill: { c: '#808080', a: 1 },
|
||||
theme: 'light', viewport: { width: 1120, height: 1120 }, ...stage },
|
||||
{ id: 'device-battery-mobile-dark', fixture: 'visual', space: 'golden-battery', mode: 'view',
|
||||
batteryBoard: 'mobile', theme: 'dark', viewport: { width: 390, height: 844 }, ...stage },
|
||||
// Real ZHA refresh + hover; differential raster controls prove natural
|
||||
// caption/battery overlap. A focused zero-diff frame protects 1px edges.
|
||||
{ id: 'device-battery-zigbee-overlap-dark', fixture: 'visual', space: 'golden-battery', mode: 'view',
|
||||
batteryZigbeeOverlap: true, iconSize: 5, theme: 'dark', viewport: { width: 1200, height: 900 },
|
||||
capture: 'battery-zigbee-overlap', threshold: { maxChannelDelta: 10, maxDiffRatio: 0 } },
|
||||
{ id: 'device-value-face-cover-source-dark', fixture: 'visual',
|
||||
space: 'golden-lighting', mode: 'view', glowEnabled: false, sunRays: false,
|
||||
showNames: false,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { launch } from '../serve.mjs';
|
||||
import { assertFreshDemoBundle } from '../bundle-freshness.mjs';
|
||||
import { goldenClip, prepareGoldenScenario } from './harness.mjs';
|
||||
import { GOLDEN_MATRIX_VERSION, GOLDEN_SCENARIOS } from './matrix.mjs';
|
||||
import { inspectBatteryBoardPixels } from './device-battery.mjs';
|
||||
import { CAPTURE_PROVENANCE_SCHEMA, captureProvenance } from '../../scripts/capture-environment.mjs';
|
||||
import {
|
||||
assertGoldenInvocation,
|
||||
@@ -1020,6 +1021,9 @@ try {
|
||||
writeFileSync(actualPath, actual);
|
||||
result.actualSha256 = sha256(actual);
|
||||
result.actual = actualPath;
|
||||
if (scenario.batteryBoard) {
|
||||
result.batteryPixels = await inspectBatteryBoardPixels(page, actual, clip, result.runtime.batteryBoard);
|
||||
}
|
||||
if (scenario.sunRayPixels) {
|
||||
const control = await captureWithoutSunRays(page, screenshotOptions);
|
||||
const sample = await countChangedPixels(page, actual, control.png, scenario.sunRayPixels);
|
||||
|
||||
@@ -17,6 +17,24 @@
|
||||
Запускает их `verify-guard.mjs`; он же вызывается из job «Смоки в браузере»
|
||||
и служит guard'ом мутантов в `scripts/mutation-gate.mjs`.
|
||||
|
||||
## Граница вердикта (#776)
|
||||
|
||||
`finish()` сначала делает round-trip по открытым страницам, затем дожидается
|
||||
`browser.close()` и только после этого считает доставленные `pageerror`.
|
||||
Round-trip не опустошает все очереди браузера: хвостовое отклонение промиса
|
||||
может прийти во время закрытия. Снимок счётчика до `close()` давал `EXC`, затем
|
||||
ложный `OK` и exit 0. Теперь любое событие, доставленное до итогового вердикта,
|
||||
входит в единую диагностику и exit 1; произвольные будущие таймеры не ожидаются.
|
||||
`reportPageErrors()` сохраняет отдельный контракт: round-trip без закрытия
|
||||
браузера, итог по событиям, доставленным к возврату этого вызова.
|
||||
|
||||
`node --test test/smoke-harness-lifecycle.test.mjs` детерминированно исполняет
|
||||
публичные `watchPage`/`finish`/`reportPageErrors` с управляемой доставкой событий,
|
||||
в том числе внутри асинхронного закрытия. Он не требует Chromium и проверяет
|
||||
реальный код выхода дочернего процесса и отсутствие ложного `OK`. Исходные
|
||||
браузерные пробы остаются без задержек и подавления ошибок: они проверяют
|
||||
настоящий канал Playwright, а не подменяют его Node-свидетелем.
|
||||
|
||||
Одна проба живёт не здесь: `--guard-probe` у `demo/benchmark_backdrop_decode.mjs`
|
||||
(#430). Benchmark нельзя переселить в этот каталог — его запускают руками при
|
||||
рекалибровке порогов, — поэтому `verify-guard.mjs` умеет запускать файл выше
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/** #792: production-shaped registry/config inputs, no private card writes. */
|
||||
export async function installBatteryFixture(page) {
|
||||
await page.evaluate(async () => {
|
||||
const card = window.__card;
|
||||
const sources = [
|
||||
['sensor.aaa_hp_battery', 'd_temp', '80'],
|
||||
['sensor.zzz_hp_battery', 'd_temp', '5'],
|
||||
['sensor.hp_light_battery', 'd_light1', '80'],
|
||||
];
|
||||
const states = { ...card.hass.states };
|
||||
for (const [id, device, state] of sources) {
|
||||
window.__addRegistryEntity(id, null, state);
|
||||
Object.assign(card.hass.entities[id], { device_id: device, device_class: 'battery' });
|
||||
states[id] = { entity_id: id, state, attributes: { device_class: 'battery', unit_of_measurement: '%' } };
|
||||
window.__setRegistryArea('entity', id, null);
|
||||
}
|
||||
card.hass = { ...card.hass, states };
|
||||
card.setConfig({ type: 'custom:houseplan-card', title: 'Battery witness', icon_size: 5, language: 'en' });
|
||||
await window.__hpTest.setMode('view');
|
||||
await window.__hpTest.setServerConfig(cfg => ({ ...cfg,
|
||||
settings: { ...cfg.settings, volumetric_view: false, show_device_battery: true, moon: false },
|
||||
markers: [
|
||||
...Object.keys(card.hass.devices).filter(id => !['d_temp', 'd_light1', 'd_leak'].includes(id))
|
||||
.map(id => ({ id, binding: `device:${id}`, hidden: true })),
|
||||
{ id: 'd_temp', binding: 'device:d_temp', space: 'f1', display: 'badge',
|
||||
value_badge: { enabled: false }, size: 1.4, tap_action: 'info' },
|
||||
{ id: 'd_light1', binding: 'device:d_light1', space: 'f1', display: 'badge',
|
||||
value_badge: { enabled: false }, tap_action: 'info' },
|
||||
{ id: 'd_leak', binding: 'device:d_leak', space: 'f1', display: 'badge', value_badge: { enabled: false } },
|
||||
],
|
||||
}));
|
||||
await window.__hpTest.setLayout(layout => ({ ...layout,
|
||||
d_temp: { s: 'f1', x: 0.34, y: 0.34 },
|
||||
d_light1: { s: 'f1', x: 0.7, y: 0.65 },
|
||||
d_leak: { s: 'f1', x: 0.78, y: 0.3 },
|
||||
}));
|
||||
await window.__hpTest.settled();
|
||||
});
|
||||
await page.waitForFunction(() => window.__card.shadowRoot
|
||||
.querySelector('[data-hp="device"][data-id="d_temp"] .device-battery[data-state="normal"]'));
|
||||
}
|
||||
|
||||
export async function setBatteryState(page, state, source = 'sensor.aaa_hp_battery') {
|
||||
await page.evaluate(async ({ state, source }) => {
|
||||
const card = window.__card;
|
||||
card.hass = { ...card.hass, states: { ...card.hass.states,
|
||||
[source]: { ...card.hass.states[source], state },
|
||||
} };
|
||||
for (const secondary of document.querySelectorAll('houseplan-space-card')) secondary.hass = card.hass;
|
||||
await window.__hpTest.settled();
|
||||
}, { state, source });
|
||||
}
|
||||
|
||||
export async function patchBatteryMarker(page, patch) {
|
||||
await page.evaluate(patch => window.__hpTest.setServerConfig(cfg => ({ ...cfg,
|
||||
markers: cfg.markers.map(marker => marker.id === 'd_temp' ? { ...marker, ...patch } : marker),
|
||||
})), patch);
|
||||
}
|
||||
|
||||
export async function batteryGeometry(page, id = 'd_temp') {
|
||||
return page.evaluate(id => {
|
||||
const node = window.__card.shadowRoot.querySelector(`[data-hp="device"][data-id="${id}"]`);
|
||||
const rect = selector => {
|
||||
const r = node?.querySelector(selector)?.getBoundingClientRect();
|
||||
return r ? { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom } : null;
|
||||
};
|
||||
return { shell: rect('.device-shell-frame'), core: rect('.device-core'), badge: rect('.value-badge'),
|
||||
lqi: rect('.lqi'), battery: rect('.device-battery'),
|
||||
state: node?.querySelector('.device-battery')?.dataset.state || null,
|
||||
};
|
||||
}, id);
|
||||
}
|
||||
|
||||
/** Owner-approved MDI mapping, independently pinned from the production renderer. */
|
||||
const BATTERY_MDI_ICONS = {
|
||||
normal: 'mdi:battery', warning: 'mdi:battery-30',
|
||||
low: 'mdi:battery-outline', unknown: 'mdi:battery-unknown',
|
||||
};
|
||||
|
||||
export async function batteryMdiIcon(page, state, id = 'd_temp') {
|
||||
return page.evaluate(({ state, id, expected }) => {
|
||||
const battery = window.__card.shadowRoot
|
||||
.querySelector(`[data-hp="device"][data-id="${id}"] .device-battery`);
|
||||
const icon = battery?.querySelector('ha-icon.device-battery-icon');
|
||||
const path = icon?.shadowRoot?.querySelector('svg path');
|
||||
const box = path?.getBBox();
|
||||
return battery?.dataset.state === state && icon?.getAttribute('icon') === expected
|
||||
&& !!window.__ICONS?.[expected] && path?.getAttribute('d') === window.__ICONS[expected]
|
||||
&& box?.width > 0 && box?.height > 0;
|
||||
}, { state, id, expected: BATTERY_MDI_ICONS[state] });
|
||||
}
|
||||
|
||||
/** Count painted colour, not just the ha-icon attribute or a nonempty path. */
|
||||
export async function batteryColorPixels(page, rectangle, rgb) {
|
||||
await page.waitForFunction(() => {
|
||||
const batteries = [...window.__card.shadowRoot.querySelectorAll('.device-battery')];
|
||||
return batteries.length > 0 && batteries.every(battery => {
|
||||
const icon = battery.querySelector('ha-icon.device-battery-icon');
|
||||
const path = icon?.shadowRoot?.querySelector('svg path');
|
||||
const box = path?.getBBox();
|
||||
return !!path?.getAttribute('d') && box.width > 0 && box.height > 0;
|
||||
});
|
||||
});
|
||||
await page.evaluate(() => window.__hpTest.settled());
|
||||
const png = await page.screenshot({ animations: 'disabled', clip: {
|
||||
x: Math.floor(rectangle.x), y: Math.floor(rectangle.y),
|
||||
width: Math.ceil(rectangle.width) + 1, height: Math.ceil(rectangle.height) + 1,
|
||||
} });
|
||||
const count = await page.evaluate(async ({ data, rgb }) => {
|
||||
const bitmap = await createImageBitmap(await (await fetch(`data:image/png;base64,${data}`)).blob());
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
const pixels = ctx.getImageData(0, 0, bitmap.width, bitmap.height).data;
|
||||
let count = 0;
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
if (rgb.every((channel, index) => Math.abs(channel - pixels[i + index]) <= 8)) count++;
|
||||
}
|
||||
return count;
|
||||
}, { data: png.toString('base64'), rgb });
|
||||
if (!count) console.log('Battery missing raster ink:', await page.evaluate(() => {
|
||||
const node = window.__card.shadowRoot.querySelector('[data-id="d_temp"] .device-battery');
|
||||
const icon = node?.querySelector('ha-icon.device-battery-icon');
|
||||
return { html: node?.outerHTML, icon: icon?.getAttribute('icon'),
|
||||
path: icon?.shadowRoot?.querySelector('path')?.getAttribute('d'),
|
||||
assets: [icon, icon?.shadowRoot?.querySelector('svg')].filter(Boolean).map(asset => {
|
||||
const css = getComputedStyle(asset), box = asset.getBoundingClientRect();
|
||||
return { tag: asset.tagName, display: css.display, visibility: css.visibility,
|
||||
opacity: css.opacity, width: box.width, height: box.height };
|
||||
}) };
|
||||
}));
|
||||
return count;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/** Browser-side #789 witnesses. No product imports or private state writes.
|
||||
* The reference freezes the d8949cff full48 mask construction (led-strip-field
|
||||
* lines 433–465). Geometry/visibility are deliberately shared with the actual
|
||||
* DOM; smoke_led_strip_field's independent distance oracle covers those.
|
||||
* Actual raster styles are resolved on the connected node, so a leaked host
|
||||
* coarse selector cannot disappear merely because the clone left its host.
|
||||
*/
|
||||
export function installLedZoomOracle() {
|
||||
const NS = 'http://www.w3.org/2000/svg';
|
||||
const root = card => card.shadowRoot;
|
||||
const bands = field => [...field.querySelectorAll('mask > g > path')];
|
||||
const fields = card => [...root(card).querySelectorAll('[data-led-field]')];
|
||||
const attrs = node => [...node.attributes].map(a => [a.name, a.value]);
|
||||
const visibleThroughHost = (node, card) => {
|
||||
for (let current = node; current;) {
|
||||
const css = getComputedStyle(current);
|
||||
if (css.display === 'none' || css.visibility !== 'visible' || !(Number(css.opacity) > 0)) return false;
|
||||
if (current === card) return true;
|
||||
current = current.parentElement || current.getRootNode()?.host || null;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const stops = [[0, 1], [45, .88], [70, .62], [86, .32], [100, 0]];
|
||||
const grey = fraction => {
|
||||
const at = fraction * 100;
|
||||
for (let i = 1; i < stops.length; i++) {
|
||||
const [a, va] = stops[i - 1], [b, vb] = stops[i];
|
||||
if (at <= b) {
|
||||
const v = Math.round((va + (vb - va) * (at - a) / (b - a)) * 255);
|
||||
return `rgb(${v}, ${v}, ${v})`;
|
||||
}
|
||||
}
|
||||
return 'rgb(0, 0, 0)';
|
||||
};
|
||||
const snapshot = card => {
|
||||
const nodes = [...root(card).querySelectorAll('.led-fields, .led-fields *, [data-led-strip], [data-led-strip] *, [data-glow-source], [data-glow-source] *')];
|
||||
return { nodes, attributes: nodes.map(attrs), cache: [...root(card).querySelectorAll('.led-fields')]
|
||||
.map(n => [n.dataset.ledCache, n.dataset.ledRecomputes]) };
|
||||
};
|
||||
const unchanged = (card, before) => {
|
||||
const after = snapshot(card);
|
||||
const hitWidthChanges = [];
|
||||
const stable = before.nodes.length === after.nodes.length && before.nodes.every((node, i) => {
|
||||
if (node !== after.nodes[i]) return false;
|
||||
const old = before.attributes[i], now = after.attributes[i];
|
||||
if (!node.matches('.led-hit')) return JSON.stringify(old) === JSON.stringify(now);
|
||||
const oldWidth = old.find(([key]) => key === 'stroke-width')?.[1];
|
||||
const newWidth = now.find(([key]) => key === 'stroke-width')?.[1];
|
||||
if (oldWidth !== newWidth) hitWidthChanges.push({ oldWidth, newWidth });
|
||||
return JSON.stringify(old.filter(([key]) => key !== 'stroke-width'))
|
||||
=== JSON.stringify(now.filter(([key]) => key !== 'stroke-width'));
|
||||
});
|
||||
return { stable, hitWidthChanges, cacheStable: JSON.stringify(before.cache) === JSON.stringify(after.cache) };
|
||||
};
|
||||
const quality = card => ({
|
||||
coarse: card.getAttribute('data-led-zoom-quality') === 'coarse',
|
||||
ordinary: [...root(card).querySelectorAll('[data-glow-source]')].map(field => ({
|
||||
visible: visibleThroughHost(field, card),
|
||||
})),
|
||||
fields: fields(card).map(field => {
|
||||
const paths = bands(field);
|
||||
const painted = paths.filter(path => getComputedStyle(path).display !== 'none');
|
||||
const pool = field.querySelector('.led-pool');
|
||||
return { id: field.dataset.ledField, retained: paths.length, painted: painted.length,
|
||||
visible: visibleThroughHost(field, card) && Number(pool.getAttribute('fill-opacity')) > 0,
|
||||
correct24: paths.length === 48 && paths.every((path, k) => k % 2
|
||||
? getComputedStyle(path).display === 'none'
|
||||
: getComputedStyle(path).display !== 'none' && getComputedStyle(path).stroke === grey(1 - (k + 1) / 48)),
|
||||
fill: pool.getAttribute('fill'), alpha: pool.getAttribute('fill-opacity'),
|
||||
};
|
||||
}),
|
||||
});
|
||||
const clone = (card, frozen48) => {
|
||||
const original = root(card).querySelector('.led-fields');
|
||||
if (!original) throw new Error('LED raster oracle: no field');
|
||||
const copy = original.cloneNode(true);
|
||||
fields(card).forEach((field, i) => {
|
||||
const target = copy.querySelectorAll('[data-led-field]')[i];
|
||||
target.style.opacity = getComputedStyle(field).opacity;
|
||||
target.style.visibility = getComputedStyle(field).visibility;
|
||||
target.style.mixBlendMode = getComputedStyle(field).mixBlendMode;
|
||||
const actualPaths = bands(field), targetPaths = bands(target);
|
||||
if (actualPaths.length !== 48) throw new Error(`LED raster oracle: expected retained48, got ${actualPaths.length}`);
|
||||
if (frozen48) {
|
||||
const group = targetPaths[0].parentNode;
|
||||
const d = actualPaths[0].getAttribute('d');
|
||||
const radius = Number(actualPaths[0].getAttribute('stroke-width')) / 2;
|
||||
group.replaceChildren();
|
||||
for (let k = 0; k < 48; k++) {
|
||||
const path = document.createElementNS(NS, 'path');
|
||||
const outer = 1 - k / 48, inner = 1 - (k + 1) / 48;
|
||||
for (const [name, value] of Object.entries({ d, fill: 'none', stroke: grey((outer + inner) / 2),
|
||||
'stroke-width': 2 * outer * radius, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' })) {
|
||||
path.setAttribute(name, String(value));
|
||||
}
|
||||
group.appendChild(path);
|
||||
}
|
||||
} else actualPaths.forEach((path, k) => {
|
||||
const css = getComputedStyle(path);
|
||||
targetPaths[k].style.stroke = css.stroke;
|
||||
targetPaths[k].style.display = css.display;
|
||||
targetPaths[k].style.visibility = css.visibility;
|
||||
});
|
||||
});
|
||||
return copy;
|
||||
};
|
||||
const raster = async (source, box, scale, background) => {
|
||||
const [x, y, w, h] = box;
|
||||
const xml = `<svg xmlns="${NS}" width="${w * scale}" height="${h * scale}" viewBox="${box.join(' ')}"><rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${background}"/>${new XMLSerializer().serializeToString(source)}</svg>`;
|
||||
const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
|
||||
await img.decode();
|
||||
const canvas = document.createElement('canvas'); canvas.width = w * scale; canvas.height = h * scale;
|
||||
const context = canvas.getContext('2d'); context.drawImage(img, 0, 0);
|
||||
return { data: context.getImageData(0, 0, canvas.width, canvas.height).data, canvas };
|
||||
};
|
||||
const full48 = async (card, { scales = [1, 2, 4], background = '#868d94', images = false } = {}) => {
|
||||
const pools = [...root(card).querySelectorAll('.led-pool')];
|
||||
const rects = pools.map(p => ['x', 'y', 'width', 'height'].map(a => Number(p.getAttribute(a))));
|
||||
const x = Math.floor(Math.min(...rects.map(r => r[0]))) - 2;
|
||||
const y = Math.floor(Math.min(...rects.map(r => r[1]))) - 2;
|
||||
const box = [x, y, Math.ceil(Math.max(...rects.map(r => r[0] + r[2]))) - x + 2,
|
||||
Math.ceil(Math.max(...rects.map(r => r[1] + r[3]))) - y + 2];
|
||||
const actual = clone(card, false), expected = clone(card, true), rows = [];
|
||||
for (const scale of scales) {
|
||||
const a = await raster(actual, box, scale, background), b = await raster(expected, box, scale, background);
|
||||
let differentPixels = 0, maxDelta = 0;
|
||||
for (let i = 0; i < a.data.length; i += 4) {
|
||||
let different = false;
|
||||
for (let c = 0; c < 4; c++) {
|
||||
const delta = Math.abs(a.data[i + c] - b.data[i + c]);
|
||||
maxDelta = Math.max(maxDelta, delta); different ||= delta !== 0;
|
||||
}
|
||||
if (different) differentPixels++;
|
||||
}
|
||||
rows.push({ scale, pixels: a.data.length / 4, differentPixels, maxDelta,
|
||||
...(images ? { actualPng: a.canvas.toDataURL(), expectedPng: b.canvas.toDataURL() } : {}) });
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
window.__ledZoomOracle = { snapshot, unchanged, quality, full48 };
|
||||
}
|
||||
@@ -464,3 +464,18 @@ batch; all batches render as one positive-winding clip path per strip. The
|
||||
paths, excluding the joined Lit/DOM clip string. Timing and the 64 MiB
|
||||
warm-cycle heap-growth limit are unchanged; that is not an absolute browser
|
||||
or GPU memory bound.
|
||||
|
||||
Issue #789 retains every historical window and budget, and adds
|
||||
`cameraFullCycleLongTaskMaxMs`: one continuous observer starting before the
|
||||
first camera wheel, spanning the original scenario and 100-step series, full
|
||||
quality restoration, two animation frames, an immediate wheel restart, a second
|
||||
restoration and two frames, then at least 500 ms of tail. Raw Long Task entries
|
||||
and phase timestamps are saved in each sample's `cameraFullCycle`. The new
|
||||
metric inherits the existing 150 ms camera ceiling for median and p95; missing
|
||||
observations or quality that fails to restore within 1000 ms fail closed. Two
|
||||
frames are a paint opportunity, not proof of compositor presentation. The
|
||||
original `cameraSeriesLongTaskMaxMs` still ends at its original boundary. This
|
||||
addition prevents deferred restoration work from escaping all judged windows;
|
||||
it does not turn local timing into canonical evidence. Issue #789's acceptance
|
||||
additionally requires a 10% task margin (warm p95 ≤1350 ms and both camera-series
|
||||
and full-cycle p95 ≤135 ms), without changing the shared budget JSON.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/** Additional #789 window: do not let deferred quality restoration escape
|
||||
* the existing camera observer. Dependencies are passed explicitly so this
|
||||
* exact function runs in the page and has a deterministic Node witness.
|
||||
* Two rAFs are a paint opportunity, never proof of compositor presentation. */
|
||||
export async function finishLedCameraCycle({ fullQuality, input, frame, now, sleep }) {
|
||||
const marks = {};
|
||||
const restored = async (name) => {
|
||||
const started = now();
|
||||
marks[`${name}RestoreStart`] = started;
|
||||
while (!fullQuality()) {
|
||||
if (now() - started >= 1000) throw new Error('LED zoom quality did not restore within 1000 ms');
|
||||
await sleep(5);
|
||||
}
|
||||
marks[`${name}FullQuality`] = now();
|
||||
await frame();
|
||||
if (!fullQuality()) throw new Error('LED zoom quality changed during the final paint opportunity');
|
||||
marks[`${name}PaintOpportunity`] = now();
|
||||
};
|
||||
await restored('first');
|
||||
marks.restartInput = now();
|
||||
await input();
|
||||
await frame();
|
||||
marks.restartSettled = now();
|
||||
await restored('final');
|
||||
await sleep(500);
|
||||
await frame();
|
||||
if (!fullQuality()) throw new Error('LED zoom quality regressed in the restore tail');
|
||||
marks.tailEnd = now();
|
||||
return marks;
|
||||
}
|
||||
|
||||
/** The new full-cycle metric inherits, never relaxes, the old camera limit.
|
||||
* Missing/nonfinite observations are failures even in a partial local run. */
|
||||
export function cameraCycleFailures(metrics, limit) {
|
||||
const failures = [];
|
||||
for (const stat of ['median', 'p95']) {
|
||||
const value = metrics?.[stat];
|
||||
if (!Number.isFinite(value)) failures.push(`cameraFullCycleLongTaskMaxMs ${stat} missing`);
|
||||
else if (Number.isFinite(limit) && value > limit) failures.push(`cameraFullCycleLongTaskMaxMs ${stat} ${value} > ${limit}`);
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
/** Check every raw sample before aggregation: filtering missing values out of
|
||||
* a median/p95 must not make a partial or old report pass --merge. */
|
||||
export function cameraCycleSampleFailures(rows) {
|
||||
const failures = [];
|
||||
const phases = ['seriesEnd', 'firstRestoreStart', 'firstFullQuality', 'firstPaintOpportunity',
|
||||
'restartInput', 'restartSettled', 'finalRestoreStart', 'finalFullQuality',
|
||||
'finalPaintOpportunity', 'tailEnd'];
|
||||
rows.forEach((row, index) => {
|
||||
const cycle = row?.cameraFullCycle;
|
||||
const times = [cycle?.startTime, ...phases.map(key => cycle?.phases?.[key]), cycle?.endTime];
|
||||
const invalidTimes = times.some((time, i) => !Number.isFinite(time) || (i > 0 && time < times[i - 1]));
|
||||
const entries = cycle?.entries;
|
||||
if (invalidTimes || !Array.isArray(entries)
|
||||
|| entries.some(entry => !Number.isFinite(entry.startTime) || !Number.isFinite(entry.duration) || entry.duration < 0)
|
||||
|| !Number.isFinite(row.cameraFullCycleLongTaskMaxMs)) {
|
||||
failures.push(`sample ${index}: missing or invalid camera full-cycle evidence`);
|
||||
return;
|
||||
}
|
||||
if (cycle.phases.tailEnd - cycle.phases.finalPaintOpportunity < 500) {
|
||||
failures.push(`sample ${index}: camera full-cycle tail shorter than 500 ms`);
|
||||
}
|
||||
const maximum = Number(Math.max(0, ...entries.map(entry => entry.duration)).toFixed(2));
|
||||
if (row.cameraFullCycleLongTaskMaxMs !== maximum) {
|
||||
failures.push(`sample ${index}: camera full-cycle maximum differs from raw Long Tasks`);
|
||||
}
|
||||
});
|
||||
return failures;
|
||||
}
|
||||
@@ -37,13 +37,12 @@ const _livePages = new Set();
|
||||
* уносит недоставленное событие. В логе это видно дословно: `EXC` печатается
|
||||
* ПОСЛЕ результата и ДО `OK`.
|
||||
*
|
||||
* Круговой запрос к странице вытесняет ранее поставленные макрозадачи, поэтому
|
||||
* всё, что страница успела произвести до этого момента, к нам уже дошло.
|
||||
*
|
||||
* Честная граница: исключение, возникшее ПОСЛЕ этого round-trip'а — например, в
|
||||
* обработчике `beforeunload` при закрытии браузера, — не учитывается. Ловить его
|
||||
* значит ждать неизвестно чего неизвестно сколько; контракт формулируется как
|
||||
* «всё, что произошло до вызова вердикта».
|
||||
* Круговой запрос даёт странице возможность доставить накопленные события,
|
||||
* но не является барьером для всех browser task queues: например, хвостовое
|
||||
* отклонение промиса может прийти уже во время browser.close() (#776).
|
||||
* finish() поэтому читает счётчик только после завершения закрытия. Граница —
|
||||
* все pageerror, доставленные до вердикта; произвольных будущих таймеров не ждём.
|
||||
* reportPageErrors() браузер не закрывает и судит события после своего запроса.
|
||||
*/
|
||||
async function roundTripLivePages() {
|
||||
for (const page of _livePages) {
|
||||
@@ -111,11 +110,11 @@ export async function reportPageErrors() {
|
||||
/** Print the result, report failures, close the browser, set the exit code. */
|
||||
export async function finish(browser, out) {
|
||||
if (out !== undefined) console.log(JSON.stringify(out, null, 1));
|
||||
// Порядок обязателен: сначала дать странице доставить события, потом читать
|
||||
// счётчик (#404). Обратный порядок и был дефектом.
|
||||
// #404/#776: дать доставить события и дождаться закрытия, затем снять итог.
|
||||
// Между round-trip и close тоже приходят pageerror: ранний снимок давал EXC + OK.
|
||||
await roundTripLivePages();
|
||||
if (_pageErrors) _failures.push(`${_pageErrors} uncaught exception(s) inside the card`);
|
||||
await browser?.close?.();
|
||||
if (_pageErrors) _failures.push(`${_pageErrors} uncaught exception(s) inside the card`);
|
||||
// #629: доказательство «перевод без потери утверждений» — отсортированный
|
||||
// список имён проверок; без переменной вывод прежний.
|
||||
if (process.env.HP_SMOKE_CHECKS === '1') {
|
||||
|
||||