mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 03:38:47 +00:00
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import stat
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
@@ -15,6 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ASSET_INTEGRITY_CACHE_ENTRIES = 256
|
||||
ASSET_HASH_CHUNK_BYTES = 64 * 1024
|
||||
ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS = 30.0
|
||||
_HASS_DATA_KEY = "asset_integrity_verifier"
|
||||
|
||||
|
||||
@@ -40,11 +42,13 @@ class _Flight:
|
||||
|
||||
|
||||
def _signature(path: Path) -> FileSignature:
|
||||
stat = path.stat()
|
||||
current = path.stat()
|
||||
if not stat.S_ISREG(current.st_mode):
|
||||
raise OSError("asset is not a regular file")
|
||||
return FileSignature(
|
||||
size=stat.st_size,
|
||||
mtime_ns=stat.st_mtime_ns,
|
||||
ctime_ns=stat.st_ctime_ns,
|
||||
size=current.st_size,
|
||||
mtime_ns=current.st_mtime_ns,
|
||||
ctime_ns=current.st_ctime_ns,
|
||||
)
|
||||
|
||||
|
||||
@@ -105,7 +109,8 @@ class AssetIntegrityVerifier:
|
||||
|
||||
assert flight is not None
|
||||
if not owner:
|
||||
flight.event.wait()
|
||||
if not flight.event.wait(ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS):
|
||||
return False
|
||||
return flight.digest == expected_digest
|
||||
|
||||
digest: str | None = None
|
||||
|
||||
@@ -373,25 +373,37 @@ def asset_meta_path(root: Path, asset_id: str) -> Path:
|
||||
return root / f"{asset_id}.json"
|
||||
|
||||
|
||||
def _physical_asset_inventory(root: Path) -> list[tuple[Path, int]]:
|
||||
"""Observe regular promoted blobs once, tolerating concurrent removals."""
|
||||
try:
|
||||
if not root.is_dir():
|
||||
return []
|
||||
entries = list(root.iterdir())
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
inventory: list[tuple[Path, int]] = []
|
||||
for path in entries:
|
||||
if path.suffix not in ASSET_EXTENSIONS or not ASSET_ID_RE.fullmatch(path.stem):
|
||||
continue
|
||||
try:
|
||||
current = path.stat(follow_symlinks=False)
|
||||
except OSError:
|
||||
continue
|
||||
if stat.S_ISREG(current.st_mode):
|
||||
inventory.append((path, current.st_size))
|
||||
return sorted(inventory, key=lambda item: item[0].name)
|
||||
|
||||
|
||||
def physical_asset_blobs(root: Path) -> list[Path]:
|
||||
"""Return exact promoted blob files, independently from their sidecars."""
|
||||
if not root.is_dir():
|
||||
return []
|
||||
blobs: list[Path] = []
|
||||
for path in root.iterdir():
|
||||
if (
|
||||
path.suffix in ASSET_EXTENSIONS
|
||||
and ASSET_ID_RE.fullmatch(path.stem)
|
||||
and stat.S_ISREG(path.stat(follow_symlinks=False).st_mode)
|
||||
):
|
||||
blobs.append(path)
|
||||
return sorted(blobs, key=lambda path: path.name)
|
||||
return [path for path, _size in _physical_asset_inventory(root)]
|
||||
|
||||
|
||||
def physical_asset_usage(root: Path) -> tuple[int, int]:
|
||||
"""Return promoted blob count and actual bytes used for quota checks."""
|
||||
blobs = physical_asset_blobs(root)
|
||||
return len(blobs), sum(path.stat().st_size for path in blobs)
|
||||
inventory = _physical_asset_inventory(root)
|
||||
return len(inventory), sum(size for _path, size in inventory)
|
||||
|
||||
|
||||
def _read_catalog_row(root: Path, path: Path) -> dict[str, Any] | None:
|
||||
|
||||
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9",
|
||||
"fingerprint": "060ec710f4ade532364b8887066629a45636ac2a8e16af549ec8f323c449b8a9",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js",
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 291175,
|
||||
"initialViewGzipBytes": 291190,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/de-1N1nfNI0.js",
|
||||
"houseplan-assets/editor-yDIoOZQr.js",
|
||||
"houseplan-assets/fr-CY0KFbzM.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js"
|
||||
],
|
||||
"lazyGzipBytes": 215559,
|
||||
"lazyGzipBytes": 215576,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/editor-yDIoOZQr.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 159876,
|
||||
"lazyEditorGzipBytes": 159881,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14133,
|
||||
"lazyOnboardingGzipBytes": 14134,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js"
|
||||
"houseplan-assets/de-1N1nfNI0.js",
|
||||
"houseplan-assets/fr-CY0KFbzM.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 48727,
|
||||
"lazyLocaleGzipBytes": 48739,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"sha256": "b556cef2823ea5e4f7c142ee5f4cee8dbbed71311dd101291faf30a0d38d6805",
|
||||
"path": "houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"sha256": "982ddc9c1b3ecbfb0dfb63703a0bf35f4a1c294b4cb612e1734f848e328db03d",
|
||||
"rawBytes": 20996,
|
||||
"gzipBytes": 7177,
|
||||
"gzipBytes": 7178,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-B-zT3Bet.js",
|
||||
"sha256": "2ee98742a517ac754434e5627f958a20def03cd1c3907dd7ec3e394104c850a1",
|
||||
"path": "houseplan-assets/de-1N1nfNI0.js",
|
||||
"sha256": "5ebb658cfb7e05c3f1e074127e8fbead0580121b3f6dc8c390075ef5550f85c4",
|
||||
"rawBytes": 81632,
|
||||
"gzipBytes": 24635,
|
||||
"gzipBytes": 24641,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-DJZS-SMT.js",
|
||||
"sha256": "3cda88a32916d4a3256cb0f76570d72f58308ad9ee6a28c9496d5c9e2c613aa4",
|
||||
"path": "houseplan-assets/editor-yDIoOZQr.js",
|
||||
"sha256": "f70e69874730180ecdf67a7ee9003f5c3564432e2c370d235025bcda17696054",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1580,
|
||||
"gzipBytes": 1582,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-DhlZhAWs.js",
|
||||
"sha256": "f37aecbd6a5aaa1fbc5c3be0854464b949cbdcefd453906b268c46d76760772d",
|
||||
"path": "houseplan-assets/fr-CY0KFbzM.js",
|
||||
"sha256": "f356e5b756d5d1746567c1e687c709799af7f842d7de8107d5efdb9107716321",
|
||||
"rawBytes": 83754,
|
||||
"gzipBytes": 24092,
|
||||
"gzipBytes": 24098,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-D9kogu9G.js",
|
||||
"sha256": "65622583b77e3ff11a34ff2c2ceb65748cb1f607412794141375841fcea818bb",
|
||||
"rawBytes": 1033646,
|
||||
"gzipBytes": 290380,
|
||||
"path": "houseplan-assets/houseplan-card-Cq0kQMae.js",
|
||||
"sha256": "d3064f61d80066e24759cac73e23afc0f4c624d813b29c0f9ff30d12d7fea4a4",
|
||||
"rawBytes": 1033685,
|
||||
"gzipBytes": 290393,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
"houseplan-assets/de-1N1nfNI0.js",
|
||||
"houseplan-assets/editor-yDIoOZQr.js",
|
||||
"houseplan-assets/fr-CY0KFbzM.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"sha256": "c1935afb94bfe01cc7ee1302492c6c9a76faac8bd60ddc1db98eded52c7d9d65",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js",
|
||||
"sha256": "098fd4c1331b45578ac8d07a0eb351b1202a5cc3d16cefba88ed17c5e1bb6a4c",
|
||||
"rawBytes": 576162,
|
||||
"gzipBytes": 151119,
|
||||
"gzipBytes": 151121,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js",
|
||||
"sha256": "20b122261b8ce7484806fbaaf17aafb5f59c1864648b1a31fa226dc861d7cbd2",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js",
|
||||
"sha256": "1b3cbd71e89904dc93378c95df0e0bc230787f9a4d1b2bfbd5d09874f93fce8b",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6956,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "ac14b26a8d45a1747833be382ab3b0b816332156a59ed7125fa3eba598cd940d",
|
||||
"sha256": "2d498cb03fe036c36702907335bcf53ee43ba2e58a5e445ee7de73b745365757",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 795,
|
||||
"gzipBytes": 797,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
+1
-1
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__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";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-D9kogu9G.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
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="060ec710f4ade532364b8887066629a45636ac2a8e16af549ec8f323c449b8a9";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-Cq0kQMae.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(h.slice(0,3))}
|
||||
${d(f.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${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);
|
||||
${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);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+49
-49
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";try{await import("./houseplan-assets/houseplan-card-D9kogu9G.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__="060ec710f4ade532364b8887066629a45636ac2a8e16af549ec8f323c449b8a9";try{await import("./houseplan-assets/houseplan-card-Cq0kQMae.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)}
|
||||
|
||||
@@ -341,7 +341,8 @@ class HouseplanDecorAssetUploadView(HomeAssistantView):
|
||||
async with runtime.upload_lock:
|
||||
row, reused = await hass.async_add_executor_job(_store)
|
||||
except DecorAssetError as err:
|
||||
return web.json_response({"error": err.code, "message": str(err)}, status=507)
|
||||
status = 507 if err.code == "capacity_exceeded" else 400
|
||||
return web.json_response({"error": err.code, "message": str(err)}, status=status)
|
||||
except OSError as err:
|
||||
_LOGGER.warning("House Plan decor asset upload: store failed: %s", err)
|
||||
return web.json_response({"error": "io_error"}, status=500)
|
||||
|
||||
@@ -54,12 +54,12 @@ const warmGate = await page.evaluate(async () => {
|
||||
const openedWhileReady = !!root().querySelector('hp-confirm');
|
||||
card._config = { ...card._config, language: 'de' };
|
||||
card.requestUpdate();
|
||||
for (let attempt = 0; attempt < 50 && card._dangerConfirmLocaleGate !== 'warm'; attempt++) {
|
||||
for (let attempt = 0; attempt < 50 && !card.inert; attempt++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
await settle();
|
||||
}
|
||||
|
||||
const entered = card._dangerConfirmLocaleGate === 'warm' && card.inert;
|
||||
const entered = card.inert && card.getAttribute('aria-busy') === 'true';
|
||||
const cancelledOnTransition = await Promise.race([
|
||||
openDecision,
|
||||
new Promise((resolve) => setTimeout(() => resolve('timeout'), 100)),
|
||||
|
||||
@@ -5,9 +5,10 @@ const result = await page.evaluate(async () => {
|
||||
const card = window.__card;
|
||||
const root = () => card.shadowRoot || card.renderRoot;
|
||||
const room = () => root().querySelector('.room');
|
||||
const mouse = (type, x, y) => new PointerEvent(type, {
|
||||
pointerType: 'mouse', bubbles: true, composed: true, clientX: x, clientY: y,
|
||||
const pointer = (type, pointerType, x, y) => new PointerEvent(type, {
|
||||
pointerType, bubbles: true, composed: true, clientX: x, clientY: y,
|
||||
});
|
||||
const mouse = (type, x, y) => pointer(type, 'mouse', x, y);
|
||||
const out = {};
|
||||
|
||||
card._openSettingsDialog();
|
||||
@@ -109,6 +110,18 @@ const result = await page.evaluate(async () => {
|
||||
out.roomHighlightSurvives = card._hoverRoom !== null
|
||||
&& !!root().querySelector('.room-hover-fill-layer,.room-hover-outline-layer');
|
||||
|
||||
room().dispatchEvent(pointer('pointermove', 'pen', 190, 190));
|
||||
await card.updateComplete;
|
||||
out.disabledPenMoveUpdatesModality = card._pointerModality.modality === 'pen';
|
||||
out.disabledPenMoveClearsMouseHover = card._hoverRoom === null && card._tip === null;
|
||||
room().dispatchEvent(mouse('pointerenter', 195, 195));
|
||||
room().dispatchEvent(mouse('pointermove', 195, 195));
|
||||
room().dispatchEvent(pointer('pointermove', 'touch', 195, 195));
|
||||
await card.updateComplete;
|
||||
out.disabledTouchMoveUpdatesModality = card._pointerModality.modality === 'touch';
|
||||
out.disabledTouchMoveClearsMouseHover = card._hoverRoom === null && card._tip === null;
|
||||
out.disabledNonMouseMovesStillSkipArea = roomAreaCalls === 0;
|
||||
|
||||
root().querySelector('.dev').dispatchEvent(mouse('pointermove', 200, 200));
|
||||
await card.updateComplete;
|
||||
out.deviceTipSurvives = card._tip?.room === false && !!root().querySelector('.tip');
|
||||
@@ -144,6 +157,11 @@ checkAll(result, {
|
||||
disabledRoomTip: true,
|
||||
disabledSkipsArea: true,
|
||||
roomHighlightSurvives: true,
|
||||
disabledPenMoveUpdatesModality: true,
|
||||
disabledPenMoveClearsMouseHover: true,
|
||||
disabledTouchMoveUpdatesModality: true,
|
||||
disabledTouchMoveClearsMouseHover: true,
|
||||
disabledNonMouseMovesStillSkipArea: true,
|
||||
deviceTipSurvives: true,
|
||||
reopenShowsOff: true,
|
||||
trueStoredAsAbsent: true,
|
||||
|
||||
@@ -53,6 +53,7 @@ const out = await page.evaluate(async () => {
|
||||
await card._load(true);
|
||||
await settleLoad();
|
||||
const exactCapabilityResolves = resolveCalls === 1 && card._decorAssets.has(assetId);
|
||||
const capabilityOnlyUpgradeAdopted = card._snap.decorAssetsApi === 1;
|
||||
await card._load(true);
|
||||
await settleLoad();
|
||||
const sameEpochUsesCache = resolveCalls === 1;
|
||||
@@ -67,6 +68,7 @@ const out = await page.evaluate(async () => {
|
||||
return {
|
||||
oldBackendSkippedResolve,
|
||||
exactCapabilityResolves,
|
||||
capabilityOnlyUpgradeAdopted,
|
||||
sameEpochUsesCache,
|
||||
downgradeRevokesWithoutResolve,
|
||||
};
|
||||
|
||||
Vendored
+55
-55
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9",
|
||||
"fingerprint": "060ec710f4ade532364b8887066629a45636ac2a8e16af549ec8f323c449b8a9",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js",
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 291175,
|
||||
"initialViewGzipBytes": 291190,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/de-1N1nfNI0.js",
|
||||
"houseplan-assets/editor-yDIoOZQr.js",
|
||||
"houseplan-assets/fr-CY0KFbzM.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js"
|
||||
],
|
||||
"lazyGzipBytes": 215559,
|
||||
"lazyGzipBytes": 215576,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/editor-yDIoOZQr.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 159876,
|
||||
"lazyEditorGzipBytes": 159881,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 14133,
|
||||
"lazyOnboardingGzipBytes": 14134,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js"
|
||||
"houseplan-assets/de-1N1nfNI0.js",
|
||||
"houseplan-assets/fr-CY0KFbzM.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 48727,
|
||||
"lazyLocaleGzipBytes": 48739,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"sha256": "b556cef2823ea5e4f7c142ee5f4cee8dbbed71311dd101291faf30a0d38d6805",
|
||||
"path": "houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"sha256": "982ddc9c1b3ecbfb0dfb63703a0bf35f4a1c294b4cb612e1734f848e328db03d",
|
||||
"rawBytes": 20996,
|
||||
"gzipBytes": 7177,
|
||||
"gzipBytes": 7178,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-B-zT3Bet.js",
|
||||
"sha256": "2ee98742a517ac754434e5627f958a20def03cd1c3907dd7ec3e394104c850a1",
|
||||
"path": "houseplan-assets/de-1N1nfNI0.js",
|
||||
"sha256": "5ebb658cfb7e05c3f1e074127e8fbead0580121b3f6dc8c390075ef5550f85c4",
|
||||
"rawBytes": 81632,
|
||||
"gzipBytes": 24635,
|
||||
"gzipBytes": 24641,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-DJZS-SMT.js",
|
||||
"sha256": "3cda88a32916d4a3256cb0f76570d72f58308ad9ee6a28c9496d5c9e2c613aa4",
|
||||
"path": "houseplan-assets/editor-yDIoOZQr.js",
|
||||
"sha256": "f70e69874730180ecdf67a7ee9003f5c3564432e2c370d235025bcda17696054",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1580,
|
||||
"gzipBytes": 1582,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-DhlZhAWs.js",
|
||||
"sha256": "f37aecbd6a5aaa1fbc5c3be0854464b949cbdcefd453906b268c46d76760772d",
|
||||
"path": "houseplan-assets/fr-CY0KFbzM.js",
|
||||
"sha256": "f356e5b756d5d1746567c1e687c709799af7f842d7de8107d5efdb9107716321",
|
||||
"rawBytes": 83754,
|
||||
"gzipBytes": 24092,
|
||||
"gzipBytes": 24098,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-D9kogu9G.js",
|
||||
"sha256": "65622583b77e3ff11a34ff2c2ceb65748cb1f607412794141375841fcea818bb",
|
||||
"rawBytes": 1033646,
|
||||
"gzipBytes": 290380,
|
||||
"path": "houseplan-assets/houseplan-card-Cq0kQMae.js",
|
||||
"sha256": "d3064f61d80066e24759cac73e23afc0f4c624d813b29c0f9ff30d12d7fea4a4",
|
||||
"rawBytes": 1033685,
|
||||
"gzipBytes": 290393,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-B-zT3Bet.js",
|
||||
"houseplan-assets/editor-DJZS-SMT.js",
|
||||
"houseplan-assets/fr-DhlZhAWs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js"
|
||||
"houseplan-assets/de-1N1nfNI0.js",
|
||||
"houseplan-assets/editor-yDIoOZQr.js",
|
||||
"houseplan-assets/fr-CY0KFbzM.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-S0bYVW5I.js",
|
||||
"sha256": "c1935afb94bfe01cc7ee1302492c6c9a76faac8bd60ddc1db98eded52c7d9d65",
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-Dv0DngiN.js",
|
||||
"sha256": "098fd4c1331b45578ac8d07a0eb351b1202a5cc3d16cefba88ed17c5e1bb6a4c",
|
||||
"rawBytes": 576162,
|
||||
"gzipBytes": 151119,
|
||||
"gzipBytes": 151121,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-DJriHHug.js",
|
||||
"sha256": "20b122261b8ce7484806fbaaf17aafb5f59c1864648b1a31fa226dc861d7cbd2",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-goBKaVug.js",
|
||||
"sha256": "1b3cbd71e89904dc93378c95df0e0bc230787f9a4d1b2bfbd5d09874f93fce8b",
|
||||
"rawBytes": 28088,
|
||||
"gzipBytes": 6956,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-BEQcDs80.js",
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/backdrop-pick--dMAHFcA.js",
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "ac14b26a8d45a1747833be382ab3b0b816332156a59ed7125fa3eba598cd940d",
|
||||
"sha256": "2d498cb03fe036c36702907335bcf53ee43ba2e58a5e445ee7de73b745365757",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 795,
|
||||
"gzipBytes": 797,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-D9kogu9G.js"
|
||||
"houseplan-assets/houseplan-card-Cq0kQMae.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
Vendored
+1
-1
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__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";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-D9kogu9G.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
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="060ec710f4ade532364b8887066629a45636ac2a8e16af549ec8f323c449b8a9";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-Cq0kQMae.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
.computeLabel=${e=>f[e.name]||e.name}
|
||||
.computeLabel=${e=>h[e.name]||e.name}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>`;return c`
|
||||
${d(h.slice(0,3))}
|
||||
${d(f.slice(0,3))}
|
||||
${_?c`<div class="default-floor-error" role="alert"
|
||||
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
|
||||
${t(s,"editor.default_floor_missing",{id:_})}
|
||||
</div>`:a}
|
||||
${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);
|
||||
${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);
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+49
-49
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="a1c6b7fa80c55b69f45f0b0975bcde7eb67d53a02f75f5b7c61d66f220e78ed9";try{await import("./houseplan-assets/houseplan-card-D9kogu9G.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__="060ec710f4ade532364b8887066629a45636ac2a8e16af549ec8f323c449b8a9";try{await import("./houseplan-assets/houseplan-card-Cq0kQMae.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)}
|
||||
|
||||
@@ -391,17 +391,19 @@ keeps its authenticated/signed exact-URL contract.
|
||||
Resolve and HTTP GET share one HA-instance memory-only integrity verifier. It
|
||||
streams SHA-256 in bounded chunks and caches at most 256 actual digests by
|
||||
canonical path plus size/mtime/ctime signature. Per-file-version single-flight
|
||||
deduplicates concurrent reads without serialising different files; a second
|
||||
`stat` prevents a digest for bytes changed mid-read from entering the cache.
|
||||
Missing, changed and corrupt files fail dark. The shared `ContentSigner` batches
|
||||
signatures for `<image>` elements. Catalog deletion rechecks references across
|
||||
every space under the config write lock. Missing or corrupt assets are never
|
||||
painted in View.
|
||||
deduplicates concurrent reads without serialising different files; followers
|
||||
have a bounded wait. Both signatures require a regular file, and the second
|
||||
`stat` prevents a digest for bytes changed or replaced mid-read from entering
|
||||
the cache. Missing, changed, non-regular and corrupt files fail dark. The shared
|
||||
`ContentSigner` batches signatures for `<image>` elements. Catalog deletion
|
||||
rechecks references across every space under the config write lock. Missing or
|
||||
corrupt assets are never painted in View.
|
||||
|
||||
Physical inventory is separate from the strict catalog projection. Quota
|
||||
counts every regular `<sha256><allowed-extension>` blob by actual file size,
|
||||
including blobs with absent or malformed sidecars; a sidecar without a blob
|
||||
does not count. Re-uploading exact bytes repairs a digest-proven orphan before
|
||||
does not count. Entries which disappear or change type during the scan are
|
||||
skipped without aborting the remaining inventory. Re-uploading exact bytes repairs a digest-proven orphan before
|
||||
new-file quota checks and reports `reused:false`, while an already valid row
|
||||
reports `reused:true`. Explicit delete removes only the exact hash sidecar and
|
||||
exact allow-listed blob names under the reference/upload locks—never a prefix,
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Custom background assets now reject special filesystem entries without
|
||||
blocking Home Assistant, tolerate files disappearing during quota scans and
|
||||
report corrupt images separately from exhausted storage; pen/touch movement
|
||||
also clears stale room hover when the room tooltip is disabled
|
||||
([#440](https://github.com/Matysh/houseplan-card/issues/440)).
|
||||
- If Home Assistant rejects a General settings save, the plan now returns to
|
||||
the last server-confirmed state instead of displaying an unsaved local draft;
|
||||
the entered values remain in the open dialog for another attempt
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Пользовательские изображения подложки теперь отклоняют специальные файловые
|
||||
объекты без зависания Home Assistant, переживают исчезновение файлов во время
|
||||
подсчёта лимита и отличают повреждённое изображение от переполненного
|
||||
хранилища; движение пером или пальцем также снимает зависшее наведение комнаты
|
||||
при выключенной подсказке
|
||||
([#440](https://github.com/Matysh/houseplan-card/issues/440)).
|
||||
- Если Home Assistant отклоняет сохранение общих настроек, план снова показывает
|
||||
последнее подтверждённое сервером состояние вместо несохранённого локального
|
||||
черновика; введённые значения остаются в открытом диалоге для повторной
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
мутантам с браузерным гвардом; компиляция тестов в worktree стартует с
|
||||
тёплого `test-build/` основного дерева.
|
||||
|
||||
Чистые Python-контракты канонизации, которым не нужен Home Assistant, находятся
|
||||
в `tests_backend/test_coordinate_canonicalization_pure.py`. Они обязаны реально
|
||||
исполняться в локальном `pytest tests_backend`, а не исчезать за module-level
|
||||
`importorskip`; schema/store/virtual-light проверки остаются в HA-зависимом
|
||||
`test_coordinate_canonicalization.py`. Поведение static-card capability
|
||||
доказывается unit/smoke-счётчиками и состоянием runtime, а не regex по исходнику
|
||||
(#440).
|
||||
|
||||
## Vacuum trail smoothing (#209)
|
||||
|
||||
- [ ] Pure `smoothVacPath` tests prove exact endpoints, separate subpaths,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": "oxipng 10.2.0",
|
||||
"sourceFingerprint": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceFingerprint": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"captureScriptSha256": "cadb8e1bcab9f1dcdd7d75b3b90ddcbaaeb2b8c2a098f575a21f39ff70f5c59c",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "80a70361dc18dd0461568df332062e6482c633af5d280954f8b675701418a76d"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "4106cc28847047505f46921ff95765d8abdf5b382d9d17d4c5e4ad129dd8f6be"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "617b51b3648498787b5039980c9f3eceb75ba56ed63a1a20e616bc05bc304362"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "1dab6cc3b9d1bf7d8c40f0e5137f8c688683c9b7eabc5167da99d41ecfdd5b79"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "94ef50762753c0ac6ddc84d2521c4232a3f9ecd89843810d2df61c517592bed7"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "7a601769de38aa19c2e280f6ff4cf3695b854b1d1c6d1f8de4e6b5e8f7eb1559"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "0939875f8631694f4de0ef4fd01032010ede8c7d49fb7c0a857612d4b3afff93"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "054170fd9ef45762b602b4d5c9c3b9ea9724858be61af137970c246f485c13bb"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -103,7 +103,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "2ae4a58853d98e10d12456b2078ec2f6a0b597722c8310d7b016abd2bc42561e"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "b352add35375f89faf34bb618d758fd2e8e11964dc850c2a3c4035afc676258d",
|
||||
"sourceSha256": "3d5b7e47cc37fa1c507f7f6f3be0c594bea9c4c448d32e2b6095d4ae5b1c24a4",
|
||||
"imageSha256": "cb37f7eefd936f98ef44969b21dfe8e40ee27d1f558abe8d53c0223f6488b8a6"
|
||||
}
|
||||
},
|
||||
|
||||
+89
-17
@@ -1049,11 +1049,11 @@ const MUTANT_DEFINITIONS = [
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' if (this._dangerConfirm && (\n'
|
||||
+ ' this._dangerConfirmMissingSpace() || this._dangerConfirmLocaleGate === \'warm\'\n'
|
||||
+ ' this._dangerConfirmMissingSpace() || this._syncDangerConfirmLocaleGate() === \'warm\'\n'
|
||||
+ ' )) {\n'
|
||||
+ ' this._cancelDangerConfirm();\n'
|
||||
+ ' }',
|
||||
replace: ' if (this._dangerConfirm && this._dangerConfirmLocaleGate === \'warm\') {\n'
|
||||
replace: ' if (this._dangerConfirm && this._syncDangerConfirmLocaleGate() === \'warm\') {\n'
|
||||
+ ' this._cancelDangerConfirm();\n'
|
||||
+ ' }',
|
||||
}],
|
||||
@@ -1065,8 +1065,8 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'confirmation has no rendered decision source and hangs (#417 AC2)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " if (this._dangerConfirmLocaleGate === 'warm') return Promise.resolve(false);",
|
||||
replace: " void this._dangerConfirmLocaleGate;",
|
||||
find: " if (this._syncDangerConfirmLocaleGate() === 'warm') return Promise.resolve(false);",
|
||||
replace: " void this._syncDangerConfirmLocaleGate();",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -1076,17 +1076,20 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'last painted branch can admit an unrenderable request or reject a renderable one (#434 AC7)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " /** Current language branch; never cached from a previous render. */\n"
|
||||
+ ' private get _dangerConfirmLocaleGate(): LanguageRenderGate {\n'
|
||||
find: " /** Synchronize host/runtime language state and return the current branch. */\n"
|
||||
+ ' private _syncDangerConfirmLocaleGate(): LanguageRenderGate {\n'
|
||||
+ " if (!this._config || !this.hass) return 'ready';\n"
|
||||
+ ' return languageRenderGate(\n'
|
||||
+ ' this, LANGUAGE_RUNTIME, langOf(this.hass, this._config.language),\n'
|
||||
+ ' );\n'
|
||||
+ ' }',
|
||||
replace: " private _dangerConfirmLocaleGate: LanguageRenderGate = 'ready';",
|
||||
replace: " private _dangerConfirmLocaleGate: LanguageRenderGate = 'ready';\n"
|
||||
+ ' private _syncDangerConfirmLocaleGate(): LanguageRenderGate {\n'
|
||||
+ ' return this._dangerConfirmLocaleGate;\n'
|
||||
+ ' }',
|
||||
}, {
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' const localeGate = this._dangerConfirmLocaleGate;\n'
|
||||
find: ' const localeGate = this._syncDangerConfirmLocaleGate();\n'
|
||||
+ " if (localeGate === 'cold') return languageLoadingTemplate();",
|
||||
replace: ' const localeGate = languageRenderGate(\n'
|
||||
+ ' this, LANGUAGE_RUNTIME, langOf(this.hass, this._config.language),\n'
|
||||
@@ -1103,7 +1106,7 @@ const MUTANT_DEFINITIONS = [
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' if (this._dangerConfirm && (\n'
|
||||
+ ' this._dangerConfirmMissingSpace() || this._dangerConfirmLocaleGate === \'warm\'\n'
|
||||
+ ' this._dangerConfirmMissingSpace() || this._syncDangerConfirmLocaleGate() === \'warm\'\n'
|
||||
+ ' )) {\n'
|
||||
+ ' this._cancelDangerConfirm();\n'
|
||||
+ ' }',
|
||||
@@ -1112,6 +1115,28 @@ const MUTANT_DEFINITIONS = [
|
||||
+ ' }',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'space-card-decor-capability-downgrade-does-not-clear-assets',
|
||||
guard: 'node demo/smoke_space_card_decor_capability.mjs',
|
||||
because: 'the static card must fail closed after a capability-only backend downgrade; '
|
||||
+ 'source-shape assertions cannot prove that the stale image map is actually cleared (#440)',
|
||||
patches: [{
|
||||
file: 'src/space-card.ts',
|
||||
find: ' if (snap.decorAssetsApi !== DECOR_ASSETS_API_VERSION) {',
|
||||
replace: ' if (false) {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'space-card-decor-capability-change-not-adopted',
|
||||
guard: 'node demo/smoke_space_card_decor_capability.mjs',
|
||||
because: 'an exact capability-only upgrade must become authoritative even when config, '
|
||||
+ 'layout and virtual-light content are unchanged (#440)',
|
||||
patches: [{
|
||||
file: 'src/space-card.ts',
|
||||
find: ' if (configChanged || layoutChanged || virtualLightsChanged || decorAssetsCapabilityChanged) {',
|
||||
replace: ' if (configChanged || layoutChanged || virtualLightsChanged) {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'furniture-edge-handles-steal-the-corner',
|
||||
guard: 'node demo/smoke_furniture_polish.mjs',
|
||||
@@ -2401,7 +2426,7 @@ const MUTANT_DEFINITIONS = [
|
||||
{
|
||||
id: 'image-box-python-canonicalization-omitted',
|
||||
guard: 'node scripts/backend-test-guard.mjs decor_box_catalog_matches_shared_contract '
|
||||
+ 'tests_backend/test_coordinate_canonicalization.py',
|
||||
+ 'tests_backend/test_coordinate_canonicalization_pure.py',
|
||||
because: 'the Python schema and storage boundary must mirror the complete frontend box '
|
||||
+ 'catalog instead of accepting image floating-point tails from older clients (#431 AC4)',
|
||||
patches: [{
|
||||
@@ -2443,7 +2468,7 @@ const MUTANT_DEFINITIONS = [
|
||||
{
|
||||
id: 'python-lattice-round-truncates',
|
||||
guard: 'node scripts/backend-test-guard.mjs all_4801_lattice_nodes '
|
||||
+ 'tests_backend/test_coordinate_canonicalization.py',
|
||||
+ 'tests_backend/test_coordinate_canonicalization_pure.py',
|
||||
because: 'the backend must use the JavaScript Math.round tie direction and nearest-node '
|
||||
+ 'semantics; truncation would make old-client writes diverge from the card (#291)',
|
||||
patches: [{
|
||||
@@ -4812,12 +4837,8 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'promoted blobs invisible, while sidecars without blobs consume no blob quota (#434 AC1)',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/decor_assets.py',
|
||||
find: ' for path in root.iterdir():\n'
|
||||
+ ' if (\n'
|
||||
+ ' path.suffix in ASSET_EXTENSIONS',
|
||||
replace: ' for path in root.glob("*.json"):\n'
|
||||
+ ' if (\n'
|
||||
+ ' path.suffix in ASSET_EXTENSIONS',
|
||||
find: ' entries = list(root.iterdir())\n',
|
||||
replace: ' entries = list(root.glob("*.json"))\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -4997,6 +5018,57 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' stable = True\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'asset-integrity-non-regular-file-admitted',
|
||||
guard: 'node scripts/backend-test-guard.mjs '
|
||||
+ 'integrity_verifier_rejects_non_regular_files_before_hashing '
|
||||
+ 'tests_backend/test_decor_assets.py',
|
||||
because: 'a FIFO or device with a valid asset filename can block an HA executor forever; '
|
||||
+ 'only regular files may reach the streaming hasher (#440 AC1)',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/asset_integrity.py',
|
||||
find: ' if not stat.S_ISREG(current.st_mode):\n'
|
||||
+ ' raise OSError("asset is not a regular file")\n',
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'asset-integrity-follower-waits-forever',
|
||||
guard: 'node scripts/backend-test-guard.mjs integrity_follower_has_a_bounded_wait '
|
||||
+ 'tests_backend/test_decor_assets.py',
|
||||
because: 'a follower must fail dark after a bounded wait even if an injected or wedged '
|
||||
+ 'owner never signals its single-flight event (#440 AC1)',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/asset_integrity.py',
|
||||
find: ' if not flight.event.wait(ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS):\n',
|
||||
replace: ' if not flight.event.wait():\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'room-tooltip-off-skips-pointer-modality',
|
||||
guard: 'node demo/smoke_room_tooltip_toggle.mjs',
|
||||
because: 'pen/touch pointermove inside an already hovered room must clear mouse-only hover '
|
||||
+ 'even when the room information tooltip is disabled (#440 AC2)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' this._notePointer(e);\n'
|
||||
+ ' if (!showRoomTooltipOf(this._settings)) {',
|
||||
replace: ' if (!showRoomTooltipOf(this._settings)) {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'decor-upload-invalid-image-reported-as-capacity',
|
||||
guard: 'node scripts/backend-test-guard.mjs '
|
||||
+ 'decor_asset_upload_deduplicates_and_rejects_mime_spoofing '
|
||||
+ 'tests_backend/test_ha_websocket.py',
|
||||
because: 'a corrupt orphan is a client/integrity rejection, not exhausted storage; only '
|
||||
+ 'capacity_exceeded may produce HTTP 507 (#440 AC4)',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/http_api.py',
|
||||
find: ' status = 507 if err.code == "capacity_exceeded" else 400\n',
|
||||
replace: ' status = 507\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'initial-view-ceiling-unplugged',
|
||||
guard: 'node --test test/bundle-assets.test.mjs',
|
||||
|
||||
@@ -2157,8 +2157,8 @@ export class HouseplanCard extends LitElement {
|
||||
} | { kind: 'run'; text: string; exec: () => void } | null = null;
|
||||
/** One eager confirmation owner shared by View, onboarding and lazy editors. */
|
||||
private _dangerConfirm: HpConfirmState | null = null;
|
||||
/** Current language branch; never cached from a previous render. */
|
||||
private get _dangerConfirmLocaleGate(): LanguageRenderGate {
|
||||
/** Synchronize host/runtime language state and return the current branch. */
|
||||
private _syncDangerConfirmLocaleGate(): LanguageRenderGate {
|
||||
if (!this._config || !this.hass) return 'ready';
|
||||
return languageRenderGate(
|
||||
this, LANGUAGE_RUNTIME, langOf(this.hass, this._config.language),
|
||||
@@ -2185,7 +2185,7 @@ export class HouseplanCard extends LitElement {
|
||||
// resolve. Keep the guards aligned with `_renderBody`'s two unrenderable
|
||||
// branches instead of registering a controller request with no DOM owner.
|
||||
if (!this._config || !this.hass) return Promise.resolve(false);
|
||||
if (this._dangerConfirmLocaleGate === 'warm') return Promise.resolve(false);
|
||||
if (this._syncDangerConfirmLocaleGate() === 'warm') return Promise.resolve(false);
|
||||
if (this._dangerConfirmMissingSpace()) return Promise.resolve(false);
|
||||
return this._dangerConfirmController.confirm(request);
|
||||
};
|
||||
@@ -4190,7 +4190,7 @@ export class HouseplanCard extends LitElement {
|
||||
// #417: losing the only renderable space while a decision is pending must
|
||||
// settle it before render() clears hp-confirm with `nothing`.
|
||||
if (this._dangerConfirm && (
|
||||
this._dangerConfirmMissingSpace() || this._dangerConfirmLocaleGate === 'warm'
|
||||
this._dangerConfirmMissingSpace() || this._syncDangerConfirmLocaleGate() === 'warm'
|
||||
)) {
|
||||
this._cancelDangerConfirm();
|
||||
}
|
||||
@@ -11233,7 +11233,7 @@ export class HouseplanCard extends LitElement {
|
||||
|
||||
private _renderBody(): TemplateResult | typeof nothing | typeof noChange {
|
||||
if (!this._config || !this.hass) return nothing;
|
||||
const localeGate = this._dangerConfirmLocaleGate;
|
||||
const localeGate = this._syncDangerConfirmLocaleGate();
|
||||
if (localeGate === 'cold') return languageLoadingTemplate();
|
||||
if (localeGate === 'warm') return noChange;
|
||||
const onboardingRuntimeRequested = !!this._importDialog
|
||||
@@ -11588,6 +11588,7 @@ export class HouseplanCard extends LitElement {
|
||||
let areaText: string | null | undefined;
|
||||
const tip = (e: PointerEvent) => {
|
||||
if (this._mode !== 'view') return;
|
||||
this._notePointer(e);
|
||||
if (!showRoomTooltipOf(this._settings)) {
|
||||
if (this._tip?.room) this._tip = null;
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,6 @@ import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const card = readFileSync(new URL('../src/space-card.ts', import.meta.url), 'utf8');
|
||||
const configStore = readFileSync(new URL('../src/config-store.ts', import.meta.url), 'utf8');
|
||||
|
||||
// #376(а): YAML `title:` with no value parses as null. The owner decided null
|
||||
// and '' mean the same thing — no header AND the compact frame. The behaviour
|
||||
@@ -24,16 +23,3 @@ test('#376(е) light_pools gates are symmetric: anything but true disposes', ()
|
||||
assert.ok(!/if \(!this\._config\.light_pools\) \{\s*\n\s*disposeGlowRuntime\(/.test(card),
|
||||
'the old truthy dispose gate must not return');
|
||||
});
|
||||
|
||||
test('#434 static card fails closed without exact decor capability', () => {
|
||||
assert.match(configStore, /decorAssetsApi: cfgResp\?\.decor_assets_api === 1 \? 1 : null/,
|
||||
'every fresh config/get must normalize and revoke the runtime capability');
|
||||
assert.match(configStore, /decorAssetsApi: null,/,
|
||||
'localStorage must never seed an unverified runtime capability');
|
||||
assert.match(card, /snap\.decorAssetsApi !== DECOR_ASSETS_API_VERSION[\s\S]*?this\._decorAssets = new Map\(\)/,
|
||||
'old/downgraded backends clear the projection and make no resolve call');
|
||||
assert.match(card, /decorAssetIds\(snap\.config\), snap\.rev/,
|
||||
'the static resolver cache must be scoped by the authoritative config revision');
|
||||
assert.match(card, /configChanged \|\| layoutChanged \|\| virtualLightsChanged \|\| decorAssetsCapabilityChanged/,
|
||||
'a capability-only downgrade must be adopted even when config content is identical');
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""Shared coordinate canonicalisation contract (#224)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
@@ -31,14 +29,8 @@ pytest.importorskip("homeassistant", reason="модуль тянет HA чере
|
||||
|
||||
from custom_components.houseplan import virtual_lights
|
||||
from custom_components.houseplan.coordinate_canonicalization import (
|
||||
COORDINATE_DECIMALS,
|
||||
DECOR_BOX_KINDS,
|
||||
LATTICE_GRID_N,
|
||||
LATTICE_NOISE_STEPS,
|
||||
canonicalize_config_geometry,
|
||||
canonicalize_lattice_coordinate,
|
||||
canonicalize_layout_geometry,
|
||||
canonicalize_number,
|
||||
)
|
||||
from custom_components.houseplan.store import (
|
||||
async_save_config_state,
|
||||
@@ -69,80 +61,6 @@ def _fixture() -> dict:
|
||||
return json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_python_and_frontend_share_the_scalar_lattice_fixture_contract() -> None:
|
||||
fixture = _fixture()
|
||||
config_input = fixture["configInput"]
|
||||
layout_input = fixture["layoutInput"]
|
||||
config_before = copy.deepcopy(config_input)
|
||||
layout_before = copy.deepcopy(layout_input)
|
||||
|
||||
assert COORDINATE_DECIMALS == fixture["decimals"]
|
||||
config = canonicalize_config_geometry(config_input)
|
||||
layout = canonicalize_layout_geometry(layout_input)
|
||||
|
||||
assert config == fixture["configExpected"]
|
||||
assert layout == fixture["layoutExpected"]
|
||||
assert config_input == config_before
|
||||
assert layout_input == layout_before
|
||||
assert canonicalize_config_geometry(config) == config
|
||||
assert canonicalize_layout_geometry(layout) == layout
|
||||
assert math.copysign(1.0, layout["rl:poly"]["x"]) == 1.0
|
||||
|
||||
|
||||
def test_decor_box_catalog_matches_shared_contract() -> None:
|
||||
fixture = _fixture()
|
||||
assert list(DECOR_BOX_KINDS) == fixture["boxKinds"]
|
||||
|
||||
input_decor = fixture["configInput"]["spaces"][0]["decor"]
|
||||
input_boxes = [item for item in input_decor if item["kind"] in DECOR_BOX_KINDS]
|
||||
assert sorted(item["kind"] for item in input_boxes) == sorted(fixture["boxKinds"])
|
||||
|
||||
result = canonicalize_config_geometry(fixture["configInput"])
|
||||
output_by_id = {item["id"]: item for item in result["spaces"][0]["decor"]}
|
||||
expected_by_id = {
|
||||
item["id"]: item for item in fixture["configExpected"]["spaces"][0]["decor"]
|
||||
}
|
||||
for source in input_boxes:
|
||||
actual = output_by_id[source["id"]]
|
||||
expected = expected_by_id[source["id"]]
|
||||
for field in ("x", "y", "w", "h", "angle"):
|
||||
assert actual[field] == expected[field]
|
||||
assert actual[field] != source[field]
|
||||
|
||||
source_image = next(item for item in input_boxes if item["kind"] == "image")
|
||||
image = output_by_id["image"]
|
||||
for field in ("asset_id", "opacity", "flip_h", "flip_v", "future"):
|
||||
assert image[field] == source_image[field]
|
||||
|
||||
|
||||
def test_scalar_contract_is_symmetric_and_keeps_off_grid_geometry() -> None:
|
||||
assert canonicalize_number(1.2345678905) == 1.234567891
|
||||
assert canonicalize_number(-1.2345678905) == -1.234567891
|
||||
value = 0.20833333333333334
|
||||
assert canonicalize_number(value) == 0.208333333
|
||||
assert abs(canonicalize_number(value) - value) <= 5e-10
|
||||
assert math.isnan(canonicalize_number(float("nan")))
|
||||
assert canonicalize_number(float("inf")) == float("inf")
|
||||
|
||||
|
||||
def test_all_4801_lattice_nodes_and_nine_decimal_forms_share_exact_bits() -> None:
|
||||
assert LATTICE_GRID_N == 240
|
||||
assert LATTICE_NOISE_STEPS == 1e-4
|
||||
for index in range(-2400, 2401):
|
||||
node = index / LATTICE_GRID_N
|
||||
nine_decimal = float(f"{node:.9f}")
|
||||
assert canonicalize_lattice_coordinate(node) == node
|
||||
assert canonicalize_lattice_coordinate(nine_decimal) == node
|
||||
assert canonicalize_lattice_coordinate(
|
||||
canonicalize_lattice_coordinate(nine_decimal)
|
||||
) == node
|
||||
assert math.copysign(1.0, canonicalize_lattice_coordinate(-0.0)) == 1.0
|
||||
assert canonicalize_lattice_coordinate(0.06) == 0.06
|
||||
assert canonicalize_lattice_coordinate(0.2875) == 0.2875
|
||||
assert canonicalize_lattice_coordinate((1 + 0.999e-4) / 240) == 1 / 240
|
||||
assert canonicalize_lattice_coordinate((1 + 1.001e-4) / 240) == 0.004167084
|
||||
|
||||
|
||||
def test_backend_schemas_apply_the_same_allowlist() -> None:
|
||||
fixture = _fixture()
|
||||
assert CONFIG_SCHEMA(fixture["configInput"]) == fixture["configExpected"]
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""HA-independent coordinate canonicalisation contracts (#224/#431/#440)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from custom_components.houseplan.coordinate_canonicalization import (
|
||||
COORDINATE_DECIMALS,
|
||||
DECOR_BOX_KINDS,
|
||||
LATTICE_GRID_N,
|
||||
LATTICE_NOISE_STEPS,
|
||||
canonicalize_config_geometry,
|
||||
canonicalize_lattice_coordinate,
|
||||
canonicalize_layout_geometry,
|
||||
canonicalize_number,
|
||||
)
|
||||
|
||||
FIXTURE = (
|
||||
Path(__file__).parents[1]
|
||||
/ "test"
|
||||
/ "fixtures"
|
||||
/ "coordinate-canonicalization.json"
|
||||
)
|
||||
|
||||
|
||||
def _fixture() -> dict:
|
||||
return json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_python_and_frontend_share_the_scalar_lattice_fixture_contract() -> None:
|
||||
fixture = _fixture()
|
||||
config_input = fixture["configInput"]
|
||||
layout_input = fixture["layoutInput"]
|
||||
config_before = copy.deepcopy(config_input)
|
||||
layout_before = copy.deepcopy(layout_input)
|
||||
|
||||
assert COORDINATE_DECIMALS == fixture["decimals"]
|
||||
config = canonicalize_config_geometry(config_input)
|
||||
layout = canonicalize_layout_geometry(layout_input)
|
||||
|
||||
assert config == fixture["configExpected"]
|
||||
assert layout == fixture["layoutExpected"]
|
||||
assert config_input == config_before
|
||||
assert layout_input == layout_before
|
||||
assert canonicalize_config_geometry(config) == config
|
||||
assert canonicalize_layout_geometry(layout) == layout
|
||||
assert math.copysign(1.0, layout["rl:poly"]["x"]) == 1.0
|
||||
|
||||
|
||||
def test_decor_box_catalog_matches_shared_contract() -> None:
|
||||
fixture = _fixture()
|
||||
assert list(DECOR_BOX_KINDS) == fixture["boxKinds"]
|
||||
|
||||
input_decor = fixture["configInput"]["spaces"][0]["decor"]
|
||||
input_boxes = [item for item in input_decor if item["kind"] in DECOR_BOX_KINDS]
|
||||
assert sorted(item["kind"] for item in input_boxes) == sorted(fixture["boxKinds"])
|
||||
|
||||
result = canonicalize_config_geometry(fixture["configInput"])
|
||||
output_by_id = {item["id"]: item for item in result["spaces"][0]["decor"]}
|
||||
expected_by_id = {
|
||||
item["id"]: item for item in fixture["configExpected"]["spaces"][0]["decor"]
|
||||
}
|
||||
for source in input_boxes:
|
||||
actual = output_by_id[source["id"]]
|
||||
expected = expected_by_id[source["id"]]
|
||||
for field in ("x", "y", "w", "h", "angle"):
|
||||
assert actual[field] == expected[field]
|
||||
assert actual[field] != source[field]
|
||||
|
||||
source_image = next(item for item in input_boxes if item["kind"] == "image")
|
||||
image = output_by_id["image"]
|
||||
for field in ("asset_id", "opacity", "flip_h", "flip_v", "future"):
|
||||
assert image[field] == source_image[field]
|
||||
|
||||
|
||||
def test_scalar_contract_is_symmetric_and_keeps_off_grid_geometry() -> None:
|
||||
assert canonicalize_number(1.2345678905) == 1.234567891
|
||||
assert canonicalize_number(-1.2345678905) == -1.234567891
|
||||
value = 0.20833333333333334
|
||||
assert canonicalize_number(value) == 0.208333333
|
||||
assert abs(canonicalize_number(value) - value) <= 5e-10
|
||||
assert math.isnan(canonicalize_number(float("nan")))
|
||||
assert canonicalize_number(float("inf")) == float("inf")
|
||||
|
||||
|
||||
def test_all_4801_lattice_nodes_and_nine_decimal_forms_share_exact_bits() -> None:
|
||||
assert LATTICE_GRID_N == 240
|
||||
assert LATTICE_NOISE_STEPS == 1e-4
|
||||
for index in range(-2400, 2401):
|
||||
node = index / LATTICE_GRID_N
|
||||
nine_decimal = float(f"{node:.9f}")
|
||||
assert canonicalize_lattice_coordinate(node) == node
|
||||
assert canonicalize_lattice_coordinate(nine_decimal) == node
|
||||
assert canonicalize_lattice_coordinate(
|
||||
canonicalize_lattice_coordinate(nine_decimal)
|
||||
) == node
|
||||
assert math.copysign(1.0, canonicalize_lattice_coordinate(-0.0)) == 1.0
|
||||
assert canonicalize_lattice_coordinate(0.06) == 0.06
|
||||
assert canonicalize_lattice_coordinate(0.2875) == 0.2875
|
||||
assert canonicalize_lattice_coordinate((1 + 0.999e-4) / 240) == 1 / 240
|
||||
assert canonicalize_lattice_coordinate((1 + 1.001e-4) / 240) == 0.004167084
|
||||
@@ -5,6 +5,7 @@ import base64
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import threading
|
||||
import zlib
|
||||
@@ -15,6 +16,7 @@ import pytest
|
||||
|
||||
from custom_components.houseplan.asset_integrity import (
|
||||
ASSET_INTEGRITY_CACHE_ENTRIES,
|
||||
ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS,
|
||||
AssetIntegrityVerifier,
|
||||
)
|
||||
from custom_components.houseplan.const import MAX_DECOR_ASSET_BYTES
|
||||
@@ -358,6 +360,40 @@ def test_physical_inventory_counts_exact_promoted_blobs_not_sidecars(tmp_path) -
|
||||
assert physical_asset_usage(tmp_path) == (2, 9)
|
||||
|
||||
|
||||
def test_physical_inventory_skips_entries_lost_during_stat(
|
||||
tmp_path, monkeypatch,
|
||||
) -> None:
|
||||
lost = tmp_path / f"{'4' * 64}.png"
|
||||
survivor = tmp_path / f"{'5' * 64}.webp"
|
||||
lost.write_bytes(b"gone")
|
||||
survivor.write_bytes(b"survives")
|
||||
original_stat = Path.stat
|
||||
|
||||
def racing_stat(path: Path, *, follow_symlinks=True):
|
||||
if path == lost and not follow_symlinks:
|
||||
raise FileNotFoundError("removed after iterdir")
|
||||
return original_stat(path, follow_symlinks=follow_symlinks)
|
||||
|
||||
monkeypatch.setattr(Path, "stat", racing_stat)
|
||||
assert physical_asset_blobs(tmp_path) == [survivor]
|
||||
assert physical_asset_usage(tmp_path) == (1, len(b"survives"))
|
||||
|
||||
|
||||
def test_physical_inventory_treats_disappearing_root_as_empty(
|
||||
tmp_path, monkeypatch,
|
||||
) -> None:
|
||||
original_iterdir = Path.iterdir
|
||||
|
||||
def racing_iterdir(path: Path):
|
||||
if path == tmp_path:
|
||||
raise FileNotFoundError("root removed before scan")
|
||||
return original_iterdir(path)
|
||||
|
||||
monkeypatch.setattr(Path, "iterdir", racing_iterdir)
|
||||
assert physical_asset_blobs(tmp_path) == []
|
||||
assert physical_asset_usage(tmp_path) == (0, 0)
|
||||
|
||||
|
||||
def test_catalog_empty_directory_and_metadata_path(tmp_path) -> None:
|
||||
missing = tmp_path / "missing"
|
||||
aid = "c" * 64
|
||||
@@ -444,6 +480,34 @@ def test_integrity_cache_invalidates_changed_signature_and_rejects_mid_read_chan
|
||||
assert not unstable._cache, "an unstable digest must not become a cache hit"
|
||||
|
||||
|
||||
def test_integrity_verifier_rejects_non_regular_files_before_hashing(tmp_path) -> None:
|
||||
calls = 0
|
||||
|
||||
def forbidden(_candidate: Path) -> str:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
raise AssertionError("non-regular files must never reach the hasher")
|
||||
|
||||
verifier = AssetIntegrityVerifier(hasher=forbidden)
|
||||
assert not verifier.verify(tmp_path, "0" * 64)
|
||||
assert calls == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(os, "mkfifo"), reason="FIFO is POSIX-only")
|
||||
def test_integrity_verifier_rejects_fifo_without_opening_it(tmp_path) -> None:
|
||||
fifo = tmp_path / "asset.png"
|
||||
os.mkfifo(fifo)
|
||||
called = False
|
||||
|
||||
def forbidden(_candidate: Path) -> str:
|
||||
nonlocal called
|
||||
called = True
|
||||
raise AssertionError("opening a FIFO could block the HA executor forever")
|
||||
|
||||
assert not AssetIntegrityVerifier(hasher=forbidden).verify(fifo, "0" * 64)
|
||||
assert not called
|
||||
|
||||
|
||||
def test_integrity_cache_single_flights_same_path_and_releases_after_error(tmp_path) -> None:
|
||||
path = tmp_path / "asset.bin"
|
||||
payload = b"concurrent"
|
||||
@@ -493,6 +557,42 @@ def test_integrity_cache_single_flights_same_path_and_releases_after_error(tmp_p
|
||||
assert attempts == 2 and not recovered._inflight
|
||||
|
||||
|
||||
def test_integrity_follower_has_a_bounded_wait(tmp_path) -> None:
|
||||
path = tmp_path / "asset.bin"
|
||||
payload = b"slow-owner"
|
||||
path.write_bytes(payload)
|
||||
expected = hashlib.sha256(payload).hexdigest()
|
||||
owner_started = threading.Event()
|
||||
release_owner = threading.Event()
|
||||
events = []
|
||||
|
||||
class TimedEvent:
|
||||
def __init__(self) -> None:
|
||||
self.timeout = None
|
||||
events.append(self)
|
||||
|
||||
def set(self) -> None:
|
||||
pass
|
||||
|
||||
def wait(self, timeout=None) -> bool:
|
||||
self.timeout = timeout
|
||||
return False
|
||||
|
||||
def slow(candidate: Path) -> str:
|
||||
owner_started.set()
|
||||
assert release_owner.wait(2)
|
||||
return hashlib.sha256(candidate.read_bytes()).hexdigest()
|
||||
|
||||
verifier = AssetIntegrityVerifier(hasher=slow, event_factory=TimedEvent)
|
||||
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||
owner = pool.submit(verifier.verify, path, expected)
|
||||
assert owner_started.wait(1)
|
||||
assert not verifier.verify(path, expected)
|
||||
assert events[0].timeout == ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS
|
||||
release_owner.set()
|
||||
assert owner.result(timeout=2)
|
||||
|
||||
|
||||
def test_integrity_checks_for_different_paths_do_not_share_a_hash_lock(tmp_path) -> None:
|
||||
first_path = tmp_path / "first.bin"
|
||||
second_path = tmp_path / "second.bin"
|
||||
|
||||
@@ -2152,17 +2152,33 @@ async def test_decor_asset_upload_deduplicates_and_rejects_mime_spoofing(
|
||||
assert repaired_body["reused"] is False
|
||||
assert json.loads((root / f"{aid}.json").read_text(encoding="utf-8"))["asset_id"] == aid
|
||||
|
||||
# Only quota exhaustion is a storage-capacity response.
|
||||
(root / f"{aid}.json").unlink()
|
||||
(root / f"{aid}.png").unlink()
|
||||
monkeypatch.setattr(hp_http, "MAX_DECOR_ASSETS_COUNT", 0)
|
||||
full = await view.post(_Request("image/png"))
|
||||
assert full.status == 507
|
||||
assert json.loads(full.text)["error"] == "capacity_exceeded"
|
||||
|
||||
# A matching filename never licenses overwriting bytes whose digest does
|
||||
# not match the content-addressed id.
|
||||
(root / f"{aid}.json").unlink()
|
||||
blob = root / f"{aid}.png"
|
||||
blob.write_bytes(b"corrupt")
|
||||
rejected = await view.post(_Request("image/png"))
|
||||
assert rejected.status == 507
|
||||
assert rejected.status == 400
|
||||
assert json.loads(rejected.text)["error"] == "invalid_image"
|
||||
assert blob.read_bytes() == b"corrupt"
|
||||
assert not (root / f"{aid}.json").exists()
|
||||
|
||||
blob.unlink()
|
||||
monkeypatch.setattr(
|
||||
hp_http, "physical_asset_usage",
|
||||
lambda _root: (_ for _ in ()).throw(OSError("inventory failed")),
|
||||
)
|
||||
failed = await view.post(_Request("image/png"))
|
||||
assert failed.status == 500
|
||||
assert json.loads(failed.text)["error"] == "io_error"
|
||||
|
||||
spoofed = await view.post(_Request("image/jpeg"))
|
||||
assert spoofed.status == 400
|
||||
assert json.loads(spoofed.text)["error"] == "invalid_format"
|
||||
|
||||
Reference in New Issue
Block a user