From 96d3a182c66cf0a4e4458e41c6f2c9593fb84341 Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 4 Jul 2026 16:10:26 +0300 Subject: [PATCH] =?UTF-8?q?fix=20v1.7.4:=20PDF=20upload=20via=20HTTP=20end?= =?UTF-8?q?point=20instead=20of=20WebSocket=20=E2=80=94=20root=20cause=20w?= =?UTF-8?q?as=20WS=20message-size=20limit=20dropping=20the=20connection=20?= =?UTF-8?q?on=20real=20(>2-3MB)=20PDFs=20(which=20also=20closed=20the=20di?= =?UTF-8?q?alog=20via=20reconnect).=20New=20POST=20/api/houseplan/upload?= =?UTF-8?q?=20(HomeAssistantView,=20multipart,=20auth);=20verified=203MB(1?= =?UTF-8?q?82ms)+10MB(446ms).=20Legible=20errors=20(=5FerrText),=20no=20mo?= =?UTF-8?q?re=20[object=20Object]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/houseplan/__init__.py | 5 +- .../__pycache__/__init__.cpython-310.pyc | Bin 4807 -> 4928 bytes custom_components/houseplan/const.py | 2 +- .../houseplan/frontend/houseplan-card.js | 18 ++-- custom_components/houseplan/http_api.py | 83 ++++++++++++++++++ custom_components/houseplan/manifest.json | 2 +- dist/houseplan-card.js | 18 ++-- docs/ARCHITECTURE.md | 5 +- docs/CHANGELOG.md | 13 +++ package.json | 2 +- src/houseplan-card.ts | 58 +++++++----- 11 files changed, 163 insertions(+), 43 deletions(-) create mode 100644 custom_components/houseplan/http_api.py diff --git a/custom_components/houseplan/__init__.py b/custom_components/houseplan/__init__.py index 641dd6d..fccf903 100755 --- a/custom_components/houseplan/__init__.py +++ b/custom_components/houseplan/__init__.py @@ -27,11 +27,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config) -> bool: - """Регистрируем WS-команды и хранилища на старте.""" + """Регистрируем WS-команды, HTTP-загрузку и хранилища на старте.""" 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) hp_ws.async_register(hass) + from .http_api import HouseplanUploadView + + hass.http.register_view(HouseplanUploadView()) return True diff --git a/custom_components/houseplan/__pycache__/__init__.cpython-310.pyc b/custom_components/houseplan/__pycache__/__init__.cpython-310.pyc index fdb41fc75eacbaac5f56024231094f67be34d43b..19070f30db6fe2e5e12ea2fe0066fc12e8073adf 100644 GIT binary patch delta 881 zcmZ8fT~E_c816Y8W$V^4CeDZw5C&4hM*7!5vmg*xXD{hupB}Rkb$Oc-VN0kbU!}I_p75FwL2p|TRLQTgUv%uu zXx~D)yfk+7{^-q7v$fmWFfS!}FYq=9qAnvx6AAfda<5>3RlK9&oNILKo(ws@+&K*F)mI0R5~f_UAG@LwD0O zM{-0aDUzl!0{P}oag>rl)+KFE^>fBP4dgY`GTe%zc^iR@%|td}8U|25K4WS1Rb4Ag zu>YuBUlsHFR7U$E;kp zb>A{7K3Gwq5flxpb!zTrqgrtd&vzD5_vI8AVOcsJl?ca0#Kn z)Q+A@VeL8euOl~ZJmTfd#yTvaD;faND9cE}QJ~0Sg>83~NRb_O%+BIsR0`n&@ty6{ z@B#tj(b?l^V3Nn4@ZZJPM|ZOXCfQu(@-5_+`OP1T1fpX*Q03wOB!X--jo-j2qgC#p zqV!LZKL~@hGDA{Wzm5IKbW72K(ac0q)IE)`%H}&iCKu66Ae?8N+1uj;Ra9|Ga-goYK_uPAy|8d=L4MUHyZ|!39 z#q4L}jQ~HB2C2hGVO!phnNqM1x_AqA#fo@fYVb*1pEd}V+M|(Tek7Y}3=W-^HL(4_ zvuwwwhk1Cf)h5@FQAH@7{?^V#Qi89>wamC5eWlkO_${~ZRXXs;*djG3$CnuPXxq)86KWg7mhJgn*D;l7XuO4W>o%BgrhW}MfjF1KSb_igs-#u~yEc$?lU;t1(HLbNuVKxBnB@f6t` QLk{%mB}LN<`eI!A2UDV--~a#s diff --git a/custom_components/houseplan/const.py b/custom_components/houseplan/const.py index 8483d79..58ed624 100755 --- a/custom_components/houseplan/const.py +++ b/custom_components/houseplan/const.py @@ -10,7 +10,7 @@ PLANS_DIR = "houseplan/plans" # относительно каталога ко FILES_URL = "/houseplan_files/files" FILES_DIR = "houseplan/files" CONF_ADMIN_ONLY = "admin_only" -VERSION = "1.7.3" +VERSION = "1.7.4" DEFAULT_CONFIG: dict = { "spaces": [], diff --git a/custom_components/houseplan/frontend/houseplan-card.js b/custom_components/houseplan/frontend/houseplan-card.js index 94b934a..246e4c4 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 n=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:o,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)=>!o(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 n=s?.call(this);a?.call(this,e),this.requestUpdate(t,n,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(n(t))}else void 0!==t&&e.push(n(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 n=a.fromAttribute(e,t.type);this[s]=n??this._$Ej?.get(s)??n,this._$Em=null}}requestUpdate(t,e,i,s=!1,a){if(void 0!==t){const n=this.constructor;if(!1===s&&(a=this[t]),i??=n.getPropertyOptions(t),!((i.hasChanged??v)(a,e)||i.useDefault&&i.reflect&&a===this._$Ej?.get(t)&&!this.hasAttribute(n._$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},n){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),!0!==a||void 0!==n)||(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 $=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,D=`<${A}>`,E=document,P=()=>E.createComment(""),M=t=>null===t||"object"!=typeof t&&"function"!=typeof t,R=Array.isArray,z="[ \t\n\f\r]",T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,U=/-->/g,O=/>/g,H=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),I=/'/g,N=/"/g,q=/^(?:script|style|textarea|title)$/i,F=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),j=F(1),L=F(2),K=Symbol.for("lit-noChange"),B=Symbol.for("lit-nothing"),W=new WeakMap,X=E.createTreeWalker(E,129);function Y(t,e){if(!R(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==k?k.createHTML(e):e}const G=(t,e)=>{const i=t.length-1,s=[];let a,n=2===e?"":3===e?"":"",o=T;for(let e=0;e"===l[0]?(o=a??T,c=-1):void 0===l[1]?c=-2:(c=o.lastIndex-l[2].length,r=l[1],o=void 0===l[3]?H:'"'===l[3]?N:I):o===N||o===I?o=H:o===U||o===O?o=T:(o=H,a=void 0);const d=o===H&&t[e+1].startsWith("/>")?" ":"";n+=o===T?i+D:c>=0?(s.push(r),i.slice(0,c)+S+i.slice(c)+C+d):i+C+(-2===c?e:d)}return[Y(t,n+(t[i]||"")+(2===e?"":3===e?"":"")),s]};class V{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let a=0,n=0;const o=t.length-1,r=this.parts,[l,c]=G(t,e);if(this.el=V.createElement(l,i),X.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(s=X.nextNode())&&r.length0){s.textContent=w?w.emptyScript:"";for(let i=0;iR(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&&M(this._$AH)?this._$AA.nextSibling.data=t:this.T(E.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=V.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(e);else{const t=new Z(s,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=W.get(t.strings);return void 0===e&&W.set(t.strings,e=new V(t)),e}k(t){R(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(P()),this.O(P()),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 n=!1;if(void 0===a)t=J(this,t,e,0),n=!M(t)||t!==this._$AH&&t!==K,n&&(this._$AH=t);else{const s=t;let o,r;for(t=a[0],o=0;o{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(P(),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 K}}rt._$litElement$=!0,rt.finalized=!0,ot.litElementHydrateSupport?.({LitElement:rt});const lt=ot.litElementPolyfillSupport;lt?.({LitElement:rt}),(ot.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(new RegExp(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}const gt=[{name:"title",selector:{text:{}}},{name:"default_floor",selector:{select:{mode:"dropdown",options:[{value:"f1",label:"1 этаж"},{value:"f2",label:"2 этаж"},{value:"yard",label:"Двор"}]}}},{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:{}}}],mt={title:"Заголовок",default_floor:"Этаж по умолчанию",icon_size:"Размер иконок, % ширины плана",show_temperature:"Показывать температуру",live_states:"Живые состояния (вкл/выкл, открыто…)",show_signal:"Показывать сигнал zigbee (LQI)"};class ft extends rt{setConfig(t){this._config=t}render(){return this.hass&&this._config?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:o,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)=>!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&&o(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 n=s?.call(this);a?.call(this,e),this.requestUpdate(t,n,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(n(t))}else void 0!==t&&e.push(n(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 n=a.fromAttribute(e,t.type);this[s]=n??this._$Ej?.get(s)??n,this._$Em=null}}requestUpdate(t,e,i,s=!1,a){if(void 0!==t){const n=this.constructor;if(!1===s&&(a=this[t]),i??=n.getPropertyOptions(t),!((i.hasChanged??v)(a,e)||i.useDefault&&i.reflect&&a===this._$Ej?.get(t)&&!this.hasAttribute(n._$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},n){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),!0!==a||void 0!==n)||(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 $=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,D=`<${A}>`,P=document,E=()=>P.createComment(""),M=t=>null===t||"object"!=typeof t&&"function"!=typeof t,R=Array.isArray,z="[ \t\n\f\r]",T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,U=/-->/g,O=/>/g,H=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),I=/'/g,N=/"/g,q=/^(?:script|style|textarea|title)$/i,F=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),j=F(1),L=F(2),K=Symbol.for("lit-noChange"),B=Symbol.for("lit-nothing"),W=new WeakMap,X=P.createTreeWalker(P,129);function Y(t,e){if(!R(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==k?k.createHTML(e):e}const G=(t,e)=>{const i=t.length-1,s=[];let a,n=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,o=l[1],r=void 0===l[3]?H:'"'===l[3]?N:I):r===N||r===I?r=H:r===U||r===O?r=T:(r=H,a=void 0);const d=r===H&&t[e+1].startsWith("/>")?" ":"";n+=r===T?i+D:c>=0?(s.push(o),i.slice(0,c)+S+i.slice(c)+C+d):i+C+(-2===c?e:d)}return[Y(t,n+(t[i]||"")+(2===e?"":3===e?"":"")),s]};class V{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let a=0,n=0;const r=t.length-1,o=this.parts,[l,c]=G(t,e);if(this.el=V.createElement(l,i),X.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(s=X.nextNode())&&o.length0){s.textContent=w?w.emptyScript:"";for(let i=0;iR(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&&M(this._$AH)?this._$AA.nextSibling.data=t:this.T(P.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=V.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(e);else{const t=new Z(s,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=W.get(t.strings);return void 0===e&&W.set(t.strings,e=new V(t)),e}k(t){R(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(E()),this.O(E()),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 n=!1;if(void 0===a)t=J(this,t,e,0),n=!M(t)||t!==this._$AH&&t!==K,n&&(this._$AH=t);else{const s=t;let r,o;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(E(),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 K}}ot._$litElement$=!0,ot.finalized=!0,rt.litElementHydrateSupport?.({LitElement:ot});const lt=rt.litElementPolyfillSupport;lt?.({LitElement:ot}),(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(new RegExp(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}const gt=[{name:"title",selector:{text:{}}},{name:"default_floor",selector:{select:{mode:"dropdown",options:[{value:"f1",label:"1 этаж"},{value:"f2",label:"2 этаж"},{value:"yard",label:"Двор"}]}}},{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:{}}}],mt={title:"Заголовок",default_floor:"Этаж по умолчанию",icon_size:"Размер иконок, % ширины плана",show_temperature:"Показывать температуру",live_states:"Живые состояния (вкл/выкл, открыто…)",show_signal:"Показывать сигнал zigbee (LQI)"};class ft extends ot{setConfig(t){this._config=t}render(){return this.hass&&this._config?j`mt[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)}}ft.properties={hass:{attribute:!1},_config:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",ft);const vt="houseplan_card_layout_v1",bt=1e3,yt=(t,e,i)=>{const s=new Event(e,{bubbles:!0,composed:!0});s.detail=i??{},t.dispatchEvent(s)},$t=(t,e)=>{let i;return(...s)=>{clearTimeout(i),i=window.setTimeout(()=>t(...s),e)}};class xt extends rt{constructor(){super(...arguments),this._space="f1",this._edit=!1,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._infoCard=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=t=>this._onKey(t),this._drag=null,this._dirtyPos=new Set,this._persistLayout=$t(()=>{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)))}}else localStorage.setItem(vt,JSON.stringify(this._layout))},600),this._saveConfig=$t(()=>{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._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)}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}get _model(){return this._serverCfg?this._serverCfg.spaces.map(t=>{const e=bt/t.aspect;return{id:t.id,title:t.title,vb:[t.view_box[0]*bt,t.view_box[1]*e,t.view_box[2]*bt,t.view_box[3]*e],bg:t.plan_url?{href:t.plan_url,x:0,y:0,w:bt,h:e}:null,rooms:t.rooms.map(t=>({id:t.id,name:t.name,area:t.area??null,x:null!=t.x?t.x*bt:void 0,y:null!=t.y?t.y*e:void 0,w:null!=t.w?t.w*bt:void 0,h:null!=t.h?t.h*e:void 0,poly:t.poly?t.poly.map(t=>[t[0]*bt,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 _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())}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)}catch(t){if(this._loadTries>=8){this._serverStorage=!1,this._serverCfg=null;try{this._layout=JSON.parse(localStorage.getItem(vt)||"{}")||{}}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._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=this._buildDevices(),this._defPos=this._defaultPositions())}_entitiesByDevice(){const t={};for(const[e,i]of Object.entries(this.hass.entities))i?.device_id&&(t[i.device_id]=t[i.device_id]||[]).push(e);return t}_domainOfDevice(t,e){if(t.identifiers?.[0]?.[0])return t.identifiers[0][0];for(const t of e){const e=this.hass.entities[t]?.platform;if(e)return e}return""}_primaryEntity(t,e){const i=t.map(t=>({eid:t,reg:this.hass.entities[t],st:this.hass.states[t]})).filter(t=>t.reg&&!t.reg.hidden),s=i.filter(t=>!t.reg.entity_category),a=s.length?s:i;if("mdi:thermometer"===e||"mdi:air-filter"===e){const t=a.find(t=>this._isTempEntity(t.eid));if(t)return t.eid}for(const t of pt){const e=a.find(e=>e.eid.split(".")[0]===t);if(e)return e.eid}return a[0]?.eid}_isTempEntity(t){const e=this.hass.states[t];if(!e)return/_temperature$/.test(t);const i=e.attributes||{};return"temperature"===i.device_class||/°C|°F/.test(i.unit_of_measurement||"")||/_temperature$/.test(t)}_lqiFor(t){const e=[];for(const i of t){const t=this.hass.states[i];if(!(/_linkquality$/.test(i)||"lqi"===(t?.attributes?.unit_of_measurement||""))||!t)continue;const s=parseFloat(t.state);isNaN(s)||e.push(s)}return e.length?Math.round(e.reduce((t,e)=>t+e,0)/e.length):null}_roomLqi(t){if(!t)return null;const e=[];for(const i of this._devices){if(i.area!==t||i.virtual)continue;const s=this._lqiFor(i.entities);null!=s&&e.push(s)}return e.length?Math.round(e.reduce((t,e)=>t+e,0)/e.length):null}_tempFor(t){for(const e of t){if(!this._isTempEntity(e))continue;const t=this.hass.states[e];if(!t)continue;const i=parseFloat(t.state);if(!isNaN(i))return Math.round(10*i)/10}return null}_lightGroups(){if(!1===this._settings.group_lights)return[];const t=this.hass,e=[];for(const[i,s]of Object.entries(t.entities)){if(!i.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 n=t.states[i];e.push({eid:i,name:s.name||n?.attributes?.friendly_name||i,area:a})}return e}_groupedLightAreas(){return new Set(this._lightGroups().map(t=>t.area))}get _markers(){return this._serverCfg?.markers||[]}_markerFor(t,e){return this._markers.find(i=>i.binding===t+":"+e)}get _claimed(){const t=new Set;for(const e of this._markers){const[i,s]=e.binding.split(":");"device"!==i&&"entity"!==i||!s||t.add(e.binding)}return t}_applyMarker(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||[]}_buildDevices(){const t=this.hass,e=this._areaToSpace,i=!1!==this._settings.group_lights,s=this._groupedLightAreas(),a=this._excluded,n=this._entitiesByDevice(),o=this._claimed,r={},l=[];for(const c of Object.values(t.devices)){const t=c.area_id;if(!t||!e[t])continue;if("service"===c.entry_type)continue;if(o.has("device:"+c.id))continue;const h=this._markerFor("device",c.id);if(h&&h.hidden)continue;const d=n[c.id]||[],p=this._domainOfDevice(c,d);if(a.has(p))continue;if("Group"===c.model)continue;if(/scene/i.test(c.model||""))continue;if(/bridge/i.test((c.model||"")+(c.name||"")))continue;if("myheat"===p&&c.via_device_id)continue;const u=(c.name_by_user||c.name||"без имени").trim(),_=u+"|"+t;if(r[_])continue;r[_]=1;let g=dt(u,c.model);if(d.some(t=>t.startsWith("lock."))&&(g="mdi:lock"),i&&"mdi:lightbulb"===g&&s.has(t))continue;const m={id:c.id,name:u,model:c.model||"",area:t,space:e[t].space,icon:g,entities:d,bindingKind:"device",bindingRef:c.id,pdfs:[]};m.primary=this._primaryEntity(d,g),"mdi:thermometer"!==g&&"mdi:air-filter"!==g||(m.temp=this._tempFor(d)),l.push(m)}for(const t of this._lightGroups())e[t.area]&&(o.has("entity:"+t.eid)||l.push({id:"lg_"+t.eid,name:t.name,model:"группа света",area:t.area,space:e[t.area].space,icon:"mdi:lightbulb-group",entities:[t.eid],primary:t.eid,bindingKind:"entity",bindingRef:t.eid,pdfs:[]}));for(const i of this._markers){if(i.hidden)continue;const[s,a]=i.binding.split(":");if("device"===s){const s=t.devices[a],o=i.area||s?.area_id||"",r=o&&e[o]?.space||i.space||this._model[0]?.id||"",c=s&&n[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:i.id,name:s?.name_by_user||s?.name||"устройство",model:s?.model||"",area:o,space:r,icon:h,entities:c,bindingKind:"device",bindingRef:a};d.primary=this._primaryEntity(c,h),"mdi:thermometer"!==h&&"mdi:air-filter"!==h||(d.temp=this._tempFor(c)),this._applyMarker(d,i),l.push(d)}else if("entity"===s){const s=t.entities[a],n=i.area||s?.area_id||s?.device_id&&t.devices[s.device_id]?.area_id||"",o=n&&e[n]?.space||i.space||this._model[0]?.id||"",r=t.states[a],c={id:i.id,name:s?.name||r?.attributes?.friendly_name||a,model:"",area:n,space:o,icon:"mdi:shape-outline",entities:[a],primary:a,bindingKind:"entity",bindingRef:a};this._applyMarker(c,i),l.push(c)}else{const t=i.area||"",s=i.space||t&&e[t]?.space||this._model[0]?.id||"",a={id:i.id,name:i.name||"виртуальное устройство",model:i.model||"",area:t,space:s,icon:i.icon||"mdi:map-marker",entities:[],bindingKind:"virtual",virtual:!0};this._applyMarker(a,i),l.push(a)}}return l}_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={};for(const e of this._model)for(const i of e.rooms){if(!i.area)continue;const s=this._devices.filter(t=>t.area===i.area&&t.space===e.id);if(!s.length)continue;const a=this._roomBounds(i),n=.12*Math.min(a.w,a.h),o=a.w-2*n,r=a.h-2*n,l=Math.max(1,Math.round(Math.sqrt(s.length*o/Math.max(r,1)))),c=Math.ceil(s.length/l),h=o/l,d=r/Math.max(c,1);s.forEach((e,i)=>{const s=i%l,o=Math.floor(i/l);t[e.id]={x:a.x+n+h*(s+.5),y:a.y+n+d*(o+.5)}})}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*bt,y:i.y*(bt/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,n=Math.round(i/s)*s,o=this._serverCfg.spaces.find(e=>e.id===t.space)?.aspect||1;this._layout={...this._layout,[t.id]:{s:t.space,x:a/bt,y:n/(bt/o)}}}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:this._tempFor(t.entities):null}_openMoreInfo(t){t?yt(this,"hass-more-info",{entityId:t}):this._showToast("У устройства нет подходящей сущности")}_clickDevice(t,e){if(t.stopPropagation(),!this._drag?.moved&&!this._markup)return this._edit?(this._selId=e.id,void this._openMarkerDialog(e)):void(this._infoCard=e)}_clickRoom(t){var e;!this._edit&&t.area&&(e="/config/areas/area/"+t.area,history.pushState(null,"",e),yt(window,"location-changed",{replace:!1}))}_pointerDown(t,e){if(!this._edit)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(),n=(t.clientX-this._drag.sx)/a.width*s[2],o=(t.clientY-this._drag.sy)/a.height*s[3];Math.abs(t.clientX-this._drag.sx)+Math.abs(t.clientY-this._drag.sy)>3&&(this._drag.moved=!0);const r=.008*Math.min(s[2],s[3]),l=Math.max(s[0]+r,Math.min(s[0]+s[2]-r,this._drag.ox+n)),c=Math.max(s[1]+r,Math.min(s[1]+s[3]-r,this._drag.oy+o));this._savePos(e,l,c)}_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))}_applyXY(t,e){if(!this._selId)return;const i=parseFloat(e);if(isNaN(i))return;const s=this._devices.find(t=>t.id===this._selId);if(!s||s.virtual)return;const a={...this._pos(s)};a[t]=i,this._savePos(s,a.x,a.y)}_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 bt/120}get _curSpaceCfg(){return this._serverCfg?.spaces.find(t=>t.id===this._space)}get _spaceH(){const t=this._curSpaceCfg;return t?bt/t.aspect:bt}get _segments(){const t=this._curSpaceCfg,e=this._spaceH;return(t?.segments||[]).map(t=>[t[0]*bt,t[1]*e,t[2]*bt,t[3]*e])}_toggleMarkup(){this._norm?(this._markup=!this._markup,this._edit=!1,this._path=[],this._cursorPt=null,this._tool="draw"):this._showToast("Разметка доступна после переноса конфига на сервер (режим правки → «На сервер»)")}_svgPoint(t){const e=this.renderRoot.querySelector(".stage"),i=this._spaceModel().vb,s=e.getBoundingClientRect();return[i[0]+(t.clientX-s.left)/s.width*i[2],i[1]+(t.clientY-s.top)/s.height*i[3]]}_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!n&&(i.segments=i.segments||[],i.segments.push([t[0]/bt,t[1]/s,e[0]/bt,e[1]/s]),this._saveConfig(),!0)}_distToSeg(t,e){const[i,s]=t,[a,n,o,r]=e,l=o-a,c=r-n;let h=((i-a)*l+(s-n)*c)/(l*l+c*c||1);h=Math.max(0,Math.min(1,h));const d=a+h*l,p=n+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-n)*(t[1]-o)/(l-o)+n&&(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 n=this._distToSeg(e,t);n=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(){if(!this._contourClosed||!this._areaSel&&!this._nameSel)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]/bt,t[1]/e])}),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 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 n=(a.name_by_user||a.name||a.id).trim();t!==this._markerDialog?.binding&&i.has(n+"|"+(a.area_id||""))||s.push({value:t,label:n,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,n]of Object.entries(t.entities)){const o="entity:"+i;if(e.has(o))continue;const r=a.has(n.platform),l="group"===n.platform;if(!r&&!l)continue;if(n.hidden)continue;const c=t.states[i];s.push({value:o,label:n.name||c?.attributes?.friendly_name||i,sub:i.split(".")[0]+" · "+("group"===n.platform?"группа":"хелпер")})}const n=(this._markerDialog?.bindingFilter||"").toLowerCase().trim(),o=n?s.filter(t=>(t.label+" "+t.sub+" "+t.value).toLowerCase().includes(n)):s;return o.sort((t,e)=>t.label.localeCompare(e.label)),o.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}_fileToBase64(t){return new Promise((e,i)=>{const s=new FileReader;s.onload=()=>{const t=String(s.result||""),i=t.indexOf(",");e(i>=0?t.slice(i+1):t)},s.onerror=()=>i(s.error||new Error("read error")),s.readAsDataURL(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=await this._fileToBase64(t),i=await this.hass.callWS({type:"houseplan/file/set",marker_id:s,filename:t.name,data:e});a.push({name:i.name||t.name,url:i.url})}catch(e){this._showToast("Файл «"+t.name+"» не загружен: "+(e?.code||e?.message||e))}a.length&&this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:[...this._markerDialog.pdfs,...a]},a.length&&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 n=s||null,o=a||null;"virtual"!==t.binding||n||(n=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=n,l.area=o);const c=r?this._devices.find(t=>t.id===r):null,h=!!t.room&&null!=c&&(c.space!==n||c.area!==o);if(e.markers=e.markers.filter(t=>t.id!==i&&t.id!==r),e.markers.push(l),!this._layout[i]||h){const t=this._spaceModel(n||void 0);let e=t.vb[0]+t.vb[2]/2,s=t.vb[1]+t.vb[3]/2;if(o){const i=t.rooms.find(t=>t.area===o);i&&([e,s]=this._roomCenter(i))}this._layout[i]=this._normPos(n||this._space,e,s)}await this._saveConfigNow(),await this.hass.callWS({type:"houseplan/layout/set",layout:this._layout}),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство сохранено")}catch(t){this._markerDialog={...this._markerDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||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(),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство убрано с плана")}catch(t){this._showToast("Ошибка: "+(t?.message||t))}}_normPos(t,e,i){const s=this._serverCfg.spaces.find(e=>e.id===t)?.aspect||1;return{s:t,x:e/bt,y:i/(bt/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 n="";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:o,aspect:l,name:i.name}}}async _saveSpaceDialog(){const t=this._spaceDialog;if(t&&!t.busy&&t.title.trim()){this._spaceDialog={...t,busy:!0};try{const e=this._serverCfg;let i;if("create"===t.mode?(i={id:"s"+Date.now().toString(36),title:t.title.trim(),plan_url:null,aspect:1.414,view_box:[0,0,1,1],rooms:[],segments:[]},e.spaces.push(i)):(i=e.spaces.find(e=>e.id===t.spaceId),i.title=t.title.trim()),t.planFile){const e=await this.hass.callWS({type:"houseplan/plan/set",space_id:i.id,ext:t.planFile.ext,data:t.planFile.b64});i.plan_url=e.url,i.aspect=t.planFile.aspect}await this._saveConfigNow(),this._spaceDialog=null,"create"===t.mode&&(this._space=i.id),this._regSignature="",this._maybeRebuildDevices(),this._showToast("create"===t.mode?"Пространство добавлено":"Пространство сохранено")}catch(t){this._spaceDialog={...this._spaceDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||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("Ошибка удаления: "+(t?.message||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` + >`: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)}}ft.properties={hass:{attribute:!1},_config:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",ft);const vt="houseplan_card_layout_v1",bt=1e3,yt=(t,e,i)=>{const s=new Event(e,{bubbles:!0,composed:!0});s.detail=i??{},t.dispatchEvent(s)},$t=(t,e)=>{let i;return(...s)=>{clearTimeout(i),i=window.setTimeout(()=>t(...s),e)}};class xt extends ot{constructor(){super(...arguments),this._space="f1",this._edit=!1,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._infoCard=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=t=>this._onKey(t),this._drag=null,this._dirtyPos=new Set,this._persistLayout=$t(()=>{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)))}}else localStorage.setItem(vt,JSON.stringify(this._layout))},600),this._saveConfig=$t(()=>{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._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)}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}get _model(){return this._serverCfg?this._serverCfg.spaces.map(t=>{const e=bt/t.aspect;return{id:t.id,title:t.title,vb:[t.view_box[0]*bt,t.view_box[1]*e,t.view_box[2]*bt,t.view_box[3]*e],bg:t.plan_url?{href:t.plan_url,x:0,y:0,w:bt,h:e}:null,rooms:t.rooms.map(t=>({id:t.id,name:t.name,area:t.area??null,x:null!=t.x?t.x*bt:void 0,y:null!=t.y?t.y*e:void 0,w:null!=t.w?t.w*bt:void 0,h:null!=t.h?t.h*e:void 0,poly:t.poly?t.poly.map(t=>[t[0]*bt,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 _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())}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)}catch(t){if(this._loadTries>=8){this._serverStorage=!1,this._serverCfg=null;try{this._layout=JSON.parse(localStorage.getItem(vt)||"{}")||{}}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._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=this._buildDevices(),this._defPos=this._defaultPositions())}_entitiesByDevice(){const t={};for(const[e,i]of Object.entries(this.hass.entities))i?.device_id&&(t[i.device_id]=t[i.device_id]||[]).push(e);return t}_domainOfDevice(t,e){if(t.identifiers?.[0]?.[0])return t.identifiers[0][0];for(const t of e){const e=this.hass.entities[t]?.platform;if(e)return e}return""}_primaryEntity(t,e){const i=t.map(t=>({eid:t,reg:this.hass.entities[t],st:this.hass.states[t]})).filter(t=>t.reg&&!t.reg.hidden),s=i.filter(t=>!t.reg.entity_category),a=s.length?s:i;if("mdi:thermometer"===e||"mdi:air-filter"===e){const t=a.find(t=>this._isTempEntity(t.eid));if(t)return t.eid}for(const t of pt){const e=a.find(e=>e.eid.split(".")[0]===t);if(e)return e.eid}return a[0]?.eid}_isTempEntity(t){const e=this.hass.states[t];if(!e)return/_temperature$/.test(t);const i=e.attributes||{};return"temperature"===i.device_class||/°C|°F/.test(i.unit_of_measurement||"")||/_temperature$/.test(t)}_lqiFor(t){const e=[];for(const i of t){const t=this.hass.states[i];if(!(/_linkquality$/.test(i)||"lqi"===(t?.attributes?.unit_of_measurement||""))||!t)continue;const s=parseFloat(t.state);isNaN(s)||e.push(s)}return e.length?Math.round(e.reduce((t,e)=>t+e,0)/e.length):null}_roomLqi(t){if(!t)return null;const e=[];for(const i of this._devices){if(i.area!==t||i.virtual)continue;const s=this._lqiFor(i.entities);null!=s&&e.push(s)}return e.length?Math.round(e.reduce((t,e)=>t+e,0)/e.length):null}_tempFor(t){for(const e of t){if(!this._isTempEntity(e))continue;const t=this.hass.states[e];if(!t)continue;const i=parseFloat(t.state);if(!isNaN(i))return Math.round(10*i)/10}return null}_lightGroups(){if(!1===this._settings.group_lights)return[];const t=this.hass,e=[];for(const[i,s]of Object.entries(t.entities)){if(!i.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 n=t.states[i];e.push({eid:i,name:s.name||n?.attributes?.friendly_name||i,area:a})}return e}_groupedLightAreas(){return new Set(this._lightGroups().map(t=>t.area))}get _markers(){return this._serverCfg?.markers||[]}_markerFor(t,e){return this._markers.find(i=>i.binding===t+":"+e)}get _claimed(){const t=new Set;for(const e of this._markers){const[i,s]=e.binding.split(":");"device"!==i&&"entity"!==i||!s||t.add(e.binding)}return t}_applyMarker(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||[]}_buildDevices(){const t=this.hass,e=this._areaToSpace,i=!1!==this._settings.group_lights,s=this._groupedLightAreas(),a=this._excluded,n=this._entitiesByDevice(),r=this._claimed,o={},l=[];for(const c of Object.values(t.devices)){const t=c.area_id;if(!t||!e[t])continue;if("service"===c.entry_type)continue;if(r.has("device:"+c.id))continue;const h=this._markerFor("device",c.id);if(h&&h.hidden)continue;const d=n[c.id]||[],p=this._domainOfDevice(c,d);if(a.has(p))continue;if("Group"===c.model)continue;if(/scene/i.test(c.model||""))continue;if(/bridge/i.test((c.model||"")+(c.name||"")))continue;if("myheat"===p&&c.via_device_id)continue;const u=(c.name_by_user||c.name||"без имени").trim(),_=u+"|"+t;if(o[_])continue;o[_]=1;let g=dt(u,c.model);if(d.some(t=>t.startsWith("lock."))&&(g="mdi:lock"),i&&"mdi:lightbulb"===g&&s.has(t))continue;const m={id:c.id,name:u,model:c.model||"",area:t,space:e[t].space,icon:g,entities:d,bindingKind:"device",bindingRef:c.id,pdfs:[]};m.primary=this._primaryEntity(d,g),"mdi:thermometer"!==g&&"mdi:air-filter"!==g||(m.temp=this._tempFor(d)),l.push(m)}for(const t of this._lightGroups())e[t.area]&&(r.has("entity:"+t.eid)||l.push({id:"lg_"+t.eid,name:t.name,model:"группа света",area:t.area,space:e[t.area].space,icon:"mdi:lightbulb-group",entities:[t.eid],primary:t.eid,bindingKind:"entity",bindingRef:t.eid,pdfs:[]}));for(const i of this._markers){if(i.hidden)continue;const[s,a]=i.binding.split(":");if("device"===s){const s=t.devices[a],r=i.area||s?.area_id||"",o=r&&e[r]?.space||i.space||this._model[0]?.id||"",c=s&&n[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:i.id,name:s?.name_by_user||s?.name||"устройство",model:s?.model||"",area:r,space:o,icon:h,entities:c,bindingKind:"device",bindingRef:a};d.primary=this._primaryEntity(c,h),"mdi:thermometer"!==h&&"mdi:air-filter"!==h||(d.temp=this._tempFor(c)),this._applyMarker(d,i),l.push(d)}else if("entity"===s){const s=t.entities[a],n=i.area||s?.area_id||s?.device_id&&t.devices[s.device_id]?.area_id||"",r=n&&e[n]?.space||i.space||this._model[0]?.id||"",o=t.states[a],c={id:i.id,name:s?.name||o?.attributes?.friendly_name||a,model:"",area:n,space:r,icon:"mdi:shape-outline",entities:[a],primary:a,bindingKind:"entity",bindingRef:a};this._applyMarker(c,i),l.push(c)}else{const t=i.area||"",s=i.space||t&&e[t]?.space||this._model[0]?.id||"",a={id:i.id,name:i.name||"виртуальное устройство",model:i.model||"",area:t,space:s,icon:i.icon||"mdi:map-marker",entities:[],bindingKind:"virtual",virtual:!0};this._applyMarker(a,i),l.push(a)}}return l}_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={};for(const e of this._model)for(const i of e.rooms){if(!i.area)continue;const s=this._devices.filter(t=>t.area===i.area&&t.space===e.id);if(!s.length)continue;const a=this._roomBounds(i),n=.12*Math.min(a.w,a.h),r=a.w-2*n,o=a.h-2*n,l=Math.max(1,Math.round(Math.sqrt(s.length*r/Math.max(o,1)))),c=Math.ceil(s.length/l),h=r/l,d=o/Math.max(c,1);s.forEach((e,i)=>{const s=i%l,r=Math.floor(i/l);t[e.id]={x:a.x+n+h*(s+.5),y:a.y+n+d*(r+.5)}})}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*bt,y:i.y*(bt/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,n=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/bt,y:n/(bt/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:this._tempFor(t.entities):null}_openMoreInfo(t){t?yt(this,"hass-more-info",{entityId:t}):this._showToast("У устройства нет подходящей сущности")}_clickDevice(t,e){if(t.stopPropagation(),!this._drag?.moved&&!this._markup)return this._edit?(this._selId=e.id,void this._openMarkerDialog(e)):void(this._infoCard=e)}_clickRoom(t){var e;!this._edit&&t.area&&(e="/config/areas/area/"+t.area,history.pushState(null,"",e),yt(window,"location-changed",{replace:!1}))}_pointerDown(t,e){if(!this._edit)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(),n=(t.clientX-this._drag.sx)/a.width*s[2],r=(t.clientY-this._drag.sy)/a.height*s[3];Math.abs(t.clientX-this._drag.sx)+Math.abs(t.clientY-this._drag.sy)>3&&(this._drag.moved=!0);const o=.008*Math.min(s[2],s[3]),l=Math.max(s[0]+o,Math.min(s[0]+s[2]-o,this._drag.ox+n)),c=Math.max(s[1]+o,Math.min(s[1]+s[3]-o,this._drag.oy+r));this._savePos(e,l,c)}_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))}_applyXY(t,e){if(!this._selId)return;const i=parseFloat(e);if(isNaN(i))return;const s=this._devices.find(t=>t.id===this._selId);if(!s||s.virtual)return;const a={...this._pos(s)};a[t]=i,this._savePos(s,a.x,a.y)}_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 bt/120}get _curSpaceCfg(){return this._serverCfg?.spaces.find(t=>t.id===this._space)}get _spaceH(){const t=this._curSpaceCfg;return t?bt/t.aspect:bt}get _segments(){const t=this._curSpaceCfg,e=this._spaceH;return(t?.segments||[]).map(t=>[t[0]*bt,t[1]*e,t[2]*bt,t[3]*e])}_toggleMarkup(){this._norm?(this._markup=!this._markup,this._edit=!1,this._path=[],this._cursorPt=null,this._tool="draw"):this._showToast("Разметка доступна после переноса конфига на сервер (режим правки → «На сервер»)")}_svgPoint(t){const e=this.renderRoot.querySelector(".stage"),i=this._spaceModel().vb,s=e.getBoundingClientRect();return[i[0]+(t.clientX-s.left)/s.width*i[2],i[1]+(t.clientY-s.top)/s.height*i[3]]}_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!n&&(i.segments=i.segments||[],i.segments.push([t[0]/bt,t[1]/s,e[0]/bt,e[1]/s]),this._saveConfig(),!0)}_distToSeg(t,e){const[i,s]=t,[a,n,r,o]=e,l=r-a,c=o-n;let h=((i-a)*l+(s-n)*c)/(l*l+c*c||1);h=Math.max(0,Math.min(1,h));const d=a+h*l,p=n+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]<(o-n)*(t[1]-r)/(l-r)+n&&(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 n=this._distToSeg(e,t);n=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(){if(!this._contourClosed||!this._areaSel&&!this._nameSel)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]/bt,t[1]/e])}),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 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 n=(a.name_by_user||a.name||a.id).trim();t!==this._markerDialog?.binding&&i.has(n+"|"+(a.area_id||""))||s.push({value:t,label:n,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,n]of Object.entries(t.entities)){const r="entity:"+i;if(e.has(r))continue;const o=a.has(n.platform),l="group"===n.platform;if(!o&&!l)continue;if(n.hidden)continue;const c=t.states[i];s.push({value:r,label:n.name||c?.attributes?.friendly_name||i,sub:i.split(".")[0]+" · "+("group"===n.platform?"группа":"хелпер")})}const n=(this._markerDialog?.bindingFilter||"").toLowerCase().trim(),r=n?s.filter(t=>(t.label+" "+t.sub+" "+t.value).toLowerCase().includes(n)):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"неизвестная ошибка";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=this.hass?.auth?.data?.access_token,n=[];for(const t of i)try{const e=new FormData;e.append("marker_id",s),e.append("file",t,t.name);const i=await fetch("/api/houseplan/upload",{method:"POST",body:e,headers:a?{authorization:`Bearer ${a}`}:{}}),r=await i.json().catch(()=>({}));if(!i.ok||r.error){const t={too_large:"файл больше "+(r.max_mb||25)+" МБ",bad_ext:"недопустимый тип (нужен PDF/изображение)",unauthorized:"нужны права администратора"};throw new Error(t[r.error]||r.error||"HTTP "+i.status)}n.push({name:r.name||t.name,url:r.url})}catch(e){this._showToast("Файл «"+t.name+"» не загружен: "+this._errText(e))}n.length&&this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:[...this._markerDialog.pdfs,...n]},this._showToast("Прикреплено файлов: "+n.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 n=s||null,r=a||null;"virtual"!==t.binding||n||(n=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 o=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=n,l.area=r);const c=o?this._devices.find(t=>t.id===o):null,h=!!t.room&&null!=c&&(c.space!==n||c.area!==r);if(e.markers=e.markers.filter(t=>t.id!==i&&t.id!==o),e.markers.push(l),!this._layout[i]||h){const t=this._spaceModel(n||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))}this._layout[i]=this._normPos(n||this._space,e,s)}await this._saveConfigNow(),await this.hass.callWS({type:"houseplan/layout/set",layout:this._layout}),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство сохранено")}catch(t){this._markerDialog={...this._markerDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||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(),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство убрано с плана")}catch(t){this._showToast("Ошибка: "+(t?.message||t))}}_normPos(t,e,i){const s=this._serverCfg.spaces.find(e=>e.id===t)?.aspect||1;return{s:t,x:e/bt,y:i/(bt/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 n="";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=o});URL.revokeObjectURL(o),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()){this._spaceDialog={...t,busy:!0};try{const e=this._serverCfg;let i;if("create"===t.mode?(i={id:"s"+Date.now().toString(36),title:t.title.trim(),plan_url:null,aspect:1.414,view_box:[0,0,1,1],rooms:[],segments:[]},e.spaces.push(i)):(i=e.spaces.find(e=>e.id===t.spaceId),i.title=t.title.trim()),t.planFile){const e=await this.hass.callWS({type:"houseplan/plan/set",space_id:i.id,ext:t.planFile.ext,data:t.planFile.b64});i.plan_url=e.url,i.aspect=t.planFile.aspect}await this._saveConfigNow(),this._spaceDialog=null,"create"===t.mode&&(this._space=i.id),this._regSignature="",this._maybeRebuildDevices(),this._showToast("create"===t.mode?"Пространство добавлено":"Пространство сохранено")}catch(t){this._spaceDialog={...this._spaceDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||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("Ошибка удаления: "+(t?.message||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`
${this._config.title||"План дома"}
@@ -65,12 +65,12 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow ${this._markup?this._renderMarkupDefs(i):B} ${e.bg?L``: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,n=this._roomCenter(t),o=t.poly?L`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,n=this._roomCenter(t),r=t.poly?L`this._clickRoom(t)} @mousemove=${s} @mouseleave=${()=>this._tip=null}>`:L`this._clickRoom(t)} @mousemove=${s} - @mouseleave=${()=>this._tip=null}>`;return L`${o}${a?L`${t.name}`:B}`})} + @mouseleave=${()=>this._tip=null}>`;return L`${r}${a?L`${t.name}`:B}`})} ${this._markup?this._renderMarkupLayer(i):B}
@@ -89,19 +89,19 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
`:B} ${this._toast?j`
${this._toast}
`:B}
- `}_renderDevice(t,e){const i=this._pos(t),s=(i.x-e[0])/e[2]*100,a=(i.y-e[1])/e[3]*100,n=this._stateClass(t),o=this._liveTemp(t),r=this._config?.show_signal&&!t.virtual?this._lqiFor(t.entities):null;return j`
this._clickDevice(e,t)} - @mousemove=${e=>this._showTip(e,t.name,t.model+(null!=o?" · "+o+"°":"")+(null!=r?" · LQI "+r:""))} + @mousemove=${e=>this._showTip(e,t.name,t.model+(null!=r?" · "+r+"°":"")+(null!=o?" · LQI "+o:""))} @mouseleave=${()=>this._tip=null} @pointerdown=${e=>this._pointerDown(e,t)} @pointermove=${e=>this._pointerMove(e,t)} @pointerup=${e=>this._pointerUp(e,t)} > - ${null!=o?j`${o}°`:B} - ${null!=r?j`${r}`:B} + ${null!=r?j`${r}°`:B} + ${null!=o?j`${o}`: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 L` @@ -986,4 +986,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow z-index: 120; max-width: 90vw; } - `,customElements.get("houseplan-card")||customElements.define("houseplan-card",xt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Интерактивный план дома: пространства, комнаты, устройства с живыми состояниями и drag-раскладкой."}),console.info("%c HOUSEPLAN-CARD %c v1.7.3 ","background:#3ea6ff;color:#04121f;font-weight:700",""); + `,customElements.get("houseplan-card")||customElements.define("houseplan-card",xt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Интерактивный план дома: пространства, комнаты, устройства с живыми состояниями и drag-раскладкой."}),console.info("%c HOUSEPLAN-CARD %c v1.7.4 ","background:#3ea6ff;color:#04121f;font-weight:700",""); diff --git a/custom_components/houseplan/http_api.py b/custom_components/houseplan/http_api.py new file mode 100644 index 0000000..61a5ec0 --- /dev/null +++ b/custom_components/houseplan/http_api.py @@ -0,0 +1,83 @@ +"""HTTP-эндпоинт загрузки файлов-инструкций House Plan. + +Файлы (PDF и т.п.) грузятся не через WebSocket (у него лимит размера сообщения — +большой PDF рвёт соединение), а обычным multipart POST — как медиа в самом HA. +""" +from __future__ import annotations + +import logging +from pathlib import Path + +from aiohttp import web + +from homeassistant.components.http import HomeAssistantView +from homeassistant.core import HomeAssistant + +from .const import CONF_ADMIN_ONLY, DOMAIN, FILES_DIR, FILES_URL +from .validation import ( + FILE_EXTENSIONS, + MAX_FILE_BYTES, + file_ext, + sanitize_filename, + sanitize_marker_id, +) + +_LOGGER = logging.getLogger(__name__) + + +class HouseplanUploadView(HomeAssistantView): + """POST /api/houseplan/upload — сохранить файл маркера, вернуть URL.""" + + url = "/api/houseplan/upload" + name = "api:houseplan:upload" + requires_auth = True + + async def post(self, request: web.Request) -> web.Response: + hass: HomeAssistant = request.app["hass"] + entry = hass.data.get(DOMAIN, {}).get("entry") + admin_only = bool(entry and entry.options.get(CONF_ADMIN_ONLY, False)) + if admin_only: + user = request.get("hass_user") + if user is None or not user.is_admin: + return web.json_response({"error": "unauthorized"}, status=403) + + marker_id = "misc" + filename: str | None = None + blob: bytes | None = None + try: + reader = await request.multipart() + async for part in reader: + if part.name == "marker_id": + marker_id = sanitize_marker_id(await part.text()) + elif part.name == "file": + filename = part.filename or "file" + blob = await part.read(decode=False) + except Exception as err: # noqa: BLE001 + _LOGGER.warning("House Plan upload: ошибка чтения multipart: %s", err) + return web.json_response({"error": "bad_request"}, status=400) + + if blob is None or not filename: + return web.json_response({"error": "no_file"}, status=400) + ext = file_ext(filename) + if ext not in FILE_EXTENSIONS: + return web.json_response( + {"error": "bad_ext", "allowed": sorted(FILE_EXTENSIONS)}, status=400 + ) + if len(blob) > MAX_FILE_BYTES: + return web.json_response( + {"error": "too_large", "max_mb": MAX_FILE_BYTES // 1024 // 1024}, status=413 + ) + + safe_name = sanitize_filename(filename) + target_dir = Path(hass.config.path(FILES_DIR)) / marker_id + path = target_dir / safe_name + + def _write() -> None: + target_dir.mkdir(parents=True, exist_ok=True) + path.write_bytes(blob) + + await hass.async_add_executor_job(_write) + mtime = int(path.stat().st_mtime) + return web.json_response( + {"ok": True, "url": f"{FILES_URL}/{marker_id}/{safe_name}?v={mtime}", "name": filename} + ) diff --git a/custom_components/houseplan/manifest.json b/custom_components/houseplan/manifest.json index 03c3f0d..34eba92 100755 --- a/custom_components/houseplan/manifest.json +++ b/custom_components/houseplan/manifest.json @@ -15,5 +15,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/justbusiness/houseplan-card/issues", "requirements": [], - "version": "1.7.3" + "version": "1.7.4" } \ No newline at end of file diff --git a/dist/houseplan-card.js b/dist/houseplan-card.js index 94b934a..246e4c4 100755 --- a/dist/houseplan-card.js +++ b/dist/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 n=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:o,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)=>!o(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 n=s?.call(this);a?.call(this,e),this.requestUpdate(t,n,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(n(t))}else void 0!==t&&e.push(n(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 n=a.fromAttribute(e,t.type);this[s]=n??this._$Ej?.get(s)??n,this._$Em=null}}requestUpdate(t,e,i,s=!1,a){if(void 0!==t){const n=this.constructor;if(!1===s&&(a=this[t]),i??=n.getPropertyOptions(t),!((i.hasChanged??v)(a,e)||i.useDefault&&i.reflect&&a===this._$Ej?.get(t)&&!this.hasAttribute(n._$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},n){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),!0!==a||void 0!==n)||(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 $=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,D=`<${A}>`,E=document,P=()=>E.createComment(""),M=t=>null===t||"object"!=typeof t&&"function"!=typeof t,R=Array.isArray,z="[ \t\n\f\r]",T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,U=/-->/g,O=/>/g,H=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),I=/'/g,N=/"/g,q=/^(?:script|style|textarea|title)$/i,F=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),j=F(1),L=F(2),K=Symbol.for("lit-noChange"),B=Symbol.for("lit-nothing"),W=new WeakMap,X=E.createTreeWalker(E,129);function Y(t,e){if(!R(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==k?k.createHTML(e):e}const G=(t,e)=>{const i=t.length-1,s=[];let a,n=2===e?"":3===e?"":"",o=T;for(let e=0;e"===l[0]?(o=a??T,c=-1):void 0===l[1]?c=-2:(c=o.lastIndex-l[2].length,r=l[1],o=void 0===l[3]?H:'"'===l[3]?N:I):o===N||o===I?o=H:o===U||o===O?o=T:(o=H,a=void 0);const d=o===H&&t[e+1].startsWith("/>")?" ":"";n+=o===T?i+D:c>=0?(s.push(r),i.slice(0,c)+S+i.slice(c)+C+d):i+C+(-2===c?e:d)}return[Y(t,n+(t[i]||"")+(2===e?"":3===e?"":"")),s]};class V{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let a=0,n=0;const o=t.length-1,r=this.parts,[l,c]=G(t,e);if(this.el=V.createElement(l,i),X.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(s=X.nextNode())&&r.length0){s.textContent=w?w.emptyScript:"";for(let i=0;iR(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&&M(this._$AH)?this._$AA.nextSibling.data=t:this.T(E.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=V.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(e);else{const t=new Z(s,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=W.get(t.strings);return void 0===e&&W.set(t.strings,e=new V(t)),e}k(t){R(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(P()),this.O(P()),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 n=!1;if(void 0===a)t=J(this,t,e,0),n=!M(t)||t!==this._$AH&&t!==K,n&&(this._$AH=t);else{const s=t;let o,r;for(t=a[0],o=0;o{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(P(),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 K}}rt._$litElement$=!0,rt.finalized=!0,ot.litElementHydrateSupport?.({LitElement:rt});const lt=ot.litElementPolyfillSupport;lt?.({LitElement:rt}),(ot.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(new RegExp(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}const gt=[{name:"title",selector:{text:{}}},{name:"default_floor",selector:{select:{mode:"dropdown",options:[{value:"f1",label:"1 этаж"},{value:"f2",label:"2 этаж"},{value:"yard",label:"Двор"}]}}},{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:{}}}],mt={title:"Заголовок",default_floor:"Этаж по умолчанию",icon_size:"Размер иконок, % ширины плана",show_temperature:"Показывать температуру",live_states:"Живые состояния (вкл/выкл, открыто…)",show_signal:"Показывать сигнал zigbee (LQI)"};class ft extends rt{setConfig(t){this._config=t}render(){return this.hass&&this._config?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:o,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)=>!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&&o(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 n=s?.call(this);a?.call(this,e),this.requestUpdate(t,n,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(n(t))}else void 0!==t&&e.push(n(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 n=a.fromAttribute(e,t.type);this[s]=n??this._$Ej?.get(s)??n,this._$Em=null}}requestUpdate(t,e,i,s=!1,a){if(void 0!==t){const n=this.constructor;if(!1===s&&(a=this[t]),i??=n.getPropertyOptions(t),!((i.hasChanged??v)(a,e)||i.useDefault&&i.reflect&&a===this._$Ej?.get(t)&&!this.hasAttribute(n._$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},n){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),!0!==a||void 0!==n)||(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 $=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,D=`<${A}>`,P=document,E=()=>P.createComment(""),M=t=>null===t||"object"!=typeof t&&"function"!=typeof t,R=Array.isArray,z="[ \t\n\f\r]",T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,U=/-->/g,O=/>/g,H=RegExp(`>|${z}(?:([^\\s"'>=/]+)(${z}*=${z}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),I=/'/g,N=/"/g,q=/^(?:script|style|textarea|title)$/i,F=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),j=F(1),L=F(2),K=Symbol.for("lit-noChange"),B=Symbol.for("lit-nothing"),W=new WeakMap,X=P.createTreeWalker(P,129);function Y(t,e){if(!R(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==k?k.createHTML(e):e}const G=(t,e)=>{const i=t.length-1,s=[];let a,n=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,o=l[1],r=void 0===l[3]?H:'"'===l[3]?N:I):r===N||r===I?r=H:r===U||r===O?r=T:(r=H,a=void 0);const d=r===H&&t[e+1].startsWith("/>")?" ":"";n+=r===T?i+D:c>=0?(s.push(o),i.slice(0,c)+S+i.slice(c)+C+d):i+C+(-2===c?e:d)}return[Y(t,n+(t[i]||"")+(2===e?"":3===e?"":"")),s]};class V{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let a=0,n=0;const r=t.length-1,o=this.parts,[l,c]=G(t,e);if(this.el=V.createElement(l,i),X.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(s=X.nextNode())&&o.length0){s.textContent=w?w.emptyScript:"";for(let i=0;iR(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&&M(this._$AH)?this._$AA.nextSibling.data=t:this.T(P.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=V.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(e);else{const t=new Z(s,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=W.get(t.strings);return void 0===e&&W.set(t.strings,e=new V(t)),e}k(t){R(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(E()),this.O(E()),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 n=!1;if(void 0===a)t=J(this,t,e,0),n=!M(t)||t!==this._$AH&&t!==K,n&&(this._$AH=t);else{const s=t;let r,o;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(E(),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 K}}ot._$litElement$=!0,ot.finalized=!0,rt.litElementHydrateSupport?.({LitElement:ot});const lt=rt.litElementPolyfillSupport;lt?.({LitElement:ot}),(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(new RegExp(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}const gt=[{name:"title",selector:{text:{}}},{name:"default_floor",selector:{select:{mode:"dropdown",options:[{value:"f1",label:"1 этаж"},{value:"f2",label:"2 этаж"},{value:"yard",label:"Двор"}]}}},{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:{}}}],mt={title:"Заголовок",default_floor:"Этаж по умолчанию",icon_size:"Размер иконок, % ширины плана",show_temperature:"Показывать температуру",live_states:"Живые состояния (вкл/выкл, открыто…)",show_signal:"Показывать сигнал zigbee (LQI)"};class ft extends ot{setConfig(t){this._config=t}render(){return this.hass&&this._config?j`mt[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)}}ft.properties={hass:{attribute:!1},_config:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",ft);const vt="houseplan_card_layout_v1",bt=1e3,yt=(t,e,i)=>{const s=new Event(e,{bubbles:!0,composed:!0});s.detail=i??{},t.dispatchEvent(s)},$t=(t,e)=>{let i;return(...s)=>{clearTimeout(i),i=window.setTimeout(()=>t(...s),e)}};class xt extends rt{constructor(){super(...arguments),this._space="f1",this._edit=!1,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._infoCard=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=t=>this._onKey(t),this._drag=null,this._dirtyPos=new Set,this._persistLayout=$t(()=>{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)))}}else localStorage.setItem(vt,JSON.stringify(this._layout))},600),this._saveConfig=$t(()=>{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._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)}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}get _model(){return this._serverCfg?this._serverCfg.spaces.map(t=>{const e=bt/t.aspect;return{id:t.id,title:t.title,vb:[t.view_box[0]*bt,t.view_box[1]*e,t.view_box[2]*bt,t.view_box[3]*e],bg:t.plan_url?{href:t.plan_url,x:0,y:0,w:bt,h:e}:null,rooms:t.rooms.map(t=>({id:t.id,name:t.name,area:t.area??null,x:null!=t.x?t.x*bt:void 0,y:null!=t.y?t.y*e:void 0,w:null!=t.w?t.w*bt:void 0,h:null!=t.h?t.h*e:void 0,poly:t.poly?t.poly.map(t=>[t[0]*bt,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 _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())}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)}catch(t){if(this._loadTries>=8){this._serverStorage=!1,this._serverCfg=null;try{this._layout=JSON.parse(localStorage.getItem(vt)||"{}")||{}}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._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=this._buildDevices(),this._defPos=this._defaultPositions())}_entitiesByDevice(){const t={};for(const[e,i]of Object.entries(this.hass.entities))i?.device_id&&(t[i.device_id]=t[i.device_id]||[]).push(e);return t}_domainOfDevice(t,e){if(t.identifiers?.[0]?.[0])return t.identifiers[0][0];for(const t of e){const e=this.hass.entities[t]?.platform;if(e)return e}return""}_primaryEntity(t,e){const i=t.map(t=>({eid:t,reg:this.hass.entities[t],st:this.hass.states[t]})).filter(t=>t.reg&&!t.reg.hidden),s=i.filter(t=>!t.reg.entity_category),a=s.length?s:i;if("mdi:thermometer"===e||"mdi:air-filter"===e){const t=a.find(t=>this._isTempEntity(t.eid));if(t)return t.eid}for(const t of pt){const e=a.find(e=>e.eid.split(".")[0]===t);if(e)return e.eid}return a[0]?.eid}_isTempEntity(t){const e=this.hass.states[t];if(!e)return/_temperature$/.test(t);const i=e.attributes||{};return"temperature"===i.device_class||/°C|°F/.test(i.unit_of_measurement||"")||/_temperature$/.test(t)}_lqiFor(t){const e=[];for(const i of t){const t=this.hass.states[i];if(!(/_linkquality$/.test(i)||"lqi"===(t?.attributes?.unit_of_measurement||""))||!t)continue;const s=parseFloat(t.state);isNaN(s)||e.push(s)}return e.length?Math.round(e.reduce((t,e)=>t+e,0)/e.length):null}_roomLqi(t){if(!t)return null;const e=[];for(const i of this._devices){if(i.area!==t||i.virtual)continue;const s=this._lqiFor(i.entities);null!=s&&e.push(s)}return e.length?Math.round(e.reduce((t,e)=>t+e,0)/e.length):null}_tempFor(t){for(const e of t){if(!this._isTempEntity(e))continue;const t=this.hass.states[e];if(!t)continue;const i=parseFloat(t.state);if(!isNaN(i))return Math.round(10*i)/10}return null}_lightGroups(){if(!1===this._settings.group_lights)return[];const t=this.hass,e=[];for(const[i,s]of Object.entries(t.entities)){if(!i.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 n=t.states[i];e.push({eid:i,name:s.name||n?.attributes?.friendly_name||i,area:a})}return e}_groupedLightAreas(){return new Set(this._lightGroups().map(t=>t.area))}get _markers(){return this._serverCfg?.markers||[]}_markerFor(t,e){return this._markers.find(i=>i.binding===t+":"+e)}get _claimed(){const t=new Set;for(const e of this._markers){const[i,s]=e.binding.split(":");"device"!==i&&"entity"!==i||!s||t.add(e.binding)}return t}_applyMarker(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||[]}_buildDevices(){const t=this.hass,e=this._areaToSpace,i=!1!==this._settings.group_lights,s=this._groupedLightAreas(),a=this._excluded,n=this._entitiesByDevice(),o=this._claimed,r={},l=[];for(const c of Object.values(t.devices)){const t=c.area_id;if(!t||!e[t])continue;if("service"===c.entry_type)continue;if(o.has("device:"+c.id))continue;const h=this._markerFor("device",c.id);if(h&&h.hidden)continue;const d=n[c.id]||[],p=this._domainOfDevice(c,d);if(a.has(p))continue;if("Group"===c.model)continue;if(/scene/i.test(c.model||""))continue;if(/bridge/i.test((c.model||"")+(c.name||"")))continue;if("myheat"===p&&c.via_device_id)continue;const u=(c.name_by_user||c.name||"без имени").trim(),_=u+"|"+t;if(r[_])continue;r[_]=1;let g=dt(u,c.model);if(d.some(t=>t.startsWith("lock."))&&(g="mdi:lock"),i&&"mdi:lightbulb"===g&&s.has(t))continue;const m={id:c.id,name:u,model:c.model||"",area:t,space:e[t].space,icon:g,entities:d,bindingKind:"device",bindingRef:c.id,pdfs:[]};m.primary=this._primaryEntity(d,g),"mdi:thermometer"!==g&&"mdi:air-filter"!==g||(m.temp=this._tempFor(d)),l.push(m)}for(const t of this._lightGroups())e[t.area]&&(o.has("entity:"+t.eid)||l.push({id:"lg_"+t.eid,name:t.name,model:"группа света",area:t.area,space:e[t.area].space,icon:"mdi:lightbulb-group",entities:[t.eid],primary:t.eid,bindingKind:"entity",bindingRef:t.eid,pdfs:[]}));for(const i of this._markers){if(i.hidden)continue;const[s,a]=i.binding.split(":");if("device"===s){const s=t.devices[a],o=i.area||s?.area_id||"",r=o&&e[o]?.space||i.space||this._model[0]?.id||"",c=s&&n[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:i.id,name:s?.name_by_user||s?.name||"устройство",model:s?.model||"",area:o,space:r,icon:h,entities:c,bindingKind:"device",bindingRef:a};d.primary=this._primaryEntity(c,h),"mdi:thermometer"!==h&&"mdi:air-filter"!==h||(d.temp=this._tempFor(c)),this._applyMarker(d,i),l.push(d)}else if("entity"===s){const s=t.entities[a],n=i.area||s?.area_id||s?.device_id&&t.devices[s.device_id]?.area_id||"",o=n&&e[n]?.space||i.space||this._model[0]?.id||"",r=t.states[a],c={id:i.id,name:s?.name||r?.attributes?.friendly_name||a,model:"",area:n,space:o,icon:"mdi:shape-outline",entities:[a],primary:a,bindingKind:"entity",bindingRef:a};this._applyMarker(c,i),l.push(c)}else{const t=i.area||"",s=i.space||t&&e[t]?.space||this._model[0]?.id||"",a={id:i.id,name:i.name||"виртуальное устройство",model:i.model||"",area:t,space:s,icon:i.icon||"mdi:map-marker",entities:[],bindingKind:"virtual",virtual:!0};this._applyMarker(a,i),l.push(a)}}return l}_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={};for(const e of this._model)for(const i of e.rooms){if(!i.area)continue;const s=this._devices.filter(t=>t.area===i.area&&t.space===e.id);if(!s.length)continue;const a=this._roomBounds(i),n=.12*Math.min(a.w,a.h),o=a.w-2*n,r=a.h-2*n,l=Math.max(1,Math.round(Math.sqrt(s.length*o/Math.max(r,1)))),c=Math.ceil(s.length/l),h=o/l,d=r/Math.max(c,1);s.forEach((e,i)=>{const s=i%l,o=Math.floor(i/l);t[e.id]={x:a.x+n+h*(s+.5),y:a.y+n+d*(o+.5)}})}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*bt,y:i.y*(bt/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,n=Math.round(i/s)*s,o=this._serverCfg.spaces.find(e=>e.id===t.space)?.aspect||1;this._layout={...this._layout,[t.id]:{s:t.space,x:a/bt,y:n/(bt/o)}}}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:this._tempFor(t.entities):null}_openMoreInfo(t){t?yt(this,"hass-more-info",{entityId:t}):this._showToast("У устройства нет подходящей сущности")}_clickDevice(t,e){if(t.stopPropagation(),!this._drag?.moved&&!this._markup)return this._edit?(this._selId=e.id,void this._openMarkerDialog(e)):void(this._infoCard=e)}_clickRoom(t){var e;!this._edit&&t.area&&(e="/config/areas/area/"+t.area,history.pushState(null,"",e),yt(window,"location-changed",{replace:!1}))}_pointerDown(t,e){if(!this._edit)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(),n=(t.clientX-this._drag.sx)/a.width*s[2],o=(t.clientY-this._drag.sy)/a.height*s[3];Math.abs(t.clientX-this._drag.sx)+Math.abs(t.clientY-this._drag.sy)>3&&(this._drag.moved=!0);const r=.008*Math.min(s[2],s[3]),l=Math.max(s[0]+r,Math.min(s[0]+s[2]-r,this._drag.ox+n)),c=Math.max(s[1]+r,Math.min(s[1]+s[3]-r,this._drag.oy+o));this._savePos(e,l,c)}_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))}_applyXY(t,e){if(!this._selId)return;const i=parseFloat(e);if(isNaN(i))return;const s=this._devices.find(t=>t.id===this._selId);if(!s||s.virtual)return;const a={...this._pos(s)};a[t]=i,this._savePos(s,a.x,a.y)}_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 bt/120}get _curSpaceCfg(){return this._serverCfg?.spaces.find(t=>t.id===this._space)}get _spaceH(){const t=this._curSpaceCfg;return t?bt/t.aspect:bt}get _segments(){const t=this._curSpaceCfg,e=this._spaceH;return(t?.segments||[]).map(t=>[t[0]*bt,t[1]*e,t[2]*bt,t[3]*e])}_toggleMarkup(){this._norm?(this._markup=!this._markup,this._edit=!1,this._path=[],this._cursorPt=null,this._tool="draw"):this._showToast("Разметка доступна после переноса конфига на сервер (режим правки → «На сервер»)")}_svgPoint(t){const e=this.renderRoot.querySelector(".stage"),i=this._spaceModel().vb,s=e.getBoundingClientRect();return[i[0]+(t.clientX-s.left)/s.width*i[2],i[1]+(t.clientY-s.top)/s.height*i[3]]}_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!n&&(i.segments=i.segments||[],i.segments.push([t[0]/bt,t[1]/s,e[0]/bt,e[1]/s]),this._saveConfig(),!0)}_distToSeg(t,e){const[i,s]=t,[a,n,o,r]=e,l=o-a,c=r-n;let h=((i-a)*l+(s-n)*c)/(l*l+c*c||1);h=Math.max(0,Math.min(1,h));const d=a+h*l,p=n+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-n)*(t[1]-o)/(l-o)+n&&(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 n=this._distToSeg(e,t);n=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(){if(!this._contourClosed||!this._areaSel&&!this._nameSel)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]/bt,t[1]/e])}),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 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 n=(a.name_by_user||a.name||a.id).trim();t!==this._markerDialog?.binding&&i.has(n+"|"+(a.area_id||""))||s.push({value:t,label:n,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,n]of Object.entries(t.entities)){const o="entity:"+i;if(e.has(o))continue;const r=a.has(n.platform),l="group"===n.platform;if(!r&&!l)continue;if(n.hidden)continue;const c=t.states[i];s.push({value:o,label:n.name||c?.attributes?.friendly_name||i,sub:i.split(".")[0]+" · "+("group"===n.platform?"группа":"хелпер")})}const n=(this._markerDialog?.bindingFilter||"").toLowerCase().trim(),o=n?s.filter(t=>(t.label+" "+t.sub+" "+t.value).toLowerCase().includes(n)):s;return o.sort((t,e)=>t.label.localeCompare(e.label)),o.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}_fileToBase64(t){return new Promise((e,i)=>{const s=new FileReader;s.onload=()=>{const t=String(s.result||""),i=t.indexOf(",");e(i>=0?t.slice(i+1):t)},s.onerror=()=>i(s.error||new Error("read error")),s.readAsDataURL(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=await this._fileToBase64(t),i=await this.hass.callWS({type:"houseplan/file/set",marker_id:s,filename:t.name,data:e});a.push({name:i.name||t.name,url:i.url})}catch(e){this._showToast("Файл «"+t.name+"» не загружен: "+(e?.code||e?.message||e))}a.length&&this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:[...this._markerDialog.pdfs,...a]},a.length&&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 n=s||null,o=a||null;"virtual"!==t.binding||n||(n=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=n,l.area=o);const c=r?this._devices.find(t=>t.id===r):null,h=!!t.room&&null!=c&&(c.space!==n||c.area!==o);if(e.markers=e.markers.filter(t=>t.id!==i&&t.id!==r),e.markers.push(l),!this._layout[i]||h){const t=this._spaceModel(n||void 0);let e=t.vb[0]+t.vb[2]/2,s=t.vb[1]+t.vb[3]/2;if(o){const i=t.rooms.find(t=>t.area===o);i&&([e,s]=this._roomCenter(i))}this._layout[i]=this._normPos(n||this._space,e,s)}await this._saveConfigNow(),await this.hass.callWS({type:"houseplan/layout/set",layout:this._layout}),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство сохранено")}catch(t){this._markerDialog={...this._markerDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||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(),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство убрано с плана")}catch(t){this._showToast("Ошибка: "+(t?.message||t))}}_normPos(t,e,i){const s=this._serverCfg.spaces.find(e=>e.id===t)?.aspect||1;return{s:t,x:e/bt,y:i/(bt/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 n="";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:o,aspect:l,name:i.name}}}async _saveSpaceDialog(){const t=this._spaceDialog;if(t&&!t.busy&&t.title.trim()){this._spaceDialog={...t,busy:!0};try{const e=this._serverCfg;let i;if("create"===t.mode?(i={id:"s"+Date.now().toString(36),title:t.title.trim(),plan_url:null,aspect:1.414,view_box:[0,0,1,1],rooms:[],segments:[]},e.spaces.push(i)):(i=e.spaces.find(e=>e.id===t.spaceId),i.title=t.title.trim()),t.planFile){const e=await this.hass.callWS({type:"houseplan/plan/set",space_id:i.id,ext:t.planFile.ext,data:t.planFile.b64});i.plan_url=e.url,i.aspect=t.planFile.aspect}await this._saveConfigNow(),this._spaceDialog=null,"create"===t.mode&&(this._space=i.id),this._regSignature="",this._maybeRebuildDevices(),this._showToast("create"===t.mode?"Пространство добавлено":"Пространство сохранено")}catch(t){this._spaceDialog={...this._spaceDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||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("Ошибка удаления: "+(t?.message||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` + >`: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)}}ft.properties={hass:{attribute:!1},_config:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",ft);const vt="houseplan_card_layout_v1",bt=1e3,yt=(t,e,i)=>{const s=new Event(e,{bubbles:!0,composed:!0});s.detail=i??{},t.dispatchEvent(s)},$t=(t,e)=>{let i;return(...s)=>{clearTimeout(i),i=window.setTimeout(()=>t(...s),e)}};class xt extends ot{constructor(){super(...arguments),this._space="f1",this._edit=!1,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._infoCard=null,this._markerDialog=null,this._spaceDialog=null,this._keyHandler=t=>this._onKey(t),this._drag=null,this._dirtyPos=new Set,this._persistLayout=$t(()=>{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)))}}else localStorage.setItem(vt,JSON.stringify(this._layout))},600),this._saveConfig=$t(()=>{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._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)}getCardSize(){return 12}get _norm(){return!(!this._serverCfg||!this._serverCfg.spaces.length)}get _model(){return this._serverCfg?this._serverCfg.spaces.map(t=>{const e=bt/t.aspect;return{id:t.id,title:t.title,vb:[t.view_box[0]*bt,t.view_box[1]*e,t.view_box[2]*bt,t.view_box[3]*e],bg:t.plan_url?{href:t.plan_url,x:0,y:0,w:bt,h:e}:null,rooms:t.rooms.map(t=>({id:t.id,name:t.name,area:t.area??null,x:null!=t.x?t.x*bt:void 0,y:null!=t.y?t.y*e:void 0,w:null!=t.w?t.w*bt:void 0,h:null!=t.h?t.h*e:void 0,poly:t.poly?t.poly.map(t=>[t[0]*bt,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 _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())}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)}catch(t){if(this._loadTries>=8){this._serverStorage=!1,this._serverCfg=null;try{this._layout=JSON.parse(localStorage.getItem(vt)||"{}")||{}}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._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=this._buildDevices(),this._defPos=this._defaultPositions())}_entitiesByDevice(){const t={};for(const[e,i]of Object.entries(this.hass.entities))i?.device_id&&(t[i.device_id]=t[i.device_id]||[]).push(e);return t}_domainOfDevice(t,e){if(t.identifiers?.[0]?.[0])return t.identifiers[0][0];for(const t of e){const e=this.hass.entities[t]?.platform;if(e)return e}return""}_primaryEntity(t,e){const i=t.map(t=>({eid:t,reg:this.hass.entities[t],st:this.hass.states[t]})).filter(t=>t.reg&&!t.reg.hidden),s=i.filter(t=>!t.reg.entity_category),a=s.length?s:i;if("mdi:thermometer"===e||"mdi:air-filter"===e){const t=a.find(t=>this._isTempEntity(t.eid));if(t)return t.eid}for(const t of pt){const e=a.find(e=>e.eid.split(".")[0]===t);if(e)return e.eid}return a[0]?.eid}_isTempEntity(t){const e=this.hass.states[t];if(!e)return/_temperature$/.test(t);const i=e.attributes||{};return"temperature"===i.device_class||/°C|°F/.test(i.unit_of_measurement||"")||/_temperature$/.test(t)}_lqiFor(t){const e=[];for(const i of t){const t=this.hass.states[i];if(!(/_linkquality$/.test(i)||"lqi"===(t?.attributes?.unit_of_measurement||""))||!t)continue;const s=parseFloat(t.state);isNaN(s)||e.push(s)}return e.length?Math.round(e.reduce((t,e)=>t+e,0)/e.length):null}_roomLqi(t){if(!t)return null;const e=[];for(const i of this._devices){if(i.area!==t||i.virtual)continue;const s=this._lqiFor(i.entities);null!=s&&e.push(s)}return e.length?Math.round(e.reduce((t,e)=>t+e,0)/e.length):null}_tempFor(t){for(const e of t){if(!this._isTempEntity(e))continue;const t=this.hass.states[e];if(!t)continue;const i=parseFloat(t.state);if(!isNaN(i))return Math.round(10*i)/10}return null}_lightGroups(){if(!1===this._settings.group_lights)return[];const t=this.hass,e=[];for(const[i,s]of Object.entries(t.entities)){if(!i.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 n=t.states[i];e.push({eid:i,name:s.name||n?.attributes?.friendly_name||i,area:a})}return e}_groupedLightAreas(){return new Set(this._lightGroups().map(t=>t.area))}get _markers(){return this._serverCfg?.markers||[]}_markerFor(t,e){return this._markers.find(i=>i.binding===t+":"+e)}get _claimed(){const t=new Set;for(const e of this._markers){const[i,s]=e.binding.split(":");"device"!==i&&"entity"!==i||!s||t.add(e.binding)}return t}_applyMarker(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||[]}_buildDevices(){const t=this.hass,e=this._areaToSpace,i=!1!==this._settings.group_lights,s=this._groupedLightAreas(),a=this._excluded,n=this._entitiesByDevice(),r=this._claimed,o={},l=[];for(const c of Object.values(t.devices)){const t=c.area_id;if(!t||!e[t])continue;if("service"===c.entry_type)continue;if(r.has("device:"+c.id))continue;const h=this._markerFor("device",c.id);if(h&&h.hidden)continue;const d=n[c.id]||[],p=this._domainOfDevice(c,d);if(a.has(p))continue;if("Group"===c.model)continue;if(/scene/i.test(c.model||""))continue;if(/bridge/i.test((c.model||"")+(c.name||"")))continue;if("myheat"===p&&c.via_device_id)continue;const u=(c.name_by_user||c.name||"без имени").trim(),_=u+"|"+t;if(o[_])continue;o[_]=1;let g=dt(u,c.model);if(d.some(t=>t.startsWith("lock."))&&(g="mdi:lock"),i&&"mdi:lightbulb"===g&&s.has(t))continue;const m={id:c.id,name:u,model:c.model||"",area:t,space:e[t].space,icon:g,entities:d,bindingKind:"device",bindingRef:c.id,pdfs:[]};m.primary=this._primaryEntity(d,g),"mdi:thermometer"!==g&&"mdi:air-filter"!==g||(m.temp=this._tempFor(d)),l.push(m)}for(const t of this._lightGroups())e[t.area]&&(r.has("entity:"+t.eid)||l.push({id:"lg_"+t.eid,name:t.name,model:"группа света",area:t.area,space:e[t.area].space,icon:"mdi:lightbulb-group",entities:[t.eid],primary:t.eid,bindingKind:"entity",bindingRef:t.eid,pdfs:[]}));for(const i of this._markers){if(i.hidden)continue;const[s,a]=i.binding.split(":");if("device"===s){const s=t.devices[a],r=i.area||s?.area_id||"",o=r&&e[r]?.space||i.space||this._model[0]?.id||"",c=s&&n[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:i.id,name:s?.name_by_user||s?.name||"устройство",model:s?.model||"",area:r,space:o,icon:h,entities:c,bindingKind:"device",bindingRef:a};d.primary=this._primaryEntity(c,h),"mdi:thermometer"!==h&&"mdi:air-filter"!==h||(d.temp=this._tempFor(c)),this._applyMarker(d,i),l.push(d)}else if("entity"===s){const s=t.entities[a],n=i.area||s?.area_id||s?.device_id&&t.devices[s.device_id]?.area_id||"",r=n&&e[n]?.space||i.space||this._model[0]?.id||"",o=t.states[a],c={id:i.id,name:s?.name||o?.attributes?.friendly_name||a,model:"",area:n,space:r,icon:"mdi:shape-outline",entities:[a],primary:a,bindingKind:"entity",bindingRef:a};this._applyMarker(c,i),l.push(c)}else{const t=i.area||"",s=i.space||t&&e[t]?.space||this._model[0]?.id||"",a={id:i.id,name:i.name||"виртуальное устройство",model:i.model||"",area:t,space:s,icon:i.icon||"mdi:map-marker",entities:[],bindingKind:"virtual",virtual:!0};this._applyMarker(a,i),l.push(a)}}return l}_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={};for(const e of this._model)for(const i of e.rooms){if(!i.area)continue;const s=this._devices.filter(t=>t.area===i.area&&t.space===e.id);if(!s.length)continue;const a=this._roomBounds(i),n=.12*Math.min(a.w,a.h),r=a.w-2*n,o=a.h-2*n,l=Math.max(1,Math.round(Math.sqrt(s.length*r/Math.max(o,1)))),c=Math.ceil(s.length/l),h=r/l,d=o/Math.max(c,1);s.forEach((e,i)=>{const s=i%l,r=Math.floor(i/l);t[e.id]={x:a.x+n+h*(s+.5),y:a.y+n+d*(r+.5)}})}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*bt,y:i.y*(bt/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,n=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/bt,y:n/(bt/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:this._tempFor(t.entities):null}_openMoreInfo(t){t?yt(this,"hass-more-info",{entityId:t}):this._showToast("У устройства нет подходящей сущности")}_clickDevice(t,e){if(t.stopPropagation(),!this._drag?.moved&&!this._markup)return this._edit?(this._selId=e.id,void this._openMarkerDialog(e)):void(this._infoCard=e)}_clickRoom(t){var e;!this._edit&&t.area&&(e="/config/areas/area/"+t.area,history.pushState(null,"",e),yt(window,"location-changed",{replace:!1}))}_pointerDown(t,e){if(!this._edit)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(),n=(t.clientX-this._drag.sx)/a.width*s[2],r=(t.clientY-this._drag.sy)/a.height*s[3];Math.abs(t.clientX-this._drag.sx)+Math.abs(t.clientY-this._drag.sy)>3&&(this._drag.moved=!0);const o=.008*Math.min(s[2],s[3]),l=Math.max(s[0]+o,Math.min(s[0]+s[2]-o,this._drag.ox+n)),c=Math.max(s[1]+o,Math.min(s[1]+s[3]-o,this._drag.oy+r));this._savePos(e,l,c)}_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))}_applyXY(t,e){if(!this._selId)return;const i=parseFloat(e);if(isNaN(i))return;const s=this._devices.find(t=>t.id===this._selId);if(!s||s.virtual)return;const a={...this._pos(s)};a[t]=i,this._savePos(s,a.x,a.y)}_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 bt/120}get _curSpaceCfg(){return this._serverCfg?.spaces.find(t=>t.id===this._space)}get _spaceH(){const t=this._curSpaceCfg;return t?bt/t.aspect:bt}get _segments(){const t=this._curSpaceCfg,e=this._spaceH;return(t?.segments||[]).map(t=>[t[0]*bt,t[1]*e,t[2]*bt,t[3]*e])}_toggleMarkup(){this._norm?(this._markup=!this._markup,this._edit=!1,this._path=[],this._cursorPt=null,this._tool="draw"):this._showToast("Разметка доступна после переноса конфига на сервер (режим правки → «На сервер»)")}_svgPoint(t){const e=this.renderRoot.querySelector(".stage"),i=this._spaceModel().vb,s=e.getBoundingClientRect();return[i[0]+(t.clientX-s.left)/s.width*i[2],i[1]+(t.clientY-s.top)/s.height*i[3]]}_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!n&&(i.segments=i.segments||[],i.segments.push([t[0]/bt,t[1]/s,e[0]/bt,e[1]/s]),this._saveConfig(),!0)}_distToSeg(t,e){const[i,s]=t,[a,n,r,o]=e,l=r-a,c=o-n;let h=((i-a)*l+(s-n)*c)/(l*l+c*c||1);h=Math.max(0,Math.min(1,h));const d=a+h*l,p=n+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]<(o-n)*(t[1]-r)/(l-r)+n&&(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 n=this._distToSeg(e,t);n=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(){if(!this._contourClosed||!this._areaSel&&!this._nameSel)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]/bt,t[1]/e])}),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 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 n=(a.name_by_user||a.name||a.id).trim();t!==this._markerDialog?.binding&&i.has(n+"|"+(a.area_id||""))||s.push({value:t,label:n,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,n]of Object.entries(t.entities)){const r="entity:"+i;if(e.has(r))continue;const o=a.has(n.platform),l="group"===n.platform;if(!o&&!l)continue;if(n.hidden)continue;const c=t.states[i];s.push({value:r,label:n.name||c?.attributes?.friendly_name||i,sub:i.split(".")[0]+" · "+("group"===n.platform?"группа":"хелпер")})}const n=(this._markerDialog?.bindingFilter||"").toLowerCase().trim(),r=n?s.filter(t=>(t.label+" "+t.sub+" "+t.value).toLowerCase().includes(n)):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"неизвестная ошибка";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=this.hass?.auth?.data?.access_token,n=[];for(const t of i)try{const e=new FormData;e.append("marker_id",s),e.append("file",t,t.name);const i=await fetch("/api/houseplan/upload",{method:"POST",body:e,headers:a?{authorization:`Bearer ${a}`}:{}}),r=await i.json().catch(()=>({}));if(!i.ok||r.error){const t={too_large:"файл больше "+(r.max_mb||25)+" МБ",bad_ext:"недопустимый тип (нужен PDF/изображение)",unauthorized:"нужны права администратора"};throw new Error(t[r.error]||r.error||"HTTP "+i.status)}n.push({name:r.name||t.name,url:r.url})}catch(e){this._showToast("Файл «"+t.name+"» не загружен: "+this._errText(e))}n.length&&this._markerDialog&&(this._markerDialog={...this._markerDialog,pdfs:[...this._markerDialog.pdfs,...n]},this._showToast("Прикреплено файлов: "+n.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 n=s||null,r=a||null;"virtual"!==t.binding||n||(n=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 o=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=n,l.area=r);const c=o?this._devices.find(t=>t.id===o):null,h=!!t.room&&null!=c&&(c.space!==n||c.area!==r);if(e.markers=e.markers.filter(t=>t.id!==i&&t.id!==o),e.markers.push(l),!this._layout[i]||h){const t=this._spaceModel(n||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))}this._layout[i]=this._normPos(n||this._space,e,s)}await this._saveConfigNow(),await this.hass.callWS({type:"houseplan/layout/set",layout:this._layout}),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство сохранено")}catch(t){this._markerDialog={...this._markerDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||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(),this._markerDialog=null,this._regSignature="",this._maybeRebuildDevices(),this._showToast("Устройство убрано с плана")}catch(t){this._showToast("Ошибка: "+(t?.message||t))}}_normPos(t,e,i){const s=this._serverCfg.spaces.find(e=>e.id===t)?.aspect||1;return{s:t,x:e/bt,y:i/(bt/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 n="";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=o});URL.revokeObjectURL(o),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()){this._spaceDialog={...t,busy:!0};try{const e=this._serverCfg;let i;if("create"===t.mode?(i={id:"s"+Date.now().toString(36),title:t.title.trim(),plan_url:null,aspect:1.414,view_box:[0,0,1,1],rooms:[],segments:[]},e.spaces.push(i)):(i=e.spaces.find(e=>e.id===t.spaceId),i.title=t.title.trim()),t.planFile){const e=await this.hass.callWS({type:"houseplan/plan/set",space_id:i.id,ext:t.planFile.ext,data:t.planFile.b64});i.plan_url=e.url,i.aspect=t.planFile.aspect}await this._saveConfigNow(),this._spaceDialog=null,"create"===t.mode&&(this._space=i.id),this._regSignature="",this._maybeRebuildDevices(),this._showToast("create"===t.mode?"Пространство добавлено":"Пространство сохранено")}catch(t){this._spaceDialog={...this._spaceDialog,busy:!1},this._showToast("Ошибка: "+(t?.message||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("Ошибка удаления: "+(t?.message||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`
${this._config.title||"План дома"}
@@ -65,12 +65,12 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow ${this._markup?this._renderMarkupDefs(i):B} ${e.bg?L``: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,n=this._roomCenter(t),o=t.poly?L`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,n=this._roomCenter(t),r=t.poly?L`this._clickRoom(t)} @mousemove=${s} @mouseleave=${()=>this._tip=null}>`:L`this._clickRoom(t)} @mousemove=${s} - @mouseleave=${()=>this._tip=null}>`;return L`${o}${a?L`${t.name}`:B}`})} + @mouseleave=${()=>this._tip=null}>`;return L`${r}${a?L`${t.name}`:B}`})} ${this._markup?this._renderMarkupLayer(i):B}
@@ -89,19 +89,19 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
`:B} ${this._toast?j`
${this._toast}
`:B}
- `}_renderDevice(t,e){const i=this._pos(t),s=(i.x-e[0])/e[2]*100,a=(i.y-e[1])/e[3]*100,n=this._stateClass(t),o=this._liveTemp(t),r=this._config?.show_signal&&!t.virtual?this._lqiFor(t.entities):null;return j`
this._clickDevice(e,t)} - @mousemove=${e=>this._showTip(e,t.name,t.model+(null!=o?" · "+o+"°":"")+(null!=r?" · LQI "+r:""))} + @mousemove=${e=>this._showTip(e,t.name,t.model+(null!=r?" · "+r+"°":"")+(null!=o?" · LQI "+o:""))} @mouseleave=${()=>this._tip=null} @pointerdown=${e=>this._pointerDown(e,t)} @pointermove=${e=>this._pointerMove(e,t)} @pointerup=${e=>this._pointerUp(e,t)} > - ${null!=o?j`${o}°`:B} - ${null!=r?j`${r}`:B} + ${null!=r?j`${r}°`:B} + ${null!=o?j`${o}`: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 L` @@ -986,4 +986,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow z-index: 120; max-width: 90vw; } - `,customElements.get("houseplan-card")||customElements.define("houseplan-card",xt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Интерактивный план дома: пространства, комнаты, устройства с живыми состояниями и drag-раскладкой."}),console.info("%c HOUSEPLAN-CARD %c v1.7.3 ","background:#3ea6ff;color:#04121f;font-weight:700",""); + `,customElements.get("houseplan-card")||customElements.define("houseplan-card",xt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Интерактивный план дома: пространства, комнаты, устройства с живыми состояниями и drag-раскладкой."}),console.info("%c HOUSEPLAN-CARD %c v1.7.4 ","background:#3ea6ff;color:#04121f;font-weight:700",""); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3d2ad6f..a895eed 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -128,4 +128,7 @@ name?, icon?, model?, link?, description?, pdfs:[{name,url}]}`. Гибрид: а | `houseplan/config/get` | — | `{config, rev}` | | `houseplan/config/set` | `config`, `expected_rev?` | `{ok, rev}` / err `conflict`; событие `houseplan_config_updated` | | `houseplan/plan/set` | `space_id`, `ext` (svg/png/jpg/webp), `data` (b64, ≤8МБ) | `{ok, url}` | -| `houseplan/file/set` | `marker_id`, `filename`, `data` (b64, ≤25МБ) | `{ok, url, name}` | +| `houseplan/file/set` | `marker_id`, `filename`, `data` (b64) | `{ok,url,name}` (legacy, лимит WS) | + +**Загрузка файлов — HTTP** (не WS, у него лимит размера сообщения): `POST /api/houseplan/upload` +(multipart: marker_id + file), HomeAssistantView, requires_auth. Отдача — `/houseplan_files/files/`. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6fdaadd..d648d1d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v1.7.4 — 2026-07-04 (PDF по HTTP вместо WebSocket) +- КОРНЕВАЯ ПРИЧИНА невозможности загрузить PDF: файл слался как base64 одним WebSocket-сообщением, + а у WS есть лимит размера — реальный мануал (>2–3 МБ) превышал его, соединение рвалось + («Connection lost»), из-за чего и падала загрузка, и закрывался диалог (обрыв WS → переподключение + → ре-рендер). Подтверждено тестом: 1 МБ по WS ок, 3 МБ → Connection lost. +- Файлы-инструкции теперь грузятся через HTTP-эндпоинт `POST /api/houseplan/upload` (multipart, + HomeAssistantView, requires_auth, admin_only-проверка) — как медиа в самом HA. Проверено: + 3 МБ (182мс) и 10 МБ (446мс) загружаются на ура. +- Читаемые ошибки везде (`_errText`): больше никаких «[object Object]»; понятные тексты для + too_large / bad_ext / unauthorized. +- WS-команда houseplan/file/set оставлена для совместимости, но фронт использует HTTP. + + ## v1.7.3 — 2026-07-04 (фиксы диалога устройства) - PDF-инструкции не загружались: ручной base64 (`btoa(String.fromCharCode(...subarray))`) падал на реальных файлах (RangeError при спреде больших массивов), а при закрытии диалога во время diff --git a/package.json b/package.json index c781917..d457e7a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "houseplan-card", - "version": "1.7.3", + "version": "1.7.4", "description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)", "license": "MIT", "type": "module", diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index 6e2a306..6ebcb8f 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -13,7 +13,7 @@ import { } from './logic'; import './editor'; -const CARD_VERSION = '1.7.3'; +const CARD_VERSION = '1.7.4'; const LS_KEY = 'houseplan_card_layout_v1'; const NORM_W = 1000; // ширина рендер-пространства для нормированных конфигов @@ -1202,42 +1202,60 @@ class HouseplanCard extends LitElement { return res; } - /** base64 файла через FileReader — надёжно для любого размера (без спреда больших массивов). */ - private _fileToBase64(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - const res = String(reader.result || ''); - const comma = res.indexOf(','); - resolve(comma >= 0 ? res.slice(comma + 1) : res); - }; - reader.onerror = () => reject(reader.error || new Error('read error')); - reader.readAsDataURL(file); - }); + /** Читаемый текст ошибки (никогда не «[object Object]»). */ + private _errText(e: any): string { + if (!e) return 'неизвестная ошибка'; + if (typeof e === 'string') return e; + if (e.message) return e.message; + if (e.error) return e.error; + if (e.code != null) return 'код ' + e.code; + try { + return JSON.stringify(e); + } catch { + return String(e); + } } + /** + * Загрузка файлов-инструкций через HTTP (multipart) — не через WebSocket, у которого лимит + * размера сообщения рвёт соединение на больших PDF. + */ private async _pickMarkerFiles(ev: Event): Promise { const input = ev.target as HTMLInputElement; const files = input.files ? [...input.files] : []; input.value = ''; if (!files.length || !this._markerDialog) return; const mid = this._markerDialog.devId || 'new'; + const token = this.hass?.auth?.data?.access_token; const uploaded: PdfRef[] = []; for (const file of files) { try { - const data = await this._fileToBase64(file); - const resp = await this.hass.callWS({ - type: 'houseplan/file/set', marker_id: mid, filename: file.name, data, + const fd = new FormData(); + fd.append('marker_id', mid); + fd.append('file', file, file.name); + const resp = await fetch('/api/houseplan/upload', { + method: 'POST', + body: fd, + headers: token ? { authorization: `Bearer ${token}` } : {}, }); - uploaded.push({ name: resp.name || file.name, url: resp.url }); + const json = await resp.json().catch(() => ({})); + if (!resp.ok || json.error) { + const map: Record = { + too_large: 'файл больше ' + (json.max_mb || 25) + ' МБ', + bad_ext: 'недопустимый тип (нужен PDF/изображение)', + unauthorized: 'нужны права администратора', + }; + throw new Error(map[json.error] || json.error || 'HTTP ' + resp.status); + } + uploaded.push({ name: json.name || file.name, url: json.url }); } catch (e: any) { - this._showToast('Файл «' + file.name + '» не загружен: ' + (e?.code || e?.message || e)); + this._showToast('Файл «' + file.name + '» не загружен: ' + this._errText(e)); } } - // диалог мог быть переоткрыт/закрыт за время загрузки — добавляем, только если он ещё открыт + // диалог мог закрыться за время загрузки — добавляем, только если он ещё открыт if (uploaded.length && this._markerDialog) { this._markerDialog = { ...this._markerDialog, pdfs: [...this._markerDialog.pdfs, ...uploaded] }; - if (uploaded.length) this._showToast('Прикреплено файлов: ' + uploaded.length); + this._showToast('Прикреплено файлов: ' + uploaded.length); } }