diff --git a/README.md b/README.md index fb8a02b..6912e19 100755 --- a/README.md +++ b/README.md @@ -1,163 +1,157 @@ -# 🏠 House Plan — интерактивный план дома для Home Assistant +# 🏠 House Plan — an interactive house plan for Home Assistant -**Живая карта вашего дома прямо в Home Assistant: этажи, комнаты и устройства на настоящем плане — с реальными состояниями, температурой и уровнем сигнала. Всё настраивается мышкой, без единой строчки YAML.** +**A live map of your home right inside Home Assistant: floors, rooms and devices on a real floor plan — with live states, temperature and signal strength. Everything is configured with the mouse, without a single line of YAML.** -![Интерактивный план дома с устройствами](docs/images/01-plan.png) +![Interactive house plan with devices](docs/images/01-plan.png) + +🇷🇺 [Документация на русском](README.ru.md) --- -## 🇬🇧 English summary +## What it is and why -**House Plan** is an interactive floor-plan for Home Assistant: a custom Lovelace card + a storage integration in one HACS package. Draw rooms right on top of your floor-plan image, bind them to HA areas, and your devices appear on the plan automatically — with live states (lights, locks, covers), temperature and Zigbee signal (LQI). Everything is configured with the mouse: drag icons, zoom/pan, attach manuals (PDF) and metadata to any device, add virtual markers. Layout and configuration are stored server-side (`.storage`), shared by all users and synced live between open tabs; multi-floor, editor with grid snapping, optimistic locking. No YAML required. +House Plan shows your smart home the way it actually looks — on a floor plan. Instead of long lists of entities, you see rooms and devices in their real places: where the leak is, what the temperature is in the kids' room, whether the light is on in the hallway, whether the gate is open. -**Install via HACS** (custom repository → `Matysh/houseplan-card`, category *Integration*), then add the *House Plan* integration in Settings → Devices & Services and put the `custom:houseplan-card` card on a dashboard. The card JS is served by the integration itself — no manual resource setup. Full documentation below is currently in Russian; open an issue if you need help in English. +This is convenient when: + +- you have many devices and lists are awkward to use; +- you need to grasp the state of the house "at a glance"; +- you want to give access to family members — anyone can figure out a picture; +- you want a beautiful overview screen for a wall-mounted tablet. + +The integration consists of two parts that are installed together: + +- **the Lovelace card** `houseplan-card` — the interactive plan itself; +- **the server-side component** — stores the room markup and icon positions in Home Assistant, so the plan is identical in all browsers and on all devices. --- -## Что это и зачем +## How it differs from alternatives -House Plan показывает ваш умный дом так, как он выглядит на самом деле — на плане этажей. Вместо длинных списков сущностей вы видите комнаты и устройства на своих местах: где протечка, какая температура в детской, включён ли свет в прихожей, открыты ли ворота. +A house plan in Home Assistant is usually built with `picture-elements`, `ha-floorplan` and similar solutions. There you have to write YAML by hand, calculate the coordinates of every icon, and edit the config again after every change. House Plan works differently: -Это удобно, когда: - -- устройств много, и списками пользоваться неудобно; -- нужно быстро понять состояние дома «одним взглядом»; -- хочется отдать доступ близким — по картинке разберётся любой; -- вы хотите красивый обзорный экран для настенного планшета. - -Интеграция состоит из двух частей, которые ставятся вместе: - -- **карточка Lovelace** `houseplan-card` — сам интерактивный план; -- **серверный компонент** — хранит разметку комнат и позиции иконок в Home Assistant, поэтому план одинаков во всех браузерах и на всех устройствах. - ---- - -## Чем отличается от аналогов - -Обычно план дома в Home Assistant делают через `picture-elements`, `ha-floorplan` и подобные решения. Там приходится вручную писать YAML, вычислять координаты каждой иконки и заново править конфиг при каждом изменении. House Plan устроен иначе: - -| | House Plan | Обычные решения (picture-elements / ha-floorplan) | +| | House Plan | Typical solutions (picture-elements / ha-floorplan) | |---|---|---| -| **Настройка** | Полностью через интерфейс, мышкой | Ручной YAML и правка кода | -| **Добавление устройств** | Автоматически по комнатам | Каждую сущность вписываете руками | -| **Координаты иконок** | Перетаскиваете мышью | Считаете пиксели и пишете в конфиг | -| **Разметка комнат** | Встроенный редактор контуров | Рисуете в стороннем редакторе SVG | -| **Хранение** | На сервере HA (общее для всех устройств) | В YAML дашборда | -| **Масштаб** | Плавный зум, всё остаётся чётким (вектор) | Обычно фиксированная картинка | +| **Setup** | Entirely through the UI, with the mouse | Manual YAML and code editing | +| **Adding devices** | Automatic, by room | You type in every entity by hand | +| **Icon coordinates** | Drag with the mouse | You count pixels and write them into the config | +| **Room markup** | Built-in outline editor | You draw in an external SVG editor | +| **Storage** | On the HA server (shared by all devices) | In the dashboard YAML | +| **Zoom** | Smooth zoom, everything stays crisp (vector) | Usually a fixed image | -Ключевые преимущества коротко: +Key advantages in short: -- **Никакого кода.** Всё — пространства, комнаты, устройства — настраивается кликами. -- **Автоматическое добавление устройств.** Обвели комнату и привязали её к зоне Home Assistant — устройства этой зоны сами появляются на плане. -- **Ручное добавление своих.** Любое устройство, группу или даже «виртуальную» точку можно поставить на план вручную, задать имя, иконку, модель, ссылку и приложить PDF-инструкцию. -- **Живые состояния.** Температура, уровень сигнала Zigbee, вкл/выкл, открыто/закрыто — всё обновляется в реальном времени. -- **Чёткий зум.** Приближение не «мылит» картинку: план, подписи и иконки остаются векторно-чёткими на любом масштабе. +- **No code at all.** Everything — spaces, rooms, devices — is configured with clicks. +- **Automatic device placement.** Outline a room and bind it to a Home Assistant area — the devices of that area appear on the plan by themselves. +- **Manual additions of your own.** Any device, group or even a "virtual" point can be placed on the plan manually, with a name, icon, model, link and an attached PDF manual. +- **Live states.** Temperature, Zigbee signal strength, on/off, open/closed — everything updates in real time. +- **Crisp zoom.** Zooming in does not "blur" the picture: the plan, labels and icons remain vector-sharp at any scale. --- -## Установка +## Installation -### Через HACS (рекомендуется) +### Via HACS (recommended) -1. Откройте **HACS → меню (⋮) → Custom repositories**. -2. Вставьте URL этого репозитория, категория — **Integration**, и нажмите **Add**. -3. Найдите в списке **House Plan**, установите и **перезапустите Home Assistant**. -4. Перейдите в **Настройки → Устройства и службы → Добавить интеграцию** и выберите **House Plan**. +1. Open **HACS → menu (⋮) → Custom repositories**. +2. Paste the URL of this repository, set the category to **Integration**, and click **Add**. +3. Find **House Plan** in the list, install it and **restart Home Assistant**. +4. Go to **Settings → Devices & Services → Add integration** and select **House Plan**. -Карточка подключается автоматически — добавлять ресурс Lovelace вручную не нужно. +The card is registered automatically — no need to add a Lovelace resource manually. -### Вручную +### Manually -1. Скопируйте папку `custom_components/houseplan` в каталог `config/custom_components` вашего Home Assistant. -2. Перезапустите Home Assistant. -3. Добавьте интеграцию: **Настройки → Устройства и службы → Добавить интеграцию → House Plan**. +1. Copy the `custom_components/houseplan` folder into the `config/custom_components` directory of your Home Assistant. +2. Restart Home Assistant. +3. Add the integration: **Settings → Devices & Services → Add integration → House Plan**. -### Добавление экрана с планом +### Adding a plan screen -Создайте новую вкладку дашборда (удобнее всего — в режиме «Панель»/Panel) и добавьте карточку: +Create a new dashboard tab (a "Panel" view works best) and add the card: ```yaml type: custom:houseplan-card -title: План дома +title: House plan ``` -Больше ничего указывать не нужно — всё остальное настраивается прямо на экране. +Nothing else needs to be specified — everything else is configured right on the screen. --- -## Как пользоваться +## How to use -### Шаг 1. Добавьте пространство (этаж) +### Step 1. Add a space (floor) -При первом открытии план ещё пуст — House Plan сразу предложит создать первое пространство. +On first open the plan is still empty — House Plan immediately offers to create the first space. -![Пустой план — предложение добавить пространство](docs/images/02-onboarding-empty.png) +![Empty plan — prompt to add a space](docs/images/02-onboarding-empty.png) -В диалоге задайте **название** (например, «1 этаж») и **загрузите подложку** — картинку плана этажа в формате SVG, PNG или JPG. Оба поля обязательны: без плана кнопка «Сохранить» неактивна. +In the dialog, set a **name** (for example, "1st floor") and **upload a background** — a floor-plan image in SVG, PNG or JPG format. Both fields are required: without a plan the "Save" button stays disabled. -![Диалог создания пространства](docs/images/03-space-dialog.png) +![Space creation dialog](docs/images/03-space-dialog.png) -> 💡 Подложку можно нарисовать в любом планировщике (например, РЕМПЛАННЕР) или сфотографировать бумажный план. Лучше всего SVG — он остаётся чётким при увеличении. +> 💡 You can draw the background in any floor planner (for example, REMPLANNER) or photograph a paper plan. SVG works best — it stays crisp when zoomed in. -Позже можно добавить сколько угодно пространств (этажи, двор, гараж) кнопкой **+** рядом со вкладками. +Later you can add as many spaces as you like (floors, yard, garage) with the **+** button next to the tabs. -### Шаг 2. Обведите комнаты +### Step 2. Outline the rooms -После добавления первого пространства карточка сама переходит в режим разметки. Кликайте по точкам сетки, соединяя их линиями, и замкните контур комнаты кликом по первой точке. +After the first space is added, the card switches to markup mode by itself. Click grid points, connecting them with lines, and close the room outline by clicking the first point. -Как только контур замкнётся, появится окно сохранения комнаты. Здесь нужно **привязать комнату к зоне Home Assistant** — именно это включает автоматику. Для служебных помещений без устройств (холл, сауна) есть кнопка **«Без зоны»**. +As soon as the outline is closed, the room-save dialog appears. Here you need to **bind the room to a Home Assistant area** — this is exactly what enables the automation. For utility rooms with no devices (hall, sauna) there is a **"No area"** button. -![Разметка комнаты и её сохранение](docs/images/05-room-dialog.png) +![Marking up a room and saving it](docs/images/05-room-dialog.png) -### Шаг 3. Устройства появляются сами +### Step 3. Devices appear by themselves -Как только вы сохранили комнату с привязкой к зоне, **устройства этой зоны автоматически расставляются внутри контура**. Берутся те же устройства, что показаны на странице **Настройки → Устройства → (фильтр по нужной комнате)** — только осмысленные, без служебных записей, мостов и дубликатов. +As soon as you save a room bound to an area, **the devices of that area are automatically laid out inside the outline**. These are the same devices shown on the **Settings → Devices → (filtered by the room)** page — only the meaningful ones, without service records, bridges and duplicates. -По умолчанию на план попадают только осмысленные устройства — служебные записи, мосты и дубликаты отфильтрованы. Если нужно видеть **вообще все** устройства зоны, включите в шапке кнопку **👁 «Показать все устройства»**. +By default only meaningful devices make it onto the plan — service records, bridges and duplicates are filtered out. If you need to see **absolutely all** devices of the area, enable the **👁 "Show all devices"** button in the header. -Дальше можно просто пользоваться планом: клик по иконке открывает карточку устройства с моделью, ссылкой и кнопкой перехода в Home Assistant. +From here on you can just use the plan: clicking an icon opens the device card with the model, link and a button to jump into Home Assistant. -![Карточка устройства по клику](docs/images/08-info.png) +![Device card on click](docs/images/08-info.png) -### Шаг 4. Масштаб +### Step 4. Zoom -Колесо мыши или кнопки **- / ⊹ / +** приближают и отдаляют план; на сенсорном экране работает «щипок» двумя пальцами. При отдалении виден весь план целиком, при приближении — детали, и всё остаётся чётким. Масштаб запоминается отдельно для каждого пространства. +The mouse wheel or the **- / ⊹ / +** buttons zoom the plan in and out; on a touch screen the two-finger pinch works. Zoomed out you see the whole plan, zoomed in you see the details, and everything stays crisp. The zoom level is remembered separately for each space. -![Приближённый план — всё остаётся чётким](docs/images/09-zoom.png) +![Zoomed-in plan — everything stays crisp](docs/images/09-zoom.png) -### Шаг 5. Расставьте значки по местам +### Step 5. Put the icons in their places -Значки устройств можно **перетаскивать мышью в любой момент** — отдельный «режим правки» включать не нужно. Позиции сохраняются на сервере и одинаковы во всех браузерах и устройствах. Кнопка **↺** в шапке возвращает автоматическую раскладку. +Device icons can be **dragged with the mouse at any time** — no separate "edit mode" needs to be enabled. Positions are saved on the server and are identical in all browsers and devices. The **↺** button in the header restores the automatic layout. -![Перетаскивание значков — доступно всегда](docs/images/06-edit.png) +![Dragging icons — available at all times](docs/images/06-edit.png) -### Шаг 6. Добавление своих устройств вручную +### Step 6. Adding your own devices manually -Не всё нужно оставлять на автоматику. Кнопкой **+** в шапке можно поставить на план любое устройство, группу или **виртуальную точку** (например, «Вентиль на вводе», которого нет как устройства). Задайте имя, иконку, модель, ссылку, описание и при желании приложите **PDF-инструкцию**. +Not everything has to be left to the automation. With the **+** button in the header you can place any device, group or a **virtual point** on the plan (for example, an "Inlet valve" that does not exist as a device). Set a name, icon, model, link, description and, if you wish, attach a **PDF manual**. -![Добавление устройства вручную](docs/images/07-marker-dialog.png) +![Adding a device manually](docs/images/07-marker-dialog.png) --- -## Удаление +## Uninstalling -1. Уберите карточку (или вкладку с планом) из дашборда. -2. **Настройки → Устройства и службы → House Plan → Удалить** запись интеграции. -3. Удалите интеграцию из **HACS** (или папку `custom_components/houseplan` при ручной установке) и перезапустите Home Assistant. -4. При желании удалите сохранённые данные плана: файлы `config/houseplan/` (подложки и вложения) и записи `houseplan.config` / `houseplan.layout` в каталоге `config/.storage`. +1. Remove the card (or the tab with the plan) from the dashboard. +2. **Settings → Devices & Services → House Plan → Delete** the integration entry. +3. Remove the integration from **HACS** (or delete the `custom_components/houseplan` folder if installed manually) and restart Home Assistant. +4. Optionally delete the saved plan data: the `config/houseplan/` files (backgrounds and attachments) and the `houseplan.config` / `houseplan.layout` entries in the `config/.storage` directory. --- -## Часто задаваемые вопросы +## Frequently asked questions -**Нужно ли что-то писать в YAML?** Нет. Единственная строчка — это добавление карточки на дашборд; всё остальное делается мышкой. +**Do I need to write anything in YAML?** No. The only line is adding the card to the dashboard; everything else is done with the mouse. -**Мои устройства не появились на плане.** Устройство появляется, только если его зона в Home Assistant привязана к нарисованной комнате. Проверьте, что у устройства задана комната (Настройки → Устройства), а комната обведена и привязана к этой зоне. Если устройство есть, но скрыто курированием (мосты, служебные, дубликаты) — включите в шапке кнопку **👁 «Показать все устройства»**. +**My devices did not appear on the plan.** A device appears only if its Home Assistant area is bound to a drawn room. Check that the device has a room assigned (Settings → Devices) and that the room is outlined and bound to that area. If the device exists but is hidden by curation (bridges, service records, duplicates) — enable the **👁 "Show all devices"** button in the header. -**Можно ли скрыть лишнее устройство или переименовать его?** Да — кликните по устройству на плане и в его карточке нажмите «Редактировать»: там можно сменить имя, иконку, модель или скрыть значок. +**Can I hide an unwanted device or rename it?** Yes — click the device on the plan and press "Edit" in its card: there you can change the name, icon, model or hide the icon. -**Данные хранятся в облаке?** Нет. Всё хранится локально в вашем Home Assistant. +**Is the data stored in the cloud?** No. Everything is stored locally in your Home Assistant. --- -

Скриншоты сделаны на реальной конфигурации Home Assistant.

+

Screenshots were taken on a real Home Assistant configuration.

diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..58bb0ad --- /dev/null +++ b/README.ru.md @@ -0,0 +1,157 @@ +# 🏠 House Plan — интерактивный план дома для Home Assistant + +**Живая карта вашего дома прямо в Home Assistant: этажи, комнаты и устройства на настоящем плане — с реальными состояниями, температурой и уровнем сигнала. Всё настраивается мышкой, без единой строчки YAML.** + +![Интерактивный план дома с устройствами](docs/images/01-plan.png) + +🇬🇧 [English documentation](README.md) + +--- + +## Что это и зачем + +House Plan показывает ваш умный дом так, как он выглядит на самом деле — на плане этажей. Вместо длинных списков сущностей вы видите комнаты и устройства на своих местах: где протечка, какая температура в детской, включён ли свет в прихожей, открыты ли ворота. + +Это удобно, когда: + +- устройств много, и списками пользоваться неудобно; +- нужно быстро понять состояние дома «одним взглядом»; +- хочется отдать доступ близким — по картинке разберётся любой; +- вы хотите красивый обзорный экран для настенного планшета. + +Интеграция состоит из двух частей, которые ставятся вместе: + +- **карточка Lovelace** `houseplan-card` — сам интерактивный план; +- **серверный компонент** — хранит разметку комнат и позиции иконок в Home Assistant, поэтому план одинаков во всех браузерах и на всех устройствах. + +--- + +## Чем отличается от аналогов + +Обычно план дома в Home Assistant делают через `picture-elements`, `ha-floorplan` и подобные решения. Там приходится вручную писать YAML, вычислять координаты каждой иконки и заново править конфиг при каждом изменении. House Plan устроен иначе: + +| | House Plan | Обычные решения (picture-elements / ha-floorplan) | +|---|---|---| +| **Настройка** | Полностью через интерфейс, мышкой | Ручной YAML и правка кода | +| **Добавление устройств** | Автоматически по комнатам | Каждую сущность вписываете руками | +| **Координаты иконок** | Перетаскиваете мышью | Считаете пиксели и пишете в конфиг | +| **Разметка комнат** | Встроенный редактор контуров | Рисуете в стороннем редакторе SVG | +| **Хранение** | На сервере HA (общее для всех устройств) | В YAML дашборда | +| **Масштаб** | Плавный зум, всё остаётся чётким (вектор) | Обычно фиксированная картинка | + +Ключевые преимущества коротко: + +- **Никакого кода.** Всё — пространства, комнаты, устройства — настраивается кликами. +- **Автоматическое добавление устройств.** Обвели комнату и привязали её к зоне Home Assistant — устройства этой зоны сами появляются на плане. +- **Ручное добавление своих.** Любое устройство, группу или даже «виртуальную» точку можно поставить на план вручную, задать имя, иконку, модель, ссылку и приложить PDF-инструкцию. +- **Живые состояния.** Температура, уровень сигнала Zigbee, вкл/выкл, открыто/закрыто — всё обновляется в реальном времени. +- **Чёткий зум.** Приближение не «мылит» картинку: план, подписи и иконки остаются векторно-чёткими на любом масштабе. + +--- + +## Установка + +### Через HACS (рекомендуется) + +1. Откройте **HACS → меню (⋮) → Custom repositories**. +2. Вставьте URL этого репозитория, категория — **Integration**, и нажмите **Add**. +3. Найдите в списке **House Plan**, установите и **перезапустите Home Assistant**. +4. Перейдите в **Настройки → Устройства и службы → Добавить интеграцию** и выберите **House Plan**. + +Карточка подключается автоматически — добавлять ресурс Lovelace вручную не нужно. + +### Вручную + +1. Скопируйте папку `custom_components/houseplan` в каталог `config/custom_components` вашего Home Assistant. +2. Перезапустите Home Assistant. +3. Добавьте интеграцию: **Настройки → Устройства и службы → Добавить интеграцию → House Plan**. + +### Добавление экрана с планом + +Создайте новую вкладку дашборда (удобнее всего — в режиме «Панель»/Panel) и добавьте карточку: + +```yaml +type: custom:houseplan-card +title: План дома +``` + +Больше ничего указывать не нужно — всё остальное настраивается прямо на экране. + +--- + +## Как пользоваться + +### Шаг 1. Добавьте пространство (этаж) + +При первом открытии план ещё пуст — House Plan сразу предложит создать первое пространство. + +![Пустой план — предложение добавить пространство](docs/images/02-onboarding-empty.png) + +В диалоге задайте **название** (например, «1 этаж») и **загрузите подложку** — картинку плана этажа в формате SVG, PNG или JPG. Оба поля обязательны: без плана кнопка «Сохранить» неактивна. + +![Диалог создания пространства](docs/images/03-space-dialog.png) + +> 💡 Подложку можно нарисовать в любом планировщике (например, РЕМПЛАННЕР) или сфотографировать бумажный план. Лучше всего SVG — он остаётся чётким при увеличении. + +Позже можно добавить сколько угодно пространств (этажи, двор, гараж) кнопкой **+** рядом со вкладками. + +### Шаг 2. Обведите комнаты + +После добавления первого пространства карточка сама переходит в режим разметки. Кликайте по точкам сетки, соединяя их линиями, и замкните контур комнаты кликом по первой точке. + +Как только контур замкнётся, появится окно сохранения комнаты. Здесь нужно **привязать комнату к зоне Home Assistant** — именно это включает автоматику. Для служебных помещений без устройств (холл, сауна) есть кнопка **«Без зоны»**. + +![Разметка комнаты и её сохранение](docs/images/05-room-dialog.png) + +### Шаг 3. Устройства появляются сами + +Как только вы сохранили комнату с привязкой к зоне, **устройства этой зоны автоматически расставляются внутри контура**. Берутся те же устройства, что показаны на странице **Настройки → Устройства → (фильтр по нужной комнате)** — только осмысленные, без служебных записей, мостов и дубликатов. + +По умолчанию на план попадают только осмысленные устройства — служебные записи, мосты и дубликаты отфильтрованы. Если нужно видеть **вообще все** устройства зоны, включите в шапке кнопку **👁 «Показать все устройства»**. + +Дальше можно просто пользоваться планом: клик по иконке открывает карточку устройства с моделью, ссылкой и кнопкой перехода в Home Assistant. + +![Карточка устройства по клику](docs/images/08-info.png) + +### Шаг 4. Масштаб + +Колесо мыши или кнопки **- / ⊹ / +** приближают и отдаляют план; на сенсорном экране работает «щипок» двумя пальцами. При отдалении виден весь план целиком, при приближении — детали, и всё остаётся чётким. Масштаб запоминается отдельно для каждого пространства. + +![Приближённый план — всё остаётся чётким](docs/images/09-zoom.png) + +### Шаг 5. Расставьте значки по местам + +Значки устройств можно **перетаскивать мышью в любой момент** — отдельный «режим правки» включать не нужно. Позиции сохраняются на сервере и одинаковы во всех браузерах и устройствах. Кнопка **↺** в шапке возвращает автоматическую раскладку. + +![Перетаскивание значков — доступно всегда](docs/images/06-edit.png) + +### Шаг 6. Добавление своих устройств вручную + +Не всё нужно оставлять на автоматику. Кнопкой **+** в шапке можно поставить на план любое устройство, группу или **виртуальную точку** (например, «Вентиль на вводе», которого нет как устройства). Задайте имя, иконку, модель, ссылку, описание и при желании приложите **PDF-инструкцию**. + +![Добавление устройства вручную](docs/images/07-marker-dialog.png) + +--- + +## Удаление + +1. Уберите карточку (или вкладку с планом) из дашборда. +2. **Настройки → Устройства и службы → House Plan → Удалить** запись интеграции. +3. Удалите интеграцию из **HACS** (или папку `custom_components/houseplan` при ручной установке) и перезапустите Home Assistant. +4. При желании удалите сохранённые данные плана: файлы `config/houseplan/` (подложки и вложения) и записи `houseplan.config` / `houseplan.layout` в каталоге `config/.storage`. + +--- + +## Часто задаваемые вопросы + +**Нужно ли что-то писать в YAML?** Нет. Единственная строчка — это добавление карточки на дашборд; всё остальное делается мышкой. + +**Мои устройства не появились на плане.** Устройство появляется, только если его зона в Home Assistant привязана к нарисованной комнате. Проверьте, что у устройства задана комната (Настройки → Устройства), а комната обведена и привязана к этой зоне. Если устройство есть, но скрыто курированием (мосты, служебные, дубликаты) — включите в шапке кнопку **👁 «Показать все устройства»**. + +**Можно ли скрыть лишнее устройство или переименовать его?** Да — кликните по устройству на плане и в его карточке нажмите «Редактировать»: там можно сменить имя, иконку, модель или скрыть значок. + +**Данные хранятся в облаке?** Нет. Всё хранится локально в вашем Home Assistant. + +--- + +

Скриншоты сделаны на реальной конфигурации Home Assistant.

