mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
feat: assign each robot map to a floor in the device editor
User-Visible: yes Issue: #162
This commit is contained in:
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "0aa082f67d7e4e87ee87e11227a79f9f754d47a470f79cea2084b6001cf18246",
|
||||
"fingerprint": "d2e52a98c720731b44a0fe51783aa8191a5218f555f1f86893701f28e99f200f",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js",
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 291203,
|
||||
"initialViewGzipBytes": 292278,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/de-BkROu8Tw.js",
|
||||
"houseplan-assets/editor-GNX-fEXr.js",
|
||||
"houseplan-assets/fr-C03DhXy5.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CvmR2VuV.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BVA5mIBD.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/de-DYuCBO-k.js",
|
||||
"houseplan-assets/editor-CVgkVajn.js",
|
||||
"houseplan-assets/fr-C2GpV0Jw.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Bpo_GpxS.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DCUDjODM.js"
|
||||
],
|
||||
"lazyGzipBytes": 215550,
|
||||
"lazyGzipBytes": 219265,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/editor-GNX-fEXr.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CvmR2VuV.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/editor-CVgkVajn.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Bpo_GpxS.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 159870,
|
||||
"lazyEditorGzipBytes": 163598,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BVA5mIBD.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DCUDjODM.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14127,
|
||||
"lazyOnboardingGzipBytes": 14135,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-BkROu8Tw.js",
|
||||
"houseplan-assets/fr-C03DhXy5.js"
|
||||
"houseplan-assets/de-DYuCBO-k.js",
|
||||
"houseplan-assets/fr-C2GpV0Jw.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 48727,
|
||||
"lazyLocaleGzipBytes": 48711,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"sha256": "01ab21891c6d5f844630b8bebe539f50b3f8d25725baa32017b2638d1341f92f",
|
||||
"path": "houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"sha256": "8e6708537de7c137d4329702fe275413c56b07f14ac1376febfc19065c9fbdd0",
|
||||
"rawBytes": 20996,
|
||||
"gzipBytes": 7174,
|
||||
"gzipBytes": 7179,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-BkROu8Tw.js",
|
||||
"sha256": "c48c3b1e09eae9066e9feac83e80109e089474c344415f27e87b0b737ef93fc3",
|
||||
"rawBytes": 81632,
|
||||
"gzipBytes": 24635,
|
||||
"path": "houseplan-assets/de-DYuCBO-k.js",
|
||||
"sha256": "d2a1c7f04fdb2e127905f083a6e8581b6ce975a301fe776c6a9f65763d86a8c4",
|
||||
"rawBytes": 81588,
|
||||
"gzipBytes": 24627,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-GNX-fEXr.js",
|
||||
"sha256": "05c249b5b3ead0987635b53de549ec1af7339eb863abf70bcc45d51dd3f2521b",
|
||||
"path": "houseplan-assets/editor-CVgkVajn.js",
|
||||
"sha256": "cde4b2f73563f74dc3c66a210b01bd83dd9c8fda30914a02cd845db2feaf029e",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1580,
|
||||
"gzipBytes": 1583,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-C03DhXy5.js",
|
||||
"sha256": "0843d25037a8ab2277a96738ca3d6e97be303b52dbffa1dc6161c4576a9c1edb",
|
||||
"rawBytes": 83754,
|
||||
"gzipBytes": 24092,
|
||||
"path": "houseplan-assets/fr-C2GpV0Jw.js",
|
||||
"sha256": "f111011a8f45476386a3d0743402d3fad8b5f4d6d3b61be30036aa983f58e3f1",
|
||||
"rawBytes": 83710,
|
||||
"gzipBytes": 24084,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-CRBn43Vb.js",
|
||||
"sha256": "9914dd884036dd91d1c2cc663e34bad77cfad3d2a3db34f84baf2e007b1b73a1",
|
||||
"rawBytes": 1033763,
|
||||
"gzipBytes": 290407,
|
||||
"path": "houseplan-assets/houseplan-card-C1YZDmj6.js",
|
||||
"sha256": "c6de4fca90b84eb8ab47f5e6e5b23d40f9929ca58ac58809cd1be4398e4a1f05",
|
||||
"rawBytes": 1036950,
|
||||
"gzipBytes": 291480,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-BkROu8Tw.js",
|
||||
"houseplan-assets/editor-GNX-fEXr.js",
|
||||
"houseplan-assets/fr-C03DhXy5.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CvmR2VuV.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BVA5mIBD.js"
|
||||
"houseplan-assets/de-DYuCBO-k.js",
|
||||
"houseplan-assets/editor-CVgkVajn.js",
|
||||
"houseplan-assets/fr-C2GpV0Jw.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Bpo_GpxS.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DCUDjODM.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CvmR2VuV.js",
|
||||
"sha256": "4ab71bdc852a95fb7488cbedcc24f69c0203a6aac39f961f3a752561f352e452",
|
||||
"rawBytes": 576162,
|
||||
"gzipBytes": 151116,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Bpo_GpxS.js",
|
||||
"sha256": "0710c7a4a73f7a2532d041984b1294727abc5a1a76a17ab395c9f347ee1a05ab",
|
||||
"rawBytes": 589868,
|
||||
"gzipBytes": 154836,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BVA5mIBD.js",
|
||||
"sha256": "36ebdfc8bb84657533fba40b9e6df64828d069a0f7be45755c6e859f0c714736",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DCUDjODM.js",
|
||||
"sha256": "37bbf57bbaede7f9ff15617e537978faf5ab76de510a758cb286e016c6717676",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6953,
|
||||
"gzipBytes": 6956,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "dcebb936b7f104ef69cc667539a7a6c5120c55ef01fd17821aac04b2fa9987a8",
|
||||
"sha256": "4bdef27d1c8e8265021aca1648b32ba32265ed970b6743f8e9c89942dec5ed32",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 796,
|
||||
"gzipBytes": 798,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
+7
-7
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0aa082f67d7e4e87ee87e11227a79f9f754d47a470f79cea2084b6001cf18246";import{b as e,l as o,t,di as s,A as a,dj as i,dk as l,dl as n,E as r,c}from"./houseplan-card-CRBn43Vb.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d2e52a98c720731b44a0fe51783aa8191a5218f555f1f86893701f28e99f200f";import{g as e,l as o,t,dm as s,A as a,dn as i,dp as l,dq as n,E as r,c}from"./houseplan-card-C1YZDmj6.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${d(h.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
${d(h.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+73
-73
File diff suppressed because one or more lines are too long
+664
-623
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0aa082f67d7e4e87ee87e11227a79f9f754d47a470f79cea2084b6001cf18246";try{await import("./houseplan-assets/houseplan-card-CRBn43Vb.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d2e52a98c720731b44a0fe51783aa8191a5218f555f1f86893701f28e99f200f";try{await import("./houseplan-assets/houseplan-card-C1YZDmj6.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
Vendored
+59
-59
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "0aa082f67d7e4e87ee87e11227a79f9f754d47a470f79cea2084b6001cf18246",
|
||||
"fingerprint": "d2e52a98c720731b44a0fe51783aa8191a5218f555f1f86893701f28e99f200f",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js",
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 291203,
|
||||
"initialViewGzipBytes": 292278,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/de-BkROu8Tw.js",
|
||||
"houseplan-assets/editor-GNX-fEXr.js",
|
||||
"houseplan-assets/fr-C03DhXy5.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CvmR2VuV.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BVA5mIBD.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/de-DYuCBO-k.js",
|
||||
"houseplan-assets/editor-CVgkVajn.js",
|
||||
"houseplan-assets/fr-C2GpV0Jw.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Bpo_GpxS.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DCUDjODM.js"
|
||||
],
|
||||
"lazyGzipBytes": 215550,
|
||||
"lazyGzipBytes": 219265,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/editor-GNX-fEXr.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CvmR2VuV.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/editor-CVgkVajn.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Bpo_GpxS.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 159870,
|
||||
"lazyEditorGzipBytes": 163598,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BVA5mIBD.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DCUDjODM.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14127,
|
||||
"lazyOnboardingGzipBytes": 14135,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-BkROu8Tw.js",
|
||||
"houseplan-assets/fr-C03DhXy5.js"
|
||||
"houseplan-assets/de-DYuCBO-k.js",
|
||||
"houseplan-assets/fr-C2GpV0Jw.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 48727,
|
||||
"lazyLocaleGzipBytes": 48711,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"sha256": "01ab21891c6d5f844630b8bebe539f50b3f8d25725baa32017b2638d1341f92f",
|
||||
"path": "houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"sha256": "8e6708537de7c137d4329702fe275413c56b07f14ac1376febfc19065c9fbdd0",
|
||||
"rawBytes": 20996,
|
||||
"gzipBytes": 7174,
|
||||
"gzipBytes": 7179,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-BkROu8Tw.js",
|
||||
"sha256": "c48c3b1e09eae9066e9feac83e80109e089474c344415f27e87b0b737ef93fc3",
|
||||
"rawBytes": 81632,
|
||||
"gzipBytes": 24635,
|
||||
"path": "houseplan-assets/de-DYuCBO-k.js",
|
||||
"sha256": "d2a1c7f04fdb2e127905f083a6e8581b6ce975a301fe776c6a9f65763d86a8c4",
|
||||
"rawBytes": 81588,
|
||||
"gzipBytes": 24627,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-GNX-fEXr.js",
|
||||
"sha256": "05c249b5b3ead0987635b53de549ec1af7339eb863abf70bcc45d51dd3f2521b",
|
||||
"path": "houseplan-assets/editor-CVgkVajn.js",
|
||||
"sha256": "cde4b2f73563f74dc3c66a210b01bd83dd9c8fda30914a02cd845db2feaf029e",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1580,
|
||||
"gzipBytes": 1583,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-C03DhXy5.js",
|
||||
"sha256": "0843d25037a8ab2277a96738ca3d6e97be303b52dbffa1dc6161c4576a9c1edb",
|
||||
"rawBytes": 83754,
|
||||
"gzipBytes": 24092,
|
||||
"path": "houseplan-assets/fr-C2GpV0Jw.js",
|
||||
"sha256": "f111011a8f45476386a3d0743402d3fad8b5f4d6d3b61be30036aa983f58e3f1",
|
||||
"rawBytes": 83710,
|
||||
"gzipBytes": 24084,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-CRBn43Vb.js",
|
||||
"sha256": "9914dd884036dd91d1c2cc663e34bad77cfad3d2a3db34f84baf2e007b1b73a1",
|
||||
"rawBytes": 1033763,
|
||||
"gzipBytes": 290407,
|
||||
"path": "houseplan-assets/houseplan-card-C1YZDmj6.js",
|
||||
"sha256": "c6de4fca90b84eb8ab47f5e6e5b23d40f9929ca58ac58809cd1be4398e4a1f05",
|
||||
"rawBytes": 1036950,
|
||||
"gzipBytes": 291480,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-BkROu8Tw.js",
|
||||
"houseplan-assets/editor-GNX-fEXr.js",
|
||||
"houseplan-assets/fr-C03DhXy5.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CvmR2VuV.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-BVA5mIBD.js"
|
||||
"houseplan-assets/de-DYuCBO-k.js",
|
||||
"houseplan-assets/editor-CVgkVajn.js",
|
||||
"houseplan-assets/fr-C2GpV0Jw.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Bpo_GpxS.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DCUDjODM.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CvmR2VuV.js",
|
||||
"sha256": "4ab71bdc852a95fb7488cbedcc24f69c0203a6aac39f961f3a752561f352e452",
|
||||
"rawBytes": 576162,
|
||||
"gzipBytes": 151116,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Bpo_GpxS.js",
|
||||
"sha256": "0710c7a4a73f7a2532d041984b1294727abc5a1a76a17ab395c9f347ee1a05ab",
|
||||
"rawBytes": 589868,
|
||||
"gzipBytes": 154836,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-BVA5mIBD.js",
|
||||
"sha256": "36ebdfc8bb84657533fba40b9e6df64828d069a0f7be45755c6e859f0c714736",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DCUDjODM.js",
|
||||
"sha256": "37bbf57bbaede7f9ff15617e537978faf5ab76de510a758cb286e016c6717676",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6953,
|
||||
"gzipBytes": 6956,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-D3Vafz-C.js",
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/backdrop-pick-CfOh3CA7.js",
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "dcebb936b7f104ef69cc667539a7a6c5120c55ef01fd17821aac04b2fa9987a8",
|
||||
"sha256": "4bdef27d1c8e8265021aca1648b32ba32265ed970b6743f8e9c89942dec5ed32",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 796,
|
||||
"gzipBytes": 798,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-CRBn43Vb.js"
|
||||
"houseplan-assets/houseplan-card-C1YZDmj6.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
Vendored
+7
-7
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -1,14 +1,14 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0aa082f67d7e4e87ee87e11227a79f9f754d47a470f79cea2084b6001cf18246";import{b as e,l as o,t,di as s,A as a,dj as i,dk as l,dl as n,E as r,c}from"./houseplan-card-CRBn43Vb.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d2e52a98c720731b44a0fe51783aa8191a5218f555f1f86893701f28e99f200f";import{g as e,l as o,t,dm as s,A as a,dn as i,dp as l,dq as n,E as r,c}from"./houseplan-card-C1YZDmj6.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(f.slice(0,3))}
|
||||
${d(h.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${d(f.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
|
||||
${d(h.slice(3))}
|
||||
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+73
-73
File diff suppressed because one or more lines are too long
+664
-623
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="0aa082f67d7e4e87ee87e11227a79f9f754d47a470f79cea2084b6001cf18246";try{await import("./houseplan-assets/houseplan-card-CRBn43Vb.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="d2e52a98c720731b44a0fe51783aa8191a5218f555f1f86893701f28e99f200f";try{await import("./houseplan-assets/houseplan-card-C1YZDmj6.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- The device editor now has a “Maps and floors” block: each map a robot vacuum
|
||||
reports can be assigned to a space, calibrated against that space and moved or
|
||||
removed later, with the dock left where it is
|
||||
([#162](https://github.com/Matysh/houseplan-card/issues/162)).
|
||||
- A robot vacuum with several maps can now show each map on its own floor: the
|
||||
dock stays in its own space while the live position and the trail appear in
|
||||
the space the active map is assigned to, and a map that is unassigned,
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- В редакторе устройства появился блок «Карты и этажи»: каждую карту робота
|
||||
можно сопоставить пространству, откалибровать её именно в этом пространстве, а
|
||||
позже перенести или удалить — док при этом остаётся на своём месте
|
||||
([#162](https://github.com/Matysh/houseplan-card/issues/162)).
|
||||
- Робот-пылесос с несколькими картами теперь показывается на своём этаже: док
|
||||
остаётся в своём пространстве, а живое положение и след рисуются в том
|
||||
пространстве, которому назначена активная карта; несопоставленная,
|
||||
|
||||
@@ -79,8 +79,20 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
|
||||
* 291 069 при потолке 292 000 — это 931 Б до отказа сверху и 1 069 Б снизу.
|
||||
* Иначе одна из сторон срабатывает на десятках байт, то есть на шуме: правило
|
||||
* должно требовать решения от РЕАЛЬНОГО изменения, а не от переименования.
|
||||
*
|
||||
* 2026-09-03, #162: потолок поднят 292 000 → 293 000. Причина — маршрутизация
|
||||
* карт робота по пространствам: `src/vacuum-routes.ts` обязан быть в
|
||||
* initial-графе, потому что решение «какая карта на каком этаже» принимает
|
||||
* View, а не редактор. Замеренная цена 1 075 Б gzip (291 203 → 292 278).
|
||||
* Редакторская половина при этом в initial-граф НЕ попала: правки маршрутов
|
||||
* вынесены в `src/vacuum-route-edit.ts`, редакторский UI — в
|
||||
* `src/editors/vacuum-maps-section.ts`, а тексты блока — в ленивый словарь
|
||||
* `src/i18n/support/*.json` (приём #423). Эти три выноса дали −1 326 Б от
|
||||
* первой редакции правки (293 604 → 292 278); без них рост был бы вдвое
|
||||
* больше. Факт 292 278 лежит в 722 Б от нового потолка и в 1 278 Б от нижней
|
||||
* границы полосы — обе стороны дальше шума.
|
||||
*/
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 292_000;
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 293_000;
|
||||
export const INITIAL_VIEW_CEILING_BAND = 2_000;
|
||||
|
||||
/**
|
||||
|
||||
@@ -3521,7 +3521,7 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'of rotating into previous; focused pure and recorder sequences must reject the old unconditional split',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/trails.py',
|
||||
find: ' resumed = bool(cur and can_resume_trail_run(cur, map_id, now))',
|
||||
find: ' resumed = bool(cur and can_resume_trail_run(cur, map_id, now, route_id))',
|
||||
replace: ' resumed = False',
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Device editor: "Maps and floors" for a multi-floor robot (#162).
|
||||
*
|
||||
* Lives outside the editor runtime on purpose. The runtime is at its size
|
||||
* ceiling (test/core-file-budget.test.mjs), and this block is only ever needed
|
||||
* with an opened device dialog, so it belongs in the lazy editor graph rather
|
||||
* than in a core file.
|
||||
*/
|
||||
import { TemplateResult, html, nothing } from 'lit';
|
||||
import { langOf } from '../i18n';
|
||||
import { supportT, type SupportI18nKey } from '../i18n/support';
|
||||
import { VacuumMapRoute, effectiveRoutes, observedMapIds, resolveRoute } from '../vacuum-routes';
|
||||
import {
|
||||
addRoute, changeRouteSpace, convertLegacyRoutes, newRouteId, removeRoute,
|
||||
} from '../vacuum-route-edit';
|
||||
|
||||
export interface VacuumMapsHost {
|
||||
host: any;
|
||||
_saveConfig: () => void;
|
||||
_vacAutoCalibrate: (dev: any) => void;
|
||||
_vacStartFit: (dev: any, routeId?: string) => void;
|
||||
}
|
||||
|
||||
const spaceName = (host: any, spaceId: string): string => {
|
||||
const space = (host._serverCfg?.spaces || []).find((item: any) => item?.id === spaceId);
|
||||
return String(space?.name || space?.id || spaceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the block, and own every routing edit it offers.
|
||||
*
|
||||
* `setVac` is the same patch writer the rest of the vacuum section uses, so a
|
||||
* routing change goes through one save path with everything else.
|
||||
*/
|
||||
export function renderVacuumMapsSection(
|
||||
runtime: VacuumMapsHost, dev: any, setVac: (patch: Record<string, unknown>) => void,
|
||||
): TemplateResult | typeof nothing {
|
||||
const host = runtime.host;
|
||||
const lang = langOf(host.hass, host._config?.language);
|
||||
const t = (key: string, vars?: Record<string, string>) => supportT(
|
||||
lang, key as SupportI18nKey, vars);
|
||||
const vacuum = dev.marker?.vacuum || {};
|
||||
const explicit = Array.isArray(vacuum.map_routes) && vacuum.map_routes.length > 0;
|
||||
const rootSource: string = host._vacSource(dev) || '';
|
||||
const routes: VacuumMapRoute[] = effectiveRoutes(dev.id, vacuum, dev.space, rootSource);
|
||||
const spaces: Array<{ id: string; name: string }> = (host._serverCfg?.spaces || [])
|
||||
.filter((space: any) => typeof space?.id === 'string' && space.id)
|
||||
.map((space: any) => ({ id: space.id, name: String(space.name || space.id) }));
|
||||
const spaceIds = new Set(spaces.map((space) => space.id));
|
||||
const observed = observedMapIds(routes, [rootSource],
|
||||
(source) => host._vacObservedMapId(dev, source));
|
||||
const resolution = resolveRoute({ routes, observed, spaceIds });
|
||||
const activeId = (resolution.kind === 'ready' || resolution.kind === 'needs_calibration'
|
||||
|| resolution.kind === 'missing_space') ? resolution.route.id : '';
|
||||
const currentMapId = rootSource ? observed[rootSource] : undefined;
|
||||
|
||||
/** Every routing edit converts legacy data first — all of it, or none. */
|
||||
const writeRoutes = (next: (current: VacuumMapRoute[]) => VacuumMapRoute[]): boolean => {
|
||||
let base: VacuumMapRoute[] | null = explicit ? vacuum.map_routes : null;
|
||||
if (!base) {
|
||||
base = convertLegacyRoutes(vacuum, dev.space, rootSource,
|
||||
(taken) => newRouteId(taken)) ?? [];
|
||||
if (!rootSource && Object.keys(vacuum.calibration || {}).length) return false;
|
||||
}
|
||||
setVac({ map_routes: next(base), calibration: undefined });
|
||||
return true;
|
||||
};
|
||||
const takenIds = () => new Set(routes.map((route) => route.id));
|
||||
|
||||
const addCurrent = () => {
|
||||
if (!rootSource || currentMapId === undefined) return;
|
||||
writeRoutes((current) => addRoute(
|
||||
current,
|
||||
{
|
||||
source: rootSource,
|
||||
map_id: currentMapId,
|
||||
space: current.length ? '' : dev.space,
|
||||
},
|
||||
newRouteId(takenIds()),
|
||||
));
|
||||
};
|
||||
|
||||
const retarget = async (route: VacuumMapRoute, space: string) => {
|
||||
if (!space || space === route.space) return;
|
||||
const accepted = await host._confirmDanger({
|
||||
key: 'vacuum_route_space',
|
||||
kind: 'warning',
|
||||
title: t('vac.route_space_title'),
|
||||
message: t('vac.route_space_body'),
|
||||
objectName: route.map_id || t('vac.route_map_default'),
|
||||
confirmLabel: t('vac.route_space_confirm'),
|
||||
cancelLabel: host._t('btn.cancel'),
|
||||
});
|
||||
if (!accepted) return;
|
||||
writeRoutes((current) => changeRouteSpace(current, route.id, space, newRouteId(takenIds())));
|
||||
};
|
||||
|
||||
const drop = async (route: VacuumMapRoute) => {
|
||||
const accepted = await host._confirmDanger({
|
||||
key: 'vacuum_route_delete',
|
||||
kind: 'danger',
|
||||
title: t('vac.route_delete_title'),
|
||||
message: t('vac.route_delete_body'),
|
||||
objectName: route.map_id || t('vac.route_map_default'),
|
||||
confirmLabel: t('vac.route_delete_confirm'),
|
||||
cancelLabel: host._t('btn.cancel'),
|
||||
});
|
||||
if (!accepted) return;
|
||||
writeRoutes((current) => removeRoute(current, route.id));
|
||||
};
|
||||
|
||||
// Space order, then map id, then source: a stable reading order that never
|
||||
// depends on the order the routes happen to be stored in.
|
||||
const order = new Map(spaces.map((space, index) => [space.id, index]));
|
||||
const rows = [...routes].sort((a, b) => (order.get(a.space) ?? 1e6) - (order.get(b.space) ?? 1e6)
|
||||
|| a.map_id.localeCompare(b.map_id) || a.source.localeCompare(b.source));
|
||||
|
||||
const statusOf = (route: VacuumMapRoute): string => {
|
||||
if (!spaceIds.has(route.space)) return t('vac.route_status_missing_space');
|
||||
if (!route.calibration) return t('vac.route_status_needs_calibration');
|
||||
if (resolution.kind === 'ambiguous' && resolution.routeIds.includes(route.id)) {
|
||||
return t('vac.route_status_ambiguous');
|
||||
}
|
||||
return t('vac.route_status_ready');
|
||||
};
|
||||
|
||||
const canAddCurrent = !!rootSource && currentMapId !== undefined
|
||||
&& !routes.some((route) => route.source === rootSource && route.map_id === currentMapId);
|
||||
|
||||
return html`
|
||||
<label>${t('vac.routes_section')}</label>
|
||||
<div class="bindbox vacroutes">
|
||||
<div class="vacdiag" role="status">
|
||||
<div><span>${t('vac.routes_current_map')}</span><b>${currentMapId === undefined
|
||||
? t('vac.routes_no_map') : (currentMapId || t('vac.route_map_default'))}</b></div>
|
||||
<div><span>${t('vac.routes_status')}</span><b>${t(`vac.routes_state_${resolution.kind}`)}</b></div>
|
||||
</div>
|
||||
${rows.length ? html`<ul class="vacroute-list">
|
||||
${rows.map((route) => html`
|
||||
<li class="vacroute ${route.id === activeId ? 'on' : ''}">
|
||||
<div class="vacroute-head">
|
||||
<b>${route.map_id || t('vac.route_map_default')}</b>
|
||||
${route.id === activeId
|
||||
? html`<span class="vacroute-active">${t('vac.route_active')}</span>` : nothing}
|
||||
</div>
|
||||
<small>${route.source}</small>
|
||||
<div class="vacroute-status">${statusOf(route)}</div>
|
||||
<label class="srcrow">
|
||||
<span>${t('vac.route_space')}</span>
|
||||
<select class="areasel" @change=${(event: Event) => {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
const next = select.value;
|
||||
select.value = route.space;
|
||||
void retarget(route, next);
|
||||
}}>
|
||||
<option value="" ?selected=${!route.space}>${t('vac.route_space_none')}</option>
|
||||
${spaces.map((space) => html`
|
||||
<option value=${space.id} ?selected=${space.id === route.space}>${space.name}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
<div class="vacbtns">
|
||||
<button type="button" class="btn ghostbtn"
|
||||
@click=${() => runtime._vacStartFit(dev, route.id)}>${host._t('vac.fit')}</button>
|
||||
<button type="button" class="btn ghostbtn danger"
|
||||
@click=${() => void drop(route)}>${t('vac.route_delete')}</button>
|
||||
</div>
|
||||
</li>`)}
|
||||
</ul>` : html`<div class="rhint">${t('vac.routes_empty')}</div>`}
|
||||
<div class="vacbtns">
|
||||
<button type="button" class="btn" ?disabled=${!canAddCurrent}
|
||||
@click=${addCurrent}>${t('vac.route_add_current')}</button>
|
||||
</div>
|
||||
${rows.some((route) => !spaceIds.has(route.space)) ? html`
|
||||
<div class="warn">${t('vac.route_missing_space_hint')}</div>` : nothing}
|
||||
${routes.some((route) => route.map_id === 'default') ? html`
|
||||
<div class="rhint">${t('vac.route_default_hint')}</div>` : nothing}
|
||||
${rows.length ? html`<div class="rhint">${t('vac.route_target_hint', {
|
||||
space: spaceName(host, dev.space),
|
||||
})}</div>` : nothing}
|
||||
</div>`;
|
||||
}
|
||||
@@ -2503,7 +2503,7 @@ export class HouseplanCard extends LitElement {
|
||||
};
|
||||
private _resumeSettling = false;
|
||||
private _viewportInvalidAt = 0;
|
||||
private _vacFit: { markerId: string; source: string; mapId: string; p: FitParams;
|
||||
private _vacFit: { markerId: string; source: string; mapId: string; routeId?: string; p: FitParams;
|
||||
drag: null | { kind: 'move' | 'scale'; sx: number; sy: number; p0: FitParams;
|
||||
fx: number; fy: number } } | null = null;
|
||||
/** Marker whose lazy «All cameras» candidate section is expanded. */
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*/
|
||||
import { LitElement, html, svg, nothing, TemplateResult, PropertyValues } from 'lit';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { renderVacuumMapsSection } from './editors/vacuum-maps-section';
|
||||
import { planVacuumFit, writeVacuumMatrix } from './vacuum-route-edit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import './hp-dialog';
|
||||
import type { HpDialog } from './hp-dialog';
|
||||
@@ -1149,7 +1151,7 @@ export interface HouseplanEditorHostPort {
|
||||
_vacEnsureMarker: (d: DevItem) => Marker | null;
|
||||
_vacEntity: (d: DevItem) => string | null;
|
||||
_vacMapId: (d: DevItem, tele: { mapId: string }, planHass?: any) => string;
|
||||
_vacFit: { markerId: string; source: string; mapId: string; p: FitParams; drag: null | { kind: "move" | "scale"; sx: number; sy: number; p0: FitParams; fx: number; fy: number; }; } | null;
|
||||
_vacFit: { markerId: string; source: string; mapId: string; routeId?: string; p: FitParams; drag: null | { kind: "move" | "scale"; sx: number; sy: number; p0: FitParams; fx: number; fy: number; }; } | null;
|
||||
_vacOpenAllCameras: (d: DevItem) => void;
|
||||
_vacRt: Map<string, { trail: VacPt[]; lastKey: string; lastTs: number; moving: boolean; jump: boolean; endedTs: number; lastPos: VacPt | null; }>;
|
||||
_vacSource: (d: DevItem, planHass?: any) => string | null;
|
||||
@@ -10850,7 +10852,6 @@ public _renderVacSection(dlg: any): TemplateResult | typeof nothing {
|
||||
const roomMatches = tele
|
||||
? vacRoomNameMatchCount(tele.rooms, planRooms.map((room) => room.name)) : 0;
|
||||
const tierA = !!(canUseSource && tele && roomMatches >= 3);
|
||||
const cals = Object.keys(v.calibration || {});
|
||||
const setVac = (patch: Record<string, unknown>) => {
|
||||
// HP-1540-01: materialise the marker first — find() alone silently
|
||||
// dropped every edit for a vacuum that had never been saved
|
||||
@@ -10967,7 +10968,7 @@ public _renderVacSection(dlg: any): TemplateResult | typeof nothing {
|
||||
${(['never', 'cleaning', 'always'] as const).map((mv) => html`
|
||||
<option value=${mv} ?selected=${vacTrailMode(v) === mv}>${this.host._t(('vac.trail_' + mv) as any)}</option>`)}
|
||||
</select>
|
||||
${cals.length ? html`<div class="rhint">${subst(this.host._t('vac.cal_maps'), { maps: cals.join(', ') })}</div>` : nothing}
|
||||
${renderVacuumMapsSection(this, dev, setVac)}
|
||||
` : nothing}
|
||||
</div>`;
|
||||
}
|
||||
@@ -10978,16 +10979,17 @@ public _vacMapId(d: DevItem, tele: { mapId: string }, planHass = this.host._plan
|
||||
return this.host._vacMapId(d, tele, planHass);
|
||||
}
|
||||
|
||||
public _vacSaveMatrix(markerId: string, source: string, mapId: string, matrix: Affine): boolean {
|
||||
public _vacSaveMatrix(
|
||||
markerId: string, source: string, mapId: string, matrix: Affine, routeId = '',
|
||||
): boolean {
|
||||
// HP-1540-01: a first-use vacuum has no marker yet — materialise it
|
||||
const dev = this.host._devices.find((x) => x.id === markerId);
|
||||
const m = dev ? this.host._vacEnsureMarker(dev)
|
||||
: this.host._serverCfg?.markers?.find((x: Marker) => x.id === markerId);
|
||||
if (!m) return false;
|
||||
const v = { ...(m.vacuum || {}) };
|
||||
v.source = source;
|
||||
v.calibration = { ...(v.calibration || {}), [mapId]: matrix.map((n) => Number(n.toFixed(6))) };
|
||||
m.vacuum = v;
|
||||
// #162: with explicit routes the matrix belongs to the route, not to a
|
||||
// marker-wide dictionary that cannot tell two floors apart.
|
||||
m.vacuum = writeVacuumMatrix(m.vacuum || {}, { source, mapId, routeId, matrix }) as any;
|
||||
this.host._regSignature = '';
|
||||
this.host._maybeRebuildDevices();
|
||||
this._saveConfig();
|
||||
@@ -11061,7 +11063,7 @@ public _vacApplyCalibrationProposal(manual: boolean): void {
|
||||
)) this.host._showToast(subst(this.host._t('vac.autocal_done'), { rooms: String(proposal.rooms) }));
|
||||
}
|
||||
|
||||
public _vacStartFit(d: DevItem): void {
|
||||
public _vacStartFit(d: DevItem, routeId = ''): void {
|
||||
const src = this.host._vacSource(d);
|
||||
const tele = src ? readVacTelemetry(this.host.hass?.states[src]?.attributes) : null;
|
||||
if (!src || !tele) {
|
||||
@@ -11069,22 +11071,21 @@ public _vacStartFit(d: DevItem): void {
|
||||
return;
|
||||
}
|
||||
const mapId = this._vacMapId(d, tele);
|
||||
const existing = d.marker?.vacuum?.calibration?.[mapId] as Affine | undefined;
|
||||
const sp = this.host._spaceModelById(d.space);
|
||||
if (!sp) return;
|
||||
const vb = sp.vb as [number, number, number, number];
|
||||
const p = (existing && existing.length === 6 && fitFromMatrix(existing))
|
||||
|| initialFit(tele.rooms, vb);
|
||||
const plan = planVacuumFit(d.id, d.marker?.vacuum ?? null, {
|
||||
routeId, source: src, mapId, dockSpace: d.space, rooms: tele.rooms,
|
||||
viewBoxOf: (id) => (this.host._spaceModelById(id)?.vb as [number, number, number, number]) ?? null,
|
||||
});
|
||||
if (!plan) return;
|
||||
this.host._markerDialog = null;
|
||||
if (d.space !== this.host._space && !this.host._commitSpace(d.space)) return;
|
||||
this.host._vacFit = { markerId: d.id, source: src, mapId, p, drag: null };
|
||||
if (plan.space !== this.host._space && !this.host._commitSpace(plan.space)) return;
|
||||
this.host._vacFit = { markerId: d.id, source: src, mapId, routeId: plan.routeId, p: plan.params, drag: null };
|
||||
}
|
||||
|
||||
public _vacFitSave(): void {
|
||||
const f = this.host._vacFit;
|
||||
if (!f) return;
|
||||
// HP-1540-01: no success toast for a save that did not happen
|
||||
const ok = this._vacSaveMatrix(f.markerId, f.source, f.mapId, fitMatrix(f.p));
|
||||
const ok = this._vacSaveMatrix(f.markerId, f.source, f.mapId, fitMatrix(f.p), f.routeId);
|
||||
this.host._vacFit = null;
|
||||
if (ok) this.host._showToast(this.host._t('vac.cal_done'));
|
||||
}
|
||||
|
||||
@@ -814,7 +814,6 @@
|
||||
"vac.autocal": "Automatisch einrichten",
|
||||
"vac.live": "Aktuelle Position im Plan",
|
||||
"vac.trail": "Roboterpfad anzeigen",
|
||||
"vac.cal_maps": "Kalibrierte Karten: {maps}",
|
||||
"vac.autocal_no_rooms": "Die Integration meldet keine Raumliste — öffnen Sie \"Manuell anpassen\"",
|
||||
"vac.autocal_no_match": "Raumnamen stimmten nicht überein (mindestens 3 müssen übereinstimmen) — öffnen Sie \"Manuell anpassen\"",
|
||||
"vac.autocal_done": "Fertig: über {rooms} Räume zugeordnet. Eine Reinigung starten und das Ergebnis prüfen.",
|
||||
|
||||
@@ -814,7 +814,6 @@
|
||||
"vac.autocal": "Set up automatically",
|
||||
"vac.live": "Live position on the plan",
|
||||
"vac.trail": "Show the robot's path",
|
||||
"vac.cal_maps": "Calibrated maps: {maps}",
|
||||
"vac.autocal_no_rooms": "The integration reports no room list — open “Fit manually”",
|
||||
"vac.autocal_no_match": "Room names did not match (need ≥3 in common) — open “Fit manually”",
|
||||
"vac.autocal_done": "Done: bound via {rooms} rooms. Start a cleanup and check",
|
||||
|
||||
@@ -814,7 +814,6 @@
|
||||
"vac.autocal": "Configurer automatiquement",
|
||||
"vac.live": "Position en direct sur le plan",
|
||||
"vac.trail": "Afficher le trajet du robot",
|
||||
"vac.cal_maps": "Cartes calibrées : {maps}",
|
||||
"vac.autocal_no_rooms": "L’intégration ne fournit aucune liste de pièces — ouvrez « Ajuster manuellement »",
|
||||
"vac.autocal_no_match": "Les noms de pièces ne correspondent pas (il en faut ≥3 en commun) — ouvrez « Ajuster manuellement »",
|
||||
"vac.autocal_done": "Terminé : association via {rooms} pièces. Lancez un nettoyage et vérifiez",
|
||||
|
||||
@@ -814,7 +814,6 @@
|
||||
"vac.autocal": "Настроить автоматически",
|
||||
"vac.live": "Живая позиция на плане",
|
||||
"vac.trail": "Показывать путь робота",
|
||||
"vac.cal_maps": "Откалиброваны карты: {maps}",
|
||||
"vac.autocal_no_rooms": "Интеграция не отдаёт список комнат — откройте «Подогнать вручную»",
|
||||
"vac.autocal_no_match": "Не совпали имена комнат (нужно ≥3 общих) — откройте «Подогнать вручную»",
|
||||
"vac.autocal_done": "Готово: привязка по {rooms} комнатам. Запустите уборку и проверьте",
|
||||
|
||||
@@ -42,5 +42,35 @@
|
||||
"support.error.support_rate_limited": "Zu viele Berichte oder Vorschauen wurden angefordert. Versuchen Sie es später erneut.",
|
||||
"support.error.support_unavailable": "Der private Supportdienst ist vorübergehend nicht verfügbar. Der Entwurf bleibt in diesem Fenster erhalten.",
|
||||
"support.error.support_rejected": "Der Bericht hat die Prüfung des Supportdienstes nicht bestanden.",
|
||||
"support.error.unauthorized": "Sie dürfen diesen Bericht nicht senden."
|
||||
"support.error.unauthorized": "Sie dürfen diesen Bericht nicht senden.",
|
||||
"vac.routes_section": "Karten und Etagen",
|
||||
"vac.routes_current_map": "Aktuelle Karte",
|
||||
"vac.routes_no_map": "keine Telemetrie",
|
||||
"vac.routes_status": "Zuordnung",
|
||||
"vac.routes_state_ready": "Karte zugeordnet und kalibriert",
|
||||
"vac.routes_state_needs_calibration": "Karte zugeordnet, Kalibrierung nötig",
|
||||
"vac.routes_state_unmapped": "aktuelle Karte keiner Etage zugeordnet",
|
||||
"vac.routes_state_ambiguous": "zwei Karten nicht unterscheidbar — keine Etage gewählt",
|
||||
"vac.routes_state_missing_space": "der Raum der Karte wurde gelöscht",
|
||||
"vac.routes_state_none": "keine Kartendaten",
|
||||
"vac.routes_empty": "Noch keine Karte einer Etage zugeordnet.",
|
||||
"vac.route_map_default": "ohne Karten-ID",
|
||||
"vac.route_active": "Aktiv",
|
||||
"vac.route_space": "Raum",
|
||||
"vac.route_space_none": "nicht gewählt",
|
||||
"vac.route_status_ready": "bereit",
|
||||
"vac.route_status_needs_calibration": "Kalibrierung nötig",
|
||||
"vac.route_status_ambiguous": "mehrdeutig",
|
||||
"vac.route_status_missing_space": "Raum gelöscht",
|
||||
"vac.route_add_current": "Aktuelle Karte hinzufügen",
|
||||
"vac.route_delete": "Karte entfernen",
|
||||
"vac.route_delete_title": "Karte von der Etage entfernen?",
|
||||
"vac.route_delete_body": "Kalibrierung und gespeicherte Pfade dieser Karte werden entfernt. Fortfahren?",
|
||||
"vac.route_delete_confirm": "Entfernen",
|
||||
"vac.route_space_title": "Karte in einen anderen Raum verschieben?",
|
||||
"vac.route_space_body": "Kalibrierung und gespeicherte Pfade dieser Karte werden entfernt. Fortfahren?",
|
||||
"vac.route_space_confirm": "Verschieben",
|
||||
"vac.route_missing_space_hint": "Eine Karte verweist auf einen gelöschten Raum: neuen wählen oder Karte entfernen. Bis dahin wird der Roboter für diese Karte nicht gezeichnet.",
|
||||
"vac.route_default_hint": "Die Quelle meldet keine stabile Karten-ID. Mehrere Etagen lassen sich ohne andere Quelle oder die vom Roboter gewählte Karte nicht unterscheiden.",
|
||||
"vac.route_target_hint": "Die Station bleibt unabhängig von der aktuellen Karte im Raum „{space}“."
|
||||
}
|
||||
|
||||
@@ -42,5 +42,35 @@
|
||||
"support.error.support_rate_limited": "Too many reports or previews were requested. Try again later.",
|
||||
"support.error.support_unavailable": "The private support service is temporarily unavailable. Your draft remains here.",
|
||||
"support.error.support_rejected": "The report did not pass the support service validation.",
|
||||
"support.error.unauthorized": "You do not have permission to send this report."
|
||||
"support.error.unauthorized": "You do not have permission to send this report.",
|
||||
"vac.routes_section": "Maps and floors",
|
||||
"vac.routes_current_map": "Current map",
|
||||
"vac.routes_no_map": "no telemetry",
|
||||
"vac.routes_status": "Routing",
|
||||
"vac.routes_state_ready": "map assigned and calibrated",
|
||||
"vac.routes_state_needs_calibration": "map assigned, calibration needed",
|
||||
"vac.routes_state_unmapped": "current map is not assigned to a floor",
|
||||
"vac.routes_state_ambiguous": "two maps are indistinguishable — no floor is chosen",
|
||||
"vac.routes_state_missing_space": "the map's space was deleted",
|
||||
"vac.routes_state_none": "no map data",
|
||||
"vac.routes_empty": "No map is assigned to a floor yet.",
|
||||
"vac.route_map_default": "no map id",
|
||||
"vac.route_active": "Active",
|
||||
"vac.route_space": "Space",
|
||||
"vac.route_space_none": "not selected",
|
||||
"vac.route_status_ready": "ready",
|
||||
"vac.route_status_needs_calibration": "needs calibration",
|
||||
"vac.route_status_ambiguous": "ambiguous",
|
||||
"vac.route_status_missing_space": "space deleted",
|
||||
"vac.route_add_current": "Add current map",
|
||||
"vac.route_delete": "Remove map",
|
||||
"vac.route_delete_title": "Remove the map from its floor?",
|
||||
"vac.route_delete_body": "Calibration and saved paths for this map will be removed. Continue?",
|
||||
"vac.route_delete_confirm": "Remove",
|
||||
"vac.route_space_title": "Move the map to another space?",
|
||||
"vac.route_space_body": "Calibration and saved paths for this map will be removed. Continue?",
|
||||
"vac.route_space_confirm": "Move",
|
||||
"vac.route_missing_space_hint": "One of the maps points at a deleted space: pick a new one or remove the map. Until then the robot is not drawn for that map.",
|
||||
"vac.route_default_hint": "The source reports no stable map id. Several floors cannot be told apart without another source or the robot's own selected map.",
|
||||
"vac.route_target_hint": "The dock stays in the “{space}” space regardless of the current map."
|
||||
}
|
||||
|
||||
@@ -42,5 +42,35 @@
|
||||
"support.error.support_rate_limited": "Trop de rapports ou d’aperçus ont été demandés. Réessayez plus tard.",
|
||||
"support.error.support_unavailable": "Le service d’assistance privé est temporairement indisponible. Le brouillon reste dans cette fenêtre.",
|
||||
"support.error.support_rejected": "Le rapport n’a pas passé la validation du service d’assistance.",
|
||||
"support.error.unauthorized": "Vous n’êtes pas autorisé à envoyer ce rapport."
|
||||
"support.error.unauthorized": "Vous n’êtes pas autorisé à envoyer ce rapport.",
|
||||
"vac.routes_section": "Cartes et étages",
|
||||
"vac.routes_current_map": "Carte actuelle",
|
||||
"vac.routes_no_map": "aucune télémétrie",
|
||||
"vac.routes_status": "Routage",
|
||||
"vac.routes_state_ready": "carte assignée et calibrée",
|
||||
"vac.routes_state_needs_calibration": "carte assignée, calibrage requis",
|
||||
"vac.routes_state_unmapped": "carte actuelle non assignée à un étage",
|
||||
"vac.routes_state_ambiguous": "deux cartes indiscernables — aucun étage choisi",
|
||||
"vac.routes_state_missing_space": "l'espace de la carte a été supprimé",
|
||||
"vac.routes_state_none": "aucune donnée de carte",
|
||||
"vac.routes_empty": "Aucune carte n'est encore assignée à un étage.",
|
||||
"vac.route_map_default": "sans identifiant",
|
||||
"vac.route_active": "Active",
|
||||
"vac.route_space": "Espace",
|
||||
"vac.route_space_none": "non sélectionné",
|
||||
"vac.route_status_ready": "prête",
|
||||
"vac.route_status_needs_calibration": "calibrage requis",
|
||||
"vac.route_status_ambiguous": "ambiguë",
|
||||
"vac.route_status_missing_space": "espace supprimé",
|
||||
"vac.route_add_current": "Ajouter la carte actuelle",
|
||||
"vac.route_delete": "Supprimer la carte",
|
||||
"vac.route_delete_title": "Retirer la carte de son étage ?",
|
||||
"vac.route_delete_body": "Le calibrage et les trajets enregistrés de cette carte seront supprimés. Continuer ?",
|
||||
"vac.route_delete_confirm": "Supprimer",
|
||||
"vac.route_space_title": "Déplacer la carte vers un autre espace ?",
|
||||
"vac.route_space_body": "Le calibrage et les trajets enregistrés de cette carte seront supprimés. Continuer ?",
|
||||
"vac.route_space_confirm": "Déplacer",
|
||||
"vac.route_missing_space_hint": "Une carte pointe vers un espace supprimé : choisissez-en un autre ou supprimez la carte. D'ici là, le robot n'est pas dessiné pour cette carte.",
|
||||
"vac.route_default_hint": "La source ne fournit pas d'identifiant de carte stable. Plusieurs étages ne peuvent pas être distingués sans une autre source ou la carte sélectionnée du robot.",
|
||||
"vac.route_target_hint": "La station reste dans l'espace « {space} » quelle que soit la carte actuelle."
|
||||
}
|
||||
|
||||
@@ -42,5 +42,35 @@
|
||||
"support.error.support_rate_limited": "Запрошено слишком много репортов или предпросмотров. Повторите позже.",
|
||||
"support.error.support_unavailable": "Закрытая служба поддержки временно недоступна. Черновик сохранён в этом окне.",
|
||||
"support.error.support_rejected": "Репорт не прошёл проверку службы поддержки.",
|
||||
"support.error.unauthorized": "У вас нет прав на отправку этого репорта."
|
||||
"support.error.unauthorized": "У вас нет прав на отправку этого репорта.",
|
||||
"vac.routes_section": "Карты и этажи",
|
||||
"vac.routes_current_map": "Текущая карта",
|
||||
"vac.routes_no_map": "источник молчит",
|
||||
"vac.routes_status": "Маршрутизация",
|
||||
"vac.routes_state_ready": "карта сопоставлена и откалибрована",
|
||||
"vac.routes_state_needs_calibration": "карта сопоставлена, нужна калибровка",
|
||||
"vac.routes_state_unmapped": "текущая карта не сопоставлена этажу",
|
||||
"vac.routes_state_ambiguous": "две карты неразличимы — этаж не выбирается",
|
||||
"vac.routes_state_missing_space": "пространство карты удалено",
|
||||
"vac.routes_state_none": "нет данных о карте",
|
||||
"vac.routes_empty": "Ни одна карта ещё не сопоставлена этажу.",
|
||||
"vac.route_map_default": "без идентификатора",
|
||||
"vac.route_active": "Текущая",
|
||||
"vac.route_space": "Пространство",
|
||||
"vac.route_space_none": "не выбрано",
|
||||
"vac.route_status_ready": "готова",
|
||||
"vac.route_status_needs_calibration": "нужна калибровка",
|
||||
"vac.route_status_ambiguous": "неоднозначна",
|
||||
"vac.route_status_missing_space": "пространство удалено",
|
||||
"vac.route_add_current": "Добавить текущую карту",
|
||||
"vac.route_delete": "Удалить карту",
|
||||
"vac.route_delete_title": "Удалить карту с этажа?",
|
||||
"vac.route_delete_body": "Калибровка и сохранённые пути этой карты будут удалены. Продолжить?",
|
||||
"vac.route_delete_confirm": "Удалить",
|
||||
"vac.route_space_title": "Перенести карту в другое пространство?",
|
||||
"vac.route_space_body": "Калибровка и сохранённые пути этой карты будут удалены. Продолжить?",
|
||||
"vac.route_space_confirm": "Перенести",
|
||||
"vac.route_missing_space_hint": "Одна из карт ссылается на удалённое пространство: выберите новое или удалите карту. Пока ссылка не исправлена, робот на этой карте не рисуется.",
|
||||
"vac.route_default_hint": "Источник не сообщает стабильный идентификатор карты. Несколько этажей нельзя различить без другого источника или собственной карты робота.",
|
||||
"vac.route_target_hint": "Док остаётся в пространстве «{space}» независимо от текущей карты."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Editor-only half of vacuum map routing (#162).
|
||||
*
|
||||
* Adding, re-targeting and deleting routes only ever happens in the device
|
||||
* editor, so these helpers live in the lazy editor graph: the View card must
|
||||
* not pay for code it can never run (docs/ARCHITECTURE.md, #367).
|
||||
*/
|
||||
import { FitParams, fitFromMatrix, initialFit, VacRoom } from './vacuum';
|
||||
import {
|
||||
Affine, VacuumMapRoute, VacuumRouteMarkerCfg, effectiveRoutes, normalizeRouteMatrix,
|
||||
} from './vacuum-routes';
|
||||
|
||||
/** A marker-local id that no existing route uses. */
|
||||
export function newRouteId(existing: Iterable<string>, seed: () => number = Math.random): string {
|
||||
const taken = new Set(existing);
|
||||
for (let attempt = 0; attempt < 64; attempt += 1) {
|
||||
const id = 'vr_' + Math.floor(seed() * 0xffffff).toString(36).padStart(5, '0');
|
||||
if (!taken.has(id)) return id;
|
||||
}
|
||||
return 'vr_' + Date.now().toString(36);
|
||||
}
|
||||
|
||||
export function addRoute(
|
||||
routes: VacuumMapRoute[], route: Omit<VacuumMapRoute, 'id'>, id: string,
|
||||
): VacuumMapRoute[] {
|
||||
return [...routes, { ...route, id, calibration: route.calibration ?? null }];
|
||||
}
|
||||
|
||||
export function removeRoute(routes: VacuumMapRoute[], routeId: string): VacuumMapRoute[] {
|
||||
return routes.filter((route) => route.id !== routeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-target a route to another space.
|
||||
*
|
||||
* A new space is a NEW identity, not an edited one: the calibration was solved
|
||||
* against the old space's geometry and the recorded runs were filed under the
|
||||
* old id, so both must go rather than silently apply to a different floor.
|
||||
*/
|
||||
export function changeRouteSpace(
|
||||
routes: VacuumMapRoute[], routeId: string, space: string, newId: string,
|
||||
): VacuumMapRoute[] {
|
||||
return routes.map((route) => (route.id === routeId
|
||||
? { ...route, id: newId, space, calibration: null } : route));
|
||||
}
|
||||
|
||||
export function saveRouteCalibration(
|
||||
routes: VacuumMapRoute[], routeId: string, matrix: Affine,
|
||||
): VacuumMapRoute[] {
|
||||
return routes.map((route) => (route.id === routeId ? { ...route, calibration: matrix } : route));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a legacy `calibration` dictionary into explicit routes — all of it.
|
||||
*
|
||||
* Converting only the map the user happens to be looking at would drop the
|
||||
* other floors' calibrations on the first routing edit, so this is all or
|
||||
* nothing: without an exact source there is no conversion at all and the
|
||||
* caller must ask for one first (spec 7.3).
|
||||
*/
|
||||
export function convertLegacyRoutes(
|
||||
marker: VacuumRouteMarkerCfg | null | undefined,
|
||||
dockSpace: string,
|
||||
source: string,
|
||||
nextId: (taken: Set<string>) => string,
|
||||
): VacuumMapRoute[] | null {
|
||||
if (!source) return null;
|
||||
const calibration = marker?.calibration;
|
||||
if (!calibration || typeof calibration !== 'object') return null;
|
||||
const taken = new Set<string>();
|
||||
const routes: VacuumMapRoute[] = [];
|
||||
for (const mapId of Object.keys(calibration)) {
|
||||
const matrix = normalizeRouteMatrix(calibration[mapId]);
|
||||
if (!matrix) continue;
|
||||
const id = nextId(taken);
|
||||
taken.add(id);
|
||||
routes.push({ id, source, map_id: mapId, space: dockSpace, calibration: matrix });
|
||||
}
|
||||
return routes.length ? routes : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a solved matrix belongs.
|
||||
*
|
||||
* With explicit routes the matrix is the route's own property; an unmapped map
|
||||
* still falls back to the Stage 1 dictionary, because refusing to save a
|
||||
* calibration the user just solved would lose real work over bookkeeping.
|
||||
*/
|
||||
export function writeVacuumMatrix(
|
||||
vacuum: VacuumRouteMarkerCfg | null | undefined,
|
||||
opts: { source: string; mapId: string; routeId?: string; matrix: Affine },
|
||||
): VacuumRouteMarkerCfg {
|
||||
const rounded = opts.matrix.map((n) => Number(n.toFixed(6))) as Affine;
|
||||
const routes = vacuum?.map_routes;
|
||||
if (Array.isArray(routes) && routes.length) {
|
||||
const target = routes.find((route) => route.id === opts.routeId)
|
||||
|| routes.find((route) => route.source === opts.source && route.map_id === opts.mapId);
|
||||
if (target) {
|
||||
return { ...vacuum, map_routes: saveRouteCalibration(routes, target.id, rounded) };
|
||||
}
|
||||
}
|
||||
return {
|
||||
...vacuum,
|
||||
source: opts.source,
|
||||
calibration: { ...(vacuum?.calibration || {}), [opts.mapId]: rounded },
|
||||
};
|
||||
}
|
||||
|
||||
export interface VacuumFitPlan {
|
||||
space: string;
|
||||
routeId: string;
|
||||
params: FitParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which space the manual fit canvas opens on, and from which matrix.
|
||||
*
|
||||
* Calibration is solved against the geometry of the space the map is assigned
|
||||
* to — not the dock's space, which is where a multi-floor robot's fit used to
|
||||
* land regardless of the map on screen (#162).
|
||||
*/
|
||||
export function planVacuumFit(
|
||||
markerId: string,
|
||||
vacuum: VacuumRouteMarkerCfg | null | undefined,
|
||||
opts: {
|
||||
routeId?: string; source: string; mapId: string; dockSpace: string; rooms: VacRoom[];
|
||||
viewBoxOf: (spaceId: string) => [number, number, number, number] | null;
|
||||
},
|
||||
): VacuumFitPlan | null {
|
||||
const routes = effectiveRoutes(markerId, vacuum, opts.dockSpace, opts.source);
|
||||
const route = routes.find((item) => item.id === opts.routeId)
|
||||
|| routes.find((item) => item.source === opts.source && item.map_id === opts.mapId)
|
||||
|| null;
|
||||
const space = route?.space || opts.dockSpace;
|
||||
const viewBox = opts.viewBoxOf(space);
|
||||
if (!viewBox) return null;
|
||||
const stored = normalizeRouteMatrix(route ? route.calibration : vacuum?.calibration?.[opts.mapId]);
|
||||
const params = (stored && fitFromMatrix(stored)) || initialFit(opts.rooms, viewBox);
|
||||
return { space, routeId: route?.id || '', params };
|
||||
}
|
||||
@@ -11,6 +11,8 @@
|
||||
* each guessing a floor. Nothing here touches the DOM, `hass` or storage.
|
||||
*/
|
||||
|
||||
import { FitParams, fitFromMatrix, initialFit, VacRoom } from './vacuum';
|
||||
|
||||
export type Affine = [number, number, number, number, number, number];
|
||||
|
||||
/** One saved answer to "this exact map of this exact source lives here". */
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ test('i18n: lazy editor dictionaries carry matching keys and placeholders', () =
|
||||
const supportEn = supportDictionaries.get('en');
|
||||
const keys = Object.keys(supportEn).sort();
|
||||
const placeholders = (value) => (String(value).match(/\{\w+\}/gu) || []).sort();
|
||||
assert.equal(keys.length, 44);
|
||||
assert.equal(keys.length, 74);
|
||||
assert.equal(Object.keys(en).filter((key) => key.startsWith('support.')).join(','), 'support.title');
|
||||
for (const { code } of LANGUAGE_REGISTRY) {
|
||||
const dictionary = supportDictionaries.get(code);
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
normalizeRouteMatrix, isEntityIdLike, VAC_ROUTE_LIMIT, VAC_ROUTE_ERROR,
|
||||
observedMapIds, runRoute, planVacuumOverlay,
|
||||
} from '../test-build/vacuum-routes.js';
|
||||
import {
|
||||
newRouteId, addRoute, removeRoute, changeRouteSpace, saveRouteCalibration, convertLegacyRoutes,
|
||||
writeVacuumMatrix, planVacuumFit,
|
||||
} from '../test-build/vacuum-route-edit.js';
|
||||
|
||||
const fixture = (name) => JSON.parse(readFileSync(
|
||||
new URL(`./fixtures/vacuum-routes/${name}.json`, import.meta.url), 'utf8',
|
||||
@@ -238,3 +242,47 @@ test('легаси-конфиг сохраняет прежнее правило
|
||||
});
|
||||
assert.deepEqual(same.previous, M2, 'прогон активной карты виден, как и раньше');
|
||||
});
|
||||
|
||||
test('новый id маршрута не сталкивается с существующими', () => {
|
||||
const values = [0.5, 0.5, 0.9];
|
||||
let i = 0;
|
||||
const first = newRouteId([], () => values[i++]);
|
||||
const second = newRouteId([first], () => values[i++]);
|
||||
assert.match(first, /^vr_/);
|
||||
assert.notEqual(second, first, 'занятый id пропускается');
|
||||
});
|
||||
|
||||
test('смена пространства — это новая идентичность без калибровки', () => {
|
||||
const before = [
|
||||
{ id: 'r1', source: 'camera.robot', map_id: 'm1', space: 'floor1', calibration: M1 },
|
||||
{ id: 'r2', source: 'camera.robot', map_id: 'm2', space: 'floor2', calibration: M2 },
|
||||
];
|
||||
const after = changeRouteSpace(before, 'r1', 'floor3', 'vr_new');
|
||||
assert.deepEqual(after[0], {
|
||||
id: 'vr_new', source: 'camera.robot', map_id: 'm1', space: 'floor3', calibration: null,
|
||||
});
|
||||
assert.deepEqual(after[1], before[1], 'соседний маршрут не тронут');
|
||||
assert.deepEqual(before[0].calibration, M1, 'вход не мутирован');
|
||||
});
|
||||
|
||||
test('добавление, удаление и запись матрицы маршрута', () => {
|
||||
const added = addRoute([], { source: 'camera.robot', map_id: 'm1', space: 'floor1' }, 'r1');
|
||||
assert.deepEqual(added, [{ id: 'r1', source: 'camera.robot', map_id: 'm1', space: 'floor1', calibration: null }]);
|
||||
const calibrated = saveRouteCalibration(added, 'r1', M2);
|
||||
assert.deepEqual(calibrated[0].calibration, M2);
|
||||
assert.deepEqual(added[0].calibration, null, 'вход не мутирован');
|
||||
assert.deepEqual(removeRoute(calibrated, 'r1'), []);
|
||||
assert.deepEqual(removeRoute(calibrated, 'нет такого'), calibrated);
|
||||
});
|
||||
|
||||
test('конверсия легаси переносит ВСЕ матрицы или ничего (AC13)', () => {
|
||||
const marker = { source: 'camera.robot', calibration: { m1: M1, m2: M2, bad: [1, 2] } };
|
||||
let n = 0;
|
||||
const routes = convertLegacyRoutes(marker, 'floor1', 'camera.robot', () => `vr_${n++}`);
|
||||
assert.equal(routes.length, 2, 'обе валидные карты');
|
||||
assert.deepEqual(routes.map((r) => r.map_id), ['m1', 'm2']);
|
||||
assert.ok(routes.every((r) => r.space === 'floor1' && r.source === 'camera.robot'));
|
||||
assert.deepEqual(routes.map((r) => r.calibration), [M1, M2]);
|
||||
assert.equal(convertLegacyRoutes(marker, 'floor1', '', () => 'vr'), null, 'без источника конверсии нет');
|
||||
assert.equal(convertLegacyRoutes({ calibration: {} }, 'floor1', 'camera.robot', () => 'vr'), null);
|
||||
});
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"src/color.ts", "src/styles.ts", "src/styles/*.styles.ts", "src/logic.ts", "src/i18n.ts", "src/i18n/registry.ts", "src/i18n/language-runtime.ts", "src/glow-blend.ts", "src/grid-scale.ts", "src/device-visual.ts", "src/device-pulse.ts", "src/device-presentation.ts", "src/device-marker-geometry.ts",
|
||||
"src/device-face.ts", "src/device-toggle.ts", "src/marker-toggle-entity.ts", "src/activity-runtime.ts", "src/device-area-relocation.ts",
|
||||
"src/ha-binding-status.ts",
|
||||
"src/integration-provider.ts", "src/vacuum.ts", "src/vacuum-routes.ts",
|
||||
"src/integration-provider.ts", "src/vacuum.ts", "src/vacuum-routes.ts", "src/vacuum-route-edit.ts",
|
||||
"src/sun.ts",
|
||||
"src/light-visibility.ts", "src/glow-scene.ts", "src/space-render.ts",
|
||||
"src/resize.ts", "src/resize-labels.ts", "src/resize-controller.ts", "src/wall-record-preservation.ts",
|
||||
|
||||
Reference in New Issue
Block a user