mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
v1.50.2: the v1.50.1 review (HP-1501-01, HP-1501-02)
- HP-1501-01: v1.50.1 bounded layout positions and left room rectangles, polygon vertices, view_box and opening coordinates on bare _finite — the same absurd-magnitude failure, one schema over. _GEOM (±4) covers them all now, opening angles get ±360. And because a store may already hold such a vertex from before the door existed, contentBounds applies its canvas envelope to room geometry exactly as it does to device positions: the point renders where it is, the frame ignores it, a space of nothing but absurd points falls back to the whole canvas. - HP-1501-02: a repair matching zero positions answered ok/moved:0 and replaced the one-deep backup with an empty one — a typo right after repairing the wrong space destroyed the promised way back. Empty match is nothing_to_repair now: no write, no revision bump, backup intact. Old test fixtures carried view_box [0,0,100,100] from the render-unit days; they now use the normalised box the product actually stores.
This commit is contained in:
@@ -45,7 +45,7 @@ PLAN_ORPHAN_TTL_S = 3600
|
||||
SCHEDULED_GRACE_S = 30 * 24 * 3600
|
||||
FILES_DIR = "houseplan/files"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.50.1"
|
||||
VERSION = "1.50.2"
|
||||
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"spaces": [],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,5 +16,5 @@
|
||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.50.1"
|
||||
"version": "1.50.2"
|
||||
}
|
||||
|
||||
@@ -107,7 +107,14 @@ POS_SCHEMA = vol.Schema(
|
||||
)
|
||||
LAYOUT_SCHEMA = vol.All(vol.Schema({str: POS_SCHEMA}), vol.Length(max=MAX_LAYOUT))
|
||||
|
||||
POINT = vol.All([_finite], vol.Length(min=2, max=2))
|
||||
# Geometry is normalised to the canvas (0..1). ±4 is generous slack for a
|
||||
# vertex nudged past an edge, not an envelope for arbitrary magnitudes: any
|
||||
# finite float used to pass here, and a single 1e100 room vertex stretched the
|
||||
# frame until the whole space was unviewable for every client (HP-1501-01) —
|
||||
# the exact failure HP-1500-03 closed for layout positions, one schema over.
|
||||
_GEOM = vol.All(_finite, vol.Range(min=-4.0, max=4.0))
|
||||
|
||||
POINT = vol.All([_GEOM], vol.Length(min=2, max=2))
|
||||
|
||||
|
||||
def _require_geometry(room: dict) -> dict:
|
||||
@@ -136,10 +143,10 @@ ROOM_SCHEMA = vol.All(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
),
|
||||
vol.Optional("x"): _finite,
|
||||
vol.Optional("y"): _finite,
|
||||
vol.Optional("w"): _finite,
|
||||
vol.Optional("h"): _finite,
|
||||
vol.Optional("x"): _GEOM,
|
||||
vol.Optional("y"): _GEOM,
|
||||
vol.Optional("w"): _GEOM,
|
||||
vol.Optional("h"): _GEOM,
|
||||
vol.Optional("poly"): vol.All([POINT], vol.Length(min=3, max=MAX_POLY_POINTS)),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
@@ -203,7 +210,7 @@ SPACE_SCHEMA = vol.Schema(
|
||||
vol.Optional("plan_aspect"): vol.Any(
|
||||
None, vol.All(vol.Coerce(float), vol.Range(min=0.05, max=20))
|
||||
),
|
||||
vol.Required("view_box"): vol.All([_finite], vol.Length(min=4, max=4)),
|
||||
vol.Required("view_box"): vol.All([_GEOM], vol.Length(min=4, max=4)),
|
||||
vol.Required("rooms"): vol.All([ROOM_SCHEMA], vol.Length(max=MAX_ROOMS)),
|
||||
vol.Optional("decor"): vol.All([DECOR_SCHEMA], vol.Length(max=MAX_DECOR)),
|
||||
vol.Optional("openings"): vol.All([
|
||||
@@ -211,9 +218,9 @@ SPACE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("id"): str,
|
||||
vol.Required("type"): vol.Any("door", "window"),
|
||||
vol.Required("x"): _finite,
|
||||
vol.Required("y"): _finite,
|
||||
vol.Required("angle"): _finite,
|
||||
vol.Required("x"): _GEOM,
|
||||
vol.Required("y"): _GEOM,
|
||||
vol.Required("angle"): vol.All(_finite, vol.Range(min=-360.0, max=360.0)),
|
||||
vol.Required("length"): vol.All(vol.Coerce(float), vol.Range(min=0.001, max=1)),
|
||||
vol.Optional("contact"): vol.Any(str, None),
|
||||
vol.Optional("lock"): vol.Any(str, None),
|
||||
|
||||
@@ -212,6 +212,16 @@ async def ws_geometry_repair(hass: HomeAssistant, connection, msg: dict[str, Any
|
||||
k: dict(v) for k, v in layout.items()
|
||||
if isinstance(v, dict) and str(v.get("s")) == space_id
|
||||
}
|
||||
if not touched:
|
||||
# A typo'd space id used to "succeed" with moved: 0 — and its
|
||||
# empty result REPLACED the one-deep backup, destroying the very
|
||||
# undo this endpoint promises (HP-1501-02). Nothing to move means
|
||||
# nothing to save: no write, no revision bump, the backup stays.
|
||||
connection.send_error(
|
||||
msg["id"], "nothing_to_repair",
|
||||
f"No stored positions belong to space '{space_id}'",
|
||||
)
|
||||
return
|
||||
preview = {k: dict(v) for k, v in touched.items()}
|
||||
migrate_layout(preview, {space_id: msg["aspect"]})
|
||||
if msg.get("dry_run"):
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## v1.50.2 — 2026-07-29
|
||||
|
||||
**From the v1.50.1 review**
|
||||
|
||||
- **Geometry magnitudes are bounded on both layers (HP-1501-01).** v1.50.1
|
||||
bounded layout positions, but room rectangles, polygon vertices, view_box
|
||||
and opening coordinates still took any finite float — one schema-valid 1e100
|
||||
vertex framed the space so wide the plan was a dot, for every client, and
|
||||
the server stored it as a perfectly good configuration. The config schema
|
||||
now bounds geometry to ±4 (angles to ±360°), and the content frame applies
|
||||
the same canvas envelope to room vertices it already applied to device
|
||||
positions — so a store that already holds an absurd coordinate from before
|
||||
this door existed still renders: the point draws wherever it is, it just no
|
||||
longer commands the frame. A vertex a bit past the canvas edge keeps
|
||||
working.
|
||||
- **A no-op repair no longer eats the undo backup (HP-1501-02).** A typo'd
|
||||
space id "succeeded" with moved: 0 — and its empty result replaced the
|
||||
one-deep backup, destroying the only way back exactly when it was needed
|
||||
most: right after repairing the wrong space. Matching nothing is an error
|
||||
now (`nothing_to_repair`); nothing is written, the revision does not move,
|
||||
and the previous repair stays undoable.
|
||||
|
||||
## v1.50.1 — 2026-07-29
|
||||
|
||||
**From the v1.50.0 review**
|
||||
|
||||
@@ -6,6 +6,28 @@
|
||||
> **Правило проекта:** оба файла пополняются в одном коммите с самим
|
||||
> изменением — как и остальная документация (см. docs/STATUS.md).
|
||||
|
||||
## v1.50.2 — 2026-07-29
|
||||
|
||||
**По ревью v1.50.1**
|
||||
|
||||
- **Величины геометрии ограничены на обоих слоях (HP-1501-01).** v1.50.1
|
||||
ограничила позиции устройств, но прямоугольники комнат, вершины полигонов,
|
||||
view_box и координаты проёмов всё ещё принимали любое конечное число — одна
|
||||
проходящая схему вершина 1e100 растягивала кадр так, что план становился
|
||||
точкой у всех клиентов, и сервер хранил это как вполне корректную
|
||||
конфигурацию. Теперь схема конфига ограничивает геометрию ±4 (углы ±360°),
|
||||
а рамка содержимого применяет к вершинам комнат тот же конверт холста, что
|
||||
уже применяла к позициям устройств — конфиг, где абсурдная координата уже
|
||||
лежит с прежних времён, всё равно рендерится: точка рисуется, где стоит,
|
||||
просто кадром больше не командует. Вершина чуть за краём холста работает
|
||||
как раньше.
|
||||
- **Пустой repair больше не съедает бэкап (HP-1501-02).** Опечатка в space_id
|
||||
«успешно» отвечала moved: 0 — и её пустой результат заменял бэкап глубиной
|
||||
один, уничтожая единственный путь назад ровно тогда, когда он нужнее всего:
|
||||
сразу после починки не того пространства. Теперь «нечего чинить» — ошибка
|
||||
(`nothing_to_repair`): ничего не пишется, ревизия не растёт, предыдущий
|
||||
repair по-прежнему отменяем.
|
||||
|
||||
## v1.50.1 — 2026-07-29
|
||||
|
||||
**По ревью v1.50.0**
|
||||
|
||||
+2
-2
@@ -15,12 +15,12 @@
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| Version | **v1.50.1** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
|
||||
| Version | **v1.50.2** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
|
||||
| Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command (merge dev→main, one tag, one release with a summary changelog, CI checked on dev beforehand) |
|
||||
| GitHub | https://github.com/Matysh/houseplan-card — **`main` carries every published release, the latest tag is the current version above**; `dev` is where work lands and is merged into `main` at release time (so `dev` is normally equal to or ahead of `main`, never behind). Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
|
||||
| CI | validate.yml (hacs + hassfest + frontend + backend) green; release.yml attaches the bundle on release publish |
|
||||
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled; ~864 older open PRs but merge rate ≈180/mo; realistic ETA 1–3 months (checked 2026-07-24) |
|
||||
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed **v1.50.1** via direct copy (HACS custom repo also installed) |
|
||||
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed **v1.50.2** via direct copy (HACS custom repo also installed) |
|
||||
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
|
||||
| Tests | Four layers: frontend unit (`npm test`, node:test over `test-build/`), pure backend (`pytest tests_backend`, runs anywhere), HA-harness backend (same folder, CI only — needs py3.13 + pytest-homeassistant-custom-component), and browser smokes (`demo/smoke_*.mjs`, headless chromium). **Counts are not written down here** — they went stale within two releases while the version line beside them was kept current, which reads as less coverage than exists (review R5-2). Run `npm run inventory` for the current numbers, or read them off the last CI run |
|
||||
| Community | **Telegram chat: https://t.me/ha_houseplan** (created 2026-07-27) — the primary user-facing support channel; GitHub issues stay for bugs/features. Link it from any new release notes and posts |
|
||||
|
||||
@@ -239,6 +239,13 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
on the plan after a reload. Same for each tap action and each fill mode
|
||||
[auto: backend test_every_display_mode_the_editor_offers_is_accepted and
|
||||
neighbours, test_a_marker_showing_its_value_can_be_saved]
|
||||
- [ ] Geometry bounds (v1.50.2, HP-1501-01): a config with a 1e100 room
|
||||
vertex is refused by the server; one already stored still renders with a
|
||||
sane frame [auto: test_geometry_magnitudes_are_bounded + unit
|
||||
contentBounds legacy case]
|
||||
- [ ] No-op repair (v1.50.2, HP-1501-02): geometry/repair with a typo'd space
|
||||
id errors, moves no revision and keeps the previous backup undoable
|
||||
[auto: test_a_noop_repair_does_not_eat_the_backup]
|
||||
- [ ] Card below other dashboard content (v1.50.1, HP-1500-02): place the card
|
||||
after a tall card in a normal dashboard — the plan still gets most of the
|
||||
viewport instead of a zero-height stage [auto: smoke_zoom_out]
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.50.1",
|
||||
"version": "1.50.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "houseplan-card",
|
||||
"version": "1.50.1",
|
||||
"version": "1.50.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lit": "^3.1.3",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.50.1",
|
||||
"version": "1.50.2",
|
||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -36,7 +36,7 @@ import { cardStyles } from './styles';
|
||||
import { fitInSquare, contentBounds } from './space-geometry';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.50.1';
|
||||
const CARD_VERSION = '1.50.2';
|
||||
const LS_KEY = 'houseplan_card_layout_v1';
|
||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||
|
||||
+12
-11
@@ -72,8 +72,17 @@ export function contentBounds(
|
||||
space: SpaceModel, pad = 0.05, extra?: ReadonlyArray<readonly [number, number]>,
|
||||
): { x: number; y: number; w: number; h: number } | null {
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
// Only points within a bounded envelope around the canvas command the
|
||||
// opening view. The server bounds what it ACCEPTS now (±4 normalised,
|
||||
// HP-1501-01), but a store may already hold an absurd coordinate from
|
||||
// before that door existed — and one 1e100 vertex framed the space so wide
|
||||
// the plan was a dot for every client. An out-of-envelope point is still
|
||||
// rendered wherever it is; it just does not decide the frame. This applies
|
||||
// to ROOM GEOMETRY and device positions alike (HP-1500-03, HP-1501-01).
|
||||
const lo = -NORM_W * 0.25, hi = NORM_W * 1.25;
|
||||
const add = (x: number, y: number) => {
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
||||
if (x < lo || x > hi || y < lo || y > hi) return;
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x;
|
||||
@@ -86,17 +95,9 @@ export function contentBounds(
|
||||
add(r.x + (r.w || 0), r.y + (r.h || 0));
|
||||
}
|
||||
}
|
||||
// Things that live outside any room still count as content — a gate sensor
|
||||
// by the fence, a camera on a pole (the card passes device positions here).
|
||||
// But only within a bounded envelope around the canvas: a stored position is
|
||||
// any finite number the layout schema took, and one absurd coordinate used
|
||||
// to stretch the frame until the plan was a dot (HP-1500-03). A point past
|
||||
// the envelope is still rendered wherever it is — it just does not command
|
||||
// the opening view.
|
||||
const lo = -NORM_W * 0.25, hi = NORM_W * 1.25;
|
||||
for (const p of extra || []) {
|
||||
if (p[0] >= lo && p[0] <= hi && p[1] >= lo && p[1] <= hi) add(p[0], p[1]);
|
||||
}
|
||||
// things that live outside any room still count as content — a gate sensor
|
||||
// by the fence, a camera on a pole (the card passes device positions here)
|
||||
for (const p of extra || []) add(p[0], p[1]);
|
||||
if (minX > maxX || minY > maxY) return null;
|
||||
// A single marker (or a collinear row of them) has no area, and an SVG
|
||||
// viewBox with a zero axis draws nothing at all (HP-1500-03). An axis with
|
||||
|
||||
@@ -144,4 +144,24 @@ test('contentBounds: never degenerate, never unbounded (HP-1500-03)', () => {
|
||||
// ...but a device a bit past the canvas edge still counts (the gate sensor)
|
||||
const near = contentBounds(room, 0.05, [[1100, 500]]);
|
||||
assert.ok(near.x + near.w > 1050, 'slightly outside the canvas still stretches the frame');
|
||||
|
||||
// HP-1501-01: the same envelope guards ROOM GEOMETRY — the server refuses
|
||||
// absurd vertices now, but a store may hold one from before that door
|
||||
// existed, and a legacy 1e100 vertex must not frame the plan into a dot
|
||||
const legacy = spaceModels({ spaces: [{
|
||||
id: 's', view_box: [0, 0, 1, 1],
|
||||
rooms: [
|
||||
{ id: 'ok', poly: [[0.4, 0.4], [0.6, 0.4], [0.6, 0.6], [0.4, 0.6]] },
|
||||
{ id: 'bad', poly: [[0, 0], [1e100, 0], [1, 1]] },
|
||||
],
|
||||
}], markers: [] })[0];
|
||||
const lb = contentBounds(legacy);
|
||||
assert.ok(lb.w < 1500 && lb.h < 1500, 'the absurd vertex does not command the frame');
|
||||
assert.ok(lb.x <= 400 && lb.x + lb.w >= 600, 'the sane room is still inside it');
|
||||
// a space where EVERY point is absurd falls back to the whole canvas
|
||||
const allBad = spaceModels({ spaces: [{
|
||||
id: 's', view_box: [0, 0, 1, 1],
|
||||
rooms: [{ id: 'b', poly: [[1e100, 1e100], [2e100, 1e100], [2e100, 2e100]] }],
|
||||
}], markers: [] })[0];
|
||||
assert.equal(contentBounds(allBad), null, 'the caller keeps the full canvas');
|
||||
});
|
||||
|
||||
@@ -1374,3 +1374,46 @@ async def test_geometry_repair_is_explicit_previewable_and_undoable(
|
||||
})
|
||||
bad = await client.receive_json()
|
||||
assert not bad["success"] and bad["error"]["code"] == "no_backup"
|
||||
|
||||
|
||||
async def test_a_noop_repair_does_not_eat_the_backup(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""HP-1501-02: a repair that matches nothing used to answer moved: 0 and
|
||||
replace the one-deep backup with an empty one — a typo after a wrong
|
||||
repair destroyed the only way back. Now it is an error, the revision does
|
||||
not move, and the previous repair is still undoable."""
|
||||
await _setup(hass)
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json_auto_id({
|
||||
"type": "houseplan/layout/set",
|
||||
"layout": {"lamp": {"s": "wide", "x": 0.2, "y": 0.1}},
|
||||
})
|
||||
assert (await client.receive_json())["success"]
|
||||
|
||||
await client.send_json_auto_id({
|
||||
"type": "houseplan/geometry/repair", "space_id": "wide", "aspect": 2.0,
|
||||
})
|
||||
rev = (await client.receive_json())["result"]["rev"]
|
||||
|
||||
# the typo: syntactically valid, matches nothing
|
||||
await client.send_json_auto_id({
|
||||
"type": "houseplan/geometry/repair", "space_id": "wid", "aspect": 2.0,
|
||||
})
|
||||
bad = await client.receive_json()
|
||||
assert not bad["success"] and bad["error"]["code"] == "nothing_to_repair"
|
||||
|
||||
await client.send_json_auto_id({"type": "houseplan/layout/get"})
|
||||
got = (await client.receive_json())["result"]
|
||||
assert got["rev"] == rev, "a refused repair moves nothing, including the revision"
|
||||
|
||||
# the wrong-space repair from before the typo is STILL undoable
|
||||
await client.send_json_auto_id({
|
||||
"type": "houseplan/geometry/repair", "space_id": "wide", "aspect": 2.0, "undo": True,
|
||||
})
|
||||
res = (await client.receive_json())["result"]
|
||||
assert res["restored"] == 1
|
||||
await client.send_json_auto_id({"type": "houseplan/layout/get"})
|
||||
lay = (await client.receive_json())["result"]["layout"]
|
||||
assert lay["lamp"] == {"s": "wide", "x": 0.2, "y": 0.1}
|
||||
|
||||
@@ -190,7 +190,7 @@ def test_collection_caps():
|
||||
|
||||
def test_finite_on_every_coordinate():
|
||||
"""audit follow-up B5: NaN/Infinity must be refused everywhere, not only in layout."""
|
||||
base = {"id": "s1", "title": "S", "aspect": 1.0, "view_box": [0, 0, 100, 100], "rooms": []}
|
||||
base = {"id": "s1", "title": "S", "aspect": 1.0, "view_box": [0, 0, 1, 1], "rooms": []}
|
||||
# view_box
|
||||
with pytest.raises(vol.Invalid):
|
||||
v.CONFIG_SCHEMA({"spaces": [{**base, "view_box": [0, 0, "NaN", 100]}]})
|
||||
@@ -211,13 +211,38 @@ def test_finite_on_every_coordinate():
|
||||
{"id": "r", "name": "R", "poly": [[0, 0], [1, 0], [1, 1]]}]}]})
|
||||
|
||||
|
||||
def test_geometry_magnitudes_are_bounded():
|
||||
"""HP-1501-01: any finite float used to pass, and one schema-valid 1e100
|
||||
room vertex made the space unviewable for every client — the exact failure
|
||||
HP-1500-03 closed for layout positions, one schema over. ±4 is slack for a
|
||||
vertex nudged past an edge, not an envelope for absurdity."""
|
||||
base = {"id": "s1", "title": "S", "view_box": [0, 0, 1, 1], "rooms": []}
|
||||
huge = 1e100
|
||||
for cfg in (
|
||||
{**base, "rooms": [{"id": "r", "name": "R", "poly": [[0, 0], [huge, 0], [1, 1]]}]},
|
||||
{**base, "rooms": [{"id": "r", "name": "R", "poly": [[0, 0], [-huge, 0], [1, 1]]}]},
|
||||
{**base, "rooms": [{"id": "r", "name": "R", "x": huge, "y": 0, "w": 1, "h": 1}]},
|
||||
{**base, "rooms": [{"id": "r", "name": "R", "x": 0, "y": 0, "w": huge, "h": 1}]},
|
||||
{**base, "view_box": [0, 0, huge, 1]},
|
||||
{**base, "openings": [{"id": "o", "type": "door", "x": huge, "y": 0.5,
|
||||
"angle": 0, "length": 0.1}]},
|
||||
{**base, "openings": [{"id": "o", "type": "door", "x": 0.5, "y": 0.5,
|
||||
"angle": 1e6, "length": 0.1}]},
|
||||
):
|
||||
with pytest.raises(vol.Invalid):
|
||||
v.CONFIG_SCHEMA({"spaces": [cfg]})
|
||||
# a vertex a bit past the canvas edge is a drawing, not an attack
|
||||
assert v.CONFIG_SCHEMA({"spaces": [{**base, "rooms": [
|
||||
{"id": "r", "name": "R", "poly": [[-0.2, 0], [1.3, 0], [1, 1]]}]}]})
|
||||
|
||||
|
||||
def test_openings_cap_enforced():
|
||||
"""audit follow-up B5: MAX_OPENINGS was defined but never wired in."""
|
||||
many = [{"id": f"o{i}", "type": "door", "x": 0.1, "y": 0.1, "angle": 0, "length": 0.1}
|
||||
for i in range(v.MAX_OPENINGS + 1)]
|
||||
with pytest.raises(vol.Invalid):
|
||||
v.CONFIG_SCHEMA({"spaces": [{"id": "s1", "title": "S", "aspect": 1.0,
|
||||
"view_box": [0, 0, 100, 100], "rooms": [],
|
||||
"view_box": [0, 0, 1, 1], "rooms": [],
|
||||
"openings": many}]})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user