=0&&(I.segments.splice(N,1),this._saveConfig(),this.requestUpdate()))}if("delroom"===this._tool){const I=[...this._spaceModel().rooms].reverse().find(I=>this._pointInRoom(M,I));if(!I)return;if(!confirm(`Удалить комнату «${I.name}»?`))return;const i=this._curSpaceCfg;return i.rooms=i.rooms.filter(M=>M.id!==I.id),this._saveConfig(),this._regSignature="",this._maybeRebuildDevices(),void this.requestUpdate()}const i=this._snap(M);if(!this._path.length)return this._path=[i],void(this._pathSegs=[]);const N=this._path[this._path.length-1];if(this._samePt(i,N))return;const T=this._addSegment(N,i);this._pathSegs=[...this._pathSegs,T?this._segKey(N,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(I){this._markup&&"draw"===this._tool&&this._path.length&&!this._contourClosed&&(this._cursorPt=this._snap(this._svgPoint(I)))}_saveRoom(){if(!this._contourClosed||!this._areaSel&&!this._nameSel)return;const I=this._curSpaceCfg;if(!I)return;const M=this._spaceH,i=this._path.slice(0,-1),N=this._areaSel?this.hass.areas[this._areaSel]?.name:"";I.rooms.push({id:"r"+Date.now().toString(36),name:this._nameSel||N||"Комната",area:this._areaSel||null,poly:i.map(I=>[I[0]/AI,I[1]/M])}),this._saveConfig(),this._path=[],this._pathSegs=[],this._areaSel="",this._nameSel="",this._roomDialog=!1,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Комната сохранена")}_cancelPath(){this._path=[],this._pathSegs=[],this._cursorPt=null,this._roomDialog=!1}_roomDialogCancel(){this._roomDialog=!1,this._undoPoint()}get _freeAreas(){const I=new Set;for(const M of this._serverCfg?.spaces||[])for(const i of M.rooms||[])i.area&&I.add(i.area);return Object.values(this.hass?.areas||{}).filter(M=>!I.has(M.area_id)).sort((I,M)=>(I.name||"").localeCompare(M.name||""))}async _migrateToServer(){if(this._serverStorage&&!this._norm&&!this._migrating&&confirm("Перенести текущую конфигурацию (планы, комнаты, раскладку) на сервер HA?")){this._migrating=!0;try{const I=[];for(const M of Object.keys(zI)){const i=zI[M],N=dI[M]||i,[T,D,j,g]=N;let c=null,w=j/g;const z=kI[M];if(z&&dI[M]){const I=LI[M];I&&(w=I[0]/I[1]);const i=z.split(",")[1];c=(await this.hass.callWS({type:"houseplan/plan/set",space_id:M,ext:"svg",data:i})).url}const O=wI.filter(I=>I.floor===M).map((I,i)=>({id:M+"_r"+i,name:I.name,area:I.area||null,x:(I.x-T)/j,y:(I.y-D)/g,w:I.w/j,h:I.h/g}));I.push({id:M,title:OI[M]||M,plan_url:c,aspect:w,view_box:[(i[0]-T)/j,(i[1]-D)/g,i[2]/j,i[3]/g],rooms:O})}const M={spaces:I,device_overrides:{},virtual_devices:[],settings:{exclude_integrations:[...yI],group_lights:!0}},i={};for(const I of this._devices){if(I.virtual)continue;const M=this._layout[I.id]||this._defPos[I.id];if(!M)continue;const N=dI[I.space]||zI[I.space];i[I.id]={s:I.space,x:(M.x-N[0])/N[2],y:(M.y-N[1])/N[3]}}await this.hass.callWS({type:"houseplan/config/set",config:M}),await this.hass.callWS({type:"houseplan/layout/set",layout:i}),this._serverCfg=M,this._layout=i,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Конфигурация перенесена на сервер — карта работает от server config")}catch(I){this._showToast("Ошибка миграции: "+(I?.message||I))}finally{this._migrating=!1}}}render(){if(!this._config||!this.hass)return v;const I=this._model;if(!I.length)return r`Нет настроенных пространств
`;const M=this._spaceModel(),i=M.vb,N=this._devices.filter(I=>I.space===M.id),T=this._config.icon_size??2.5,D=T>8?2.5:T;return r`
${this._config.title||"План дома"}
${I.map(I=>r``)}
${N.length} устр.
${this._edit?this._renderEditbar():v}
${this._markup?this._renderMarkupBar():v}
this._markupClick(I)}
@mousemove=${I=>this._markupMove(I)}>
${N.map(I=>this._renderDevice(I,i))}
${this._menuDev?this._renderMenu():v}
${this._roomDialog?this._renderRoomDialog():v}
${this._tip?r`
${this._tip.title}${this._tip.meta?r`${this._tip.meta}`:v}
${null!=this._tip.lqi?r`средний сигнал zigbee:
${this._tip.lqi}`:v}
`:v}
${this._toast?r`${this._toast}
`:v}
`}_renderDevice(I,M){const i=this._pos(I),N=(i.x-M[0])/M[2]*100,T=(i.y-M[1])/M[3]*100,D=this._stateClass(I),j=this._liveTemp(I),g=this._config?.show_signal&&!I.virtual?this._lqiFor(I.entities):null;return r`this._clickDevice(M,I)}
@mousemove=${M=>this._showTip(M,I.name,I.model+(null!=j?" · "+j+"°":"")+(null!=g?" · LQI "+g:""))}
@mouseleave=${()=>this._tip=null}
@pointerdown=${M=>this._pointerDown(M,I)}
@pointermove=${M=>this._pointerMove(M,I)}
@pointerup=${M=>this._pointerUp(M,I)}
>
${null!=j?r`${j}°`:v}
${null!=g?r`${g}`:v}
`}_roomCenter(I){if(I.poly){const M=I.poly.length;return[I.poly.reduce((I,M)=>I+M[0],0)/M,I.poly.reduce((I,M)=>I+M[1],0)/M]}return[I.x+I.w/2,I.y+.1*Math.min(I.w,I.h)]}_renderMarkupDefs(I){const M=this._gridPitch,i=.14*M;return p`
`}_renderMarkupLayer(I){const M=this._segments,i=this._path,N=this._gridPitch;return p`
${M.map(I=>p``)}
${i.length>1?p``:v}
${i.length&&this._cursorPt&&"draw"===this._tool&&!this._contourClosed?p``:v}
${i.map((I,M)=>p``)}
`}_renderMarkupBar(){return r`
${"draw"===this._tool?r`${this._path.length?"точек: "+this._path.length+" · Esc/Ctrl+Z — убрать точку · замкните контур кликом по первой":"кликните точку сетки, чтобы начать контур"}
${this._path.length?r``:v}`:v}
`}_renderRoomDialog(){const I=this._freeAreas;return r``}_renderEditbar(){const I=this._selId?this._devices.find(I=>I.id===this._selId):null,M=I?this._pos(I):null;return r`
${I?I.name:"Режим правки — тащите иконки мышью"}
${I&&M?r`this._applyXY("x",I.target.value)} />
this._applyXY("y",I.target.value)} />`:v}
${this._serverStorage?this._norm?"конфиг и раскладка: сервер":"раскладка: сервер · конфиг: встроенный":"сохранение: этот браузер"}
${this._serverStorage&&!this._norm?r``:v}
`}_renderMenu(){const I=this._menuDev;return r``}}bI.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_edit:{state:!0},_layout:{state:!0},_devices:{state:!0},_menuDev:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_migrating:{state:!0},_markup:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0}},bI.styles=((I,...M)=>{const N=1===I.length?I[0]:M.reduce((M,i,N)=>M+(I=>{if(!0===I._$cssResult$)return I.cssText;if("number"==typeof I)return I;throw Error("Value passed to 'css' function must be a 'css' function result: "+I+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+I[N+1],I[0]);return new T(N,I,i)})`
:host {
--hp-bg: var(--card-background-color, #16212e);
--hp-line: var(--divider-color, #2b3d4f);
--hp-txt: var(--primary-text-color, #e6edf3);
--hp-muted: var(--secondary-text-color, #8aa0b3);
--hp-accent: var(--primary-color, #3ea6ff);
--hp-on: #ffd45c;
--hp-open: #ff9f43;
}
ha-card {
overflow: visible; /* overflow:hidden ломает position:sticky у шапки */
}
.empty {
padding: 32px;
color: var(--hp-muted);
text-align: center;
}
.hdr {
position: sticky;
top: var(--header-height, 56px);
z-index: 20;
background: var(--card-background-color, var(--hp-bg));
border-radius: var(--ha-card-border-radius, 12px) var(--ha-card-border-radius, 12px) 0 0;
}
.head {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-bottom: 1px solid var(--hp-line);
flex-wrap: wrap;
}
.title {
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.title ha-icon {
color: var(--hp-accent);
--mdc-icon-size: 18px;
}
.tabs {
display: flex;
gap: 4px;
background: rgba(127, 127, 127, 0.12);
padding: 3px;
border-radius: 10px;
}
.tab {
border: 0;
background: transparent;
color: var(--hp-muted);
padding: 6px 13px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: 0.15s;
font-family: inherit;
}
.tab:hover {
color: var(--hp-txt);
}
.tab.active {
background: var(--hp-accent);
color: var(--text-primary-color, #fff);
}
.count {
font-size: 12px;
color: var(--hp-muted);
}
.spacer {
flex: 1;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--hp-line);
background: transparent;
color: var(--hp-txt);
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
transition: 0.15s;
font-family: inherit;
font-size: 12.5px;
}
.btn ha-icon {
--mdc-icon-size: 17px;
}
.btn:hover {
border-color: var(--hp-accent);
}
.btn.on {
background: var(--hp-accent);
color: var(--text-primary-color, #fff);
border-color: var(--hp-accent);
}
.btn.ghost {
border: none;
}
.btn[disabled] {
opacity: 0.5;
pointer-events: none;
}
.stage {
position: relative;
width: 100%;
container-type: inline-size;
}
.stage svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.room {
transition: 0.12s;
cursor: pointer;
}
.room.overlay {
fill: transparent;
stroke: transparent;
stroke-width: 2;
}
.room.overlay:hover {
fill: rgba(62, 166, 255, 0.18);
stroke: var(--hp-accent);
}
.room.yard {
fill: rgba(75, 140, 90, 0.14);
stroke: #4b8c5a;
stroke-width: 2;
}
.room.yard:hover {
fill: rgba(75, 140, 90, 0.24);
stroke: #6fbf86;
}
.rlabel {
fill: var(--hp-muted);
font-size: 15px;
font-weight: 600;
pointer-events: none;
text-anchor: middle;
}
.stage.edit .room {
pointer-events: none;
}
.stage.markup {
cursor: crosshair;
}
.stage.markup .room {
pointer-events: none;
}
.stage.markup .devlayer {
display: none; /* в разметке иконки не мешают */
}
.room.outlined {
stroke: rgba(62, 166, 255, 0.55);
fill: rgba(62, 166, 255, 0.06);
}
.griddot {
fill: var(--hp-accent);
opacity: 0.75;
stroke: rgba(0, 0, 0, 0.35);
stroke-width: 0.4;
}
.seg {
stroke: var(--hp-accent);
stroke-width: 2.5;
stroke-linecap: round;
}
.pathline {
stroke: #ffc14d;
stroke-width: 3;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.preview {
stroke: #ffc14d;
stroke-width: 2;
stroke-dasharray: 6 5;
opacity: 0.7;
}
.vertex {
fill: #ffc14d;
stroke: #4a2800;
stroke-width: 1;
}
.vertex.first {
fill: #4bd28f;
stroke: #04121f;
}
.areasel,
.namein {
background: var(--hp-bg);
border: 1px solid var(--hp-line);
color: var(--hp-txt);
border-radius: 6px;
padding: 6px 8px;
font-size: 13px;
font-family: inherit;
}
.namein {
width: 130px;
}
.devlayer {
position: absolute;
inset: 0;
pointer-events: none;
}
.dev {
position: absolute;
width: var(--icon-size, 2.5cqw);
height: var(--icon-size, 2.5cqw);
margin: calc(var(--icon-size, 2.5cqw) / -2) 0 0 calc(var(--icon-size, 2.5cqw) / -2);
border-radius: 22%;
background: var(--hp-bg);
border: 1px solid var(--hp-line);
display: flex;
align-items: center;
justify-content: center;
color: var(--hp-txt);
cursor: pointer;
pointer-events: auto;
transition: background 0.15s, border-color 0.15s, opacity 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
z-index: 2;
}
.dev ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62);
}
.dev:hover {
background: var(--hp-accent);
color: var(--text-primary-color, #fff);
z-index: 5;
}
.dev.on {
background: var(--hp-on);
border-color: var(--hp-on);
color: #503c00;
box-shadow: 0 0 8px rgba(255, 212, 92, 0.7);
}
.dev.open {
background: var(--hp-open);
border-color: var(--hp-open);
color: #4a2800;
}
.dev.unavail {
opacity: 0.35;
}
.dev.virtual {
border-style: dashed;
}
.dev.sel {
border-color: #ffc14d;
box-shadow: 0 0 0 3px rgba(255, 193, 77, 0.35);
}
.stage.edit .dev {
cursor: grab;
}
.dev .tval {
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: calc(var(--icon-size, 2.5cqw) * 0.1);
background: rgba(4, 18, 31, 0.9);
border: 1px solid var(--hp-accent);
border-radius: calc(var(--icon-size, 2.5cqw) * 0.18);
padding: 0 calc(var(--icon-size, 2.5cqw) * 0.14);
font-size: calc(var(--icon-size, 2.5cqw) * 0.45);
font-weight: 700;
line-height: calc(var(--icon-size, 2.5cqw) * 0.68);
color: #dff1ff;
white-space: nowrap;
pointer-events: none;
}
.dev .lqi {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: calc(var(--icon-size, 2.5cqw) * 0.05);
font-size: calc(var(--icon-size, 2.5cqw) * 0.38);
font-weight: 700;
line-height: 1;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 2px rgba(0, 0, 0, 0.9);
white-space: nowrap;
pointer-events: none;
}
.editbar {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 14px;
border-bottom: 1px solid var(--hp-line);
font-size: 13px;
flex-wrap: wrap;
}
.dialogwrap {
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 90;
}
.dialog {
background: var(--card-background-color, var(--hp-bg));
border: 1px solid var(--hp-accent);
border-radius: 14px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
width: min(360px, 92vw);
overflow: hidden;
}
.dialog .hd {
padding: 12px 16px;
font-weight: 600;
border-bottom: 1px solid var(--hp-line);
display: flex;
align-items: center;
gap: 8px;
}
.dialog .hd ha-icon {
color: var(--hp-accent);
}
.dialog .body {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 6px;
}
.dialog .body label {
font-size: 12px;
color: var(--hp-muted);
margin-top: 6px;
}
.dialog .body .namein,
.dialog .body .areasel {
width: 100%;
box-sizing: border-box;
}
.dialog .row {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid var(--hp-line);
}
.editbar .warn {
color: #ffc14d;
}
.editbar .sname {
font-weight: 600;
}
.editbar input {
width: 74px;
background: transparent;
border: 1px solid var(--hp-line);
color: var(--hp-txt);
border-radius: 6px;
padding: 5px 7px;
font-size: 13px;
}
.editbar label,
.editbar .hint {
color: var(--hp-muted);
font-size: 12px;
}
.menuwrap {
position: fixed;
inset: 0;
z-index: 80;
}
.menu {
position: fixed;
background: var(--hp-bg);
border: 1px solid var(--hp-accent);
border-radius: 10px;
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
min-width: 210px;
max-width: 300px;
overflow: hidden;
transform: translate(0, 8px);
}
.menu .hd {
padding: 8px 12px;
font-weight: 600;
font-size: 12.5px;
border-bottom: 1px solid var(--hp-line);
display: flex;
align-items: center;
gap: 6px;
}
.menu .hd ha-icon,
.menu .it.all ha-icon {
color: var(--hp-accent);
--mdc-icon-size: 16px;
}
.menu .it {
padding: 8px 12px;
font-size: 12.5px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.menu .it ha-icon {
--mdc-icon-size: 16px;
color: var(--hp-muted);
}
.menu .it:hover {
background: rgba(127, 127, 127, 0.15);
}
.menu .it.all {
color: var(--hp-accent);
font-weight: 600;
}
.tip {
position: fixed;
pointer-events: none;
background: var(--hp-bg);
border: 1px solid var(--hp-accent);
color: var(--hp-txt);
padding: 6px 10px;
border-radius: 8px;
font-size: 12.5px;
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
z-index: 99;
max-width: 260px;
}
.tip .m {
color: var(--hp-muted);
font-size: 11px;
display: block;
}
.toast {
position: fixed;
left: 50%;
bottom: 22px;
transform: translateX(-50%);
background: var(--hp-bg);
border: 1px solid var(--hp-accent);
color: var(--hp-txt);
padding: 9px 16px;
border-radius: 10px;
font-size: 13px;
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
z-index: 120;
max-width: 90vw;
}
`,customElements.get("houseplan-card")||customElements.define("houseplan-card",bI),window.customCards=window.customCards||[],window.customCards.find(I=>"houseplan-card"===I.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Интерактивный план дома: пространства, комнаты, устройства с живыми состояниями и drag-раскладкой."}),console.info("%c HOUSEPLAN-CARD %c v1.4.4 ","background:#3ea6ff;color:#04121f;font-weight:700","");