diff --git a/RELEASE_NOTES_v1.9.3.md b/RELEASE_NOTES_v1.9.3.md deleted file mode 100644 index 36243f2..0000000 --- a/RELEASE_NOTES_v1.9.3.md +++ /dev/null @@ -1,9 +0,0 @@ -## v1.9.3 — 2026-07-05 (аудит + рефакторинг + тесты) -- Чистые функции `fitView` (contain-прямоугольник вьюпорта) и `declump` (расталкивание значков) - вынесены из карточки в `logic.ts` — покрыты юнит-тестами (было 9 → стало 14 тестов). -- `_lqiFor` и `_roomLqi` теперь используют общую `averageLqi` (убрано дублирование усреднения). -- Удалён мёртвый код после отказа от отдельного режима правки: поле `_edit` (писалось, но не - читалось), методы `_renderEditbar` и `_applyXY` (не вызывались). -- Поведение не изменилось — только структура и покрытие тестами. Проверено: tsc, сборка, - 14 фронт-тестов + 10 бэк-тестов зелёные, headless-дымтест (рендер/зум/устройства/declump) чист. - diff --git a/custom_components/houseplan/__init__.py b/custom_components/houseplan/__init__.py index fccf903..b8a06db 100755 --- a/custom_components/houseplan/__init__.py +++ b/custom_components/houseplan/__init__.py @@ -1,4 +1,4 @@ -"""House Plan: серверная конфигурация плана дома + раздача Lovelace-карточки.""" +"""House Plan: server-side house plan configuration + serving the Lovelace card.""" from __future__ import annotations import logging @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config) -> bool: - """Регистрируем WS-команды, HTTP-загрузку и хранилища на старте.""" + """Register WS commands, the HTTP upload and the stores on startup.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN]["store"] = Store(hass, STORAGE_VERSION, STORAGE_KEY) hass.data[DOMAIN]["config_store"] = Store(hass, STORAGE_VERSION, STORAGE_CONFIG_KEY) @@ -39,7 +39,7 @@ async def async_setup(hass: HomeAssistant, config) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Config entry: статика фронтенда и планов + авто-подключение JS.""" + """Config entry: static frontend and plan files + auto-registration of the JS.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN]["entry"] = entry entry.async_on_unload(entry.add_update_listener(_update_listener)) @@ -60,20 +60,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: static_paths.append(StaticPathConfig(PLANS_URL, str(plans_path), cache_headers=True)) static_paths.append(StaticPathConfig(FILES_URL, str(files_path), cache_headers=True)) await hass.http.async_register_static_paths(static_paths) - except ImportError: # старые версии HA + except ImportError: # older HA versions if card_path.exists(): hass.http.register_static_path(FRONTEND_URL, str(card_path), cache_headers=False) hass.http.register_static_path(PLANS_URL, str(plans_path), cache_headers=True) hass.http.register_static_path(FILES_URL, str(files_path), cache_headers=True) if not card_path.exists(): - _LOGGER.warning("houseplan-card.js не найден рядом с интеграцией: %s", card_path) + _LOGGER.warning("houseplan-card.js not found next to the integration: %s", card_path) return True - # Подключаем карточку. Предпочтительно — как Lovelace-ресурс (его фронтенд ДОЖИДАЕТСЯ - # перед рендером дашбордов, поэтому карточка доступна даже на холодном старте мобильного - # приложения). Если реестр ресурсов недоступен (YAML-режим Lovelace, старые версии) — - # откатываемся на extra_module_url. + # Register the card. Preferably as a Lovelace resource (the frontend WAITS for those + # before rendering dashboards, so the card is available even on a cold start of the mobile + # app). If the resource registry is unavailable (Lovelace YAML mode, older versions) — + # fall back to extra_module_url. module_url = f"{FRONTEND_URL}?v={VERSION}" if not await _register_lovelace_resource(hass, module_url): add_extra_js_url(hass, module_url) @@ -81,10 +81,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _register_lovelace_resource(hass: HomeAssistant, module_url: str) -> bool: - """Зарегистрировать (или обновить) карточку в реестре Lovelace-ресурсов. + """Register (or update) the card in the Lovelace resource registry. - Возвращает True при успехе. Пишем идемпотентно: если ресурс с нашим путём уже есть — - обновляем URL при смене версии; отсутствует — создаём. Any-except → False (фолбэк на JS). + Returns True on success. Writes idempotently: if a resource with our path already exists — + update the URL on a version change; if absent — create it. Any-except → False (fallback to JS). """ try: lovelace = hass.data.get("lovelace") @@ -93,13 +93,13 @@ async def _register_lovelace_resource(hass: HomeAssistant, module_url: str) -> b resources = lovelace.get("resources") if resources is None: return False - # реестр ресурсов должен быть загружен + # the resource registry must be loaded if hasattr(resources, "loaded") and not resources.loaded: await resources.async_load() resources.loaded = True elif hasattr(resources, "async_get_info"): await resources.async_get_info() - # только storage-режим позволяет создавать элементы + # only storage mode allows creating items if not hasattr(resources, "async_create_item"): return False base = FRONTEND_URL @@ -113,10 +113,10 @@ async def _register_lovelace_resource(hass: HomeAssistant, module_url: str) -> b await resources.async_update_item(item["id"], {"url": module_url}) return True await resources.async_create_item({"res_type": "module", "url": module_url}) - _LOGGER.debug("House Plan card зарегистрирована как Lovelace-ресурс: %s", module_url) + _LOGGER.debug("House Plan card registered as a Lovelace resource: %s", module_url) return True - except Exception as err: # noqa: BLE001 — любой сбой → фолбэк - _LOGGER.debug("Не удалось зарегистрировать Lovelace-ресурс (%s), фолбэк на extra_module_url", err) + except Exception as err: # noqa: BLE001 — any failure → fallback + _LOGGER.debug("Failed to register the Lovelace resource (%s), falling back to extra_module_url", err) return False diff --git a/custom_components/houseplan/config_flow.py b/custom_components/houseplan/config_flow.py index c5d697d..4696212 100755 --- a/custom_components/houseplan/config_flow.py +++ b/custom_components/houseplan/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow: одна запись без параметров.""" +"""Config flow: a single entry with no parameters.""" from __future__ import annotations import voluptuous as vol @@ -9,7 +9,7 @@ from .const import CONF_ADMIN_ONLY, DOMAIN class HouseplanConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Установка в один шаг.""" + """One-step setup.""" VERSION = 1 @@ -29,7 +29,7 @@ class HouseplanConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class HouseplanOptionsFlow(config_entries.OptionsFlow): - """Опция: правка раскладки только администраторами.""" + """Option: layout editing by administrators only.""" async def async_step_init(self, user_input=None): if user_input is not None: diff --git a/custom_components/houseplan/const.py b/custom_components/houseplan/const.py index f7c3d43..3a64ba3 100755 --- a/custom_components/houseplan/const.py +++ b/custom_components/houseplan/const.py @@ -1,4 +1,4 @@ -"""Константы интеграции House Plan.""" +"""Constants of the House Plan integration.""" DOMAIN = "houseplan" STORAGE_KEY = f"{DOMAIN}.layout" @@ -6,11 +6,11 @@ STORAGE_CONFIG_KEY = f"{DOMAIN}.config" STORAGE_VERSION = 1 FRONTEND_URL = "/houseplan_files/houseplan-card.js" PLANS_URL = "/houseplan_files/plans" -PLANS_DIR = "houseplan/plans" # относительно каталога конфигурации HA +PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory FILES_URL = "/houseplan_files/files" FILES_DIR = "houseplan/files" CONF_ADMIN_ONLY = "admin_only" -VERSION = "1.10.0" +VERSION = "1.11.0" DEFAULT_CONFIG: dict = { "spaces": [], diff --git a/custom_components/houseplan/frontend/houseplan-card.js b/custom_components/houseplan/frontend/houseplan-card.js index 5499d40..d7f07c3 100755 --- a/custom_components/houseplan/frontend/houseplan-card.js +++ b/custom_components/houseplan/frontend/houseplan-card.js @@ -1,10 +1,10 @@ -const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),s=new WeakMap;let a=class{constructor(t,e,s){if(this._$cssResult$=!0,s!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const i=this.t;if(e&&void 0===t){const e=void 0!==i&&1===i.length;e&&(t=s.get(i)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&s.set(i,t))}return t}toString(){return this.cssText}};const o=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new a("string"==typeof t?t:t+"",void 0,i))(e)})(t):t,{is:n,defineProperty:r,getOwnPropertyDescriptor:l,getOwnPropertyNames:c,getOwnPropertySymbols:h,getPrototypeOf:d}=Object,p=globalThis,u=p.trustedTypes,_=u?u.emptyScript:"",g=p.reactiveElementPolyfillSupport,m=(t,e)=>t,f={toAttribute(t,e){switch(e){case Boolean:t=t?_:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},v=(t,e)=>!n(t,e),b={attribute:!0,type:String,converter:f,reflect:!1,useDefault:!1,hasChanged:v};Symbol.metadata??=Symbol("metadata"),p.litPropertyMetadata??=new WeakMap;let y=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),s=this.getPropertyDescriptor(t,i,e);void 0!==s&&r(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){const{get:s,set:a}=l(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:s,set(e){const o=s?.call(this);a?.call(this,e),this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(m("elementProperties")))return;const t=d(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(m("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(m("properties"))){const t=this.properties,e=[...c(t),...h(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(o(t))}else void 0!==t&&e.push(o(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,s)=>{if(e)i.adoptedStyleSheets=s.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of s){const s=document.createElement("style"),a=t.litNonce;void 0!==a&&s.setAttribute("nonce",a),s.textContent=e.cssText,i.appendChild(s)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),s=this.constructor._$Eu(t,i);if(void 0!==s&&!0===i.reflect){const a=(void 0!==i.converter?.toAttribute?i.converter:f).toAttribute(e,i.type);this._$Em=t,null==a?this.removeAttribute(s):this.setAttribute(s,a),this._$Em=null}}_$AK(t,e){const i=this.constructor,s=i._$Eh.get(t);if(void 0!==s&&this._$Em!==s){const t=i.getPropertyOptions(s),a="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:f;this._$Em=s;const o=a.fromAttribute(e,t.type);this[s]=o??this._$Ej?.get(s)??o,this._$Em=null}}requestUpdate(t,e,i,s=!1,a){if(void 0!==t){const o=this.constructor;if(!1===s&&(a=this[t]),i??=o.getPropertyOptions(t),!((i.hasChanged??v)(a,e)||i.useDefault&&i.reflect&&a===this._$Ej?.get(t)&&!this.hasAttribute(o._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:s,wrapped:a},o){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,o??e??this[t]),!0!==a||void 0!==o)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===s&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,s=this[e];!0!==t||this._$AL.has(e)||void 0===s||this.C(e,void 0,i,s)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};y.elementStyles=[],y.shadowRootOptions={mode:"open"},y[m("elementProperties")]=new Map,y[m("finalized")]=new Map,g?.({ReactiveElement:y}),(p.reactiveElementVersions??=[]).push("2.1.2");const x=globalThis,$=t=>t,w=x.trustedTypes,k=w?w.createPolicy("lit-html",{createHTML:t=>t}):void 0,S="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,A="?"+C,z=`<${A}>`,M=document,D=()=>M.createComment(""),P=t=>null===t||"object"!=typeof t&&"function"!=typeof t,E=Array.isArray,R="[ \t\n\f\r]",T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,O=/-->/g,U=/>/g,H=RegExp(`>|${R}(?:([^\\s"'>=/]+)(${R}*=${R}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),I=/'/g,N=/"/g,q=/^(?:script|style|textarea|title)$/i,L=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),j=L(1),F=L(2),W=Symbol.for("lit-noChange"),B=Symbol.for("lit-nothing"),K=new WeakMap,V=M.createTreeWalker(M,129);function Z(t,e){if(!E(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==k?k.createHTML(e):e}const Y=(t,e)=>{const i=t.length-1,s=[];let a,o=2===e?"":3===e?"":"",n=T;for(let e=0;e"===l[0]?(n=a??T,c=-1):void 0===l[1]?c=-2:(c=n.lastIndex-l[2].length,r=l[1],n=void 0===l[3]?H:'"'===l[3]?N:I):n===N||n===I?n=H:n===O||n===U?n=T:(n=H,a=void 0);const d=n===H&&t[e+1].startsWith("/>")?" ":"";o+=n===T?i+z:c>=0?(s.push(r),i.slice(0,c)+S+i.slice(c)+C+d):i+C+(-2===c?e:d)}return[Z(t,o+(t[i]||"")+(2===e?"":3===e?"":"")),s]};class X{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let a=0,o=0;const n=t.length-1,r=this.parts,[l,c]=Y(t,e);if(this.el=X.createElement(l,i),V.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(s=V.nextNode())&&r.length0){s.textContent=w?w.emptyScript:"";for(let i=0;iE(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==B&&P(this._$AH)?this._$AA.nextSibling.data=t:this.T(M.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,s="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=X.createElement(Z(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(e);else{const t=new G(s,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=K.get(t.strings);return void 0===e&&K.set(t.strings,e=new X(t)),e}k(t){E(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,s=0;for(const a of t)s===e.length?e.push(i=new Q(this.O(D()),this.O(D()),this,this.options)):i=e[s],i._$AI(a),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=B}_$AI(t,e=this,i,s){const a=this.strings;let o=!1;if(void 0===a)t=J(this,t,e,0),o=!P(t)||t!==this._$AH&&t!==W,o&&(this._$AH=t);else{const s=t;let n,r;for(t=a[0],n=0;n{const s=i?.renderBefore??e;let a=s._$litPart$;if(void 0===a){const t=i?.renderBefore??null;s._$litPart$=a=new Q(e.insertBefore(D(),t),t,void 0,i??{})}return a._$AI(t),a})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return W}}rt._$litElement$=!0,rt.finalized=!0,nt.litElementHydrateSupport?.({LitElement:rt});const lt=nt.litElementPolyfillSupport;lt?.({LitElement:rt}),(nt.litElementVersions??=[]).push("4.2.2");const ct=new Set(["hacs","sun","backup","hassio","met","telegram_bot","mobile_app","systemmonitor","better_thermostat","adaptive_lighting","yandex_pogoda","upnp_serial_number"]),ht=[[/протечк/,"mdi:water-alert"],[/клапан/,"mdi:pipe-valve"],[/дым/,"mdi:smoke-detector"],[/термоголов/,"mdi:radiator"],[/температ/,"mdi:thermometer"],[/qingping|air monitor|молекул/,"mdi:air-filter"],[/штор/,"mdi:roller-shade"],[/розетк|plug/,"mdi:power-socket-de"],[/выключат|switch/,"mdi:light-switch"],[/лампа|лампочк|bulb|gx53|светильник|rgb/,"mdi:lightbulb"],[/камер|camera/,"mdi:cctv"],[/замок|ttlock|lock|sn609|sn9161/,"mdi:lock"],[/ворота|garage/,"mdi:garage-variant"],[/калитк|door|открыт/,"mdi:door"],[/счётчик|счетчик|kws|meter/,"mdi:meter-electric"],[/вводный автомат|breaker|wifimcbn/,"mdi:electric-switch"],[/myheat|котёл|котел|boiler|отоплен/,"mdi:water-boiler"],[/холодильник|fridge/,"mdi:fridge"],[/стиральн|washer/,"mdi:washing-machine"],[/сушилк|dryer/,"mdi:tumble-dryer"],[/пылесос|vacuum|dreame/,"mdi:robot-vacuum"],[/soundbar|колонк|станц/,"mdi:soundbar"],[/tv|телевизор|hyundaitv|mitv/,"mdi:television"],[/keenetic|роутер|router/,"mdi:router-wireless"],[/ибп|ups|kirpich/,"mdi:battery-charging-high"],[/slzb|координат|zigbee/,"mdi:zigbee"]];function dt(t,e){const i=((t||"")+" "+(e||"")).toLowerCase();for(const[t,e]of ht)if(t.test(i))return e;return"mdi:chip"}const pt=["light","switch","cover","valve","lock","climate","fan","media_player","camera","vacuum","humidifier","water_heater","alarm_control_panel","sensor","binary_sensor","event","button","number","select","update"];function ut(t){const e=Math.max(0,Math.min(120,(t-40)/140*120));return`hsl(${Math.round(e)}, 85%, 55%)`}function _t(t,e){return Math.round(t/e)*e}function gt(t){return t.length?Math.round(t.reduce((t,e)=>t+e,0)/t.length):null}function mt(t,e){if(e>t[2]/t[3]){const i=t[3],s=t[3]*e;return{x:t[0]-(s-t[2])/2,y:t[1],w:s,h:i}}const i=t[2],s=t[2]/e;return{x:t[0],y:t[1]-(s-t[3])/2,w:i,h:s}}function ft(t,e,i,s){if(t.length<2)return;const a=e.x+s,o=e.x+e.w-s,n=e.y+s,r=e.y+e.h-s;for(let e=0;e<60;e++){let e=!1;for(let s=0;s({eid:e,reg:t.entities[e],st:t.states[e]})).filter(t=>t.reg&&!t.reg.hidden),a=s.filter(t=>!t.reg.entity_category),o=a.length?a:s;if("mdi:thermometer"===i||"mdi:air-filter"===i){const e=o.find(e=>yt(t,e.eid));if(e)return e.eid}for(const t of pt){const e=o.find(e=>e.eid.split(".")[0]===t);if(e)return e.eid}return o[0]?.eid}function $t(t,e){const i=[];for(const s of e){const e=t.states[s];if(!e)continue;const a=(e.attributes?.unit_of_measurement||"").toLowerCase();if(/_(linkquality|lqi)$/.test(s)||"lqi"===a){const t=parseFloat(e.state);isNaN(t)||i.push(t);continue}const o=e.attributes?.linkquality??e.attributes?.lqi;if(null!=o){const t=parseFloat(o);isNaN(t)||i.push(t)}}return gt(i)}function wt(t,e){for(const i of e){if(!yt(t,i))continue;const e=t.states[i];if(!e)continue;const s=parseFloat(e.state);if(!isNaN(s))return Math.round(10*s)/10}return null}function kt(t,e){t.marker=e,e.name&&(t.name=e.name),e.icon&&(t.icon=e.icon),null!=e.model&&(t.model=e.model),t.link=e.link??null,t.description=e.description??null,t.pdfs=e.pdfs||[]}function St(t){const{hass:e,areaToSpace:i,markers:s,settings:a,excluded:o,showAll:n,firstSpaceId:r}=t,l=!1!==a.group_lights,c=function(t,e){if(!e)return[];const i=[];for(const[e,s]of Object.entries(t.entities)){if(!e.startsWith("light.")||s.hidden)continue;let a=null;if("group"===s.platform)a=s.area_id||null;else{if(!s.device_id)continue;{const e=t.devices[s.device_id];if("Group"!==e?.model)continue;a=e.area_id||s.area_id||null}}if(!a)continue;const o=t.states[e];i.push({eid:e,name:s.name||o?.attributes?.friendly_name||e,area:a})}return i}(e,l),h=new Set(c.map(t=>t.area)),d=function(t){const e={};for(const[i,s]of Object.entries(t.entities))s?.device_id&&(e[s.device_id]=e[s.device_id]||[]).push(i);return e}(e),p=new Set;for(const t of s){const[e,i]=t.binding.split(":");"device"!==e&&"entity"!==e||!i||p.add(t.binding)}const u=(t,e)=>s.find(i=>i.binding===t+":"+e),_={},g=[];for(const t of Object.values(e.devices)){const s=t.area_id;if(!s||!i[s])continue;if("service"===t.entry_type)continue;if(p.has("device:"+t.id))continue;const a=u("device",t.id);if(a&&a.hidden)continue;const r=d[t.id]||[],c=bt(e,t,r);if(!n){if(o.has(c))continue;if("Group"===t.model)continue;if(/scene/i.test(t.model||""))continue;if(/bridge/i.test((t.model||"")+(t.name||"")))continue;if("myheat"===c&&t.via_device_id)continue}const m=(t.name_by_user||t.name||"без имени").trim(),f=m+"|"+s;let v=dt(m,t.model);if(r.some(t=>t.startsWith("lock."))&&(v="mdi:lock"),!n&&l&&"mdi:lightbulb"===v&&h.has(s))continue;_[f]=(_[f]||0)+1;const b=_[f]>1?m+" "+_[f]:m,y={id:t.id,name:b,model:t.model||"",area:s,space:i[s],icon:v,entities:r,bindingKind:"device",bindingRef:t.id,pdfs:[]};y.primary=xt(e,r,v),"mdi:thermometer"!==v&&"mdi:air-filter"!==v||(y.temp=wt(e,r)),g.push(y)}for(const t of c)i[t.area]&&(p.has("entity:"+t.eid)||g.push({id:"lg_"+t.eid,name:t.name,model:"группа света",area:t.area,space:i[t.area],icon:"mdi:lightbulb-group",entities:[t.eid],primary:t.eid,bindingKind:"entity",bindingRef:t.eid,pdfs:[]}));for(const t of s){if(t.hidden)continue;const[s,a]=t.binding.split(":");if("device"===s){const s=e.devices[a],o=t.area||s?.area_id||"",n=o&&i[o]||t.space||r,l=s&&d[s.id]||[];let c=s?dt(s.name_by_user||s.name||"",s.model):"mdi:help-circle";l.some(t=>t.startsWith("lock."))&&(c="mdi:lock");const h={id:t.id,name:s?.name_by_user||s?.name||"устройство",model:s?.model||"",area:o,space:n,icon:c,entities:l,bindingKind:"device",bindingRef:a};h.primary=xt(e,l,c),"mdi:thermometer"!==c&&"mdi:air-filter"!==c||(h.temp=wt(e,l)),kt(h,t),g.push(h)}else if("entity"===s){const s=e.entities[a],o=t.area||s?.area_id||s?.device_id&&e.devices[s.device_id]?.area_id||"",n=o&&i[o]||t.space||r,l=e.states[a],c={id:t.id,name:s?.name||l?.attributes?.friendly_name||a,model:"",area:o,space:n,icon:"mdi:shape-outline",entities:[a],primary:a,bindingKind:"entity",bindingRef:a};kt(c,t),g.push(c)}else{const e=t.area||"",s=t.space||e&&i[e]||r,a={id:t.id,name:t.name||"виртуальное устройство",model:t.model||"",area:e,space:s,icon:t.icon||"mdi:map-marker",entities:[],bindingKind:"virtual",virtual:!0};kt(a,t),g.push(a)}}return g}const Ct={title:"Заголовок",default_floor:"Пространство по умолчанию",icon_size:"Размер иконок, % ширины плана",show_temperature:"Показывать температуру",live_states:"Живые состояния (вкл/выкл, открыто…)",show_signal:"Показывать сигнал zigbee (LQI)"};class At extends rt{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1}setConfig(t){this._config=t}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const t=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(t?.config?.spaces||[]).map(t=>({value:t.id,label:t.title||t.id}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _schema(){const t=this._spaces||[];return[{name:"title",selector:{text:{}}},t.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:t}}}:{name:"default_floor",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:"show_signal",selector:{boolean:{}}}]}render(){return this.hass&&this._config?(this._loadSpaces(),j`t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new a("string"==typeof t?t:t+"",void 0,i))(e)})(t):t,{is:r,defineProperty:n,getOwnPropertyDescriptor:l,getOwnPropertyNames:c,getOwnPropertySymbols:h,getPrototypeOf:d}=Object,p=globalThis,_=p.trustedTypes,u=_?_.emptyScript:"",m=p.reactiveElementPolyfillSupport,g=(t,e)=>t,f={toAttribute(t,e){switch(e){case Boolean:t=t?u:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},v=(t,e)=>!r(t,e),b={attribute:!0,type:String,converter:f,reflect:!1,useDefault:!1,hasChanged:v};Symbol.metadata??=Symbol("metadata"),p.litPropertyMetadata??=new WeakMap;let y=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),s=this.getPropertyDescriptor(t,i,e);void 0!==s&&n(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){const{get:s,set:a}=l(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:s,set(e){const o=s?.call(this);a?.call(this,e),this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const t=d(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const t=this.properties,e=[...c(t),...h(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(o(t))}else void 0!==t&&e.push(o(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const i=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((i,s)=>{if(e)i.adoptedStyleSheets=s.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of s){const s=document.createElement("style"),a=t.litNonce;void 0!==a&&s.setAttribute("nonce",a),s.textContent=e.cssText,i.appendChild(s)}})(i,this.constructor.elementStyles),i}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),s=this.constructor._$Eu(t,i);if(void 0!==s&&!0===i.reflect){const a=(void 0!==i.converter?.toAttribute?i.converter:f).toAttribute(e,i.type);this._$Em=t,null==a?this.removeAttribute(s):this.setAttribute(s,a),this._$Em=null}}_$AK(t,e){const i=this.constructor,s=i._$Eh.get(t);if(void 0!==s&&this._$Em!==s){const t=i.getPropertyOptions(s),a="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:f;this._$Em=s;const o=a.fromAttribute(e,t.type);this[s]=o??this._$Ej?.get(s)??o,this._$Em=null}}requestUpdate(t,e,i,s=!1,a){if(void 0!==t){const o=this.constructor;if(!1===s&&(a=this[t]),i??=o.getPropertyOptions(t),!((i.hasChanged??v)(a,e)||i.useDefault&&i.reflect&&a===this._$Ej?.get(t)&&!this.hasAttribute(o._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:s,wrapped:a},o){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,o??e??this[t]),!0!==a||void 0!==o)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===s&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,s=this[e];!0!==t||this._$AL.has(e)||void 0===s||this.C(e,void 0,i,s)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};y.elementStyles=[],y.shadowRootOptions={mode:"open"},y[g("elementProperties")]=new Map,y[g("finalized")]=new Map,m?.({ReactiveElement:y}),(p.reactiveElementVersions??=[]).push("2.1.2");const $=globalThis,x=t=>t,w=$.trustedTypes,k=w?w.createPolicy("lit-html",{createHTML:t=>t}):void 0,S="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,A="?"+C,z=`<${A}>`,D=document,M=()=>D.createComment(""),P=t=>null===t||"object"!=typeof t&&"function"!=typeof t,E=Array.isArray,R="[ \t\n\f\r]",T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,O=/-->/g,U=/>/g,H=RegExp(`>|${R}(?:([^\\s"'>=/]+)(${R}*=${R}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),N=/'/g,q=/"/g,I=/^(?:script|style|textarea|title)$/i,L=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),F=L(1),j=L(2),W=Symbol.for("lit-noChange"),B=Symbol.for("lit-nothing"),K=new WeakMap,V=D.createTreeWalker(D,129);function Z(t,e){if(!E(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==k?k.createHTML(e):e}const Y=(t,e)=>{const i=t.length-1,s=[];let a,o=2===e?"":3===e?"":"",r=T;for(let e=0;e"===l[0]?(r=a??T,c=-1):void 0===l[1]?c=-2:(c=r.lastIndex-l[2].length,n=l[1],r=void 0===l[3]?H:'"'===l[3]?q:N):r===q||r===N?r=H:r===O||r===U?r=T:(r=H,a=void 0);const d=r===H&&t[e+1].startsWith("/>")?" ":"";o+=r===T?i+z:c>=0?(s.push(n),i.slice(0,c)+S+i.slice(c)+C+d):i+C+(-2===c?e:d)}return[Z(t,o+(t[i]||"")+(2===e?"":3===e?"":"")),s]};class X{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let a=0,o=0;const r=t.length-1,n=this.parts,[l,c]=Y(t,e);if(this.el=X.createElement(l,i),V.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(s=V.nextNode())&&n.length0){s.textContent=w?w.emptyScript:"";for(let i=0;iE(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==B&&P(this._$AH)?this._$AA.nextSibling.data=t:this.T(D.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,s="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=X.createElement(Z(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(e);else{const t=new G(s,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=K.get(t.strings);return void 0===e&&K.set(t.strings,e=new X(t)),e}k(t){E(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,s=0;for(const a of t)s===e.length?e.push(i=new Q(this.O(M()),this.O(M()),this,this.options)):i=e[s],i._$AI(a),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=B}_$AI(t,e=this,i,s){const a=this.strings;let o=!1;if(void 0===a)t=J(this,t,e,0),o=!P(t)||t!==this._$AH&&t!==W,o&&(this._$AH=t);else{const s=t;let r,n;for(t=a[0],r=0;r{const s=i?.renderBefore??e;let a=s._$litPart$;if(void 0===a){const t=i?.renderBefore??null;s._$litPart$=a=new Q(e.insertBefore(M(),t),t,void 0,i??{})}return a._$AI(t),a})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return W}}nt._$litElement$=!0,nt.finalized=!0,rt.litElementHydrateSupport?.({LitElement:nt});const lt=rt.litElementPolyfillSupport;lt?.({LitElement:nt}),(rt.litElementVersions??=[]).push("4.2.2");const ct=new Set(["hacs","sun","backup","hassio","met","telegram_bot","mobile_app","systemmonitor","better_thermostat","adaptive_lighting","yandex_pogoda","upnp_serial_number"]),ht=[[/протечк/,"mdi:water-alert"],[/клапан/,"mdi:pipe-valve"],[/дым/,"mdi:smoke-detector"],[/термоголов/,"mdi:radiator"],[/температ/,"mdi:thermometer"],[/qingping|air monitor|молекул/,"mdi:air-filter"],[/штор/,"mdi:roller-shade"],[/розетк|plug/,"mdi:power-socket-de"],[/выключат|switch/,"mdi:light-switch"],[/лампа|лампочк|bulb|gx53|светильник|rgb/,"mdi:lightbulb"],[/камер|camera/,"mdi:cctv"],[/замок|ttlock|lock|sn609|sn9161/,"mdi:lock"],[/ворота|garage/,"mdi:garage-variant"],[/калитк|door|открыт/,"mdi:door"],[/счётчик|счетчик|kws|meter/,"mdi:meter-electric"],[/вводный автомат|breaker|wifimcbn/,"mdi:electric-switch"],[/myheat|котёл|котел|boiler|отоплен/,"mdi:water-boiler"],[/холодильник|fridge/,"mdi:fridge"],[/стиральн|washer/,"mdi:washing-machine"],[/сушилк|dryer/,"mdi:tumble-dryer"],[/пылесос|vacuum|dreame/,"mdi:robot-vacuum"],[/soundbar|колонк|станц/,"mdi:soundbar"],[/tv|телевизор|hyundaitv|mitv/,"mdi:television"],[/keenetic|роутер|router/,"mdi:router-wireless"],[/ибп|ups|kirpich/,"mdi:battery-charging-high"],[/slzb|координат|zigbee/,"mdi:zigbee"]];function dt(t,e){const i=((t||"")+" "+(e||"")).toLowerCase();for(const[t,e]of ht)if(t.test(i))return e;return"mdi:chip"}const pt=["light","switch","cover","valve","lock","climate","fan","media_player","camera","vacuum","humidifier","water_heater","alarm_control_panel","sensor","binary_sensor","event","button","number","select","update"];function _t(t){const e=Math.max(0,Math.min(120,(t-40)/140*120));return`hsl(${Math.round(e)}, 85%, 55%)`}function ut(t,e){return Math.round(t/e)*e}function mt(t){return t.length?Math.round(t.reduce((t,e)=>t+e,0)/t.length):null}function gt(t,e){if(e>t[2]/t[3]){const i=t[3],s=t[3]*e;return{x:t[0]-(s-t[2])/2,y:t[1],w:s,h:i}}const i=t[2],s=t[2]/e;return{x:t[0],y:t[1]-(s-t[3])/2,w:i,h:s}}function ft(t,e,i,s){if(t.length<2)return;const a=e.x+s,o=e.x+e.w-s,r=e.y+s,n=e.y+e.h-s;for(let e=0;e<60;e++){let e=!1;for(let s=0;s({eid:e,reg:t.entities[e],st:t.states[e]})).filter(t=>t.reg&&!t.reg.hidden),a=s.filter(t=>!t.reg.entity_category),o=a.length?a:s;if("mdi:thermometer"===i||"mdi:air-filter"===i){const e=o.find(e=>yt(t,e.eid));if(e)return e.eid}for(const t of pt){const e=o.find(e=>e.eid.split(".")[0]===t);if(e)return e.eid}return o[0]?.eid}function xt(t,e){const i=[];for(const s of e){const e=t.states[s];if(!e)continue;const a=(e.attributes?.unit_of_measurement||"").toLowerCase();if(/_(linkquality|lqi)$/.test(s)||"lqi"===a){const t=parseFloat(e.state);isNaN(t)||i.push(t);continue}const o=e.attributes?.linkquality??e.attributes?.lqi;if(null!=o){const t=parseFloat(o);isNaN(t)||i.push(t)}}return mt(i)}function wt(t,e){for(const i of e){if(!yt(t,i))continue;const e=t.states[i];if(!e)continue;const s=parseFloat(e.state);if(!isNaN(s))return Math.round(10*s)/10}return null}function kt(t,e){t.marker=e,e.name&&(t.name=e.name),e.icon&&(t.icon=e.icon),null!=e.model&&(t.model=e.model),t.link=e.link??null,t.description=e.description??null,t.pdfs=e.pdfs||[]}function St(t){const{hass:e,areaToSpace:i,markers:s,settings:a,excluded:o,showAll:r,firstSpaceId:n,loc:l}=t,c=!1!==a.group_lights,h=function(t,e){if(!e)return[];const i=[];for(const[e,s]of Object.entries(t.entities)){if(!e.startsWith("light.")||s.hidden)continue;let a=null;if("group"===s.platform)a=s.area_id||null;else{if(!s.device_id)continue;{const e=t.devices[s.device_id];if("Group"!==e?.model)continue;a=e.area_id||s.area_id||null}}if(!a)continue;const o=t.states[e];i.push({eid:e,name:s.name||o?.attributes?.friendly_name||e,area:a})}return i}(e,c),d=new Set(h.map(t=>t.area)),p=function(t){const e={};for(const[i,s]of Object.entries(t.entities))s?.device_id&&(e[s.device_id]=e[s.device_id]||[]).push(i);return e}(e),_=new Set;for(const t of s){const[e,i]=t.binding.split(":");"device"!==e&&"entity"!==e||!i||_.add(t.binding)}const u=(t,e)=>s.find(i=>i.binding===t+":"+e),m={},g=[];for(const t of Object.values(e.devices)){const s=t.area_id;if(!s||!i[s])continue;if("service"===t.entry_type)continue;if(_.has("device:"+t.id))continue;const a=u("device",t.id);if(a&&a.hidden)continue;const n=p[t.id]||[],h=bt(e,t,n);if(!r){if(o.has(h))continue;if("Group"===t.model)continue;if(/scene/i.test(t.model||""))continue;if(/bridge/i.test((t.model||"")+(t.name||"")))continue;if("myheat"===h&&t.via_device_id)continue}const f=(t.name_by_user||t.name||l("device.unnamed")).trim(),v=f+"|"+s;let b=dt(f,t.model);if(n.some(t=>t.startsWith("lock."))&&(b="mdi:lock"),!r&&c&&"mdi:lightbulb"===b&&d.has(s))continue;m[v]=(m[v]||0)+1;const y=m[v]>1?f+" "+m[v]:f,$={id:t.id,name:y,model:t.model||"",area:s,space:i[s],icon:b,entities:n,bindingKind:"device",bindingRef:t.id,pdfs:[]};$.primary=$t(e,n,b),"mdi:thermometer"!==b&&"mdi:air-filter"!==b||($.temp=wt(e,n)),g.push($)}for(const t of h)i[t.area]&&(_.has("entity:"+t.eid)||g.push({id:"lg_"+t.eid,name:t.name,model:l("device.light_group"),area:t.area,space:i[t.area],icon:"mdi:lightbulb-group",entities:[t.eid],primary:t.eid,bindingKind:"entity",bindingRef:t.eid,pdfs:[]}));for(const t of s){if(t.hidden)continue;const[s,a]=t.binding.split(":");if("device"===s){const s=e.devices[a],o=t.area||s?.area_id||"",r=o&&i[o]||t.space||n,c=s&&p[s.id]||[];let h=s?dt(s.name_by_user||s.name||"",s.model):"mdi:help-circle";c.some(t=>t.startsWith("lock."))&&(h="mdi:lock");const d={id:t.id,name:s?.name_by_user||s?.name||l("device.fallback"),model:s?.model||"",area:o,space:r,icon:h,entities:c,bindingKind:"device",bindingRef:a};d.primary=$t(e,c,h),"mdi:thermometer"!==h&&"mdi:air-filter"!==h||(d.temp=wt(e,c)),kt(d,t),g.push(d)}else if("entity"===s){const s=e.entities[a],o=t.area||s?.area_id||s?.device_id&&e.devices[s.device_id]?.area_id||"",r=o&&i[o]||t.space||n,l=e.states[a],c={id:t.id,name:s?.name||l?.attributes?.friendly_name||a,model:"",area:o,space:r,icon:"mdi:shape-outline",entities:[a],primary:a,bindingKind:"entity",bindingRef:a};kt(c,t),g.push(c)}else{const e=t.area||"",s=t.space||e&&i[e]||n,a={id:t.id,name:t.name||l("device.virtual"),model:t.model||"",area:e,space:s,icon:t.icon||"mdi:map-marker",entities:[],bindingKind:"virtual",virtual:!0};kt(a,t),g.push(a)}}return g}const Ct={"card.title":"House plan","count.devices":"{n} dev.","empty.no_spaces":"No spaces yet.","empty.add_first":"Add the first space and upload a floor plan.","empty.install":'Install the House Plan integration and add it in "Devices & services".',"btn.add_space":"Add space","btn.cancel":"Cancel","btn.save":"Save","btn.close":"Close","btn.delete":"Delete","btn.remove":"Remove","btn.edit":"Edit","btn.open_in_ha":"Open in HA","btn.reset":"Reset","btn.attach":"Attach…","btn.upload":"Upload…","btn.replace":"Replace…","btn.no_area":"No area","title.zoom_in":"Zoom in","title.zoom_out":"Zoom out","title.zoom_reset":"Reset zoom","title.add_device":"Add a device to the plan","title.show_all":"Show all area devices (no curation)","title.reset_layout":"Reset icon positions to auto layout","title.markup":"Room markup: grid, lines, outlines","title.configure_space":"Configure space","title.add_space":"Add space","title.markup_add":"Add a room: connect grid dots with lines until the outline closes","title.markup_erase":"Erase a line: click the line","title.markup_delroom":"Delete a room: click inside the room","title.no_area_room":"Decorative room without an HA area (e.g. a hallway)","title.choose_area":"Select a Home Assistant area","title.need_plan":"Upload a floor-plan image","markup.add":"Add","markup.erase":"Erase","markup.delete":"Delete","markup.hint_points":"points: {n} · Esc/Ctrl+Z — undo a dot · close the outline by clicking the first one","markup.hint_start":"click a grid dot to start the outline","tip.room":"room — open the area","tip.lqi":"average zigbee signal:","info.device_header":"Device on the plan","info.model":"Model","info.state":"State","info.link":"Link","info.manuals":"Manuals","info.none":"No additional information","marker.new_device":"New device","marker.name_label":"Name (shown on the plan)","marker.name_ph":"Name","marker.binding_label":"Bind to an HA device","marker.virtual_option":"Virtual device (no binding)","marker.search_ph":"Search device / group…","marker.nothing_found":"nothing found","marker.room_label":"Room","marker.room_override":" (override placement)","marker.room_choose":"— select a room —","marker.room_auto":"— by device area (auto) —","marker.icon_label":"Icon","marker.icon_ph":"mdi:… (empty = auto)","marker.model_label":"Model","marker.model_ph":"e.g. Aqara T&H","marker.link_label":"Link","marker.desc_label":"Description","marker.desc_ph":"Notes, specs…","marker.manuals_label":"Manuals (PDF etc.)","marker.sub_device":"device","marker.sub_z2m_group":" · Z2M group","marker.sub_group":"group","marker.sub_helper":"helper","space.new":"New space","space.header":"Space","space.title_label":"Title","space.title_ph":"e.g. Garage","space.plan_label":"Floor plan (background)","space.no_plan":"no plan image","space.plan_alt":"plan","room.new":"New room","room.name_label":"Display name","room.name_ph":"e.g. Terrace","room.area_label":"Home Assistant area (unassigned)","room.no_area_option":"— no area —","room.default_name":"Room","device.unnamed":"unnamed","device.light_group":"light group","device.fallback":"device","device.virtual":"virtual device","confirm.reset_layout":"Reset all icon positions to the auto layout?","confirm.delete_room":'Delete room "{name}"?',"confirm.remove_marker":'Remove "{name}" from the plan?',"confirm.delete_space":'Delete space "{title}" with all its rooms and markup?',"toast.pos_save_failed":"Failed to save position: {err}","toast.no_entity":"The device has no suitable entity","toast.markup_needs_server":"Markup is available after the config is moved to the server","toast.conflict":"Config was changed in another window — data refreshed, repeat your last action","toast.cfg_save_failed":"Failed to save config: {err}","toast.room_saved":"Room saved ({n}). Devices added: {added}. Outline the next one or exit markup.","toast.room_saved_no_area":"Room saved ({n}, no area). Outline the next one or exit markup.","toast.marker_needs_server":"Device editing is available after the config is moved to the server","toast.virtual_name_required":"Enter a name for the virtual device","toast.marker_saved":"Device saved","toast.marker_removed":"Device removed from the plan","toast.integration_missing":"The House Plan integration is not installed — management unavailable","toast.plan_formats":"Supported formats: SVG, PNG, JPG, WebP","toast.plan_required":"Upload a floor plan — it is required","toast.space_added_onboard":"Space added. Outline the rooms: click grid dots and close the contour.","toast.space_added":"Space added","toast.space_saved":"Space saved","toast.space_deleted":"Space deleted","toast.delete_failed":"Delete failed: {err}","toast.error":"Error: {err}","toast.file_failed":'File "{name}" was not uploaded: {err}',"toast.files_attached":"Files attached: {n}","err.unknown":"unknown error","err.code":"code {code}","err.too_large":"file larger than {mb} MB","err.bad_ext":"unsupported type (PDF/image expected)","err.unauthorized":"administrator rights required","editor.title":"Title","editor.default_floor":"Default space","editor.icon_size":"Icon size, % of plan width","editor.show_temperature":"Show temperature","editor.live_states":"Live states (on/off, open…)","editor.show_signal":"Show zigbee signal (LQI)","editor.language":"Interface language","editor.lang_auto":"Auto (HA profile)","editor.lang_en":"English","editor.lang_ru":"Русский"},At={en:Ct,ru:{"card.title":"План дома","count.devices":"{n} устр.","empty.no_spaces":"Пространств пока нет.","empty.add_first":"Добавьте первое пространство и загрузите план этажа.","empty.install":"Установите интеграцию House Plan и добавьте запись в «Устройства и службы».","btn.add_space":"Добавить пространство","btn.cancel":"Отмена","btn.save":"Сохранить","btn.close":"Закрыть","btn.delete":"Удалить","btn.remove":"Убрать","btn.edit":"Редактировать","btn.open_in_ha":"Открыть в HA","btn.reset":"Сброс","btn.attach":"Прикрепить…","btn.upload":"Загрузить…","btn.replace":"Заменить…","btn.no_area":"Без зоны","title.zoom_in":"Приблизить","title.zoom_out":"Отдалить","title.zoom_reset":"Сбросить масштаб","title.add_device":"Добавить устройство на план","title.show_all":"Показывать все устройства зоны (без курирования)","title.reset_layout":"Сбросить позиции значков к авто-раскладке","title.markup":"Разметка комнат: сетка, линии, контуры","title.configure_space":"Настроить пространство","title.add_space":"Добавить пространство","title.markup_add":"Добавить комнату: соединяйте точки сетки линиями до замкнутого контура","title.markup_erase":"Стереть линию: клик по линии","title.markup_delroom":"Удалить комнату: клик внутри комнаты","title.no_area_room":"Декоративная комната без привязки к зоне (например, холл)","title.choose_area":"Выберите зону Home Assistant","title.need_plan":"Загрузите подложку (план этажа)","markup.add":"Добавить","markup.erase":"Стереть","markup.delete":"Удалить","markup.hint_points":"точек: {n} · Esc/Ctrl+Z — убрать точку · замкните контур кликом по первой","markup.hint_start":"кликните точку сетки, чтобы начать контур","tip.room":"комната — открыть зону","tip.lqi":"средний сигнал zigbee:","info.device_header":"Устройство на плане","info.model":"Модель","info.state":"Состояние","info.link":"Ссылка","info.manuals":"Инструкции","info.none":"Нет дополнительной информации","marker.new_device":"Новое устройство","marker.name_label":"Имя (отображается на плане)","marker.name_ph":"Название","marker.binding_label":"Привязка к устройству HA","marker.virtual_option":"Виртуальное устройство (без привязки)","marker.search_ph":"Поиск устройства / группы…","marker.nothing_found":"ничего не найдено","marker.room_label":"Комната","marker.room_override":" (переопределить размещение)","marker.room_choose":"— выберите комнату —","marker.room_auto":"— по зоне устройства (авто) —","marker.icon_label":"Иконка","marker.icon_ph":"mdi:… (пусто = авто)","marker.model_label":"Модель","marker.model_ph":"напр. Aqara T&H","marker.link_label":"Ссылка","marker.desc_label":"Описание","marker.desc_ph":"Заметки, характеристики…","marker.manuals_label":"Инструкции (PDF и т.п.)","marker.sub_device":"устройство","marker.sub_z2m_group":" · Z2M-группа","marker.sub_group":"группа","marker.sub_helper":"хелпер","space.new":"Новое пространство","space.header":"Пространство","space.title_label":"Название","space.title_ph":"Например: Гараж","space.plan_label":"Подложка (план)","space.no_plan":"нет подложки","space.plan_alt":"план","room.new":"Новая комната","room.name_label":"Отображаемое имя","room.name_ph":"Например: Терраса","room.area_label":"Зона Home Assistant (свободные)","room.no_area_option":"— без зоны —","room.default_name":"Комната","device.unnamed":"без имени","device.light_group":"группа света","device.fallback":"устройство","device.virtual":"виртуальное устройство","confirm.reset_layout":"Сбросить позиции всех иконок к авто-раскладке?","confirm.delete_room":"Удалить комнату «{name}»?","confirm.remove_marker":"Убрать «{name}» с плана?","confirm.delete_space":"Удалить пространство «{title}» со всеми комнатами и разметкой?","toast.pos_save_failed":"Не удалось сохранить позицию: {err}","toast.no_entity":"У устройства нет подходящей сущности","toast.markup_needs_server":"Разметка доступна после переноса конфига на сервер","toast.conflict":"Конфиг изменён в другом окне — данные обновлены, повторите последнее действие","toast.cfg_save_failed":"Не удалось сохранить конфиг: {err}","toast.room_saved":"Комната сохранена ({n}). Устройств добавлено: {added}. Обведите следующую или выйдите из разметки.","toast.room_saved_no_area":"Комната сохранена ({n}, без зоны). Обведите следующую или выйдите из разметки.","toast.marker_needs_server":"Редактирование устройств доступно после переноса конфига на сервер","toast.virtual_name_required":"Укажите имя виртуального устройства","toast.marker_saved":"Устройство сохранено","toast.marker_removed":"Устройство убрано с плана","toast.integration_missing":"Интеграция House Plan не установлена — управление недоступно","toast.plan_formats":"Поддерживаются SVG, PNG, JPG, WebP","toast.plan_required":"Загрузите подложку — план этажа обязателен","toast.space_added_onboard":"Пространство добавлено. Обведите комнаты: кликайте по точкам сетки и замкните контур.","toast.space_added":"Пространство добавлено","toast.space_saved":"Пространство сохранено","toast.space_deleted":"Пространство удалено","toast.delete_failed":"Ошибка удаления: {err}","toast.error":"Ошибка: {err}","toast.file_failed":"Файл «{name}» не загружен: {err}","toast.files_attached":"Прикреплено файлов: {n}","err.unknown":"неизвестная ошибка","err.code":"код {code}","err.too_large":"файл больше {mb} МБ","err.bad_ext":"недопустимый тип (нужен PDF/изображение)","err.unauthorized":"нужны права администратора","editor.title":"Заголовок","editor.default_floor":"Пространство по умолчанию","editor.icon_size":"Размер иконок, % ширины плана","editor.show_temperature":"Показывать температуру","editor.live_states":"Живые состояния (вкл/выкл, открыто…)","editor.show_signal":"Показывать сигнал zigbee (LQI)","editor.language":"Язык интерфейса","editor.lang_auto":"Авто (профиль HA)","editor.lang_en":"English","editor.lang_ru":"Русский"}};function zt(t,e){if("ru"===e||"en"===e)return e;return(t?.locale?.language||t?.language||"en").toLowerCase().startsWith("ru")?"ru":"en"}function Dt(t,e,i){let s=At[t][e]??Ct[e]??e;if(i)for(const[t,e]of Object.entries(i))s=s.replace("{"+t+"}",String(e));return s}class Mt extends nt{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1}setConfig(t){this._config=t}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const t=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(t?.config?.spaces||[]).map(t=>({value:t.id,label:t.title||t.id}))}catch{this._spaces=[]}finally{this._spacesLoading=!1}}}get _lang(){return zt(this.hass,this._config?.language)}get _schema(){const t=this._spaces||[],e=this._lang;return[{name:"title",selector:{text:{}}},t.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:t}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:[{value:"",label:Dt(e,"editor.lang_auto")},{value:"en",label:Dt(e,"editor.lang_en")},{value:"ru",label:Dt(e,"editor.lang_ru")}]}}},{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:{}}}]}render(){if(!this.hass||!this._config)return B;this._loadSpaces();const t=this._lang,e={title:Dt(t,"editor.title"),default_floor:Dt(t,"editor.default_floor"),language:Dt(t,"editor.language"),icon_size:Dt(t,"editor.icon_size"),show_temperature:Dt(t,"editor.show_temperature"),live_states:Dt(t,"editor.live_states"),show_signal:Dt(t,"editor.show_signal")};return F`Ct[t.name]||t.name} + .computeLabel=${t=>e[t.name]||t.name} @value-changed=${this._valueChanged} - >`):B}_valueChanged(t){const e={...this._config,...t.detail.value},i=new Event("config-changed",{bubbles:!0,composed:!0});i.detail={config:e},this.dispatchEvent(i)}}At.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",At);const zt=((t,...e)=>{const s=1===t.length?t[0]:e.reduce((e,i,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[s+1],t[0]);return new a(s,t,i)})` + >`}_valueChanged(t){const e={...this._config,...t.detail.value},i=new Event("config-changed",{bubbles:!0,composed:!0});i.detail={config:e},this.dispatchEvent(i)}}Mt.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",Mt);const Pt=((t,...e)=>{const s=1===t.length?t[0]:e.reduce((e,i,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[s+1],t[0]);return new a(s,t,i)})` :host { --hp-bg: var(--card-background-color, #16212e); --hp-line: var(--divider-color, #2b3d4f); @@ -15,7 +15,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow --hp-open: #ff9f43; } ha-card { - overflow: visible; /* overflow:hidden ломает position:sticky у шапки */ + overflow: visible; /* overflow:hidden breaks position:sticky on the header */ } .empty { padding: 40px 24px; @@ -142,7 +142,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow width: 100%; container-type: inline-size; overflow: hidden; - touch-action: none; /* свои жесты pinch/pan */ + touch-action: none; /* custom pinch/pan gestures */ background: var(--ha-card-background, var(--card-background-color, #111)); } .zoomwrap { @@ -223,7 +223,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow pointer-events: none; } .stage.markup .devlayer { - display: none; /* в разметке иконки не мешают */ + display: none; /* in markup mode icons must not get in the way */ } .room.outlined { stroke: rgba(62, 166, 255, 0.55); @@ -727,63 +727,63 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow z-index: 120; max-width: 90vw; } - `,Mt="houseplan_card_layout_v1",Dt="houseplan_card_cfg_v1",Pt="houseplan_card_zoom_v1",Et=1e3,Rt=(t,e,i)=>{const s=new Event(e,{bubbles:!0,composed:!0});s.detail=i??{},t.dispatchEvent(s)},Tt=(t,e)=>{let i;return(...s)=>{clearTimeout(i),i=window.setTimeout(()=>t(...s),e)}};class Ot extends rt{constructor(){super(...arguments),this._space="f1",this._layout={},this._serverStorage=!1,this._loadOk=!1,this._loading=!1,this._loadTries=0,this._serverCfg=null,this._cfgRev=0,this._unsubCfg=null,this._devices=[],this._regSignature="",this._defPos={},this._tip=null,this._selId=null,this._toast="",this._markup=!1,this._tool="draw",this._path=[],this._pathSegs=[],this._cursorPt=null,this._areaSel="",this._nameSel="",this._roomDialog=!1,this._zoom=1,this._view=null,this._zoomBySpace={},this._pointers=new Map,this._panStart=null,this._pinchStart=null,this._suppressClick=!1,this._onboardingShown=!1,this._infoCard=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=t=>this._onKey(t),this._drag=null,this._dirtyPos=new Set,this._persistLayout=Tt(()=>{if(this._serverStorage){const t=[...this._dirtyPos];this._dirtyPos.clear();for(const e of t){const t=this._layout[e];t&&this.hass.callWS({type:"houseplan/layout/update",device_id:e,pos:t}).catch(t=>this._showToast("Не удалось сохранить позицию: "+(t?.message||t)))}this._cacheSnapshot()}else localStorage.setItem(Mt,JSON.stringify(this._layout))},600),this._saveConfig=Tt(()=>{this._serverCfg&&this.hass.callWS({type:"houseplan/config/set",config:this._serverCfg,expected_rev:this._cfgRev}).then(t=>{this._cfgRev=t?.rev??this._cfgRev+1}).catch(t=>{"conflict"===t?.code?(this._showToast("Конфиг изменён в другом окне — данные обновлены, повторите последнее действие"),this._cancelPath(),this._reloadConfigOnly()):this._showToast("Не удалось сохранить конфиг: "+(t?.message||t))})},500)}connectedCallback(){super.connectedCallback(),window.addEventListener("keydown",this._keyHandler)}disconnectedCallback(){window.removeEventListener("keydown",this._keyHandler),this._roViewport?.disconnect(),this._roViewport=void 0,this._unsubCfg&&(this._unsubCfg(),this._unsubCfg=null),super.disconnectedCallback()}_onKey(t){if("Escape"===t.key){if(this._infoCard)return void(this._infoCard=null);if(this._markerDialog)return void(this._markerDialog=null)}if(!this._markup)return;return"Escape"===t.key||(t.ctrlKey||t.metaKey)&&"z"===t.key.toLowerCase()?this._roomDialog?(t.preventDefault(),void this._roomDialogCancel()):void("draw"===this._tool&&this._path.length&&(t.preventDefault(),this._undoPoint())):void 0}_undoPoint(){if(!this._path.length)return;if(1===this._path.length)return this._path=[],void(this._pathSegs=[]);const t=this._pathSegs[this._pathSegs.length-1];this._pathSegs=this._pathSegs.slice(0,-1),t&&this._removeSegmentByKey(t),this._path=this._path.slice(0,-1)}_removeSegmentByKey(t){const e=this._curSpaceCfg;if(!e?.segments)return;const i=this._segments.findIndex(e=>this._segKey([e[0],e[1]],[e[2],e[3]])===t);i>=0&&(e.segments.splice(i,1),this._saveConfig())}static getConfigElement(){return document.createElement("houseplan-card-editor")}static getStubConfig(){return{type:"custom:houseplan-card",title:"План дома"}}setConfig(t){this._config={icon_size:2.5,show_temperature:!0,live_states:!0,show_signal:!0,...t},t.default_floor&&(this._space=t.default_floor);try{this._zoomBySpace=JSON.parse(localStorage.getItem(Pt)||"{}")||{}}catch{this._zoomBySpace={}}try{const e=JSON.parse(localStorage.getItem(Dt)||"null");e&&e.config&&Array.isArray(e.config.spaces)&&(this._serverCfg=e.config,this._cfgRev=e.rev||0,this._layout=e.layout||{},this._serverStorage=!0,t.default_floor?this._space=t.default_floor:this._model.find(t=>t.id===this._space)||(this._space=this._model[0]?.id||this._space))}catch{}}_cacheSnapshot(){if(this._serverCfg)try{localStorage.setItem(Dt,JSON.stringify({config:this._serverCfg,rev:this._cfgRev,layout:this._layout}))}catch{}}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}get _model(){return this._serverCfg?this._serverCfg.spaces.map(t=>{const e=Et/t.aspect;return{id:t.id,title:t.title,vb:[t.view_box[0]*Et,t.view_box[1]*e,t.view_box[2]*Et,t.view_box[3]*e],bg:t.plan_url?{href:t.plan_url,x:0,y:0,w:Et,h:e}:null,rooms:t.rooms.map(t=>({id:t.id,name:t.name,area:t.area??null,x:null!=t.x?t.x*Et:void 0,y:null!=t.y?t.y*e:void 0,w:null!=t.w?t.w*Et:void 0,h:null!=t.h?t.h*e:void 0,poly:t.poly?t.poly.map(t=>[t[0]*Et,t[1]*e]):void 0}))}}):[]}_spaceModel(t){const e=this._model;return e.find(e=>e.id===(t??this._space))||e[0]}get _areaToSpace(){const t={};for(const e of this._model)for(const i of e.rooms)i.area&&(t[i.area]={space:e.id,room:i});return t}get _settings(){return this._serverCfg?.settings||{}}get _showAll(){return!!this._settings.show_all}_toggleShowAll(){this._serverCfg&&(this._serverCfg={...this._serverCfg,settings:{...this._serverCfg.settings,show_all:!this._showAll}},this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate())}get _excluded(){const t=this._settings.exclude_integrations;return t?new Set(t):ct}willUpdate(t){t.has("hass")&&this.hass&&(!this._loadOk&&!this._loading&&this._loadTries<8&&this._loadFromServer(),this._maybeRebuildDevices())}updated(){const t=this._stageEl;t&&!this._roViewport&&(this._roViewport=new ResizeObserver(()=>this._refitView()),this._roViewport.observe(t)),t&&!this._view&&this._refitView(),this._serverStorage&&this._loadOk&&0===this._model.length&&!this._spaceDialog&&!this._onboardingShown&&(this._onboardingShown=!0,this._openSpaceDialog("create"))}async _loadFromServer(){this._loading=!0,this._loadTries++;try{const[t,e]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]);this._loadOk=!0,this._serverStorage=!0;const i=t?.config;this._serverCfg=i&&Array.isArray(i.spaces)?i:null,this._cfgRev=t?.rev||0,this._layout=e?.layout||{},this._unsubCfg||(this._unsubCfg=await this.hass.connection.subscribeEvents(t=>{(t?.data?.rev??-1)!==this._cfgRev&&this._reloadConfigOnly()},"houseplan_config_updated")),this._norm&&!this._model.find(t=>t.id===this._space)&&(this._space=this._model[0]?.id||this._space),this._cacheSnapshot(),this._restoreZoom()}catch(t){if(this._loadTries>=8){this._serverStorage=!1,this._serverCfg=null;try{this._layout=JSON.parse(localStorage.getItem(Mt)||"{}")||{}}catch{this._layout={}}}}finally{this._loading=!1}this._regSignature="",this.requestUpdate()}async _reloadConfigOnly(){try{const t=await this.hass.callWS({type:"houseplan/config/get"}),e=t?.config;this._serverCfg=e&&Array.isArray(e.spaces)?e:null,this._cfgRev=t?.rev||0,this._cacheSnapshot(),this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}catch{}}_maybeRebuildDevices(){const t=this.hass;if(!t?.devices||!t?.entities||!t?.areas)return;const e=Object.keys(t.devices).length+":"+Object.keys(t.entities).length+":"+Object.keys(t.areas).length+":"+(this._norm?"n":"l");e===this._regSignature&&this._devices.length||(this._regSignature=e,this._devices=St({hass:t,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([t,e])=>[t,e.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,showAll:this._showAll,firstSpaceId:this._model[0]?.id||""}),this._defPos=this._defaultPositions())}get _markers(){return this._serverCfg?.markers||[]}_roomLqi(t){if(!t)return null;const e=[];for(const i of this._devices){if(i.area!==t||i.virtual)continue;const s=$t(this.hass,i.entities);null!=s&&e.push(s)}return gt(e)}_roomBounds(t){if(t.poly&&t.poly.length){const e=t.poly.map(t=>t[0]),i=t.poly.map(t=>t[1]),s=Math.min(...e),a=Math.min(...i);return{x:s,y:a,w:Math.max(...e)-s,h:Math.max(...i)-a}}return{x:t.x??0,y:t.y??0,w:t.w??0,h:t.h??0}}_defaultPositions(){const t={},e=(this._config?.icon_size??2.5)/100*Et*1.3;for(const i of this._model)for(const s of i.rooms){if(!s.area)continue;const a=this._devices.filter(t=>t.area===s.area&&t.space===i.id);if(!a.length)continue;const o=this._roomBounds(s),n=.1*Math.min(o.w,o.h),r=o.w-2*n,l=o.h-2*n,c=Math.max(1,Math.round(Math.sqrt(a.length*r/Math.max(l,1)))),h=Math.ceil(a.length/c),d=r/c,p=l/Math.max(h,1),u=a.map((t,e)=>({x:o.x+n+d*(e%c+.5),y:o.y+n+p*(Math.floor(e/c)+.5)}));ft(u,o,e,.5*n),a.forEach((e,i)=>t[e.id]=u[i])}return t}_pos(t){const e=this._spaceModel(t.space),i=this._layout[t.id];if(i)if(this._norm){if(i.s===t.space){const e=this._serverCfg.spaces.find(e=>e.id===t.space)?.aspect||1;return{x:i.x*Et,y:i.y*(Et/e)}}}else if(void 0===i.s)return{x:i.x,y:i.y};if(this._defPos[t.id])return this._defPos[t.id];const s=e.vb;return{x:s[0]+s[2]/2,y:s[1]+s[3]/2}}_savePos(t,e,i){if(this._norm){const s=this._gridPitch,a=Math.round(e/s)*s,o=Math.round(i/s)*s,n=this._serverCfg.spaces.find(e=>e.id===t.space)?.aspect||1;this._layout={...this._layout,[t.id]:{s:t.space,x:a/Et,y:o/(Et/n)}}}else this._layout={...this._layout,[t.id]:{x:Math.round(e),y:Math.round(i)}};this._dirtyPos.add(t.id),this._persistLayout()}_stateClass(t){if(!this._config?.live_states)return"";const e=t.primary?this.hass.states[t.primary]:void 0;if(!e)return"";if("unavailable"===e.state)return"unavail";const i=e.entity_id.split(".")[0];if(["light","switch","fan","humidifier"].includes(i))return"on"===e.state?"on":"";if("cover"===i||"valve"===i)return["open","opening"].includes(e.state)?"open":"";if("lock"===i)return["unlocked","open"].includes(e.state)?"open":"";if("binary_sensor"===i){const t=e.attributes?.device_class;if(["door","window","garage_door","opening","gas","smoke","moisture","problem"].includes(t))return"on"===e.state?"open":""}return"media_player"===i?["playing","on"].includes(e.state)?"on":"":"vacuum"===i&&["cleaning","returning"].includes(e.state)?"on":""}_liveTemp(t){return this._config?.show_temperature?"mdi:thermometer"!==t.icon&&"mdi:air-filter"!==t.icon?null:wt(this.hass,t.entities):null}_openMoreInfo(t){t?Rt(this,"hass-more-info",{entityId:t}):this._showToast("У устройства нет подходящей сущности")}_clickDevice(t,e){t.stopPropagation(),this._drag?.moved||this._suppressClick||this._markup||(this._infoCard=e)}get _stageEl(){return this.renderRoot.querySelector(".stage")}_stageAspect(){const t=this._stageEl,e=this._spaceModel().vb;return t&&t.clientHeight?t.clientWidth/t.clientHeight:e[2]/e[3]}_viewOr(t){return this._view&&this._view.w?this._view:mt(t,this._stageAspect())}_screenToVb(t,e){const i=this._stageEl,s=this._viewOr(this._spaceModel().vb),a=i?.clientWidth||1,o=i?.clientHeight||1;return[s.x+t/a*s.w,s.y+e/o*s.h]}_clampView(t,e){return{w:t.w,h:t.h,x:Math.max(e.x,Math.min(e.x+e.w-t.w,t.x)),y:Math.max(e.y,Math.min(e.y+e.h-t.h,t.y))}}_applyView(t,e,i){const s=this._spaceModel().vb,a=mt(s,this._stageAspect()),o=Math.min(8,Math.max(1,t)),n=a.w/o,r=a.h/o,l=this._viewOr(s),c=e??l.x+l.w/2,h=i??l.y+l.h/2;this._zoom=o,this._view=this._clampView({x:c-n/2,y:h-r/2,w:n,h:r},a)}_refitView(){if(!this._stageEl)return;const t=this._view;this._applyView(this._zoom,t?t.x+t.w/2:void 0,t?t.y+t.h/2:void 0),this.requestUpdate()}_zoomAt(t,e,i){const s=this._stageEl;if(!s)return;const a=mt(this._spaceModel().vb,this._stageAspect()),o=Math.min(8,Math.max(1,i)),n=s.clientWidth,r=s.clientHeight,l=this._screenToVb(t,e),c=a.w/o,h=a.h/o;this._zoom=o,this._view=this._clampView({x:l[0]-t/n*c,y:l[1]-e/r*h,w:c,h:h},a)}_onWheel(t){const e=this._stageEl;if(!e)return;t.preventDefault();const i=e.getBoundingClientRect(),s=t.deltaY<0?1.15:1/1.15;this._zoomAt(t.clientX-i.left,t.clientY-i.top,this._zoom*s),this._saveZoom()}_stepZoom(t){const e=this._stageEl;e&&(this._zoomAt(e.clientWidth/2,e.clientHeight/2,this._zoom*(t>0?1.4:1/1.4)),this._saveZoom())}_resetZoom(){const t=this._spaceModel().vb;this._zoom=1,this._view=mt(t,this._stageAspect()),this._saveZoom()}_saveZoom(){this._zoomBySpace={...this._zoomBySpace,[this._space]:this._zoom};try{localStorage.setItem(Pt,JSON.stringify(this._zoomBySpace))}catch{}}_restoreZoom(){const t=this._zoomBySpace[this._space]||1;this._zoom=t,this._view=null,requestAnimationFrame(()=>{if(!this._stageEl)return;const e=this._spaceModel().vb;this._applyView(t,e[0]+e[2]/2,e[1]+e[3]/2),this.requestUpdate()})}_stagePointerDown(t){if(this._drag||this._markup)return;if(t.target.closest(".dev"))return;this._pointers.set(t.pointerId,{x:t.clientX,y:t.clientY});const e=this._viewOr(this._spaceModel().vb);if(1===this._pointers.size)this._panStart={sx:t.clientX,sy:t.clientY,vx:e.x,vy:e.y},this._suppressClick=!1;else if(2===this._pointers.size){const t=[...this._pointers.values()],e=Math.hypot(t[0].x-t[1].x,t[0].y-t[1].y);this._pinchStart={dist:e,zoom:this._zoom},this._panStart=null}}_stagePointerMove(t){if(this._pointers.has(t.pointerId)){if(this._pointers.set(t.pointerId,{x:t.clientX,y:t.clientY}),this._pinchStart&&this._pointers.size>=2){const t=[...this._pointers.values()],e=Math.hypot(t[0].x-t[1].x,t[0].y-t[1].y)/(this._pinchStart.dist||1),i=this._stageEl.getBoundingClientRect(),s=(t[0].x+t[1].x)/2-i.left,a=(t[0].y+t[1].y)/2-i.top;this._zoomAt(s,a,this._pinchStart.zoom*e),this._suppressClick=!0,this._saveZoom()}else if(this._panStart){const e=t.clientX-this._panStart.sx,i=t.clientY-this._panStart.sy;if(Math.abs(e)+Math.abs(i)>4&&(this._suppressClick=!0),this._zoom>1&&this._view){const t=this._stageEl,s=this._view,a=mt(this._spaceModel().vb,this._stageAspect());this._view=this._clampView({x:this._panStart.vx-e/t.clientWidth*s.w,y:this._panStart.vy-i/t.clientHeight*s.h,w:s.w,h:s.h},a)}}}else this._markupMove(t)}_stagePointerUp(t){this._pointers.delete(t.pointerId),this._pointers.size<2&&(this._pinchStart=null),0===this._pointers.size&&(this._panStart=null,setTimeout(()=>this._suppressClick=!1,0))}_clickRoom(t){var e;!this._suppressClick&&t.area&&(e="/config/areas/area/"+t.area,history.pushState(null,"",e),Rt(window,"location-changed",{replace:!1}))}_pointerDown(t,e){if(this._markup)return;t.preventDefault();const i=this._pos(e);this._drag={id:e.id,sx:t.clientX,sy:t.clientY,ox:i.x,oy:i.y,moved:!1},t.target.setPointerCapture(t.pointerId),this._tip=null}_pointerMove(t,e){if(!this._drag||this._drag.id!==e.id)return;const i=this.renderRoot.querySelector(".stage");if(!i)return;const s=this._spaceModel().vb,a=i.getBoundingClientRect(),o=this._viewOr(s),n=(t.clientX-this._drag.sx)/a.width*o.w,r=(t.clientY-this._drag.sy)/a.height*o.h;Math.abs(t.clientX-this._drag.sx)+Math.abs(t.clientY-this._drag.sy)>3&&(this._drag.moved=!0);const l=.008*Math.min(s[2],s[3]),c=Math.max(s[0]+l,Math.min(s[0]+s[2]-l,this._drag.ox+n)),h=Math.max(s[1]+l,Math.min(s[1]+s[3]-l,this._drag.oy+r));this._savePos(e,c,h)}_pointerUp(t,e){if(!this._drag||this._drag.id!==e.id)return;const i=this._drag.moved;this._drag=i?this._drag:null,i&&(this._selId=e.id,window.setTimeout(()=>this._drag=null,0))}_resetLayout(){confirm("Сбросить позиции всех иконок к авто-раскладке?")&&(this._layout={},this._persistLayout())}_showToast(t){this._toast=t,clearTimeout(this._toastTimer),this._toastTimer=window.setTimeout(()=>{this._toast=""},3500)}_showTip(t,e,i,s){this._drag||(this._tip={x:t.clientX,y:t.clientY,title:e,meta:i,lqi:s})}get _gridPitch(){return Et/240}get _curSpaceCfg(){return this._serverCfg?.spaces.find(t=>t.id===this._space)}get _spaceH(){const t=this._curSpaceCfg;return t?Et/t.aspect:Et}get _segments(){const t=this._curSpaceCfg,e=this._spaceH;return(t?.segments||[]).map(t=>[t[0]*Et,t[1]*e,t[2]*Et,t[3]*e])}_toggleMarkup(){this._norm?(this._markup=!this._markup,this._path=[],this._cursorPt=null,this._tool="draw"):this._showToast("Разметка доступна после переноса конфига на сервер (режим правки → «На сервер»)")}_svgPoint(t){const e=this.renderRoot.querySelector(".stage").getBoundingClientRect();return this._screenToVb(t.clientX-e.left,t.clientY-e.top)}_snap(t){const e=this._gridPitch;return[_t(t[0],e),_t(t[1],e)]}_samePt(t,e){return function(t,e,i=.001){return Math.abs(t[0]-e[0])this._segKey([t[0],t[1]],[t[2],t[3]])===a);return!o&&(i.segments=i.segments||[],i.segments.push([t[0]/Et,t[1]/s,e[0]/Et,e[1]/s]),this._saveConfig(),!0)}_distToSeg(t,e){const[i,s]=t,[a,o,n,r]=e,l=n-a,c=r-o;let h=((i-a)*l+(s-o)*c)/(l*l+c*c||1);h=Math.max(0,Math.min(1,h));const d=a+h*l,p=o+h*c;return Math.hypot(i-d,s-p)}_pointInRoom(t,e){return e.poly?function(t,e){let i=!1;for(let s=0,a=e.length-1;st[1]!=l>t[1]&&t[0]<(r-o)*(t[1]-n)/(l-n)+o&&(i=!i)}return i}(t,e.poly):null!=e.x&&t[0]>=e.x&&t[0]<=e.x+e.w&&t[1]>=e.y&&t[1]<=e.y+e.h}_markupClick(t){if(!this._markup)return;const e=this._svgPoint(t);if("erase"===this._tool){const t=this._curSpaceCfg;if(!t?.segments?.length)return;const i=this._segments;let s=-1,a=.5*this._gridPitch;return i.forEach((t,i)=>{const o=this._distToSeg(e,t);o=0&&(t.segments.splice(s,1),this._saveConfig(),this.requestUpdate()))}if("delroom"===this._tool){const t=[...this._spaceModel().rooms].reverse().find(t=>this._pointInRoom(e,t));if(!t)return;if(!confirm(`Удалить комнату «${t.name}»?`))return;const i=this._curSpaceCfg;return i.rooms=i.rooms.filter(e=>e.id!==t.id),this._saveConfig(),this._regSignature="",this._maybeRebuildDevices(),void this.requestUpdate()}const i=this._snap(e);if(!this._path.length)return this._path=[i],void(this._pathSegs=[]);const s=this._path[this._path.length-1];if(this._samePt(i,s))return;const a=this._addSegment(s,i);this._pathSegs=[...this._pathSegs,a?this._segKey(s,i):null],this._path=[...this._path,i],this._path.length>=4&&this._samePt(i,this._path[0])&&(this._cursorPt=null,this._nameSel="",this._areaSel="",this._roomDialog=!0)}get _contourClosed(){return this._path.length>=4&&this._samePt(this._path[0],this._path[this._path.length-1])}_markupMove(t){this._markup&&"draw"===this._tool&&this._path.length&&!this._contourClosed&&(this._cursorPt=this._snap(this._svgPoint(t)))}_saveRoom(){this._areaSel&&this._commitRoom()}_saveRoomNoArea(){this._nameSel.trim()&&(this._areaSel="",this._commitRoom())}_commitRoom(){if(!this._contourClosed)return;const t=this._curSpaceCfg;if(!t)return;const e=this._spaceH,i=this._path.slice(0,-1),s=this._areaSel?this.hass.areas[this._areaSel]?.name:"";t.rooms.push({id:"r"+Date.now().toString(36),name:this._nameSel||s||"Комната",area:this._areaSel||null,poly:i.map(t=>[t[0]/Et,t[1]/e])}),this._saveConfig(),this._path=[],this._pathSegs=[];const a=this._areaSel;this._areaSel="",this._nameSel="",this._roomDialog=!1,this._regSignature="",this._maybeRebuildDevices();let o=0;if(a){const t=this._serverCfg?.spaces.find(t=>t.id===this._space)?.aspect||1,e=Et/t,i={...this._layout};for(const t of this._devices){if(t.area!==a||t.space!==this._space)continue;if(o++,this._layout[t.id])continue;const s=this._defPos[t.id];s&&(i[t.id]={s:this._space,x:s.x/Et,y:s.y/e},this._dirtyPos.add(t.id))}this._layout=i,this._persistLayout()}const n=this._model.find(t=>t.id===this._space)?.rooms.length||0;this._showToast(a?`Комната сохранена (${n}). Устройств добавлено: ${o}. Обведите следующую или выйдите из разметки.`:`Комната сохранена (${n}, без зоны). Обведите следующую или выйдите из разметки.`)}_cancelPath(){this._path=[],this._pathSegs=[],this._cursorPt=null,this._roomDialog=!1}_roomDialogCancel(){this._roomDialog=!1,this._undoPoint()}get _freeAreas(){const t=new Set;for(const e of this._serverCfg?.spaces||[])for(const i of e.rooms||[])i.area&&t.add(i.area);return Object.values(this.hass?.areas||{}).filter(e=>!t.has(e.area_id)).sort((t,e)=>(t.name||"").localeCompare(e.name||""))}_openMarkerDialog(t){this._norm?this._markerDialog=t?{devId:t.id,name:t.name,binding:"virtual"===t.bindingKind?"virtual":t.bindingKind+":"+t.bindingRef,bindingFilter:"",icon:t.marker?.icon||"",model:t.model||"",link:t.link||"",description:t.description||"",pdfs:[...t.pdfs||[]],room:t.space&&t.area?t.space+"#"+t.area:"",busy:!1}:{name:"",binding:"virtual",bindingFilter:"",icon:"",model:"",link:"",description:"",pdfs:[],room:"",busy:!1}:this._showToast("Редактирование устройств доступно после переноса конфига на сервер")}_bindingCandidates(){const t=this.hass,e=new Set;for(const t of this._devices)t.id!==this._markerDialog?.devId&&("device"===t.bindingKind&&t.bindingRef&&e.add("device:"+t.bindingRef),"entity"===t.bindingKind&&t.bindingRef&&e.add("entity:"+t.bindingRef));const i=new Set;for(const t of this._devices)"device"===t.bindingKind&&t.name&&i.add(t.name.trim()+"|"+(t.area||""));const s=[];for(const a of Object.values(t.devices)){if("service"===a.entry_type)continue;const t="device:"+a.id;if(e.has(t))continue;const o=(a.name_by_user||a.name||a.id).trim();t!==this._markerDialog?.binding&&i.has(o+"|"+(a.area_id||""))||s.push({value:t,label:o,sub:(a.model||"устройство")+("Group"===a.model?" · Z2M-группа":"")})}const a=new Set(["group","template","derivative","min_max","threshold","integration","statistics","trend","utility_meter","tod","switch_as_x","schedule"]);for(const[i,o]of Object.entries(t.entities)){const n="entity:"+i;if(e.has(n))continue;const r=a.has(o.platform),l="group"===o.platform;if(!r&&!l)continue;if(o.hidden)continue;const c=t.states[i];s.push({value:n,label:o.name||c?.attributes?.friendly_name||i,sub:i.split(".")[0]+" · "+("group"===o.platform?"группа":"хелпер")})}const o=(this._markerDialog?.bindingFilter||"").toLowerCase().trim(),n=o?s.filter(t=>(t.label+" "+t.sub+" "+t.value).toLowerCase().includes(o)):s;return n.sort((t,e)=>t.label.localeCompare(e.label)),n.slice(0,200)}_allRoomsFlat(){const t=[];for(const e of this._serverCfg?.spaces||[])for(const i of e.rooms||[])i.area&&t.push({value:e.id+"#"+i.area,label:(e.title||e.id)+" · "+i.name});return t}_errText(t){if(!t)return"неизвестная ошибка";if("string"==typeof t)return t;if(t.message)return t.message;if(t.error)return t.error;if(null!=t.code)return"код "+t.code;try{return JSON.stringify(t)}catch{return String(t)}}async _pickMarkerFiles(t){const e=t.target,i=e.files?[...e.files]:[];if(e.value="",!i.length||!this._markerDialog)return;const s=this._markerDialog.devId||"new",a=[];for(const t of i)try{const e=new FormData;e.append("marker_id",s),e.append("file",t,t.name);const i=this.hass?.fetchWithAuth?await this.hass.fetchWithAuth("/api/houseplan/upload",{method:"POST",body:e}):await fetch("/api/houseplan/upload",{method:"POST",body:e,headers:this.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.hass.auth.data.access_token}`}:{}}),o=await i.json().catch(()=>({}));if(!i.ok||o.error){const t={too_large:"файл больше "+(o.max_mb||25)+" МБ",bad_ext:"недопустимый тип (нужен PDF/изображение)",unauthorized:"нужны права администратора"};throw new Error(t[o.error]||o.error||"HTTP "+i.status)}a.push({name:o.name||t.name,url:o.url})}catch(e){this._showToast("Файл «"+t.name+"» не загружен: "+this._errText(e))}a.length&&this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:[...this._markerDialog.pdfs,...a]},this._showToast("Прикреплено файлов: "+a.length))}_removeMarkerPdf(t){this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:this._markerDialog.pdfs.filter(e=>e.url!==t)})}async _saveMarker(){const t=this._markerDialog;if(t&&!t.busy)if("virtual"!==t.binding||t.name.trim()){this._markerDialog={...t,busy:!0};try{const e=this._serverCfg;let i;e.markers=e.markers||[];const[s,a]=t.room?t.room.split("#"):["",""];let o=s||null,n=a||null;"virtual"!==t.binding||o||(o=this._space),i=function(t,e,i){const[s,a]=t.split(":");return"device"===s?a:"entity"===s?"lg_"+a:e&&e.startsWith("v_")?e:i()}(t.binding,t.devId,()=>"v_"+Date.now().toString(36));const r=t.devId,l={id:i,binding:t.binding,name:t.name.trim()||null,icon:t.icon||null,model:t.model.trim()||null,link:t.link.trim()||null,description:t.description.trim()||null,pdfs:t.pdfs};("virtual"===t.binding||t.room)&&(l.space=o,l.area=n);const c=r?this._devices.find(t=>t.id===r):null,h=!!t.room&&null!=c&&(c.space!==o||c.area!==n);e.markers=e.markers.filter(t=>t.id!==i&&t.id!==r),e.markers.push(l);let d=null;if(!this._layout[i]||h){const t=this._spaceModel(o||void 0);let e=t.vb[0]+t.vb[2]/2,s=t.vb[1]+t.vb[3]/2;if(n){const i=t.rooms.find(t=>t.area===n);i&&([e,s]=this._roomCenter(i))}d=this._normPos(o||this._space,e,s),this._layout={...this._layout,[i]:d}}await this._saveConfigNow(),d&&await this.hass.callWS({type:"houseplan/layout/update",device_id:i,pos:d}),r&&r!==i&&(delete this._layout[r],await this.hass.callWS({type:"houseplan/layout/delete",device_id:r}).catch(()=>{})),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство сохранено")}catch(t){this._markerDialog={...this._markerDialog,busy:!1},this._showToast("Ошибка: "+this._errText(t))}}else this._showToast("Укажите имя виртуального устройства")}async _deleteMarker(){const t=this._markerDialog;if(!t)return;const e=t.devId?this._devices.find(e=>e.id===t.devId):null,i=t.name||"устройство";if(!confirm(`Убрать «${i}» с плана?`))return;const s=this._serverCfg;s.markers=s.markers||[],e&&"virtual"===e.bindingKind?s.markers=s.markers.filter(t=>t.id!==e.id):e&&e.marker?(s.markers=s.markers.filter(t=>t.id!==e.id),"device"===e.bindingKind&&e.bindingRef?s.markers.push({id:e.id,binding:"device:"+e.bindingRef,hidden:!0}):"entity"===e.bindingKind&&e.bindingRef&&s.markers.push({id:e.id,binding:"entity:"+e.bindingRef,hidden:!0})):e&&"device"===e.bindingKind&&e.bindingRef?s.markers.push({id:e.id,binding:"device:"+e.bindingRef,hidden:!0}):e&&"entity"===e.bindingKind&&e.bindingRef&&s.markers.push({id:e.id,binding:"entity:"+e.bindingRef,hidden:!0});try{await this._saveConfigNow(),e&&"virtual"===e.bindingKind&&this._layout[e.id]&&(delete this._layout[e.id],await this.hass.callWS({type:"houseplan/layout/delete",device_id:e.id}).catch(()=>{})),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство убрано с плана")}catch(t){this._showToast("Ошибка: "+this._errText(t))}}_normPos(t,e,i){const s=this._serverCfg.spaces.find(e=>e.id===t)?.aspect||1;return{s:t,x:e/Et,y:i/(Et/s)}}_openSpaceDialog(t,e){if(this._serverStorage&&this._serverCfg)if("edit"===t){const i=this._serverCfg.spaces.find(t=>t.id===e);if(!i)return;this._spaceDialog={mode:t,spaceId:e,title:i.title,planUrl:i.plan_url||null,planFile:null,busy:!1}}else this._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,busy:!1};else this._showToast("Интеграция House Plan не установлена — управление недоступно")}async _pickPlanFile(t){const e=t.target,i=e.files?.[0];if(!i||!this._spaceDialog)return;const s={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[i.type]||(i.name.toLowerCase().endsWith(".svg")?"svg":"");if(!s)return void this._showToast("Поддерживаются SVG, PNG, JPG, WebP");const a=new Uint8Array(await i.arrayBuffer());let o="";for(let t=0;t{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:1.414),e.onerror=()=>t(1.414),e.src=r});URL.revokeObjectURL(r),this._spaceDialog={...this._spaceDialog,planFile:{ext:s,b64:n,aspect:l,name:i.name}}}async _saveSpaceDialog(){const t=this._spaceDialog;if(!t||t.busy||!t.title.trim())return;if(!t.planFile&&!t.planUrl)return void this._showToast("Загрузите подложку — план этажа обязателен");const e="create"===t.mode&&0===(this._serverCfg?.spaces.length||0);this._spaceDialog={...t,busy:!0};try{const i=this._serverCfg;let s;if("create"===t.mode?(s={id:"s"+Date.now().toString(36),title:t.title.trim(),plan_url:null,aspect:1.414,view_box:[0,0,1,1],rooms:[],segments:[]},i.spaces.push(s)):(s=i.spaces.find(e=>e.id===t.spaceId),s.title=t.title.trim()),t.planFile){const e=await this.hass.callWS({type:"houseplan/plan/set",space_id:s.id,ext:t.planFile.ext,data:t.planFile.b64});s.plan_url=e.url,s.aspect=t.planFile.aspect}await this._saveConfigNow(),this._spaceDialog=null,"create"===t.mode&&(this._space=s.id),this._regSignature="",this._maybeRebuildDevices(),e?(this._markup=!0,this._tool="draw",this._path=[],this._cursorPt=null,this._showToast("Пространство добавлено. Обведите комнаты: кликайте по точкам сетки и замкните контур.")):this._showToast("create"===t.mode?"Пространство добавлено":"Пространство сохранено")}catch(t){this._spaceDialog={...this._spaceDialog,busy:!1},this._showToast("Ошибка: "+this._errText(t))}}async _deleteSpace(){const t=this._spaceDialog;if(!t||"edit"!==t.mode)return;const e=this._serverCfg.spaces.find(e=>e.id===t.spaceId);if(confirm(`Удалить пространство «${e.title}» со всеми комнатами и разметкой?`)){this._serverCfg.spaces=this._serverCfg.spaces.filter(e=>e.id!==t.spaceId);try{await this._saveConfigNow(),this._spaceDialog=null,this._space===t.spaceId&&(this._space=this._serverCfg.spaces[0]?.id||""),this._regSignature="",this._maybeRebuildDevices(),this._showToast("Пространство удалено")}catch(t){this._showToast("Ошибка удаления: "+this._errText(t))}}}async _saveConfigNow(){const t=await this.hass.callWS({type:"houseplan/config/set",config:this._serverCfg,expected_rev:this._cfgRev});this._cfgRev=t?.rev??this._cfgRev+1}render(){if(!this._config||!this.hass)return B;const t=this._model;if(!t.length)return j` + `,Et="houseplan_card_layout_v1",Rt="houseplan_card_cfg_v1",Tt="houseplan_card_zoom_v1",Ot=1e3,Ut=(t,e,i)=>{const s=new Event(e,{bubbles:!0,composed:!0});s.detail=i??{},t.dispatchEvent(s)},Ht=(t,e)=>{let i;return(...s)=>{clearTimeout(i),i=window.setTimeout(()=>t(...s),e)}};class Nt extends nt{constructor(){super(...arguments),this._space="f1",this._layout={},this._serverStorage=!1,this._loadOk=!1,this._loading=!1,this._loadTries=0,this._serverCfg=null,this._cfgRev=0,this._unsubCfg=null,this._devices=[],this._regSignature="",this._defPos={},this._tip=null,this._selId=null,this._toast="",this._markup=!1,this._tool="draw",this._path=[],this._pathSegs=[],this._cursorPt=null,this._areaSel="",this._nameSel="",this._roomDialog=!1,this._zoom=1,this._view=null,this._zoomBySpace={},this._pointers=new Map,this._panStart=null,this._pinchStart=null,this._suppressClick=!1,this._onboardingShown=!1,this._infoCard=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=t=>this._onKey(t),this._drag=null,this._dirtyPos=new Set,this._persistLayout=Ht(()=>{if(this._serverStorage){const t=[...this._dirtyPos];this._dirtyPos.clear();for(const e of t){const t=this._layout[e];t&&this.hass.callWS({type:"houseplan/layout/update",device_id:e,pos:t}).catch(t=>this._showToast(this._t("toast.pos_save_failed",{err:this._errText(t)})))}this._cacheSnapshot()}else localStorage.setItem(Et,JSON.stringify(this._layout))},600),this._saveConfig=Ht(()=>{this._serverCfg&&this.hass.callWS({type:"houseplan/config/set",config:this._serverCfg,expected_rev:this._cfgRev}).then(t=>{this._cfgRev=t?.rev??this._cfgRev+1}).catch(t=>{"conflict"===t?.code?(this._showToast(this._t("toast.conflict")),this._cancelPath(),this._reloadConfigOnly()):this._showToast(this._t("toast.cfg_save_failed",{err:this._errText(t)}))})},500)}connectedCallback(){super.connectedCallback(),window.addEventListener("keydown",this._keyHandler)}disconnectedCallback(){window.removeEventListener("keydown",this._keyHandler),this._roViewport?.disconnect(),this._roViewport=void 0,this._unsubCfg&&(this._unsubCfg(),this._unsubCfg=null),super.disconnectedCallback()}_onKey(t){if("Escape"===t.key){if(this._infoCard)return void(this._infoCard=null);if(this._markerDialog)return void(this._markerDialog=null)}if(!this._markup)return;return"Escape"===t.key||(t.ctrlKey||t.metaKey)&&"z"===t.key.toLowerCase()?this._roomDialog?(t.preventDefault(),void this._roomDialogCancel()):void("draw"===this._tool&&this._path.length&&(t.preventDefault(),this._undoPoint())):void 0}_undoPoint(){if(!this._path.length)return;if(1===this._path.length)return this._path=[],void(this._pathSegs=[]);const t=this._pathSegs[this._pathSegs.length-1];this._pathSegs=this._pathSegs.slice(0,-1),t&&this._removeSegmentByKey(t),this._path=this._path.slice(0,-1)}_removeSegmentByKey(t){const e=this._curSpaceCfg;if(!e?.segments)return;const i=this._segments.findIndex(e=>this._segKey([e[0],e[1]],[e[2],e[3]])===t);i>=0&&(e.segments.splice(i,1),this._saveConfig())}static getConfigElement(){return document.createElement("houseplan-card-editor")}static getStubConfig(){return{type:"custom:houseplan-card"}}setConfig(t){this._config={icon_size:2.5,show_temperature:!0,live_states:!0,show_signal:!0,...t},t.default_floor&&(this._space=t.default_floor);try{this._zoomBySpace=JSON.parse(localStorage.getItem(Tt)||"{}")||{}}catch{this._zoomBySpace={}}try{const e=JSON.parse(localStorage.getItem(Rt)||"null");e&&e.config&&Array.isArray(e.config.spaces)&&(this._serverCfg=e.config,this._cfgRev=e.rev||0,this._layout=e.layout||{},this._serverStorage=!0,t.default_floor?this._space=t.default_floor:this._model.find(t=>t.id===this._space)||(this._space=this._model[0]?.id||this._space))}catch{}}_cacheSnapshot(){if(this._serverCfg)try{localStorage.setItem(Rt,JSON.stringify({config:this._serverCfg,rev:this._cfgRev,layout:this._layout}))}catch{}}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}get _model(){return this._serverCfg?this._serverCfg.spaces.map(t=>{const e=Ot/t.aspect;return{id:t.id,title:t.title,vb:[t.view_box[0]*Ot,t.view_box[1]*e,t.view_box[2]*Ot,t.view_box[3]*e],bg:t.plan_url?{href:t.plan_url,x:0,y:0,w:Ot,h:e}:null,rooms:t.rooms.map(t=>({id:t.id,name:t.name,area:t.area??null,x:null!=t.x?t.x*Ot:void 0,y:null!=t.y?t.y*e:void 0,w:null!=t.w?t.w*Ot:void 0,h:null!=t.h?t.h*e:void 0,poly:t.poly?t.poly.map(t=>[t[0]*Ot,t[1]*e]):void 0}))}}):[]}_spaceModel(t){const e=this._model;return e.find(e=>e.id===(t??this._space))||e[0]}get _areaToSpace(){const t={};for(const e of this._model)for(const i of e.rooms)i.area&&(t[i.area]={space:e.id,room:i});return t}get _settings(){return this._serverCfg?.settings||{}}get _showAll(){return!!this._settings.show_all}_toggleShowAll(){this._serverCfg&&(this._serverCfg={...this._serverCfg,settings:{...this._serverCfg.settings,show_all:!this._showAll}},this._regSignature="",this._maybeRebuildDevices(),this._saveConfig(),this.requestUpdate())}get _excluded(){const t=this._settings.exclude_integrations;return t?new Set(t):ct}willUpdate(t){t.has("hass")&&this.hass&&(!this._loadOk&&!this._loading&&this._loadTries<8&&this._loadFromServer(),this._maybeRebuildDevices())}updated(){const t=this._stageEl;t&&!this._roViewport&&(this._roViewport=new ResizeObserver(()=>this._refitView()),this._roViewport.observe(t)),t&&!this._view&&this._refitView(),this._serverStorage&&this._loadOk&&0===this._model.length&&!this._spaceDialog&&!this._onboardingShown&&(this._onboardingShown=!0,this._openSpaceDialog("create"))}async _loadFromServer(){this._loading=!0,this._loadTries++;try{const[t,e]=await Promise.all([this.hass.callWS({type:"houseplan/config/get"}),this.hass.callWS({type:"houseplan/layout/get"})]);this._loadOk=!0,this._serverStorage=!0;const i=t?.config;this._serverCfg=i&&Array.isArray(i.spaces)?i:null,this._cfgRev=t?.rev||0,this._layout=e?.layout||{},this._unsubCfg||(this._unsubCfg=await this.hass.connection.subscribeEvents(t=>{(t?.data?.rev??-1)!==this._cfgRev&&this._reloadConfigOnly()},"houseplan_config_updated")),this._norm&&!this._model.find(t=>t.id===this._space)&&(this._space=this._model[0]?.id||this._space),this._cacheSnapshot(),this._restoreZoom()}catch(t){if(this._loadTries>=8){this._serverStorage=!1,this._serverCfg=null;try{this._layout=JSON.parse(localStorage.getItem(Et)||"{}")||{}}catch{this._layout={}}}}finally{this._loading=!1}this._regSignature="",this.requestUpdate()}async _reloadConfigOnly(){try{const t=await this.hass.callWS({type:"houseplan/config/get"}),e=t?.config;this._serverCfg=e&&Array.isArray(e.spaces)?e:null,this._cfgRev=t?.rev||0,this._cacheSnapshot(),this._regSignature="",this._maybeRebuildDevices(),this.requestUpdate()}catch{}}_maybeRebuildDevices(){const t=this.hass;if(!t?.devices||!t?.entities||!t?.areas)return;const e=Object.keys(t.devices).length+":"+Object.keys(t.entities).length+":"+Object.keys(t.areas).length+":"+(this._norm?"n":"l")+":"+zt(t,this._config?.language);e===this._regSignature&&this._devices.length||(this._regSignature=e,this._devices=St({hass:t,areaToSpace:Object.fromEntries(Object.entries(this._areaToSpace).map(([t,e])=>[t,e.space])),markers:this._markers,settings:this._settings,excluded:this._excluded,showAll:this._showAll,firstSpaceId:this._model[0]?.id||"",loc:t=>this._t(t)}),this._defPos=this._defaultPositions())}get _markers(){return this._serverCfg?.markers||[]}_roomLqi(t){if(!t)return null;const e=[];for(const i of this._devices){if(i.area!==t||i.virtual)continue;const s=xt(this.hass,i.entities);null!=s&&e.push(s)}return mt(e)}_roomBounds(t){if(t.poly&&t.poly.length){const e=t.poly.map(t=>t[0]),i=t.poly.map(t=>t[1]),s=Math.min(...e),a=Math.min(...i);return{x:s,y:a,w:Math.max(...e)-s,h:Math.max(...i)-a}}return{x:t.x??0,y:t.y??0,w:t.w??0,h:t.h??0}}_defaultPositions(){const t={},e=(this._config?.icon_size??2.5)/100*Ot*1.3;for(const i of this._model)for(const s of i.rooms){if(!s.area)continue;const a=this._devices.filter(t=>t.area===s.area&&t.space===i.id);if(!a.length)continue;const o=this._roomBounds(s),r=.1*Math.min(o.w,o.h),n=o.w-2*r,l=o.h-2*r,c=Math.max(1,Math.round(Math.sqrt(a.length*n/Math.max(l,1)))),h=Math.ceil(a.length/c),d=n/c,p=l/Math.max(h,1),_=a.map((t,e)=>({x:o.x+r+d*(e%c+.5),y:o.y+r+p*(Math.floor(e/c)+.5)}));ft(_,o,e,.5*r),a.forEach((e,i)=>t[e.id]=_[i])}return t}_pos(t){const e=this._spaceModel(t.space),i=this._layout[t.id];if(i)if(this._norm){if(i.s===t.space){const e=this._serverCfg.spaces.find(e=>e.id===t.space)?.aspect||1;return{x:i.x*Ot,y:i.y*(Ot/e)}}}else if(void 0===i.s)return{x:i.x,y:i.y};if(this._defPos[t.id])return this._defPos[t.id];const s=e.vb;return{x:s[0]+s[2]/2,y:s[1]+s[3]/2}}_savePos(t,e,i){if(this._norm){const s=this._gridPitch,a=Math.round(e/s)*s,o=Math.round(i/s)*s,r=this._serverCfg.spaces.find(e=>e.id===t.space)?.aspect||1;this._layout={...this._layout,[t.id]:{s:t.space,x:a/Ot,y:o/(Ot/r)}}}else this._layout={...this._layout,[t.id]:{x:Math.round(e),y:Math.round(i)}};this._dirtyPos.add(t.id),this._persistLayout()}_stateClass(t){if(!this._config?.live_states)return"";const e=t.primary?this.hass.states[t.primary]:void 0;if(!e)return"";if("unavailable"===e.state)return"unavail";const i=e.entity_id.split(".")[0];if(["light","switch","fan","humidifier"].includes(i))return"on"===e.state?"on":"";if("cover"===i||"valve"===i)return["open","opening"].includes(e.state)?"open":"";if("lock"===i)return["unlocked","open"].includes(e.state)?"open":"";if("binary_sensor"===i){const t=e.attributes?.device_class;if(["door","window","garage_door","opening","gas","smoke","moisture","problem"].includes(t))return"on"===e.state?"open":""}return"media_player"===i?["playing","on"].includes(e.state)?"on":"":"vacuum"===i&&["cleaning","returning"].includes(e.state)?"on":""}_liveTemp(t){return this._config?.show_temperature?"mdi:thermometer"!==t.icon&&"mdi:air-filter"!==t.icon?null:wt(this.hass,t.entities):null}_openMoreInfo(t){t?Ut(this,"hass-more-info",{entityId:t}):this._showToast(this._t("toast.no_entity"))}_clickDevice(t,e){t.stopPropagation(),this._drag?.moved||this._suppressClick||this._markup||(this._infoCard=e)}_t(t,e){return Dt(zt(this.hass,this._config?.language),t,e)}get _stageEl(){return this.renderRoot.querySelector(".stage")}_stageAspect(){const t=this._stageEl,e=this._spaceModel().vb;return t&&t.clientHeight?t.clientWidth/t.clientHeight:e[2]/e[3]}_viewOr(t){return this._view&&this._view.w?this._view:gt(t,this._stageAspect())}_screenToVb(t,e){const i=this._stageEl,s=this._viewOr(this._spaceModel().vb),a=i?.clientWidth||1,o=i?.clientHeight||1;return[s.x+t/a*s.w,s.y+e/o*s.h]}_clampView(t,e){return{w:t.w,h:t.h,x:Math.max(e.x,Math.min(e.x+e.w-t.w,t.x)),y:Math.max(e.y,Math.min(e.y+e.h-t.h,t.y))}}_applyView(t,e,i){const s=this._spaceModel().vb,a=gt(s,this._stageAspect()),o=Math.min(8,Math.max(1,t)),r=a.w/o,n=a.h/o,l=this._viewOr(s),c=e??l.x+l.w/2,h=i??l.y+l.h/2;this._zoom=o,this._view=this._clampView({x:c-r/2,y:h-n/2,w:r,h:n},a)}_refitView(){if(!this._stageEl)return;const t=this._view;this._applyView(this._zoom,t?t.x+t.w/2:void 0,t?t.y+t.h/2:void 0),this.requestUpdate()}_zoomAt(t,e,i){const s=this._stageEl;if(!s)return;const a=gt(this._spaceModel().vb,this._stageAspect()),o=Math.min(8,Math.max(1,i)),r=s.clientWidth,n=s.clientHeight,l=this._screenToVb(t,e),c=a.w/o,h=a.h/o;this._zoom=o,this._view=this._clampView({x:l[0]-t/r*c,y:l[1]-e/n*h,w:c,h:h},a)}_onWheel(t){const e=this._stageEl;if(!e)return;t.preventDefault();const i=e.getBoundingClientRect(),s=t.deltaY<0?1.15:1/1.15;this._zoomAt(t.clientX-i.left,t.clientY-i.top,this._zoom*s),this._saveZoom()}_stepZoom(t){const e=this._stageEl;e&&(this._zoomAt(e.clientWidth/2,e.clientHeight/2,this._zoom*(t>0?1.4:1/1.4)),this._saveZoom())}_resetZoom(){const t=this._spaceModel().vb;this._zoom=1,this._view=gt(t,this._stageAspect()),this._saveZoom()}_saveZoom(){this._zoomBySpace={...this._zoomBySpace,[this._space]:this._zoom};try{localStorage.setItem(Tt,JSON.stringify(this._zoomBySpace))}catch{}}_restoreZoom(){const t=this._zoomBySpace[this._space]||1;this._zoom=t,this._view=null,requestAnimationFrame(()=>{if(!this._stageEl)return;const e=this._spaceModel().vb;this._applyView(t,e[0]+e[2]/2,e[1]+e[3]/2),this.requestUpdate()})}_stagePointerDown(t){if(this._drag||this._markup)return;if(t.target.closest(".dev"))return;this._pointers.set(t.pointerId,{x:t.clientX,y:t.clientY});const e=this._viewOr(this._spaceModel().vb);if(1===this._pointers.size)this._panStart={sx:t.clientX,sy:t.clientY,vx:e.x,vy:e.y},this._suppressClick=!1;else if(2===this._pointers.size){const t=[...this._pointers.values()],e=Math.hypot(t[0].x-t[1].x,t[0].y-t[1].y);this._pinchStart={dist:e,zoom:this._zoom},this._panStart=null}}_stagePointerMove(t){if(this._pointers.has(t.pointerId)){if(this._pointers.set(t.pointerId,{x:t.clientX,y:t.clientY}),this._pinchStart&&this._pointers.size>=2){const t=[...this._pointers.values()],e=Math.hypot(t[0].x-t[1].x,t[0].y-t[1].y)/(this._pinchStart.dist||1),i=this._stageEl.getBoundingClientRect(),s=(t[0].x+t[1].x)/2-i.left,a=(t[0].y+t[1].y)/2-i.top;this._zoomAt(s,a,this._pinchStart.zoom*e),this._suppressClick=!0,this._saveZoom()}else if(this._panStart){const e=t.clientX-this._panStart.sx,i=t.clientY-this._panStart.sy;if(Math.abs(e)+Math.abs(i)>4&&(this._suppressClick=!0),this._zoom>1&&this._view){const t=this._stageEl,s=this._view,a=gt(this._spaceModel().vb,this._stageAspect());this._view=this._clampView({x:this._panStart.vx-e/t.clientWidth*s.w,y:this._panStart.vy-i/t.clientHeight*s.h,w:s.w,h:s.h},a)}}}else this._markupMove(t)}_stagePointerUp(t){this._pointers.delete(t.pointerId),this._pointers.size<2&&(this._pinchStart=null),0===this._pointers.size&&(this._panStart=null,setTimeout(()=>this._suppressClick=!1,0))}_clickRoom(t){var e;!this._suppressClick&&t.area&&(e="/config/areas/area/"+t.area,history.pushState(null,"",e),Ut(window,"location-changed",{replace:!1}))}_pointerDown(t,e){if(this._markup)return;t.preventDefault();const i=this._pos(e);this._drag={id:e.id,sx:t.clientX,sy:t.clientY,ox:i.x,oy:i.y,moved:!1},t.target.setPointerCapture(t.pointerId),this._tip=null}_pointerMove(t,e){if(!this._drag||this._drag.id!==e.id)return;const i=this.renderRoot.querySelector(".stage");if(!i)return;const s=this._spaceModel().vb,a=i.getBoundingClientRect(),o=this._viewOr(s),r=(t.clientX-this._drag.sx)/a.width*o.w,n=(t.clientY-this._drag.sy)/a.height*o.h;Math.abs(t.clientX-this._drag.sx)+Math.abs(t.clientY-this._drag.sy)>3&&(this._drag.moved=!0);const l=.008*Math.min(s[2],s[3]),c=Math.max(s[0]+l,Math.min(s[0]+s[2]-l,this._drag.ox+r)),h=Math.max(s[1]+l,Math.min(s[1]+s[3]-l,this._drag.oy+n));this._savePos(e,c,h)}_pointerUp(t,e){if(!this._drag||this._drag.id!==e.id)return;const i=this._drag.moved;this._drag=i?this._drag:null,i&&(this._selId=e.id,window.setTimeout(()=>this._drag=null,0))}_resetLayout(){confirm(this._t("confirm.reset_layout"))&&(this._layout={},this._persistLayout())}_showToast(t){this._toast=t,clearTimeout(this._toastTimer),this._toastTimer=window.setTimeout(()=>{this._toast=""},3500)}_showTip(t,e,i,s){this._drag||(this._tip={x:t.clientX,y:t.clientY,title:e,meta:i,lqi:s})}get _gridPitch(){return Ot/240}get _curSpaceCfg(){return this._serverCfg?.spaces.find(t=>t.id===this._space)}get _spaceH(){const t=this._curSpaceCfg;return t?Ot/t.aspect:Ot}get _segments(){const t=this._curSpaceCfg,e=this._spaceH;return(t?.segments||[]).map(t=>[t[0]*Ot,t[1]*e,t[2]*Ot,t[3]*e])}_toggleMarkup(){this._norm?(this._markup=!this._markup,this._path=[],this._cursorPt=null,this._tool="draw"):this._showToast(this._t("toast.markup_needs_server"))}_svgPoint(t){const e=this.renderRoot.querySelector(".stage").getBoundingClientRect();return this._screenToVb(t.clientX-e.left,t.clientY-e.top)}_snap(t){const e=this._gridPitch;return[ut(t[0],e),ut(t[1],e)]}_samePt(t,e){return function(t,e,i=.001){return Math.abs(t[0]-e[0])this._segKey([t[0],t[1]],[t[2],t[3]])===a);return!o&&(i.segments=i.segments||[],i.segments.push([t[0]/Ot,t[1]/s,e[0]/Ot,e[1]/s]),this._saveConfig(),!0)}_distToSeg(t,e){const[i,s]=t,[a,o,r,n]=e,l=r-a,c=n-o;let h=((i-a)*l+(s-o)*c)/(l*l+c*c||1);h=Math.max(0,Math.min(1,h));const d=a+h*l,p=o+h*c;return Math.hypot(i-d,s-p)}_pointInRoom(t,e){return e.poly?function(t,e){let i=!1;for(let s=0,a=e.length-1;st[1]!=l>t[1]&&t[0]<(n-o)*(t[1]-r)/(l-r)+o&&(i=!i)}return i}(t,e.poly):null!=e.x&&t[0]>=e.x&&t[0]<=e.x+e.w&&t[1]>=e.y&&t[1]<=e.y+e.h}_markupClick(t){if(!this._markup)return;const e=this._svgPoint(t);if("erase"===this._tool){const t=this._curSpaceCfg;if(!t?.segments?.length)return;const i=this._segments;let s=-1,a=.5*this._gridPitch;return i.forEach((t,i)=>{const o=this._distToSeg(e,t);o=0&&(t.segments.splice(s,1),this._saveConfig(),this.requestUpdate()))}if("delroom"===this._tool){const t=[...this._spaceModel().rooms].reverse().find(t=>this._pointInRoom(e,t));if(!t)return;if(!confirm(this._t("confirm.delete_room",{name:t.name})))return;const i=this._curSpaceCfg;return i.rooms=i.rooms.filter(e=>e.id!==t.id),this._saveConfig(),this._regSignature="",this._maybeRebuildDevices(),void this.requestUpdate()}const i=this._snap(e);if(!this._path.length)return this._path=[i],void(this._pathSegs=[]);const s=this._path[this._path.length-1];if(this._samePt(i,s))return;const a=this._addSegment(s,i);this._pathSegs=[...this._pathSegs,a?this._segKey(s,i):null],this._path=[...this._path,i],this._path.length>=4&&this._samePt(i,this._path[0])&&(this._cursorPt=null,this._nameSel="",this._areaSel="",this._roomDialog=!0)}get _contourClosed(){return this._path.length>=4&&this._samePt(this._path[0],this._path[this._path.length-1])}_markupMove(t){this._markup&&"draw"===this._tool&&this._path.length&&!this._contourClosed&&(this._cursorPt=this._snap(this._svgPoint(t)))}_saveRoom(){this._areaSel&&this._commitRoom()}_saveRoomNoArea(){this._nameSel.trim()&&(this._areaSel="",this._commitRoom())}_commitRoom(){if(!this._contourClosed)return;const t=this._curSpaceCfg;if(!t)return;const e=this._spaceH,i=this._path.slice(0,-1),s=this._areaSel?this.hass.areas[this._areaSel]?.name:"";t.rooms.push({id:"r"+Date.now().toString(36),name:this._nameSel||s||this._t("room.default_name"),area:this._areaSel||null,poly:i.map(t=>[t[0]/Ot,t[1]/e])}),this._saveConfig(),this._path=[],this._pathSegs=[];const a=this._areaSel;this._areaSel="",this._nameSel="",this._roomDialog=!1,this._regSignature="",this._maybeRebuildDevices();let o=0;if(a){const t=this._serverCfg?.spaces.find(t=>t.id===this._space)?.aspect||1,e=Ot/t,i={...this._layout};for(const t of this._devices){if(t.area!==a||t.space!==this._space)continue;if(o++,this._layout[t.id])continue;const s=this._defPos[t.id];s&&(i[t.id]={s:this._space,x:s.x/Ot,y:s.y/e},this._dirtyPos.add(t.id))}this._layout=i,this._persistLayout()}const r=this._model.find(t=>t.id===this._space)?.rooms.length||0;this._showToast(a?this._t("toast.room_saved",{n:r,added:o}):this._t("toast.room_saved_no_area",{n:r}))}_cancelPath(){this._path=[],this._pathSegs=[],this._cursorPt=null,this._roomDialog=!1}_roomDialogCancel(){this._roomDialog=!1,this._undoPoint()}get _freeAreas(){const t=new Set;for(const e of this._serverCfg?.spaces||[])for(const i of e.rooms||[])i.area&&t.add(i.area);return Object.values(this.hass?.areas||{}).filter(e=>!t.has(e.area_id)).sort((t,e)=>(t.name||"").localeCompare(e.name||""))}_openMarkerDialog(t){this._norm?this._markerDialog=t?{devId:t.id,name:t.name,binding:"virtual"===t.bindingKind?"virtual":t.bindingKind+":"+t.bindingRef,bindingFilter:"",icon:t.marker?.icon||"",model:t.model||"",link:t.link||"",description:t.description||"",pdfs:[...t.pdfs||[]],room:t.space&&t.area?t.space+"#"+t.area:"",busy:!1}:{name:"",binding:"virtual",bindingFilter:"",icon:"",model:"",link:"",description:"",pdfs:[],room:"",busy:!1}:this._showToast(this._t("toast.marker_needs_server"))}_bindingCandidates(){const t=this.hass,e=new Set;for(const t of this._devices)t.id!==this._markerDialog?.devId&&("device"===t.bindingKind&&t.bindingRef&&e.add("device:"+t.bindingRef),"entity"===t.bindingKind&&t.bindingRef&&e.add("entity:"+t.bindingRef));const i=new Set;for(const t of this._devices)"device"===t.bindingKind&&t.name&&i.add(t.name.trim()+"|"+(t.area||""));const s=[];for(const a of Object.values(t.devices)){if("service"===a.entry_type)continue;const t="device:"+a.id;if(e.has(t))continue;const o=(a.name_by_user||a.name||a.id).trim();t!==this._markerDialog?.binding&&i.has(o+"|"+(a.area_id||""))||s.push({value:t,label:o,sub:(a.model||this._t("marker.sub_device"))+("Group"===a.model?this._t("marker.sub_z2m_group"):"")})}const a=new Set(["group","template","derivative","min_max","threshold","integration","statistics","trend","utility_meter","tod","switch_as_x","schedule"]);for(const[i,o]of Object.entries(t.entities)){const r="entity:"+i;if(e.has(r))continue;const n=a.has(o.platform),l="group"===o.platform;if(!n&&!l)continue;if(o.hidden)continue;const c=t.states[i];s.push({value:r,label:o.name||c?.attributes?.friendly_name||i,sub:i.split(".")[0]+" · "+("group"===o.platform?this._t("marker.sub_group"):this._t("marker.sub_helper"))})}const o=(this._markerDialog?.bindingFilter||"").toLowerCase().trim(),r=o?s.filter(t=>(t.label+" "+t.sub+" "+t.value).toLowerCase().includes(o)):s;return r.sort((t,e)=>t.label.localeCompare(e.label)),r.slice(0,200)}_allRoomsFlat(){const t=[];for(const e of this._serverCfg?.spaces||[])for(const i of e.rooms||[])i.area&&t.push({value:e.id+"#"+i.area,label:(e.title||e.id)+" · "+i.name});return t}_errText(t){if(!t)return this._t("err.unknown");if("string"==typeof t)return t;if(t.message)return t.message;if(t.error)return t.error;if(null!=t.code)return this._t("err.code",{code:t.code});try{return JSON.stringify(t)}catch{return String(t)}}async _pickMarkerFiles(t){const e=t.target,i=e.files?[...e.files]:[];if(e.value="",!i.length||!this._markerDialog)return;const s=this._markerDialog.devId||"new",a=[];for(const t of i)try{const e=new FormData;e.append("marker_id",s),e.append("file",t,t.name);const i=this.hass?.fetchWithAuth?await this.hass.fetchWithAuth("/api/houseplan/upload",{method:"POST",body:e}):await fetch("/api/houseplan/upload",{method:"POST",body:e,headers:this.hass?.auth?.data?.access_token?{authorization:`Bearer ${this.hass.auth.data.access_token}`}:{}}),o=await i.json().catch(()=>({}));if(!i.ok||o.error){const t={too_large:this._t("err.too_large",{mb:o.max_mb||25}),bad_ext:this._t("err.bad_ext"),unauthorized:this._t("err.unauthorized")};throw new Error(t[o.error]||o.error||"HTTP "+i.status)}a.push({name:o.name||t.name,url:o.url})}catch(e){this._showToast(this._t("toast.file_failed",{name:t.name,err:this._errText(e)}))}a.length&&this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:[...this._markerDialog.pdfs,...a]},this._showToast(this._t("toast.files_attached",{n:a.length})))}_removeMarkerPdf(t){this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:this._markerDialog.pdfs.filter(e=>e.url!==t)})}async _saveMarker(){const t=this._markerDialog;if(t&&!t.busy)if("virtual"!==t.binding||t.name.trim()){this._markerDialog={...t,busy:!0};try{const e=this._serverCfg;let i;e.markers=e.markers||[];const[s,a]=t.room?t.room.split("#"):["",""];let o=s||null,r=a||null;"virtual"!==t.binding||o||(o=this._space),i=function(t,e,i){const[s,a]=t.split(":");return"device"===s?a:"entity"===s?"lg_"+a:e&&e.startsWith("v_")?e:i()}(t.binding,t.devId,()=>"v_"+Date.now().toString(36));const n=t.devId,l={id:i,binding:t.binding,name:t.name.trim()||null,icon:t.icon||null,model:t.model.trim()||null,link:t.link.trim()||null,description:t.description.trim()||null,pdfs:t.pdfs};("virtual"===t.binding||t.room)&&(l.space=o,l.area=r);const c=n?this._devices.find(t=>t.id===n):null,h=!!t.room&&null!=c&&(c.space!==o||c.area!==r);e.markers=e.markers.filter(t=>t.id!==i&&t.id!==n),e.markers.push(l);let d=null;if(!this._layout[i]||h){const t=this._spaceModel(o||void 0);let e=t.vb[0]+t.vb[2]/2,s=t.vb[1]+t.vb[3]/2;if(r){const i=t.rooms.find(t=>t.area===r);i&&([e,s]=this._roomCenter(i))}d=this._normPos(o||this._space,e,s),this._layout={...this._layout,[i]:d}}await this._saveConfigNow(),d&&await this.hass.callWS({type:"houseplan/layout/update",device_id:i,pos:d}),n&&n!==i&&(delete this._layout[n],await this.hass.callWS({type:"houseplan/layout/delete",device_id:n}).catch(()=>{})),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.marker_saved"))}catch(t){this._markerDialog={...this._markerDialog,busy:!1},this._showToast(this._t("toast.error",{err:this._errText(t)}))}}else this._showToast(this._t("toast.virtual_name_required"))}async _deleteMarker(){const t=this._markerDialog;if(!t)return;const e=t.devId?this._devices.find(e=>e.id===t.devId):null,i=t.name||this._t("device.fallback");if(!confirm(this._t("confirm.remove_marker",{name:i})))return;const s=this._serverCfg;s.markers=s.markers||[],e&&"virtual"===e.bindingKind?s.markers=s.markers.filter(t=>t.id!==e.id):e&&e.marker?(s.markers=s.markers.filter(t=>t.id!==e.id),"device"===e.bindingKind&&e.bindingRef?s.markers.push({id:e.id,binding:"device:"+e.bindingRef,hidden:!0}):"entity"===e.bindingKind&&e.bindingRef&&s.markers.push({id:e.id,binding:"entity:"+e.bindingRef,hidden:!0})):e&&"device"===e.bindingKind&&e.bindingRef?s.markers.push({id:e.id,binding:"device:"+e.bindingRef,hidden:!0}):e&&"entity"===e.bindingKind&&e.bindingRef&&s.markers.push({id:e.id,binding:"entity:"+e.bindingRef,hidden:!0});try{await this._saveConfigNow(),e&&"virtual"===e.bindingKind&&this._layout[e.id]&&(delete this._layout[e.id],await this.hass.callWS({type:"houseplan/layout/delete",device_id:e.id}).catch(()=>{})),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.marker_removed"))}catch(t){this._showToast(this._t("toast.error",{err:this._errText(t)}))}}_normPos(t,e,i){const s=this._serverCfg.spaces.find(e=>e.id===t)?.aspect||1;return{s:t,x:e/Ot,y:i/(Ot/s)}}_openSpaceDialog(t,e){if(this._serverStorage&&this._serverCfg)if("edit"===t){const i=this._serverCfg.spaces.find(t=>t.id===e);if(!i)return;this._spaceDialog={mode:t,spaceId:e,title:i.title,planUrl:i.plan_url||null,planFile:null,busy:!1}}else this._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,busy:!1};else this._showToast(this._t("toast.integration_missing"))}async _pickPlanFile(t){const e=t.target,i=e.files?.[0];if(!i||!this._spaceDialog)return;const s={"image/svg+xml":"svg","image/png":"png","image/jpeg":"jpg","image/webp":"webp"}[i.type]||(i.name.toLowerCase().endsWith(".svg")?"svg":"");if(!s)return void this._showToast(this._t("toast.plan_formats"));const a=new Uint8Array(await i.arrayBuffer());let o="";for(let t=0;t{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:1.414),e.onerror=()=>t(1.414),e.src=n});URL.revokeObjectURL(n),this._spaceDialog={...this._spaceDialog,planFile:{ext:s,b64:r,aspect:l,name:i.name}}}async _saveSpaceDialog(){const t=this._spaceDialog;if(!t||t.busy||!t.title.trim())return;if(!t.planFile&&!t.planUrl)return void this._showToast(this._t("toast.plan_required"));const e="create"===t.mode&&0===(this._serverCfg?.spaces.length||0);this._spaceDialog={...t,busy:!0};try{const i=this._serverCfg;let s;if("create"===t.mode?(s={id:"s"+Date.now().toString(36),title:t.title.trim(),plan_url:null,aspect:1.414,view_box:[0,0,1,1],rooms:[],segments:[]},i.spaces.push(s)):(s=i.spaces.find(e=>e.id===t.spaceId),s.title=t.title.trim()),t.planFile){const e=await this.hass.callWS({type:"houseplan/plan/set",space_id:s.id,ext:t.planFile.ext,data:t.planFile.b64});s.plan_url=e.url,s.aspect=t.planFile.aspect}await this._saveConfigNow(),this._spaceDialog=null,"create"===t.mode&&(this._space=s.id),this._regSignature="",this._maybeRebuildDevices(),e?(this._markup=!0,this._tool="draw",this._path=[],this._cursorPt=null,this._showToast(this._t("toast.space_added_onboard"))):this._showToast("create"===t.mode?this._t("toast.space_added"):this._t("toast.space_saved"))}catch(t){this._spaceDialog={...this._spaceDialog,busy:!1},this._showToast(this._t("toast.error",{err:this._errText(t)}))}}async _deleteSpace(){const t=this._spaceDialog;if(!t||"edit"!==t.mode)return;const e=this._serverCfg.spaces.find(e=>e.id===t.spaceId);if(confirm(this._t("confirm.delete_space",{title:e.title}))){this._serverCfg.spaces=this._serverCfg.spaces.filter(e=>e.id!==t.spaceId);try{await this._saveConfigNow(),this._spaceDialog=null,this._space===t.spaceId&&(this._space=this._serverCfg.spaces[0]?.id||""),this._regSignature="",this._maybeRebuildDevices(),this._showToast(this._t("toast.space_deleted"))}catch(t){this._showToast(this._t("toast.delete_failed",{err:this._errText(t)}))}}}async _saveConfigNow(){const t=await this.hass.callWS({type:"houseplan/config/set",config:this._serverCfg,expected_rev:this._cfgRev});this._cfgRev=t?.rev??this._cfgRev+1}render(){if(!this._config||!this.hass)return B;const t=this._model;if(!t.length)return F`
-
${this._config.title||"План дома"}
+
${this._config.title||this._t("card.title")}
-

