Files
houseplan-card/test/fixtures/coordinate-canonicalization.json
T
claude[bot]andCodex 870237f3fb feat(backend): LED strips are stored, normalised and transferred (#780)
Stage 1 of #780 — the data model. A space carries an optional
`led_strips: [{id, points, marker, active?}]` (custom_components/houseplan/
led_strips.py, pure, strict mypy).

- Type schema inside SPACE_SCHEMA: 2–50 finite numeric points (no strings,
  booleans, NaN or off-canvas values), ≤50 strips per space including hidden
  shapes, strict boolean `active`, marker = non-empty string or null.
- A config-level step after coordinate canonicalisation judges the shape
  (two distinct points, non-zero length, a closed strip needs three distinct
  vertices, a hidden shape needs a marker, unique ids per space) and the links
  in the order the spec fixes: duplicates are rejected before any
  normalisation (two links to the same missing id still conflict); a link to
  a marker that is not live becomes an unbound strip (marker null, active
  true, id and points kept), so a client that does not know strips can delete
  a bound marker without its save failing; a live marker with an empty space
  adopts the strip's space; a non-empty foreign space rejects the write.
- config/set answers with `led_strips: {unbound, space_adopted}` when the
  write was normalised, so a new client re-reads; old clients ignore it.
- Space import remaps links through the marker id map; a skipped or
  virtualised duplicate leaves the strip unbound; a coinciding old id never
  binds. Plan-only export keeps geometry and nulls every link. Import details
  report `unbound_led_strips`, computed by the server, never read from the file.
- Coordinates get JSON-noise cleanup only, like stairs (face contacts are
  off-lattice), in both canonicalisers with a shared fixture case.
- Support package: counters only (total/unbound/hidden), no coordinates or ids.

Tests: tests_backend/test_led_strips.py (41, pure), test_ha_import_export
(6 cases: full round trip with a hidden shape, orphan count, remap against a
coinciding id, skip and virtual duplicates, plan-only), test_ha_websocket
(old client deletes a bound marker → save stands, counters, foreign space
rejects without a new revision). Full backend with the HA harness: 969 passed.
Mutating the shape check, the duplicate check, the orphan normalisation or the
space adoption each turns the pure suite red.

Issue: #780
User-Visible: no
2026-10-02 20:54:49 +03:00

479 lines
12 KiB
JSON

{
"decimals": 9,
"boxKinds": ["rect", "ellipse", "furniture", "image"],
"configInput": {
"spaces": [
{
"id": "floor",
"title": "Floor",
"cell_cm": 5.0000000004,
"plan_aspect": 1.5000000004,
"plan_x": 0.1234567894,
"plan_y": -0.1234567896,
"plan_scale": 1.0000000001,
"plan_scale_x": 1.2345678906,
"plan_scale_y": 0.9876543216,
"plan_angle": 44.1234567896,
"view_box": [0.0000000004, -0.0000000004, 1.0000000004, 1.0000000004],
"settings": {
"room_opacity": 0.5000000004,
"temp_min": 18.1234567896,
"temp_max": 28.1234567896,
"card_font_scale": 1.0000000004
},
"rooms": [
{
"id": "poly",
"name": "Poly",
"area": null,
"poly": [
[0.0000000004, -0.0000000004],
[0.3000000004, 0.0000000004],
[0.0000000004, 0.3000000004]
],
"settings": {
"name_scale": 1.0000000004,
"label_scale": 1.2000000004
}
},
{
"id": "rect",
"name": "Rect",
"area": null,
"x": 0.4000000004,
"y": 0.4000000006,
"w": 0.2000000004,
"h": 0.2000000006
}
],
"walls": [
{
"key": "wall",
"cm": 15.0000000004,
"a": [0.0000000004, 0.3000000004],
"b": [0.3000000006, 0.3000000006]
}
],
"openings": [
{
"id": "opening",
"type": "door",
"x": 0.5000000004,
"y": 0.0000000004,
"angle": -0.0000000004,
"length": 0.2000000006,
"host": {
"kind": "partition",
"id": "partition",
"t": 0.5000000004
}
}
],
"decor": [
{
"id": "line",
"kind": "line",
"x1": 0.1000000004,
"y1": 0.1000000006,
"x2": 0.2000000004,
"y2": 0.2000000006,
"width_cm": 2.0000000004,
"opacity": 0.7000000004
},
{
"id": "rect-decor",
"kind": "rect",
"x": 0.1000000004,
"y": 0.2000000006,
"w": 0.3000000004,
"h": 0.4000000006,
"angle": 12.1234567896,
"fill_opacity": 0.6000000004
},
{
"id": "ellipse",
"kind": "ellipse",
"x": -0.1000000004,
"y": -0.2000000006,
"w": 0.3000000006,
"h": 0.4000000004,
"angle": -12.1234567896
},
{
"id": "text",
"kind": "text",
"x": 0.7000000004,
"y": 0.8000000006,
"text": "Text",
"scale": 1.2345678906,
"angle": 2.1234567896,
"size_cm": 25.0000000004
},
{
"id": "chair",
"kind": "furniture",
"symbol": "chair",
"x": 0.6000000004,
"y": 0.6000000006,
"w": 0.1000000004,
"h": 0.1000000006,
"angle": 90.1234567896
},
{
"id": "image",
"kind": "image",
"asset_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"x": 0.5000000004,
"y": 0.5000000006,
"w": 0.2000000004,
"h": 0.3000000006,
"angle": 45.1234567896,
"opacity": 0.5000000004,
"flip_h": true,
"flip_v": false,
"future": {"numeric": 0.1234567896}
}
],
"room_drafts": [
{
"id": "draft",
"points": [
[0.1000000004, 0.9000000006],
[0.2000000006, 0.9000000004],
[0.3000000004, 0.9000000006]
],
"segments": [
{"cm": 10.0000000004},
{"cm": 20.0000000004}
]
}
],
"partitions": [
{
"id": "partition",
"a": [0.0000000004, 0.0000000004],
"b": [1.0000000004, 0.0000000004],
"cm": 12.0000000004
}
],
"wall_columns": [
{
"id": "column",
"shape": "square",
"center": [0.7500000004, 0.7500000006],
"cm": 30.0000000004,
"angle": 45.1234567896
}
],
"open_spans": [
{
"a": [0.3000000004, 0.3000000006],
"b": [0.6000000006, 0.3000000004]
}
],
"led_strips": [
{
"id": "led-1",
"points": [[0.3000000004, 0.4166666671], [0.1234567896, -0.0000000004]],
"marker": null
}
],
"future": {
"numeric": 0.1234567896
}
}
],
"markers": [
{
"id": "virtual",
"binding": "virtual",
"angle": -30.1234567896,
"size": 1.1000000004,
"ripple_size": 1.5000000004,
"glow_radius_cm": 120.0000000004,
"glow_color": {
"c": "#112233",
"bri": 0.5000000004
},
"vacuum": {
"calibration": {
"map": [
-0.0017921234567,
0.0000000004,
1.0000000004,
0.0017921234567,
-0.0000000004,
2.0000000004
]
}
}
}
],
"settings": {
"glow_radius_cm": 150.0000000004,
"fill_colors": {
"on": {
"c": "#abcdef",
"a": 0.5000000004
}
},
"future_numeric": 0.9876543216
},
"future_root": {
"numeric": 0.1234567896
}
},
"configExpected": {
"spaces": [
{
"id": "floor",
"title": "Floor",
"cell_cm": 5.0000000004,
"plan_aspect": 1.5000000004,
"plan_x": 0.123456789,
"plan_y": -0.12345679,
"plan_scale": 1,
"plan_scale_x": 1.234567891,
"plan_scale_y": 0.987654322,
"plan_angle": 44.12345679,
"view_box": [0.0000000004, -0.0000000004, 1.0000000004, 1.0000000004],
"settings": {
"room_opacity": 0.5000000004,
"temp_min": 18.1234567896,
"temp_max": 28.1234567896,
"card_font_scale": 1.0000000004
},
"rooms": [
{
"id": "poly",
"name": "Poly",
"area": null,
"poly": [
[0, 0],
[0.3, 0],
[0, 0.3]
],
"settings": {
"name_scale": 1.0000000004,
"label_scale": 1.2000000004
}
},
{
"id": "rect",
"name": "Rect",
"area": null,
"x": 0.4,
"y": 0.4,
"w": 0.2,
"h": 0.2
}
],
"walls": [
{
"key": "wall",
"cm": 15.0000000004,
"a": [0, 0.3],
"b": [0.3, 0.3]
}
],
"openings": [
{
"id": "opening",
"type": "door",
"x": 0.5,
"y": 0,
"angle": 0,
"length": 0.200000001,
"host": {
"kind": "partition",
"id": "partition",
"t": 0.5
}
}
],
"decor": [
{
"id": "line",
"kind": "line",
"x1": 0.1,
"y1": 0.1,
"x2": 0.2,
"y2": 0.2,
"width_cm": 2.0000000004,
"opacity": 0.7000000004
},
{
"id": "rect-decor",
"kind": "rect",
"x": 0.1,
"y": 0.2,
"w": 0.3,
"h": 0.4,
"angle": 12.12345679,
"fill_opacity": 0.6000000004
},
{
"id": "ellipse",
"kind": "ellipse",
"x": -0.1,
"y": -0.2,
"w": 0.3,
"h": 0.4,
"angle": -12.12345679
},
{
"id": "text",
"kind": "text",
"x": 0.7,
"y": 0.8,
"text": "Text",
"scale": 1.234567891,
"angle": 2.12345679,
"size_cm": 25.0000000004
},
{
"id": "chair",
"kind": "furniture",
"symbol": "chair",
"x": 0.6,
"y": 0.6,
"w": 0.1,
"h": 0.1,
"angle": 90.12345679
},
{
"id": "image",
"kind": "image",
"asset_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"x": 0.5,
"y": 0.5,
"w": 0.2,
"h": 0.3,
"angle": 45.12345679,
"opacity": 0.5000000004,
"flip_h": true,
"flip_v": false,
"future": {"numeric": 0.1234567896}
}
],
"room_drafts": [
{
"id": "draft",
"points": [
[0.1, 0.9],
[0.2, 0.9],
[0.3, 0.9]
],
"segments": [
{"cm": 10.0000000004},
{"cm": 20.0000000004}
]
}
],
"partitions": [
{
"id": "partition",
"a": [0, 0],
"b": [1, 0],
"cm": 12.0000000004
}
],
"wall_columns": [
{
"id": "column",
"shape": "square",
"center": [0.75, 0.75],
"cm": 30.0000000004,
"angle": 45.12345679
}
],
"open_spans": [
{
"a": [0.3, 0.3],
"b": [0.6, 0.3]
}
],
"led_strips": [
{
"id": "led-1",
"points": [[0.3, 0.416666667], [0.12345679, 0]],
"marker": null
}
],
"future": {
"numeric": 0.1234567896
}
}
],
"markers": [
{
"id": "virtual",
"binding": "virtual",
"angle": -30.12345679,
"size": 1.1000000004,
"ripple_size": 1.5000000004,
"glow_radius_cm": 120.0000000004,
"glow_color": {
"c": "#112233",
"bri": 0.5000000004
},
"vacuum": {
"calibration": {
"map": [
-0.0017921234567,
0.0000000004,
1.0000000004,
0.0017921234567,
-0.0000000004,
2.0000000004
]
}
}
}
],
"settings": {
"glow_radius_cm": 150.0000000004,
"fill_colors": {
"on": {
"c": "#abcdef",
"a": 0.5000000004
}
},
"future_numeric": 0.9876543216
},
"future_root": {
"numeric": 0.1234567896
}
},
"layoutInput": {
"virtual": {
"s": "floor",
"x": 0.1234567894,
"y": -0.1234567896,
"k": 1.2000000004,
"future_numeric": 0.9876543216
},
"rl:poly": {
"s": "floor",
"x": -0.0000000004,
"y": 0.7500000006,
"k": 1.5000000004
}
},
"layoutExpected": {
"virtual": {
"s": "floor",
"x": 0.123456789,
"y": -0.12345679,
"k": 1.2000000004,
"future_numeric": 0.9876543216
},
"rl:poly": {
"s": "floor",
"x": 0,
"y": 0.75,
"k": 1.5000000004
}
}
}