Пространств пока нет.

- ${this._serverStorage?j`

Добавьте первое пространство и загрузите план этажа.

+

${this._t("empty.no_spaces")}

+ ${this._serverStorage?F`

${this._t("empty.add_first")}

`:j`

Установите интеграцию House Plan и добавьте запись в «Устройства и службы».

`} + ${this._t("btn.add_space")} + `:F`

${this._t("empty.install")}

`}
${this._spaceDialog?this._renderSpaceDialog():B} - ${this._toast?j`
${this._toast}
`:B} -
`;const e=this._spaceModel(),i=e.vb,s=this._devices.filter(t=>t.space===e.id),a=this._config.icon_size??2.5,o=a>8?2.5:a,n=this._viewOr(i);return j` + ${this._toast?F`
${this._toast}
`:B} +
`;const e=this._spaceModel(),i=e.vb,s=this._devices.filter(t=>t.space===e.id),a=this._config.icon_size??2.5,o=a>8?2.5:a,r=this._viewOr(i);return F`
- ${this._config.title||"План дома"} + ${this._config.title||this._t("card.title")}
- ${t.map(t=>j``)} - ${this._norm?j``:B}
- ${s.length} устр. + ${this._t("count.devices",{n:s.length})}
- + - + title=${this._t("title.zoom_reset")}> +
- ${this._norm?j``:B} - ${this._norm?j` - `:B}
@@ -799,243 +799,243 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow @pointerup=${t=>this._stagePointerUp(t)} @pointercancel=${t=>this._stagePointerUp(t)}>
- + ${this._markup?this._renderMarkupDefs(i):B} - ${e.bg?F``:B} - ${e.rooms.filter(t=>t.area||this._markup).map(t=>{const i="room "+(e.bg?"overlay":"yard")+(this._markup?" outlined":""),s=e=>this._showTip(e,t.name,"комната — открыть зону",this._config?.show_signal?this._roomLqi(t.area):null),a=!e.bg||this._markup,o=this._roomCenter(t),n=t.poly?F``:B} + ${e.rooms.filter(t=>t.area||this._markup).map(t=>{const i="room "+(e.bg?"overlay":"yard")+(this._markup?" outlined":""),s=e=>this._showTip(e,t.name,this._t("tip.room"),this._config?.show_signal?this._roomLqi(t.area):null),a=!e.bg||this._markup,o=this._roomCenter(t),r=t.poly?j`this._clickRoom(t)} @mousemove=${s} - @mouseleave=${()=>this._tip=null}>`:F`this._tip=null}>`:j`this._clickRoom(t)} @mousemove=${s} - @mouseleave=${()=>this._tip=null}>`;return F`${n}${a?F`${t.name}`:B}`})} + @mouseleave=${()=>this._tip=null}>`;return j`${r}${a?j`${t.name}`:B}`})} ${this._markup?this._renderMarkupLayer(i):B} -
- ${s.map(t=>this._renderDevice(t,n))} +
+ ${s.map(t=>this._renderDevice(t,r))}
- ${this._zoom>1?j`
${Math.round(100*this._zoom)}%
`:B} + ${this._zoom>1?F`
${Math.round(100*this._zoom)}%
`:B}
${this._roomDialog?this._renderRoomDialog():B} ${this._spaceDialog?this._renderSpaceDialog():B} ${this._markerDialog?this._renderMarkerDialog():B} ${this._infoCard?this._renderInfoCard():B} - ${this._tip?j`
- ${this._tip.title}${this._tip.meta?j`${this._tip.meta}`:B} - ${null!=this._tip.lqi?j`средний сигнал zigbee: - ${this._tip.lqi}`:B} + ${this._tip?F`
+ ${this._tip.title}${this._tip.meta?F`${this._tip.meta}`:B} + ${null!=this._tip.lqi?F`${this._t("tip.lqi")} + ${this._tip.lqi}`:B}
`:B} - ${this._toast?j`
${this._toast}
`:B} + ${this._toast?F`
${this._toast}
`:B} - `}_renderDevice(t,e){const i=this._pos(t),s=(i.x-e.x)/e.w*100,a=(i.y-e.y)/e.h*100,o=this._stateClass(t),n=this._liveTemp(t),r=this._config?.show_signal&&!t.virtual?$t(this.hass,t.entities):null;return j`
this._clickDevice(e,t)} - @mousemove=${e=>this._showTip(e,t.name,t.model+(null!=n?" · "+n+"°":"")+(null!=r?" · LQI "+r:""))} + @mousemove=${e=>this._showTip(e,t.name,t.model+(null!=r?" · "+r+"°":"")+(null!=n?" · LQI "+n:""))} @mouseleave=${()=>this._tip=null} @pointerdown=${e=>this._pointerDown(e,t)} @pointermove=${e=>this._pointerMove(e,t)} @pointerup=${e=>this._pointerUp(e,t)} > - ${null!=n?j`${n}°`:B} - ${null!=r?j`${r}`:B} -
`}_roomCenter(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}_renderMarkupDefs(t){const e=this._gridPitch,i=.14*e;return F` + ${null!=r?F`${r}°`:B} + ${null!=n?F`${n}`:B} +
`}_roomCenter(t){if(t.poly){const e=t.poly.length;return[t.poly.reduce((t,e)=>t+e[0],0)/e,t.poly.reduce((t,e)=>t+e[1],0)/e]}return[t.x+t.w/2,t.y+.1*Math.min(t.w,t.h)]}_renderMarkupDefs(t){const e=this._gridPitch,i=.14*e;return j` - `}_renderMarkupLayer(t){const e=this._segments,i=this._path,s=this._gridPitch;return F` + `}_renderMarkupLayer(t){const e=this._segments,i=this._path,s=this._gridPitch;return j` - ${e.map(t=>F``)} - ${i.length>1?F``:B} - ${i.length&&this._cursorPt&&"draw"===this._tool&&!this._contourClosed?F`j``)} + ${i.length>1?j``:B} + ${i.length&&this._cursorPt&&"draw"===this._tool&&!this._contourClosed?j``:B} - ${i.map((t,e)=>F``)} - `}_renderMarkupBar(){return j`
+ ${i.map((t,e)=>j``)} + `}_renderMarkupBar(){return F`
- ${"draw"===this._tool?j`${this._path.length?"точек: "+this._path.length+" · Esc/Ctrl+Z — убрать точку · замкните контур кликом по первой":"кликните точку сетки, чтобы начать контур"} - ${this._path.length?j``:B}`:B} -
`}_renderInfoCard(){const t=this._infoCard,e=t.primary?this.hass.states[t.primary]:void 0,i=e?this.hass.formatEntityState?.(e)??e.state:null;return j``}_renderInfoCard(){const t=this._infoCard,e=t.primary?this.hass.states[t.primary]:void 0,i=e?this.hass.formatEntityState?.(e)??e.state:null;return F`